swap to embit for xpub and address derivation (#588)

This commit is contained in:
Stepan Snigirev 2020-11-10 16:11:10 +01:00 committed by GitHub
parent 1825df9628
commit 17c8f82bd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 357 additions and 255 deletions

View file

@ -14,3 +14,4 @@ requests==2.23.0
pysocks==1.7.1
six==1.12.0
stem==1.8.0
embit==0.1.1

View file

@ -94,6 +94,9 @@ ecdsa==0.13.3 \
--hash=sha256:163c80b064a763ea733870feb96f9dd9b92216cfcacd374837af18e4e8ec3d4d \
--hash=sha256:9814e700890991abeceeb2242586024d4758c8fc18445b194a49bd62d85861db \
# via bitbox02, hwi
embit==0.1.1 \
--hash=sha256:15ab4f35036113e35cfcddd786a4123cecb2059ca2417c12ee52eba68bd91615 \
# via -r requirements.in
flask-cors==3.0.8 \
--hash=sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16 \
--hash=sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a \

View file

@ -1,6 +1,6 @@
import os
import shutil
from mnemonic import Mnemonic
from embit import bip39, bip32, networks
from ..device import Device
from ..helpers import alias
from ..util.descriptor import AddChecksum
@ -41,8 +41,11 @@ class BitcoinCore(Device):
testnet,
keys_range=[0, 1000],
):
seed = Mnemonic.to_seed(mnemonic, passphrase)
xprv = seed_to_hd_master_key(seed, testnet=testnet)
seed = bip39.mnemonic_to_seed(mnemonic, passphrase)
root = bip32.HDKey.from_seed(seed)
network = networks.NETWORKS["test" if testnet else "main"]
root.version = network["xprv"]
xprv = root.to_base58()
# Load the wallet if not loaded
self._load_wallet(wallet_manager)
rpc = wallet_manager.rpc.wallet(
@ -54,7 +57,9 @@ class BitcoinCore(Device):
[
{
"desc": AddChecksum(
"sh(wpkh({}{}/0/*))".format(xprv, path.replace("m", ""))
"sh(wpkh({}{}/0/*))".format(
xprv, path.rstrip("/").replace("m", "")
)
),
"range": keys_range,
"timestamp": "now",
@ -64,7 +69,9 @@ class BitcoinCore(Device):
+ [
{
"desc": AddChecksum(
"sh(wpkh({}{}/1/*))".format(xprv, path.replace("m", ""))
"sh(wpkh({}{}/1/*))".format(
xprv, path.rstrip("/").replace("m", "")
)
),
"range": keys_range,
"timestamp": "now",
@ -75,41 +82,16 @@ class BitcoinCore(Device):
)
xpubs_str = ""
paths = ["m"] + paths
xpubs = derive_xpubs_from_xprv(xprv, paths, wallet_manager.rpc)
# it's not parent fingerprint, it's self fingerprint
master_fpr = get_xpub_fingerprint(xpubs[0]).hex()
xpubs = [root.derive(path).to_public().to_base58() for path in paths]
# root fingerprint is fingerprint field of the first child
master_fpr = root.child(0).fingerprint.hex()
slip132_paths = [
"m/49'/0'/0'",
"m/84'/0'/0'",
"m/48'/0'/0'/1'",
"m/48'/0'/0'/2'",
"m/49'/1'/0'",
"m/84'/1'/0'",
"m/48'/1'/0'/1'",
"m/48'/1'/0'/2'",
]
slip132_prefixes = [
b"\x04\x9d\x7c\xb2",
b"\x04\xb2\x47\x46",
b"\x02\x95\xb4\x3f",
b"\x02\xaa\x7e\xd3",
b"\x04\x4a\x52\x62",
b"\x04\x5f\x1c\xf6",
b"\x02\x42\x89\xef",
b"\x02\x57\x54\x83",
]
for i in range(1, len(paths)):
for i in range(len(paths)):
path = paths[i]
xpub = xpubs[i]
slip132_prefix = None
for j, slip132_path in enumerate(slip132_paths):
if path.replace("h", "'").startswith(slip132_path):
slip132_prefix = slip132_prefixes[j]
break
if slip132_prefix:
xpub = convert_xpub_prefix(xpub, slip132_prefix)
# detect slip132 version for xpubs
slip132_prefix = bip32.detect_version(path, default="xpub", network=network)
xpub = convert_xpub_prefix(xpub, slip132_prefix)
xpubs_str += "[{}{}]{}\n".format(master_fpr, path.replace("m", ""), xpub)
keys, failed = Key.parse_xpubs(xpubs_str)
@ -192,127 +174,3 @@ class BitcoinCore(Device):
break
except:
pass # We tried...
# We need to copy it like this because HWI uses it as a dependency,
# but requires v0.18 which doesn't have this function.
def seed_to_hd_master_key(seed, testnet=False) -> str:
"""Converts bip32 seed to xprv"""
if len(seed) < 16 or len(seed) > 64:
raise ValueError("Provided seed should be between 16 and 64 bytes")
# Compute HMAC-SHA512 of seed
seed = hmac.new(b"Bitcoin seed", seed, digestmod="sha512").digest()
# Serialization format can be found at:
# https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Serialization_format
xprv = b"\x04\x88\xad\xe4" # Version for private mainnet
if testnet:
xprv = b"\x04\x35\x83\x94" # Version for private testnet
xprv += b"\x00" * 9 # Depth, parent fingerprint, and child number
xprv += seed[32:] # Chain code
xprv += b"\x00" + seed[:32] # Master key
return encode_base58_checksum(xprv)
def derive_xpubs_from_xprv(xprv, paths: list, rpc):
"""
Derives xpubs from root xprv and list of paths.
Requires running BitcoinRPC instance to derive xpub from xprv
"""
derived_xprvs = []
for path in paths:
derivation = parse_path(path)
if len(derivation) == 0:
# tuple: (parent, derived)
derived_xprvs.append((None, xprv))
else:
# we need parent for fingerprint
parent = xprv
for idx in derivation[:-1]:
parent = get_child(parent, idx, rpc)
child = get_child(parent, derivation[-1], rpc)
derived_xprvs.append((parent, child))
xpubs = []
for parent, child in derived_xprvs:
res = rpc.getdescriptorinfo(f"wpkh({child})")
xpub = res["descriptor"].split("(")[1].split(")")[0]
if parent is not None:
res = rpc.getdescriptorinfo(f"wpkh({parent})")
parent_xpub = res["descriptor"].split("(")[1].split(")")[0]
fingerprint = get_xpub_fingerprint(parent_xpub)
xpub = swap_fingerprint(xpub, fingerprint)
xpubs.append(xpub)
return xpubs
def swap_fingerprint(xpub, fingerprint):
"""Replaces fingerprint in xpub"""
raw = decode_base58(xpub)
swapped = raw[:5] + fingerprint + raw[9:]
return encode_base58_checksum(swapped)
# curve order
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
def get_child(xprv, index, rpc=None):
"""Derives a child from xprv but without fingerprint information"""
if index > 0xFFFFFFFF or index < 0:
raise ValueError("Index should be between 0 and 2^32")
stream = BytesIO(decode_base58(xprv))
version = stream.read(4)
depth = stream.read(1)[0]
fingerprint = stream.read(4)
child_number = int.from_bytes(stream.read(4), "big")
chain_code = stream.read(32)
stream.read(1)
secret = stream.read(32)
key = b"\x00" + secret
if index < 0x80000000:
if rpc is None:
raise ValueError("Can't do non-hardened without rpc")
# non hardened - we need pubkey
else:
# convert to public
res = rpc.getdescriptorinfo(f"wpkh({xprv})")
xpub = res["descriptor"].split("(")[1].split(")")[0]
# decode pubkey
key = decode_base58(xpub)[-33:]
data = key + index.to_bytes(4, "big")
raw = hmac.new(chain_code, data, digestmod="sha512").digest()
tweak = raw[:32]
chain_code = raw[32:]
new_secret = (int.from_bytes(secret, "big") + int.from_bytes(tweak, "big")) % N
res = version + bytes([depth + 1]) + fingerprint + index.to_bytes(4, "big")
res += chain_code + b"\x00" + new_secret.to_bytes(32, "big")
return encode_base58_checksum(res)
def parse_path(path: str) -> list:
"""
Converts derivation path of the form
m/44h/1'/0'/0/32 to int array
"""
arr = path.split("/")
if arr[0] == "m":
arr = arr[1:]
if len(arr) == 0:
return []
if arr[-1] == "":
# trailing slash
arr = arr[:-1]
for i, e in enumerate(arr):
if e[-1] == "h" or e[-1] == "'":
arr[i] = int(e[:-1]) + 0x80000000
else:
arr[i] = int(e)
return arr

View file

@ -13,7 +13,6 @@ from collections import OrderedDict
from mnemonic import Mnemonic
from hwilib.serializations import PSBT, CTransaction
from .persistence import read_json_file, write_json_file
from .util.descriptor import AddChecksum
from .util.bcur import bcur_decode
import threading
from io import BytesIO
@ -171,49 +170,6 @@ def get_devices_with_keys_by_type(app, cosigners, wallet_type):
return devices
def sort_descriptor(rpc, descriptor, index=None, change=False):
descriptor = descriptor.replace("sortedmulti", "multi")
if index is not None:
descriptor = descriptor.replace("*", f"{index}")
# remove checksum
descriptor = descriptor.split("#")[0]
# get address (should be already imported to the wallet)
address = rpc.deriveaddresses(AddChecksum(descriptor), change=change)[0]
# get pubkeys involved
address_info = rpc.getaddressinfo(address)
if "pubkeys" in address_info:
pubkeys = address_info["pubkeys"]
elif "embedded" in address_info and "pubkeys" in address_info["embedded"]:
pubkeys = address_info["embedded"]["pubkeys"]
else:
raise Exception(
"Could not find 'pubkeys' in address info:\n%s"
% json.dumps(address_info, indent=2)
)
# get xpubs from the descriptor
arr = descriptor.split("(multi(")[1].split(")")[0].split(",")
# getting [wsh] or [sh, wsh]
prefix = descriptor.split("(multi(")[0].split("(")
sigs_required = arr[0]
keys = arr[1:]
# sort them according to sortedmulti
z = sorted(zip(pubkeys, keys), key=lambda x: x[0])
keys = [zz[1] for zz in z]
inner = f"{sigs_required}," + ",".join(keys)
desc = f"multi({inner})"
# Write from the inside out
prefix.reverse()
for p in prefix:
desc = f"{p}({desc})"
return AddChecksum(desc)
def clean_psbt(b64psbt):
psbt = PSBT()
psbt.deserialize(b64psbt)

View file

@ -1,4 +1,5 @@
import re
from embit import bip32, ec, networks, script
# From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
@ -53,6 +54,21 @@ def AddChecksum(desc):
return desc + "#" + DescriptorChecksum(desc)
def derive_pubkey(key, path_suffix=None, idx=None):
# if SEC pubkey - just return it
if key[:2] in ["02", "03", "04"]:
return ec.PublicKey.parse(bytes.fromhex(key))
# otherwise - xpub or xprv
hd = bip32.HDKey.from_base58(key)
if hd.is_private:
hd = hd.to_public()
# if we have path suffix
path = "m" + (path_suffix or "")
if idx is not None:
path = path.replace("*", str(idx))
return hd.derive(path).key
class Descriptor:
def __init__(
self,
@ -68,6 +84,7 @@ class Descriptor:
wsh=None,
multisig_M=None,
multisig_N=None,
sort_keys=True,
):
self.origin_fingerprint = origin_fingerprint
self.origin_path = origin_path
@ -82,10 +99,21 @@ class Descriptor:
self.multisig_M = multisig_M
self.multisig_N = multisig_N
self.m_path = None
self.sort_keys = sort_keys
if origin_path and not isinstance(origin_path, list):
self.m_path_base = "m" + origin_path
self.m_path = "m" + origin_path + (path_suffix or "")
elif isinstance(origin_path, list):
self.m_path_base = []
self.m_path = []
for i in range(0, len(origin_path)):
if origin_path[i]:
self.m_path_base.append("m" + origin_path[i])
self.m_path.append("m" + origin_path[i] + (path_suffix[i] or ""))
else:
self.m_path_base.append(None)
self.m_path.append(None)
@classmethod
def parse(cls, desc, testnet=False):
@ -101,6 +129,7 @@ class Descriptor:
path_suffix = None
multisig_M = None
multisig_N = None
sort_keys = True
# Check the checksum
check_split = desc.split("#")
@ -133,8 +162,13 @@ class Descriptor:
return None
# get the list of keys only
keys = desc.split(",", 1)[1].split(")", 1)[0].split(",")
sort_keys = "sortedmulti" in desc
if "sortedmulti" in desc:
keys.sort(key=lambda x: x if "]" not in x else x.split("]")[1])
# sorting makes sense only if individual pubkeys are provided
base_keys = [x if "]" not in x else x.split("]")[1] for x in keys]
bare_pubkeys = [k for k in base_keys if k[:2] in ["02", "03", "04"]]
if len(bare_pubkeys) == len(keys):
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)
else:
@ -180,6 +214,7 @@ class Descriptor:
sh,
sh_wsh,
wsh,
sort_keys,
)
)
if len(descriptors) == 1:
@ -199,29 +234,170 @@ class Descriptor:
wsh,
multisig_M,
multisig_N,
sort_keys,
)
@property
def is_multisig(self):
return bool(self.multisig_N)
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
so [fgp/path]xpub/suffix changes to [fgp/path/suffix]pubkey
Otherwise xpubs will be sorted according to pubkeys
but remain in the descriptor
"""
if self.is_multisig:
keys = []
for i, key in enumerate(self.base_key):
keys.append(
(
derive_pubkey(key, self.path_suffix[i], idx),
key,
self.origin_fingerprint[i],
self.origin_path[i],
self.path_suffix[i],
)
)
if self.sort_keys:
keys = sorted(keys, key=lambda k: k[0])
origin_fingerprint = [k[2] for k in keys]
if keep_xpubs:
base_key = [k[1] for k in keys]
origin_path = [k[3] for k in keys]
path_suffix = [(k[4] or "") for k in keys]
path_suffix = [(p.replace("*", str(idx)) or None) for p in path_suffix]
else:
base_key = [k[0].sec().hex() for k in keys]
origin_path = [(k[3] or "") + (k[4] or "") for k in keys]
origin_path = [(p.replace("*", str(idx)) or None) for p in origin_path]
path_suffix = [None for k in keys]
else:
origin_fingerprint = self.origin_fingerprint
if keep_xpubs:
base_key = self.base_key
origin_path = self.origin_path
path_suffix = self.path_suffix
path_suffix = (
path_suffix.replace("*", str(idx)) if path_suffix else None
)
else:
base_key = (
derive_pubkey(self.base_key, self.path_suffix, idx).sec().hex()
)
origin_path = (self.origin_path or "") + (self.path_suffix or "")
origin_path = origin_path.replace("*", str(idx)) or None
path_suffix = None
return Descriptor(
origin_fingerprint,
origin_path,
base_key,
path_suffix,
self.testnet,
self.sh_wpkh,
self.wpkh,
self.sh,
self.sh_wsh,
self.wsh,
self.multisig_M,
self.multisig_N,
self.sort_keys,
)
def scriptpubkey(self, idx=None):
if idx is None and "*" in self.serialize():
raise RuntimeError("Index is required")
if self.is_multisig:
keys = []
for i, key in enumerate(self.base_key):
keys.append(derive_pubkey(key, self.path_suffix[i], idx))
if self.sort_keys:
keys = sorted(keys)
sc = script.multisig(int(self.multisig_M), keys)
if self.sh:
return script.p2sh(sc)
elif self.sh_wsh:
return script.p2sh(script.p2wsh(sc))
elif self.wsh:
return script.p2wsh(sc)
else:
key = derive_pubkey(self.base_key, self.path_suffix, idx)
if self.wpkh:
return script.p2wpkh(key)
elif self.sh_wpkh:
return script.p2sh(script.p2wpkh(key))
else:
return script.p2pkh(key)
def address(self, idx=None, network=None):
if network is None:
net = networks.NETWORKS["test" if self.testnet else "main"]
else:
net = networks.NETWORKS[network]
return self.scriptpubkey(idx).address(net)
def serialize(self):
descriptor_open = "pkh("
descriptor_close = ")"
origin = ""
path_suffix = ""
if self.wpkh:
descriptor_open = "wpkh("
elif self.sh_wpkh:
descriptor_open = "sh(wpkh("
descriptor_close = "))"
elif self.sh or self.sh_wsh or self.wsh:
# serialize multisig descriptor is not supported yet.
return None
elif self.sh:
descriptor_open = "sh("
descriptor_close = ")"
elif self.sh_wsh:
descriptor_open = "sh(wsh("
descriptor_close = "))"
elif self.wsh:
descriptor_open = "wsh("
descriptor_close = ")"
if self.origin_fingerprint and self.origin_path:
origin = "[" + self.origin_fingerprint + self.origin_path + "]"
if self.is_multisig:
multi = "sortedmulti" if self.sort_keys else "multi"
base_open = f"{multi}({self.multisig_M},"
base_close = ")"
origins = []
for i in range(self.multisig_N):
path_suffix = ""
origin = ""
if self.origin_fingerprint[i] and self.origin_path[i]:
origin = (
"[" + self.origin_fingerprint[i] + self.origin_path[i] + "]"
)
if self.path_suffix:
path_suffix = self.path_suffix
if self.path_suffix[i]:
path_suffix = self.path_suffix[i]
return AddChecksum(
descriptor_open + origin + self.base_key + path_suffix + descriptor_close
)
origins.append(origin + self.base_key[i] + path_suffix)
base = base_open + ",".join(origins) + base_close
else:
origin = ""
path_suffix = ""
if self.origin_fingerprint and self.origin_path:
origin = "[" + self.origin_fingerprint + self.origin_path + "]"
if self.path_suffix:
path_suffix = self.path_suffix
base = origin + self.base_key + path_suffix
return AddChecksum(descriptor_open + base + descriptor_close)
def sort_descriptor(descriptor, index=None):
"""
Sorts descriptor to maintain compatibility with Core 19
as it doesn't support sortedmulti.
Returns a derived multi() descriptor with sorted xpubs inside.
"""
desc = Descriptor.parse(descriptor)
desc.sort_keys = True
sorted_desc = desc.derive(index, keep_xpubs=True)
sorted_desc.sort_keys = False
return sorted_desc.serialize()

View file

@ -0,0 +1,75 @@
from embit.transaction import Transaction
from embit.networks import NETWORKS
from hashlib import sha256
import math
TYPES_MAP = {
"p2wpkh": "witness_v0_keyhash",
}
def decoderawinput(vin):
result = {
"txid": vin.txid.hex(),
"vout": vin.vout,
"scriptSig": {
# TODO: asm
# "asm": "0014c08fd0c4658b89678b9e0726838c2c2c2f41c3df",
"hex": vin.script_sig.data.hex()
},
"sequence": vin.sequence,
}
if vin.is_segwit:
result["txinwitness"] = [item.hex() for item in vin.witness.items]
return result
def decoderawoutput(vout, chain):
result = {
"value": vout.value * 1e-8,
"scriptPubKey": {
# TODO: asm
# "asm": "0 f81b3e69f5cafc2f1e69ed5625d07876e3558e69",
"hex": vout.script_pubkey.data.hex(),
# TODO: reqSigs, type, address only if you can
# "reqSigs": 1,
# "type": "witness_v0_keyhash",
},
}
try:
result["addresses"] = [vout.script_pubkey.address(NETWORKS[chain])]
except:
pass
return result
def decoderawtransaction(hextx, chain="main"):
raw = bytes.fromhex(hextx)
tx = Transaction.parse(raw)
txhash = sha256(sha256(raw).digest()).digest()[::-1].hex()
txsize = len(raw)
if tx.is_segwit:
# tx size - flag - marker - witness
non_witness_size = (
txsize - 2 - sum([len(inp.witness.serialize()) for inp in tx.vin])
)
witness_size = txsize - non_witness_size
weight = non_witness_size * 4 + witness_size
vsize = math.ceil(weight / 4)
else:
vsize = txsize
weight = txsize * 4
result = {
"txid": tx.txid().hex(),
"hash": txhash,
"version": tx.version,
"size": txsize,
"vsize": vsize,
"weight": weight,
"locktime": tx.locktime,
"vin": [decoderawinput(vin) for vin in tx.vin],
"vout": [
dict(decoderawoutput(vout, chain), n=i) for i, vout in enumerate(tx.vout)
],
}
return result

View file

@ -1,13 +1,13 @@
import copy, hashlib, json, logging, os
import time
from hwilib.descriptor import AddChecksum
from .device import Device
from .key import Key
from .util.merkleblock import is_valid_merkle_proof
from .helpers import der_to_bytes, sort_descriptor, parse_utxo
from .helpers import der_to_bytes, parse_utxo
from .util.base58 import decode_base58
from .util.descriptor import Descriptor
from .util.descriptor import Descriptor, sort_descriptor, AddChecksum
from .util.xpub import get_xpub_fingerprint
from .util.tx import decoderawtransaction
from .persistence import write_json_file
from hwilib.serializations import PSBT, CTransaction
from io import BytesIO
@ -426,8 +426,8 @@ class Wallet:
if tx["confirmations"] == 0 and (
tx["category"] == "send" and tx["bip125-replaceable"] == "yes"
):
raw_tx = self.rpc.decoderawtransaction(
self.rpc.gettransaction(tx["txid"])["hex"]
raw_tx = decoderawtransaction(
self.rpc.gettransaction(tx["txid"])["hex"], self.manager.chain
)
tx["vsize"] = raw_tx["vsize"]
@ -665,16 +665,7 @@ class Wallet:
if pool < index + self.GAP_LIMIT:
self.keypoolrefill(pool, index + self.GAP_LIMIT, change=change)
desc = self.change_descriptor if change else self.recv_descriptor
if self.is_multisig:
try:
# first try with sortedmulti
addr = self.rpc.deriveaddresses(desc, [index, index + 1])[0]
except Exception:
# if sortedmulti is not supported
desc = sort_descriptor(self.rpc, desc, index=index, change=change)
addr = self.rpc.deriveaddresses(desc)[0]
return addr
return self.rpc.deriveaddresses(desc, [index, index + 1])[0]
return Descriptor.parse(desc).address(index, self.manager.chain)
def get_descriptor(self, index=None, change=False, address=None):
"""
@ -685,24 +676,12 @@ class Wallet:
return self.rpc.getaddressinfo(address).get("desc", "")
if index is None:
index = self.change_index if change else self.address_index
desc = self.rpc.getaddressinfo(self.get_address(index, change)).get("desc", "")
result = {"descriptor": desc, "xpubs_descriptor": None}
if self.is_multisig:
if address is not None:
d = self.rpc.getaddressinfo(address)["desc"]
path = d.split("[")[1].split("]")[0].split("/")
change = bool(int(path[-2]))
index = int(path[-1])
if index is None:
index = self.change_index if change else self.address_index
desc = self.change_descriptor if change else self.recv_descriptor
try:
result["xpubs_descriptor"] = sort_descriptor(
self.rpc, desc, index=index, change=change
)
except Exception:
pass
return result
desc = self.change_descriptor if change else self.recv_descriptor
derived_desc = Descriptor.parse(desc).derive(index).serialize()
derived_desc_xpubs = (
Descriptor.parse(desc).derive(index, keep_xpubs=True).serialize()
)
return {"descriptor": derived_desc, "xpubs_descriptor": derived_desc_xpubs}
def get_electrum_watchonly(self):
if len(self.keys) == 1:
@ -757,7 +736,7 @@ class Wallet:
available.update(balance)
for tx in locked_utxo:
tx_data = self.rpc.gettransaction(tx["txid"])
raw_tx = self.rpc.decoderawtransaction(tx_data["hex"])
raw_tx = decoderawtransaction(tx_data["hex"], self.manager.chain)
delta = raw_tx["vout"][tx["vout"]]["value"]
if "confirmations" not in tx_data or tx_data["confirmations"] == 0:
available["untrusted_pending"] -= delta
@ -813,9 +792,7 @@ class Wallet:
arg.pop("range")
batch = []
for i in range(start, end):
sorted_desc = sort_descriptor(
self.rpc, desc, index=i, change=change
)
sorted_desc = sort_descriptor(desc, index=i)
# create fresh object
obj = {}
obj.update(arg)

