diff --git a/HISTORY.md b/HISTORY.md index b8325dfe7..fe439ad09 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -44,6 +44,7 @@ Major changes include: Major changes include: +- add support for PSBT's taproot fields (bip370) - added support for Python 3.11 - fixed the OpenSSL 3.x RIPEMD160 issue in btclib/hashes.py - added CONTRIBUTING and SECURITY diff --git a/TODO.md b/TODO.md index d3df4c3bb..286e8de1b 100644 --- a/TODO.md +++ b/TODO.md @@ -39,3 +39,7 @@ - SSA: ask about why e=e(k), making impossible to select e, k indipendently - SSA: ask about benefit of removing 02/03 from pub_key - SSA: suggest better k + +- compare of dsa.rfc6979_and ssa.det_nonce_ + +- refactor Psbt diff --git a/btclib/psbt/psbt_in.py b/btclib/psbt/psbt_in.py index 00c652260..7d894efa4 100644 --- a/btclib/psbt/psbt_in.py +++ b/btclib/psbt/psbt_in.py @@ -14,12 +14,12 @@ """ from __future__ import annotations +# Standard library imports from dataclasses import dataclass from typing import Any, Mapping from btclib.alias import Octets -from btclib.bip32 import ( - BIP32KeyOrigin, +from btclib.bip32.key_origin import ( HdKeyPaths, assert_valid_hd_key_paths, decode_from_bip32_derivs, @@ -30,18 +30,33 @@ from btclib.ecc import dsa from btclib.exceptions import BTClibValueError from btclib.hashes import hash160, hash256, ripemd160, sha256 +from btclib.psbt.psbt_out import BIP32KeyOrigin from btclib.psbt.psbt_utils import ( + assert_valid_leaf_scripts, assert_valid_redeem_script, + assert_valid_taproot_bip32_derivation, + assert_valid_taproot_internal_key, + assert_valid_taproot_script_keys, + assert_valid_taproot_signatures, assert_valid_unknown, assert_valid_witness_script, decode_dict_bytes_bytes, + decode_leaf_scripts, + decode_taproot_bip32, deserialize_bytes, deserialize_int, deserialize_tx, encode_dict_bytes_bytes, + encode_leaf_scripts, + parse_leaf_script, + parse_taproot_bip32, serialize_bytes, serialize_dict_bytes_bytes, serialize_hd_key_paths, + serialize_leaf_scripts, + serialize_taproot_bip32, + taproot_bip32_from_dict, + taproot_bip32_to_dict, ) from btclib.script import Witness from btclib.script.sig_hash import assert_valid_hash_type @@ -61,6 +76,12 @@ PSBT_IN_SHA256 = b"\x0b" PSBT_IN_HASH160 = b"\x0c" PSBT_IN_HASH256 = b"\x0d" +PSBT_IN_TAP_KEY_SIG = b"\x13" +PSBT_IN_TAP_SCRIPT_SIG = b"\x14" +PSBT_IN_TAP_LEAF_SCRIPT = b"\x15" +PSBT_IN_TAP_BIP32_DERIVATION = b"\x16" +PSBT_IN_TAP_INTERNAL_KEY = b"\x17" +PSBT_IN_TAP_MERKLE_ROOT = b"\x18" # 0xfc is reserved for proprietary # explicit code support for proprietary (and por) is unnecessary @@ -147,6 +168,12 @@ class PsbtIn: sha256_preimages: dict[bytes, bytes] hash160_preimages: dict[bytes, bytes] hash256_preimages: dict[bytes, bytes] + taproot_key_spend_signature: bytes + taproot_script_spend_signatures: dict[bytes, bytes] + taproot_leaf_scripts: dict[bytes, tuple[bytes, int]] + taproot_hd_key_paths: dict[bytes, tuple[list[bytes], BIP32KeyOrigin]] + taproot_internal_key: bytes + taproot_merkle_root: bytes unknown: dict[bytes, bytes] @property @@ -172,6 +199,13 @@ def __init__( sha256_preimages: Mapping[Octets, Octets] | None = None, hash160_preimages: Mapping[Octets, Octets] | None = None, hash256_preimages: Mapping[Octets, Octets] | None = None, + taproot_key_spend_signature: Octets = b"", + taproot_script_spend_signatures: Mapping[Octets, Octets] | None = None, + taproot_leaf_scripts: Mapping[Octets, tuple[Octets, int]] | None = None, + taproot_hd_key_paths: Mapping[Octets, tuple[list[Octets], BIP32KeyOrigin]] + | None = None, + taproot_internal_key: Octets = b"", + taproot_merkle_root: Octets = b"", unknown: Mapping[Octets, Octets] | None = None, check_validity: bool = True, ) -> None: @@ -189,6 +223,16 @@ def __init__( self.sha256_preimages = decode_dict_bytes_bytes(sha256_preimages) self.hash160_preimages = decode_dict_bytes_bytes(hash160_preimages) self.hash256_preimages = decode_dict_bytes_bytes(hash256_preimages) + self.taproot_key_spend_signature = bytes_from_octets( + taproot_key_spend_signature + ) + self.taproot_script_spend_signatures = decode_dict_bytes_bytes( + taproot_script_spend_signatures + ) + self.taproot_leaf_scripts = decode_leaf_scripts(taproot_leaf_scripts) + self.taproot_hd_key_paths = decode_taproot_bip32(taproot_hd_key_paths) + self.taproot_internal_key = bytes_from_octets(taproot_internal_key) + self.taproot_merkle_root = bytes_from_octets(taproot_merkle_root) self.unknown = dict(sorted(decode_dict_bytes_bytes(unknown).items())) if check_validity: @@ -218,6 +262,22 @@ def assert_valid(self) -> None: _assert_valid_hash160_preimages(self.hash160_preimages) _assert_valid_hash256_preimages(self.hash256_preimages) + assert_valid_taproot_internal_key(self.taproot_internal_key) + assert_valid_taproot_signatures( + [self.taproot_key_spend_signature], + "invalid taproot key path signature length", + ) + assert_valid_taproot_script_keys( + list(self.taproot_script_spend_signatures.keys()), + "invalid taproot script path key length", + ) + assert_valid_taproot_signatures( + list(self.taproot_script_spend_signatures.values()), + "invalid taproot script path signature length", + ) + assert_valid_leaf_scripts(self.taproot_leaf_scripts) + assert_valid_taproot_bip32_derivation(self.taproot_hd_key_paths) + assert_valid_unknown(self.unknown) def to_dict(self, check_validity: bool = True) -> dict[str, Any]: @@ -245,6 +305,14 @@ def to_dict(self, check_validity: bool = True) -> dict[str, Any]: "sha256_preimages": encode_dict_bytes_bytes(self.sha256_preimages), "hash160_preimages": encode_dict_bytes_bytes(self.hash160_preimages), "hash256_preimages": encode_dict_bytes_bytes(self.hash256_preimages), + "taproot_key_spend_signature": self.taproot_key_spend_signature.hex(), + "taproot_script_spend_signatures": encode_dict_bytes_bytes( + self.taproot_script_spend_signatures + ), + "taproot_leaf_scripts": encode_leaf_scripts(self.taproot_leaf_scripts), + "taproot_hd_key_paths": taproot_bip32_to_dict(self.taproot_hd_key_paths), + "taproot_internal_key": self.taproot_internal_key.hex(), + "taproot_merkle_root": self.taproot_merkle_root.hex(), "unknown": dict(sorted(encode_dict_bytes_bytes(self.unknown).items())), } @@ -271,6 +339,12 @@ def from_dict( dict_["sha256_preimages"], dict_["hash160_preimages"], dict_["hash256_preimages"], + dict_["taproot_key_spend_signature"], + dict_["taproot_script_spend_signatures"], + dict_["taproot_leaf_scripts"], + taproot_bip32_from_dict(dict_["taproot_hd_key_paths"]), # type: ignore + dict_["taproot_internal_key"], + dict_["taproot_merkle_root"], dict_["unknown"], check_validity, ) @@ -328,10 +402,6 @@ def serialize(self, check_validity: bool = True) -> bytes: psbt_in_bin.append(serialize_dict_bytes_bytes(b"", self.unknown)) if self.ripemd160_preimages: - print( - serialize_dict_bytes_bytes(PSBT_IN_RIPEMD160, self.ripemd160_preimages) - ) - print(self.ripemd160_preimages) psbt_in_bin.append( serialize_dict_bytes_bytes(PSBT_IN_RIPEMD160, self.ripemd160_preimages) ) @@ -351,6 +421,44 @@ def serialize(self, check_validity: bool = True) -> bytes: serialize_dict_bytes_bytes(PSBT_IN_HASH256, self.hash256_preimages) ) + # FIXME: we should put conditions on serializations + + if self.taproot_key_spend_signature: + psbt_in_bin.append( + serialize_bytes(PSBT_IN_TAP_KEY_SIG, self.taproot_key_spend_signature) + ) + + if self.taproot_script_spend_signatures: + psbt_in_bin.append( + serialize_dict_bytes_bytes( + PSBT_IN_TAP_SCRIPT_SIG, self.taproot_script_spend_signatures + ) + ) + + if self.taproot_leaf_scripts: + psbt_in_bin.append( + serialize_leaf_scripts( + PSBT_IN_TAP_LEAF_SCRIPT, self.taproot_leaf_scripts + ) + ) + + if self.taproot_hd_key_paths: + psbt_in_bin.append( + serialize_taproot_bip32( + PSBT_IN_TAP_BIP32_DERIVATION, self.taproot_hd_key_paths + ) + ) + + if self.taproot_internal_key: + psbt_in_bin.append( + serialize_bytes(PSBT_IN_TAP_INTERNAL_KEY, self.taproot_internal_key) + ) + + if self.taproot_merkle_root: + psbt_in_bin.append( + serialize_bytes(PSBT_IN_TAP_MERKLE_ROOT, self.taproot_merkle_root) + ) + return b"".join(psbt_in_bin) @classmethod @@ -375,6 +483,12 @@ def parse( sha256_preimages: dict[Octets, Octets] = {} hash160_preimages: dict[Octets, Octets] = {} hash256_preimages: dict[Octets, Octets] = {} + taproot_key_spend_signature = b"" + taproot_script_spend_signatures: dict[Octets, Octets] = {} + taproot_leaf_scripts: dict[Octets, tuple[Octets, int]] = {} + taproot_hd_key_paths: dict[Octets, tuple[list[Octets], BIP32KeyOrigin]] = {} + taproot_internal_key = b"" + taproot_merkle_root = b"" unknown: dict[Octets, Octets] = {} for k, v in input_map.items(): @@ -404,6 +518,20 @@ def parse( hash256_preimages[k[1:]] = v elif k[:1] == PSBT_IN_FINAL_SCRIPTWITNESS: final_script_witness = _deserialize_final_script_witness(k, v) + elif k[:1] == PSBT_IN_TAP_KEY_SIG: + taproot_key_spend_signature = deserialize_bytes( + k, v, "taproot key spend signature" + ) + elif k[:1] == PSBT_IN_TAP_SCRIPT_SIG: + taproot_script_spend_signatures[k[1:]] = v + elif k[:1] == PSBT_IN_TAP_LEAF_SCRIPT: + taproot_leaf_scripts[k[1:]] = parse_leaf_script(v) + elif k[:1] == PSBT_IN_TAP_BIP32_DERIVATION: + taproot_hd_key_paths[k[1:]] = parse_taproot_bip32(v) # type: ignore + elif k[:1] == PSBT_IN_TAP_INTERNAL_KEY: + taproot_internal_key = deserialize_bytes(k, v, "taproot internal key") + elif k[:1] == PSBT_IN_TAP_MERKLE_ROOT: + taproot_merkle_root = deserialize_bytes(k, v, "taproot merkle root") else: # unknown unknown[k] = v @@ -421,6 +549,12 @@ def parse( sha256_preimages, hash160_preimages, hash256_preimages, + taproot_key_spend_signature, + taproot_script_spend_signatures, + taproot_leaf_scripts, + taproot_hd_key_paths, + taproot_internal_key, + taproot_merkle_root, unknown, check_validity, ) diff --git a/btclib/psbt/psbt_out.py b/btclib/psbt/psbt_out.py index a60ae4098..e191d2c44 100644 --- a/btclib/psbt/psbt_out.py +++ b/btclib/psbt/psbt_out.py @@ -16,7 +16,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Mapping +from typing import Any, Mapping, Sequence from btclib.alias import Octets from btclib.bip32 import ( @@ -29,20 +29,35 @@ ) from btclib.psbt.psbt_utils import ( assert_valid_redeem_script, + assert_valid_taproot_bip32_derivation, + assert_valid_taproot_internal_key, + assert_valid_taproot_tree, assert_valid_unknown, assert_valid_witness_script, decode_dict_bytes_bytes, + decode_taproot_bip32, + decode_taproot_tree, deserialize_bytes, encode_dict_bytes_bytes, + encode_taproot_tree, + parse_taproot_bip32, + parse_taproot_tree, serialize_bytes, serialize_dict_bytes_bytes, serialize_hd_key_paths, + serialize_taproot_bip32, + serialize_taproot_tree, + taproot_bip32_from_dict, + taproot_bip32_to_dict, ) from btclib.utils import bytes_from_octets PSBT_OUT_REDEEM_SCRIPT = b"\x00" PSBT_OUT_WITNESS_SCRIPT = b"\x01" PSBT_OUT_BIP32_DERIVATION = b"\x02" +PSBT_OUT_TAP_INTERNAL_KEY = b"\x05" +PSBT_OUT_TAP_TREE = b"\x06" +PSBT_OUT_TAP_BIP32_DERIVATION = b"\x07" # 0xfc is reserved for proprietary # explicit code support for proprietary (and por) is unnecessary # see https://github.com/bitcoin/bips/pull/1038 @@ -54,6 +69,9 @@ class PsbtOut: redeem_script: bytes witness_script: bytes hd_key_paths: HdKeyPaths + taproot_internal_key: bytes + taproot_tree: list[tuple[int, int, bytes]] + taproot_hd_key_paths: dict[bytes, tuple[list[bytes], BIP32KeyOrigin]] unknown: dict[bytes, bytes] def __init__( @@ -61,12 +79,19 @@ def __init__( redeem_script: Octets = b"", witness_script: Octets = b"", hd_key_paths: Mapping[Octets, BIP32KeyOrigin] | None = None, + taproot_internal_key: Octets = b"", + taproot_tree: Sequence[tuple[int, int, Octets]] | None = None, + taproot_hd_key_paths: Mapping[Octets, tuple[list[bytes], BIP32KeyOrigin]] + | None = None, unknown: Mapping[Octets, Octets] | None = None, check_validity: bool = True, ) -> None: self.redeem_script = bytes_from_octets(redeem_script) self.witness_script = bytes_from_octets(witness_script) self.hd_key_paths = decode_hd_key_paths(hd_key_paths) + self.taproot_internal_key = bytes_from_octets(taproot_internal_key) + self.taproot_tree = decode_taproot_tree(taproot_tree) + self.taproot_hd_key_paths = decode_taproot_bip32(taproot_hd_key_paths) self.unknown = dict(sorted(decode_dict_bytes_bytes(unknown).items())) if check_validity: @@ -77,6 +102,9 @@ def assert_valid(self) -> None: assert_valid_redeem_script(self.redeem_script) assert_valid_witness_script(self.witness_script) assert_valid_hd_key_paths(self.hd_key_paths) + assert_valid_taproot_internal_key(self.taproot_internal_key) + assert_valid_taproot_tree(self.taproot_tree) + assert_valid_taproot_bip32_derivation(self.taproot_hd_key_paths) assert_valid_unknown(self.unknown) def to_dict(self, check_validity: bool = True) -> dict[str, Any]: @@ -89,6 +117,9 @@ def to_dict(self, check_validity: bool = True) -> dict[str, Any]: # TODO make it { "asm": "", "hex": "" } "witness_script": self.witness_script.hex(), "bip32_derivs": encode_to_bip32_derivs(self.hd_key_paths), + "taproot_internal_key": self.taproot_internal_key.hex(), + "taproot_tree": encode_taproot_tree(self.taproot_tree), + "taproot_hd_key_paths": taproot_bip32_to_dict(self.taproot_hd_key_paths), "unknown": dict(sorted(encode_dict_bytes_bytes(self.unknown).items())), } @@ -101,6 +132,10 @@ def from_dict( dict_["witness_script"], # FIXME decode_from_bip32_derivs(dict_["bip32_derivs"]), # type: ignore + dict_["taproot_internal_key"], + dict_["taproot_tree"], + # FIXME + taproot_bip32_from_dict(dict_["taproot_hd_key_paths"]), # type: ignore dict_["unknown"], check_validity, ) @@ -126,6 +161,23 @@ def serialize(self, check_validity: bool = True) -> bytes: serialize_hd_key_paths(PSBT_OUT_BIP32_DERIVATION, self.hd_key_paths) ) + if self.taproot_internal_key: + psbt_out_bin.append( + serialize_bytes(PSBT_OUT_TAP_INTERNAL_KEY, self.taproot_internal_key) + ) + + if self.taproot_tree: + psbt_out_bin.append( + serialize_taproot_tree(PSBT_OUT_TAP_TREE, self.taproot_tree) + ) + + if self.taproot_hd_key_paths: + psbt_out_bin.append( + serialize_taproot_bip32( + PSBT_OUT_TAP_BIP32_DERIVATION, self.taproot_hd_key_paths + ) + ) + if self.unknown: psbt_out_bin.append(serialize_dict_bytes_bytes(b"", self.unknown)) @@ -142,6 +194,9 @@ def parse( redeem_script = b"" witness_script = b"" hd_key_paths: dict[Octets, BIP32KeyOrigin] = {} + taproot_internal_key = b"" + taproot_tree: list[tuple[int, int, bytes]] = [] + taproot_hd_key_paths: dict[Octets, tuple[list[Octets], BIP32KeyOrigin]] = {} unknown: dict[Octets, Octets] = {} for k, v in output_map.items(): @@ -152,6 +207,13 @@ def parse( elif k[:1] == PSBT_OUT_BIP32_DERIVATION: #  parse just one hd key path at time :-( hd_key_paths[k[1:]] = BIP32KeyOrigin.parse(v) + elif k[:1] == PSBT_OUT_TAP_INTERNAL_KEY: + taproot_internal_key = deserialize_bytes(k, v, "taproot internal key") + elif k[:1] == PSBT_OUT_TAP_TREE: + taproot_tree = parse_taproot_tree(v) + elif k[:1] == PSBT_OUT_TAP_BIP32_DERIVATION: + #  parse just one hd key path at time :-( + taproot_hd_key_paths[k[1:]] = parse_taproot_bip32(v) # type: ignore else: # unknown unknown[k] = v @@ -159,6 +221,9 @@ def parse( redeem_script, witness_script, hd_key_paths, + taproot_internal_key, + taproot_tree, + taproot_hd_key_paths, # type: ignore unknown, check_validity, ) diff --git a/btclib/psbt/psbt_utils.py b/btclib/psbt/psbt_utils.py index 46f4b1618..ea3ae5799 100644 --- a/btclib/psbt/psbt_utils.py +++ b/btclib/psbt/psbt_utils.py @@ -15,12 +15,15 @@ from __future__ import annotations from io import BytesIO -from typing import Mapping +from typing import Any, Mapping, Sequence from btclib import var_bytes, var_int from btclib.alias import BinaryData, Octets from btclib.bip32 import BIP32KeyOrigin +from btclib.bip32.der_path import indexes_from_bip32_path, str_from_bip32_path from btclib.exceptions import BTClibValueError +from btclib.script import parse as parse_script +from btclib.script.taproot import assert_valid_control_block from btclib.tx import Tx from btclib.utils import bytes_from_octets, bytesio_from_binarydata @@ -93,6 +96,166 @@ def serialize_dict_bytes_bytes( ) +def encode_leaf_scripts( + dict_: Mapping[bytes, tuple[bytes, int]] +) -> dict[str, tuple[str, int]]: + """Return the json representation of a tap_leaf_script. + + A tap_leaf_script has a control block as key, and a taproot script + and leaf version as value. + """ + return {k.hex(): (v[0].hex(), v[1]) for k, v in dict_.items()} + + +def decode_leaf_scripts( + map_: Mapping[Octets, tuple[Octets, int]] | None +) -> dict[bytes, tuple[bytes, int]]: + """Return a tap_leaf_script from its json representation.""" + if map_ is None: + return {} + return { + bytes_from_octets(k): (bytes_from_octets(v[0]), v[1]) for k, v in map_.items() + } + + +def serialize_leaf_scripts( + type_: bytes, dictionary: dict[bytes, tuple[bytes, int]] +) -> bytes: + """Return the binary representation of the tap_leaf_script.""" + return b"".join( + [ + var_bytes.serialize(type_ + k) + + var_bytes.serialize(v[0] + v[1].to_bytes(1, "big")) + for k, v in sorted(dictionary.items()) + ] + ) + + +def parse_leaf_script(v: bytes) -> tuple[bytes, int]: + """Split the script and the leaf version.""" + return (v[:-1], v[-1]) + + +def encode_taproot_tree( + list_: list[tuple[int, int, bytes]] +) -> list[tuple[int, int, str]]: + """Return the json representation of a tap_tree. + + A tapree is a list of depth, leaf version, and taproot script. + """ + return [(v[0], v[1], v[2].hex()) for v in list_] + + +def decode_taproot_tree( + list_: Sequence[tuple[int, int, Octets]] | None +) -> list[tuple[int, int, bytes]]: + """Return a tap_tree from its json representation.""" + if list_ is None: + return [] + return [(v[0], v[1], bytes_from_octets(v[2])) for v in list_] + + +def serialize_taproot_tree(type_: bytes, list_: list[tuple[int, int, bytes]]) -> bytes: + """Return the binary representation of the tap_tree.""" + return var_bytes.serialize(type_) + var_bytes.serialize( + b"".join( + [ + v[0].to_bytes(1, "big") + + v[1].to_bytes(1, "big") + + var_bytes.serialize(v[2]) + for v in list_ + ] + ) + ) + + +def parse_taproot_tree(v: bytes) -> list[tuple[int, int, bytes]]: + """Return a tap_tree from its bytes representation.""" + out: list[tuple[int, int, bytes]] = [] + + stream = bytesio_from_binarydata(v) + while True: + v = stream.read(1) + if not v: + return out + depth = int.from_bytes(v, "big") + leaf_version = int.from_bytes(stream.read(1), "big") + script = var_bytes.parse(stream) + out.append((depth, leaf_version, script)) + + +def taproot_bip32_to_dict( + taproot_hd_key_paths: dict[bytes, tuple[list[bytes], BIP32KeyOrigin]] +) -> list[dict[str, Any]]: + """Return the json representation of a tap_bip32_derivation. + + A tap_bip32_derivation is a list of leaf_hashes, master fingerprint, + derivation path. + """ + return [ + { + "pub_key": pub_key.hex(), + "leaf_hashes": [x.hex() for x in leaf_hashes], + "master_fingerprint": key_origin.master_fingerprint.hex(), + "path": str_from_bip32_path(key_origin.der_path), + } + for pub_key, (leaf_hashes, key_origin) in sorted(taproot_hd_key_paths.items()) + ] + + +def taproot_bip32_from_dict( + taproot_hd_key_paths: list[dict[str, str]] +) -> dict[bytes, tuple[list[bytes], BIP32KeyOrigin]]: + """Return a tap_bip32_derivation from its json representation.""" + return { + bytes_from_octets(bip32_deriv["pub_key"], 4): ( + [bytes_from_octets(x) for x in bip32_deriv["leaf_hashes"]], + BIP32KeyOrigin( + bytes_from_octets(bip32_deriv["master_fingerprint"], 4), + indexes_from_bip32_path(bip32_deriv["path"]), + ), + ) + for bip32_deriv in taproot_hd_key_paths + } + + +def decode_taproot_bip32( + dict_: Mapping[Octets, tuple[Sequence[Octets], BIP32KeyOrigin]] | None +) -> dict[bytes, tuple[list[bytes], BIP32KeyOrigin]]: + """Parse correctly the tap_bip32_derivation init arguments.""" + if dict_ is None: + return {} + taproot_bip32 = { + bytes_from_octets(k): ([bytes_from_octets(x) for x in v[0]], v[1]) + for k, v in dict_.items() + } + return dict(sorted(taproot_bip32.items())) + + +def serialize_taproot_bip32( + type_: bytes, dict_: dict[bytes, tuple[list[bytes], BIP32KeyOrigin]] +) -> bytes: + """Return the binary representation of the tap_bip32_derivation.""" + return b"".join( + [ + var_bytes.serialize(type_ + k) + + var_bytes.serialize( + var_int.serialize(len(v[0])) + b"".join(v[0]) + v[1].serialize() + ) + for k, v in sorted(dict_.items()) + ] + ) + + +def parse_taproot_bip32(v: bytes) -> tuple[list[bytes], BIP32KeyOrigin]: + """Return a tap_bip32_derivation from its bytes representation.""" + stream = bytesio_from_binarydata(v) + len_ = var_int.parse(stream) + leafs = [stream.read(4) for _ in range(len_)] + bip32keyorigin = BIP32KeyOrigin.parse(stream.read()) + return (leafs, bip32keyorigin) + + def serialize_bytes(type_: bytes, value: bytes) -> bytes: """Return the binary representation of the dataclass element.""" return var_bytes.serialize(type_) + var_bytes.serialize(value) @@ -125,6 +288,48 @@ def assert_valid_unknown(data: Mapping[bytes, bytes]) -> None: bytes(value) +def assert_valid_taproot_internal_key(key: bytes) -> None: + """Fails when the internal pubkey has not the correct length.""" + if key and len(key) != 32: + raise BTClibValueError("invalid taproot internal key length") + + +def assert_valid_taproot_script_keys(keys: list[bytes], err_msg: str) -> None: + """Fails when the keys have not the correct length. + + Each key is the sum of a 32byte pubkey and a 32 byte leaf hash. + """ + if any(key and len(key) != 64 for key in keys): + raise BTClibValueError(err_msg) + + +def assert_valid_taproot_signatures(signatures: list[bytes], err_msg: str) -> None: + """Fails when the signatures have not the correct length.""" + if any(signature and len(signature) != 64 for signature in signatures): + raise BTClibValueError(err_msg) + + +def assert_valid_taproot_tree(tree: list[tuple[int, int, bytes]]) -> None: + """Fails when the scripts are not valid.""" + for _, _, tapscript in tree: + parse_script(tapscript, True) + + +def assert_valid_taproot_bip32_derivation( + derivations: dict[bytes, tuple[list[bytes], BIP32KeyOrigin]] +) -> None: + """Fails when the public keys have not the correct length.""" + for pubkey in derivations: + if len(pubkey) != 32: + raise BTClibValueError("invalid taproot bip32 derivation") + + +def assert_valid_leaf_scripts(leaf_scripts: dict[bytes, tuple[bytes, int]]) -> None: + """Fails when the control blocks have not the correct length.""" + for control_block in leaf_scripts: + assert_valid_control_block(control_block) + + def deserialize_tx( k: bytes, v: bytes, type_: str, include_witness: bool | None = True ) -> Tx: diff --git a/btclib/script/taproot.py b/btclib/script/taproot.py index 31dc11a97..e791eb9f4 100644 --- a/btclib/script/taproot.py +++ b/btclib/script/taproot.py @@ -140,3 +140,8 @@ def check_output_pubkey( P = (p, secp256k1.y_even(p)) Q = secp256k1.add(P, mult(t)) return Q[0] == int.from_bytes(q, "big") and control[0] & 1 == Q[1] % 2 + + +def assert_valid_control_block(control_block: bytes) -> None: + if (len(control_block) - 1) % 32 != 0: + raise BTClibValueError("invalid control block size") diff --git a/tests/psbt/_data/bip371_test_vectors.json b/tests/psbt/_data/bip371_test_vectors.json new file mode 100644 index 000000000..2e8c492d1 --- /dev/null +++ b/tests/psbt/_data/bip371_test_vectors.json @@ -0,0 +1,85 @@ +{ + "invalid psbts": [ + { + "description": "PSBT With PSBT_IN_TAP_INTERNAL_KEY key that is too long (incorrectly serialized as compressed DER)", + "error message": "invalid taproot internal key length", + "encoded psbt": "cHNidP8BAHECAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////Anh8AQAAAAAAFgAUg6fjS9mf8DpJYu+KGhAbspVGHs5gawQqAQAAABYAFHrDad8bIOAz1hFmI5V7CsSfPFLoAAAAAAABASsA8gUqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXARchAv40kGTJjW4qhT+jybEr2LMEoZwZXGDvp+4jkwRtP6IyAAAA" + }, + { + "description": "PSBT With PSBT_KEY_PATH_SIG signature that is too short", + "error message": "invalid taproot key path signature length", + "encoded psbt": "cHNidP8BAHECAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////Anh8AQAAAAAAFgAUg6fjS9mf8DpJYu+KGhAbspVGHs5gawQqAQAAABYAFHrDad8bIOAz1hFmI5V7CsSfPFLoAAAAAAABASsA8gUqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXARM/Fzuz02wHSvtxb+xjB6BpouRQuZXzyCeFlFq43w4kJg3NcDsMvzTeOZGEqUgawrNYbbZgHwJqd/fkk4SBvDR1AAAA" + }, + { + "description": "PSBT With PSBT_KEY_PATH_SIG signature that is too long", + "error message": "invalid taproot key path signature length", + "encoded psbt": "cHNidP8BAHECAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////Anh8AQAAAAAAFgAUg6fjS9mf8DpJYu+KGhAbspVGHs5gawQqAQAAABYAFHrDad8bIOAz1hFmI5V7CsSfPFLoAAAAAAABASsA8gUqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXARNCFzuz02wHSvtxb+xjB6BpouRQuZXzyCeFlFq43w4kJg3NcDsMvzTeOZGEqUgawrNYbbZgHwJqd/fkk4SBvDR1FwGqAAAA" + }, + { + "description": "PSBT With PSBT_IN_TAP_BIP32_DERIVATION key that is too long (incorrectly serialized as compressed DER)", + "error message": "invalid taproot bip32 derivation", + "encoded psbt": "cHNidP8BAHECAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////Anh8AQAAAAAAFgAUg6fjS9mf8DpJYu+KGhAbspVGHs5gawQqAQAAABYAFHrDad8bIOAz1hFmI5V7CsSfPFLoAAAAAAABASsA8gUqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXIhYC/jSQZMmNbiqFP6PJsSvYswShnBlcYO+n7iOTBG0/ojIZAHcrLadWAACAAQAAgAAAAIABAAAAAAAAAAAAAA==" + }, + { + "description": "PSBT With PSBT_OUT_TAP_INTERNAL_KEY key that is too long (incorrectly serialized as compressed DER)", + "error message": "invalid taproot internal key length", + "encoded psbt": "cHNidP8BAH0CAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////Aoh7AQAAAAAAFgAUI4KHHH6EIaAAk/dU2RKB5nWHS59gawQqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXAAAAAAABASsA8gUqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXAAABBSEC/jSQZMmNbiqFP6PJsSvYswShnBlcYO+n7iOTBG0/ojIA" + }, + { + "description": "PSBT With PSBT_OUT_TAP_BIP32_DERIVATION key that is too long (incorrectly serialized as compressed DER)", + "error message": "invalid taproot bip32 derivation", + "encoded psbt": "cHNidP8BAH0CAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////Aoh7AQAAAAAAFgAUI4KHHH6EIaAAk/dU2RKB5nWHS59gawQqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXAAAAAAABASsA8gUqAQAAACJRIFosLPW1LPMfg60ujaY/8DGD7Nj2CcdRCuikjgORCgdXAAAiBwL+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMhkAdystp1YAAIABAACAAAAAgAEAAAAAAAAAAA==" + }, + { + "description": "PSBT With PSBT_IN_TAP_SCRIPT_SIG key that is too long (incorrectly serialized as compressed DER)", + "error message": "invalid taproot script path key length", + "encoded psbt": "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJCFAIssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20s2XDhX1P8DIL5UP1WD/qRm3YXK+AXNoqJkTrwdPQAsJQIl1aqNznMxonsD886NgvjLMC1mxbpOh6LtGBXJrLKej/3BsQXZkljKyzGjh+RK4pXjjcZzncQiFx6lm9JvNQ8sAAA==" + }, + { + "description": "PSBT With PSBT_IN_TAP_SCRIPT_SIG signature that is too long", + "error message": "invalid taproot script path signature length", + "encoded psbt": "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJBFCyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwlCiXVqo3OczGiewPzzo2C+MswLWbFuk6Hou0YFcmssp6P/cGxBdmSWMrLMaOH5ErileONxnOdxCIXHqWb0m81DywEBAAA=" + }, + { + "description": "PSBT With PSBT_IN_TAP_SCRIPT_SIG signature that is too short", + "error message": "invalid taproot script path signature length", + "encoded psbt": "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJBFCyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwk/iXVqo3OczGiewPzzo2C+MswLWbFuk6Hou0YFcmssp6P/cGxBdmSWMrLMaOH5ErileONxnOdxCIXHqWb0m81DAAA=" + }, + { + "description": "PSBT With PSBT_IN_TAP_LEAF_SCRIPT Control block that is too long", + "error message": "invalid control block size", + "encoded psbt": "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJjFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4fgAIyAssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20qzAAAA=" + }, + { + "description": "PSBT With PSBT_IN_TAP_LEAF_SCRIPT Control block that is too short", + "error message": "invalid control block size", + "encoded psbt": "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgAw2k/OT32yjCyylRYx4ANxOFZZf+ljiCy1AOaBEsymMAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJhFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4SMgLLE6xoJI3oBqpqNlnPPAPraCHQnIEUpOho/r3oZbttKswAAA" + } + ], + "valid psbts": [ + { + "description": "PSBT with one P2TR key only input with internal key and its derivation path", + "encoded psbt": "cHNidP8BAFICAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAFgAUdo4e60z0IIZgM/gKzv8PlyB0SWkAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1chFv40kGTJjW4qhT+jybEr2LMEoZwZXGDvp+4jkwRtP6IyGQB3Ky2nVgAAgAEAAIAAAACAAQAAAAAAAAABFyD+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMgAiAgNrdyptt02HU8mKgnlY3mx4qzMSEJ830+AwRIQkLs5z2Bh3Ky2nVAAAgAEAAIAAAACAAAAAAAAAAAAA" + }, + { + "description": "PSBT with one P2TR key only input with internal key, its derivation path, and signature", + "encoded psbt": "cHNidP8BAFICAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAFgAUdo4e60z0IIZgM/gKzv8PlyB0SWkAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1cBE0C7U+yRe62dkGrxuocYHEi4as5aritTYFpyXKdGJWMUdvxvW67a9PLuD0d/NvWPOXDVuCc7fkl7l68uPxJcl680IRb+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMhkAdystp1YAAIABAACAAAAAgAEAAAAAAAAAARcg/jSQZMmNbiqFP6PJsSvYswShnBlcYO+n7iOTBG0/ojIAIgIDa3cqbbdNh1PJioJ5WN5seKszEhCfN9PgMESEJC7Oc9gYdystp1QAAIABAACAAAAAgAAAAAAAAAAAAA==" + }, + { + "description": "PSBT with one P2TR key only output with internal key and its derivation path", + "encoded psbt": "cHNidP8BAF4CAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAIlEgg2mORYxmZOFZXXXaJZfeHiLul9eY5wbEwKS1qYI810MAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1chFv40kGTJjW4qhT+jybEr2LMEoZwZXGDvp+4jkwRtP6IyGQB3Ky2nVgAAgAEAAIAAAACAAQAAAAAAAAABFyD+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMgABBSARJNp67JLM0GyVRWJkf0N7E4uVchqEvivyJ2u92rPmcSEHESTaeuySzNBslUViZH9DexOLlXIahL4r8idrvdqz5nEZAHcrLadWAACAAQAAgAAAAIAAAAAABQAAAAA=" + }, + { + "description": "PSBT with one P2TR script path only input with dummy internal key, scripts, derivation paths for keys in the scripts, and merkle root", + "encoded psbt": "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgg2mORYxmZOFZXXXaJZfeHiLul9eY5wbEwKS1qYI810MAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJiFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4fgjICyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSrMBCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wJfG5v6l/3FP9XJEmZkIEOQG6YqhD1v35fZ4S8HQqabOIyBDILC/FvARtT6nvmFZJKp/J+XSmtIOoRVdhIZ2w7rRsqzAYhXBUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsDNlw4V9T/AyC+VD9Vg/6kZt2FyvgFzaKiZE68HT0ALCRFfLkkK98xFxPeFEfNgV85cWlxWMlop+0TfwgPzVuH4IyD6D3o87zsdDAps59JuF62gsuXJLRnvrUi0GFnLikUcqazAIRYssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20jkBzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwl3Ky2nVgAAgAEAAIACAACAAAAAAAAAAAAhFkMgsL8W8BG1Pqe+YVkkqn8n5dKa0g6hFV2EhnbDutGyOQERXy5JCvfMRcT3hRHzYFfOXFpcVjJaKftE38ID81bh+HcrLadWAACAAQAAgAEAAIAAAAAAAAAAACEWUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAFAHxGHl0hFvoPejzvOx0MCmzn0m4XraCy5cktGe+tSLQYWcuKRRypOQFvfWIFnpSXoaSiZ1admHbaYBAa/zjjUpubk5zn+RrpcHcrLadWAACAAQAAgAMAAIAAAAAAAAAAAAEXIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAARgg8DYuL3Wm9CClvePrIh2WrmcgzyX4GJDJWx13WstRXmUAAQUgESTaeuySzNBslUViZH9DexOLlXIahL4r8idrvdqz5nEhBxEk2nrskszQbJVFYmR/Q3sTi5VyGoS+K/Ina73as+ZxGQB3Ky2nVgAAgAEAAIAAAACAAAAAAAUAAAAA" + }, + { + "description": "PSBT with one P2TR script path only output with dummy internal key, taproot tree, and script key derivation paths", + "encoded psbt": "cHNidP8BAF4CAAAAASd0Srq/MCf+DWzyOpbu4u+xiO9SMBlUWFiD5ptmJLJCAAAAAAD/////AUjmBSoBAAAAIlEgCoy9yG3hzhwPnK6yLW33ztNoP+Qj4F0eQCqHk0HW9vUAAAAAAAEBKwDyBSoBAAAAIlEgWiws9bUs8x+DrS6Npj/wMYPs2PYJx1EK6KSOA5EKB1chFv40kGTJjW4qhT+jybEr2LMEoZwZXGDvp+4jkwRtP6IyGQB3Ky2nVgAAgAEAAIAAAACAAQAAAAAAAAABFyD+NJBkyY1uKoU/o8mxK9izBKGcGVxg76fuI5MEbT+iMgABBSBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wAEGbwLAIiBzblcpAP4SUliaIUPI88efcaBBLSNTr3VelwHHgmlKAqwCwCIgYxxfO1gyuPvev7GXBM7rMjwh9A96JPQ9aO8MwmsSWWmsAcAiIET6pJoDON5IjI3//s37bzKfOAvVZu8gyN9tgT6rHEJzrCEHRPqkmgM43kiMjf/+zftvMp84C9Vm7yDI322BPqscQnM5AfBreYuSoQ7ZqdC7/Trxc6U7FhfaOkFZygCCFs2Fay4Odystp1YAAIABAACAAQAAgAAAAAADAAAAIQdQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wAUAfEYeXSEHYxxfO1gyuPvev7GXBM7rMjwh9A96JPQ9aO8MwmsSWWk5ARis5AmIl4Xg6nDO67jhyokqenjq7eDy4pbPQ1lhqPTKdystp1YAAIABAACAAgAAgAAAAAADAAAAIQdzblcpAP4SUliaIUPI88efcaBBLSNTr3VelwHHgmlKAjkBKaW0kVCQFi11mv0/4Pk/ozJgVtC0CIy5M8rngmy42Cx3Ky2nVgAAgAEAAIADAACAAAAAAAMAAAAA" + }, + { + "description": "PSBT with one P2TR script path only input with dummy internal key, scripts, script key derivation paths, merkle root, and script path signatures", + "encoded psbt": "cHNidP8BAF4CAAAAAZvUh2UjC/mnLmYgAflyVW5U8Mb5f+tWvLVgDYF/aZUmAQAAAAD/////AUjmBSoBAAAAIlEgg2mORYxmZOFZXXXaJZfeHiLul9eY5wbEwKS1qYI810MAAAAAAAEBKwDyBSoBAAAAIlEgwiR++/2SrEf29AuNQtFpF1oZ+p+hDkol1/NetN2FtpJBFCyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwlAv4GNl1fW/+tTi6BX+0wfxOD17xhudlvrVkeR4Cr1/T1eJVHU404z2G8na4LJnHmu0/A5Wgge/NLMLGXdfmk9eUEUQyCwvxbwEbU+p75hWSSqfyfl0prSDqEVXYSGdsO60bIRXy5JCvfMRcT3hRHzYFfOXFpcVjJaKftE38ID81bh+EDh8atvq/omsjbyGDNxncHUKKt2jYD5H5mI2KvvR7+4Y7sfKlKfdowV8AzjTsKDzcB+iPhCi+KPbvZAQ8MpEYEaQRT6D3o87zsdDAps59JuF62gsuXJLRnvrUi0GFnLikUcqW99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwQOwfA3kgZGHIM0IoVCMyZwirAx8NpKJT7kWq+luMkgNNi2BUkPjNE+APmJmJuX4hX6o28S3uNpPS2szzeBwXV/ZiFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wG99YgWelJehpKJnVp2YdtpgEBr/OONSm5uTnOf5GulwEV8uSQr3zEXE94UR82BXzlxaXFYyWin7RN/CA/NW4fgjICyxOsaCSN6AaqajZZzzwD62gh0JyBFKToaP696GW7bSrMBCFcFQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wJfG5v6l/3FP9XJEmZkIEOQG6YqhD1v35fZ4S8HQqabOIyBDILC/FvARtT6nvmFZJKp/J+XSmtIOoRVdhIZ2w7rRsqzAYhXBUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsDNlw4V9T/AyC+VD9Vg/6kZt2FyvgFzaKiZE68HT0ALCRFfLkkK98xFxPeFEfNgV85cWlxWMlop+0TfwgPzVuH4IyD6D3o87zsdDAps59JuF62gsuXJLRnvrUi0GFnLikUcqazAIRYssTrGgkjegGqmo2Wc88A+toIdCcgRSk6Gj+vehlu20jkBzZcOFfU/wMgvlQ/VYP+pGbdhcr4Bc2iomROvB09ACwl3Ky2nVgAAgAEAAIACAACAAAAAAAAAAAAhFkMgsL8W8BG1Pqe+YVkkqn8n5dKa0g6hFV2EhnbDutGyOQERXy5JCvfMRcT3hRHzYFfOXFpcVjJaKftE38ID81bh+HcrLadWAACAAQAAgAEAAIAAAAAAAAAAACEWUJKbdMGgSVS3i0tgNel6XgeKWg8o7JbVR7/ums6AOsAFAHxGHl0hFvoPejzvOx0MCmzn0m4XraCy5cktGe+tSLQYWcuKRRypOQFvfWIFnpSXoaSiZ1admHbaYBAa/zjjUpubk5zn+RrpcHcrLadWAACAAQAAgAMAAIAAAAAAAAAAAAEXIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAARgg8DYuL3Wm9CClvePrIh2WrmcgzyX4GJDJWx13WstRXmUAAQUgESTaeuySzNBslUViZH9DexOLlXIahL4r8idrvdqz5nEhBxEk2nrskszQbJVFYmR/Q3sTi5VyGoS+K/Ina73as+ZxGQB3Ky2nVgAAgAEAAIAAAACAAAAAAAUAAAAA" + } + ] +} diff --git a/tests/psbt/_generated_files/psbt.json b/tests/psbt/_generated_files/psbt.json index 0c2f6499a..be3dd55f8 100644 --- a/tests/psbt/_generated_files/psbt.json +++ b/tests/psbt/_generated_files/psbt.json @@ -128,6 +128,12 @@ "sha256_preimages": {}, "hash160_preimages": {}, "hash256_preimages": {}, + "taproot_key_spend_signature": "", + "taproot_script_spend_signatures": {}, + "taproot_leaf_scripts": {}, + "taproot_hd_key_paths": [], + "taproot_internal_key": "", + "taproot_merkle_root": "", "unknown": {} }, { @@ -169,6 +175,12 @@ "sha256_preimages": {}, "hash160_preimages": {}, "hash256_preimages": {}, + "taproot_key_spend_signature": "", + "taproot_script_spend_signatures": {}, + "taproot_leaf_scripts": {}, + "taproot_hd_key_paths": [], + "taproot_internal_key": "", + "taproot_merkle_root": "", "unknown": {} } ], @@ -183,6 +195,9 @@ "path": "m/0h/0h/4h" } ], + "taproot_internal_key": "", + "taproot_tree": [], + "taproot_hd_key_paths": [], "unknown": {} }, { @@ -195,6 +210,9 @@ "path": "m/0h/0h/5h" } ], + "taproot_internal_key": "", + "taproot_tree": [], + "taproot_hd_key_paths": [], "unknown": {} } ], diff --git a/tests/psbt/_generated_files/psbt_in.json b/tests/psbt/_generated_files/psbt_in.json index 42ac539be..860aaf705 100644 --- a/tests/psbt/_generated_files/psbt_in.json +++ b/tests/psbt/_generated_files/psbt_in.json @@ -71,5 +71,11 @@ "sha256_preimages": {}, "hash160_preimages": {}, "hash256_preimages": {}, + "taproot_key_spend_signature": "", + "taproot_script_spend_signatures": {}, + "taproot_leaf_scripts": {}, + "taproot_hd_key_paths": [], + "taproot_internal_key": "", + "taproot_merkle_root": "", "unknown": {} } diff --git a/tests/psbt/_generated_files/psbt_out.json b/tests/psbt/_generated_files/psbt_out.json index 098be5ff7..734e462fc 100644 --- a/tests/psbt/_generated_files/psbt_out.json +++ b/tests/psbt/_generated_files/psbt_out.json @@ -8,5 +8,8 @@ "path": "m/0h/0h/4h" } ], + "taproot_internal_key": "", + "taproot_tree": [], + "taproot_hd_key_paths": [], "unknown": {} } diff --git a/tests/psbt/test_psbt.py b/tests/psbt/test_psbt.py index f42ded619..7ab6c9485 100644 --- a/tests/psbt/test_psbt.py +++ b/tests/psbt/test_psbt.py @@ -61,6 +61,31 @@ def test_vectors_bip174() -> None: assert test_vector["encoded psbt"] == Psbt.b64encode(psbt_decoded) +def test_vectors_bip371() -> None: + """Test https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki.""" + + data_folder = path.join(path.dirname(__file__), "_data") + filename = path.join(data_folder, "bip371_test_vectors.json") + with open(filename, encoding="ascii") as file_: + # json.dump(test_vectors, f, indent=4) + test_vectors = json.load(file_) + + for i, test_vector in enumerate(test_vectors["valid psbts"]): + try: + psbt_decoded = Psbt.b64decode(test_vector["encoded psbt"]) + except Exception as e: # pragma: no cover # pylint: disable=broad-except + print(f"valid case {i+1}: {test_vector['description']}") # pragma: no cover + raise e # pragma: no cover + assert test_vector["encoded psbt"] == Psbt.b64encode(psbt_decoded) + + for i, test_vector in enumerate(test_vectors["invalid psbts"]): + with pytest.raises(BTClibValueError) as excinfo: + Psbt.b64decode(test_vector["encoded psbt"]) + assert test_vector["error message"] in str( + excinfo.value + ), f"invalid case {i+1}: {test_vector['description']}\n{excinfo.value}" + + def test_creation() -> None: psbt_str = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" psbt = Psbt.b64decode(psbt_str) diff --git a/tox.ini b/tox.ini index 15b4ebe20..338f53876 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,8 @@ envlist = pre-commit, py [testenv] ignore_errors = True +passenv = + SSH_AUTH_SOCK deps = -rrequirements-dev.txt commands =