diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 9e5a57ad4..a95064659 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -24,7 +24,7 @@ # SOFTWARE. import hashlib -from typing import List, Tuple, TYPE_CHECKING, Optional, Union, Sequence +from typing import List, Tuple, TYPE_CHECKING, Optional, Union, Sequence, Any import enum from enum import IntEnum, Enum @@ -686,6 +686,14 @@ def is_segwit_address(addr: str, *, net=None) -> bool: return False return witprog is not None +def is_taproot_address(addr: str, *, net=None) -> bool: + if net is None: net = constants.net + try: + witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr) + except Exception as e: + return False + return witver == 1 + def is_b58_address(addr: str, *, net=None) -> bool: if net is None: net = constants.net try: @@ -753,3 +761,80 @@ class DummyAddress: class DummyAddressUsedInTxException(Exception): pass + + +def taproot_tweak_pubkey(pubkey32: bytes, h: bytes) -> Tuple[int, bytes]: + assert isinstance(pubkey32, bytes), type(pubkey32) + assert isinstance(h, bytes), type(h) + assert len(pubkey32) == 32, len(pubkey32) + int_from_bytes = lambda x: int.from_bytes(x, byteorder="big", signed=False) + + tweak = int_from_bytes(ecc.bip340_tagged_hash(b"TapTweak", pubkey32 + h)) + if tweak >= ecc.CURVE_ORDER: + raise ValueError + P = ecc.ECPubkey(b"\x02" + pubkey32) + Q = P + (ecc.GENERATOR * tweak) + return 0 if Q.has_even_y() else 1, Q.get_public_key_bytes(compressed=True)[1:] + + +def taproot_tweak_seckey(seckey0: bytes, h: bytes) -> bytes: + assert isinstance(seckey0, bytes), type(seckey0) + assert isinstance(h, bytes), type(h) + assert len(seckey0) == 32, len(seckey0) + int_from_bytes = lambda x: int.from_bytes(x, byteorder="big", signed=False) + + P = ecc.ECPrivkey(seckey0) + seckey = P.secret_scalar if P.has_even_y() else ecc.CURVE_ORDER - P.secret_scalar + pubkey32 = P.get_public_key_bytes(compressed=True)[1:] + tweak = int_from_bytes(ecc.bip340_tagged_hash(b"TapTweak", pubkey32 + h)) + if tweak >= ecc.CURVE_ORDER: + raise ValueError + return int.to_bytes((seckey + tweak) % ecc.CURVE_ORDER, length=32, byteorder="big", signed=False) + + +# a TapTree is either: +# - a (leaf_version, script) tuple (leaf_version is 0xc0 for BIP-0342 scripts) +# - a list of two elements, each with the same structure as TapTree itself +TapTreeLeaf = Tuple[int, bytes] +TapTree = Union[TapTreeLeaf, Sequence['TapTree']] + + +def taproot_tree_helper(script_tree: TapTree): + if isinstance(script_tree, tuple): + leaf_version, script = script_tree + h = ecc.bip340_tagged_hash(b"TapLeaf", bytes([leaf_version]) + witness_push(script)) + return ([((leaf_version, script), bytes())], h) + left, left_h = taproot_tree_helper(script_tree[0]) + right, right_h = taproot_tree_helper(script_tree[1]) + ret = [(l, c + right_h) for l, c in left] + [(l, c + left_h) for l, c in right] + if right_h < left_h: + left_h, right_h = right_h, left_h + return (ret, ecc.bip340_tagged_hash(b"TapBranch", left_h + right_h)) + + +def taproot_output_script(internal_pubkey: bytes, *, script_tree: Optional[TapTree]) -> bytes: + """Given an internal public key and a tree of scripts, compute the output script.""" + assert isinstance(internal_pubkey, bytes), type(internal_pubkey) + assert len(internal_pubkey) == 32, len(internal_pubkey) + if script_tree is None: + merkle_root = bytes() + else: + _, merkle_root = taproot_tree_helper(script_tree) + _, output_pubkey = taproot_tweak_pubkey(internal_pubkey, merkle_root) + return construct_script([1, output_pubkey]) + + +def control_block_for_taproot_script_spend( + *, internal_pubkey: bytes, script_tree: TapTree, script_num: int, +) -> Tuple[bytes, bytes]: + """Constructs the control block necessary for spending a taproot UTXO using a script. + script_num indicates which script to use, which indexes into (flattened) script_tree. + """ + assert isinstance(internal_pubkey, bytes), type(internal_pubkey) + assert len(internal_pubkey) == 32, len(internal_pubkey) + info, merkle_root = taproot_tree_helper(script_tree) + (leaf_version, leaf_script), merkle_path = info[script_num] + output_pubkey_y_parity, _ = taproot_tweak_pubkey(internal_pubkey, merkle_root) + pubkey_data = bytes([output_pubkey_y_parity + leaf_version]) + internal_pubkey + control_block = pubkey_data + merkle_path + return (leaf_script, control_block) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index 94c0f4a85..6abf5bacc 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -25,18 +25,19 @@ from typing import ( Sequence, Mapping, Set, + Union, ) from .bip32 import convert_bip32_strpath_to_intpath, BIP32Node, KeyOriginInfo, BIP32_PRIME from . import bitcoin -from .bitcoin import construct_script, opcodes, construct_witness +from .bitcoin import construct_script, opcodes, construct_witness, taproot_output_script from . import constants from .crypto import hash_160, sha256 from . import ecc from . import segwit_addr -MAX_TAPROOT_NODES = 128 +MAX_TAPROOT_DEPTH = 128 # we guess that signatures will be 72 bytes long # note: DER-encoded ECDSA signatures are 71 or 72 bytes in practice @@ -413,6 +414,9 @@ class Descriptor(object): def is_segwit(self) -> bool: return any([desc.is_segwit() for desc in self.subdescriptors]) + def is_taproot(self) -> bool: + return False + def get_all_pubkeys(self) -> Set[bytes]: """Returns set of pubkeys that appear at any level in this descriptor.""" assert not self.is_range() @@ -752,43 +756,78 @@ class TRDescriptor(Descriptor): def __init__( self, internal_key: 'PubkeyProvider', - subdescriptors: List['Descriptor'] = None, - depths: List[int] = None, + desc_tree: List[Union['Descriptor', List]] = None, ) -> None: r""" :param internal_key: The :class:`PubkeyProvider` that is the internal key for this descriptor - :param subdescriptors: The :class:`Descriptor`\ s that are the leaf scripts for this descriptor - :param depths: The depths of the leaf scripts in the same order as `subdescriptors` + :param desc_tree: Taproot script binary tree, as a nested list of Descriptors """ - if subdescriptors is None: - subdescriptors = [] - if depths is None: - depths = [] - super().__init__([internal_key], subdescriptors, "tr") - self.depths = depths + if desc_tree is None: + desc_tree = [] + self.desc_tree = desc_tree + desc_list = [] + if desc_tree: + if self.get_max_tree_depth() > MAX_TAPROOT_DEPTH: + raise ValueError(f"tr() supports at most {MAX_TAPROOT_DEPTH} nesting levels") + def flatten(tree_node): + if isinstance(tree_node, Descriptor): + return [tree_node] + assert len(tree_node) == 2, len(tree_node) + return flatten(tree_node[0]) + flatten(tree_node[1]) + desc_list = flatten(desc_tree) + super().__init__( + pubkeys=[internal_key], + subdescriptors=desc_list, # FIXME we could do without the flattened list (dupl) + name="tr", + ) def to_string_no_checksum(self) -> str: - r = f"{self.name}({self.pubkeys[0].to_string()}" - path: List[bool] = [] # Track left or right for each depth - for p, depth in enumerate(self.depths): - r += "," - while len(path) <= depth: - if len(path) > 0: - r += "{" - path.append(False) - r += self.subdescriptors[p].to_string_no_checksum() - while len(path) > 0 and path[-1]: - if len(path) > 0: - r += "}" - path.pop() - if len(path) > 0: - path[-1] = True - r += ")" - return r + ret = f"{self.name}({self.pubkeys[0].to_string()}" + if self.desc_tree: + ret += "," + def tree_to_str(tree_node): + if isinstance(tree_node, Descriptor): + return tree_node.to_string_no_checksum() + assert len(tree_node) == 2, len(tree_node) + return "{" + tree_to_str(tree_node[0]) + "," + tree_to_str(tree_node[1]) + "}" + ret += tree_to_str(self.desc_tree) + ret += ")" + return ret def is_segwit(self) -> bool: return True + def is_taproot(self) -> bool: + return True + + # TODO add more test vectors from BIP-0386 + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": + internal_pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos) + script_tree = None + if self.desc_tree: + def transform(tree_node): + if isinstance(tree_node, Descriptor): + leaf_version = 0xc0 + leaf_script = tree_node.expand(pos=pos).scriptcode_for_sighash # FIXME maybe rename scriptcode_for_sighash + return (leaf_version, leaf_script) + assert len(tree_node) == 2, len(tree_node) + return [transform(tree_node[0]), transform(tree_node[1])] + script_tree = transform(self.desc_tree) + output_script = taproot_output_script(internal_pubkey, script_tree=script_tree) + return ExpandedScripts( + output_script=output_script, + ) + + def get_max_tree_depth(self) -> Optional[int]: + if not self.desc_tree: + return None + def depth(tree_node) -> int: + if isinstance(tree_node, Descriptor): + return 0 + assert len(tree_node) == 2, len(tree_node) + return 1 + max(depth(tree_node[0]), depth(tree_node[1])) + return depth(self.desc_tree) + def _get_func_expr(s: str) -> Tuple[str, str]: """ @@ -798,9 +837,12 @@ def _get_func_expr(s: str) -> Tuple[str, str]: :return: The function name as the first element of the tuple, and the expression contained within the function as the second element :raises: ValueError: if a matching pair of parentheses cannot be found """ - start = s.index("(") - end = s.rindex(")") - return s[0:start], s[start + 1:end] + try: + start = s.index("(") + end = s.rindex(")") + return s[0:start], s[start + 1:end] + except ValueError: + raise ValueError("A matching pair of parentheses cannot be found") def _get_const(s: str, const: str) -> str: @@ -836,6 +878,8 @@ def _get_expr(s: str) -> Tuple[str, str]: level -= 1 elif level == 0 and c in [")", "}", ","]: break + else: + return s, "" return s[0:i], s[i:] def parse_pubkey(expr: str, *, ctx: '_ParseDescriptorContext') -> Tuple['PubkeyProvider', str]: @@ -939,39 +983,24 @@ def _parse_descriptor(desc: str, *, ctx: '_ParseDescriptorContext') -> 'Descript if ctx != _ParseDescriptorContext.TOP: raise ValueError("Can only have tr at top level") internal_key, expr = parse_pubkey(expr, ctx=ctx) - subscripts = [] - depths = [] + desc_tree = [] if expr: - # Path from top of the tree to what we're currently processing. - # branches[i] == False: left branch in the i'th step from the top - # branches[i] == true: right branch - branches = [] - while True: - # Process open braces - while True: - try: - expr = _get_const(expr, "{") - branches.append(False) - except ValueError: - break - if len(branches) > MAX_TAPROOT_NODES: - raise ValueError(f"tr() supports at most {MAX_TAPROOT_NODES} nesting levels") # TODO xxxx fixed upstream bug here - # Process script expression - sarg, expr = _get_expr(expr) - subscripts.append(_parse_descriptor(sarg, ctx=_ParseDescriptorContext.P2TR)) - depths.append(len(branches)) - # Process closing braces - while len(branches) > 0 and branches[-1]: - expr = _get_const(expr, "}") - branches.pop() - # If we're at the end of a left branch, expect a comma - if len(branches) > 0 and not branches[-1]: - expr = _get_const(expr, ",") - branches[-1] = True - - if len(branches) == 0: - break - return TRDescriptor(internal_key, subscripts, depths) + def parse_tree(tree_str): + if len(tree_str) == 0: + raise ValueError("Invalid Taproot tree expression") + if tree_str[0] != "{": # leaf + sarg, remaining = _get_expr(tree_str) + return _parse_descriptor(sarg, ctx=_ParseDescriptorContext.P2TR), remaining + if len(tree_str) < len("{x,y}") or tree_str[-1] != "}": + raise ValueError("Invalid Taproot tree expression") + left, remaining = parse_tree(tree_str[1:]) + if remaining[0] != ",": raise ValueError + right, remaining = parse_tree(remaining[1:]) + if remaining[0] != "}": raise ValueError + return [left, right], remaining[1:] + desc_tree, _remaining = parse_tree(expr) + if len(_remaining) != 0: raise ValueError + return TRDescriptor(internal_key, desc_tree) if ctx == _ParseDescriptorContext.P2SH: raise ValueError("A function is needed within P2SH") elif ctx == _ParseDescriptorContext.P2WSH: diff --git a/electrum/ecc.py b/electrum/ecc.py index 4c86cfd8d..9fabb69b5 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -414,6 +414,9 @@ class ECPubkey(object): except Exception: return False + def has_even_y(self) -> bool: + return self.y() % 2 == 0 + GENERATOR = ECPubkey(bytes.fromhex('0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' '483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8')) diff --git a/electrum/keystore.py b/electrum/keystore.py index 4c5c5b804..3fe7483ad 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -118,7 +118,7 @@ class KeyStore(Logger, ABC): return {} keypairs = {} for pubkey in txin.pubkeys: - if pubkey in txin.part_sigs: + if pubkey in txin.sigs_ecdsa: # this pubkey already signed continue derivation = self.get_pubkey_derivation(pubkey, txin) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 6209363cd..fbf34a2b8 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1089,7 +1089,7 @@ def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> st def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): tx.sign({local_config.multisig_key.pubkey: local_config.multisig_key.privkey}) - sig = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey] + sig = tx.inputs()[0].sigs_ecdsa[local_config.multisig_key.pubkey] sig_64 = ecdsa_sig64_from_der_sig(sig[:-1]) return sig_64 diff --git a/electrum/transaction.py b/electrum/transaction.py index 5ae075f6b..8573aec96 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -48,8 +48,9 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, opcodes, base_decode, - base_encode, construct_witness, construct_script) -from .crypto import sha256d + base_encode, construct_witness, construct_script, + taproot_tweak_seckey) +from .crypto import sha256d, sha256 from .logging import get_logger from .util import ShortID, OldTaskGroup from .bitcoin import DummyAddress @@ -107,21 +108,26 @@ class TxinDataFetchProgress(NamedTuple): class Sighash(IntEnum): # note: this is not an IntFlag, as ALL|NONE != SINGLE + DEFAULT = 0 # taproot only (bip-0341) ALL = 1 NONE = 2 SINGLE = 3 ANYONECANPAY = 0x80 @classmethod - def is_valid(cls, sighash: int) -> bool: - for flag in Sighash: - for base_flag in [Sighash.ALL, Sighash.NONE, Sighash.SINGLE]: - if (flag & ~0x1f | base_flag) == sighash: - return True - return False + def is_valid(cls, sighash: int, *, is_taproot: bool = False) -> bool: + valid_flags = { + 0x01, 0x02, 0x03, + 0x81, 0x82, 0x83, + } + if is_taproot: + valid_flags.add(0x00) + return sighash in valid_flags @classmethod def to_sigbytes(cls, sighash: int) -> bytes: + if sighash == Sighash.DEFAULT: + return b"" return sighash.to_bytes(length=1, byteorder="big") @@ -210,11 +216,74 @@ class TxOutput: return d -class BIP143SharedTxDigestFields(NamedTuple): +class BIP143SharedTxDigestFields(NamedTuple): # witness v0 hashPrevouts: bytes hashSequence: bytes hashOutputs: bytes + @classmethod + def from_tx(cls, tx: 'PartialTransaction') -> 'BIP143SharedTxDigestFields': + inputs = tx.inputs() + outputs = tx.outputs() + hashPrevouts = sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs)) + hashSequence = sha256d(b''.join( + int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False) + for txin in inputs)) + hashOutputs = sha256d(b''.join(o.serialize_to_network() for o in outputs)) + return BIP143SharedTxDigestFields( + hashPrevouts=hashPrevouts, + hashSequence=hashSequence, + hashOutputs=hashOutputs, + ) + + +class BIP341SharedTxDigestFields(NamedTuple): # witness v1 + sha_prevouts: bytes + sha_amounts: bytes + sha_scriptpubkeys: bytes + sha_sequences: bytes + sha_outputs: bytes + + @classmethod + def from_tx(cls, tx: 'PartialTransaction') -> 'BIP341SharedTxDigestFields': + inputs = tx.inputs() + outputs = tx.outputs() + sha_prevouts = sha256(b''.join(txin.prevout.serialize_to_network() for txin in inputs)) + sha_amounts = sha256(b''.join( + int.to_bytes(txin.value_sats(), length=8, byteorder="little", signed=False) + for txin in inputs)) + sha_scriptpubkeys = sha256(b''.join( + var_int(len(txin.scriptpubkey)) + txin.scriptpubkey + for txin in inputs)) + sha_sequences = sha256(b''.join( + int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False) + for txin in inputs)) + sha_outputs = sha256(b''.join(o.serialize_to_network() for o in outputs)) + return BIP341SharedTxDigestFields( + sha_prevouts=sha_prevouts, + sha_amounts=sha_amounts, + sha_scriptpubkeys=sha_scriptpubkeys, + sha_sequences=sha_sequences, + sha_outputs=sha_outputs, + ) + + +class SighashCache: + + def __init__(self): + self._witver0 = None # type: Optional[BIP143SharedTxDigestFields] + self._witver1 = None # type: Optional[BIP341SharedTxDigestFields] + + def get_witver0_data_for_tx(self, tx: 'PartialTransaction') -> BIP143SharedTxDigestFields: + if self._witver0 is None: + self._witver0 = BIP143SharedTxDigestFields.from_tx(tx) + return self._witver0 + + def get_witver1_data_for_tx(self, tx: 'PartialTransaction') -> BIP341SharedTxDigestFields: + if self._witver1 is None: + self._witver1 = BIP341SharedTxDigestFields.from_tx(tx) + return self._witver1 + class TxOutpoint(NamedTuple): txid: bytes # endianness same as hex string displayed; reverse of tx serialization order @@ -849,7 +918,7 @@ class Transaction: if estimate_size: dummy_desc = create_dummy_descriptor_from_address(txin.address) if desc := (txin.script_descriptor or dummy_desc): - sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs) + sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.sigs_ecdsa) if sol.witness is not None: return sol.witness return construct_witness([]) @@ -876,7 +945,7 @@ class Transaction: if redeem_script := desc.expand().redeem_script: return construct_script([redeem_script]) return b"" - sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs) + sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.sigs_ecdsa) if sol.script_sig is not None: return sol.script_sig return b"" @@ -900,18 +969,6 @@ class Transaction: raise Exception(f"don't know scriptcode for descriptor: {desc.to_string()}") raise UnknownTxinType(f'cannot construct preimage_script') - def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: - inputs = self.inputs() - outputs = self.outputs() - hashPrevouts = sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs)) - hashSequence = sha256d(b''.join( - int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False) - for txin in inputs)) - hashOutputs = sha256d(b''.join(o.serialize_to_network() for o in outputs)) - return BIP143SharedTxDigestFields(hashPrevouts=hashPrevouts, - hashSequence=hashSequence, - hashOutputs=hashOutputs) - def is_segwit(self, *, guess_for_address=False): return any(txin.is_segwit(guess_for_address=guess_for_address) for txin in self.inputs()) @@ -1290,6 +1347,8 @@ class PSBTInputType(IntEnum): BIP32_DERIVATION = 6 FINAL_SCRIPTSIG = 7 FINAL_SCRIPTWITNESS = 8 + TAP_KEY_SIG = 0x13 + TAP_MERKLE_ROOT = 0x18 SLIP19_OWNERSHIP_PROOF = 0x19 @@ -1301,6 +1360,7 @@ class PSBTOutputType(IntEnum): # Serialization/deserialization tools def deser_compact_size(f) -> Optional[int]: + # note: ~inverse of bitcoin.var_int try: nit = f.read(1)[0] except IndexError: @@ -1383,11 +1443,13 @@ class PartialTxInput(TxInput, PSBTSection): def __init__(self, *args, **kwargs): TxInput.__init__(self, *args, **kwargs) self._witness_utxo = None # type: Optional[TxOutput] - self.part_sigs = {} # type: Dict[bytes, bytes] # pubkey -> sig + self.sigs_ecdsa = {} # type: Dict[bytes, bytes] # pubkey -> sig + self.tap_key_sig = None # type: Optional[bytes] # sig for taproot key-path-spending self.sighash = None # type: Optional[int] self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) self.redeem_script = None # type: Optional[bytes] self.witness_script = None # type: Optional[bytes] + self.tap_merkle_root = None # type: Optional[bytes] self.slip_19_ownership_proof = None # type: Optional[bytes] self._unknown = {} # type: Dict[bytes, bytes] @@ -1397,6 +1459,7 @@ class PartialTxInput(TxInput, PSBTSection): self._trusted_address = None # type: Optional[str] self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown self._is_native_segwit = None # type: Optional[bool] # None means unknown + self._is_taproot = None # type: Optional[bool] # None means unknown self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est @property @@ -1439,7 +1502,9 @@ class PartialTxInput(TxInput, PSBTSection): 'sighash': self.sighash, 'redeem_script': self.redeem_script.hex() if self.redeem_script else None, 'witness_script': self.witness_script.hex() if self.witness_script else None, - 'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()}, + 'sigs_ecdsa': {pubkey.hex(): sig.hex() for pubkey, sig in self.sigs_ecdsa.items()}, + 'tap_key_sig': self.tap_key_sig.hex() if self.tap_key_sig else None, + 'tap_merkle_root': self.tap_merkle_root.hex() if self.tap_merkle_root else None, 'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) for pubkey, (xfp, path) in self.bip32_paths.items()}, 'slip_19_ownership_proof': self.slip_19_ownership_proof.hex() if self.slip_19_ownership_proof else None, @@ -1519,11 +1584,25 @@ class PartialTxInput(TxInput, PSBTSection): self.witness_utxo = TxOutput.from_network_bytes(val) if key: raise SerializationError(f"key for {repr(kt)} must be empty") elif kt == PSBTInputType.PARTIAL_SIG: - if key in self.part_sigs: + if key in self.sigs_ecdsa: raise SerializationError(f"duplicate key: {repr(kt)}") - if len(key) not in (33, 65): # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32... + if len(key) not in (33, 65): raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}") - self.part_sigs[key] = val + self.sigs_ecdsa[key] = val + elif kt == PSBTInputType.TAP_KEY_SIG: + if self.tap_key_sig is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + if len(val) not in (64, 65): + raise SerializationError(f"value for {repr(kt)} has unexpected length: {len(val)}") + self.tap_key_sig = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.TAP_MERKLE_ROOT: + if self.tap_merkle_root is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + if len(val) != 32: + raise SerializationError(f"value for {repr(kt)} has unexpected length: {len(val)}") + self.tap_merkle_root = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") elif kt == PSBTInputType.SIGHASH_TYPE: if self.sighash is not None: raise SerializationError(f"duplicate key: {repr(kt)}") @@ -1534,7 +1613,7 @@ class PartialTxInput(TxInput, PSBTSection): elif kt == PSBTInputType.BIP32_DERIVATION: if key in self.bip32_paths: raise SerializationError(f"duplicate key: {repr(kt)}") - if len(key) not in (33, 65): # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32... + if len(key) not in (33, 65): raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}") self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val) elif kt == PSBTInputType.REDEEM_SCRIPT: @@ -1573,8 +1652,12 @@ class PartialTxInput(TxInput, PSBTSection): wr(PSBTInputType.WITNESS_UTXO, self.witness_utxo.serialize_to_network()) if self.utxo: wr(PSBTInputType.NON_WITNESS_UTXO, bfh(self.utxo.serialize_to_network(include_sigs=True))) - for pk, val in sorted(self.part_sigs.items()): + for pk, val in sorted(self.sigs_ecdsa.items()): wr(PSBTInputType.PARTIAL_SIG, val, pk) + if self.tap_key_sig is not None: + wr(PSBTInputType.TAP_KEY_SIG, self.tap_key_sig) + if self.tap_merkle_root is not None: + wr(PSBTInputType.TAP_MERKLE_ROOT, self.tap_merkle_root) if self.sighash is not None: wr(PSBTInputType.SIGHASH_TYPE, struct.pack(' Tuple[int, int]: if desc := self.script_descriptor: - return desc.get_satisfaction_progress(sigdata=self.part_sigs) + return desc.get_satisfaction_progress(sigdata=self.sigs_ecdsa) return 0, 0 def finalize(self) -> None: def clear_fields_when_finalized(): # BIP-174: "All other data except the UTXO and unknown fields in the # input key-value map should be cleared from the PSBT" - self.part_sigs = {} + self.sigs_ecdsa = {} + self.tap_key_sig = None + self.tap_merkle_root = None self.sighash = None self.bip32_paths = {} self.redeem_script = None @@ -1673,9 +1758,13 @@ class PartialTxInput(TxInput, PSBTSection): self.witness_utxo = other_txin.witness_utxo if other_txin.utxo: self.utxo = other_txin.utxo - self.part_sigs.update(other_txin.part_sigs) + self.sigs_ecdsa.update(other_txin.sigs_ecdsa) if other_txin.sighash is not None: self.sighash = other_txin.sighash + if other_txin.tap_key_sig is not None: + self.tap_key_sig = other_txin.tap_key_sig + if other_txin.tap_merkle_root is not None: + self.tap_merkle_root = other_txin.tap_merkle_root self.bip32_paths.update(other_txin.bip32_paths) if other_txin.redeem_script is not None: self.redeem_script = other_txin.redeem_script @@ -1692,7 +1781,7 @@ class PartialTxInput(TxInput, PSBTSection): self._utxo = None # type: Optional[Transaction] def is_native_segwit(self) -> Optional[bool]: - """Whether this input is native segwit. None means inconclusive.""" + """Whether this input is native segwit (any witness version). None means inconclusive.""" if self._is_native_segwit is None: if self.address: self._is_native_segwit = bitcoin.is_segwit_address(self.address) @@ -1726,6 +1815,7 @@ class PartialTxInput(TxInput, PSBTSection): return self._is_p2sh_segwit def is_segwit(self, *, guess_for_address=False) -> bool: + """Whether this input is segwit (any witness version).""" if super().is_segwit(): return True if self.is_native_segwit() or self.is_p2sh_segwit(): @@ -1741,9 +1831,18 @@ class PartialTxInput(TxInput, PSBTSection): return dummy_desc.is_segwit() return False # can be false-negative + def is_taproot(self) -> bool: + if self._is_taproot is None: + if self.address: + self._is_taproot = bitcoin.is_taproot_address(self.address) + if desc := self.script_descriptor: + return desc.is_taproot() + return self._is_taproot + def already_has_some_signatures(self) -> bool: """Returns whether progress has been made towards completing this input.""" - return (self.part_sigs + return (self.sigs_ecdsa + or self.tap_key_sig is not None or self.script_sig is not None or self.witness is not None) @@ -1816,7 +1915,7 @@ class PartialTxOutput(TxOutput, PSBTSection): elif kt == PSBTOutputType.BIP32_DERIVATION: if key in self.bip32_paths: raise SerializationError(f"duplicate key: {repr(kt)}") - if len(key) not in (33, 65): # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32... + if len(key) not in (33, 65): raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}") self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val) else: @@ -2086,53 +2185,101 @@ class PartialTransaction(Transaction): self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey)) self.invalidate_ser_cache() - def serialize_preimage(self, txin_index: int, *, - bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> bytes: + def serialize_preimage( + self, + txin_index: int, + *, + sighash_cache: SighashCache = None, + ) -> bytes: nVersion = int.to_bytes(self.version, length=4, byteorder="little", signed=True) nLocktime = int.to_bytes(self.locktime, length=4, byteorder="little", signed=False) inputs = self.inputs() outputs = self.outputs() txin = inputs[txin_index] - sighash = txin.sighash if txin.sighash is not None else Sighash.ALL - if not Sighash.is_valid(sighash): + sighash = txin.sighash + if sighash is None: + sighash = Sighash.DEFAULT if txin.is_taproot() else Sighash.ALL + if not Sighash.is_valid(sighash, is_taproot=txin.is_taproot()): raise Exception(f"SIGHASH_FLAG ({sighash}) not supported!") - nHashType = int.to_bytes(sighash, length=4, byteorder="little", signed=False) - preimage_script = self.get_preimage_script(txin) + if sighash_cache is None: + sighash_cache = SighashCache() if txin.is_segwit(): - if bip143_shared_txdigest_fields is None: - bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() - if not (sighash & Sighash.ANYONECANPAY): - hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts - else: - hashPrevouts = bytes(32) - if not (sighash & Sighash.ANYONECANPAY) and (sighash & 0x1f) != Sighash.SINGLE and (sighash & 0x1f) != Sighash.NONE: - hashSequence = bip143_shared_txdigest_fields.hashSequence - else: - hashSequence = bytes(32) - if (sighash & 0x1f) != Sighash.SINGLE and (sighash & 0x1f) != Sighash.NONE: - hashOutputs = bip143_shared_txdigest_fields.hashOutputs - elif (sighash & 0x1f) == Sighash.SINGLE and txin_index < len(outputs): - hashOutputs = sha256d(outputs[txin_index].serialize_to_network()) - else: - hashOutputs = bytes(32) - outpoint = txin.prevout.serialize_to_network() - scriptCode = var_int(len(preimage_script)) + preimage_script - amount = int.to_bytes(txin.value_sats(), length=8, byteorder="little", signed=False) - nSequence = int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False) - preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType - else: + if txin.is_taproot(): + scache = sighash_cache.get_witver1_data_for_tx(self) + sighash_epoch = b"\x00" + hash_type = int.to_bytes(sighash, length=1, byteorder="little", signed=False) + # txdata + preimage_txdata = bytearray() + preimage_txdata += nVersion + preimage_txdata += nLocktime + if sighash & 0x80 != Sighash.ANYONECANPAY: + preimage_txdata += scache.sha_prevouts + preimage_txdata += scache.sha_amounts + preimage_txdata += scache.sha_scriptpubkeys + preimage_txdata += scache.sha_sequences + if sighash & 3 not in (Sighash.NONE, Sighash.SINGLE): + preimage_txdata += scache.sha_outputs + # inputdata + preimage_inputdata = bytearray() + spend_type = bytes([0]) # (ext_flag * 2) + annex_present + preimage_inputdata += spend_type + if sighash & 0x80 == Sighash.ANYONECANPAY: + preimage_inputdata += txin.prevout.serialize_to_network() + preimage_inputdata += int.to_bytes(txin.value_sats(), length=8, byteorder="little", signed=False) + preimage_inputdata += var_int(len(txin.scriptpubkey)) + txin.scriptpubkey + preimage_inputdata += int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False) + else: + preimage_inputdata += int.to_bytes(txin_index, length=4, byteorder="little", signed=False) + # TODO sha_annex + # outputdata + preimage_outputdata = bytearray() + if sighash & 3 == Sighash.SINGLE: + try: + txout = outputs[txin_index] + except IndexError: + raise Exception("Using SIGHASH_SINGLE without a corresponding output") from None + preimage_outputdata += sha256(txout.serialize_to_network()) + return bytes(sighash_epoch + hash_type + preimage_txdata + preimage_inputdata + preimage_outputdata) + else: # segwit (witness v0) + scache = sighash_cache.get_witver0_data_for_tx(self) + if not (sighash & Sighash.ANYONECANPAY): + hashPrevouts = scache.hashPrevouts + else: + hashPrevouts = bytes(32) + if not (sighash & Sighash.ANYONECANPAY) and (sighash & 0x1f) != Sighash.SINGLE and (sighash & 0x1f) != Sighash.NONE: + hashSequence = scache.hashSequence + else: + hashSequence = bytes(32) + if (sighash & 0x1f) != Sighash.SINGLE and (sighash & 0x1f) != Sighash.NONE: + hashOutputs = scache.hashOutputs + elif (sighash & 0x1f) == Sighash.SINGLE and txin_index < len(outputs): + hashOutputs = sha256d(outputs[txin_index].serialize_to_network()) + else: + hashOutputs = bytes(32) + outpoint = txin.prevout.serialize_to_network() + preimage_script = self.get_preimage_script(txin) + scriptCode = var_int(len(preimage_script)) + preimage_script + amount = int.to_bytes(txin.value_sats(), length=8, byteorder="little", signed=False) + nSequence = int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False) + nHashType = int.to_bytes(sighash, length=4, byteorder="little", signed=False) + preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType + return preimage + else: # legacy sighash (pre-segwit) if sighash != Sighash.ALL: raise Exception(f"SIGHASH_FLAG ({sighash}) not supported! (for legacy sighash)") + preimage_script = self.get_preimage_script(txin) txins = var_int(len(inputs)) + b"".join( txin.serialize_to_network(script_sig=preimage_script if txin_index==k else b"") for k, txin in enumerate(inputs)) txouts = var_int(len(outputs)) + b"".join(o.serialize_to_network() for o in outputs) + nHashType = int.to_bytes(sighash, length=4, byteorder="little", signed=False) preimage = nVersion + txins + txouts + nLocktime + nHashType - return preimage + return preimage + raise Exception("should not reach this") def sign(self, keypairs: Mapping[bytes, bytes]) -> None: # keypairs: pubkey_bytes -> secret_bytes - bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() + sighash_cache = SighashCache() for i, txin in enumerate(self.inputs()): for pubkey in txin.pubkeys: if txin.is_complete(): @@ -2141,7 +2288,7 @@ class PartialTransaction(Transaction): continue _logger.info(f"adding signature for {pubkey}. spending utxo {txin.prevout.to_str()}") sec = keypairs[pubkey] - sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields) + sig = self.sign_txin(i, sec, sighash_cache=sighash_cache) self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey, sig=sig) _logger.debug(f"tx.sign() finished. is_complete={self.is_complete()}") @@ -2152,17 +2299,25 @@ class PartialTransaction(Transaction): txin_index: int, privkey_bytes: bytes, *, - bip143_shared_txdigest_fields=None, + sighash_cache: SighashCache = None, ) -> bytes: txin = self.inputs()[txin_index] txin.validate_data(for_signing=True) - sighash = txin.sighash if txin.sighash is not None else Sighash.ALL - pre_hash = self.serialize_preimage(txin_index, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields) - msg_hash = sha256d(pre_hash) - privkey = ecc.ECPrivkey(privkey_bytes) - sig = privkey.ecdsa_sign(msg_hash, sigencode=ecc.ecdsa_der_sig_from_r_and_s) - sig = sig + Sighash.to_sigbytes(sighash) - return sig + pre_hash = self.serialize_preimage(txin_index, sighash_cache=sighash_cache) + if txin.is_taproot(): + # note: privkey_bytes is the internal key + merkle_root = txin.tap_merkle_root or bytes() + output_privkey_bytes = taproot_tweak_seckey(privkey_bytes, merkle_root) + output_privkey = ecc.ECPrivkey(output_privkey_bytes) + msg_hash = ecc.bip340_tagged_hash(b"TapSighash", pre_hash) + sig = output_privkey.schnorr_sign(msg_hash) + sighash = txin.sighash if txin.sighash is not None else Sighash.DEFAULT + else: + privkey = ecc.ECPrivkey(privkey_bytes) + msg_hash = sha256d(pre_hash) + sig = privkey.ecdsa_sign(msg_hash, sigencode=ecc.ecdsa_der_sig_from_r_and_s) + sighash = txin.sighash if txin.sighash is not None else Sighash.ALL + return sig + Sighash.to_sigbytes(sighash) def is_complete(self) -> bool: return all([txin.is_complete() for txin in self.inputs()]) @@ -2211,7 +2366,7 @@ class PartialTransaction(Transaction): sig = signatures[i] if sig is None: continue - if sig in list(txin.part_sigs.values()): + if sig in list(txin.sigs_ecdsa.values()): continue msg_hash = sha256d(self.serialize_preimage(i)) sig64 = ecc.ecdsa_sig64_from_der_sig(sig[:-1]) @@ -2233,7 +2388,7 @@ class PartialTransaction(Transaction): def add_signature_to_txin(self, *, txin_idx: int, signing_pubkey: bytes, sig: bytes) -> None: txin = self._inputs[txin_idx] - txin.part_sigs[signing_pubkey] = sig + txin.sigs_ecdsa[signing_pubkey] = sig # force re-serialization txin.script_sig = None txin.witness = None @@ -2316,7 +2471,8 @@ class PartialTransaction(Transaction): def remove_signatures(self): for txin in self.inputs(): - txin.part_sigs = {} + txin.sigs_ecdsa = {} + txin.tap_key_sig = None txin.script_sig = None txin.witness = None assert not self.is_complete() diff --git a/tests/bip-0341/wallet-test-vectors.json b/tests/bip-0341/wallet-test-vectors.json new file mode 100644 index 000000000..11261b00b --- /dev/null +++ b/tests/bip-0341/wallet-test-vectors.json @@ -0,0 +1,452 @@ +{ + "version": 1, + "scriptPubKey": [ + { + "given": { + "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + "scriptTree": null + }, + "intermediary": { + "merkleRoot": null, + "tweak": "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + "tweakedPubkey": "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343" + }, + "expected": { + "scriptPubKey": "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "bip350Address": "bc1p2wsldez5mud2yam29q22wgfh9439spgduvct83k3pm50fcxa5dps59h4z5" + } + }, + { + "given": { + "internalPubkey": "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + "scriptTree": { + "id": 0, + "script": "20d85a959b0290bf19bb89ed43c916be835475d013da4b362117393e25a48229b8ac", + "leafVersion": 192 + } + }, + "intermediary": { + "leafHashes": [ + "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21" + ], + "merkleRoot": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "tweak": "cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001", + "tweakedPubkey": "147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3" + }, + "expected": { + "scriptPubKey": "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "bip350Address": "bc1pz37fc4cn9ah8anwm4xqqhvxygjf9rjf2resrw8h8w4tmvcs0863sa2e586", + "scriptPathControlBlocks": [ + "c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27" + ] + } + }, + { + "given": { + "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "scriptTree": { + "id": 0, + "script": "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac", + "leafVersion": 192 + } + }, + "intermediary": { + "leafHashes": [ + "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b" + ], + "merkleRoot": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "tweak": "6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30", + "tweakedPubkey": "e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e" + }, + "expected": { + "scriptPubKey": "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "bip350Address": "bc1punvppl2stp38f7kwv2u2spltjuvuaayuqsthe34hd2dyy5w4g58qqfuag5", + "scriptPathControlBlocks": [ + "c093478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820" + ] + } + }, + { + "given": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "scriptTree": [ + { + "id": 0, + "script": "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac", + "leafVersion": 192 + }, + { + "id": 1, + "script": "06424950333431", + "leafVersion": 250 + } + ] + }, + "intermediary": { + "leafHashes": [ + "8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7", + "f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a" + ], + "merkleRoot": "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef", + "tweak": "9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9", + "tweakedPubkey": "712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5" + }, + "expected": { + "scriptPubKey": "5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5", + "bip350Address": "bc1pwyjywgrd0ffr3tx8laflh6228dj98xkjj8rum0zfpd6h0e930h6saqxrrm", + "scriptPathControlBlocks": [ + "c0ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a", + "faee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf37865928ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + ] + } + }, + { + "given": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "scriptTree": [ + { + "id": 0, + "script": "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac", + "leafVersion": 192 + }, + { + "id": 1, + "script": "07546170726f6f74", + "leafVersion": 192 + } + ] + }, + "intermediary": { + "leafHashes": [ + "64512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89", + "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + ], + "merkleRoot": "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + "tweak": "639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e", + "tweakedPubkey": "77e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220" + }, + "expected": { + "scriptPubKey": "512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220", + "bip350Address": "bc1pwl3s54fzmk0cjnpl3w9af39je7pv5ldg504x5guk2hpecpg2kgsqaqstjq", + "scriptPathControlBlocks": [ + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd82cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb", + "c1f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd864512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89" + ] + } + }, + { + "given": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "scriptTree": [ + { + "id": 0, + "script": "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac", + "leafVersion": 192 + }, + { + "id": 2, + "script": "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c", + "9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf6" + ], + "merkleRoot": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "tweak": "b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4", + "tweakedPubkey": "91b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605" + }, + "expected": { + "scriptPubKey": "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "bip350Address": "bc1pjxmy65eywgafs5tsunw95ruycpqcqnev6ynxp7jaasylcgtcxczs6n332e", + "scriptPathControlBlocks": [ + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "c0e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6fba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + ] + } + }, + { + "given": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "scriptTree": [ + { + "id": 0, + "script": "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac", + "leafVersion": 192 + }, + { + "id": 2, + "script": "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711", + "d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7" + ], + "merkleRoot": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "tweak": "6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9", + "tweakedPubkey": "75169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831" + }, + "expected": { + "scriptPubKey": "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "bip350Address": "bc1pw5tf7sqp4f50zka7629jrr036znzew70zxyvvej3zrpf8jg8hqcssyuewe", + "scriptPathControlBlocks": [ + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d3cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312dd7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "c155adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ] + } + } + ], + "keyPathSpending": [ + { + "given": { + "rawUnsignedTx": "02000000097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a418420000000000fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0065cd1d", + "utxosSpent": [ + { + "scriptPubKey": "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "amountSats": 420000000 + }, + { + "scriptPubKey": "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "amountSats": 462000000 + }, + { + "scriptPubKey": "76a914751e76e8199196d454941c45d1b3a323f1433bd688ac", + "amountSats": 294000000 + }, + { + "scriptPubKey": "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "amountSats": 504000000 + }, + { + "scriptPubKey": "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "amountSats": 630000000 + }, + { + "scriptPubKey": "00147dd65592d0ab2fe0d0257d571abf032cd9db93dc", + "amountSats": 378000000 + }, + { + "scriptPubKey": "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "amountSats": 672000000 + }, + { + "scriptPubKey": "5120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5", + "amountSats": 546000000 + }, + { + "scriptPubKey": "512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220", + "amountSats": 588000000 + } + ] + }, + "intermediary": { + "hashAmounts": "58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde6", + "hashOutputs": "a2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc5", + "hashPrevouts": "e3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f", + "hashScriptPubkeys": "23ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e21", + "hashSequences": "18959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e" + }, + "inputSpending": [ + { + "given": { + "txinIndex": 0, + "internalPrivkey": "6b973d88838f27366ed61c9ad6367663045cb456e28335c109e30717ae0c6baa", + "merkleRoot": null, + "hashType": 3 + }, + "intermediary": { + "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", + "tweak": "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70", + "tweakedPrivkey": "2405b971772ad26915c8dcdf10f238753a9b837e5f8e6a86fd7c0cce5b7296d9", + "sigMsg": "0003020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0000000000d0418f0e9a36245b9a50ec87f8bf5be5bcae434337b87139c3a5b1f56e33cba0", + "precomputedUsed": [ + "hashAmounts", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "2514a6272f85cfa0f45eb907fcb0d121b808ed37c6ea160a5a9046ed5526d555" + }, + "expected": { + "witness": [ + "ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c03" + ] + } + }, + { + "given": { + "txinIndex": 1, + "internalPrivkey": "1e4da49f6aaf4e5cd175fe08a32bb5cb4863d963921255f33d3bc31e1343907f", + "merkleRoot": "5b75adecf53548f3ec6ad7d78383bf84cc57b55a3127c72b9a2481752dd88b21", + "hashType": 131 + }, + "intermediary": { + "internalPubkey": "187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27", + "tweak": "cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001", + "tweakedPrivkey": "ea260c3b10e60f6de018455cd0278f2f5b7e454be1999572789e6a9565d26080", + "sigMsg": "0083020000000065cd1d00d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd9900000000808f891b00000000225120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3ffffffffffcef8fb4ca7efc5433f591ecfc57391811ce1e186a3793024def5c884cba51d", + "precomputedUsed": [], + "sigHash": "325a644af47e8a5a2591cda0ab0723978537318f10e6a63d4eed783b96a71a4d" + }, + "expected": { + "witness": [ + "052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83" + ] + } + }, + { + "given": { + "txinIndex": 3, + "internalPrivkey": "d3c7af07da2d54f7a7735d3d0fc4f0a73164db638b2f2f7c43f711f6d4aa7e64", + "merkleRoot": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b", + "hashType": 1 + }, + "intermediary": { + "internalPubkey": "93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820", + "tweak": "6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30", + "tweakedPrivkey": "97323385e57015b75b0339a549c56a948eb961555973f0951f555ae6039ef00d", + "sigMsg": "0001020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50003000000", + "precomputedUsed": [ + "hashAmounts", + "hashOutputs", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "bf013ea93474aa67815b1b6cc441d23b64fa310911d991e713cd34c7f5d46669" + }, + "expected": { + "witness": [ + "ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a01" + ] + } + }, + { + "given": { + "txinIndex": 4, + "internalPrivkey": "f36bb07a11e469ce941d16b63b11b9b9120a84d9d87cff2c84a8d4affb438f4e", + "merkleRoot": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2", + "hashType": 0 + }, + "intermediary": { + "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", + "tweak": "b57bfa183d28eeb6ad688ddaabb265b4a41fbf68e5fed2c72c74de70d5a786f4", + "tweakedPrivkey": "a8e7aa924f0d58854185a490e6c41f6efb7b675c0f3331b7f14b549400b4d501", + "sigMsg": "0000020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957ea2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc50004000000", + "precomputedUsed": [ + "hashAmounts", + "hashOutputs", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "4f900a0bae3f1446fd48490c2958b5a023228f01661cda3496a11da502a7f7ef" + }, + "expected": { + "witness": [ + "b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f" + ] + } + }, + { + "given": { + "txinIndex": 6, + "internalPrivkey": "415cfe9c15d9cea27d8104d5517c06e9de48e2f986b695e4f5ffebf230e725d8", + "merkleRoot": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def", + "hashType": 2 + }, + "intermediary": { + "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", + "tweak": "6579138e7976dc13b6a92f7bfd5a2fc7684f5ea42419d43368301470f3b74ed9", + "tweakedPrivkey": "241c14f2639d0d7139282aa6abde28dd8a067baa9d633e4e7230287ec2d02901", + "sigMsg": "0002020000000065cd1de3b33bb4ef3a52ad1fffb555c0d82828eb22737036eaeb02a235d82b909c4c3f58a6964a4f5f8f0b642ded0a8a553be7622a719da71d1f5befcefcdee8e0fde623ad0f61ad2bca5ba6a7693f50fce988e17c3780bf2b1e720cfbb38fbdd52e2118959c7221ab5ce9e26c3cd67b22c24f8baa54bac281d8e6b05e400e6c3a957e0006000000", + "precomputedUsed": [ + "hashAmounts", + "hashPrevouts", + "hashScriptPubkeys", + "hashSequences" + ], + "sigHash": "15f25c298eb5cdc7eb1d638dd2d45c97c4c59dcaec6679cfc16ad84f30876b85" + }, + "expected": { + "witness": [ + "a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee002" + ] + } + }, + { + "given": { + "txinIndex": 7, + "internalPrivkey": "c7b0e81f0a9a0b0499e112279d718cca98e79a12e2f137c72ae5b213aad0d103", + "merkleRoot": "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef", + "hashType": 130 + }, + "intermediary": { + "internalPubkey": "ee4fe085983462a184015d1f782d6a5f8b9c2b60130aff050ce221ecf3786592", + "tweak": "9e0517edc8259bb3359255400b23ca9507f2a91cd1e4250ba068b4eafceba4a9", + "tweakedPrivkey": "65b6000cd2bfa6b7cf736767a8955760e62b6649058cbc970b7c0871d786346b", + "sigMsg": "0082020000000065cd1d00e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf00000000804c8b2000000000225120712447206d7a5238acc7ff53fbe94a3b64539ad291c7cdbc490b7577e4b17df5ffffffff", + "precomputedUsed": [], + "sigHash": "cd292de50313804dabe4685e83f923d2969577191a3e1d2882220dca88cbeb10" + }, + "expected": { + "witness": [ + "ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c482" + ] + } + }, + { + "given": { + "txinIndex": 8, + "internalPrivkey": "77863416be0d0665e517e1c375fd6f75839544eca553675ef7fdf4949518ebaa", + "merkleRoot": "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc", + "hashType": 129 + }, + "intermediary": { + "internalPubkey": "f9f400803e683727b14f463836e1e78e1c64417638aa066919291a225f0e8dd8", + "tweak": "639f0281b7ac49e742cd25b7f188657626da1ad169209078e2761cefd91fd65e", + "tweakedPrivkey": "ec18ce6af99f43815db543f47b8af5ff5df3b2cb7315c955aa4a86e8143d2bf5", + "sigMsg": "0081020000000065cd1da2e6dab7c1f0dcd297c8d61647fd17d821541ea69c3cc37dcbad7f90d4eb4bc500a778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af101000000002b0c230000000022512077e30a5522dd9f894c3f8b8bd4c4b2cf82ca7da8a3ea6a239655c39c050ab220ffffffff", + "precomputedUsed": [ + "hashOutputs" + ], + "sigHash": "cccb739eca6c13a8a89e6e5cd317ffe55669bbda23f2fd37b0f18755e008edd2" + }, + "expected": { + "witness": [ + "bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd981" + ] + } + } + ], + "auxiliary": { + "fullySignedTx": "020000000001097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a41842000000006b4830450221008f3b8f8f0537c420654d2283673a761b7ee2ea3c130753103e08ce79201cf32a022079e7ab904a1980ef1c5890b648c8783f4d10103dd62f740d13daa79e298d50c201210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffffaa5202bdf6d8ccd2ee0f0202afbbb7461d9264a25e5bfd3c5a52ee1239e0ba6c0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd050000000000000000000e664b9773b88c09c32cb70a2a3e4da0ced63b7ba3b22f848531bbb1d5d5f4c94010000000000000000e9aa6b8e6c9de67619e6a3924ae25696bb7b694bb677a632a74ef7eadfd4eabf0000000000ffffffffa778eb6a263dc090464cd125c466b5a99667720b1c110468831d058aa1b82af10100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0141ed7c1647cb97379e76892be0cacff57ec4a7102aa24296ca39af7541246d8ff14d38958d4cc1e2e478e4d4a764bbfd835b16d4e314b72937b29833060b87276c030141052aedffc554b41f52b521071793a6b88d6dbca9dba94cf34c83696de0c1ec35ca9c5ed4ab28059bd606a4f3a657eec0bb96661d42921b5f50a95ad33675b54f83000141ff45f742a876139946a149ab4d9185574b98dc919d2eb6754f8abaa59d18b025637a3aa043b91817739554f4ed2026cf8022dbd83e351ce1fabc272841d2510a010140b4010dd48a617db09926f729e79c33ae0b4e94b79f04a1ae93ede6315eb3669de185a17d2b0ac9ee09fd4c64b678a0b61a0a86fa888a273c8511be83bfd6810f0247304402202b795e4de72646d76eab3f0ab27dfa30b810e856ff3a46c9a702df53bb0d8cc302203ccc4d822edab5f35caddb10af1be93583526ccfbade4b4ead350781e2f8adcd012102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90141a3785919a2ce3c4ce26f298c3d51619bc474ae24014bcdd31328cd8cfbab2eff3395fa0a16fe5f486d12f22a9cedded5ae74feb4bbe5351346508c5405bcfee0020141ea0c6ba90763c2d3a296ad82ba45881abb4f426b3f87af162dd24d5109edc1cdd11915095ba47c3a9963dc1e6c432939872bc49212fe34c632cd3ab9fed429c4820141bbc9584a11074e83bc8c6759ec55401f0ae7b03ef290c3139814f545b58a9f8127258000874f44bc46db7646322107d4d86aec8e73b8719a61fff761d75b5dd9810065cd1d" + } + } + ] +} diff --git a/tests/test_bitcoin.py b/tests/test_bitcoin.py index df86cac95..1d48cbac7 100644 --- a/tests/test_bitcoin.py +++ b/tests/test_bitcoin.py @@ -1,7 +1,10 @@ import asyncio import base64 +import json +import os import sys +from electrum import bitcoin from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, is_address, is_private_key, var_int, _op_push, address_to_script, OnchainOutputType, address_to_payload, @@ -9,7 +12,9 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, is_b58_address, address_to_scripthash, is_minikey, is_compressed_privkey, EncodeBase58Check, DecodeBase58Check, script_num_to_bytes, push_script, add_number_to_script, - opcodes, base_encode, base_decode, BitcoinException) + opcodes, base_encode, base_decode, BitcoinException, + taproot_tweak_pubkey, taproot_tweak_seckey, taproot_output_script, + control_block_for_taproot_script_spend) from electrum import bip32 from electrum import segwit_addr from electrum.segwit_addr import DecodedBech32 @@ -1229,3 +1234,47 @@ class TestBaseEncode(ElectrumTestCase): data_base58check) self.assertEqual(data_bytes, DecodeBase58Check(data_base58check)) + + +class TestTaprootHelpers(ElectrumTestCase): + + def test_taproot_tweak_homomorphism(self): + # For any byte string h it holds that + # taproot_tweak_pubkey(pubkey_gen(seckey), h)[1] == pubkey_gen(taproot_tweak_seckey(seckey, h)). + for secret_scalar in (8, 11, 99999): + privkey = ecc.ECPrivkey.from_secret_scalar(secret_scalar) + pubkey32 = privkey.get_public_key_bytes(compressed=True)[1:] + for tree_hash in (b"", b"satoshi", b"1234"*8, bytes(range(100)), ): + tweaked_pubkey = taproot_tweak_pubkey(pubkey32, tree_hash)[1] + tweaked_seckey = taproot_tweak_seckey(privkey.get_secret_bytes(), tree_hash) + self.assertEqual(tweaked_pubkey, ecc.ECPrivkey(tweaked_seckey).get_public_key_bytes(compressed=True)[1:]) + + def test_taproot_output_script(self): + # test vectors from https://github.com/bitcoin/bips/blob/70d9b07ab80ab3c267ece48f74e4e2250226d0cc/bip-0341/wallet-test-vectors.json + test_vector_file = os.path.join(os.path.dirname(__file__), "bip-0341", "wallet-test-vectors.json") + with open(test_vector_file, "r") as f: + vectors = json.load(f) + def transform_tree(tree_node): + if isinstance(tree_node, dict): + return (tree_node["leafVersion"], bfh(tree_node["script"])) + assert len(tree_node) == 2, len(tree_node) + return [transform_tree(tree_node[0]), transform_tree(tree_node[1])] + def flatten_tree(tree_node): + if isinstance(tree_node, tuple): + return [tree_node] + assert len(tree_node) == 2, len(tree_node) + return flatten_tree(tree_node[0]) + flatten_tree(tree_node[1]) + assert len(vectors["scriptPubKey"]) > 0, "test vectors missing" + for tcase in vectors["scriptPubKey"]: + script_tree = transform_tree(tcase["given"]["scriptTree"]) if tcase["given"]["scriptTree"] else None + internal_pubkey = bfh(tcase["given"]["internalPubkey"]) + spk = taproot_output_script(internal_pubkey, script_tree=script_tree) + self.assertEqual(bfh(tcase["expected"]["scriptPubKey"]), spk) + self.assertEqual(tcase["expected"]["bip350Address"], bitcoin.script_to_address(spk)) + if script_tree: + flat_tree = flatten_tree(script_tree) + for script_num, jcontrol_block in enumerate(tcase["expected"]["scriptPathControlBlocks"]): + leaf_script, control_block = control_block_for_taproot_script_spend( + internal_pubkey=internal_pubkey, script_tree=script_tree, script_num=script_num) + self.assertEqual(jcontrol_block, control_block.hex()) + self.assertEqual(flat_tree[script_num][1].hex(), leaf_script.hex()) diff --git a/tests/test_descriptor.py b/tests/test_descriptor.py index 936d36683..a324755fb 100644 --- a/tests/test_descriptor.py +++ b/tests/test_descriptor.py @@ -227,19 +227,36 @@ class TestDescriptor(ElectrumTestCase): self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.get_max_tree_depth(), None) self.assertEqual(desc.to_string_no_checksum(), d) d = "tr([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),{{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)},pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)}})" desc = parse_descriptor(d) self.assertTrue(isinstance(desc, TRDescriptor)) self.assertEqual(len(desc.subdescriptors), 4) + self.assertEqual(len(desc.desc_tree), 2) + self.assertEqual(len(desc.pubkeys), 1) self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") - self.assertEqual(desc.depths, [1, 3, 3, 2]) + self.assertEqual(desc.desc_tree[0].to_string_no_checksum(), "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)") + self.assertEqual(desc.desc_tree[1][0][0].to_string_no_checksum(), "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)") + self.assertEqual(desc.desc_tree[1][0][1].to_string_no_checksum(), "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)") + self.assertEqual(desc.desc_tree[1][1].to_string_no_checksum(), "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)") + self.assertEqual(desc.get_max_tree_depth(), 3) self.assertEqual(desc.to_string_no_checksum(), d) + def test_tr_descriptor_bip386(self): + # test vectors from https://github.com/bitcoin/bips/blob/e2f7481a132e1c5863f5ffcbff009964d7c2af20/bip-0386.mediawiki#test-vectors + # TODO add missing tests + self.assertEqual( + "512077aab6e066f8a7419c5ab714c12c67d25007ed55a43cadcacb4d7a970a093f11", + parse_descriptor("tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)").expand().output_script.hex()) + self.assertEqual( + "512017cf18db381d836d8923b1bdb246cfcd818da1a9f0e6e7907f187f0b2f937754", + parse_descriptor("tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,pk(669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0))").expand().output_script.hex()) + @as_testnet def test_parse_descriptor_with_range(self): d = "wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)" diff --git a/tests/test_psbt.py b/tests/test_psbt.py index 3a9655838..11338d2f6 100644 --- a/tests/test_psbt.py +++ b/tests/test_psbt.py @@ -58,7 +58,7 @@ class TestValidPSBT(ElectrumTestCase): self.assertTrue(tx.inputs()[0].redeem_script is not None) self.assertTrue(tx.inputs()[0].witness_script is not None) self.assertEqual(2, len(tx.inputs()[0].bip32_paths)) - self.assertEqual(1, len(tx.inputs()[0].part_sigs)) + self.assertEqual(1, len(tx.inputs()[0].sigs_ecdsa)) def test_valid_psbt_006(self): # Case: PSBT with one P2WSH input of a 2-of-2 multisig. witnessScript, keypaths, and global xpubs are available. Contains no signatures. Outputs filled. @@ -70,7 +70,7 @@ class TestValidPSBT(ElectrumTestCase): self.assertTrue(tx.inputs()[0].witness_script is not None) self.assertEqual(2, len(tx.inputs()[0].bip32_paths)) self.assertEqual(2, len(tx.xpubs)) - self.assertEqual(0, len(tx.inputs()[0].part_sigs)) + self.assertEqual(0, len(tx.inputs()[0].sigs_ecdsa)) def test_valid_psbt_007(self): # Case: PSBT with unknown types in the inputs. diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 060d06637..aec69bae7 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -1,10 +1,12 @@ +import json +import os from typing import NamedTuple, Union from electrum import transaction, bitcoin from electrum.transaction import (convert_raw_tx_to_hex, tx_from_any, Transaction, PartialTransaction, TxOutpoint, PartialTxInput, PartialTxOutput, Sighash, match_script_against_template, - SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT) + SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT, TxOutput) from electrum.util import bfh from electrum.bitcoin import (deserialize_privkey, opcodes, construct_script, construct_witness) @@ -929,7 +931,7 @@ class TestTransactionTestnet(ElectrumTestCase): self.assertEqual('020000000001019ad573c69e60c209e0ff36f281ae4f700a8d59f846e7ff5c020352fd1e97808600000000000000000001fa840100000000001600145a209b202bc19b3d345a75cf8ab51cb471913a790247304402207b191c1e3ff1a2d3541770b496c9f871406114746b3aa7347ec4ef0423d3a975022043d3a746fa7a794d97e95d74b6d17d618dfc4cd7644476813e08006f271e51bd012a046c4f855fb1752102aec53aa5f347219a7378b13006eb16ce48125f9cf14f04a5509a565ad5e51507ac6c4f855f', tx.serialize()) -class TestSighashTypes(ElectrumTestCase): +class TestSighashBIP143(ElectrumTestCase): #These tests are taken from bip143, https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki #Input of transaction locktime=0 @@ -992,3 +994,36 @@ class TestSighashTypes(ElectrumTestCase): sig = tx.sign_txin(0,privkey) self.assertEqual('30440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783', sig.hex()) + + +class TestSighashBIP341(ElectrumTestCase): + + def test_taproot_keypath_spending(self): + test_vector_file = os.path.join(os.path.dirname(__file__), "bip-0341", "wallet-test-vectors.json") + with open(test_vector_file, "r") as f: + vectors = json.load(f) + assert len(vectors["keyPathSpending"]) > 0, "test vectors missing" + for tcase in vectors["keyPathSpending"]: + unsigned_tx = Transaction(tcase["given"]["rawUnsignedTx"]) + self.assertEqual(len(tcase["given"]["utxosSpent"]), len(unsigned_tx.inputs())) + tx = PartialTransaction.from_tx(unsigned_tx) + # add utxo data + for txin, json_utxo in zip(tx.inputs(), tcase["given"]["utxosSpent"]): + txin.witness_utxo = TxOutput(scriptpubkey=bfh(json_utxo["scriptPubKey"]), value=int(json_utxo["amountSats"])) + for txin_test in tcase["inputSpending"]: + txin_idx = txin_test["given"]["txinIndex"] + txin = tx.inputs()[txin_idx] + txin.sighash = int(txin_test["given"]["hashType"]) + txin.tap_merkle_root = bfh(txin_test["given"]["merkleRoot"]) if txin_test["given"]["merkleRoot"] else None + pre_hash = tx.serialize_preimage(txin_idx) + self.assertEqual(txin_test["intermediary"]["sigMsg"], pre_hash.hex()) + privkey = bfh(txin_test["given"]["internalPrivkey"]) + sig = tx.sign_txin(txin_idx, privkey) + assert len(txin_test["expected"]["witness"]) == 1 + self.assertEqual(txin_test["expected"]["witness"][0], sig.hex()) + txin.witness = construct_witness([sig]) + txin.script_sig = b"" + self.assertTrue(txin.is_complete()) + # note: some input utxos are not taproot, and there is no key data for them + # - txin_idx=2, addr 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH + # - txin_idx=5, addr bc1q0ht9tyks4vh7p5p904t340cr9nvahy7u3re7zg