23
tests/test_decodetx.py Normal file
View file

@ -0,0 +1,23 @@
from cryptoadvance.specter.util.tx import *
def test_decoderawtransaction():
hextx = "02000000000101902666609a245e45e426ead256ad47dca8a2b4dd65d1a634ad35b4a1c0603c0e0000000017160014c08fd0c4658b89678b9e0726838c2c2c2f41c3dffeffffff02b44d5a0300000000160014f81b3e69f5cafc2f1e69ed5625d07876e3558e6900e1f50500000000160014255fe80139657184c1de8e04f71c74c667dd7c3f02473044022038a29d1958d295a9739798c1a5f138404711d824f3bf103221d31bcbc11ff0010220635b4996de17290980c78e3de2547717de119e8312a52eb54fce73c27728c5e00121020356c34dd0931251a6e306704a912dbfeedb0f550a30a89689a1c8434cefa91000000000"
res = decoderawtransaction(hextx, "regtest")
assert res["size"] == 245
assert res["vsize"] == 164
assert res["weight"] == 653
# coinbase tx
hextx = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5f03a2030a1c2f5669614254432f4d696e656420627920736762756c6f686b79632f2cfabe6d6ded74726703908120aa4fa4f10557f50c66aa7419c7285edab024872b7bdd96bf1000000000000000101772a30fd92f0420e9dce2e133ebaa2affffffff0488b49929000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3a6f819a142bdad27916d8e8daf419907fa2cd7b085019d035823f3125002b83500000000000000000266a24b9e11b6d563b4d7c8486af0c15dac4c2764a39be25365cfb8990db6d170d794a562080660000000000000000266a24aa21a9ed256d453640cd18d3fe21bdc7f127e9c075e630b28be937a0c8e402807bc010360120000000000000000000000000000000000000000000000000000000000000000000000000"
res = decoderawtransaction(hextx, "regtest")
assert res["size"] == 362
assert res["vsize"] == 335
assert res["weight"] == 1340
# legacy tx
hextx = "020000000191f381c648c70f2388cce607f5955fe6b9f0b50a49c9bfa618413f931e55cf16000000006a4730440220543b92a31ed7cd00781cdce8cac4ef37fbfdce30a9dfc1f8e00a77f2dd35a2ec02201eb21ec97126f0dad8f0f066e0ae1cf44de8a3027caa99b819511ec57ba632c70121020f9c0041942551b00abcf1ba8d00f6ac93e67ddb378eecd0fb240a9ef3ddc9c0ffffffff0182480a010000000017a9143524696d526f50ab583c829bcca02553af9c64fa8700000000"
res = decoderawtransaction(hextx, "regtest")
assert res["size"] == 189
assert res["vsize"] == 189
assert res["weight"] == 756

33
tests/test_descriptor.py Normal file
View file

@ -0,0 +1,33 @@
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"