bugfix: Util testing and small bugfix about multisig treshold (#698)

* new file

* commited changes before master pull

* copied embit tests and adapted to specter

* max blackify

* ultra max blackify

* ultra ultra max blackify

* Integration of base58 functions from embit + tests to ensure correct refactoring.

* Integration of base58 functions from embit + tests to ensure correct refactoring.

* Consistent rescan labels (#843)

* added indirect dependencies because pip-compile did not detect them (#842)

Co-authored-by: benk10 <ben.kaufman10@gmail.com>

* descriptor

* removed suspicious bitcoin-submodule-link bla

* Added tests/bitcoin to gitignore

* just renaming by adding _util_

* fix of bugs in hwi lib clone + tests

* test of mainnet address from desc now correct

Co-authored-by: Justin Moon <mail@justinmoon.com>
Co-authored-by: Kim Neunert <k9ert@gmx.de>
Co-authored-by: benk10 <ben.kaufman10@gmail.com>
This commit is contained in:
Manolis 2021-01-30 11:36:59 +01:00 committed by GitHub
parent 0a483316d0
commit c1ce0e982a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 503 additions and 52 deletions

3
.gitignore vendored
View file

@ -27,4 +27,5 @@ cypress/videos
cypress/screenshots
node_modules
btcd-conn.json
/tests/bitcoin
tests/bitcoin

View file

@ -1,6 +1,6 @@
from collections import OrderedDict
from binascii import hexlify
from .util.base58 import decode_base58, encode_base58_checksum
from embit import base58
from .util.xpub import get_xpub_fingerprint
@ -101,7 +101,7 @@ class Key:
derivation = ""
# checking xpub prefix and defining key type
xpub_bytes = decode_base58(xpub, num_bytes=82)
xpub_bytes = base58.decode_check(xpub)
prefix = xpub_bytes[:4]
is_valid = False
key_type = ""
@ -115,7 +115,7 @@ class Key:
raise Exception("Invalid xpub prefix: %s", prefix.hex())
xpub_bytes = prefix + xpub_bytes[4:]
xpub = encode_base58_checksum(xpub_bytes)
xpub = base58.encode_check(xpub_bytes)
# defining key type from derivation
if derivation != "" and key_type == "":
@ -133,7 +133,7 @@ class Key:
key_type = "wsh"
# infer fingerprint and derivation if depth == 0 or depth == 1
xpub_bytes = decode_base58(xpub)
xpub_bytes = base58.decode_check(xpub)
depth = xpub_bytes[4]
if depth == 0:
fingerprint = hexlify(get_xpub_fingerprint(xpub)).decode()

View file

@ -27,10 +27,15 @@ def encode_base58(s):
def encode_base58_checksum(s):
""" Adds the checksum and then encodes to base58 """
return encode_base58(s + double_sha256(s)[:4]).decode("ascii")
def decode_base58(s, num_bytes=82, strip_leading_zeros=False):
"""Decodes a base58 encoded string with a checksum at the end, does not support legacy
addresses due to their prefixes (0 bytes), returns WITHOUT
checksum, strip_leading_zeros has to be set to True to avoid
raising a ValueError"""
num = 0
for c in s.encode("ascii"):
num *= 58

View file

@ -1,7 +1,9 @@
import re
from embit import bip32, ec, networks, script
from cryptoadvance.specter.specter_error import SpecterError
# From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
# Based on hwilib by achow101: https://github.com/bitcoin-core/HWI/blob/1.2.1/hwilib/descriptor.py which is from
# https://github.com/bitcoin/bitcoin/blob/0.21/src/script/descriptor.cpp
def PolyMod(c, val):
@ -54,6 +56,7 @@ def AddChecksum(desc):
return desc + "#" + DescriptorChecksum(desc)
# Not in hwilib
def derive_pubkey(key, path_suffix=None, idx=None):
# if SEC pubkey - just return it
if key[:2] in ["02", "03", "04"]:
@ -133,16 +136,29 @@ class Descriptor:
# Check the checksum
check_split = desc.split("#")
# Multiple # in desc
if len(check_split) > 2:
return None
raise SpecterError(
f"Too many separators in the descriptor. Check if there are multiple # in {desc}."
)
if len(check_split) == 2:
if len(check_split[1]) != 8:
return None
# Empty checkusm
if len(check_split[1]) == 0:
raise SpecterError("Checksum is empty.")
# Incorrect length
elif len(check_split[1]) != 8:
raise SpecterError(
f"Checksum {check_split[1]} doesn't have the correct length. Should be 8 characters not {len(check_split[1])}."
)
checksum = DescriptorChecksum(check_split[0])
if not checksum.strip():
return None
# Check of checksum calc
if checksum.strip() == "":
raise SpecterError(f"Checksum calculation went wrong.")
# Wrong checksum
if checksum != check_split[1]:
return None
raise SpecterError(
f"{check_split[1]} is the wrong checkum should be {checksum}."
)
desc = check_split[0]
if desc.startswith("sh(wpkh("):
@ -171,11 +187,19 @@ class Descriptor:
keys.sort(key=lambda x: x if "]" not in x else x.split("]")[1])
multisig_M = desc.split(",")[0].split("(")[-1]
multisig_N = len(keys)
if int(multisig_M) > multisig_N:
raise SpecterError(
f"Multisig threshold cannot be larger than the number of keys. Threshold is {int(multisig_M)} but only {multisig_N} keys specified."
)
else:
keys = [desc.split("(")[-1].split(")", 1)[0]]
descriptors = []
for key in keys:
origin_fingerprint = None
origin_path = None
base_key = None
path_suffix = None
origin_match = re.search(r"\[(.*)\]", key)
if origin_match:
origin = origin_match.group(1)
@ -217,6 +241,7 @@ class Descriptor:
sort_keys,
)
)
if len(descriptors) == 1:
return descriptors[0]
else:
@ -244,7 +269,7 @@ class Descriptor:
def derive(self, idx, keep_xpubs=False):
"""
Derives a descriptor with index idx up to the pubkeys.
If keep_xpubs is False all xpubs will be replaces by pubkeys
If keep_xpubs is False all xpubs will be replaced by pubkeys
so [fgp/path]xpub/suffix changes to [fgp/path/suffix]pubkey
Otherwise xpubs will be sorted according to pubkeys
but remain in the descriptor

View file

@ -1,15 +1,16 @@
import hashlib
from .base58 import decode_base58, encode_base58_checksum
from embit import base58
def hash160(d):
# ripemd160(sha256(d))
return hashlib.new("ripemd160", hashlib.sha256(d).digest()).digest()
def convert_xpub_prefix(xpub, prefix_bytes):
# Update xpub to specified prefix and re-encode
b = decode_base58(xpub)
return encode_base58_checksum(prefix_bytes + b[4:])
b = base58.decode_check(xpub)
return base58.encode_check(prefix_bytes + b[4:])
def get_xpub_fingerprint(xpub):
@ -17,5 +18,5 @@ def get_xpub_fingerprint(xpub):
Retuns fingerprint of the XPUB itself.
IMPORTANT! NOT parent fingerprint, but hash160(pubkey) itself!
"""
b = decode_base58(xpub)
b = base58.decode_check(xpub)
return hash160(b[-33:])[:4]

View file

@ -4,7 +4,7 @@ from .device import Device
from .key import Key
from .util.merkleblock import is_valid_merkle_proof
from .helpers import der_to_bytes
from .util.base58 import decode_base58
from embit import base58
from .util.descriptor import Descriptor, sort_descriptor, AddChecksum
from .util.xpub import get_xpub_fingerprint
from .util.tx import decoderawtransaction
@ -1262,7 +1262,7 @@ class Wallet:
# for multisig add xpub fields
if len(self.keys) > 1:
for k in self.keys:
key = b"\x01" + decode_base58(k.xpub)
key = b"\x01" + base58.decode_check(k.xpub)
if k.fingerprint != "":
fingerprint = bytes.fromhex(k.fingerprint)
else:

View file

@ -17,6 +17,8 @@ from cryptoadvance.specter.device_manager import DeviceManager
from cryptoadvance.specter.specter import Specter
from cryptoadvance.specter.server import create_app, init_app
pytest_plugins = ["ghost_machine"]
def pytest_addoption(parser):
"""Internally called to add options to pytest

67
tests/ghost_machine.py Normal file
View file

@ -0,0 +1,67 @@
import pytest
# Using https://iancoleman.io/bip39/ and https://jlopp.github.io/xpub-converter/
# mnemonic = "ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost ghost machine"
# m/44'/0'/0'
@pytest.fixture
def ghost_machine_xpub_44():
xpub = "xpub6CGap5qbgNCEsvXg2gAjEho17zECMA9PbZa7QkrEWTPnPRaubE6qKots5pNwhyFtuYSPa9gQu4jTTZi8WPaXJhtCHrvHQaFRqayN1saQoWv"
return xpub
# m/49'/0'/0'
@pytest.fixture
def ghost_machine_xpub_49():
xpub = "xpub6BtcNhqbaFaoC3oEfKky3Sm22pF48U2jmAf78cB3wdAkkGyAgmsVrgyt1ooSt3bHWgzsdUQh2pTJ867yTeUAMmFDKNSBp8J7WPmp7Df7zjv"
return xpub
@pytest.fixture
def ghost_machine_ypub():
ypub = "ypub6WisgNWWiw8H3LzMVgYbFXrXCnPW562EgHBKv14wKdYdoNnPwS34Uke231m2sxFCvL7gNx1FVUor1NjYBLtB9zvpBi8cQ37bn7qTVqo3fjR"
return ypub
@pytest.fixture
def ghost_machine_tpub_49():
tpub = "tpubDC5CZBbVc15fpTeqkyUBKgHqYCqkeaUtPjvGz7RJEttndfcN29psPcxTSj5RNJaWYaRQq8kqovLBrZA2tju3ThSAP9fY1eiSvorchnseFZu"
return tpub
@pytest.fixture
def ghost_machine_upub():
upub = "upub5DCn7wm4SgVmzmtdoi8DVVfxhBJkqL1L6mmKHNgVky1Fj5VyBxV6NzKD957sr5fWXkY5y8THtqSVWWpjLnomBYw4iXpxaPbkXg5Gn6s5tQf"
return upub
# m/84'/0'/0'
@pytest.fixture
def ghost_machine_xpub_84():
xpub = "xpub6CjsHfiuBnHMPBkxThQ4DDjTw2Qq3VMEVcPBoMBGejZGkj3WQR15LeJLmymPpSzYHX21C8SdFWHgMw2RUBdAQ2Aj4MMS93a68mxPQeS8oHr"
return xpub
@pytest.fixture
def ghost_machine_zpub():
zpub = "zpub6rQPu14jV9NK5n9C8QyJdPvUGxhivjLEKqRdN8y3QkK2rvfxujLCamccpPgZpGJP6oFch5dkApzn8WFYuaTBzVXvo2kHJsD4gE5gBnCBYj1"
return zpub
@pytest.fixture
def ghost_machine_tpub_84():
tpub = "tpubDC4DsqH5rqHqipMNqUbDFtQT3AkKkUrvLsN6miySvortU3s1LGaNVAb7wX2No2VsuxQV82T8s3HJLv3kdx1CPjsJ3onC1Zo5mWCQzRVaWVX"
return tpub
@pytest.fixture
def ghost_machine_vpub():
vpub = "vpub5Y24kG7ZrCFRkRnHia2sdnt5N7MmsrNry1jMrP8XptMEcZZqkjQA6bc1f52RGiEoJmdy1Vk9Qck9tAL1ohKvuq3oFXe3ADVse6UiTHzuyKx"
return vpub

View file

@ -1,33 +0,0 @@
from cryptoadvance.specter.util.descriptor import *
def test_parse():
descs = [
"wpkh([5d5c5649/84h/1h/0h]tpubDCB5nE2GEEuX9xyFigt33xT1RidfkSsH2VSqDx93D1TrvghcZgoDBTjWnWwKTtA6DfvW7fKDAzPJoSduEbt1QkUW2YGaC2CgYxvmF9RyRZS/0/*)#ypamvruf",
"sh(wpkh([5d5c5649/84h/1h/0h]tpubDCB5nE2GEEuX9xyFigt33xT1RidfkSsH2VSqDx93D1TrvghcZgoDBTjWnWwKTtA6DfvW7fKDAzPJoSduEbt1QkUW2YGaC2CgYxvmF9RyRZS/0/*))",
"wsh(sortedmulti(2,[5d5c5649/48h/1h/0h/2h]tpubDEizCJr6sdiKWC6Be8b5EB7akzS7omSX8CHfAYNYewweRDzjmX2kgDAnig9RcVxqtcxdKuYQSKhkjHecYjyej22b7WThS8r1RBmY3Rfczb9/0/*,[0b9fb36b/48h/1h/0h/2h]tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/0/*))#2mdfgjf6",
"sh(wsh(sortedmulti(2,[5d5c5649/48h/1h/0h/2h]tpubDEizCJr6sdiKWC6Be8b5EB7akzS7omSX8CHfAYNYewweRDzjmX2kgDAnig9RcVxqtcxdKuYQSKhkjHecYjyej22b7WThS8r1RBmY3Rfczb9/0/*,[0b9fb36b/48h/1h/0h/2h]tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/0/*)))",
"sh(wsh(sortedmulti(2,tpubDEizCJr6sdiKWC6Be8b5EB7akzS7omSX8CHfAYNYewweRDzjmX2kgDAnig9RcVxqtcxdKuYQSKhkjHecYjyej22b7WThS8r1RBmY3Rfczb9/0/*,[0b9fb36b/48h/1h/0h/2h]tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/0/*,tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV,tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/10,03d568305d7ce6185f2512472bcad032a672626cf15dc2c6b5f68fdd2f3e5898ef)))",
]
for desc in descs:
d = Descriptor.parse(desc, True)
def test_derive():
desc = "sh(wsh(sortedmulti(2,tpubDEizCJr6sdiKWC6Be8b5EB7akzS7omSX8CHfAYNYewweRDzjmX2kgDAnig9RcVxqtcxdKuYQSKhkjHecYjyej22b7WThS8r1RBmY3Rfczb9/0/*,[0b9fb36b/48h/1h/0h/2h]tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/0/*,tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV,tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/10,03d568305d7ce6185f2512472bcad032a672626cf15dc2c6b5f68fdd2f3e5898ef)))"
d = Descriptor.parse(desc, True)
assert (
d.derive(1).serialize()
== "sh(wsh(sortedmulti(2,[0b9fb36b/48'/1'/0'/2'/10]0249f0282636a8f3fac54a37387686705ddf717ab255cc18d4cc60fff284b8585c,[0b9fb36b/48'/1'/0'/2']02c0ca2aa23a2c83039437973d7eb44d15978900733569583103d03c705aa8383a,[0b9fb36b/48'/1'/0'/2'/0/1]036e5e49573aa861e10c3a01342bc7badcaf8acb88a02aaf4bdae46187260ca262,[0b9fb36b/48'/1'/0'/2']03d568305d7ce6185f2512472bcad032a672626cf15dc2c6b5f68fdd2f3e5898ef,03d7b53b60cbf4c0a9075d65911ce37f759d513f9ac3eca45752256f8aa2d82a9e)))#6n9weurx"
)
assert (
d.derive(1, keep_xpubs=True).serialize()
== "sh(wsh(sortedmulti(2,[0b9fb36b/48'/1'/0'/2']tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/10,[0b9fb36b/48'/1'/0'/2']tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV,[0b9fb36b/48'/1'/0'/2']tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/0/1,[0b9fb36b/48'/1'/0'/2']03d568305d7ce6185f2512472bcad032a672626cf15dc2c6b5f68fdd2f3e5898ef,tpubDEizCJr6sdiKWC6Be8b5EB7akzS7omSX8CHfAYNYewweRDzjmX2kgDAnig9RcVxqtcxdKuYQSKhkjHecYjyej22b7WThS8r1RBmY3Rfczb9/0/1)))#7r3s692f"
)
assert (
sort_descriptor(desc, 11)
== "sh(wsh(multi(2,tpubDEizCJr6sdiKWC6Be8b5EB7akzS7omSX8CHfAYNYewweRDzjmX2kgDAnig9RcVxqtcxdKuYQSKhkjHecYjyej22b7WThS8r1RBmY3Rfczb9/0/11,[0b9fb36b/48'/1'/0'/2']tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/10,[0b9fb36b/48'/1'/0'/2']tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV,[0b9fb36b/48'/1'/0'/2']tpubDEhAkijE6ovaiWkJpnGnLhW3VJSSbbQeAtRWQro8EaWgNarWFv2TumZ1sj4iBPReCufziRnnb9QSYSEE8tgZQbbaXJTdLtGtQgQTGXEJdfV/0/11,[0b9fb36b/48'/1'/0'/2']03d568305d7ce6185f2512472bcad032a672626cf15dc2c6b5f68fdd2f3e5898ef)))#sys0qqe8"
)
assert d.address(10) == "2N1TgzrzxjdgkWSuJLoNUtoLhZBJQSakRRk"
assert d.address(10, "main") == "39uUw84w8BBQJfGkffkcGrMSLq6Ee4A2f7"

30
tests/test_key.py Normal file
View file

@ -0,0 +1,30 @@
from cryptoadvance.specter.key import Key
### Testing for the major attributes of the Key class using its classmethod
### parse_xpub which in turn uses base58 de- and encoding functions from embit
def test_fingerprint(ghost_machine_xpub_44):
key = Key.parse_xpub(ghost_machine_xpub_44)
assert key.fingerprint == "81f802e3"
def test_key_type(ghost_machine_ypub):
key = Key.parse_xpub(ghost_machine_ypub)
assert key.key_type == "sh-wpkh"
def test_purpose(ghost_machine_ypub):
key = Key.parse_xpub(ghost_machine_ypub)
assert key.purpose == "Single (Nested)"
def test_xpub(ghost_machine_ypub, ghost_machine_xpub_49):
key = Key.parse_xpub(ghost_machine_ypub)
assert key.xpub == ghost_machine_xpub_49
def test_derivation(ghost_machine_zpub):
key = Key.parse_xpub(f"[81f802e3/84'/0'/3]{ghost_machine_zpub}")
assert key.derivation == "m/84h/0h/3"

View file

@ -0,0 +1,310 @@
from cryptoadvance.specter.util.descriptor import *
from embit import bip32, ec, networks, script
from cryptoadvance.specter.util.xpub import hash160
from cryptoadvance.specter.util.base58 import *
import pytest
### Tests based on https://github.com/bitcoin-core/HWI/blob/1b1596ac6f4fb1ce47a0d1ca7feb1fc553d08e09/test/test_descriptor.py
def test_parse_descriptor_with_origin():
desc = Descriptor.parse(
"wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)",
True,
)
assert desc is not None
assert desc.wpkh == True
assert desc.sh_wpkh == None
assert desc.origin_fingerprint == "00000001"
assert desc.origin_path == "/84'/1'/0'"
assert (
desc.base_key
== "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
)
assert desc.path_suffix == "/0/0"
assert desc.testnet == True
assert desc.m_path_base == "m/84'/1'/0'"
assert desc.m_path == "m/84'/1'/0'/0/0"
def test_parse_multisig_descriptor_with_origin():
# achow101 uses 48'/0'/0'/2' which isn't testnet, though
desc = Descriptor.parse(
"wsh(multi(2,[00000001/48'/1'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/1'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))",
True,
)
assert desc is not None
assert desc.wsh == True
assert desc.origin_fingerprint == ["00000001", "00000002"]
assert desc.origin_path == ["/48'/1'/0'/2'", "/48'/1'/0'/2'"]
assert desc.base_key == [
"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B",
"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty",
]
assert desc.path_suffix == ["/0/0", "/0/0"]
assert desc.testnet == True
assert desc.m_path_base == ["m/48'/1'/0'/2'", "m/48'/1'/0'/2'"]
assert desc.m_path == ["m/48'/1'/0'/2'/0/0", "m/48'/1'/0'/2'/0/0"]
def test_parse_multisig_descriptor_with_origin_one_lacking():
desc = Descriptor.parse(
"wsh(multi(2,[00000001/48'/1'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))",
True,
)
assert desc is not None
assert desc.wsh == True
assert desc.origin_fingerprint == ["00000001", None]
assert desc.origin_path == ["/48'/1'/0'/2'", None]
assert desc.base_key == [
"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B",
"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty",
]
assert desc.path_suffix == ["/0/0", "/0/0"]
assert desc.testnet == True
assert desc.m_path_base == ["m/48'/1'/0'/2'", None]
assert desc.m_path == ["m/48'/1'/0'/2'/0/0", None]
def test_parse_multisig_descriptor_with_origin_nested():
desc = Descriptor.parse(
"sh(wsh(multi(2,[00000001/48'/1'/0'/1']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/1'/0'/1']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))",
True,
)
assert desc is not None
assert desc.wsh == None
assert desc.sh_wsh == True
assert desc.origin_fingerprint == ["00000001", "00000002"]
assert desc.origin_path == ["/48'/1'/0'/1'", "/48'/1'/0'/1'"]
assert desc.base_key == [
"tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B",
"tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty",
]
assert desc.path_suffix == ["/0/0", "/0/0"]
assert desc.testnet == True
assert desc.m_path_base == ["m/48'/1'/0'/1'", "m/48'/1'/0'/1'"]
assert desc.m_path == ["m/48'/1'/0'/1'/0/0", "m/48'/1'/0'/1'/0/0"]
def test_parse_descriptor_without_origin():
desc = Descriptor.parse(
"wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)",
True,
)
assert desc is not None
assert desc.wpkh == True
assert desc.sh_wpkh == None
assert desc.origin_fingerprint == None
assert desc.origin_path == None
assert (
desc.base_key
== "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
)
assert desc.path_suffix == "/0/0"
assert desc.testnet == True
assert desc.m_path == None
def test_parse_descriptor_with_origin_fingerprint_only():
desc = Descriptor.parse(
"wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)",
True,
)
assert desc is not None
assert desc.wpkh == True
assert desc.sh_wpkh == None
assert desc.origin_fingerprint == "00000001"
assert desc.origin_path == ""
assert (
desc.base_key
== "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
)
assert desc.path_suffix == "/0/0"
assert desc.testnet == True
assert desc.m_path == None
def test_parse_descriptor_with_key_at_end_with_origin():
desc = Descriptor.parse(
"wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)",
True,
)
assert desc is not None
assert desc.wpkh == True
assert desc.sh_wpkh == None
assert desc.origin_fingerprint == "00000001"
assert desc.origin_path == "/84'/1'/0'/0/0"
assert (
desc.base_key
== "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7"
)
assert desc.path_suffix == None
assert desc.testnet == True
assert desc.m_path == "m/84'/1'/0'/0/0"
def test_parse_descriptor_with_key_at_end_without_origin():
desc = Descriptor.parse(
"wpkh(0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)", True
)
assert desc is not None
assert desc.wpkh == True
assert desc.sh_wpkh == None
assert desc.origin_fingerprint == None
assert desc.origin_path == None
assert (
desc.base_key
== "0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7"
)
assert desc.path_suffix == None
assert desc.testnet == True
assert desc.m_path == None
def test_parse_empty_descriptor():
desc = Descriptor.parse("", True)
assert desc is None
def test_parse_descriptor_replace_h():
desc = Descriptor.parse(
"wpkh([00000001/84h/1h/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)",
True,
)
assert desc is not None
assert desc.origin_path == "/84'/1'/0'"
def test_serialize_descriptor_with_origin():
descriptor = "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)#mz20k55p"
desc = Descriptor.parse(descriptor, True)
assert desc.serialize() == descriptor
def test_serialize_descriptor_without_origin():
descriptor = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)#ac0p4yhq"
desc = Descriptor.parse(descriptor, True)
assert desc.serialize() == descriptor
def test_serialize_descriptor_with_key_at_end_with_origin():
descriptor = "wpkh([00000001/84'/1'/0'/0/0]0297dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)#rh7p6vk2"
desc = Descriptor.parse(descriptor, True)
assert desc.serialize() == descriptor
def test_parse_descriptor_multi_error():
with pytest.raises(SpecterError) as excinfo:
Descriptor.parse(
"wsh(multi(3,[00000001/48'/1'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*,[00000002/48'/1'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))"
)
assert (
str(excinfo.value)
== "Multisig threshold cannot be larger than the number of keys. Threshold is 3 but only 2 keys specified."
)
def test_checksums():
# Correct checksum
descriptor_ex_checksum = "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))"
assert DescriptorChecksum(descriptor_ex_checksum) == "tjg09x5t"
# Empty checksum
with pytest.raises(SpecterError) as excinfo:
Descriptor.parse(
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#"
)
assert str(excinfo.value) == "Checksum is empty."
# Checksum too long
with pytest.raises(SpecterError) as excinfo:
Descriptor.parse(
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5tq"
)
assert (
str(excinfo.value)
== "Checksum tjg09x5tq doesn't have the correct length. Should be 8 characters not 9."
)
# Checksum too short
with pytest.raises(SpecterError) as excinfo:
Descriptor.parse(
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5"
)
assert (
str(excinfo.value)
== "Checksum tjg09x5 doesn't have the correct length. Should be 8 characters not 7."
)
# Error in checksum
with pytest.raises(SpecterError) as excinfo:
Descriptor.parse(
"sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjq09x4t"
)
assert str(excinfo.value) == "tjq09x4t is the wrong checkum should be tjg09x5t."
### Tests of additional descriptor functionality in Specter
def test_derive_regtest():
# Using ghost x 11 + machine (1)
# abandon x 11 + about (2)
# and zoo x 11 + wrong (3)
# /48'/1'/0'/1'
# Using iancoleman to get tpubs
desc = Descriptor.parse(
"sh(wsh(sortedmulti(2,tpubDDzWqfZ5TH4819JtJT1MaJGh2FYnbn2KGoqkznXRFdNZAuKLD2CsYtQiV5rEVCUezzz9GaRkeHct5NSxVEG9KWUaRoeEtcafVHr2SVE5DRN/*,tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/*,tpubDFPtPArj4GzBBcuqDySkeQbKx4r6HwRgcPbbAjbjB5cxYRzJT6iFtiqzce4qQ9XFWZ83DZJ43WCJJsotdG75p7pw4SgUHZ2nkG4YxLQ414i/2)))",
True,
)
assert (
desc.derive(1).serialize()
== "sh(wsh(sortedmulti(2,03407a711574ae73aa5824f5a66bf4f9a9f49dd274407eb4c27d996019cf4a6552,0376166abb71efb6c9a497a64c1b24c484c29b8a4219a526737e1f370768b1bbe8,03a1427c178f0b1cd679464c4c90444bcfd775d5edb89cf33828170e8be008d921)))#j54e69dq"
)
# pubkeys cross-checked with iancoleman
assert desc.address(5) == "2NGamwat67EUkABbkY1HLYjQW3oumUcThnV"
# Verified with bitcoin-cli deriveaddresses
assert (
desc.derive(1, keep_xpubs=True).serialize()
== "sh(wsh(sortedmulti(2,tpubDFPtPArj4GzBBcuqDySkeQbKx4r6HwRgcPbbAjbjB5cxYRzJT6iFtiqzce4qQ9XFWZ83DZJ43WCJJsotdG75p7pw4SgUHZ2nkG4YxLQ414i/2,tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/1,tpubDDzWqfZ5TH4819JtJT1MaJGh2FYnbn2KGoqkznXRFdNZAuKLD2CsYtQiV5rEVCUezzz9GaRkeHct5NSxVEG9KWUaRoeEtcafVHr2SVE5DRN/1)))#3mfhjamz"
)
def test_derive_main():
# Using ghost x 11 + machine (1)
# abandon x 11 + about (2)
# and zoo x 11 + wrong (3)
# /48'/0'/0'/1'
# Using iancoleman to get xpubs
desc = Descriptor.parse(
"sh(wsh(sortedmulti(2,xpub6DiXipxEgSYqTw3xX2apub7vzsC5gBzmikxriTRnfKRKQjUSpiGQ9XzyFktkVLTGVGF5emH8up1qtsyw726rvnmzHRU8cHH8gDxeLMXSkYE/*,xpub6DkFAXWQ2dHxnMKoSBogHrw1rgNJKR4umdbnNVNTYeCGcduxWnNUHgGptqEQWPKRmeW4Zn4FHSbLMBKEWYaMDYu47Ytg6DdFnPNt8hwn5mE/*,xpub6FHZCoNb3tg3mxjcXsQx1xLpNmod6woECf2fB4nQbe9NXbvha2ucpDpnGbTFF68KUMUr1hNQ9E5jVEvpT2kUkVmFVDrJawcbgXzDpJc2hkF/2)))",
True,
)
assert (
desc.derive(1).serialize()
== "sh(wsh(sortedmulti(2,029dfee2aaa23e2220476c34eda9a76591c1257f8dfce54e42ff014f922ede0838,03151d5b21c6491915e7a103bff913b4d85246c8209a342bb7104850e4cb394686,03646d8e624fedb63739e7963d0c7ad368a7f7935557b2b28c4c954882b19fe6e1)))#rzmdthwy"
)
# pubkeys cross-checked with iancoleman
assert desc.address(5, "main") == "388fc825v9R6Ev8BKodXQMFumQRe7C8SZ5"
# Verified with bitcoin-cli deriveaddresses
assert (
desc.derive(1, keep_xpubs=True).serialize()
== "sh(wsh(sortedmulti(2,xpub6DkFAXWQ2dHxnMKoSBogHrw1rgNJKR4umdbnNVNTYeCGcduxWnNUHgGptqEQWPKRmeW4Zn4FHSbLMBKEWYaMDYu47Ytg6DdFnPNt8hwn5mE/1,xpub6DiXipxEgSYqTw3xX2apub7vzsC5gBzmikxriTRnfKRKQjUSpiGQ9XzyFktkVLTGVGF5emH8up1qtsyw726rvnmzHRU8cHH8gDxeLMXSkYE/1,xpub6FHZCoNb3tg3mxjcXsQx1xLpNmod6woECf2fB4nQbe9NXbvha2ucpDpnGbTFF68KUMUr1hNQ9E5jVEvpT2kUkVmFVDrJawcbgXzDpJc2hkF/2)))#mgjhd0rk"
)
def test_sort():
descriptor = "sh(wsh(multi(2,tpubDDzWqfZ5TH4819JtJT1MaJGh2FYnbn2KGoqkznXRFdNZAuKLD2CsYtQiV5rEVCUezzz9GaRkeHct5NSxVEG9KWUaRoeEtcafVHr2SVE5DRN/*,tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/*,tpubDFPtPArj4GzBBcuqDySkeQbKx4r6HwRgcPbbAjbjB5cxYRzJT6iFtiqzce4qQ9XFWZ83DZJ43WCJJsotdG75p7pw4SgUHZ2nkG4YxLQ414i/2)))"
assert (
sort_descriptor(descriptor, 1)
== "sh(wsh(multi(2,tpubDFPtPArj4GzBBcuqDySkeQbKx4r6HwRgcPbbAjbjB5cxYRzJT6iFtiqzce4qQ9XFWZ83DZJ43WCJJsotdG75p7pw4SgUHZ2nkG4YxLQ414i/2,tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/1,tpubDDzWqfZ5TH4819JtJT1MaJGh2FYnbn2KGoqkznXRFdNZAuKLD2CsYtQiV5rEVCUezzz9GaRkeHct5NSxVEG9KWUaRoeEtcafVHr2SVE5DRN/1)))#w5qd99tr"
)

43
tests/test_util_xpub.py Normal file
View file

@ -0,0 +1,43 @@
import pytest
from cryptoadvance.specter.util.xpub import (
convert_xpub_prefix,
get_xpub_fingerprint,
)
### Tests for xpub
def test_convert_to_ypub(ghost_machine_xpub_49, ghost_machine_ypub):
new_prefix = b"\x04\x9d\x7c\xb2"
assert convert_xpub_prefix(ghost_machine_xpub_49, new_prefix) == ghost_machine_ypub
def test_convert_to_zpub(ghost_machine_xpub_84, ghost_machine_zpub):
new_prefix = b"\x04\xb2\x47\x46"
assert convert_xpub_prefix(ghost_machine_xpub_84, new_prefix) == ghost_machine_zpub
def test_convert_ypub_back(ghost_machine_ypub, ghost_machine_xpub_49):
new_prefix = b"\x04\x88\xb2\x1e"
assert convert_xpub_prefix(ghost_machine_ypub, new_prefix) == ghost_machine_xpub_49
def test_convert_zpub_back(ghost_machine_zpub, ghost_machine_xpub_84):
new_prefix = b"\x04\x88\xb2\x1e"
assert convert_xpub_prefix(ghost_machine_zpub, new_prefix) == ghost_machine_xpub_84
def test_convert_to_upub(ghost_machine_tpub_49, ghost_machine_upub):
new_prefix = b"\x04\x4a\x52\x62"
assert convert_xpub_prefix(ghost_machine_tpub_49, new_prefix) == ghost_machine_upub
def test_convert_to_vpub(ghost_machine_tpub_84, ghost_machine_vpub):
new_prefix = b"\x04\x5f\x1c\xf6"
assert convert_xpub_prefix(ghost_machine_tpub_84, new_prefix) == ghost_machine_vpub
def test_get_xpub_fingerprint(ghost_machine_xpub_44):
# fingerprint from https://jlopp.github.io/xpub-converter/
assert get_xpub_fingerprint(ghost_machine_xpub_44).hex() == "81f802e3"