Browse Source

transaction.py: impl taproot key-spends

Add support for key-path-spending taproot utxos into transaction.py.

- no wallet support yet
- add some psbt, and minimal descriptor support
- preliminary work towards script-path spends
master
SomberNight 2 years ago
parent
commit
3a305881cc
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 87
      electrum/bitcoin.py
  2. 157
      electrum/descriptor.py
  3. 3
      electrum/ecc.py
  4. 2
      electrum/keystore.py
  5. 2
      electrum/lnutil.py
  6. 314
      electrum/transaction.py
  7. 452
      tests/bip-0341/wallet-test-vectors.json
  8. 51
      tests/test_bitcoin.py
  9. 19
      tests/test_descriptor.py
  10. 4
      tests/test_psbt.py
  11. 39
      tests/test_transaction.py

87
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)

157
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:

3
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'))

2
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)

2
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

314
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('<I', self.sighash))
if self.redeem_script is not None:
@ -1632,7 +1715,7 @@ class PartialTxInput(TxInput, PSBTSection):
return True
if desc := self.script_descriptor:
try:
desc.satisfy(allow_dummy=False, sigdata=self.part_sigs)
desc.satisfy(allow_dummy=False, sigdata=self.sigs_ecdsa)
except MissingSolutionPiece:
pass
else:
@ -1641,14 +1724,16 @@ class PartialTxInput(TxInput, PSBTSection):
def get_satisfaction_progress(self) -> 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()

452
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"
}
}
]
}

51
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())

19
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/*)"

4
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.

39
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

Loading…
Cancel
Save