diff --git a/src/embit/bech32.py b/src/embit/bech32.py index 24ee7fa7..ba99219d 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -19,7 +19,9 @@ # THE SOFTWARE. """Reference implementation for Bech32 and segwit addresses.""" + from .misc import const +from .base import EmbitError CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" BECH32_CONST = const(1) @@ -33,6 +35,10 @@ class Encoding: BECH32M = 2 +class Bech32DecodeError(EmbitError): + pass + + def bech32_polymod(values): """Internal function that computes the Bech32 checksum.""" generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] @@ -77,21 +83,32 @@ def bech32_encode(encoding, hrp, data): def bech32_decode(bech): """Validate a Bech32/Bech32m string, and determine HRP and data.""" - if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( - bech.lower() != bech and bech.upper() != bech - ): - return (None, None, None) + if any(ord(x) < 33 or ord(x) > 126 for x in bech): + raise Bech32DecodeError("Invalid character in input") + if bech.lower() != bech and bech.upper() != bech: + raise Bech32DecodeError("Mixed case strings not allowed") bech = bech.lower() pos = bech.rfind("1") - if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: - return (None, None, None) - if not all(x in CHARSET for x in bech[pos + 1 :]): - return (None, None, None) + if pos < 1: + raise Bech32DecodeError("Separator '1' not found or misplaced") + # BIP-173: the HRP must contain 1 to 83 characters. + if pos > 83: + raise Bech32DecodeError("HRP too long (max 83 characters)") + if pos + 7 > len(bech): + raise Bech32DecodeError("Data part too short") + # BIP-173 caps Bech32 strings at 90 chars, but BIP-352 silent payment + # addresses are longer (>=117) and recommend a 1023-char limit to leave + # room for future versions. + if len(bech) > 1023: + raise Bech32DecodeError("String too long (max 1023 characters)") hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos + 1 :]] + data_part = bech[pos + 1 :] + if not all(x in CHARSET for x in data_part): + raise Bech32DecodeError("Data part contains invalid characters") + data = [CHARSET.find(x) for x in data_part] encoding = bech32_verify_checksum(hrp, data) if encoding is None: - return (None, None, None) + raise Bech32DecodeError("Checksum verification failed") return (encoding, hrp, data[:-6]) @@ -104,7 +121,7 @@ def convertbits(data, frombits, tobits, pad=True): max_acc = (1 << (frombits + tobits - 1)) - 1 for value in data: if value < 0 or (value >> frombits): - return None + raise Bech32DecodeError("Invalid input value for bit conversion") acc = ((acc << frombits) | value) & max_acc bits += frombits while bits >= tobits: @@ -114,33 +131,51 @@ def convertbits(data, frombits, tobits, pad=True): if bits: ret.append((acc << (tobits - bits)) & maxv) elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None + raise Bech32DecodeError("Invalid padding in bit conversion") return ret def decode(hrp, addr): - """Decode a segwit address.""" + """Decode a segwit address. + + Silent payment (sp/tsp) addresses are not witness programs and must not be + decoded here; use bech32_decode + convertbits for those. + """ + # BIP-173/BIP-350: segwit addresses are capped at 90 characters. + if len(addr) > 90: + raise Bech32DecodeError("Segwit address too long (max 90 characters)") encoding, hrpgot, data = bech32_decode(addr) if hrpgot != hrp: - return (None, None) + raise Bech32DecodeError("HRP mismatch: expected {}, got {}".format(hrp, hrpgot)) decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: - return (None, None) + # BIP-141: a witness program is 2 to 40 bytes. + if len(decoded) < 2 or len(decoded) > 40: + raise Bech32DecodeError("Invalid witness program length") if data[0] > 16: - return (None, None) + raise Bech32DecodeError("Invalid witness version") if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) + raise Bech32DecodeError("Invalid witness program length for version 0") if (data[0] == 0 and encoding != Encoding.BECH32) or ( data[0] != 0 and encoding != Encoding.BECH32M ): - return (None, None) + raise Bech32DecodeError("Invalid encoding for witness version") return (data[0], decoded) def encode(hrp, witver, witprog): """Encode a segwit address.""" + if witver < 0 or witver > 16: + raise Bech32DecodeError("Invalid witness version") + if len(witprog) < 2 or len(witprog) > 40: + raise Bech32DecodeError("Invalid witness program length") + if witver == 0 and len(witprog) != 20 and len(witprog) != 32: + raise Bech32DecodeError("Invalid witness program length for version 0") + encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) - if decode(hrp, ret) == (None, None): - return None + + # Sanity check: the result must round-trip. Any failure propagates with its + # original, descriptive Bech32DecodeError. + decode(hrp, ret) + return ret diff --git a/src/embit/descriptor/__init__.py b/src/embit/descriptor/__init__.py index 600296d3..00298b68 100644 --- a/src/embit/descriptor/__init__.py +++ b/src/embit/descriptor/__init__.py @@ -1,3 +1,4 @@ from . import miniscript from .descriptor import Descriptor from .arguments import Key +from .sp import SilentPaymentDescriptor diff --git a/src/embit/descriptor/descriptor.py b/src/embit/descriptor/descriptor.py index fd3c3057..e0cc00f7 100644 --- a/src/embit/descriptor/descriptor.py +++ b/src/embit/descriptor/descriptor.py @@ -6,6 +6,7 @@ from .miniscript import Miniscript, Multi, Sortedmulti from .arguments import Key from .taptree import TapTree +from .sp import SilentPaymentDescriptor class Descriptor(DescriptorBase): @@ -302,6 +303,15 @@ def read_from(cls, s): is_miniscript = True taproot = False taptree = TapTree() + if start.startswith(b"sp("): + # rewind to right after "sp(" with a relative seek (robust to a + # short read(8)). MicroPython's BytesIO has no tell(), so avoid it. + s.seek(3 - len(start), 1) + sp_desc = SilentPaymentDescriptor.read_from(s) + end = s.read(1) + if end != b")": + raise DescriptorError("Expected closing ) for sp()") + return sp_desc if start.startswith(b"tr("): taproot = True s.seek(-5, 1) diff --git a/src/embit/descriptor/sp.py b/src/embit/descriptor/sp.py new file mode 100644 index 00000000..a184a70a --- /dev/null +++ b/src/embit/descriptor/sp.py @@ -0,0 +1,291 @@ +from io import BytesIO +from .. import bech32, ec +from ..misc import read_until +from .base import DescriptorBase +from .errors import DescriptorError +from .arguments import KeyOrigin, Key + +SPSCAN_HRPS = {"spscan": "main", "tspscan": "test"} +SPSPEND_HRPS = {"spspend": "main", "tspspend": "test"} +SP_KEY_HRPS = SPSCAN_HRPS.copy() +SP_KEY_HRPS.update(SPSPEND_HRPS) + + +def _bech32m_decode_sp_key(encoded): + encoding, hrp, data = bech32.bech32_decode(encoded) + payload = bech32.convertbits(data[1:], 5, 8, False) + return hrp, bytes(payload) + + +def _bech32m_encode_sp_key(hrp, payload): + data = bech32.convertbits(payload, 8, 5) + return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [0] + data) + + +class SPScanKey: + """spscan key expression: encodes scan_privkey + spend_pubkey.""" + + def __init__(self, scan_privkey, spend_pubkey, origin=None, network="main"): + if not isinstance(scan_privkey, ec.PrivateKey): + raise DescriptorError("SPScanKey scan key must be a PrivateKey") + if not isinstance(spend_pubkey, ec.PublicKey): + raise DescriptorError("SPScanKey spend key must be a PublicKey") + self.scan_privkey = scan_privkey + self.spend_pubkey = spend_pubkey + self.origin = origin + self.network = network + + @property + def is_watch_only(self): + return True + + @classmethod + def decode(cls, encoded, origin=None): + hrp, payload = _bech32m_decode_sp_key(encoded) + if hrp not in SPSCAN_HRPS: + raise DescriptorError("Expected spscan HRP, got: %s" % hrp) + if len(payload) != 65: + raise DescriptorError( + "spscan payload must be 65 bytes (32 privkey + 33 pubkey), got %d" + % len(payload) + ) + scan_privkey = ec.PrivateKey(payload[:32]) + spend_pubkey = ec.PublicKey.parse(payload[32:65]) + network = SPSCAN_HRPS[hrp] + return cls(scan_privkey, spend_pubkey, origin, network) + + def encode(self): + hrp = "tspscan" if self.network == "test" else "spscan" + payload = self.scan_privkey.secret + self.spend_pubkey.sec() + return _bech32m_encode_sp_key(hrp, payload) + + def __str__(self): + prefix = "[%s]" % self.origin if self.origin else "" + return prefix + self.encode() + + +class SPSpendKey: + """spspend key expression: encodes scan_privkey + spend_privkey.""" + + def __init__(self, scan_privkey, spend_privkey, origin=None, network="main"): + if not isinstance(scan_privkey, ec.PrivateKey): + raise DescriptorError("SPSpendKey scan key must be a PrivateKey") + if not isinstance(spend_privkey, ec.PrivateKey): + raise DescriptorError("SPSpendKey spend key must be a PrivateKey") + self.scan_privkey = scan_privkey + self.spend_privkey = spend_privkey + self.origin = origin + self.network = network + + @property + def spend_pubkey(self): + return self.spend_privkey.get_public_key() + + @property + def is_watch_only(self): + return False + + @classmethod + def decode(cls, encoded, origin=None): + hrp, payload = _bech32m_decode_sp_key(encoded) + if hrp not in SPSPEND_HRPS: + raise DescriptorError("Expected spspend HRP, got: %s" % hrp) + if len(payload) != 64: + raise DescriptorError( + "spspend payload must be 64 bytes (32 + 32), got %d" % len(payload) + ) + scan_privkey = ec.PrivateKey(payload[:32]) + spend_privkey = ec.PrivateKey(payload[32:64]) + network = SPSPEND_HRPS[hrp] + return cls(scan_privkey, spend_privkey, origin, network) + + def encode(self): + hrp = "tspspend" if self.network == "test" else "spspend" + payload = self.scan_privkey.secret + self.spend_privkey.secret + return _bech32m_encode_sp_key(hrp, payload) + + def __str__(self): + prefix = "[%s]" % self.origin if self.origin else "" + return prefix + self.encode() + + +def _read_sp_key_expression(s): + """Read an spscan/spspend expression or a standard Key from stream.""" + first = s.read(1) + origin = None + origin_len = 0 + if first == b"[": + prefix, char = read_until(s, b"]") + if char != b"]": + raise DescriptorError("Invalid key - missing ]") + origin = KeyOrigin.from_string(prefix.decode()) + origin_len = len(prefix) + 2 # '[' + prefix + ']' + else: + s.seek(-1, 1) + + token, char = read_until(s, b",)") + if char is not None: + s.seek(-1, 1) + token_str = token.decode() + + if not token_str: + raise DescriptorError("Empty key expression in sp()") + + lower = token_str.lower() + for hrp in SPSCAN_HRPS: + if lower.startswith(hrp + "1"): + return SPScanKey.decode(token_str, origin), char + for hrp in SPSPEND_HRPS: + if lower.startswith(hrp + "1"): + return SPSpendKey.decode(token_str, origin), char + + # Not a silent-payment key: rewind past the origin prefix and token (no + # tell(); MicroPython's BytesIO lacks it) and let Key.read_from(s) consume + # the expression in-place, leaving the stream at the delimiter for the caller. + s.seek(-(origin_len + len(token)), 1) + key = Key.read_from(s) + return key, None + + +class SilentPaymentDescriptor(DescriptorBase): + """BIP-392 sp() descriptor for Silent Payments.""" + + def __init__(self, sp_key=None, scan_key=None, spend_key=None): + if sp_key is not None: + if not isinstance(sp_key, (SPScanKey, SPSpendKey)): + raise DescriptorError( + "Single-arg sp() requires an spscan or spspend key expression" + ) + self.sp_key = sp_key + self.scan_key = None + self.spend_key = None + elif scan_key is not None: + if not _is_private_key(scan_key): + raise DescriptorError("Two-arg sp(): scan key must be private") + self.sp_key = None + self.scan_key = scan_key + self.spend_key = spend_key + else: + raise DescriptorError("sp() requires at least one argument") + + @property + def is_single_arg(self): + return self.sp_key is not None + + @property + def is_watch_only(self): + if self.sp_key is not None: + return self.sp_key.is_watch_only + return not _is_private_key(self.spend_key) + + @property + def keys(self): + if self.sp_key is not None: + return [self.sp_key] + return [self.scan_key, self.spend_key] + + def get_scan_privkey(self): + if self.sp_key is not None: + return self.sp_key.scan_privkey + k = self.scan_key + if isinstance(k, Key): + return k.private_key + return None + + def get_spend_pubkey(self): + if self.sp_key is not None: + return self.sp_key.spend_pubkey + k = self.spend_key + if isinstance(k, Key): + return k.get_public_key() + return None + + @classmethod + def from_string(cls, desc): + if "#" in desc: + desc = desc.split("#")[0] + s = BytesIO(desc.encode()) + start = s.read(3) + if start != b"sp(": + raise DescriptorError("Expected sp( prefix, got: %s" % start.decode()) + res = cls._read_args(s) + end = s.read(1) + if end != b")": + raise DescriptorError("Expected closing ) for sp()") + left = s.read() + if len(left) > 0: + raise DescriptorError("Unexpected characters after sp(): %r" % left) + return res + + @classmethod + def read_from(cls, s): + return cls._read_args(s) + + @classmethod + def _read_args(cls, s): + first_arg, sep = _read_sp_key_expression(s) + + if isinstance(first_arg, (SPScanKey, SPSpendKey)): + c = s.read(1) + if c == b")": + s.seek(-1, 1) + return cls(sp_key=first_arg) + raise DescriptorError( + "spscan/spspend key must be the only argument to sp()" + ) + + c = s.read(1) + if c != b",": + raise DescriptorError( + "Single-arg sp() requires spscan or spspend key expression, " + "got a standard key" + ) + + scan_key = first_arg + if not _is_private_key(scan_key): + raise DescriptorError("Two-arg sp(): scan key must be private") + if isinstance(scan_key.key, ec.PrivateKey) and not scan_key.key.compressed: + raise DescriptorError("Uncompressed keys are not allowed in sp()") + + spend_arg, _ = _read_sp_key_expression(s) + if isinstance(spend_arg, (SPScanKey, SPSpendKey)): + raise DescriptorError( + "Two-arg sp() cannot use spscan/spspend key expressions" + ) + if isinstance(spend_arg, Key) and isinstance(spend_arg.key, ec.PrivateKey): + if not spend_arg.key.compressed: + raise DescriptorError("Uncompressed keys are not allowed in sp()") + + return cls(scan_key=scan_key, spend_key=spend_arg) + + def derive(self, *args, **kwargs): + raise DescriptorError( + "sp() descriptors do not support derive(); see BIP-352 for output derivation" + ) + + def script_pubkey(self, *args, **kwargs): + raise DescriptorError( + "sp() descriptors have no fixed script_pubkey(); outputs are derived per BIP-352" + ) + + def address(self, *args, **kwargs): + raise DescriptorError( + "sp() descriptors have no address(); use BIP-352 silent payment address generation" + ) + + def to_string(self): + if self.sp_key is not None: + return "sp(%s)" % self.sp_key + return "sp(%s,%s)" % (self.scan_key, self.spend_key) + + def __str__(self): + return self.to_string() + + def __repr__(self): + return self.to_string() + + +def _is_private_key(key): + if isinstance(key, Key): + return key.is_private + return False diff --git a/src/embit/ec.py b/src/embit/ec.py index b36d861d..25e353e4 100644 --- a/src/embit/ec.py +++ b/src/embit/ec.py @@ -77,7 +77,7 @@ def taproot_tweak(self, h=b""): pub = secp256k1.ec_pubkey_add(point, tweak) sec = secp256k1.ec_pubkey_serialize(pub) return PublicKey.from_xonly(sec[1:33]) - + def write_to(self, stream) -> int: return stream.write(self.sec()) @@ -253,6 +253,20 @@ def read_from(cls, stream): # just to unify the API return cls(stream.read(32)) + + def sp_spend_tweak(self, tweak: bytes): + """BIP-352/376: tweak a private key for SP spend: sk' = sk + tweak.""" + if len(tweak) != 32 or not secp256k1.ec_seckey_verify(tweak): + raise EmbitError("Invalid silent payment tweak") + sec = secp256k1.ec_privkey_add(self._secret, tweak) + return PrivateKey(sec) + + def even_y(self): + """Return a copy of this key with even Y coordinate (negate if odd).""" + if self.sec()[0] == 0x03: + return PrivateKey(secp256k1.ec_privkey_negate(self._secret)) + return self + @property def is_private(self) -> bool: return True diff --git a/src/embit/liquid/descriptor.py b/src/embit/liquid/descriptor.py index 23829315..cefeebe5 100644 --- a/src/embit/liquid/descriptor.py +++ b/src/embit/liquid/descriptor.py @@ -225,12 +225,15 @@ def sec(self): def musig_combine_privs(privs, sort=True): keys = [ - [b"" + prv, secp256k1.ec_pubkey_serialize(secp256k1.ec_pubkey_create(prv))] + [ + bytearray(prv), + secp256k1.ec_pubkey_serialize(secp256k1.ec_pubkey_create(prv)), + ] for prv in privs ] for karr in keys: if karr[1][0] == 0x03: - secp256k1.ec_privkey_negate(karr[0]) + karr[0] = bytearray(secp256k1.ec_privkey_negate(karr[0])) # x only karr[1] = karr[1][1:] if sort: @@ -238,7 +241,7 @@ def musig_combine_privs(privs, sort=True): secs = [k[1] for k in keys] ll = sha256(b"".join(secs)) coefs = [ - tagged_hash("MuSig coefficient", ll + i.to_bytes(4, "little")) + bytearray(tagged_hash("MuSig coefficient", ll + i.to_bytes(4, "little"))) for i in range(len(keys)) ] # tweak them all @@ -249,15 +252,15 @@ def musig_combine_privs(privs, sort=True): s = secp256k1.ec_privkey_add(s, c) pub = secp256k1.ec_pubkey_create(s) if secp256k1.ec_pubkey_serialize(pub)[0] == 0x03: - secp256k1.ec_privkey_negate(s) + s = secp256k1.ec_privkey_negate(s) return s def musig_combine_pubs(pubs, sort=True): - keys = [[pub, secp256k1.ec_pubkey_serialize(pub)] for pub in pubs] + keys = [[bytearray(pub), secp256k1.ec_pubkey_serialize(pub)] for pub in pubs] for karr in keys: if karr[1][0] == 0x03: - secp256k1.ec_pubkey_negate(karr[0]) + karr[0] = bytearray(secp256k1.ec_pubkey_negate(karr[0])) # x only karr[1] = karr[1][1:] if sort: diff --git a/src/embit/liquid/pset.py b/src/embit/liquid/pset.py index 290c59ac..22f75b2a 100644 --- a/src/embit/liquid/pset.py +++ b/src/embit/liquid/pset.py @@ -5,10 +5,8 @@ else: from ..util import secp256k1 -from .. import compact, hashes +from .. import hashes from ..psbt import * -from collections import OrderedDict -from io import BytesIO from .transaction import ( LTransaction, LTransactionOutput, @@ -140,10 +138,10 @@ def blinded_vin(self): witness=TxInWitness(self.issue_rangeproof, self.token_rangeproof), ) - def read_value(self, stream, k): + def read_value(self, stream, k, version=None): # standard bitcoin stuff if (b"\xfc\x08elements" not in k) and (b"\xfc\x04pset" not in k): - super().read_value(stream, k) + super().read_value(stream, k, version=version) elif k == b"\xfc\x04pset\x0e": # range proof is very large, # so we don't load it if compress flag is set. @@ -370,7 +368,7 @@ def reblind(self, nonce, blinding_pubkey=None, extra_message=b""): blinding_pubkey = blinding_pubkey or self.blinding_pubkey if not blinding_pubkey: raise PSBTError("Blinding pubkey required") - pub = secp256k1.ec_pubkey_parse(blinding_pubkey) + pub = bytearray(secp256k1.ec_pubkey_parse(blinding_pubkey)) self.ecdh_pubkey = ec.PrivateKey(nonce).sec() secp256k1.ec_pubkey_tweak_mul(pub, nonce) sec = secp256k1.ec_pubkey_serialize(pub) @@ -386,9 +384,9 @@ def reblind(self, nonce, blinding_pubkey=None, extra_message=b""): secp256k1.generator_parse(self.asset_commitment), ) - def read_value(self, stream, k): + def read_value(self, stream, k, version=None): if (b"\xfc\x08elements" not in k) and (b"\xfc\x04pset" not in k): - super().read_value(stream, k) + super().read_value(stream, k, version=version) # range proof and surjection proof are very large, # so we don't load them if compress flag is set. elif k in [b"\xfc\x08elements\x04", b"\xfc\x04pset\x04"]: @@ -503,6 +501,18 @@ class PSET(PSBT): PSBTOUT_CLS = LOutputScope TX_CLS = LTransaction + @classmethod + def _validate_v2_output(cls, out, i): + """PSET allows value_commitment as an alternative to value (blinded outputs).""" + if out.value is None and not getattr(out, "value_commitment", None): + raise PSBTError( + "PSBTv2 output %d missing required PSBT_OUT_AMOUNT (0x03)" % i + ) + if out.script_pubkey is None: + raise PSBTError( + "PSBTv2 output %d missing required PSBT_OUT_SCRIPT (0x04)" % i + ) + def unblind(self, blinding_key): for inp in self.inputs: inp.unblind(blinding_key) diff --git a/src/embit/liquid/transaction.py b/src/embit/liquid/transaction.py index ca7af0a5..43c0b6ef 100644 --- a/src/embit/liquid/transaction.py +++ b/src/embit/liquid/transaction.py @@ -82,7 +82,7 @@ def unblind( raise TransactionError("Invalid value commitment length") if len(asset_commitment) != 33: raise TransactionError("Invalid asset commitment length") - pub = secp256k1.ec_pubkey_parse(pubkey) + pub = bytearray(secp256k1.ec_pubkey_parse(pubkey)) secp256k1.ec_pubkey_tweak_mul(pub, blinding_key) sec = secp256k1.ec_pubkey_serialize(pub) nonce = hashlib.sha256(hashlib.sha256(sec).digest()).digest() diff --git a/src/embit/psbt.py b/src/embit/psbt.py index d20c88bb..a8110f4a 100644 --- a/src/embit/psbt.py +++ b/src/embit/psbt.py @@ -1,5 +1,12 @@ from collections import OrderedDict -from .transaction import Transaction, TransactionOutput, TransactionInput, SIGHASH +from .transaction import ( + Transaction, + TransactionOutput, + TransactionInput, + SIGHASH, + _signed_from_bytes, + _signed_to_bytes, +) from . import compact from . import bip32 from . import ec @@ -11,6 +18,8 @@ from binascii import b2a_base64, a2b_base64, hexlify, unhexlify from io import BytesIO +LOCKTIME_THRESHOLD = 500000000 + class PSBTError(EmbitError): pass @@ -40,6 +49,42 @@ def skip_string(stream) -> int: return len(compact.to_bytes(l)) + l +def resolve_signing_root(root): + """Resolve the signing fingerprint for ``root``, honoring descriptor-key + wrapping. + + Returns ``(fingerprint, can_sign, root)``. ``fingerprint`` is None for + raw/WIF keys (matched by key material instead). ``can_sign`` is False + when ``root`` is a public-only descriptor key that cannot sign. ``root`` + is unwrapped to its underlying WIF key (``root.key``) when it was a + non-extended descriptor key; otherwise it is returned unchanged. + """ + fingerprint = None + if hasattr(root, "origin"): + if not root.is_private: # pubkey can't sign + return None, False, root + if root.is_extended: # use fingerprint only for HDKey + fingerprint = root.fingerprint + else: + root = root.key # WIF key + if not fingerprint and hasattr(root, "my_fingerprint"): + fingerprint = root.my_fingerprint + return fingerprint, True, root + + +def derive_hdkey(root, derivation): + """Derive the HDKey for ``derivation``, honoring a descriptor key's origin prefix.""" + der = derivation.derivation + if hasattr(root, "origin"): + if root.origin: + prefix = root.origin.derivation + if der[: len(prefix)] != prefix: + return None + der = der[len(prefix) :] + return root.key.derive(der) + return root.derive(der) + + class DerivationPath(EmbitBase): def __init__(self, fingerprint: bytes, derivation: list): self.fingerprint = fingerprint @@ -89,7 +134,7 @@ def parse_unknowns(self): s.seek(0) self.read_value(s, k) - def read_value(self, stream, key, *args, **kwargs): + def read_value(self, stream, key, version=None): # separator if len(key) == 0: return @@ -148,6 +193,8 @@ def __init__(self, unknown: dict = {}, vin=None, compress=CompressMode.KEEP_ALL) self.final_scriptsig = None self.final_scriptwitness = None + self.required_time_locktime = None + self.required_height_locktime = None self.parse_unknowns() def clear_metadata(self, compress=CompressMode.CLEAR_ALL): @@ -186,18 +233,32 @@ def update(self, other): self.witness_script = other.witness_script or self.witness_script self.bip32_derivations.update(other.bip32_derivations) self.taproot_bip32_derivations.update(other.taproot_bip32_derivations) - self.taproot_internal_key = other.taproot_internal_key + self.taproot_internal_key = ( + other.taproot_internal_key or self.taproot_internal_key + ) self.taproot_merkle_root = other.taproot_merkle_root or self.taproot_merkle_root self.taproot_key_sig = other.taproot_key_sig or self.taproot_key_sig self.taproot_sigs.update(other.taproot_sigs) self.taproot_scripts.update(other.taproot_scripts) self.final_scriptsig = other.final_scriptsig or self.final_scriptsig self.final_scriptwitness = other.final_scriptwitness or self.final_scriptwitness + self.required_time_locktime = ( + other.required_time_locktime + if other.required_time_locktime is not None + else self.required_time_locktime + ) + self.required_height_locktime = ( + other.required_height_locktime + if other.required_height_locktime is not None + else self.required_height_locktime + ) @property def vin(self): return TransactionInput( - self.txid, self.vout, sequence=(self.sequence or 0xFFFFFFFF) + self.txid, + self.vout, + sequence=(self.sequence if self.sequence is not None else 0xFFFFFFFF), ) @property @@ -245,7 +306,7 @@ def verify(self, ignore_missing=False): raise PSBTError("Missing non_witness_utxo") return False - def read_value(self, stream, k): + def read_value(self, stream, k, version=None): # separator if len(k) == 0: return @@ -256,10 +317,18 @@ def read_value(self, stream, k): elif self.non_witness_utxo is not None: raise PSBTError("Duplicated utxo value") else: - l = compact.read_from(stream) + compact.read_from(stream) + # For PSBTv2, PSBT_IN_OUTPUT_INDEX may follow PSBT_IN_NON_WITNESS_UTXO; + # use the pre-scanned vout (set by read_from) when the field hasn't + # been parsed yet so the OOM protection is key-order independent. + effective_vout = ( + self.vout + if self.vout is not None + else getattr(self, "_prescan_vout", None) + ) # we verified and saved utxo - if self.compress and self.txid and self.vout is not None: - txout, txhash = self.TX_CLS.read_vout(stream, self.vout) + if self.compress and effective_vout is not None: + txout, txhash = self.TX_CLS.read_vout(stream, effective_vout) self._txhash = txhash self._utxo = txout else: @@ -345,13 +414,64 @@ def read_value(self, stream, k): else: raise PSBTError("Duplicated final scriptwitness") + # PSBTv2 fields elif k == b"\x0e": + if version != 2: + raise PSBTError("PSBT_IN_PREVIOUS_TXID not allowed in PSBTv0") + if len(v) != 32: + raise PSBTError("PSBT_IN_PREVIOUS_TXID must be 32 bytes") + if self.txid is not None: + raise PSBTError("Duplicated PSBT_IN_PREVIOUS_TXID") self.txid = bytes(reversed(v)) elif k == b"\x0f": + if version != 2: + raise PSBTError("PSBT_IN_OUTPUT_INDEX not allowed in PSBTv0") + if len(v) != 4: + raise PSBTError("PSBT_IN_OUTPUT_INDEX must be 4 bytes") + if self.vout is not None: + raise PSBTError("Duplicated PSBT_IN_OUTPUT_INDEX") self.vout = int.from_bytes(v, "little") elif k == b"\x10": + if version != 2: + raise PSBTError("PSBT_IN_SEQUENCE not allowed in PSBTv0") + if len(v) != 4: + raise PSBTError("PSBT_IN_SEQUENCE must be 4 bytes") + if self.sequence is not None: + raise PSBTError("Duplicated PSBT_IN_SEQUENCE") self.sequence = int.from_bytes(v, "little") + # PSBT_IN_REQUIRED_TIME_LOCKTIME + elif k == b"\x11": + if version != 2: + raise PSBTError("PSBT_IN_REQUIRED_TIME_LOCKTIME not allowed in PSBTv0") + if self.required_time_locktime is not None: + raise PSBTError("Duplicated required time locktime") + if len(v) != 4: + raise PSBTError("PSBT_IN_REQUIRED_TIME_LOCKTIME must be 4 bytes") + locktime = int.from_bytes(v, "little") + if locktime < LOCKTIME_THRESHOLD: + raise PSBTError( + "Time-based locktime must be >= %d" % LOCKTIME_THRESHOLD + ) + self.required_time_locktime = locktime + + # PSBT_IN_REQUIRED_HEIGHT_LOCKTIME + elif k == b"\x12": + if version != 2: + raise PSBTError( + "PSBT_IN_REQUIRED_HEIGHT_LOCKTIME not allowed in PSBTv0" + ) + if self.required_height_locktime is not None: + raise PSBTError("Duplicated required height locktime") + if len(v) != 4: + raise PSBTError("PSBT_IN_REQUIRED_HEIGHT_LOCKTIME must be 4 bytes") + locktime = int.from_bytes(v, "little") + if locktime >= LOCKTIME_THRESHOLD or locktime == 0: + raise PSBTError( + "Height-based locktime must be > 0 and < %d" % LOCKTIME_THRESHOLD + ) + self.required_height_locktime = locktime + # PSBT_IN_TAP_KEY_SIG elif k[0] == 0x13: # read the taproot key sig @@ -444,6 +564,20 @@ def write_to(self, stream, skip_separator=False, version=None, **kwargs) -> int: r += ser_string(stream, b"\x10") r += ser_string(stream, self.sequence.to_bytes(4, "little")) + # Add required time locktime if present + if self.required_time_locktime is not None: + r += ser_string(stream, b"\x11") + r += ser_string( + stream, self.required_time_locktime.to_bytes(4, "little") + ) + + # Add required height locktime if present + if self.required_height_locktime is not None: + r += ser_string(stream, b"\x12") + r += ser_string( + stream, self.required_height_locktime.to_bytes(4, "little") + ) + # PSBT_IN_TAP_KEY_SIG if self.taproot_key_sig is not None: r += ser_string(stream, b"\x13") @@ -489,6 +623,41 @@ def write_to(self, stream, skip_separator=False, version=None, **kwargs) -> int: r += stream.write(b"\x00") return r + @classmethod + def read_from(cls, stream, compress=CompressMode.KEEP_ALL, vin=None, version=None): + res = cls({}, vin=vin, compress=compress) + # PSBT maps are unordered, so PSBTv2 input maps may provide the output + # index after the non-witness UTXO. Peek only at compact-sized keys and + # skip values to keep compressed parsing independent of key order. + res._prescan_vout = None + if version == 2 and compress and res.vout is None: + try: + start_pos = stream.tell() + while True: + k = read_string(stream) + if len(k) == 0: + break + if k == b"\x0f": + v = read_string(stream) + if len(v) == 4: + res._prescan_vout = int.from_bytes(v, "little") + else: + # Let the real parse raise the precise validation error. + pass + break + skip_string(stream) + stream.seek(start_pos) + except (AttributeError, OSError): + pass # stream not seekable; OOM protection may be unavailable + while True: + key = read_string(stream) + # separator + if len(key) == 0: + break + res.read_value(stream, key, version=version) + del res._prescan_vout + return res + class OutputScope(PSBTScope): def __init__(self, unknown: dict = {}, vout=None, compress=CompressMode.KEEP_ALL): @@ -525,13 +694,15 @@ def update(self, other): self.witness_script = other.witness_script or self.witness_script self.bip32_derivations.update(other.bip32_derivations) self.taproot_bip32_derivations.update(other.taproot_bip32_derivations) - self.taproot_internal_key = other.taproot_internal_key + self.taproot_internal_key = ( + other.taproot_internal_key or self.taproot_internal_key + ) @property def vout(self): return TransactionOutput(self.value, self.script_pubkey) - def read_value(self, stream, k): + def read_value(self, stream, k, version=None): # separator if len(k) == 0: return @@ -562,9 +733,22 @@ def read_value(self, stream, k): else: self.bip32_derivations[pub] = DerivationPath.parse(v) + # PSBTv2 fields elif k == b"\x03": - self.value = int.from_bytes(v, "little") + if version != 2: + raise PSBTError("PSBT_OUT_AMOUNT not allowed in PSBTv0") + if len(v) != 8: + raise PSBTError("PSBT_OUT_AMOUNT must be 8 bytes") + if self.value is not None: + raise PSBTError("Duplicated PSBT_OUT_AMOUNT") + self.value = _signed_from_bytes(v) + if self.value < 0: + raise PSBTError("PSBT_OUT_AMOUNT must be non-negative") elif k == b"\x04": + if version != 2: + raise PSBTError("PSBT_OUT_SCRIPT not allowed in PSBTv0") + if self.script_pubkey is not None: + raise PSBTError("Duplicated PSBT_OUT_SCRIPT") self.script_pubkey = Script(v) # PSBT_OUT_TAP_INTERNAL_KEY @@ -603,7 +787,8 @@ def write_to(self, stream, skip_separator=False, version=None, **kwargs) -> int: if version == 2: if self.value is not None: r += ser_string(stream, b"\x03") - r += ser_string(stream, self.value.to_bytes(8, "little")) + # BIP370: PSBT_OUT_AMOUNT is a signed 64-bit integer + r += ser_string(stream, _signed_to_bytes(self.value, 8)) if self.script_pubkey is not None: r += ser_string(stream, b"\x04") r += self.script_pubkey.write_to(stream) @@ -633,6 +818,23 @@ def write_to(self, stream, skip_separator=False, version=None, **kwargs) -> int: r += stream.write(b"\x00") return r + @classmethod + def read_from(cls, stream, compress=CompressMode.KEEP_ALL, vout=None, version=None): + res = cls({}, vout=vout, compress=compress) + while True: + key = read_string(stream) + # separator + if len(key) == 0: + break + res.read_value(stream, key, version=version) + return res + + +class TxModifiable: + INPUTS = 0b00000001 + OUTPUTS = 0b00000010 + SIGHASH_SINGLE = 0b00000100 + class PSBT(EmbitBase): MAGIC = b"psbt\xff" @@ -641,17 +843,33 @@ class PSBT(EmbitBase): PSBTOUT_CLS = OutputScope TX_CLS = Transaction - def __init__(self, tx=None, unknown={}, version=None): - self.version = version # None for v0 + @classmethod + def _validate_v2_output(cls, out, i): + """Check that a PSBTv2 output has all required fields. + Subclasses (e.g. PSET) may override this to accept alternative fields.""" + if out.value is None: + raise PSBTError( + "PSBTv2 output %d missing required PSBT_OUT_AMOUNT (0x03)" % i + ) + if out.script_pubkey is None: + raise PSBTError( + "PSBTv2 output %d missing required PSBT_OUT_SCRIPT (0x04)" % i + ) + + def __init__(self, tx=None, unknown=None, version=None): + self.version = version # None for v0, 2 for v2 self.inputs = [] self.outputs = [] self.tx_version = None self.locktime = None + self.tx_modifiable_flags = None + self._raw_input_count_from_global = None + self._raw_output_count_from_global = None if tx is not None: self.parse_tx(tx) - self.unknown = unknown + self.unknown = {} if unknown is None else unknown self.xpubs = OrderedDict() self.parse_unknowns() @@ -661,11 +879,86 @@ def parse_tx(self, tx): self.inputs = [self.PSBTIN_CLS(vin=vin) for vin in tx.vin] self.outputs = [self.PSBTOUT_CLS(vout=vout) for vout in tx.vout] + @staticmethod + def _classify_locktimes(inputs): + """Classify inputs by locktime requirement type. + + Returns a tuple of (height_locktimes, time_locktimes, inputs_with_requirements) + where the lists contain the respective locktime values and the count is the + number of inputs that express at least one locktime requirement. + """ + height_locktimes = [] + time_locktimes = [] + inputs_with_requirements = 0 + for inp in inputs: + has_requirement = False + if inp.required_height_locktime is not None: + height_locktimes.append(inp.required_height_locktime) + has_requirement = True + if inp.required_time_locktime is not None: + time_locktimes.append(inp.required_time_locktime) + has_requirement = True + if has_requirement: + inputs_with_requirements += 1 + return height_locktimes, time_locktimes, inputs_with_requirements + + def determine_locktime(self): + """ + Determines the appropriate locktime according to PSBTv2 rules. + Returns the locktime that should be used for the transaction. + """ + if self.version != 2: + return self.locktime or 0 + + height_locktimes, time_locktimes, inputs_with_locktime_requirements = ( + self._classify_locktimes(self.inputs) + ) + + # If no inputs have locktime requirements, use fallback + if inputs_with_locktime_requirements == 0: + return self.locktime or 0 + + # height preferred, if ALL inputs with requirements support it + if len(height_locktimes) == inputs_with_locktime_requirements: + return max(height_locktimes) + + # fall back to time if ALL inputs with requirements support it + if len(time_locktimes) == inputs_with_locktime_requirements: + return max(time_locktimes) + + raise PSBTError( + "Cannot determine locktime: inputs have conflicting height and time locktime requirements" + ) + + def _validate_locktime_compatibility(self, inputs): + """Verify that the given input list has mutually compatible locktime requirements.""" + height_locktimes, time_locktimes, inputs_with_requirements = ( + self._classify_locktimes(inputs) + ) + if inputs_with_requirements == 0: + return + if ( + len(height_locktimes) == inputs_with_requirements + or len(time_locktimes) == inputs_with_requirements + ): + return + raise PSBTError( + "Input locktime requirements are incompatible with existing inputs" + ) + @property def tx(self): + if self.version == 2: + locktime = self.determine_locktime() + else: + locktime = self.locktime or 0 + + if self.version == 2 and self.tx_version is None: + raise PSBTError("PSBTv2 is missing required PSBT_GLOBAL_TX_VERSION") + tx_version = self.tx_version if self.tx_version is not None else 2 return self.TX_CLS( - version=self.tx_version or 2, - locktime=self.locktime or 0, + version=tx_version, + locktime=locktime, vin=[inp.vin for inp in self.inputs], vout=[out.vout for out in self.outputs], ) @@ -703,6 +996,9 @@ def fee(self): fee -= sum([out.value for out in self.tx.vout]) return fee + def _write_extra_globals(self, stream) -> int: + return 0 + def write_to(self, stream) -> int: # magic bytes r = stream.write(self.MAGIC) @@ -718,9 +1014,13 @@ def write_to(self, stream) -> int: r += ser_string(stream, self.xpubs[xpub].serialize()) if self.version == 2: - if self.tx_version is not None: - r += ser_string(stream, b"\x02") - r += ser_string(stream, self.tx_version.to_bytes(4, "little")) + if self.tx_version is None: + raise PSBTError( + "Cannot serialize PSBTv2: missing required PSBT_GLOBAL_TX_VERSION" + ) + r += ser_string(stream, b"\x02") + # BIP370: PSBT_GLOBAL_TX_VERSION is a signed int32 + r += ser_string(stream, _signed_to_bytes(self.tx_version, 4)) if self.locktime is not None: r += ser_string(stream, b"\x03") r += ser_string(stream, self.locktime.to_bytes(4, "little")) @@ -728,8 +1028,13 @@ def write_to(self, stream) -> int: r += ser_string(stream, compact.to_bytes(len(self.inputs))) r += ser_string(stream, b"\x05") r += ser_string(stream, compact.to_bytes(len(self.outputs))) + if self.tx_modifiable_flags is not None: + r += ser_string(stream, b"\x06") + r += ser_string(stream, bytes([self.tx_modifiable_flags])) r += ser_string(stream, b"\xfb") r += ser_string(stream, self.version.to_bytes(4, "little")) + + r += self._write_extra_globals(stream) # unknown for key in self.unknown: r += ser_string(stream, key) @@ -772,72 +1077,190 @@ def read_from(cls, stream, compress=CompressMode.KEEP_ALL): without storing them in memory and save the utxo internally for signing. This helps against out-of-memory errors. """ - tx = None - unknown = {} - version = None - # check magic if stream.read(len(cls.MAGIC)) != cls.MAGIC: raise PSBTError("Invalid PSBT magic") + + global_kvs = OrderedDict() while True: key = read_string(stream) - # separator - if len(key) == 0: + if len(key) == 0: # Separator break value = read_string(stream) - # tx - if key == b"\x00": - if tx is None: - tx = cls.TX_CLS.parse(value) - else: + if key in global_kvs: + raise PSBTError("Duplicated global key: %s" % hexlify(key).decode()) + global_kvs[key] = value + + # Determine PSBT version from PSBT_GLOBAL_VERSION (0xfb) + version = None + if b"\xfb" in global_kvs: + if len(global_kvs[b"\xfb"]) != 4: + raise PSBTError("PSBT_GLOBAL_VERSION must be 4 bytes") + parsed_version = int.from_bytes(global_kvs[b"\xfb"], "little") + if parsed_version == 2: + version = 2 + elif parsed_version == 0: + version = ( + None # explicit PSBT_GLOBAL_VERSION=0 is valid for v0 per BIP174 + ) + else: + raise PSBTError( + "Unsupported PSBT_GLOBAL_VERSION value: %d" % parsed_version + ) + + if version == 2: # PSBTv2 + if b"\x00" in global_kvs: # PSBT_GLOBAL_UNSIGNED_TX + raise PSBTError("PSBT_GLOBAL_UNSIGNED_TX is not allowed in PSBTv2") + # Pass all global KVs to unknown; __init__ calls parse_unknowns. + psbt = cls(tx=None, unknown=global_kvs, version=version) + + # Validate that input/output counts were processed by parse_unknowns + if psbt._raw_input_count_from_global is None: + raise PSBTError( + "PSBTv2 missing or invalid PSBT_GLOBAL_INPUT_COUNT (0x04)" + ) + if psbt._raw_output_count_from_global is None: + raise PSBTError( + "PSBTv2 missing or invalid PSBT_GLOBAL_OUTPUT_COUNT (0x05)" + ) + if psbt.tx_version is None: + raise PSBTError("PSBTv2 missing required PSBT_GLOBAL_TX_VERSION (0x02)") + else: # PSBTv0 (version is None or 0) + if b"\x00" not in global_kvs: + raise PSBTError("PSBT_GLOBAL_UNSIGNED_TX (0x00) is required for PSBTv0") + + # Check for forbidden v2-only global keys in v0 context. + # PSBT_GLOBAL_VERSION (0xfb) with value 0 is explicitly permitted by BIP174 + # and was already validated above, so it is excluded from this set. + v2_only_global_keys = {b"\x02", b"\x03", b"\x04", b"\x05", b"\x06"} + for k_v2_only in v2_only_global_keys: + if k_v2_only in global_kvs: raise PSBTError( - "Failed to parse PSBT - duplicated transaction field" + "Global key %s is not allowed in PSBTv0" + % hexlify(k_v2_only).decode() ) - elif key == b"\xfb": - version = int.from_bytes(value, "little") - else: - if key in unknown: - raise PSBTError("Duplicated key") - unknown[key] = value - - if tx and version == 2: - raise PSBTError("Global TX field is not allowed in PSBTv2") - psbt = cls(tx, unknown, version=version) - # input scopes - for i, vin in enumerate(psbt.tx.vin): - psbt.inputs[i] = cls.PSBTIN_CLS.read_from( - stream, compress=compress, vin=vin - ) - # output scopes - for i, vout in enumerate(psbt.tx.vout): - psbt.outputs[i] = cls.PSBTOUT_CLS.read_from( - stream, compress=compress, vout=vout - ) + + tx_bytes = global_kvs.pop(b"\x00") # Remove so it's not in unknown + tx_for_v0 = cls.TX_CLS.parse(tx_bytes) + psbt = cls(tx=tx_for_v0, unknown=global_kvs, version=version) + + if version == 2: + num_inputs = psbt._raw_input_count_from_global + num_outputs = psbt._raw_output_count_from_global + + parsed_inputs = [] + for _ in range(num_inputs): + parsed_inputs.append( + cls.PSBTIN_CLS.read_from( + stream, + compress=compress, + vin=None, + version=version, + ) + ) + psbt.inputs = parsed_inputs + + for i, inp in enumerate(psbt.inputs): + if inp.txid is None: + raise PSBTError( + "PSBTv2 input %d missing required " + "PSBT_IN_PREVIOUS_TXID (0x0e)" % i + ) + if inp.vout is None: + raise PSBTError( + "PSBTv2 input %d missing required " + "PSBT_IN_OUTPUT_INDEX (0x0f)" % i + ) + + parsed_outputs = [] + for _ in range(num_outputs): + parsed_outputs.append( + cls.PSBTOUT_CLS.read_from( + stream, + compress=compress, + vout=None, + version=version, + ) + ) + psbt.outputs = parsed_outputs + + for i, out in enumerate(psbt.outputs): + cls._validate_v2_output(out, i) + else: + temp_inputs = [] + for i in range(len(psbt.inputs)): + temp_inputs.append( + cls.PSBTIN_CLS.read_from( + stream, + compress=compress, + vin=psbt.inputs[i].vin, + version=version, + ) + ) + psbt.inputs = temp_inputs + + temp_outputs = [] + for i in range(len(psbt.outputs)): + temp_outputs.append( + cls.PSBTOUT_CLS.read_from( + stream, + compress=compress, + vout=psbt.outputs[i].vout, + version=version, + ) + ) + psbt.outputs = temp_outputs + return psbt def parse_unknowns(self): + # Handle PSBT_GLOBAL_VERSION first + if b"\xfb" in self.unknown: + self.unknown.pop(b"\xfb", None) + + if b"\x06" in self.unknown: + flags_bytes = self.unknown.pop(b"\x06") + if len(flags_bytes) != 1: + raise PSBTError("PSBT_GLOBAL_TX_MODIFIABLE must be 1 byte") + self.tx_modifiable_flags = flags_bytes[0] + for k in list(self.unknown): # xpub field if k[0] == 0x01: xpub = bip32.HDKey.parse(k[1:]) self.xpubs[xpub] = DerivationPath.parse(self.unknown.pop(k)) elif k == b"\x02": - self.tx_version = int.from_bytes(self.unknown.pop(k), "little") + if self.version == 2: + v = self.unknown.pop(k) + if len(v) != 4: + raise PSBTError("PSBT_GLOBAL_TX_VERSION must be 4 bytes") + # BIP370: PSBT_GLOBAL_TX_VERSION is a signed int32 + self.tx_version = _signed_from_bytes(v) elif k == b"\x03": - self.locktime = int.from_bytes(self.unknown.pop(k), "little") + if self.version == 2: + v = self.unknown.pop(k) + if len(v) != 4: + raise PSBTError("PSBT_GLOBAL_FALLBACK_LOCKTIME must be 4 bytes") + self.locktime = int.from_bytes(v, "little") elif k == b"\x04": - if len(self.inputs) > 0: - raise PSBTError("Inputs already initialized") - self.inputs = [ - self.PSBTIN_CLS() - for _ in range(compact.from_bytes(self.unknown.pop(k))) - ] + if self.version == 2: + if self._raw_input_count_from_global is not None: + self.unknown.pop(k, None) + continue + self._raw_input_count_from_global = compact.from_bytes( + self.unknown.pop(k) + ) + # Store count only; do not pre-allocate objects to avoid + # attacker-controlled memory exhaustion. elif k == b"\x05": - if len(self.outputs) > 0: - raise PSBTError("Outputs already initialized") - self.outputs = [ - self.PSBTOUT_CLS() - for _ in range(compact.from_bytes(self.unknown.pop(k))) - ] + if self.version == 2: + if self._raw_output_count_from_global is not None: + self.unknown.pop(k, None) + continue + self._raw_output_count_from_global = compact.from_bytes( + self.unknown.pop(k) + ) + # Store count only; do not pre-allocate objects to avoid + # attacker-controlled memory exhaustion. def sighash(self, i, sighash=SIGHASH.ALL, **kwargs): inp = self.inputs[i] @@ -948,19 +1371,9 @@ def sign_with(self, root, sighash=SIGHASH.DEFAULT) -> int: counter += self.sign_with(k, sighash) return counter - # if WIF - fingerprint is None - fingerprint = None - # if descriptor key - if hasattr(root, "origin"): - if not root.is_private: # pubkey can't sign - return 0 - if root.is_extended: # use fingerprint only for HDKey - fingerprint = root.fingerprint - else: - root = root.key # WIF key - # if HDKey - if not fingerprint and hasattr(root, "my_fingerprint"): - fingerprint = root.my_fingerprint + fingerprint, can_sign, root = resolve_signing_root(root) + if not can_sign: + return 0 rootpub = root.get_public_key() sec = rootpub.sec() @@ -996,7 +1409,7 @@ def sign_with(self, root, sighash=SIGHASH.DEFAULT) -> int: if fingerprint: # if taproot derivations are present add them for pub in inp.taproot_bip32_derivations: - (_leafs, derivation) = inp.taproot_bip32_derivations[pub] + _leafs, derivation = inp.taproot_bip32_derivations[pub] if derivation.fingerprint == fingerprint: # Add only if not already present if (pub, derivation) not in bip32_derivations: @@ -1012,17 +1425,10 @@ def sign_with(self, root, sighash=SIGHASH.DEFAULT) -> int: # get derived keys for signing derived_keypairs = OrderedDict() # (prv, pub) for pub, derivation in bip32_derivations: - der = derivation.derivation - # descriptor key has origin derivation that we take into account - if hasattr(root, "origin"): - if root.origin: - if root.origin.derivation != der[: len(root.origin.derivation)]: - # derivation doesn't match - go to next input - continue - der = der[len(root.origin.derivation) :] - hdkey = root.key.derive(der) - else: - hdkey = root.derive(der) + hdkey = derive_hdkey(root, derivation) + if hdkey is None: + # derivation doesn't match - go to next candidate + continue if hdkey.xonly() != pub.xonly(): raise PSBTError("Derivation path doesn't look right") @@ -1034,7 +1440,7 @@ def sign_with(self, root, sighash=SIGHASH.DEFAULT) -> int: if inp.is_taproot: # try to sign with individual private key (WIF) # or with root without derivations - counter += self.sign_input_with_tapkey( + tap_sigs = self.sign_input_with_tapkey( root, i, inp, @@ -1042,12 +1448,15 @@ def sign_with(self, root, sighash=SIGHASH.DEFAULT) -> int: ) # sign with all derived keys for prv, pub in derived_keypairs: - counter += self.sign_input_with_tapkey( + tap_sigs += self.sign_input_with_tapkey( prv, i, inp, sighash=inp_sighash, ) + if tap_sigs > 0: + counter += tap_sigs + self._update_tx_modifiable(inp_sighash) continue # hash can be reused @@ -1057,13 +1466,102 @@ def sign_with(self, root, sighash=SIGHASH.DEFAULT) -> int: # check if root itself is included in the script if sec in sc.data or pkh in sc.data: sig = root.sign(h) - # sig plus sighash flag inp.partial_sigs[rootpub] = sig.serialize() + bytes([inp_sighash]) counter += 1 + self._update_tx_modifiable(inp_sighash) for prv, pub in derived_keypairs: sig = prv.sign(h) - # sig plus sighash flag inp.partial_sigs[pub] = sig.serialize() + bytes([inp_sighash]) counter += 1 + self._update_tx_modifiable(inp_sighash) + return counter + + def _update_tx_modifiable(self, inp_sighash: int) -> None: + """Update PSBT_GLOBAL_TX_MODIFIABLE after a signature is added.""" + + if self.version != 2: + return + + is_anyonecanpay = bool(inp_sighash & SIGHASH.ANYONECANPAY) + sighash_type = inp_sighash & 0x1F + + if self.tx_modifiable_flags is None: + self.tx_modifiable_flags = 0 + + if not is_anyonecanpay: + self.tx_modifiable_flags &= ~TxModifiable.INPUTS + if sighash_type != SIGHASH.NONE: + self.tx_modifiable_flags &= ~TxModifiable.OUTPUTS + if sighash_type == SIGHASH.SINGLE: + self.tx_modifiable_flags |= TxModifiable.SIGHASH_SINGLE + + def get_tx_modifiable(self): + return self.tx_modifiable_flags + + def set_tx_modifiable(self, flags): + if self.version != 2: + raise PSBTError("GLOBAL_TX_MODIFIABLE only supported in PSBTv2") + self.tx_modifiable_flags = flags + + def is_inputs_modifiable(self): + if self.version != 2: + return True + if self.tx_modifiable_flags is None: + return False + return bool(self.tx_modifiable_flags & TxModifiable.INPUTS) + + def is_outputs_modifiable(self): + if self.version != 2: + return True + if self.tx_modifiable_flags is None: + return False + return bool(self.tx_modifiable_flags & TxModifiable.OUTPUTS) + + def has_sighash_single(self): + if self.version != 2 or self.tx_modifiable_flags is None: + return False + return bool(self.tx_modifiable_flags & TxModifiable.SIGHASH_SINGLE) + + @classmethod + def create_v2(cls, tx_version=2, fallback_locktime=None, tx_modifiable=None): + """Creator role convenience factory: initialise an empty PSBTv2. + + BIP-370 Creator role: set PSBT_GLOBAL_VERSION=2, PSBT_GLOBAL_TX_VERSION, + and optionally PSBT_GLOBAL_FALLBACK_LOCKTIME and PSBT_GLOBAL_TX_MODIFIABLE + so the Constructor role can immediately begin adding inputs and outputs. + """ + if tx_modifiable is None: + tx_modifiable = TxModifiable.INPUTS | TxModifiable.OUTPUTS + psbt = cls(tx=None, version=2) + psbt.tx_version = tx_version + psbt.locktime = fallback_locktime + psbt.tx_modifiable_flags = tx_modifiable + return psbt + + def add_input(self, input_scope): + if not self.is_inputs_modifiable(): + raise PSBTError("Inputs are not modifiable") + if self.version == 2: + if input_scope.txid is None: + raise PSBTError("PSBTv2 input must have PSBT_IN_PREVIOUS_TXID") + if input_scope.vout is None: + raise PSBTError("PSBTv2 input must have PSBT_IN_OUTPUT_INDEX") + if ( + input_scope.required_height_locktime is not None + or input_scope.required_time_locktime is not None + ): + self._validate_locktime_compatibility(self.inputs + [input_scope]) + self.inputs.append(input_scope) + if self.version == 2: + self._raw_input_count_from_global = len(self.inputs) + + def add_output(self, output_scope): + if not self.is_outputs_modifiable(): + raise PSBTError("Outputs are not modifiable") + if self.version == 2: + self._validate_v2_output(output_scope, len(self.outputs)) + self.outputs.append(output_scope) + if self.version == 2: + self._raw_output_count_from_global = len(self.outputs) diff --git a/src/embit/psbtview.py b/src/embit/psbtview.py index c66bcb5b..aa24f8cb 100644 --- a/src/embit/psbtview.py +++ b/src/embit/psbtview.py @@ -15,6 +15,7 @@ """ # TODO: refactor, a lot of code is duplicated here from transaction.py from collections import OrderedDict +from binascii import hexlify import hashlib from . import compact from . import ec @@ -26,6 +27,8 @@ CompressMode, InputScope, OutputScope, + TxModifiable, + LOCKTIME_THRESHOLD, read_string, ser_string, skip_string, @@ -36,6 +39,8 @@ SIGHASH, hash_amounts, hash_script_pubkeys, + _signed_from_bytes, + _signed_to_bytes, ) @@ -88,7 +93,8 @@ def __init__(self, stream, offset): def version(self): if self._version is None: self.stream.seek(self.offset) - self._version = int.from_bytes(self.stream.read(4), "little") + # tx version is a signed int32, matching Transaction.read_from + self._version = _signed_from_bytes(self.stream.read(4)) return self._version @property @@ -180,6 +186,7 @@ def __init__( version=None, tx_offset=None, compress=CompressMode.KEEP_ALL, + global_kvs=None, ): if version != 2 and tx_offset is None: raise PSBTError("Global tx is not found, but PSBT version is %d" % version) @@ -196,8 +203,35 @@ def __init__( self.compress = compress self._tx_version = self.tx.version if self.tx else None self._locktime = self.tx.locktime if self.tx else None + # For PSBTv2: dict of all global key→value pairs (excluding the \x00 global tx). + # This is the single source of truth for every global field, including + # PSBT_GLOBAL_TX_MODIFIABLE (\x06). None for PSBTv0 where the global + # scope is streamed verbatim. + self._global_kvs = global_kvs self.clear_cache() + @property + def tx_modifiable_flags(self): + """PSBT_GLOBAL_TX_MODIFIABLE byte (PSBTv2 only). None when absent or for PSBTv0.""" + if self._global_kvs is None: + return None + v = self._global_kvs.get(b"\x06") + if v is None: + return None + if len(v) != 1: + raise PSBTError("Invalid PSBT_GLOBAL_TX_MODIFIABLE length") + return int.from_bytes(v, "little") + + @tx_modifiable_flags.setter + def tx_modifiable_flags(self, value): + """Setting to None removes the field; ignored for PSBTv0.""" + if self._global_kvs is None: + return + if value is None: + self._global_kvs.pop(b"\x06", None) + else: + self._global_kvs[b"\x06"] = bytes([value]) + def clear_cache(self): # cache for digests self._hash_prevouts = None @@ -222,6 +256,9 @@ def view(cls, stream, offset=None, compress=CompressMode.KEEP_ALL): num_inputs = None num_outputs = None tx_offset = None + # Collect all non-global-tx key-value pairs for PSBTv2 global scope rewriting. + # The global scope is small, so materialising it avoids byte-level injection. + global_kvs = {} while True: # read key and update cursor key = read_string(stream) @@ -229,19 +266,12 @@ def view(cls, stream, offset=None, compress=CompressMode.KEEP_ALL): # separator if len(key) == 0: break - if key in [b"\xfb", b"\x04", b"\x05"]: - value = read_string(stream) - cur += len(value) + len(compact.to_bytes(len(value))) - if key == b"\xfb": - version = int.from_bytes(value, "little") - elif key == b"\x04": - num_inputs = compact.from_bytes(value) - elif key == b"\x05": - num_outputs = compact.from_bytes(value) - elif key == b"\x00": - # we found global transaction - if version == 2: - raise PSBTError("Global transaction with version 2 PSBT") + if key == b"\x00": + # we found global transaction; defer version==2 check until after the loop + # so that PSBT_GLOBAL_UNSIGNED_TX is rejected even when it appears before + # PSBT_GLOBAL_VERSION (key order is not guaranteed). + if tx_offset is not None: + raise PSBTError("Duplicate global transaction") if (num_inputs is not None) or (num_outputs is not None): raise PSBTError("Invalid global transaction") tx_len = compact.read_from(stream) @@ -254,8 +284,33 @@ def view(cls, stream, offset=None, compress=CompressMode.KEEP_ALL): stream.seek(tx_offset + tx_len) cur += tx_len else: - cur += skip_string(stream) + value = read_string(stream) + cur += len(value) + len(compact.to_bytes(len(value))) + if key in global_kvs: + raise PSBTError("Duplicate global key: %s" % key) + global_kvs[key] = value + if key == b"\xfb": + if len(value) != 4: + raise PSBTError("PSBT_GLOBAL_VERSION must be 4 bytes") + version = int.from_bytes(value, "little") + elif key == b"\x04": + num_inputs = compact.from_bytes(value) + elif key == b"\x05": + num_outputs = compact.from_bytes(value) first_scope = cur + if version not in (None, 0, 2): + raise PSBTError("Unsupported PSBT_GLOBAL_VERSION value: %d" % version) + if version == 0: + version = None + # PSBTv2 must not have a global unsigned transaction, regardless of key order + if tx_offset is not None and version == 2: + raise PSBTError("Global transaction with version 2 PSBT") + if version != 2: + for k in {b"\x02", b"\x03", b"\x04", b"\x05", b"\x06"}: + if k in global_kvs: + raise PSBTError( + "Global key %s is not allowed in PSBTv0" % hexlify(k).decode() + ) if None in [version or tx_offset, num_inputs, num_outputs]: raise PSBTError("Missing something important in PSBT") return cls( @@ -267,6 +322,7 @@ def view(cls, stream, offset=None, compress=CompressMode.KEEP_ALL): version, tx_offset, compress, + global_kvs=global_kvs if version == 2 else None, ) def _skip_scope(self): @@ -313,7 +369,9 @@ def input(self, i, compress=None): raise PSBTError("Invalid input index") vin = self.tx.vin(i) if self.tx else None self.seek_to_scope(i) - return self.PSBTIN_CLS.read_from(self.stream, vin=vin, compress=compress) + return self.PSBTIN_CLS.read_from( + self.stream, vin=vin, compress=compress, version=self.version + ) def output(self, i, compress=None): """Reads, parses and returns PSBT OutputScope #i""" @@ -323,7 +381,9 @@ def output(self, i, compress=None): raise PSBTError("Invalid output index") vout = self.tx.vout(i) if self.tx else None self.seek_to_scope(self.num_inputs + i) - return self.PSBTOUT_CLS.read_from(self.stream, vout=vout, compress=compress) + return self.PSBTOUT_CLS.read_from( + self.stream, vout=vout, compress=compress, version=self.version + ) # compress is not used here, but may be used by subclasses (liquid) def vin(self, i, compress=None): @@ -341,7 +401,7 @@ def vin(self, i, compress=None): vout = int.from_bytes(v, "little") self.seek_to_scope(i) - v = self.get_value(b"\x10", from_current=True) or b"\xFF\xFF\xFF\xFF" + v = self.get_value(b"\x10", from_current=True) or b"\xff\xff\xff\xff" sequence = int.from_bytes(v, "little") return TransactionInput(txid, vout, sequence=sequence) @@ -355,7 +415,7 @@ def vout(self, i, compress=None): self.seek_to_scope(self.num_inputs + i) v = self.get_value(b"\x03", from_current=True) - value = int.from_bytes(v, "little") + value = _signed_from_bytes(v) self.seek_to_scope(self.num_inputs + i) v = self.get_value(b"\x04", from_current=True) @@ -366,15 +426,93 @@ def vout(self, i, compress=None): @property def locktime(self): if self._locktime is None: - v = self.get_value(b"\x03") - self._locktime = int.from_bytes(v, "little") if v is not None else 0 + if self.version == 2: + self._locktime = self._determine_locktime_v2() + else: + v = self.get_value(b"\x03") + self._locktime = int.from_bytes(v, "little") if v is not None else 0 return self._locktime + def _determine_locktime_v2(self): + """BIP370 locktime determination for PSBTv2. + + Derives the transaction locktime from per-input required locktime fields. + Falls back to PSBT_GLOBAL_FALLBACK_LOCKTIME (or 0) when no input imposes + a requirement. + """ + v = self.get_value(b"\x03") + if v is not None: + if len(v) != 4: + raise PSBTError("PSBT_GLOBAL_FALLBACK_LOCKTIME must be 4 bytes") + fallback = int.from_bytes(v, "little") + else: + fallback = 0 + + height_locktimes = [] + time_locktimes = [] + inputs_with_requirements = 0 + + for i in range(self.num_inputs): + self.seek_to_scope(i) + v_height = self.get_value(b"\x12", from_current=True) + self.seek_to_scope(i) + v_time = self.get_value(b"\x11", from_current=True) + + has_requirement = False + if v_height is not None: + if len(v_height) != 4: + raise PSBTError("PSBT_IN_REQUIRED_HEIGHT_LOCKTIME must be 4 bytes") + height_locktime = int.from_bytes(v_height, "little") + if height_locktime == 0 or height_locktime >= LOCKTIME_THRESHOLD: + raise PSBTError( + "Height-based locktime must be > 0 and < %d" + % LOCKTIME_THRESHOLD + ) + height_locktimes.append(height_locktime) + has_requirement = True + if v_time is not None: + if len(v_time) != 4: + raise PSBTError("PSBT_IN_REQUIRED_TIME_LOCKTIME must be 4 bytes") + time_locktime = int.from_bytes(v_time, "little") + if time_locktime < LOCKTIME_THRESHOLD: + raise PSBTError( + "Time-based locktime must be >= %d" % LOCKTIME_THRESHOLD + ) + time_locktimes.append(time_locktime) + has_requirement = True + if has_requirement: + inputs_with_requirements += 1 + + if inputs_with_requirements == 0: + return fallback + + inputs_supporting_height = len(height_locktimes) + inputs_supporting_time = len(time_locktimes) + + # Prefer height-based locktime if every input with requirements supports it + if inputs_supporting_height == inputs_with_requirements: + return max(height_locktimes) + # Fall back to time-based locktime if every input with requirements supports it + if inputs_supporting_time == inputs_with_requirements: + return max(time_locktimes) + + raise PSBTError( + "Cannot determine locktime: inputs have conflicting height and time locktime requirements" + ) + @property def tx_version(self): if self._tx_version is None: v = self.get_value(b"\x02") - self._tx_version = int.from_bytes(v, "little") if v is not None else 0 + if v is None: + if self.version == 2: + raise PSBTError("Missing PSBT_GLOBAL_TX_VERSION in PSBTv2") + self._tx_version = 0 + else: + if len(v) != 4: + raise PSBTError("Invalid PSBT_GLOBAL_TX_VERSION length") + # BIP370: PSBT_GLOBAL_TX_VERSION is a signed int32 + self._tx_version = _signed_from_bytes(v) return self._tx_version def seek_to_value(self, key_start, from_current=False): @@ -464,7 +602,7 @@ def sighash_taproot( sh, anyonecanpay = SIGHASH.check(sighash) h = hashes.tagged_hash_init("TapSighash", b"\x00") h.update(bytes([sighash])) - h.update(self.tx_version.to_bytes(4, "little")) + h.update(_signed_to_bytes(self.tx_version, 4)) h.update(self.locktime.to_bytes(4, "little")) if not anyonecanpay: h.update(self.hash_prevouts()) @@ -512,7 +650,7 @@ def sighash_segwit(self, input_index, script_pubkey, value, sighash=SIGHASH.ALL) inp = self.vin(input_index) zero = b"\x00" * 32 # for sighashes h = hashlib.sha256() - h.update(self.tx_version.to_bytes(4, "little")) + h.update(_signed_to_bytes(self.tx_version, 4)) if anyonecanpay: h.update(zero) else: @@ -552,7 +690,7 @@ def sighash_legacy(self, input_index, script_pubkey, sighash=SIGHASH.ALL): return b"\x00" * 31 + b"\x01" h = hashlib.sha256() - h.update(self.tx_version.to_bytes(4, "little")) + h.update(_signed_to_bytes(self.tx_version, 4)) # ANYONECANPAY - only one input is serialized if anyonecanpay: h.update(compact.to_bytes(1)) @@ -686,6 +824,23 @@ def sign_input_with_tapkey( counter += 1 return counter + def _update_tx_modifiable(self, inp_sighash: int) -> None: + if self.version != 2: + return + + is_anyonecanpay = bool(inp_sighash & SIGHASH.ANYONECANPAY) + sighash_type = inp_sighash & 0x1F + + if self.tx_modifiable_flags is None: + self.tx_modifiable_flags = 0 + + if not is_anyonecanpay: + self.tx_modifiable_flags &= ~TxModifiable.INPUTS + if sighash_type != SIGHASH.NONE: + self.tx_modifiable_flags &= ~TxModifiable.OUTPUTS + if sighash_type == SIGHASH.SINGLE: + self.tx_modifiable_flags |= TxModifiable.SIGHASH_SINGLE + def sign_input( self, i, root, sig_stream, sighash=SIGHASH.DEFAULT, extra_scope_data=None ) -> int: @@ -809,6 +964,8 @@ def sign_input( for pub, leaf in inp.taproot_sigs: ser_string(sig_stream, b"\x14" + pub.xonly() + leaf) ser_string(sig_stream, inp.taproot_sigs[(pub, leaf)]) + if counter > 0: + self._update_tx_modifiable(inp_sighash) return counter h = self.sighash(i, sighash=inp_sighash, input_scope=inp) @@ -820,11 +977,13 @@ def sign_input( # sig plus sighash flag inp.partial_sigs[rootpub] = sig.serialize() + bytes([inp_sighash]) counter += 1 + self._update_tx_modifiable(inp_sighash) for prv, pub in derived_keypairs: sig = prv.sign(h) # sig plus sighash flag inp.partial_sigs[pub] = sig.serialize() + bytes([inp_sighash]) counter += 1 + self._update_tx_modifiable(inp_sighash) for pub in inp.partial_sigs: ser_string(sig_stream, b"\x02" + pub.serialize()) ser_string(sig_stream, inp.partial_sigs[pub]) @@ -852,6 +1011,11 @@ def sign_with(self, root, sig_stream, sighash=SIGHASH.DEFAULT) -> int: counter += self.sign_input(i, root, sig_stream, sighash=sighash) # add separator sig_stream.write(b"\x00") + # BIP-370: signer must set PSBT_GLOBAL_TX_MODIFIABLE for PSBTv2. + # Default to 0 (nothing modifiable) when no signing occurred and the + # field was absent — avoids leaving the field missing after a sign pass. + if self.version == 2 and self.tx_modifiable_flags is None: + self.tx_modifiable_flags = 0 return counter def write_to( @@ -875,15 +1039,29 @@ def write_to( compress = self.compress # first we write global scope - self.stream.seek(self.offset) - res = read_write(self.stream, writable_stream, self.first_scope - self.offset) + if self._global_kvs is not None: + # PSBTv2: reconstruct global scope from the materialised key-value dict. + # This sidesteps byte-level injection and gives deterministic key ordering. + writable_stream.write(self.MAGIC) + res = len(self.MAGIC) + for k in sorted(self._global_kvs.keys()): + res += ser_string(writable_stream, k) + res += ser_string(writable_stream, self._global_kvs[k]) + writable_stream.write(b"\x00") # global scope separator + res += 1 + else: + # PSBTv0: stream global scope directly from source (includes global tx). + self.stream.seek(self.offset) + res = read_write( + self.stream, writable_stream, self.first_scope - self.offset + ) # write all inputs for i in range(self.num_inputs): inp = self.input(i) # add extra data from extra input streams for s in extra_input_streams: - extra = InputScope.read_from(s) + extra = InputScope.read_from(s, version=self.version) inp.update(extra) if compress: inp.clear_metadata(compress=compress) @@ -894,7 +1072,7 @@ def write_to( out = self.output(i) # add extra data from extra input streams for s in extra_output_streams: - extra = OutputScope.read_from(s) + extra = OutputScope.read_from(s, version=self.version) out.update(extra) if compress: out.clear_metadata(compress=compress) diff --git a/src/embit/silent_payments/__init__.py b/src/embit/silent_payments/__init__.py new file mode 100644 index 00000000..67bce7a2 --- /dev/null +++ b/src/embit/silent_payments/__init__.py @@ -0,0 +1,21 @@ +from . import dleq +from .psbt import SilentPaymentsPSBT +from .sp import ( + get_eligible_inputs, + encode_silent_payment_address, + decode_silent_payment_address, + generate_silent_payment_address, +) +from .psbt import SPValidationError, SPFieldError, SilentPaymentData + +__all__ = [ + "dleq", + "SilentPaymentsPSBT", + "get_eligible_inputs", + "encode_silent_payment_address", + "decode_silent_payment_address", + "generate_silent_payment_address", + "SPValidationError", + "SPFieldError", + "SilentPaymentData", +] diff --git a/src/embit/silent_payments/dleq.py b/src/embit/silent_payments/dleq.py new file mode 100644 index 00000000..cad3bcd0 --- /dev/null +++ b/src/embit/silent_payments/dleq.py @@ -0,0 +1,179 @@ +""" +BIP-374 DLEQ (Discrete Log Equality Proofs). + +Given points A = a*G and C = a*B, proves knowledge of scalar a +across both bases (G and B) without revealing a. + +Spec: https://github.com/bitcoin/bips/blob/master/bip-0374.mediawiki +Reference: https://github.com/bitcoin/bips/blob/master/bip-0374/reference.py +""" + +from .. import hashes +from ..util.key import SECP256K1_ORDER, SECP256K1_G +from ..util.secp256k1 import ( + ec_pubkey_create, + ec_pubkey_parse, + ec_pubkey_serialize, + ec_pubkey_tweak_mul, + ec_pubkey_combine, + EC_COMPRESSED, +) + +_G_COMPRESSED = b"\x02" + SECP256K1_G[0].to_bytes(32, "big") +DLEQ_TAG_AUX = "BIP0374/aux" +DLEQ_TAG_NONCE = "BIP0374/nonce" +DLEQ_TAG_CHALLENGE = "BIP0374/challenge" + + +class DLEQError(Exception): + """Raised when DLEQ proof generation fails due to invalid inputs.""" + + +def _point_mul(scalar_bytes, base_sec): + """scalar * base → opaque pubkey handle. Fast path for secp256k1 generator G.""" + if base_sec == _G_COMPRESSED: + return ec_pubkey_create(scalar_bytes) + pub = bytearray(ec_pubkey_parse(base_sec)) + ec_pubkey_tweak_mul(pub, scalar_bytes) + return pub + + +def _point_mul_or_inf(base_sec, scalar_int): + """scalar * base → handle, or None (point at infinity) when scalar == 0.""" + if scalar_int == 0: + return None + return _point_mul(scalar_int.to_bytes(32, "big"), base_sec) + + +def _add_points(p1, p2): + """p1 + p2, treating None as the point at infinity.""" + if p1 is None: + return p2 + if p2 is None: + return p1 + return ec_pubkey_combine(p1, p2) + + +def _serialize(pub): + """Compress an opaque pubkey handle to 33 bytes.""" + return ec_pubkey_serialize(pub, EC_COMPRESSED) + + +def generate_dleq_proof(a_bytes, B_sec, r=None, m=None, G=None): + """ + Generate a 64-byte DLEQ proof (BIP-374 GenerateProof). + + Proves log_G(A) == log_B(C) without revealing scalar a, + where A = a*G and C = a*B. + + Args: + a_bytes: 32-byte private key scalar. + B_sec: 33-byte compressed pubkey (alternative base point B). + r: 32-byte auxiliary randomness. Must be fresh per proof. + m: Optional 32-byte message bound during generation. + G: Optional 33-byte compressed base point. Defaults to secp256k1 G. + + Returns: + 64-byte proof: bytes(32, e) || bytes(32, s). + + Raises: + DLEQError: on invalid inputs or internal self-verification failure. + """ + if len(a_bytes) != 32: + raise DLEQError("a_bytes must be 32 bytes") + if len(B_sec) != 33: + raise DLEQError("B_sec must be a 33-byte compressed pubkey") + if m is not None and len(m) != 32: + raise DLEQError("m must be exactly 32 bytes per BIP-374") + if G is not None and len(G) != 33: + raise DLEQError("G must be a 33-byte compressed pubkey") + if r is None or len(r) != 32: + raise DLEQError("r must be provided as 32 bytes of fresh auxiliary randomness") + + G_sec = _G_COMPRESSED if G is None else G + a_int = int.from_bytes(a_bytes, "big") + if a_int == 0 or a_int >= SECP256K1_ORDER: + raise DLEQError("a is out of range [1, n-1]") + + try: + A_compressed = _serialize(_point_mul(a_bytes, G_sec)) + C_compressed = _serialize(_point_mul(a_bytes, B_sec)) + except (ValueError, OverflowError): + raise DLEQError("Invalid private key or base point") + + t_int = a_int ^ int.from_bytes(hashes.tagged_hash(DLEQ_TAG_AUX, r), "big") + m_prime = m if m is not None else b"" + + nonce_preimage = t_int.to_bytes(32, "big") + A_compressed + C_compressed + m_prime + k = ( + int.from_bytes(hashes.tagged_hash(DLEQ_TAG_NONCE, nonce_preimage), "big") + % SECP256K1_ORDER + ) + if k == 0: + raise DLEQError("Derived nonce k is zero") + + k_bytes = k.to_bytes(32, "big") + R1_compressed = _serialize(_point_mul(k_bytes, G_sec)) + R2_compressed = _serialize(_point_mul(k_bytes, B_sec)) + + challenge_preimage = ( + A_compressed + + B_sec + + C_compressed + + G_sec + + R1_compressed + + R2_compressed + + m_prime + ) + e = int.from_bytes( + hashes.tagged_hash(DLEQ_TAG_CHALLENGE, challenge_preimage), "big" + ) + s = (k + e * a_int) % SECP256K1_ORDER + proof = e.to_bytes(32, "big") + s.to_bytes(32, "big") + + if not verify_dleq_proof(A_compressed, B_sec, C_compressed, proof, m, G=G): + raise DLEQError("Self-verification of generated proof failed") + return proof + + +def verify_dleq_proof(A_sec, B_sec, C_sec, proof, m=None, G=None): + """Verify a 64-byte DLEQ proof (BIP-374 VerifyProof).""" + + if len(proof) != 64: + return False + if m is not None and len(m) != 32: + return False + + G_sec = _G_COMPRESSED if G is None else G + try: + e = int.from_bytes(proof[:32], "big") + s = int.from_bytes(proof[32:], "big") + if s >= SECP256K1_ORDER: + return False + + neg_e = (-e) % SECP256K1_ORDER + m_prime = m if m is not None else b"" + + R1 = _add_points(_point_mul_or_inf(G_sec, s), _point_mul_or_inf(A_sec, neg_e)) + if R1 is None: + return False + R2 = _add_points(_point_mul_or_inf(B_sec, s), _point_mul_or_inf(C_sec, neg_e)) + if R2 is None: + return False + + e_check = int.from_bytes( + hashes.tagged_hash( + DLEQ_TAG_CHALLENGE, + A_sec + + B_sec + + C_sec + + G_sec + + _serialize(R1) + + _serialize(R2) + + m_prime, + ), + "big", + ) + return e == e_check + except (ValueError, OverflowError): + return False diff --git a/src/embit/silent_payments/psbt.py b/src/embit/silent_payments/psbt.py new file mode 100644 index 00000000..20900e3f --- /dev/null +++ b/src/embit/silent_payments/psbt.py @@ -0,0 +1,458 @@ +""" +SP-aware PSBT scopes and subclass. + +Sections: + 1. SPInputScope / SPOutputScope — per-input/output BIP-375 field handlers + 2. SilentPaymentsPSBT — PSBT subclass with global SP fields and sign_with() SP hook +""" + +import io +from collections import OrderedDict + +from .. import ec +from ..base import EmbitError +from ..script import Script +from ..psbt import ( + PSBT, + DerivationPath, + InputScope, + OutputScope, + PSBTError, + SIGHASH, + derive_hdkey, + read_string, + resolve_signing_root, + ser_string, +) +class SPFieldError(EmbitError): + pass + + +class SPValidationError(EmbitError): + pass + + +class SilentPaymentData: + """Represents PSBT_OUT_SP_V0_INFO field (scan key + spend key).""" + + def __init__(self, scan_key: ec.PublicKey, spend_key: ec.PublicKey): + self.scan_key = scan_key + self.spend_key = spend_key + + def serialize(self) -> bytes: + """Serialize as 33-byte scan key + 33-byte spend key.""" + return self.scan_key.sec() + self.spend_key.sec() + + @classmethod + def parse(cls, data: bytes) -> "SilentPaymentData": + """Parse from 66 bytes (33-byte scan key + 33-byte spend key).""" + if len(data) != 66: + raise SPFieldError( + "PSBT_OUT_SP_V0_INFO must be 66 bytes, got {}".format(len(data)) + ) + try: + scan_key = ec.PublicKey.parse(data[:33]) + spend_key = ec.PublicKey.parse(data[33:66]) + return cls(scan_key, spend_key) + except Exception as e: + raise SPFieldError("Invalid SP data: {}".format(e)) + + +from .sp import ( + all_outpoints, + derive_sp_outputs, + get_eligible_inputs, + group_sp_outputs_by_scan_key, +) +from .signing import ( + compute_global_dleq_proof, + match_sp_spend_base, + resolve_input_privkey, +) + + +class SPInputScope(InputScope): + def __init__(self, *args, **kwargs): + self.sp_spend_bip32_derivations = OrderedDict() # pub_bytes -> DerivationPath + self.sp_tweak = None # 32 bytes or None + super().__init__(*args, **kwargs) + + def clear_metadata(self, *args, **kwargs): + super().clear_metadata(*args, **kwargs) + self.sp_spend_bip32_derivations = OrderedDict() + self.sp_tweak = None + + def update(self, other): + super().update(other) + if isinstance(other, SPInputScope): + self.sp_spend_bip32_derivations.update(other.sp_spend_bip32_derivations) + self.sp_tweak = other.sp_tweak or self.sp_tweak + + def read_value(self, stream, k, version=None): + # FUTURE: add per input ecdh shares PSBT_IN_SP_ECDH_SHARE and PSBT_IN_SP_DLEQ for multi-party SP spend inputs. + if k[0] == 0x1F: # PSBT_IN_SP_SPEND_BIP32_DERIVATION (BIP-376) + v = read_string(stream) + if version != 2: + raise PSBTError( + "PSBT_IN_SP_SPEND_BIP32_DERIVATION not allowed in PSBTv0" + ) + if len(k) != 34: + raise PSBTError("Invalid PSBT_IN_SP_SPEND_BIP32_DERIVATION key length") + pub_bytes = k[1:] + if pub_bytes in self.sp_spend_bip32_derivations: + raise PSBTError( + "Duplicated PSBT_IN_SP_SPEND_BIP32_DERIVATION for pubkey" + ) + self.sp_spend_bip32_derivations[pub_bytes] = DerivationPath.read_from( + io.BytesIO(v) + ) + elif k == b"\x20": # PSBT_IN_SP_TWEAK (BIP-376) + v = read_string(stream) + if version != 2: + raise PSBTError("PSBT_IN_SP_TWEAK not allowed in PSBTv0") + if len(v) != 32: + raise PSBTError("PSBT_IN_SP_TWEAK value must be 32 bytes") + if self.sp_tweak is not None: + raise PSBTError("Duplicated PSBT_IN_SP_TWEAK") + self.sp_tweak = v + else: + super().read_value(stream, k, version=version) + + def write_to(self, stream, skip_separator=False, version=None, **kwargs) -> int: + r = super().write_to(stream, skip_separator=True, version=version, **kwargs) + if version == 2: + for pub_bytes, derivation in self.sp_spend_bip32_derivations.items(): + r += ser_string(stream, b"\x1f" + pub_bytes) + r += ser_string(stream, derivation.serialize()) + if self.sp_tweak is not None: + r += ser_string(stream, b"\x20") + r += ser_string(stream, self.sp_tweak) + if not skip_separator: + r += stream.write(b"\x00") + return r + + + +class SPOutputScope(OutputScope): + def __init__(self, *args, **kwargs): + self.sp_data = None # SilentPaymentData (PSBT_OUT_SP_V0_INFO) + self.sp_label = None # uint32 label (PSBT_OUT_SP_V0_LABEL) + super().__init__(*args, **kwargs) + + def clear_metadata(self, *args, **kwargs): + super().clear_metadata(*args, **kwargs) + self.sp_data = None + self.sp_label = None + + def update(self, other): + super().update(other) + if isinstance(other, SPOutputScope): + self.sp_data = other.sp_data or self.sp_data + self.sp_label = ( + other.sp_label if other.sp_label is not None else self.sp_label + ) + + def read_value(self, stream, k, version=None): + if k == b"\x09": # PSBT_OUT_SP_V0_INFO (BIP-375) + v = read_string(stream) + if version != 2: + raise PSBTError("PSBT_OUT_SP_V0_INFO not allowed in PSBTv0") + if len(v) != 66: + raise PSBTError("PSBT_OUT_SP_V0_INFO must be 66 bytes") + if self.sp_data is not None: + raise PSBTError("Duplicated PSBT_OUT_SP_V0_INFO") + try: + self.sp_data = SilentPaymentData.parse(v) + except SPFieldError as e: + raise PSBTError("Invalid PSBT_OUT_SP_V0_INFO: {}".format(e)) + elif k == b"\x0a": # PSBT_OUT_SP_V0_LABEL (BIP-375) + v = read_string(stream) + if version != 2: + raise PSBTError("PSBT_OUT_SP_V0_LABEL not allowed in PSBTv0") + if len(v) != 4: + raise PSBTError("PSBT_OUT_SP_V0_LABEL must be 4 bytes") + if self.sp_label is not None: + raise PSBTError("Duplicated PSBT_OUT_SP_V0_LABEL") + self.sp_label = int.from_bytes(v, "little") + else: + super().read_value(stream, k, version=version) + + def write_to(self, stream, skip_separator=False, version=None, **kwargs) -> int: + r = super().write_to(stream, skip_separator=True, version=version, **kwargs) + if version == 2: + if self.sp_data is not None: + r += ser_string(stream, b"\x09") + r += ser_string(stream, self.sp_data.serialize()) + if self.sp_label is not None: + r += ser_string(stream, b"\x0a") + r += ser_string(stream, self.sp_label.to_bytes(4, "little")) + if not skip_separator: + r += stream.write(b"\x00") + return r + + + +class SilentPaymentsPSBT(PSBT): + PSBTIN_CLS = SPInputScope + PSBTOUT_CLS = SPOutputScope + + def __init__(self, *args, **kwargs): + self.sp_ecdh_shares = OrderedDict() # scan_key -> ecdh_share (33 bytes) + self.sp_dleq_proofs = OrderedDict() # scan_key -> dleq_proof (64 bytes) + super().__init__(*args, **kwargs) + + @property + def has_sp_outputs(self) -> bool: + """Whether this PSBT has any Silent Payment output (BIP-375).""" + return any(out.sp_data is not None for out in self.outputs) + + @property + def has_sp_spend_inputs(self) -> bool: + """Whether this PSBT has any input spending a previously-received + Silent Payment output (BIP-376).""" + return any(inp.sp_tweak is not None for inp in self.inputs) + + @property + def has_sp_content(self) -> bool: + """Whether this PSBT carries any Silent Payment data at all (send or spend).""" + return self.has_sp_outputs or self.has_sp_spend_inputs + + @classmethod + def _validate_v2_output(cls, out, i): + """Same as PSBT._validate_v2_output, but SP outputs may omit + PSBT_OUT_SCRIPT (the taproot script is derived from ECDH shares, not + known up front). Used by both the parser and add_output() (see + base PSBT.add_output).""" + if out.value is None: + raise PSBTError( + "PSBTv2 output %d missing required PSBT_OUT_AMOUNT (0x03)" % i + ) + if out.script_pubkey is None and out.sp_data is None: + raise PSBTError( + "PSBTv2 output %d missing required PSBT_OUT_SCRIPT (0x04)" % i + ) + + def parse_unknowns(self): + super().parse_unknowns() + for k in list(self.unknown): + if k[0] == 0x07 and len(k) == 34: # PSBT_GLOBAL_SP_ECDH_SHARE + if self.version != 2: + continue + v = self.unknown.pop(k) + if len(v) != 33: + raise PSBTError("PSBT_GLOBAL_SP_ECDH_SHARE value must be 33 bytes") + scan_key = k[1:] + if scan_key in self.sp_ecdh_shares: + raise PSBTError("Duplicated PSBT_GLOBAL_SP_ECDH_SHARE for scan key") + self.sp_ecdh_shares[scan_key] = v + elif k[0] == 0x08 and len(k) == 34: # PSBT_GLOBAL_SP_DLEQ + if self.version != 2: + continue + v = self.unknown.pop(k) + if len(v) != 64: + raise PSBTError("PSBT_GLOBAL_SP_DLEQ value must be 64 bytes") + scan_key = k[1:] + if scan_key in self.sp_dleq_proofs: + raise PSBTError("Duplicated PSBT_GLOBAL_SP_DLEQ for scan key") + self.sp_dleq_proofs[scan_key] = v + + def _write_extra_globals(self, stream) -> int: + r = 0 + if self.version == 2: + for scan_key in self.sp_ecdh_shares: + r += ser_string(stream, b"\x07" + scan_key) + r += ser_string(stream, self.sp_ecdh_shares[scan_key]) + for scan_key in self.sp_dleq_proofs: + r += ser_string(stream, b"\x08" + scan_key) + r += ser_string(stream, self.sp_dleq_proofs[scan_key]) + return r + + def sign_with(self, root, sighash=None, aux_rand=None): + has_sp = self.version == 2 and self.has_sp_outputs + # BIP-375: only SIGHASH_ALL may be used when SP outputs are present. + # SIGHASH.DEFAULT (taproot) is functionally SIGHASH_ALL. + if ( + has_sp + and sighash is not None + and sighash not in (SIGHASH.ALL, SIGHASH.DEFAULT) + ): + raise SPValidationError("Silent payment signing requires SIGHASH_ALL") + + if has_sp: + self.derive_sp_outputs(root, aux_rand=aux_rand) + + if sighash is not None: + counter = super().sign_with(root, sighash=sighash) + else: + counter = super().sign_with(root) + + if self.version == 2: + counter += self._sign_sp_spends(root) + return counter + + @staticmethod + def _signing_fingerprint(root): + """Resolve the signing fingerprint for ``root``. + + Returns ``(fingerprint, can_sign)``. ``fingerprint`` is None for raw/WIF + keys (which match by key material instead). ``can_sign`` is False when + ``root`` is a public-only descriptor key that cannot produce signatures. + """ + fingerprint, can_sign, _root = resolve_signing_root(root) + return fingerprint, can_sign + + def sign_input_with_sp_tweak( + self, + key: "ec.PrivateKey", + input_index: int, + inp=None, + sighash=SIGHASH.DEFAULT, + ) -> int: + """BIP-376: sign a Silent Payment spend input using sp_tweak field.""" + inp = inp or self.inputs[input_index] + sp_tweak = getattr(inp, "sp_tweak", None) + if sp_tweak is None: + return 0 + if not inp.is_taproot: + return 0 + try: + pk = key.sp_spend_tweak(sp_tweak) + except (EmbitError, ValueError): + return 0 + output_xonly = inp.utxo.script_pubkey.data[2:34] + if pk.xonly() != output_xonly: + return 0 + h = self.sighash(input_index, sighash=sighash) + sig = pk.schnorr_sign(h) + sigdata = sig.serialize() + if sighash != SIGHASH.DEFAULT: + sigdata += bytes([sighash]) + inp.taproot_key_sig = sigdata + return 1 + + def _sign_sp_spends(self, root) -> int: + """BIP-376: sign inputs that carry sp_tweak using sp_spend_bip32_derivations.""" + fingerprint, can_sign = self._signing_fingerprint(root) + if not can_sign: + return 0 + + counter = 0 + for i, inp in enumerate(self.inputs): + if getattr(inp, "sp_tweak", None) is None: + continue + if inp.taproot_key_sig is not None: + continue + + priv_key = match_sp_spend_base(inp, root, fingerprint, derive_hdkey) + if priv_key is None: + continue + + counter += self.sign_input_with_sp_tweak(priv_key, i, inp) + + return counter + + + + def derive_sp_outputs(self, root, aux_rand=None) -> None: + """BIP-375 single-signer Silent Payment send: this signer controls + every eligible input and acts as its own output generator. + + Uses the same core derivation as derive_sp_outputs (verified + against BIP-352 test vectors) to compute output scripts and global ECDH + shares in one pass. + + Raises SPValidationError if an eligible input is not controlled by + ``root`` (multi-party Silent Payment sends are out of scope for a + stateless single-party signer), or if no eligible input matches + ``root`` at all. + """ + if not (self.version == 2 and self.has_sp_outputs): + return + + groups = group_sp_outputs_by_scan_key(self.outputs) + scan_spend_groups = {} + output_indices = {} + for sk_bytes, (scan_key, pairs) in groups.items(): + scan_spend_groups[sk_bytes] = ( + scan_key, + [spend_key for _, spend_key in pairs], + ) + output_indices[sk_bytes] = [out_idx for out_idx, _ in pairs] + + eligible = get_eligible_inputs(self.inputs, has_sp_outputs=True) + if not eligible: + raise SPValidationError( + "Silent Payment send requires at least one eligible input " + "(P2PKH, P2SH-P2WPKH, P2WPKH, or P2TR)." + ) + + fingerprint, _ = self._signing_fingerprint(root) + priv_keys = [] + foreign_inputs = [] + for i in eligible: + priv = resolve_input_privkey( + self.inputs[i], root, fingerprint, derive_hdkey + ) + if priv is None: + foreign_inputs.append(i) + else: + priv_keys.append(priv) + + if foreign_inputs: + if priv_keys: + raise SPValidationError( + "Silent Payment signing failed: input(s) {} belong to another " + "signer; multi-party Silent Payment sends are not " + "supported.".format(", ".join(str(i) for i in foreign_inputs)) + ) + raise SPValidationError( + "Silent Payment signing failed: no eligible input is controlled " + "by this seed (check derivation / fingerprint)." + ) + + self.sp_ecdh_shares.clear() + self.sp_dleq_proofs.clear() + for inp in self.inputs: + keys_to_delete = [k for k in inp.unknown if len(k) > 0 and k[0] in (0x1D, 0x1E)] + for k in keys_to_delete: + del inp.unknown[k] + + outpoints = all_outpoints(self) + + derivation = derive_sp_outputs(priv_keys, outpoints, scan_spend_groups) + if derivation is None: + raise SPValidationError( + "Silent Payment signing failed: private key sum is zero." + ) + + a_sum_bytes, results = derivation + + for sk_bytes, (ecdh_share, outputs) in results.items(): + self.sp_ecdh_shares[sk_bytes] = ecdh_share + scan_key = scan_spend_groups[sk_bytes][0] + self.sp_dleq_proofs[sk_bytes] = compute_global_dleq_proof( + priv_keys, + scan_key, + ecdh_share, + aux_rand=aux_rand, + verify=False, + _a_sum_bytes=a_sum_bytes, + ) + for pos, out_idx in enumerate(output_indices[sk_bytes]): + self.outputs[out_idx].script_pubkey = Script(b"\x51\x20" + outputs[pos]) + +def finalize_sp_spends(psbt) -> int: + """ + BIP-376: Finalize SP spends by converting taproot_key_sig into final_scriptwitness + and clearing SP spend fields. + """ + from ..script import Witness + count = 0 + for inp in psbt.inputs: + if getattr(inp, "sp_tweak", None) is not None and inp.taproot_key_sig is not None: + inp.final_scriptwitness = Witness([inp.taproot_key_sig]) + inp.sp_tweak = None + inp.sp_spend_bip32_derivations.clear() + inp.taproot_key_sig = None + count += 1 + return count diff --git a/src/embit/silent_payments/signing.py b/src/embit/silent_payments/signing.py new file mode 100644 index 00000000..53592d55 --- /dev/null +++ b/src/embit/silent_payments/signing.py @@ -0,0 +1,125 @@ +""" +BIP-375 signing helpers: DLEQ proof wrappers and PSBT input private key +resolution. + +These need HD key derivation / root / fingerprint, unlike the pure crypto +in bip352.py. +""" + +from ..base import EmbitError +from ..hashes import hash160, tagged_hash +from ..misc import urandom +from . import dleq +from .psbt import SPFieldError +from .sp import pubkey_hash_from_script, sum_privkeys, tweak_mul + + + +def _default_aux_rand(*secret_material): + """Default aux_rand: fresh entropy hedged with the proof's private-key material.""" + return tagged_hash( + "embit/DLEQDefaultAuxRand", urandom(32) + b"".join(secret_material) + ) + + +def compute_global_dleq_proof( + private_keys, scan_key, global_share, aux_rand=None, verify=True, _a_sum_bytes=None +): + """Generate a 64-byte DLEQ proof for a global ECDH share (a_sum·B_scan).""" + if _a_sum_bytes is not None: + a_sum_bytes = _a_sum_bytes + else: + a_sum = sum_privkeys(private_keys) + if a_sum == 0: + raise SPFieldError("Cannot generate proof for zero sum") + a_sum_bytes = a_sum.to_bytes(32, "big") + + if verify and tweak_mul(scan_key.sec(), a_sum_bytes) != global_share: + raise SPFieldError("global_share does not match private_keys and scan_key") + + if aux_rand is not None: + r = aux_rand + else: + r = _default_aux_rand(a_sum_bytes, scan_key.sec()) + try: + return dleq.generate_dleq_proof(a_sum_bytes, scan_key.sec(), r=r) + except dleq.DLEQError as e: + raise SPFieldError("Failed to generate global DLEQ proof: {}".format(e)) + + +def match_sp_spend_base(inp, root, fingerprint, derive_hdkey): + """Return the base PrivateKey for a BIP-376 SP-spend input, matched via + sp_spend_bip32_derivations for fingerprint, or None.""" + if fingerprint: + for pub_bytes, derivation in inp.sp_spend_bip32_derivations.items(): + if derivation.fingerprint != fingerprint: + continue + hdkey = derive_hdkey(root, derivation) + if hdkey is not None and hdkey.xonly() == pub_bytes[1:]: + return hdkey.key + if fingerprint is None and hasattr(root, "secret"): + return root + return None + + +def _resolve_taproot_privkey(inp, root, fingerprint, derive_hdkey): + """Resolve the private key for a taproot input.""" + output_xonly = bytes(inp.script_pubkey.data[2:34]) + + sp_tweak = getattr(inp, "sp_tweak", None) + if sp_tweak is not None: + base = match_sp_spend_base(inp, root, fingerprint, derive_hdkey) + if base is None: + return None + try: + out_priv = base.sp_spend_tweak(sp_tweak).even_y() + except (EmbitError, ValueError): + return None + return out_priv.secret if out_priv.xonly() == output_xonly else None + + merkle = inp.taproot_merkle_root or b"" + if fingerprint: + for pub, (_leaves, derivation) in inp.taproot_bip32_derivations.items(): + if derivation.fingerprint != fingerprint: + continue + hdkey = derive_hdkey(root, derivation) + if hdkey is None or hdkey.xonly() != pub.xonly(): + continue + out_priv = hdkey.key.taproot_tweak(merkle) + if out_priv.xonly() == output_xonly: + return out_priv.secret + + if fingerprint is None and hasattr(root, "secret"): + try: + out_priv = root.taproot_tweak(merkle) + except (EmbitError, ValueError): + return None + if out_priv.xonly() == output_xonly: + return out_priv.secret + return None + + +def _resolve_bip32_privkey(inp, root, fingerprint, derive_hdkey): + """Resolve the private key for non-taproot inputs via BIP32.""" + if fingerprint: + for pub, derivation in inp.bip32_derivations.items(): + if derivation.fingerprint != fingerprint: + continue + hdkey = derive_hdkey(root, derivation) + if hdkey is None or hdkey.xonly() != pub.xonly(): + continue + return hdkey.key.secret + + if fingerprint is None and hasattr(root, "secret"): + pkh = pubkey_hash_from_script(inp.script_pubkey, inp.redeem_script) + if pkh is not None and pkh == hash160(root.get_public_key().sec()): + return root.secret + return None + + +def resolve_input_privkey(inp, root, fingerprint, derive_hdkey): + """Return the 32-byte private scalar for an eligible input's ECDH share, + or None if root does not control the input.""" + if inp.script_pubkey is not None and inp.script_pubkey.script_type() == "p2tr": + return _resolve_taproot_privkey(inp, root, fingerprint, derive_hdkey) + return _resolve_bip32_privkey(inp, root, fingerprint, derive_hdkey) diff --git a/src/embit/silent_payments/sp.py b/src/embit/silent_payments/sp.py new file mode 100644 index 00000000..773b0f09 --- /dev/null +++ b/src/embit/silent_payments/sp.py @@ -0,0 +1,276 @@ +""" +BIP-352: Silent Payments — core cryptography, address encoding, output +derivation, and PSBT input/output helpers. + +see: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki +""" + +import hashlib + +from .. import bech32, ec +from ..hashes import tagged_hash +from ..util import secp256k1 +from ..util.key import SECP256K1_ORDER +from ..util.secp256k1 import ( + ec_pubkey_serialize, + ec_pubkey_parse, + ec_pubkey_tweak_add, + ec_pubkey_create, + ec_pubkey_tweak_mul, + ec_seckey_verify, + EC_COMPRESSED, +) +from .psbt import SPFieldError, SPValidationError + +# BIP-352 Sending: max number of outputs per scan key. +K_MAX = 2323 +_NUMS_XONLY = ec.NUMS_PUBKEY.xonly() + + +def sum_privkeys(private_keys): + """Sum private key scalars mod SECP256K1_ORDER (BIP-352 a_sum).""" + return sum(int.from_bytes(priv, "big") for priv in private_keys) % SECP256K1_ORDER + + +def tweak_mul(point_sec, scalar): + """Multiply a compressed point by a scalar, returning the compressed result.""" + point = bytearray(ec_pubkey_parse(point_sec)) + ec_pubkey_tweak_mul(point, scalar) + return ec_pubkey_serialize(point, EC_COMPRESSED) + + +def compute_ecdh_share(private_key, scan_key): + """Compute ECDH share: a·B_scan (33-byte compressed).""" + if len(private_key) != 32: + raise SPFieldError("Private key must be 32 bytes") + return tweak_mul(scan_key.sec(), private_key) + + +def compute_global_ecdh_share(private_keys, scan_key): + """Compute global ECDH share: a_sum·B_scan (33-byte compressed), + or None if a_sum=0.""" + if not private_keys: + return None + for priv in private_keys: + if not ec_seckey_verify(priv): + raise SPFieldError("Invalid private key") + a_sum = sum_privkeys(private_keys) + if a_sum == 0: + return None + return tweak_mul(scan_key.sec(), a_sum.to_bytes(32, "big")) + + +def _validate_label(m, allow_zero=True): + """Validate a BIP-352 label index.""" + if not isinstance(m, int) or isinstance(m, bool): + raise TypeError("Label must be an int.") + lo = 0 if allow_zero else 1 + if not lo <= m <= 0xFFFFFFFF: + raise ValueError( + "Label must be a 32-bit unsigned integer in [{}, 2**32 - 1].".format(lo) + ) + + +def apply_label(spend_pubkey, scan_privkey, m): + """BIP-352 label tweak: + B_m = B_spend + tagged_hash("BIP0352/Label", scan_priv || ser32(m))·G""" + _validate_label(m, allow_zero=True) + tweak = tagged_hash("BIP0352/Label", scan_privkey.secret + m.to_bytes(4, "big")) + return ec.PublicKey( + secp256k1.ec_pubkey_add(secp256k1.ec_pubkey_parse(spend_pubkey.sec()), tweak) + ) + + +def encode_silent_payment_address(scan_pubkey, spend_pubkey, network="main", version=0): + """Bech32m-encode a BIP-352 Silent Payment address from its scan/spend + public keys.""" + data = bech32.convertbits(scan_pubkey.sec() + spend_pubkey.sec(), 8, 5) + hrp = "sp" if network == "main" else "tsp" + return bech32.bech32_encode(bech32.Encoding.BECH32M, hrp, [version] + data) + + +def generate_silent_payment_address( + scan_privkey, spend_pubkey, label=None, network="main", version=0 +): + """Generates the recipient's reusable silent payment address.""" + scan_pubkey = scan_privkey.get_public_key() + if label is not None: + _validate_label(label, allow_zero=False) + spend_pubkey = apply_label(spend_pubkey, scan_privkey, label) + return encode_silent_payment_address( + scan_pubkey, spend_pubkey, network=network, version=version + ) + + +def decode_silent_payment_address(address): + """Decode a silent payment address and return the (scan, spend) public keys.""" + try: + encoding, hrpgot, data = bech32.bech32_decode(address) + except bech32.Bech32DecodeError as e: + raise ValueError("Invalid silent payment address: {}".format(e)) + if hrpgot not in ("sp", "tsp"): + raise ValueError("Invalid silent payment address: unknown HRP") + if encoding != bech32.Encoding.BECH32M: + raise ValueError("Invalid silent payment address: must use bech32m encoding") + if data[0] != 0: + raise ValueError( + "Invalid silent payment address: unsupported version {}".format(data[0]) + ) + try: + decoded = bech32.convertbits(data[1:], 5, 8, False) + except bech32.Bech32DecodeError: + raise ValueError("Invalid silent payment address: conversion failed") + + return ( + ec.PublicKey.parse(bytes(decoded[:33])), + ec.PublicKey.parse(bytes(decoded[33:])), + ) + + +def _serialize_outpoint(o): + return bytes(reversed(o.txid)) + o.vout.to_bytes(4, "little") + + +def get_input_hash(outpoints, A_sum): + """BIP-352 input_hash: tagged_hash("BIP0352/Inputs", lowest_outpoint || A).""" + if not outpoints: + raise ValueError("get_input_hash requires at least one outpoint") + serialized = [_serialize_outpoint(o) for o in outpoints] + lowest = min(serialized) + return tagged_hash("BIP0352/Inputs", lowest + A_sum) + + +_SHARED_SECRET_TAG = hashlib.sha256(b"BIP0352/SharedSecret").digest() +_SHARED_SECRET_TAG2 = _SHARED_SECRET_TAG + _SHARED_SECRET_TAG + + +def derive_silent_payment_outputs(ecdh_share, spend_keys): + """Derive silent payment x-only outputs for recipients from a precomputed ECDH share.""" + if not spend_keys: + return {} + if len(spend_keys) > K_MAX: + raise ValueError( + "Too many outputs for one scan key: {} > {}".format(len(spend_keys), K_MAX) + ) + result = {} + for k, spend_key in enumerate(spend_keys): + t_k = hashlib.sha256(_SHARED_SECRET_TAG2 + ecdh_share + k.to_bytes(4, "big")).digest() + p_k = bytearray(ec_pubkey_parse(spend_key.sec())) + ec_pubkey_tweak_add(p_k, t_k) + result[k] = ec_pubkey_serialize(p_k, EC_COMPRESSED)[1:33] + return result + + +def derive_sp_outputs(priv_keys, outpoints, scan_spend_groups): + """Core output derivation verified against BIP-352 test vectors. + + Args: + priv_keys: List of 32-byte normalized private key scalars (single-signer). + outpoints: Objects with .txid/.vout (e.g. TransactionInput). + scan_spend_groups: {scan_key_bytes: (scan_key, [spend_key, ...])}. + + Returns: + (a_sum_bytes, {scan_key_bytes: (ecdh_share, outputs)}) + """ + for _scan_key, spend_keys in scan_spend_groups.values(): + if len(spend_keys) > K_MAX: + raise ValueError( + "Too many outputs for one scan key: {} > {}".format( + len(spend_keys), K_MAX + ) + ) + + a_sum = sum_privkeys(priv_keys) + if a_sum == 0: + return None + a_sum_bytes = a_sum.to_bytes(32, "big") + A_sum_bytes = ec_pubkey_serialize(ec_pubkey_create(a_sum_bytes)) + + input_hash = get_input_hash(outpoints, A_sum_bytes) + + results = {} + for sk_bytes, (scan_key, spend_keys) in scan_spend_groups.items(): + ecdh_share = compute_ecdh_share(a_sum_bytes, scan_key) + + outputs = derive_silent_payment_outputs( + tweak_mul(ecdh_share, input_hash), spend_keys + ) + results[sk_bytes] = (ecdh_share, outputs) + + return a_sum_bytes, results + + +def pubkey_hash_from_script(script, redeem_script=None): + """Return the 20-byte HASH160(pubkey) committed by a single-key script. + Handles P2WPKH, P2PKH, P2SH-P2WPKH; returns None for others.""" + if script is None: + return None + stype = script.script_type() + if stype == "p2wpkh": + return bytes(script.data[2:22]) + if stype == "p2pkh": + return bytes(script.data[3:23]) + if stype == "p2sh" and redeem_script and redeem_script.script_type() == "p2wpkh": + return bytes(redeem_script.data[2:22]) + return None + + +def witness_version(script): + """Return the segwit witness version (0-16), or None if not a witness program.""" + data = script.data + if len(data) < 4 or len(data) > 42: + return None + op = data[0] + if op == 0x00: + ver = 0 + elif 0x51 <= op <= 0x60: + ver = op - 0x50 + else: + return None + return ver if data[1] == len(data) - 2 and 2 <= data[1] <= 40 else None + + +def get_eligible_inputs(inputs, has_sp_outputs=False): + """Get list of eligible input indices for SP computation. + Per BIP-352: P2PKH, P2WPKH, P2SH-P2WPKH, P2TR. + Per BIP-375: if has_sp_outputs, reject inputs spending Segwit v>1.""" + eligible = [] + for i, inp in enumerate(inputs): + script = inp.script_pubkey + if script is None: + continue + stype = script.script_type() + if has_sp_outputs: + wv = witness_version(script) + if wv is not None and wv > 1: + raise SPValidationError( + "Input {} spends a Segwit version > 1 output with SP " + "outputs".format(i) + ) + if ( + (stype == "p2tr" and (inp.taproot_internal_key is None or inp.taproot_internal_key.xonly() != _NUMS_XONLY)) + or stype in {"p2pkh", "p2wpkh"} + or (stype == "p2sh" and inp.redeem_script and inp.redeem_script.script_type() == "p2wpkh") + ): + eligible.append(i) + return eligible + + +def all_outpoints(psbt): + """Return outpoint objects (.txid/.vout) for every input of a PSBT. + Returns the TransactionInput objects from the underlying transaction.""" + return [psbt.tx.vin[i] for i in range(len(psbt.inputs))] + + +def group_sp_outputs_by_scan_key(outputs): + """Group SP outputs by scan key, preserving output-index order. + Returns {scan_key_bytes: (scan_key, [(out_idx, spend_key), ...])}.""" + groups = {} + for out_idx, out in enumerate(outputs): + if out.sp_data is None: + continue + sk_bytes = out.sp_data.scan_key.sec() + if sk_bytes not in groups: + groups[sk_bytes] = (out.sp_data.scan_key, []) + groups[sk_bytes][1].append((out_idx, out.sp_data.spend_key)) + return groups diff --git a/src/embit/transaction.py b/src/embit/transaction.py index 6eec9b87..bab053c0 100644 --- a/src/embit/transaction.py +++ b/src/embit/transaction.py @@ -33,6 +33,23 @@ def check(cls, sighash: int): # util functions +def _signed_from_bytes(b): + res = int.from_bytes(b, "little") + bits = 8 * len(b) + if bits and res >= (1 << (bits - 1)): + res -= 1 << bits + return res + + +def _signed_to_bytes(i, length): + bits = 8 * length + if i < -(1 << (bits - 1)) or i >= (1 << (bits - 1)): + raise OverflowError("int too big to convert") + if i < 0: + i += 1 << bits + return i.to_bytes(length, "little") + + def hash_amounts(amounts): h = hashlib.sha256() for a in amounts: @@ -76,7 +93,7 @@ def is_segwit(self): def write_to(self, stream): """Returns the byte serialization of the transaction""" - res = stream.write(self.version.to_bytes(4, "little")) + res = stream.write(_signed_to_bytes(self.version, 4)) if self.is_segwit: res += stream.write(b"\x00\x01") # segwit marker and flag res += stream.write(compact.to_bytes(len(self.vin))) @@ -93,7 +110,7 @@ def write_to(self, stream): def hash(self): h = hashlib.sha256() - h.update(self.version.to_bytes(4, "little")) + h.update(_signed_to_bytes(self.version, 4)) h.update(compact.to_bytes(len(self.vin))) for inp in self.vin: h.update(inp.serialize()) @@ -144,7 +161,7 @@ def read_vout(cls, stream, idx): @classmethod def read_from(cls, stream): - ver = int.from_bytes(stream.read(4), "little") + ver = _signed_from_bytes(stream.read(4)) num_vin = compact.read_from(stream) # if num_vin is zero it is a segwit transaction is_segwit = num_vin == 0 @@ -221,7 +238,7 @@ def sighash_taproot( sh, anyonecanpay = SIGHASH.check(sighash) h = hashes.tagged_hash_init("TapSighash", b"\x00") h.update(bytes([sighash])) - h.update(self.version.to_bytes(4, "little")) + h.update(_signed_to_bytes(self.version, 4)) h.update(self.locktime.to_bytes(4, "little")) if not anyonecanpay: h.update(self.hash_prevouts()) @@ -268,7 +285,7 @@ def sighash_segwit(self, input_index, script_pubkey, value, sighash=SIGHASH.ALL) inp = self.vin[input_index] zero = b"\x00" * 32 # for sighashes h = hashlib.sha256() - h.update(self.version.to_bytes(4, "little")) + h.update(_signed_to_bytes(self.version, 4)) if anyonecanpay: h.update(zero) else: @@ -307,7 +324,7 @@ def sighash_legacy(self, input_index, script_pubkey, sighash=SIGHASH.ALL): return b"\x00" * 31 + b"\x01" h = hashlib.sha256() - h.update(self.version.to_bytes(4, "little")) + h.update(_signed_to_bytes(self.version, 4)) # ANYONECANPAY - only one input is serialized if anyonecanpay: h.update(compact.to_bytes(1)) diff --git a/src/embit/util/ctypes_secp256k1.py b/src/embit/util/ctypes_secp256k1.py index 5ce2b6c3..cddb3971 100644 --- a/src/embit/util/ctypes_secp256k1.py +++ b/src/embit/util/ctypes_secp256k1.py @@ -42,7 +42,19 @@ def _copy(a: bytes) -> bytes: """Ugly copy that works everywhere incl micropython""" if len(a) == 0: return b"" - return a[:1] + a[1:] + return bytes(a[:1] + a[1:]) + + +def _readable_buffer(a): + if isinstance(a, bytearray): + return bytes(a) + return a + + +def _writable_buffer(a): + if isinstance(a, bytearray): + return (c_char * len(a)).from_buffer(a) + return a def _platform_binary_ext(): @@ -562,7 +574,7 @@ def _init(flags=(CONTEXT_SIGN | CONTEXT_VERIFY)): def context_randomize(seed, context=_secp.ctx): if len(seed) != 32: raise ValueError("Seed should be 32 bytes long") - if _secp.secp256k1_context_randomize(context, seed) == 0: + if _secp.secp256k1_context_randomize(context, _readable_buffer(seed)) == 0: raise RuntimeError("Failed to randomize context") @@ -571,7 +583,7 @@ def ec_pubkey_create(secret, context=_secp.ctx): if len(secret) != 32: raise ValueError("Private key should be 32 bytes long") pub = bytes(64) - r = _secp.secp256k1_ec_pubkey_create(context, pub, secret) + r = _secp.secp256k1_ec_pubkey_create(context, pub, _readable_buffer(secret)) if r == 0: raise ValueError("Invalid private key") return pub @@ -588,7 +600,9 @@ def ec_pubkey_parse(sec, context=_secp.ctx): if sec[0] != 0x04: raise ValueError("Uncompressed pubkey should start with 0x04") pub = bytes(64) - r = _secp.secp256k1_ec_pubkey_parse(context, pub, sec, len(sec)) + r = _secp.secp256k1_ec_pubkey_parse( + context, pub, _readable_buffer(sec), len(sec) + ) if r == 0: raise ValueError("Failed parsing public key") return pub @@ -602,7 +616,9 @@ def ec_pubkey_serialize(pubkey, flag=EC_COMPRESSED, context=_secp.ctx): raise ValueError("Invalid flag") sec = bytes(33) if (flag == EC_COMPRESSED) else bytes(65) sz = c_size_t(len(sec)) - r = _secp.secp256k1_ec_pubkey_serialize(context, sec, byref(sz), pubkey, flag) + r = _secp.secp256k1_ec_pubkey_serialize( + context, sec, byref(sz), _readable_buffer(pubkey), flag + ) if r == 0: raise ValueError("Failed to serialize pubkey") return sec @@ -693,7 +709,7 @@ def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=_secp. def ec_seckey_verify(secret, context=_secp.ctx): if len(secret) != 32: raise ValueError("Secret should be 32 bytes long") - return bool(_secp.secp256k1_ec_seckey_verify(context, secret)) + return bool(_secp.secp256k1_ec_seckey_verify(context, _readable_buffer(secret))) @locked @@ -720,7 +736,12 @@ def ec_pubkey_negate(pubkey, context=_secp.ctx): def ec_privkey_tweak_add(secret, tweak, context=_secp.ctx): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") - if _secp.secp256k1_ec_privkey_tweak_add(context, secret, tweak) == 0: + if ( + _secp.secp256k1_ec_privkey_tweak_add( + context, _writable_buffer(secret), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the secret") return None @@ -731,7 +752,12 @@ def ec_pubkey_tweak_add(pub, tweak, context=_secp.ctx): raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") - if _secp.secp256k1_ec_pubkey_tweak_add(context, pub, tweak) == 0: + if ( + _secp.secp256k1_ec_pubkey_tweak_add( + context, _writable_buffer(pub), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the public key") return None @@ -755,7 +781,7 @@ def ec_pubkey_add(pub, tweak, context=_secp.ctx): if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") p = _copy(pub) - if _secp.secp256k1_ec_pubkey_tweak_add(context, p, tweak) == 0: + if _secp.secp256k1_ec_pubkey_tweak_add(context, p, _readable_buffer(tweak)) == 0: raise ValueError("Failed to tweak the public key") return p @@ -764,7 +790,12 @@ def ec_pubkey_add(pub, tweak, context=_secp.ctx): def ec_privkey_tweak_mul(secret, tweak, context=_secp.ctx): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") - if _secp.secp256k1_ec_privkey_tweak_mul(context, secret, tweak) == 0: + if ( + _secp.secp256k1_ec_privkey_tweak_mul( + context, _writable_buffer(secret), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the secret") @@ -774,15 +805,21 @@ def ec_pubkey_tweak_mul(pub, tweak, context=_secp.ctx): raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") - if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0: + if ( + _secp.secp256k1_ec_pubkey_tweak_mul( + context, _writable_buffer(pub), _readable_buffer(tweak) + ) + == 0 + ): raise ValueError("Failed to tweak the public key") @locked def ec_pubkey_combine(*args, context=_secp.ctx): pub = bytes(64) - pubkeys = (c_char_p * len(args))(*args) - r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args)) + readable_args = [_readable_buffer(arg) for arg in args] + pubkeys = (c_char_p * len(readable_args))(*readable_args) + r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(readable_args)) if r == 0: raise ValueError("Failed to combine pubkeys") return pub diff --git a/src/embit/util/key.py b/src/embit/util/key.py index f573ec5b..31383854 100644 --- a/src/embit/util/key.py +++ b/src/embit/util/key.py @@ -291,6 +291,7 @@ def set(self, data): self.valid = False else: self.valid = False + return self @property def is_compressed(self): diff --git a/src/embit/util/py_secp256k1.py b/src/embit/util/py_secp256k1.py index 0f1f8e1d..ba52a98d 100644 --- a/src/embit/util/py_secp256k1.py +++ b/src/embit/util/py_secp256k1.py @@ -205,18 +205,52 @@ def ec_pubkey_negate(pubkey, context=None): return ec_pubkey_parse(bytes([0x05 - sec[0]]) + sec[1:]) -def ec_privkey_tweak_add(secret, tweak, context=None): +def _ensure_mutable_key(key): + if not isinstance(key, bytearray): + raise TypeError("Tweaked key must be a bytearray") + + +def ec_privkey_tweak_add( + secret: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Add tweak to secret in place.""" + _ensure_mutable_key(secret) res = ec_privkey_add(secret, tweak) for i in range(len(secret)): secret[i] = res[i] -def ec_pubkey_tweak_add(pub, tweak, context=None): +def ec_pubkey_tweak_add( + pub: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Add tweak to public key in place.""" + _ensure_mutable_key(pub) res = ec_pubkey_add(pub, tweak) for i in range(len(pub)): pub[i] = res[i] +def ec_privkey_tweak_mul( + secret: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Multiply secret by tweak in place.""" + _ensure_mutable_key(secret) + if len(secret) != 32 or len(tweak) != 32: + raise ValueError("Secret and tweak should both be 32 bytes long") + s = int.from_bytes(secret, "big") + t = int.from_bytes(tweak, "big") + if ( + s == 0 + or s >= _key.SECP256K1_ORDER + or t == 0 + or t >= _key.SECP256K1_ORDER + ): + raise ValueError("Failed to tweak the secret") + res = ((s * t) % _key.SECP256K1_ORDER).to_bytes(32, "big") + for i in range(len(secret)): + secret[i] = res[i] + + def ec_privkey_add(secret, tweak, context=None): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") @@ -242,33 +276,39 @@ def ec_pubkey_add(pub, tweak, context=None): return Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") -# def ec_privkey_tweak_mul(secret, tweak, context=None): -# if len(secret)!=32 or len(tweak)!=32: -# raise ValueError("Secret and tweak should both be 32 bytes long") -# s = int.from_bytes(secret, 'big') -# t = int.from_bytes(tweak, 'big') -# if t > _key.SECP256K1_ORDER or s > _key.SECP256K1_ORDER: -# raise ValueError("Failed to tweak the secret") -# r = pow(s, t, _key.SECP256K1_ORDER) -# res = r.to_bytes(32, 'big') -# for i in range(len(secret)): -# secret[i] = res[i] - -# def ec_pubkey_tweak_mul(pub, tweak, context=None): -# if len(pub)!=64: -# raise ValueError("Public key should be 64 bytes long") -# if len(tweak)!=32: -# raise ValueError("Tweak should be 32 bytes long") -# if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0: -# raise ValueError("Failed to tweak the public key") - -# def ec_pubkey_combine(*args, context=None): -# pub = bytes(64) -# pubkeys = (c_char_p * len(args))(*args) -# r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args)) -# if r == 0: -# raise ValueError("Failed to negate pubkey") -# return pub +def ec_pubkey_tweak_mul( + pub: bytearray, tweak: bytes | bytearray, context: object | None = None +) -> None: + """Multiply public key by tweak in place.""" + _ensure_mutable_key(pub) + if len(pub) != 64: + raise ValueError("Public key should be 64 bytes long") + if len(tweak) != 32: + raise ValueError("Tweak should be 32 bytes long") + t = int.from_bytes(tweak, "big") + if t == 0 or t >= _key.SECP256K1_ORDER: + raise ValueError("Failed to tweak the public key") + pubkey = _pubkey_parse(pub) + Q = _key.SECP256K1.affine(_key.SECP256K1.mul([(pubkey.p, t)])) + if Q is None: + raise ValueError("Failed to tweak the public key") + res = Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") + for i in range(len(pub)): + pub[i] = res[i] + + +def ec_pubkey_combine(*args, context=None): + if len(args) == 0: + raise ValueError("At least one pubkey is required") + acc = (0, 1, 0) + for pub in args: + if len(pub) != 64: + raise ValueError("Public key should be 64 bytes long") + acc = _key.SECP256K1.add(acc, _pubkey_parse(pub).p) + Q = _key.SECP256K1.affine(acc) + if Q is None: + raise ValueError("Failed to combine pubkeys") + return Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") # schnorrsig diff --git a/tests/test_psbtV2.py b/tests/test_psbtV2.py new file mode 100644 index 00000000..2830728e --- /dev/null +++ b/tests/test_psbtV2.py @@ -0,0 +1,656 @@ +""" +BIP 370 Test Vectors for PSBTv2 +https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#user-content-Test_Vectors +""" + +import pytest +from binascii import unhexlify +from io import BytesIO +from embit.psbt import ( + PSBT, + PSBTError, + TxModifiable, + InputScope, + OutputScope, + CompressMode, +) +from embit.psbtview import PSBTView +from embit.script import Script +from embit.transaction import SIGHASH, Transaction, TransactionInput, TransactionOutput + + +class TestPSBTVectors: + """Test vectors for PSBT parsing and validation""" + + def test_invalid_psbtv0_with_version_field(self): + """PSBTv0 but with PSBT_GLOBAL_VERSION set to 2 should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc68850000000001fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_tx_version(self): + """PSBTv0 but with PSBT_GLOBAL_TX_VERSION should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc68850000000001020402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_fallback_locktime(self): + """PSBTv0 but with PSBT_GLOBAL_FALLBACK_LOCKTIME should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc68850000000001030402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_input_count(self): + """PSBTv0 but with PSBT_GLOBAL_INPUT_COUNT should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc68850000000001040102000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_output_count(self): + """PSBTv0 but with PSBT_GLOBAL_OUTPUT_COUNT should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc68850000000001050102000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_tx_modifiable(self): + """PSBTv0 but with PSBT_GLOBAL_TX_MODIFIABLE should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc68850000000001060100000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_previous_txid(self): + """PSBTv0 but with PSBT_IN_PREVIOUS_TXID should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc688500000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a27010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc800220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_output_index(self): + """PSBTv0 but with PSBT_IN_OUTPUT_INDEX should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc688500000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a27010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_sequence(self): + """PSBTv0 but with PSBT_IN_SEQUENCE should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc688500000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a27011004ffffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_required_time_locktime(self): + """PSBTv0 but with PSBT_IN_REQUIRED_TIME_LOCKTIME should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc688500000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a270111048c8dc46200220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_required_height_locktime(self): + """PSBTv0 but with PSBT_IN_REQUIRED_HEIGHT_LOCKTIME should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc688500000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a270112041027000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_output_amount(self): + """PSBTv0 but with PSBT_OUT_AMOUNT should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc688500000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f00000000002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv0_with_output_script(self): + """PSBTv0 but with PSBT_OUT_SCRIPT should be invalid""" + hex_data = "70736274ff01007102000000010b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc80000000000feffffff020008af2f00000000160014c430f64c4756da310dbd1a085572ef299926272c8bbdeb0b00000000160014a07dac8ab6ca942d379ed795f835ba71c9cc688500000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e01086b02473044022005275a485734e0ae1f3b971237586f0e72dc85833d278c0e474cd23112c0fa5e02206b048c83cebc3c41d0b93cc7da76185cedbd030d005b08018be2b98bbacbdf7b012103760dcca05f3997dc65b293060f7f29f1514c8c527048e12802b041d4fc340a2700220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000104160014a07dac8ab6ca942d379ed795f835ba71c9cc6885002202036efe2c255621986553ba9d65c3ddc64165ca1436e05aa35a4c6eb02451cf796d18f69d873e540000800100008000000080010000006200000000" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_global_unsigned_tx(self): + """PSBTv2 with PSBT_GLOBAL_UNSIGNED_TX should be invalid""" + hex_data = "70736274ff0100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e00000000010204020000000103040000000001040101010501020106010701fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff0111048c8dc4620112041027000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_missing_input_count(self): + """PSBTv2 missing PSBT_GLOBAL_INPUT_COUNT should be invalid""" + hex_data = "70736274ff01020402000000010304000000000105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_missing_output_count(self): + """PSBTv2 missing PSBT_GLOBAL_OUTPUT_COUNT should be invalid""" + hex_data = "70736274ff01020402000000010304000000000104010101fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_missing_tx_version(self): + """PSBTv2 missing PSBT_GLOBAL_TX_VERSION should be invalid""" + hex_data = "70736274ff010401010105010201fb040200000000010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_missing_previous_txid(self): + """PSBTv2 input missing PSBT_IN_PREVIOUS_TXID should be invalid""" + hex_data = "70736274ff0102040200000001030400000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010f0400000000011004feffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_missing_output_index(self): + """PSBTv2 input missing PSBT_IN_OUTPUT_INDEX should be invalid""" + hex_data = "70736274ff0102040200000001030400000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8011004feffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_missing_output_amount(self): + """PSBTv2 output missing PSBT_OUT_AMOUNT should be invalid""" + hex_data = "70736274ff0102040200000001030400000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_missing_output_script(self): + """PSBTv2 output missing PSBT_OUT_SCRIPT should be invalid""" + hex_data = "70736274ff0102040200000001030400000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f0000000000220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_invalid_time_locktime(self): + """PSBTv2 with PSBT_IN_REQUIRED_TIME_LOCKTIME less than 500000000 should be invalid""" + hex_data = "70736274ff01020402000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011104ff64cd1d00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_invalid_height_locktime(self): + """PSBTv2 with PSBT_IN_REQUIRED_HEIGHT_LOCKTIME >= 500000000 should be invalid""" + hex_data = "70736274ff01020402000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f04000000000112040065cd1d00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + def test_invalid_psbtv2_height_locktime_zero(self): + """PSBTv2 with PSBT_IN_REQUIRED_HEIGHT_LOCKTIME == 0 should be invalid""" + hex_data = "70736274ff010204020000000103040000000001040101010501020106010701fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff0111048c8dc4620112040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + with pytest.raises(PSBTError): + PSBT.parse(unhexlify(hex_data)) + + # Valid PSBTs + def test_valid_psbtv2_minimal(self): + """1 input, 2 output PSBTv2, required fields only should be valid""" + hex_data = "70736274ff01020402000000010401010105010201fb040200000000010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + + def test_valid_psbtv2_updated(self): + """1 input, 2 output updated PSBTv2 should be valid""" + hex_data = "70736274ff01020402000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + + def test_valid_psbtv2_with_sequence(self): + """1 input, 2 output PSBTv2 with sequence should be valid""" + hex_data = "70736274ff01020402000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff00220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert psbt.inputs[0].sequence == 0xFFFFFFFE + + def test_valid_psbtv2_with_sequence_and_locktime(self): + """PSBTv2 with all possible fields should be valid, including sequence""" + hex_data = "70736274ff0102040200000001030400000000010401010105010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff0111048c8dc4620112041027000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert psbt.inputs[0].sequence == 0xFFFFFFFE + assert psbt.inputs[0].required_time_locktime == 1657048460 + assert psbt.inputs[0].required_height_locktime == 10000 + + def test_inputs_modifiable_flag(self): + """Inputs Modifiable Flag (bit 0) only""" + hex_data = "70736274ff0102040200000001040101010501020106010101fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x01 + assert psbt.is_inputs_modifiable() + assert not psbt.is_outputs_modifiable() + assert not psbt.has_sighash_single() + + def test_outputs_modifiable_flag(self): + """Outputs Modifiable Flag (bit 1) only""" + hex_data = "70736274ff0102040200000001040101010501020106010201fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x02 + assert not psbt.is_inputs_modifiable() + assert psbt.is_outputs_modifiable() + assert not psbt.has_sighash_single() + + def test_sighash_single_flag(self): + """Has SIGHASH_SINGLE Flag (bit 2) only""" + hex_data = "70736274ff0102040200000001040101010501020106010401fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x04 + assert not psbt.is_inputs_modifiable() + assert not psbt.is_outputs_modifiable() + assert psbt.has_sighash_single() + + def test_undefined_flag_bit3(self): + """Undefined flag (bit 3) — parser must not reject it""" + hex_data = "70736274ff0102040200000001040101010501020106010801fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x08 + # bits 0/1/2 are all clear + assert not psbt.is_inputs_modifiable() + assert not psbt.is_outputs_modifiable() + assert not psbt.has_sighash_single() + + def test_inputs_and_outputs_modifiable(self): + """Inputs Modifiable (bit 0) + Outputs Modifiable (bit 1)""" + hex_data = "70736274ff0102040200000001040101010501020106010301fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x03 + assert psbt.is_inputs_modifiable() + assert psbt.is_outputs_modifiable() + assert not psbt.has_sighash_single() + + def test_inputs_modifiable_and_sighash_single(self): + """Inputs Modifiable (bit 0) + Has SIGHASH_SINGLE (bit 2)""" + hex_data = "70736274ff0102040200000001040101010501020106010501fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x05 + assert psbt.is_inputs_modifiable() + assert not psbt.is_outputs_modifiable() + assert psbt.has_sighash_single() + + def test_outputs_modifiable_and_sighash_single(self): + """Outputs Modifiable (bit 1) + Has SIGHASH_SINGLE (bit 2)""" + hex_data = "70736274ff0102040200000001040101010501020106010601fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x06 + assert not psbt.is_inputs_modifiable() + assert psbt.is_outputs_modifiable() + assert psbt.has_sighash_single() + + def test_all_defined_flags(self): + """All defined flags set: Inputs (bit 0) + Outputs (bit 1) + SIGHASH_SINGLE (bit 2)""" + hex_data = "70736274ff0102040200000001040101010501020106010701fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0x07 + assert psbt.is_inputs_modifiable() + assert psbt.is_outputs_modifiable() + assert psbt.has_sighash_single() + + def test_all_possible_flags(self): + """All 8 bits set (0xFF) — defined + undefined flags""" + hex_data = "70736274ff010204020000000104010101050102010601ff01fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f040000000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert len(psbt.inputs) == 1 + assert len(psbt.outputs) == 2 + assert psbt.tx_modifiable_flags == 0xFF + assert psbt.is_inputs_modifiable() + assert psbt.is_outputs_modifiable() + assert psbt.has_sighash_single() + # undefined bits preserved as-is + assert psbt.tx_modifiable_flags & ~( + TxModifiable.INPUTS | TxModifiable.OUTPUTS | TxModifiable.SIGHASH_SINGLE + ) + + def test_valid_psbtv2_with_all_fields(self): + """PSBTv2 with all possible fields should be valid""" + hex_data = "70736274ff010204020000000103040000000001040101010501020106010701fb0402000000000100520200000001c1aa256e214b96a1822f93de42bff3b5f3ff8d0519306e3515d7515a5e805b120000000000ffffffff0118c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e0000000001011f18c69a3b00000000160014b0a3af144208412693ca7d166852b52db0aef06e010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000011004feffffff0111048c8dc4620112041027000000220202d601f84846a6755f776be00e3d9de8fb10acc935fb83c45fb0162d4cad5ab79218f69d873e540000800100008000000080000000002a0000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c00220202e36fbff53dd534070cf8fd396614680f357a9b85db7340bf1cfa745d2ad7b34018f69d873e54000080010000800000008001000000640000000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.version == 2 + assert psbt.inputs[0].required_time_locktime == 1657048460 + assert psbt.inputs[0].required_height_locktime == 10000 + + +class TestPSBTLocktime: + """Test locktime determination algorithm for PSBTv2""" + + def test_locktime_no_locktimes_specified(self): + """No locktimes specified should return 0""" + hex_data = "70736274ff01020402000000010401010105010201fb040200000000010e200b0ad921419c1c8719735d72dc739f9ea9e0638d1fe4c1eef0f9944084815fc8010f0400000000000103080008af2f000000000104160014c430f64c4756da310dbd1a085572ef299926272c000103088bbdeb0b0000000001041600144dd193ac964a56ac1b9e1cca8454fe2f474f851300" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.determine_locktime() == 0 + + def test_locktime_fallback_zero(self): + """Fallback locktime of 0 should return 0""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f040100000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f0400000000000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.determine_locktime() == 0 + + def test_locktime_height_10000(self): + """Height locktime of 10000 should return 10000""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f04010000000112041027000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f0400000000000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert psbt.determine_locktime() == 10000 + + def test_locktime_max_height(self): + """Multiple height locktimes should return maximum""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f04010000000112041027000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f040000000001120428230000000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert len(psbt.inputs) == 2 + assert psbt.determine_locktime() == 10000 # max(10000, 9000) + + def test_locktime_height_and_time(self): + """1 input with Height and other with height and time locktimes should return max height value""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f04010000000112041027000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f04000000000111048c8dc46201120428230000000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert len(psbt.inputs) == 2 + assert psbt.determine_locktime() == 10000 + + def test_locktime_height_preferred_over_time(self): + """Height locktime should be preferred when both present (BIP-370: height preferred on tie)""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f04010000000111048b8dc4620112041027000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f04000000000111048c8dc46201120428230000000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert len(psbt.inputs) == 2 + assert psbt.determine_locktime() == 10000 # Height takes priority over time + + def test_locktime_time_only(self): + """Time locktime only should return time value""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f04010000000111048b8dc46200010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f04000000000111048c8dc46201120428230000000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert len(psbt.inputs) == 2 + assert psbt.determine_locktime() == 1657048460 + + def test_locktime_max_time(self): + """Multiple time locktimes should return maximum""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f040100000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f04000000000111048c8dc462000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert len(psbt.inputs) == 2 + assert psbt.determine_locktime() == 1657048460 + + def test_locktime_time_wins_when_not_all_support_height(self): + """Input 1 has height 10000 and time 1657048459, Input 2 has time 1657048460 only should return max time 1657048460""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f04010000000111048b8dc4620112041027000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f04000000000111048c8dc462000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert len(psbt.inputs) == 2 + assert psbt.determine_locktime() == 1657048460 + + def test_locktime_uncomputable(self): + """Input with height only and input with time only should raise PSBTError""" + hex_data = "70736274ff0102040200000001030400000000010401020105010101fb040200000000010e200f758dbfbd4da7c16c8a3309c3c81e1100f561ea646db5b01752c485e1bdde9f010f04010000000112041027000000010e203a1b3b3c837d6489ea7a31d8e6c7dd503c001bef3e06958e7574808d68ca78a5010f04000000000111048c8dc462000103084f9335770000000001041600140b1352cacd03cf6aa1b7f3c8d6388671b34a5e1100" + + psbt = PSBT.parse(unhexlify(hex_data)) + assert len(psbt.inputs) == 2 + with pytest.raises(PSBTError): + psbt.determine_locktime() + + +class TestPSBTv2Constructor: + """Test BIP-370 Creator / Constructor role: create_v2(), add_input(), add_output()""" + + def test_create_v2_defaults(self): + """create_v2() produces a version-2 PSBT with sensible defaults""" + psbt = PSBT.create_v2() + assert psbt.version == 2 + assert psbt.tx_version == 2 + assert psbt.locktime is None + assert psbt.tx_modifiable_flags == TxModifiable.INPUTS | TxModifiable.OUTPUTS + assert len(psbt.inputs) == 0 + assert len(psbt.outputs) == 0 + + def test_create_v2_custom_params(self): + """create_v2() accepts custom tx_version, fallback_locktime and tx_modifiable""" + psbt = PSBT.create_v2( + tx_version=1, + fallback_locktime=600000, + tx_modifiable=TxModifiable.INPUTS, + ) + assert psbt.tx_version == 1 + assert psbt.locktime == 600000 + assert psbt.tx_modifiable_flags == TxModifiable.INPUTS + + def test_add_input_appends_and_updates_count(self): + """add_input() appends the scope and increments the global input count""" + psbt = PSBT.create_v2() + inp = InputScope() + inp.txid = bytes(32) + inp.vout = 0 + psbt.add_input(inp) + assert len(psbt.inputs) == 1 + assert psbt._raw_input_count_from_global == 1 + + def test_add_input_requires_inputs_modifiable(self): + """add_input() raises when INPUTS bit is clear""" + psbt = PSBT.create_v2(tx_modifiable=TxModifiable.OUTPUTS) + inp = InputScope() + inp.txid = bytes(32) + inp.vout = 0 + with pytest.raises(PSBTError): + psbt.add_input(inp) + + def test_add_input_requires_txid(self): + """add_input() raises when PSBT_IN_PREVIOUS_TXID is absent""" + psbt = PSBT.create_v2() + inp = InputScope() + inp.vout = 0 + with pytest.raises(PSBTError): + psbt.add_input(inp) + + def test_add_input_requires_vout(self): + """add_input() raises when PSBT_IN_OUTPUT_INDEX is absent""" + psbt = PSBT.create_v2() + inp = InputScope() + inp.txid = bytes(32) + with pytest.raises(PSBTError): + psbt.add_input(inp) + + def test_add_output_appends_and_updates_count(self): + """add_output() appends the scope and increments the global output count""" + psbt = PSBT.create_v2() + out = OutputScope() + out.value = 100000 + out.script_pubkey = Script(b"\x00\x14" + bytes(20)) + psbt.add_output(out) + assert len(psbt.outputs) == 1 + assert psbt._raw_output_count_from_global == 1 + + def test_add_output_requires_outputs_modifiable(self): + """add_output() raises when OUTPUTS bit is clear""" + psbt = PSBT.create_v2(tx_modifiable=TxModifiable.INPUTS) + out = OutputScope() + out.value = 100000 + out.script_pubkey = Script(b"\x00\x14" + bytes(20)) + with pytest.raises(PSBTError): + psbt.add_output(out) + + def test_add_output_requires_amount(self): + """add_output() raises when PSBT_OUT_AMOUNT is absent""" + psbt = PSBT.create_v2() + out = OutputScope() + out.script_pubkey = Script(b"\x00\x14" + bytes(20)) + with pytest.raises(PSBTError): + psbt.add_output(out) + + def test_add_output_requires_script(self): + """add_output() raises when PSBT_OUT_SCRIPT is absent""" + psbt = PSBT.create_v2() + out = OutputScope() + out.value = 100000 + with pytest.raises(PSBTError): + psbt.add_output(out) + + +class TestTxModifiableUpdate: + """Test BIP-370 Signer role: _update_tx_modifiable() correctly restricts future modifications""" + + def test_sighash_all_clears_inputs_and_outputs(self): + """SIGHASH_ALL (not ANYONECANPAY) must clear both INPUTS and OUTPUTS bits""" + psbt = PSBT.create_v2() + psbt.tx_modifiable_flags = TxModifiable.INPUTS | TxModifiable.OUTPUTS + psbt._update_tx_modifiable(SIGHASH.ALL) + assert not psbt.is_inputs_modifiable() + assert not psbt.is_outputs_modifiable() + assert not psbt.has_sighash_single() + + def test_sighash_none_clears_inputs_preserves_outputs(self): + """SIGHASH_NONE clears INPUTS bit but must not touch the OUTPUTS bit""" + psbt = PSBT.create_v2() + psbt.tx_modifiable_flags = TxModifiable.INPUTS | TxModifiable.OUTPUTS + psbt._update_tx_modifiable(SIGHASH.NONE) + assert not psbt.is_inputs_modifiable() + assert psbt.is_outputs_modifiable() + + def test_sighash_single_sets_flag_and_clears_inputs(self): + """SIGHASH_SINGLE sets SIGHASH_SINGLE flag and clears INPUTS and OUTPUTS bits""" + psbt = PSBT.create_v2() + psbt.tx_modifiable_flags = TxModifiable.INPUTS | TxModifiable.OUTPUTS + psbt._update_tx_modifiable(SIGHASH.SINGLE) + assert not psbt.is_inputs_modifiable() + assert not psbt.is_outputs_modifiable() + assert psbt.has_sighash_single() + + def test_sighash_all_anyonecanpay_preserves_inputs_clears_outputs(self): + """SIGHASH_ALL|ANYONECANPAY preserves INPUTS bit and clears OUTPUTS bit""" + psbt = PSBT.create_v2() + psbt.tx_modifiable_flags = TxModifiable.INPUTS | TxModifiable.OUTPUTS + psbt._update_tx_modifiable(SIGHASH.ALL | SIGHASH.ANYONECANPAY) + assert psbt.is_inputs_modifiable() + assert not psbt.is_outputs_modifiable() + + def test_update_creates_flags_when_absent(self): + """_update_tx_modifiable initialises the field to 0 when it was absent""" + psbt = PSBT.create_v2() + psbt.tx_modifiable_flags = None + psbt._update_tx_modifiable(SIGHASH.ALL) + assert psbt.tx_modifiable_flags == 0 + + def test_no_op_for_psbtv0(self): + """_update_tx_modifiable is a no-op for PSBTv0 (PSBT_GLOBAL_TX_MODIFIABLE is v2-only)""" + psbt = PSBT.create_v2() + psbt.version = None # downgrade to v0 + psbt.tx_modifiable_flags = TxModifiable.INPUTS | TxModifiable.OUTPUTS + psbt._update_tx_modifiable(SIGHASH.ALL) + # must remain untouched because version != 2 + assert psbt.tx_modifiable_flags == TxModifiable.INPUTS | TxModifiable.OUTPUTS + + +class TestPSBTViewV2Validation: + def test_psbtview_rejects_v2_only_global_in_v0(self): + """PSBTView should enforce the same v0/v2 global separation as PSBT""" + psbt = PSBT(Transaction()) + raw = psbt.serialize() + stream = BytesIO(raw) + stream.read(len(PSBT.MAGIC)) + while True: + pos = stream.tell() + key_len = stream.read(1)[0] + if key_len == 0: + separator = pos + break + stream.seek(key_len, 1) + value_len = stream.read(1)[0] + stream.seek(value_len, 1) + + tx_version_kv = b"\x01\x02\x04\x02\x00\x00\x00" + invalid = raw[:separator] + tx_version_kv + raw[separator:] + with pytest.raises(PSBTError): + PSBTView.view(BytesIO(invalid)) + + +class TestPSBTv2TransactionVersion: + def test_psbt_tx_preserves_zero_and_negative_versions(self): + """PSBT_GLOBAL_TX_VERSION is signed and zero must not fall back to 2""" + psbt = PSBT.create_v2(tx_version=0) + inp = InputScope() + inp.txid = bytes(32) + inp.vout = 0 + psbt.add_input(inp) + out = OutputScope() + out.value = 1 + out.script_pubkey = Script(b"") + psbt.add_output(out) + assert psbt.tx.serialize()[:4] == b"\x00\x00\x00\x00" + + psbt.tx_version = -1 + assert psbt.tx.serialize()[:4] == b"\xff\xff\xff\xff" + + +class TestPSBTv2Compression: + def test_psbtv2_compress_non_witness_utxo_is_key_order_independent(self): + """PSBTv2 compress should not load the full prev tx when 0x00 precedes 0x0f""" + prev = Transaction( + vin=[TransactionInput(bytes([1]) * 32, 0)], + vout=[TransactionOutput(1234, Script(b"\x51"))], + ) + psbt = PSBT.create_v2() + inp = InputScope() + inp.txid = prev.txid() + inp.vout = 0 + # InputScope serializes non_witness_utxo before PSBTv2 prevout fields. + inp.non_witness_utxo = prev + psbt.add_input(inp) + out = OutputScope() + out.value = 1000 + out.script_pubkey = Script(b"\x51") + psbt.add_output(out) + + parsed = PSBT.parse(psbt.serialize(), compress=CompressMode.PARTIAL) + assert parsed.inputs[0].non_witness_utxo is None + assert parsed.inputs[0]._utxo.value == 1234 + assert parsed.verify() diff --git a/tests/tests/data/bip352_send_test_vectors.json b/tests/tests/data/bip352_send_test_vectors.json new file mode 100644 index 00000000..264f7bec --- /dev/null +++ b/tests/tests/data/bip352_send_test_vectors.json @@ -0,0 +1,2760 @@ +[ + { + "comment": "Simple send: two inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "4851455bfbe1ab4f80156570aa45063201aa5c9e1b1dcd29f0f8c33d10bf77ae", + "pub_key": "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", + "signature": "10332eea808b6a13f70059a8a73195808db782012907f5ba32b6eae66a2f66b4f65147e2b968a1678c5f73d57d5d195dbaf667b606ff80c8490eac1f3b710657" + } + ] + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "ab0c9b87181bf527879f48db9f14a02233619b986f8e8f2d5d408ce68a709f51", + "pub_key": "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", + "signature": "398a9790865791a9db41a8015afad3a47d60fec5086c50557806a49a1bc038808632b8fe679a7bb65fc6b455be994502eed849f1da3729cd948fc7be73d67295" + } + ] + } + } + ] + }, + { + "comment": "Outpoint ordering byte-lexicographically vs. vout-integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "c8ac0292997b5bca98b3ebd99a57e253071137550f270452cd3df8a3e2266d36", + "pub_key": "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c", + "signature": "c036ee38bfe46aba03234339ae7219b31b824b52ef9d5ce05810a0d6f62330dedc2b55652578aa5bdabf930fae941acd839d5a66f8fce7caa9710ccb446bddd1" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: multiple UTXOs from the same public key", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + } + ], + "outputs": [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f032695e2636619efa523fffaa9ef93c8802299181fd0461913c1b8daf9784cd", + "pub_key": "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", + "signature": "f238386c5d5e5444f8d2c75aabbcb28c346f208c76f60823f5de3b67b79e0ec72ea5de2d7caec314e0971d3454f122dda342b3eede01b3857e83654e36b25f76" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only inputs with even y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + } + ], + "outputs": [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3fb9ce5ce1746ced103c8ed254e81f6690764637ddbc876ec1f9b3ddab776b03", + "pub_key": "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", + "signature": "c5acd25a8f021a4192f93bc34403fd8b76484613466336fb259c72d04c169824f2690ca34e96cee86b69f376c8377003268fda56feeb1b873e5783d7e19bcca5" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot only with mixed even/odd y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + } + ], + "outputs": [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f5382508609771068ed079b24e1f72e4a17ee6d1c979066bf1d4e2a5676f09d4", + "pub_key": "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", + "signature": "ff65833b8fd1ed3ef9d0443b4f702b45a3f2dd457ba247687e8207745c3be9d2bdad0ab3f07118f8b2efc6a04b95f7b3e218daf8a64137ec91bd2fc67fc137a5" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with even y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "b40017865c79b1fcbed68896791be93186d08f47e416b289b8c063777e14e8df", + "pub_key": "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", + "signature": "d1edeea28cf1033bcb3d89376cabaaaa2886cbd8fda112b5c61cc90a4e7f1878bdd62180b07d1dfc8ffee1863c525a0c7b5bcd413183282cfda756cb65787266" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with odd y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "a2f9dd05d1d398347c885d9c61a64d18a264de6d49cea4326bafc2791d627fa7", + "pub_key": "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", + "signature": "96038ad233d8befe342573a6e54828d863471fb2afbad575cc65271a2a649480ea14912b6abbd3fbf92efc1928c036f6e3eef927105af4ec1dd57cb909f360b8" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", + "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, multiple recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ] + }, + "expected": { + "outputs": [ + [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9902c3c56e84002a7cd410113a9ab21d142be7f53cf5200720bb01314c5eb920", + "scan_priv_key": "060b751d7892149006ed7b98606955a29fe284a1e900070c0971f5fb93dbf422" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ], + "outputs": [ + { + "priv_key_tweak": "72cd082cccb633bf85240a83494b32dc943a4d05647a6686d23ad4ca59c0ebe4", + "pub_key": "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "signature": "38745f3d9f5eef0b1cfb17ca314efa8c521efab28a23aa20ec5e3abb561d42804d539906dce60c4ee7977966184e6f2cab1faa0e5377ceb7148ec5218b4e7878" + }, + { + "priv_key_tweak": "2f17ea873a0047fc01ba8010fef0969e76d0e4283f600d48f735098b1fee6eb9", + "pub_key": "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "signature": "c26f4e3cf371b90b840f48ea0e761b5ec31883ed55719f9ef06a90e282d85f565790ab780a3f491bc2668cc64e944dca849d1022a878cdadb8d168b8da4a6da3" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with even parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq" + ] + }, + "expected": { + "outputs": [ + [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "51d4e9d0d482b5700109b4b2e16ff508269b03d800192a043d61dca4a0a72a52", + "pub_key": "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", + "signature": "c30fa63bad6f0a317f39a773a5cbf0b0f8193c71dfebba05ee6ae4ed28e3775e6e04c3ea70a83703bb888122855dc894cab61692e7fd10c9b3494d479a60785e" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n" + ] + }, + "expected": { + "outputs": [ + [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "6024ae214876356b8d917716e7707d267ae16a0fdb07de2a786b74a7bbcddead", + "pub_key": "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", + "signature": "a86d554d0d6b7aa0907155f7e0b47f0182752472fffaeddd68da90e99b9402f166fd9b33039c302c7115098d971c1399e67c19e9e4de180b10ea0b9d6f0db832" + } + ] + } + } + ] + }, + { + "comment": "Receiving with labels: large label integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ] + }, + "expected": { + "outputs": [ + [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "e336b92330c33030285ce42e4115ad92d5197913c88e06b9072b4a9b47c664a2", + "pub_key": "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", + "signature": "c9e80dd3bdd25ca2d352ce77510f1aed37ba3509dc8cc0677f2d7c2dd04090707950ce9dd6c83d2a428063063aff5c04f1744e334f661f2fc01b4ef80b50f739" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "9d5fd3b91cac9ddfea6fc2e6f9386f680e6cee623cda02f53706306c081de87f", + "pub_key": "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "signature": "db0dfacc98b6a6fcc67cc4631f080b1ca38c60d8c397f2f19843f8f95ec91594b24e47c5bd39480a861c1209f7e3145c440371f9191fb96e324690101eac8e8e" + } + ] + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; same recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" + ] + }, + "expected": { + "outputs": [ + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1, + 1337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" + ], + "outputs": [ + { + "priv_key_tweak": "4e3352fbe0505c25e718d96007c259ef08db34f8c844e4ff742d9855ff03805a", + "pub_key": "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "signature": "6eeae1ea9eb826e3d0e812f65937100e0836ea188c04f36fabc4981eda29de8d3d3529390a0a8b3d830f7bca4f5eae5994b9788ddaf05ad259ffe26d86144b4b" + }, + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "bf709f98d4418f8a67e738154ae48818dad44689cd37fbc070891a396dd1c633", + "pub_key": "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "signature": "42a19fd8a63dde1824966a95d65a28203e631e49bf96ca5dae1b390e7a0ace2cc8709c9b0c5715047032f57f536a3c80273cbecf4c05be0b5456c183fa122c06" + }, + { + "priv_key_tweak": "736f05e4e3072c3b8656bedef2e9bf54cbcaa2b6fe5320d3e86f5b96874dda71", + "pub_key": "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "signature": "2e61bb3d79418ecf55f68847cf121bfc12d397b39d1da8643246b2f0a9b96c3daa4bfe9651beb5c9ce20e1f29282c4566400a4b45ee6657ec3b18fdc554da0b4" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: use silent payments for sender change", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" + ] + }, + "expected": { + "outputs": [ + [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "b8f87388cbb41934c50daca018901b00070a5ff6cc25a7e9e716a9d5b9e4d664", + "scan_priv_key": "11b7a82e06ca2648d5fded2366478078ec4fc9dc1d8ff487518226f229d768fd" + }, + "labels": [ + 0 + ] + }, + "expected": { + "addresses": [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqauj52ymtc4xdkmx3tgyhrsemg2g3303xk2gtzfy8h8ejet8fz8jcw23zua", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" + ], + "outputs": [ + { + "priv_key_tweak": "80cd767ed20bd0bb7d8ea5e803f8c381293a62e8a073cf46fb0081da46e64e1f", + "pub_key": "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "signature": "7fbd5074cf1377273155eefafc7c330cb61b31da252f22206ac27530d2b2567040d9af7808342ed4a09598c26d8307446e4ed77079e6a2e61fea736e44da5f5a" + } + ] + } + }, + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ] + } + } + ] + }, + { + "comment": "Single recipient: taproot input with NUMS point", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + } + } + ], + "outputs": [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3ddec3232609d348d6b8b53123b4f40f6d4f5398ca586f087b0416ec3b851496", + "pub_key": "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", + "signature": "d7d06e3afb68363031e4eb18035c46ceae41bdbebe7888a4754bc9848c596436869aeaecff0527649a1f458b71c9ceecec10b535c09d01d720229aa228547706" + } + ] + } + } + ] + }, + { + "comment": "Pubkey extraction from malleated p2pkh", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + } + } + ], + "outputs": [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "10bde9781def20d7701e7603ef1b1e5e71c67bae7154818814e3c81ef5b1a3d3", + "pub_key": "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", + "signature": "6137969f810e9e8ef6c9755010e808f5dd1aed705882e44d7f0ae64eb0c509ec8b62a0671bee0d5914ac27d2c463443e28e999d82dc3d3a4919f093872d947bb" + } + ] + } + } + ] + }, + { + "comment": "P2PKH and P2WPKH Uncompressed Keys are skipped", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ] + } + } + ] + }, + { + "comment": "Skip invalid P2SH inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ] + } + } + ] + }, + { + "comment": "Recipient ignores unrelated outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ] + }, + "expected": { + "outputs": [ + [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8" + ] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [] + } + } + ] + }, + { + "comment": "No valid inputs, sender generates no outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ] + }, + "expected": { + "outputs": [ + [] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + } + ], + "outputs": [ + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [] + } + } + ] + }, + { + "comment": "Input keys sum up to zero / point at infinity: sending fails, receiver skips tx", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + }, + "private_key": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb" + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + }, + "private_key": "592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456" + } + ], + "recipients": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ] + }, + "expected": { + "outputs": [ + [] + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + } + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + } + } + ], + "outputs": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "key_material": { + "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", + "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ], + "outputs": [] + } + } + ] + } +] diff --git a/tests/tests/data/bip375_test_vectors.json b/tests/tests/data/bip375_test_vectors.json new file mode 100644 index 00000000..3f16f51b --- /dev/null +++ b/tests/tests/data/bip375_test_vectors.json @@ -0,0 +1,1759 @@ +{ + "description": "BIP-375 Test Vectors", + "version": "1.1", + "notes": [ + "Generated by https://github.com/macgyver13/bip375-test-generator/", + "Each vector includes: base64-encoded psbt and description", + "Supplementary material (inputs, outputs, sp_proofs) is included for diagnostics and should not be used for validation", + "'checks' overrides the validation sequence, e.g. skip structure checks to test input eligibility" + ], + "invalid": [ + { + "description": "psbt structure: missing PSBT_OUT_SP_V0_INFO field when PSBT_OUT_SP_V0_LABEL set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgImjPUplZ8wrI96VHHKTGdTalHw5bForwPu1HFe3McuABCgQBAAAAAA==", + "supplementary": {} + }, + { + "description": "psbt structure: incorrect byte length for PSBT_OUT_SP_V0_INFO field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUECekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84AA=", + "supplementary": {} + }, + { + "description": "psbt structure: incorrect byte length for PSBT_IN_SP_ECDH_SHARE field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GggA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QoiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaECKE7OYVUX3K9bolxSuuQmz41SoQqm7i1bNDt7SHfihmVmLMSKKSeC9fpXOEFP3xbKKy1Q6aHB2AOPOiYIu4yAhAAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": {} + }, + { + "description": "psbt structure: incorrect byte length for PSBT_IN_SP_DLEQ field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4Gg/ihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgAAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": {} + }, + { + "description": "psbt structure: PSBT_GLOBAL_TX_MODIFIABLE field is non-zero when PSBT_OUT_SCRIPT set for sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAQABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": {} + }, + { + "description": "psbt structure: missing PSBT_OUT_SCRIPT field when sending to non-sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAA=", + "supplementary": {} + }, + { + "description": "ecdh coverage: only one ineligible P2SH multisig input when PSBT_OUT_SCRIPT set for sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiDNOBpFUI/j2cq2O34PcEoPVp2/1VqYEC2eozruYht19AEPBAAAAAABAFMCAAAAAXyoMfPUiQn+aMsOqqoR+Hc97n6McwhTN1kOJLsGOQ20AAAAAAD/////AaCGAQAAAAAAF6kU2OZohxTu7Qqi7dSJqhyjwXFwHk2HAAAAAAEER1IhAr5Ar99ismPLfe4rmF0cOP5BsZlNKwUHU0PRcsIY6rHEIQJ6R5JT0AogWhCpQ483IBm582TgqDaAC/g529KOfOa5VVKuARAE/v///wABAwiQXwEAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "", + "public_key": "", + "prevout_txid": "cd381a45508fe3d9cab63b7e0f704a0f569dbfd55a98102d9ea33aee621b75f4", + "prevout_index": 0, + "prevout_scriptpubkey": "a914d8e6688714eeed0aa2edd489aa1ca3c171701e4d87", + "amount": 100000, + "non_witness_utxo": "02000000017ca831f3d48909fe68cb0eaaaa11f8773dee7e8c73085337590e24bb06390db40000000000ffffffff01a08601000000000017a914d8e6688714eeed0aa2edd489aa1ca3c171701e4d8700000000", + "sequence": 4294967294, + "signed": false + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_IN_SP_ECDH_SHARE field for input 0 when PSBT_OUT_SCRIPT set for sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBHxAnAAAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0BAwQBAAAAIgYCQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsIAAAAgAEAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghAuV6etywGNj7MVToDBS5elCZS0I1aEHqn8ExcGFEAOY3Ih4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhA9/o4HeILO0WcbkJsYhPUF1MI8kgqROMyhCMmJNTa/Vcymb868WqzAsViY2OjfUSHB/fnoevqBke8zUhiA7srpgABAwgYcwEAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 10000, + "witness_utxo": "10270000000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02e57a7adcb018d8fb3154e80c14b97a50994b42356841ea9fc13170614400e637", + "dleq_proof": "f7fa381de20b3b459c6e426c6213d4175308f2482a44e33284232624d4dafd573299bf3af16ab302c5626363a37d448707f7e7a1ebea0647bccd486203bb2ba6", + "input_index": 1 + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_IN_SP_DLEQ field for input when PSBT_IN_SP_ECDH_SHARE set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbAAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": null, + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_GLOBAL_SP_DLEQ field when PSBT_GLOBAL_SP_ECDH_SHARE set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWwABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEiBgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwgAAACAAAAAAAEQBP7///8AAQMIGHMBAAAAAAABBCJRIDJt9Q/goHt6y3IHC+s7Yy65rRW6aVzB5apqoAe2FG/YAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b" + } + ] + } + }, + { + "description": "ecdh coverage: invalid proof in PSBT_IN_SP_DLEQ field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAbvHreDwFl9ZoVl4787j95b7SgVKV9D4XAVEk1EXcy9rdKkne51lhePEhbl1NuRdP/dpjNON8kDEQjAjOE3TeDwABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "6ef1eb783c0597d668565e3bf3b8fde5bed2815295f43e17015124d445dccbdadd2a49dee7596178f1216e5d4db9174ffdda6334e37c9031108c08ce1374de0f", + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: invalid proof in PSBT_GLOBAL_SP_DLEQ field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIIAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQOfI/UQrlNiKKmoskrQ0ZI+gB8n+CARyozeA0LUjKETQm9iLP+6G0S1DhsRlf7t4Xi0vFL/PkYV0P+EqyCUPldkAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfoIYBAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/RzBEAiAJB3pqpkrBSuehqk/mjAKs4gbJ6scjSQYa52A5ALkibgIgbxQypXj44zCvC5ACVnRryfTlKqLozHkCncjnajwpSwYBIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "2430479d2da1310a024d2aab8e4c7b49d76cd51f45ee8127c159b0393a675b6f75c581ebe3d0817ddaf3bb598cfd7a022f1a87ed479664f6b947a47db5941cb2" + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_IN_BIP32_DERIVATION field for input when PSBT_IN_SP_DLEQ set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIGHMBAAAAAAABBCJRIDJt9Q/goHt6y3IHC+s7Yy65rRW6aVzB5apqoAe2FG/YAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: output 1 missing ECDH share for scan key with one input / three sp outputs (different scan keys)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEDAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR9ADQMAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdArlFYVfpRu8H90tU+gm+Sn6Tjcr7+ye0s3LFc8BZ8SqwIQLChkR0sISqyB9uMKxe30ZnOTzmBNgI0/SZoJhOi1JJdCIdA2UdLAc/ywKk2C3aU/HVAdd6A1BmZ5hUZHOyEmts0afeIQN2iSMnxOeAru7UzhvJK7ASK3t5ENlvCtUUi0lvnD6S+yIeArlFYVfpRu8H90tU+gm+Sn6Tjcr7+ye0s3LFc8BZ8SqwQPCxHVHD/ROzjgpsjvwu4Mpxdzr2dLMVHmiSFJJcS1fIFojJ4Hi3UIhabKsoa1d+k6JJfrSWNrGvG2vILnyRElEiHgNlHSwHP8sCpNgt2lPx1QHXegNQZmeYVGRzshJrbNGn3kCY8UrEWvZFmrDITUfaU2TPsUT/kL2scy+cq8bbAA3PGpXjsx97cKBCmKUbb9RRbIHXFsmVT4rxDD1ZgJruK704AAEDCJBfAQAAAAAAAQQiUSDTlxRqgoZbVOsSuepx1oJP6rD5KkgTf3p5OmhZazNnpAEJQgNlHSwHP8sCpNgt2lPx1QHXegNQZmeYVGRzshJrbNGn3gPycOClYyGDtqsBB06HfmgA2wD1x8XS+vu2j5wo1IVFJAABAwgQJwAAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUIDUteMQTkAMq2RgWppf8dA2OuQnPBNcIhSZLBR8jheJewC4MTBh7chX5l8hIWnW0WRtsKEd7laESO5lpItMVSGC/gAAQMIIE4AAAAAAAABBCJRINLSQezkGFFKoY45kuyVPE1K1exwSmk+IQa6IsOwWnqDAQlCArlFYVfpRu8H90tU+gm+Sn6Tjcr7+ye0s3LFc8BZ8SqwAhJoHj8J6uOKjy8/7NfdVvxYbPoCjoqzP3KpD4QKbSJcAA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de", + "ecdh_share": "0376892327c4e780aeeed4ce1bc92bb0122b7b7910d96f0ad5148b496f9c3e92fb", + "dleq_proof": "98f14ac45af6459ab0c84d47da5364cfb144ff90bdac732f9cabc6db000dcf1a95e3b31f7b70a04298a51b6fd4516c81d716c9954f8af10c3d59809aee2bbd38", + "input_index": 0 + }, + { + "scan_key": "02b9456157e946ef07f74b54fa09be4a7e938dcafbfb27b4b372c573c059f12ab0", + "ecdh_share": "02c2864474b084aac81f6e30ac5edf4667393ce604d808d3f499a0984e8b524974", + "dleq_proof": "f0b11d51c3fd13b38e0a6c8efc2ee0ca71773af674b3151e689214925c4b57c81688c9e078b750885a6cab286b577e93a2497eb49636b1af1b6bc82e7c911251", + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: input 1 missing ECDH share for output 1 with two inputs / two sp outputs (different scan keys)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsIQMJH9SAORd/RNe3npYWiEnGLrP4eADEZ2fFCo84+Y79FSIdA2UdLAc/ywKk2C3aU/HVAdd6A1BmZ5hUZHOyEmts0afeIQN2iSMnxOeAru7UzhvJK7ASK3t5ENlvCtUUi0lvnD6S+yIeA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsQL4acjrvVeiYG5lZmpvBKzj4froBWMxwZokPImnK3kY7dzQal9KScZZNAcAkHMMi/VOQwpuZFg+CbzKTZYmMvwIiHgNlHSwHP8sCpNgt2lPx1QHXegNQZmeYVGRzshJrbNGn3kCY8UrEWvZFmrDITUfaU2TPsUT/kL2scy+cq8bbAA3PGpXjsx97cKBCmKUbb9RRbIHXFsmVT4rxDD1ZgJruK704AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0BAwQBAAAAIgYCQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsIAAAAgAEAAAABEAT+////Ih0DZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp94hAnUEQAf+LQMNV3wymRzyo+deU1BCRbPL5bMFOufF8iBaIh4DZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp95AOLnjgGtJvr7vxs7gZcKwZv2wCuzs8ei1sqr0fPBBeyma4KJW7Xy2yCcC63E31zv1eSccQoTQBuO1nXCpuWfYiQABAwiQXwEAAAAAAAEEIlEg9Jwu8ZtnBhG4bd7+iQzvlCaKgq4zHkb+GVHWxuNyPLEBCUIDZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp94D8nDgpWMhg7arAQdOh35oANsA9cfF0vr7to+cKNSFRSQAAQMIkF8BAAAAAAABBCJRID2h4XbGT7RZU+xG4FA2NLx+jjQ6do2+77sKPojMSkyaAQlCA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsAuDEwYe3IV+ZfISFp1tFkbbChHe5WhEjuZaSLTFUhgv4AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 100000, + "witness_utxo": "a0860100000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de", + "ecdh_share": "0376892327c4e780aeeed4ce1bc92bb0122b7b7910d96f0ad5148b496f9c3e92fb", + "dleq_proof": "98f14ac45af6459ab0c84d47da5364cfb144ff90bdac732f9cabc6db000dcf1a95e3b31f7b70a04298a51b6fd4516c81d716c9954f8af10c3d59809aee2bbd38", + "input_index": 0 + }, + { + "scan_key": "0352d78c41390032ad91816a697fc740d8eb909cf04d70885264b051f2385e25ec", + "ecdh_share": "03091fd48039177f44d7b79e96168849c62eb3f87800c46767c50a8f38f98efd15", + "dleq_proof": "be1a723aef55e8981b99599a9bc12b38f87eba0158cc7066890f2269cade463b77341a97d29271964d01c0241cc322fd5390c29b99160f826f329365898cbf02", + "input_index": 0 + }, + { + "scan_key": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de", + "ecdh_share": "0275044007fe2d030d577c32991cf2a3e75e53504245b3cbe5b3053ae7c5f2205a", + "dleq_proof": "38b9e3806b49bebeefc6cee065c2b066fdb00aececf1e8b5b2aaf47cf0417b299ae0a256ed7cb6c82702eb7137d73bf579271c4284d006e3b59d70a9b967d889", + "input_index": 1 + } + ] + } + }, + { + "description": "ecdh coverage: input 1 missing ECDH share for scan key with two inputs / one sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR9QwwAAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4gvp1lyDkV+wkkirvfyoyj7ubONQxVT2Xo1Sbl31J6tSIBDwQAAAAAAQEfUMMAAAAAAAAWABRGM9WcrRNDR5ly4MmWkh0KXa22XSIGAkOg1A6d4IgnlBYXUrrkK6kJUnhCCfBjQw8K8lY1SoW7CAAAAIABAAAAARAE/v///wABAwgYcwEAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 50000, + "witness_utxo": "50c3000000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 50000, + "witness_utxo": "50c30000000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ] + } + }, + { + "description": "input eligibility: segwit version greater than 1 in transaction inputs with sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABZSFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIHLzeIUvuENyYHLyUHbw53Vg7UwuBHFm7mpibHW/2znWAiAhKq5fCVdONB9YhvX/8y9XFuq5AwpKU3nmAWtqTULcJAEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "5214229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000165214229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ] + }, + "checks": [ + "input_eligibility" + ] + }, + { + "description": "input eligibility: non-SIGHASH_ALL signature on input with sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIHLzeIUvuENyYHLyUHbw53Vg7UwuBHFm7mpibHW/2znWAiAhKq5fCVdONB9YhvX/8y9XFuq5AwpKU3nmAWtqTULcJAEBAwQCAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ] + } + }, + { + "description": "output scripts: P2TR input with NUMS internal key cannot derive sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASsQJwAAAAAAACJRIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAAQMEAQAAAAEQBP7///8BFyBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wCIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIHCUAAAAAAAABBCJRICdoyIruoiMiW6OthOeGNS+YKdLT6TeKOToL/1mvh83QAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "amount": 10000, + "witness_utxo": "102700000000000022512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 9500, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51202768c88aeea223225ba3ad84e786352f9829d2d3e9378a393a0bff59af87cdd0" + } + ] + } + }, + { + "description": "output scripts: PSBT_OUT_SCRIPT does not match derived sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIGHMBAAAAAAABBCJRIM3jnYsFtJb48Y8gsn0K+aMjMwngRyIZ4IE/87XTeH4pAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120cde39d8b05b496f8f18f20b27d0af9a3233309e0472219e0813ff3b5d3787e29" + } + ] + } + }, + { + "description": "output scripts: two sp outputs (same scan / different spend keys) not sorted lexicographically by spend key", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCID9iBcRocGpGJF8XE6u4uLmjp7/YOsdF98ByDUQxtM38AiBxWKv7gg8r9a1nRfVvwHCcmVzMrP4XCY2KobYfjJ/y/wEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwiQXwEAAAAAAAEEIlEgdUe4Fj1bvFSYQL5MwUaq0JUgc2e565RwbeZwjUCPk/kBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgCi4D5lDNIleEbx2x1q254/iwluzMvIXshHEAXvx9nfYYAAQMIECcAAAAAAAABBCJRIGziw1OV8XDyS20tgNCkDEb0a6S0hD9qsxGpNLMcXYTcAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA6r4Yq7amM+SU5r33DN10dpfsGsSHvCh8DGp9zgv1T27AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068028b80f994334895e11bc76c75ab6e78fe2c25bb332f217b211c4017bf1f677d86", + "script": "51207547b8163d5bbc549840be4cc146aad095207367b9eb94706de6708d408f93f9" + }, + { + "output_index": 1, + "amount": 10000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe06803aaf862aeda98cf92539af7dc3375d1da5fb06b121ef0a1f031a9f7382fd53dbb", + "script": "51206ce2c35395f170f24b6d2d80d0a40c46f46ba4b4843f6ab311a934b31c5d84dc" + } + ] + } + }, + { + "description": "output scripts: k values assigned to wrong output indices with three sp outputs (same scan / spend keys)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEDAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIFg36hg2U7tRrGNQq+bDhHvokGogZdwwSNbSCOp8DkyCAiAQ+0/6sv7Vqmy2KAqTxHnPsE5Gyp7fF6owXNFnSiQsSQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwiQXwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkAAQMIECcAAAAAAAABBCJRIJcUQsifBHhuCCcorhFu6rgC8/qelf2w03U6aEAgIAbeAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AAEDCBAnAAAAAAAAAQQiUSA/y66kkIf44D2d79Ory9bejAMrWD+Fpl0FeIHExEPNwAEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + }, + { + "output_index": 1, + "amount": 10000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120971442c89f04786e082728ae116eeab802f3fa9e95fdb0d3753a6840202006de" + }, + { + "output_index": 2, + "amount": 10000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51203fcbaea49087f8e03d9defd3abcbd6de8c032b583f85a65d057881c4c443cdc0" + } + ] + } + } + ], + "valid": [ + { + "description": "can finalize: one P2PKH input single-signer", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiBSJ0jrF3ZNKMpJSBXsjUnn0w1SvHNCLHyG63TjlwVylAEPBAAAAAABAFUCAAAAAfTCEtWu0ef2/2M/LOCcZHxXvt2TAxTZjed1A9WOlAszAAAAAAD/////AaCGAQAAAAAAGXapFB4q14ctMpQTpW3wlovjOCIngxY7iKwAAAAAIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIDnBDcvHz0XG2UNW/1DBK42GqVUM8DcXPZzr94cU5nx1AiBxlVpC7SBTJDIHI8TwFCXc6J9CX4NwKEy0J2z9tt6jrAEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEg4UDSh7RbRs1OqvpDdwYVcLq+g9G4vJUKdKn+oJIz16YBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "522748eb17764d28ca494815ec8d49e7d30d52bc73422c7c86eb74e397057294", + "prevout_index": 0, + "prevout_scriptpubkey": "76a9141e2ad7872d329413a56df0968be338222783163b88ac", + "amount": 100000, + "non_witness_utxo": "0200000001f4c212d5aed1e7f6ff633f2ce09c647c57bedd930314d98de77503d58e940b330000000000ffffffff01a0860100000000001976a9141e2ad7872d329413a56df0968be338222783163b88ac00000000", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120e140d287b45b46cd4eaafa4377061570babe83d1b8bc950a74a9fea09233d7a6" + } + ] + } + }, + { + "description": "can finalize: two inputs single-signer using global ECDH share", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQLTJicjNSYH6E/K92UfjS1jfgIK6obfrYDTBs1PSM/MYiIIAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQMZTtx+UaBipW9w58JGERbyQOgh2tzgGDp5dsdzsMKvfVOC18XhXP1EZ3nGqY60ZtEnjvLZUY5Nsi67qTZFzkrkAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfoIYBAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/RzBEAiAXBfzaBiZu2zKxaYZ3s39nu25F1JLr/OZWqIDufxOOtgIgPc3jI53ZBjavZPskEspB45fJWKAwd3z/nIlAucd310wBIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAUE5NQEXxXeUGphYArue3vwROTK8ciAgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3EcwRAIgA4mx4K7v1/q2Ce+rAYv7AWrP8hu4EPPEjDi6ckQYDpUCIBUP7GDiVvQMR1dN/RKaCOr3rB2Xq+4x9AaZ+Hk1RhnzASIGAvW1n6XkkiIev1W6eK1EJgW+rpUWa6HroyUNC7qsfi7cCAAAAIABAAAAARAE/v///wABAwgYcwEAAAAAAAEEIlEgEz1qT1CQlbV5T3SR+8PxLDxFvC2bYK8luLunBMgHKq8BCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 100000, + "witness_utxo": "a086010000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02d3262723352607e84fcaf7651f8d2d637e020aea86dfad80d306cd4f48cfcc62", + "dleq_proof": "c653b71f946818a95bdc39f0918445bc903a0876b738060e9e5db1dcec30abdf54e0b5f178573f5119de71aa63ad19b449e3bcb65463936c8baeea4d917392b9" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120133d6a4f509095b5794f7491fbc3f12c3c45bc2d9b60af25b8bba704c8072aaf" + } + ] + } + }, + { + "description": "can finalize: two inputs single-signer using per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIBcF/NoGJm7bMrFphnezf2e7bkXUkuv85laogO5/E462AiA9zeMjndkGNq9k+yQSykHjl8lYoDB3fP+ciUC5x3fXTAEiBgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwgAAACAAAAAAAEQBP7///8iHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCED7KT/Ebco4uD2DOYiKUOm/1W52V9ie/mpnQhLyHLVClsiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaECKE7OYVUX3K9bolxSuuQmz41SoQqm7i1bNDt7SHfihmVmLMSKKSeC9fpXOEFP3xbKKy1Q6aHB2AOPOiYIu4yAhAAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAUE5NQEXxXeUGphYArue3vwROTK8ciAgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3EcwRAIgA4mx4K7v1/q2Ce+rAYv7AWrP8hu4EPPEjDi6ckQYDpUCIBUP7GDiVvQMR1dN/RKaCOr3rB2Xq+4x9AaZ+Hk1RhnzAQEDBAEAAAAiBgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3AgAAACAAQAAAAEQBP7///8iHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCEDNNLO907e1EZCJ2B/xZrB3oDBKiQzpWJxUo7cz1D6bCsiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaEA3wAwLQqBz9rZfDEtuVuBndLOAwd894otkCxIe4LO0z3hQ30VuC12OGXXd/yUmLIOB4g+FLNhwveP2qDKmMlvMAAEDCBhzAQAAAAAAAQQiUSATPWpPUJCVtXlPdJH7w/EsPEW8LZtgryW4u6cEyAcqrwEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 100000, + "witness_utxo": "a086010000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + }, + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "0334d2cef74eded4464227607fc59ac1de80c12a2433a56271528edccf50fa6c2b", + "dleq_proof": "37c00c0b42a073f6b65f0c4b6e56e06774b380c1df3de28b640b121ee0b3b4cf7850df456e0b5d8e1975ddff25262c8381e20f852cd870bde3f6a832a6325bcc", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120133d6a4f509095b5794f7491fbc3f12c3c45bc2d9b60af25b8bba704c8072aaf" + } + ] + } + }, + { + "description": "can finalize: two inputs / two sp outputs with mixed global and per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQECAQYBACIHAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQNmz8f9zgbgjtSbGzlSuNFRqPfgdf5fbdWN1o+dre7KlCIIAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQF6riwhtSFD+nRyflhqR+Ze2HKjQhQr8BqeL3zx0nvVAUOaStT+1sIbl54eT8B5203R/ieMXen/v9vjBYX8bBuMAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfQA0DAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/SDBFAiEA1k6e8CREAEbpyjkwLrh7LfYN/FHJwKkX1oLzz4EFkIwCIB6+EqXx/SGW8u+11Tr+V850W5Af6ImXitWa7+FKT1A3AQEDBAEAAAAiBgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwgAAACAAAAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECED+9Fz2cSNWMUCJCbQUni8oXuajiKCZmkBobALngS3mUkiHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEEDAXaQQSHz67DeP6+avUAXjrmyGI2gYsOVbIUAX61Bz7e9AF5V1tdzaqQY0kayLg/0GJNQXek0t1JlMRkDWuMzeAAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH0ANAwAAAAAAFgAUE5NQEXxXeUGphYArue3vwROTK8ciAgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3EcwRAIgWoJ5pshwRPECyFkwyJ7C/wGUKGJBraOMeFTy6DZR1fICICvMZkqflwTVcxLbwryDaGcIDyLEdYuR27ktF6HkEeqpAQEDBAEAAAAiBgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3AgAAACAAQAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECEDpqibG5hqmVUz4XvtZFALVBuw1zkHHv4q4rfvsKDKa88iHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEEDDSmopQLvppzVsHMb/41jdREHbWIl8upnCEut1KF59oeyhHHksl6OASTNFqyJN6dlMKzbuhSauI1KBcaMjHeczAAEDCJBfAQAAAAAAAQQiUSCA65gxi89t1hGsF6lksJFzbATK6t8ubodOXO+Gx8YFFwEJQgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKAMSZlgePoQcu5TeYupzVYOiMsTIiPpu3vLsL4cHifwd1AABAwiQXwEAAAAAAAEEIlEgELYvxakKvKAWeWHb/0zq6aoyTvssfyRmJ+i1oz3jfIQBCUICqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRACSkjXbiDUC89fLhpcP/6z3CqLO4jXmkhtVMnZ6qj6GI4A", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 200000, + "witness_utxo": "400d030000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "0366cfc7fdce06e08ed49b1b3952b8d151a8f7e075fe5f6dd58dd68f9dadeeca94", + "dleq_proof": "5eab8b086d4850fe9d1c9f961a91f997b61ca8d0850afc06a78bdf3c749ef54050e692b53fb5b086e5e78793f01e76d3747f89e3177a7feff6f8c1617f1b06e3" + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03fbd173d9c48d58c5022426d05278bca17b9a8e2282666901a1b00b9e04b79949", + "dleq_proof": "c05da410487cfaec378febe6af5005e3ae6c86236818b0e55b214017eb5073edef40179575b5dcdaa9063491ac8b83fd0624d4177a4d2dd4994c4640d6b8ccde", + "input_index": 0 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03a6a89b1b986a995533e17bed64500b541bb0d739071efe2ae2b7efb0a0ca6bcf", + "dleq_proof": "c34a6a2940bbe9a7356c1cc6ffe358dd4441db58897cba99c212eb75285e7da1eca11c792c97a380493345ab224de9d94c2b36ee8526ae23528171a3231de733", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "512080eb98318bcf6dd611ac17a964b091736c04caeadf2e6e874e5cef86c7c60517" + }, + { + "output_index": 1, + "amount": 90000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "512010b62fc5a90abca0167961dbff4ceae9aa324efb2c7f246627e8b5a33de37c84" + } + ] + } + }, + { + "description": "can finalize: one input / one sp output with both global and per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBACIHAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQMKGLm7QZm4kzDrF/juWseG4IU9zS88zfF0S5fOTt4FPiIIAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQCf7kMXWHHjjEVIA/9GD0o0KtIOAYslUGTTGQW1CZ84xvUKH6TWUUsCBevDvTVDP9XhDaw0o+e2PIbeaRHZAgI4AAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfQA0DAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/RzBEAiA3eo+fWjPY+iVcM7OSJtO88CKd+tlgRw74AWpxQvIzWAIgCBvWlIh63us2Ia9aLeTw05yxNpLQQb99vM00oysFQekBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQMKGLm7QZm4kzDrF/juWseG4IU9zS88zfF0S5fOTt4FPiIeAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQAATg9RmG+hyXvKQSPSMkR+J9fNDF9fAwKzGDvZfG1K//3qW2DObYl34Orq1eqJz7c2aNceU2suxyGEXG7JnB18AAQMIkF8BAAAAAAABBCJRINxNRNnjYeXnQ2V5x44lklVB4in9wVOtWYmtiuawYkx4AQlCAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oAxJmWB4+hBy7lN5i6nNVg6IyxMiI+m7e8uwvhweJ/B3UAA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "030a18b9bb4199b89330eb17f8ee5ac786e0853dcd2f3ccdf1744b97ce4ede053e", + "dleq_proof": "27fb90c5d61c78e3115200ffd183d28d0ab4838062c9541934c6416d4267ce31bd4287e9359452c0817af0ef4d50cff578436b0d28f9ed8f21b79a447640808e" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "5120dc4d44d9e361e5e7436579c78e25925541e229fdc153ad5989ad8ae6b0624c78" + } + ] + } + }, + { + "description": "can finalize: three sp outputs (different scan keys) with multiple global ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEDAQYBACIHAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQLC2d9g7mmlqGYE3ZpigsIu+OLJJ22m5ZWlRj83Gq3uJCIHAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQLcfS7KxWiwEVF1ujzVSW/0xLbZCUItvMrrjyUvpU+L0CIHA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQJwvvXAapqFX8Y7/yvQvi1wAzyCC2FTQTyNbtpR8lobKSIIAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQOyCQr97WIHO4GBWbH8TzEfkcQyJomRYwgMBswMeSBc7wL6gFoDTN1616AYRqw1Qix5Jq7VxsYeAMqrknwsC99IiCAMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEDqQ0cOXhvjKRtIoLy1P1WVLGSGDnY177pQA8yt7XfdEo4hZpew9SO+IX7Gh1B3l8zOvFzZQ2KvG0bYA2Qr1flAIggDjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxAG9/lTtSioG//mwAJrk49gPxHYWR1r98hJEz+PG0QTfJHQah1UU14ZU+5yhEY5tPlUvOPjKujVS6DM8UBvwy9PwABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR8goQcAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCLg3ekbsHGTj/QlvLhIwW7hrrfzKDP2b9X3JYim55whAIgfn3H3eyoVVwXfr7/yUnqTDNSD+W+7Hun4nJ+NPvUjf0BIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0iAgJDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFu0gwRQIhAPmSLe+tUsYSCQS6pacAUymDbsHD2uFsoGjAFrJrBNLYAiBw+V8k+cklbkGWlGnDUqz6yOiUMjUC6s0WPJSi42nuHwEiBgJDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFuwgAAACAAQAAAAEQBP7///8AAQMIkF8BAAAAAAABBCJRIKFPr9x+36QEQnF0sSasoOmZMTP+F+JCCurtIO9U/DjcAQlCAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oAxJmWB4+hBy7lN5i6nNVg6IyxMiI+m7e8uwvhweJ/B3UAAEDCJBfAQAAAAAAAQQiUSB99fJJWUQ0FLvv6J0tqJSGSlYotoAzuSPoaGvcw96dlAEJQgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEAJKSNduINQLz18uGlw//rPcKos7iNeaSG1UydnqqPoYjgABAwiQXwEAAAAAAAEEIlEgsvEqf1cMxQV4QPpjk/cfBpABQgiBgYEMNLuIdh2SlQ0BCUIDjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwD+BRzhEyddMkh/xelr0ELYzc0jXr7/3OJG8XLDLJMsMAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 500000, + "witness_utxo": "20a1070000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 100000, + "witness_utxo": "a0860100000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02dc7d2ecac568b0115175ba3cd5496ff4c4b6d909422dbccaeb8f252fa54f8bd0", + "dleq_proof": "ea43470e5e1be3291b48a0bcb53f55952c64860e7635efba5003ccaded77dd128e216697b0f523be217ec687507797cccebc5cd94362af1b46d803642bd5f940" + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02c2d9df60ee69a5a86604dd9a6282c22ef8e2c9276da6e595a5463f371aadee24", + "dleq_proof": "ec8242bf7b5881cee060566c7f13cc47e4710c89a26458c20301b3031e48173bc0bea01680d3375eb5e80611ab0d508b1e49abb571b1878032aae49f0b02f7d2" + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "0270bef5c06a9a855fc63bff2bd0be2d70033c820b6153413c8d6eda51f25a1b29", + "dleq_proof": "1bdfe54ed4a2a06fff9b0009ae4e3d80fc47616475afdf21244cfe3c6d104df24741a875514d78654fb9ca1118e6d3e552f38f8caba3552e8333c501bf0cbd3f" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "5120a14fafdc7edfa404427174b126aca0e9993133fe17e2420aeaed20ef54fc38dc" + }, + { + "output_index": 1, + "amount": 90000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "51207df5f24959443414bbefe89d2da894864a5628b68033b923e8686bdcc3de9d94" + }, + { + "output_index": 2, + "amount": 90000, + "sp_v0_info": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c03f81473844c9d74c921ff17a5af410b6337348d7afbff73891bc5cb0cb24cb0c0", + "script": "5120b2f12a7f570cc5057840fa6393f71f06900142088181810c34bb88761d92950d" + } + ] + } + }, + { + "description": "can finalize: one P2WPKH input / two mixed outputs - labeled sp output and BIP 32 change", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR8goQcAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCspcmETsmSLIHmcIF+My2i3RXSxdOpRiq21l5TtoKJXgIgTfMs6jgucsQ8BYQE/a5wWVHmuj3jPYM8g6lbsBg4CscBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIoIYBAAAAAAABBCJRICJoz1KZWfMKyPelRxykxnU2pR8OWxaK8D7tRxXtzHLgAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA+qPIovitxbaze/LKBAHKHTwCkdvXAe9BRkXEmzGSz3vAQoEAQAAAAABAwhw8wUAAAAAAAEEFgAUOMGPNd2AY+OApw/+tPmP9O80gXciAgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwwAAAAAAAAAAAAAAAEA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 500000, + "witness_utxo": "20a1070000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "sp_v0_label": 1, + "script": "51202268cf529959f30ac8f7a5471ca4c67536a51f0e5b168af03eed4715edcc72e0" + }, + { + "output_index": 1, + "amount": 390000, + "script": "001438c18f35dd8063e380a70ffeb4f98ff4ef348177" + } + ] + } + }, + { + "description": "can finalize: one input / two sp outputs - output 0 has no label / output 1 uses label=0 convention for sp change", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR/gkwQAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCICJa45kPVF365uoUKpN0arSqgSfjyBNY63wha08anrtiAiBl42BSGRArJQdeOVXz80zjvPKjXR4QFd1dw7/tPs81qQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0CXw7Bs42ZhKfrYvFb5AMCBBDxQu301p42P1YVr9Kmts0hA84eNnXZDZkmSfqd2xH5AlSbs7AvVvbonMCgTIuMN92UIh0Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWghAlWBv6pzMVb2uY5xom9w/XazZ4ZJ9isfxed7jpq45hBQIh4CXw7Bs42ZhKfrYvFb5AMCBBDxQu301p42P1YVr9Kmts1ADolhPR9rJTbbj6kkIA2zpvjvyhciVWTVaxEuUIZbTTAuBY+m9hwp4jZqSnJp1FVaXGHWVVnb+/rJ4urf5TY31SIeAot5DgmH/enw0SOY62vjDHN2eTxkeGWioC8u/JYNlmVoQHlAgZ626fJYwVg4fApkzBb1F8fvRAkSLM6qlPTi/1N2xCR7/BcNmyY7Dj0ga2aMh+lLNN7q+gCheSsEeteIdbsAAQMIoIYBAAAAAAABBCJRIKBIDZZnnBk25au9SLqw4B/6yx7n0cnhaC8VQBaZZYXZAQlCAot5DgmH/enw0SOY62vjDHN2eTxkeGWioC8u/JYNlmVoA7RDRzNcY/UF5V4mirKA8xi0pqoUSS/zecNmih9U74ybAAEDCDDmAgAAAAAAAQQiUSB2Hb00kpiEpmcp69zbrPFnSC32LBTwmOTvGNcP4Grv0AEJQgJfDsGzjZmEp+ti8VvkAwIEEPFC7fTWnjY/VhWv0qa2zQK+EFEqlVNkHkB62OUJo0GEUqkC6FVqDboumD9eAPe8UQEKBAAAAAAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 300000, + "witness_utxo": "e093040000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "025f0ec1b38d9984a7eb62f15be403020410f142edf4d69e363f5615afd2a6b6cd", + "ecdh_share": "03ce1e3675d90d992649fa9ddb11f902549bb3b02f56f6e89cc0a04c8b8c37dd94", + "dleq_proof": "0e89613d1f6b2536db8fa924200db3a6f8efca17225564d56b112e50865b4d302e058fa6f61c29e2366a4a7269d4555a5c61d65559dbfbfac9e2eadfe53637d5", + "input_index": 0 + }, + { + "scan_key": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d966568", + "ecdh_share": "025581bfaa733156f6b98e71a26f70fd76b3678649f62b1fc5e77b8e9ab8e61050", + "dleq_proof": "7940819eb6e9f258c158387c0a64cc16f517c7ef4409122cceaa94f4e2ff5376c4247bfc170d9b263b0e3d206b668c87e94b34deeafa00a1792b047ad78875bb", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", + "script": "5120a0480d96679c1936e5abbd48bab0e01ffacb1ee7d1c9e1682f154016996585d9" + }, + { + "output_index": 1, + "amount": 190000, + "sp_v0_info": "025f0ec1b38d9984a7eb62f15be403020410f142edf4d69e363f5615afd2a6b6cd037cd462358c69c40525247a8793944d9e67484b248fd84a1e7ee4f920b4da41b5", + "sp_v0_label": 0, + "script": "5120761dbd34929884a66729ebdcdbacf167482df62c14f098e4ef18d70fe06aefd0" + } + ] + } + }, + { + "description": "can finalize: two sp outputs - output 0 uses label=3 / output 1 uses label=1", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR/gkwQAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCICJa45kPVF365uoUKpN0arSqgSfjyBNY63wha08anrtiAiBl42BSGRArJQdeOVXz80zjvPKjXR4QFd1dw7/tPs81qQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWghAlWBv6pzMVb2uY5xom9w/XazZ4ZJ9isfxed7jpq45hBQIh4Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWhAeUCBnrbp8ljBWDh8CmTMFvUXx+9ECRIszqqU9OL/U3bEJHv8Fw2bJjsOPSBrZoyH6Us03ur6AKF5KwR614h1uwABAwighgEAAAAAAAEEIlEgyvcy9B51UZCclKM10VeOp7dmAnuIbN0cFwHZv8a3hIgBCUICi3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWgDliRhtuNd91TQDUhtw53swgJxhFB2YU5cDtLJDyPT6b4BCgQDAAAAAAEDCDDmAgAAAAAAAQQiUSBNB6DTsPYafF7+NsLDxYfItphBhUYAjPgZu9pM4xgSIQEJQgKLeQ4Jh/3p8NEjmOtr4wxzdnk8ZHhloqAvLvyWDZZlaANaVzfUxngmF8xrDElrsrxiVVoyslQkT7ycnANd9GnlqwEKBAEAAAAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 300000, + "witness_utxo": "e093040000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d966568", + "ecdh_share": "025581bfaa733156f6b98e71a26f70fd76b3678649f62b1fc5e77b8e9ab8e61050", + "dleq_proof": "7940819eb6e9f258c158387c0a64cc16f517c7ef4409122cceaa94f4e2ff5376c4247bfc170d9b263b0e3d206b668c87e94b34deeafa00a1792b047ad78875bb", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", + "sp_v0_label": 3, + "script": "5120caf732f41e7551909c94a335d1578ea7b766027b886cdd1c1701d9bfc6b78488" + }, + { + "output_index": 1, + "amount": 190000, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", + "sp_v0_label": 1, + "script": "51204d07a0d3b0f61a7c5efe36c2c3c587c8b698418546008cf819bbda4ce3181221" + } + ] + } + }, + { + "description": "can finalize: two inputs using per-input ECDH shares - only eligible inputs contribute shares (P2SH excluded)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCLTLcGPIp7P5Ia4ABZbNd4jlRA4dY+8e4bzsjCrJQMogIgNw2OmoWgzI3SWwuwgfMaotzFugoiSOqGCoFlHKNKDGgBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4g0iImLyaHKbt8hMVYEppdaYYYXO9TXsFJwN9g3UqSwgQBDwQAAAAAAQBTAgAAAAFA5vqmEmjxehAQHWJVNYKIKuSFi+4TNj24D98oH4Jf/AAAAAAA/////wHwSQIAAAAAABepFPRfjMomjsJuS76JkXDxOCUNfPVehwAAAAABBEdSIQKHfKAvFEBZvYLQDhs5muN094pSzvehyjfyjXiLNA0veSEDTIekSHL14fAG002IoAlZKCvUU150d8PWjDFlmR6hs5ZSrgEQBP7///8iHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCECYototkRuaocCWXPZOvsZTkIKEPOhzLOK3YLUZKiUpCgiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEDCJBfAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "", + "public_key": "", + "prevout_txid": "d222262f268729bb7c84c558129a5d6986185cef535ec149c0df60dd4a92c204", + "prevout_index": 0, + "prevout_scriptpubkey": "a914f45f8cca268ec26e4bbe899170f138250d7cf55e87", + "amount": 150000, + "non_witness_utxo": "020000000140e6faa61268f17a10101d62553582882ae4858bee13363db80fdf281f825ffc0000000000ffffffff01f04902000000000017a914f45f8cca268ec26e4bbe899170f138250d7cf55e8700000000", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + } + ] + } + }, + { + "description": "can finalize: two inputs using global ECDH share - only eligible inputs contribute shares (P2SH excluded)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIIAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQCQwR50toTEKAk0qq45Me0nXbNUfRe6BJ8FZsDk6Z1tvdcWB6+PQgX3a87tZjP16Ai8ah+1HlmT2uUekfbWUHLIAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfoIYBAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/SDBFAiEAi0y3BjyKez+SGuAAWWzXeI5UQOHWPvHuG87IwqyUDKICIDcNjpqFoMyN0lsLsIHzGqLcxboKIkjqhgqBZRyjSgxoASIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///wABDiDSIiYvJocpu3yExVgSml1phhhc71NewUnA32DdSpLCBAEPBAAAAAABAFMCAAAAAUDm+qYSaPF6EBAdYlU1gogq5IWL7hM2PbgP3ygfgl/8AAAAAAD/////AfBJAgAAAAAAF6kU9F+MyiaOwm5LvomRcPE4JQ189V6HAAAAAAEER1IhAod8oC8UQFm9gtAOGzma43T3ilLO96HKN/KNeIs0DS95IQNMh6RIcvXh8AbTTYigCVkoK9RTXnR3w9aMMWWZHqGzllKuARAE/v///wABAwiQXwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "", + "public_key": "", + "prevout_txid": "d222262f268729bb7c84c558129a5d6986185cef535ec149c0df60dd4a92c204", + "prevout_index": 0, + "prevout_scriptpubkey": "a914f45f8cca268ec26e4bbe899170f138250d7cf55e87", + "amount": 150000, + "non_witness_utxo": "020000000140e6faa61268f17a10101d62553582882ae4858bee13363db80fdf281f825ffc0000000000ffffffff01f04902000000000017a914f45f8cca268ec26e4bbe899170f138250d7cf55e8700000000", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "2430479d2da1310a024d2aab8e4c7b49d76cd51f45ee8127c159b0393a675b6f75c581ebe3d0817ddaf3bb598cfd7a022f1a87ed479664f6b947a47db5941cb2" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + } + ] + } + }, + { + "description": "can finalize: two mixed input types - only eligible inputs contribute ECDH shares (NUMS internal key excluded)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASsQJwAAAAAAACJRIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAARAE/v///wEXIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAAAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0iAgJDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFu0gwRQIhAPRhsN/2Q/wKVsFSIUU2OBYiEkh+UGpZ4GnXXtHwq4T2AiBSxx6U6TEuqKkAH80mNyvnCZfdwqpinFqBFtxR9bfpogEBAwQBAAAAIgYCQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsIAAAAgAEAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghAuV6etywGNj7MVToDBS5elCZS0I1aEHqn8ExcGFEAOY3Ih4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhA9/o4HeILO0WcbkJsYhPUF1MI8kgqROMyhCMmJNTa/Vcymb868WqzAsViY2OjfUSHB/fnoevqBke8zUhiA7srpgABAwgcJQAAAAAAAAEEIlEgBXZyJSSDmoybydLZc46Yu0DdLsUCO9x9p/cGI818hfsBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "amount": 10000, + "witness_utxo": "102700000000000022512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 100000, + "witness_utxo": "a0860100000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02e57a7adcb018d8fb3154e80c14b97a50994b42356841ea9fc13170614400e637", + "dleq_proof": "f7fa381de20b3b459c6e426c6213d4175308f2482a44e33284232624d4dafd573299bf3af16ab302c5626363a37d448707f7e7a1ebea0647bccd486203bb2ba6", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 9500, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51200576722524839a8c9bc9d2d9738e98bb40dd2ec5023bdc7da7f70623cd7c85fb" + } + ] + } + }, + { + "description": "can finalize: three sp outputs (same scan key) - each output has distinct k value", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEEAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+AGgYAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIA3+9XB4r8pV2yK0ze42yxF/h2HkeAgptCHRBkisr62PAiBk5vWJD0/QW3emSnGDHJiGTZyYTJ7x/r6afFC08VkHIQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwighgEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkAAQMIoIYBAAAAAAABBCJRID/LrqSQh/jgPZ3v06vL1t6MAytYP4WmXQV4gcTEQ83AAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AAEDCKCGAQAAAAAAAQQiUSCXFELInwR4bggnKK4Rbuq4AvP6npX9sNN1OmhAICAG3gEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQABAwiQXwEAAAAAAAEEIlEgee9WiKP6f+/DkidFWx8cLwjHAIdCTWoA+EwxQhrKjaQA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 400000, + "witness_utxo": "801a060000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + }, + { + "output_index": 1, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51203fcbaea49087f8e03d9defd3abcbd6de8c032b583f85a65d057881c4c443cdc0" + }, + { + "output_index": 2, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120971442c89f04786e082728ae116eeab802f3fa9e95fdb0d3753a6840202006de" + }, + { + "output_index": 3, + "amount": 90000, + "script": "512079ef5688a3fa7fefc39227455b1f1c2f08c70087424d6a00f84c31421aca8da4" + } + ] + } + }, + { + "description": "can finalize: three sp outputs (same scan key) / two regular outputs - k values assigned independently of output index", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEGAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR8goQcAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQD333KAYwDd2+SyTNcz9TsypkayNsKUYFfeR65QOyqxaAIgTGlAW3e+2vOyB+3ll0m7o9VMRhWoi7025Ugkr3fJ5zEBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIoIYBAAAAAAABBCJRIDJt9Q/goHt6y3IHC+s7Yy65rRW6aVzB5apqoAe2FG/YAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AAEDCFDDAAAAAAAAAQQiUSCcaj0jgY6CDtHQz965Y2V8jsMne32bhlx411V/Xe3WogABAwighgEAAAAAAAEEIlEgP8uupJCH+OA9ne/Tq8vW3owDK1g/haZdBXiBxMRDzcABCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkAAQMIUMMAAAAAAAABBCJRIHnvVoij+n/vw5InRVsfHC8IxwCHQk1qAPhMMUIayo2kAAEDCKCGAQAAAAAAAQQiUSCXFELInwR4bggnKK4Rbuq4AvP6npX9sNN1OmhAICAG3gEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQABAwjILAEAAAAAAAEEIlEgb8UG+AUqn1qKqbmQ1emU9JXd7W1HusRMqs2t2NiZHNAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 500000, + "witness_utxo": "20a1070000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + }, + { + "output_index": 1, + "amount": 50000, + "script": "51209c6a3d23818e820ed1d0cfdeb963657c8ec3277b7d9b865c78d7557f5dedd6a2" + }, + { + "output_index": 2, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51203fcbaea49087f8e03d9defd3abcbd6de8c032b583f85a65d057881c4c443cdc0" + }, + { + "output_index": 3, + "amount": 50000, + "script": "512079ef5688a3fa7fefc39227455b1f1c2f08c70087424d6a00f84c31421aca8da4" + }, + { + "output_index": 4, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120971442c89f04786e082728ae116eeab802f3fa9e95fdb0d3753a6840202006de" + }, + { + "output_index": 5, + "amount": 77000, + "script": "51206fc506f8052a9f5a8aa9b990d5e994f495dded6d47bac44caacdadd8d8991cd0" + } + ] + } + }, + { + "description": "in progress: two P2TR inputs, neither is signed", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASughgEAAAAAACJRIMgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/AQMEAQAAAAEQBP7///8BFyDIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvyIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4gK5A3/0HdF05VIvD0BFw/QENiMq8mANTaXJ6hPMPKtLUBDwQAAAAAAQErECcAAAAAAAAiUSBDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFuwEDBAEAAAABEAT+////ARcgQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsiHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCEC5Xp63LAY2PsxVOgMFLl6UJlLQjVoQeqfwTFwYUQA5jciHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaED3+jgd4gs7RZxuQmxiE9QXUwjySCpE4zKEIyYk1Nr9VzKZvzrxarMCxWJjY6N9RIcH9+eh6+oGR7zNSGIDuyumAAEDCLj5AgAAAAAAAQQiUSDLH766UNBuv/tpC9qdX/Oc4vrbT87clmMu/xukHMDAewEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "5120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "amount": 100000, + "witness_utxo": "a086010000000000225120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "2b9037ff41dd174e5522f0f4045c3f40436232af2600d4da5c9ea13cc3cab4b5", + "prevout_index": 0, + "prevout_scriptpubkey": "512043a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "amount": 10000, + "witness_utxo": "102700000000000022512043a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + }, + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02e57a7adcb018d8fb3154e80c14b97a50994b42356841ea9fc13170614400e637", + "dleq_proof": "f7fa381de20b3b459c6e426c6213d4175308f2482a44e33284232624d4dafd573299bf3af16ab302c5626363a37d448707f7e7a1ebea0647bccd486203bb2ba6", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 195000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120cb1fbeba50d06ebffb690bda9d5ff39ce2fadb4fcedc96632eff1ba41cc0c07b" + } + ] + } + }, + { + "description": "in progress: one P2TR input / one sp output with no ECDH shares when PSBT_OUT_SCRIPT field is not set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASughgEAAAAAACJRIMgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/ARAE/v///wEXIMgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/AAEDCBhzAQAAAAAAAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "5120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "amount": 100000, + "witness_utxo": "a086010000000000225120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + } + ] + } + }, + { + "description": "in progress: two inputs / one sp output, input 1 missing ECDH share when PSBT_OUT_SCRIPT field is not set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4gvp1lyDkV+wkkirvfyoyj7ubONQxVT2Xo1Sbl31J6tSIBDwQAAAAAAQEf8EkCAAAAAAAWABRGM9WcrRNDR5ly4MmWkh0KXa22XSIGAkOg1A6d4IgnlBYXUrrkK6kJUnhCCfBjQw8K8lY1SoW7CAAAAIABAAAAARAE/v///wABAwhADQMAAAAAAAEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 150000, + "witness_utxo": "f0490200000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 200000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + } + ] + } + }, + { + "description": "in progress: one input / two sp outputs, input 0 missing ECDH share for output 0 when PSBT_OUT_SCRIPT field is not set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR9ADQMAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsIQMJH9SAORd/RNe3npYWiEnGLrP4eADEZ2fFCo84+Y79FSIeA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsQL4acjrvVeiYG5lZmpvBKzj4froBWMxwZokPImnK3kY7dzQal9KScZZNAcAkHMMi/VOQwpuZFg+CbzKTZYmMvwIAAQMIkF8BAAAAAAABCUIDZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp94D8nDgpWMhg7arAQdOh35oANsA9cfF0vr7to+cKNSFRSQAAQMIkF8BAAAAAAABBCJRIENYHfbIefmqrcU2YN6xVrnxIr0a7KczIDvOh/zx84buAQlCA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsAuDEwYe3IV+ZfISFp1tFkbbChHe5WhEjuZaSLTFUhgv4AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "0352d78c41390032ad91816a697fc740d8eb909cf04d70885264b051f2385e25ec", + "ecdh_share": "03091fd48039177f44d7b79e96168849c62eb3f87800c46767c50a8f38f98efd15", + "dleq_proof": "be1a723aef55e8981b99599a9bc12b38f87eba0158cc7066890f2269cade463b77341a97d29271964d01c0241cc322fd5390c29b99160f826f329365898cbf02", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de03f270e0a5632183b6ab01074e877e6800db00f5c7c5d2fafbb68f9c28d4854524" + }, + { + "output_index": 1, + "amount": 90000, + "sp_v0_info": "0352d78c41390032ad91816a697fc740d8eb909cf04d70885264b051f2385e25ec02e0c4c187b7215f997c8485a75b4591b6c28477b95a1123b996922d3154860bf8", + "script": "512043581df6c879f9aaadc53660deb156b9f122bd1aeca733203bce87fcf1f386ee" + } + ] + } + }, + { + "description": "in progress: large PSBT with nine mixed inputs / six outputs - some inputs signed", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQkBBQEGAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCLNThMVlDzsjgXC0oPt/Sorr8CiDHHC50NMY3FUceJxgIgMZmwKWN19OdCfS+4KMjLfkHblItuYaaIKCeY8x+RIogBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQP70XPZxI1YxQIkJtBSeLyhe5qOIoJmaQGhsAueBLeZSSIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQMKGLm7QZm4kzDrF/juWseG4IU9zS88zfF0S5fOTt4FPiIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQNzctF59PYo2oLMvU5CF72wgJ8UF8mr6qNfqAXUitZVTCIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQMBdpBBIfPrsN4/r5q9QBeOubIYjaBiw5VshQBfrUHPt70AXlXW13NqpBjSRrIuD/QYk1Bd6TS3UmUxGQNa4zN4iHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEAAE4PUZhvocl7ykEj0jJEfifXzQxfXwMCsxg72XxtSv/96ltgzm2Jd+Dq6tXqic+3NmjXHlNrLschhFxuyZwdfIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxApoctTrEZ/ir3CVP3UqIiNrVKO1f3DlEfEL2LQv9Wtaz2NlCTxIDqnxN3Z4SUW1kxisKSfZFariRM+2g/QYijpwABDiC+nWXIORX7CSSKu9/KjKPu5s41DFVPZejVJuXfUnq1IgEPBAAAAAABAR+ghgEAAAAAABYAFBOTUBF8V3lBqYWAK7nt78ETkyvHIgIC9bWfpeSSIh6/Vbp4rUQmBb6ulRZroeujJQ0Luqx+LtxHMEQCIGBe6cE9y5EoaPHeTsHHUaxLRis49eXxBj+EHthVABbpAiBHqaafS5jOmNbyz3jQjoWlVWhRC3tc+65oAn9FjH5+BQEBAwQBAAAAIgYC9bWfpeSSIh6/Vbp4rUQmBb6ulRZroeujJQ0Luqx+LtwIAAAAgAEAAAABEAT+////Ih0CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRAhA6aomxuYaplVM+F77WRQC1QbsNc5Bx7+KuK377CgymvPIh0DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bSghAvaue4qsmg+cnwTqtIQGIXLfLjjZ8vbsMbQEu1ooaqvwIh0DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwhArHT7S+kvDFqCOsUhfp17X3VTKeprhjQxKF9qT8ww4XVIh4CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRBAw0pqKUC76ac1bBzG/+NY3URB21iJfLqZwhLrdShefaHsoRx5LJejgEkzRasiTenZTCs27oUmriNSgXGjIx3nMyIeAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQDNKztgQbrqkKH2spWzKJnlfLSpzl4TPa3NNozcHV/SZ4SDA5AvkBDbNdMTvJpBXBvUvp/pujnZFHoWDeDBRqEMiHgOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobEDYQaNzrSiWuXK4z+oWNP8Uow+A2Ez9bvGIkzl7jzRsgFGcmxYaciRNDptk5UNsmM6d1OF6Byj8mXtce885zstoAAEOINBBx0LKNkRy0aGMeo6uB+bpBKnqG4XxmFJfuD7yduNDAQ8EAAAAAAEBH6CGAQAAAAAAFgAUIwf8d0yhdLQq2oFLwubkRP89gwwiAgJlXvXa8F0o0cpzuo4hEwDbK3BBZzcYPg+ow1XuTBy6xkcwRAIgKTOk1x6k+7U8mzjYjLK+bqIQKQBrzEx5rJgFGKrIYxkCICj9GcotjGhqD9CjsK6gmpqVgMw4oisFIgS+RXXwNa0/AQEDBAEAAAAiBgJlXvXa8F0o0cpzuo4hEwDbK3BBZzcYPg+ow1XuTBy6xggAAACAAgAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECED33Bv8m1nL+UMb1aiyh5xdOdQVckgu9M34afWvygm9twiHQMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKCEDv7hiU765b1i4xGnkQs/Pn3ilQTqVQjvo5oZ2Ty+nvT8iHQOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobCEDhgSqjP8wnd30mx82b4nCaWFlh/xB22CFQPlDFTb2ZG8iHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEED9LowiubZy07ueJtS22i3D8SVFWpaf2OKywbp5EHatcbOMAfkhjD1ndv2JXS8wZ0hJltheJoOQwrQz5sSyGJHxIh4DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bShAUt/78AlfKbHeJMnWjVSdHFS85pAEVP43/RD1yQPf7oX3utinKtqOUs2QkvUdnpC1p+TzbKBN+Y53+60TRzBbxyIeA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsQJPh5dSIi8HP4C7s+p3qFE6thPI6EPwaMBEDweVjk1J7Y0F8bEpa9FNZ/4tuF5r3OXjk1i6GLu3Soob1J45Ms0AAAQ4gUiYs1dQCvzJhbQf8Ayhqj3/nivesQR3/9BZV3cEBHm8BDwQAAAAAAQEfoIYBAAAAAAAWABRwGLzpZSiNi/DM8+DsSLbx27/lSCICAwOg9FKNJk6OlSVmNLlEFOJ0j3KWJwT+V+iwosxE0jg9SDBFAiEA2yW73RvPhQMJZj64j7uw62ETCVeF1/bVM42du8cFAycCIHmV0Cb1bqmeBz39CnlsbnT8Ye0I7NJGcM7zQNrVCueaAQEDBAEAAAAiBgMDoPRSjSZOjpUlZjS5RBTidI9ylicE/lfosKLMRNI4PQgAAACAAwAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECEDgCmZjHPCsTnbdVV+wwEexl7NISRHljAGVxhSo+kiicwiHQMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKCECmAuscmThS/ggzENbaaeN9ybBd+sCbxuBtS3oEScvWOsiHQOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobCEDon2fH6U+Q/cINaaZCVS3ijX3RwZve4K0qeqC6LteBqYiHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEEA4HOPBrJ/H9qUVNkViUphHaOj70BQHAjn0yxOxZ5PHTMXtZ3yKfqzBc+mI01Oj8onH4hQdt6S0FFxUmeyDfzauIh4DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bShAOOA0Y7ZFDb0FEGWoEqbyYxCH95UeRqyE5m2oEXVXF8CTGA5NSYJ7jAl8pwk/6S05P5Wl162EmkGALcTkVYq96SIeA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsQHMqoJV6nbLHWeOayl0BZ+8T0qNKQ8gS6cydcS1Fj3sihdp1EesW9h+WANIL5OlN/dK8OuN/2UP8NJR9/D/cZ5cAAQ4g4pjq8k/hymi0Hktrg8w9kc7ZnG541bmSz7FXcbVFcGQBDwQAAAAAAQEfoIYBAAAAAAAWABSTMrsCDLA9SetNBHEsHMCOKwbY2CICA6iRG+taSP4RnLAVCIzww6EYzwJnckSJctG3DHZKxZQSRzBEAiBr1hL2RzkhB3F8V6+V7oRzl08sMjurcHnoGRbsuQIcjgIgWlToLfcW0eQKehOGkN9w6tSjzILOEImugKMj+1/ui1UBAQMEAQAAACIGA6iRG+taSP4RnLAVCIzww6EYzwJnckSJctG3DHZKxZQSCAAAAIAEAAAAARAE/v///yIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQK+9HOMbzJ/FySxQHmIlZzfK9GSEIlL/QQ1NbYROCcIGCIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQJ+t8HEr81+Yqtq09K6JzOjdFIgZB+RkD6/57zF29xrXSIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQPKiLzbbR9SkekMddPaYBqL0pmR6rv3+UsvOUg+OFjJoSIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQApYSScQKCC3tsrWvPsDGiOjcxKth9I/HLWoNZvi1f85tqKi/jfNDVPuNH8T5rNSV2my54nwaZy5hViOrC2vEN0iHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEB3sVcjNtm9Ti9zQR649qliNaEw9ETL6wekifRHkh43SJ1x/T2yigu6o1uIw3t1eezb9s4WOHANuB38hr95zRvnIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxAXQ3oon8NUogCzzNUoa9Qe88P7UGdpkHJwIB811Tgevh5C1EccF1fv2MwWrIq/gSX+s8HC1r+r87jppAFR3Z3VgABDiAXMIM+y1vgE2LXFJUiYESEIW4cexjPMT0c8VQ++F3ztgEPBAAAAAABASughgEAAAAAACJRINFVcTxSDOLYJ2FkIV1lmrYjxfcwb4ZSwXVuJoM+iG4BAQMEAQAAAAEQBP7///8BFyDRVXE8Ugzi2CdhZCFdZZq2I8X3MG+GUsF1biaDPohuASIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQJPKhWksVprePCmMvx/3xEpKfZL1N0U9npjz0+48yWiBCIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQOAK2mzYNpFhVErHPlnsYQBAibXXOW3Yud6ASi8sGjMuyIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQJAdhVQXsveqIExJQTu9vRoGiYxbvc9M1N6f9eT9GWOqSIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQI8iPeJ5ht2HkqA9nVtmIyPoaXgw66u2c2OIH+cshkgq5hoJJo0IO4NUz8K601wUjuspGvYi3UYGNGBjMq8bgpciHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEAQOuBcEOQymDdsP+4dxfDQQ4n08gr0k0bUjq0yTWFdbyj8oKMYRFEw6laoYgGbLeDCY32zxBjAsHS1hUiuiyDJIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxA8FWZBzhUTQmbFZKQ5/VELLkSJiYBdKqdjByDELfLAc4rlMxdK4/fgWZ+Avt5bbDlwnh33g1BoqDnCbAURbrylgABDiDpNTLg7aOcTTnaLT+R0UUpvENfwu+5GW4l+6rEtOibgQEPBAAAAAABASughgEAAAAAACJRINAOFftian7+6FxbzmdtY2tgSJFOmjSs4LTLxEvJSAd6AQMEAQAAAAEQBP7///8BFyDQDhX7Ymp+/uhcW85nbWNrYEiRTpo0rOC0y8RLyUgHeiIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQIVP83+lq5wNj/RVCZG0wnCEyuDJVQOPdf4jjf5eWB5piIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQIPqjoMMa52ledpVG4wqngqvTH7aBuXChOqNcwh39E4ACIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQPzE/j0desB487LwOqsL3JtKQMsb/fDrWzDv4uxAYZSfCIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQEMg6fYP08Km9zeqzlRsI7DH126EFZwK0onB30wPilAOUSCfKW+dfSI6HSkr1B0mb6rLG+hDvs9LcG6xXbpwKAYiHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEDkB0puyJPquAbV35p07SV+eF/zeGQ9uqeTPHgTp7X2yQyoK2JTnzmzjCxy/lz4HCKl5kH2c2RPbEhrPbV3RkRjIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxAOynhLPaT8nITn3RFLyvMwrzQdDLc6PsQc1TBRYoSpHyFRNLWyh4zvOBXi2UIz4pZNGc/n8Q1pfhKIqVLWty8/AABDiC2Nou8Mwzd/p4lUO2338Fa50ysm8x9ln/oHm8n+Fl7HQEPBAAAAAABAFMCAAAAAf6hTz0WmysEANsUmWIhsxGCu56GyhaekbnHvZP5xz6IAAAAAAD/////AaCGAQAAAAAAF6kUDfEBNX9z5mWFAM9W86Ey17uK+92HAAAAAAEBIKCGAQAAAAAAF6kUDfEBNX9z5mWFAM9W86Ey17uK+92HIgICBQDxZ89mnn2sOeauzaHyjGa3jscJfu/vJdVPe+eF5/ZHMEQCIA134+usWENCsMr5fopp9YcHxKsAzK2+hsRKoYe1Zfv/AiBubgVutYtmJeL++r0axbRapqEto5ypCKEaIcY396JcMAEBAwQBAAAAAQQWABTGvIEF7G22DE2lOsNc2iBAaekOiiIGAgUA8WfPZp59rDnmrs2h8oxmt47HCX7v7yXVT3vnhef2CAAAAIAHAAAAARAE/v///yIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQL+eEHN37Rv2yypveVslVjC0bNhIWZWWQgCJOJAMUOECCIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQJvAnsjWnWTyolDxte3432S6rM5YX9/DDRBBsWN0MvGqiIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQIGEUUMbBlE7N7LePYYFsdn+6Q2jyH36bFpd5x28jz0zCIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQIgHJVFmLD7r7lRfGIuNr8aClzovIY58oH/I5C/1ehE0YwRJ/XAxd9ehLQyc7QJIRNwbng7K1nWH3VxHlDGzahoiHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEAWtYFI97r/eD5DEPGFUD/pJBfbpDuWbIOiGEMiyx8gQky50ECuahrqTnREvceGJYqXVkTOGgGVVkwrn8PGBqKSIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxASrNr3WnrHNDUsK6uQD3OTbUijFujopHccl3iM4LRc20mXXvDP4NpClylYJcZ8Z6BlS1MjV4oj9DLjGQREEuAowABDiADvOulN51z2ur+Z3lysD3PfvMLfxDqqDrQS8C/ANiy9wEPBAAAAAABAFUCAAAAASo0rBsNt3qBmMHikALMR7v81u2DCWAwTSyaOrVISGtdAAAAAAD/////AaCGAQAAAAAAGXapFFab8xHmijg3Z7Q/gwQt+r5gCT7eiKwAAAAAIgIDGZ0vFV8y67FnrrDMym5a+3NAiY3xt6uZNlAPbUORkCpHMEQCIGIn/SRuyIcF32NUW0i+PRLD1KVITUtOxDRAmsuEETxaAiA8y8m0eI77f7hg2oVYqUx+TEJa11XoaOLY8Fz7RhJflwEBAwQBAAAAIgYDGZ0vFV8y67FnrrDMym5a+3NAiY3xt6uZNlAPbUORkCoIAAAAgAgAAAABEAT+////Ih0CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRAhAvPBdbCLIHY3YWqnTme6rBoYB8lLznCAD3b1sXtSwAgtIh0DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bSghAvoc7ZMlmKJ/ooQQuVXqZ0ebWkAztp8smeTiZ2e7oKVWIh0DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwhAwDg/RcQ3FatfBc6qRQdcxV3AJvD3/y6hXakjnerNNlwIh4CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRBAmSJWWP7eeowkN+s4Ib3J03AKvIlui23O+E4pNHt4vCsD5tozg25PQHZzkMNmRM4Bazax1UaWNUD6UdCwQH221yIeAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQKCUjFF9cJdsDjnKZ6vxnnIXBA2+Fyu5ZTQTPm/6OUHBZZQW9UBmK3Vh77cAZLTXGpsXtjvFY/Yw20Qi8nPg3xIiHgOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobEDuCqplynPMxUezHCt+0q4xpPTM2upz8q3SRjDWCO3prqXYCGiBhuvKQrh376sT815g8hDU9iBgL4UPwHMcIB6RAAEDCDB1AAAAAAAAAQQiUSAa+l2IGIzvm5Gz8QArEeD56UrTmBl4xNEqBgVWTAgSMwEJQgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKAMSZlgePoQcu5TeYupzVYOiMsTIiPpu3vLsL4cHifwd1AABAwgwdQAAAAAAAAEEIlEgyr9+Sg3kJOybut1/ckLM52c+FwH0nWoZqXBV2jMrEyQBCUIDM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bSgDEmZYHj6EHLuU3mLqc1WDojLEyIj6bt7y7C+HB4n8HdQAAQMIIE4AAAAAAAABBCJRIEqtyeClkNIeDBnLICTycUD5sZwqEoLVDqfF9F6wP9SiAQlCAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQAkpI124g1AvPXy4aXD/+s9wqizuI15pIbVTJ2eqo+hiOAAEDCCBOAAAAAAAAAQQiUSAvpDcA6h0o/XrU3LMys+XloNjLVCWPZmHE3gN31ygE5QEJQgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEAJKSNduINQLz18uGlw//rPcKos7iNeaSG1UydnqqPoYjgABAwgQJwAAAAAAAAEEIlEgUZhvruMZQ2tRs4QJwbMA9DluKzLjzJoSripL4TXab24BCUIDjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwD+BRzhEyddMkh/xelr0ELYzc0jXr7/3OJG8XLDLJMsMAAAQMIUMMAAAAAAAABBCJRIG/FBvgFKp9aiqm5kNXplPSV3e1tR7rETKrNrdjYmRzQAA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 100000, + "witness_utxo": "a086010000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 2, + "private_key": "e6db4a04e2fc29745ea1a9dcf1fed7449c8228ffe0503d8171e64126d5399800", + "public_key": "02655ef5daf05d28d1ca73ba8e211300db2b70416737183e0fa8c355ee4c1cbac6", + "prevout_txid": "d041c742ca364472d1a18c7a8eae07e6e904a9ea1b85f198525fb83ef276e343", + "prevout_index": 0, + "prevout_scriptpubkey": "00142307fc774ca174b42ada814bc2e6e444ff3d830c", + "amount": 100000, + "witness_utxo": "a0860100000000001600142307fc774ca174b42ada814bc2e6e444ff3d830c", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 3, + "private_key": "f5208ed67b0019ba4e306aff029d247931e0dfacd1fe6e69091bc6bc70a42619", + "public_key": "0303a0f4528d264e8e95256634b94414e2748f72962704fe57e8b0a2cc44d2383d", + "prevout_txid": "52262cd5d402bf32616d07fc03286a8f7fe78af7ac411dfff41655ddc1011e6f", + "prevout_index": 0, + "prevout_scriptpubkey": "00147018bce965288d8bf0ccf3e0ec48b6f1dbbfe548", + "amount": 100000, + "witness_utxo": "a0860100000000001600147018bce965288d8bf0ccf3e0ec48b6f1dbbfe548", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 4, + "private_key": "0c19c34dc1005feb5ca60104d20472f7f2f97bd4d521264618196bf71155b20a", + "public_key": "03a8911beb5a48fe119cb015088cf0c3a118cf026772448972d1b70c764ac59412", + "prevout_txid": "e298eaf24fe1ca68b41e4b6b83cc3d91ced99c6e78d5b992cfb15771b5457064", + "prevout_index": 0, + "prevout_scriptpubkey": "00149332bb020cb03d49eb4d04712c1cc08e2b06d8d8", + "amount": 100000, + "witness_utxo": "a0860100000000001600149332bb020cb03d49eb4d04712c1cc08e2b06d8d8", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 5, + "private_key": "5aaa151cbdeb566911784d166b01ce57f35993b98f96846efec1cfefcc0d8f56", + "public_key": "02d155713c520ce2d8276164215d659ab623c5f7306f8652c1756e26833e886e01", + "prevout_txid": "1730833ecb5be01362d7149522604484216e1c7b18cf313d1cf1543ef85df3b6", + "prevout_index": 0, + "prevout_scriptpubkey": "5120d155713c520ce2d8276164215d659ab623c5f7306f8652c1756e26833e886e01", + "amount": 100000, + "witness_utxo": "a086010000000000225120d155713c520ce2d8276164215d659ab623c5f7306f8652c1756e26833e886e01", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 6, + "private_key": "d92c1506167b3a00a21f4d90d7b5ccb4713c00194aa66f9c7deb162c467f9643", + "public_key": "02d00e15fb626a7efee85c5bce676d636b6048914e9a34ace0b4cbc44bc948077a", + "prevout_txid": "e93532e0eda39c4d39da2d3f91d14529bc435fc2efb9196e25fbaac4b4e89b81", + "prevout_index": 0, + "prevout_scriptpubkey": "5120d00e15fb626a7efee85c5bce676d636b6048914e9a34ace0b4cbc44bc948077a", + "amount": 100000, + "witness_utxo": "a086010000000000225120d00e15fb626a7efee85c5bce676d636b6048914e9a34ace0b4cbc44bc948077a", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 7, + "private_key": "d8bc8d0e1233a97133f3573d6dceb6736e8b6d2e4839925145022ea3efdad8f8", + "public_key": "020500f167cf669e7dac39e6aecda1f28c66b78ec7097eefef25d54f7be785e7f6", + "prevout_txid": "b6368bbc330cddfe9e2550edb7dfc15ae74cac9bcc7d967fe81e6f27f8597b1d", + "prevout_index": 0, + "prevout_scriptpubkey": "a9140df101357f73e6658500cf56f3a132d7bb8afbdd87", + "amount": 100000, + "witness_utxo": "a08601000000000017a9140df101357f73e6658500cf56f3a132d7bb8afbdd87", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 8, + "private_key": "81e30f5b5d049bdf5c5bea9c3329bf57cc8b473240182a9f2a6eee07435bd54a", + "public_key": "03199d2f155f32ebb167aeb0ccca6e5afb7340898df1b7ab9936500f6d4391902a", + "prevout_txid": "03bceba5379d73daeafe677972b03dcf7ef30b7f10eaa83ad04bc0bf00d8b2f7", + "prevout_index": 0, + "prevout_scriptpubkey": "76a914569bf311e68a383767b43f83042dfabe60093ede88ac", + "amount": 100000, + "non_witness_utxo": "02000000012a34ac1b0db77a8198c1e29002cc47bbfcd6ed830960304d2c9a3ab548486b5d0000000000ffffffff01a0860100000000001976a914569bf311e68a383767b43f83042dfabe60093ede88ac00000000", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "030a18b9bb4199b89330eb17f8ee5ac786e0853dcd2f3ccdf1744b97ce4ede053e", + "dleq_proof": "001383d4661be8725ef29048f48c911f89f5f34317d7c0c0acc60ef65f1b52bfff7a96d8339b625df83abab57aa273edcd9a35c794dacbb1c861171bb267075f", + "input_index": 0 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03fbd173d9c48d58c5022426d05278bca17b9a8e2282666901a1b00b9e04b79949", + "dleq_proof": "c05da410487cfaec378febe6af5005e3ae6c86236818b0e55b214017eb5073edef40179575b5dcdaa9063491ac8b83fd0624d4177a4d2dd4994c4640d6b8ccde", + "input_index": 0 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "037372d179f4f628da82ccbd4e4217bdb0809f1417c9abeaa35fa805d48ad6554c", + "dleq_proof": "a6872d4eb119fe2af70953f752a22236b54a3b57f70e511f10bd8b42ff56b5acf6365093c480ea9f13776784945b59318ac2927d915aae244cfb683f4188a3a7", + "input_index": 0 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02f6ae7b8aac9a0f9c9f04eab484062172df2e38d9f2f6ec31b404bb5a286aabf0", + "dleq_proof": "334aced8106ebaa4287daca56cca26795f2d2a739784cf6b734da3370757f499e120c0e40be40436cd74c4ef26905706f52fa7fa6e8e76451e8583783051a843", + "input_index": 1 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03a6a89b1b986a995533e17bed64500b541bb0d739071efe2ae2b7efb0a0ca6bcf", + "dleq_proof": "c34a6a2940bbe9a7356c1cc6ffe358dd4441db58897cba99c212eb75285e7da1eca11c792c97a380493345ab224de9d94c2b36ee8526ae23528171a3231de733", + "input_index": 1 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "02b1d3ed2fa4bc316a08eb1485fa75ed7dd54ca7a9ae18d0c4a17da93f30c385d5", + "dleq_proof": "d841a373ad2896b972b8cfea1634ff14a30f80d84cfd6ef18893397b8f346c80519c9b161a72244d0e9b64e5436c98ce9dd4e17a0728fc997b5c7bcf39cecb68", + "input_index": 1 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "03bfb86253beb96f58b8c469e442cfcf9f78a5413a95423be8e686764f2fa7bd3f", + "dleq_proof": "52dffbf0095f29b1de24c9d68d549d1c54bce6900454fe37fd10f5c903dfee85f7bad8a72ada8e52cd9092f51d9e90b5a7e4f36ca04df98e77fbad1347305bc7", + "input_index": 2 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03df706ff26d672fe50c6f56a2ca1e7174e75055c920bbd337e1a7d6bf2826f6dc", + "dleq_proof": "fd2e8c22b9b672d3bb9e26d4b6da2dc3f125455a969fd8e2b2c1ba791076ad71b38c01f9218c3d6776fd895d2f3067484996d85e268390c2b433e6c4b21891f1", + "input_index": 2 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "038604aa8cff309dddf49b1f366f89c269616587fc41db608540f9431536f6646f", + "dleq_proof": "93e1e5d4888bc1cfe02eecfa9dea144ead84f23a10fc1a301103c1e56393527b63417c6c4a5af45359ff8b6e179af73978e4d62e862eedd2a286f5278e4cb340", + "input_index": 2 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02980bac7264e14bf820cc435b69a78df726c177eb026f1b81b52de811272f58eb", + "dleq_proof": "38e03463b6450dbd051065a812a6f2631087f7951e46ac84e66da811755717c093180e4d49827b8c097ca7093fe92d393f95a5d7ad849a41802dc4e4558abde9", + "input_index": 3 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "038029998c73c2b139db75557ec3011ec65ecd212447963006571852a3e92289cc", + "dleq_proof": "381ce3c1ac9fc7f6a51536456252984768e8fbd014070239f4cb13b16793c74cc5ed677c8a7eacc173e988d353a3f289c7e2141db7a4b4145c5499ec837f36ae", + "input_index": 3 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "03a27d9f1fa53e43f70835a6990954b78a35f747066f7b82b4a9ea82e8bb5e06a6", + "dleq_proof": "732aa0957a9db2c759e39aca5d0167ef13d2a34a43c812e9cc9d712d458f7b2285da7511eb16f61f9600d20be4e94dfdd2bc3ae37fd943fc34947dfc3fdc6797", + "input_index": 3 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "027eb7c1c4afcd7e62ab6ad3d2ba2733a3745220641f91903ebfe7bcc5dbdc6b5d", + "dleq_proof": "77b1572336d9bd4e2f73411eb8f6a96235a130f444cbeb07a489f447921e37489d71fd3db28a0bbaa35b88c37b7579ecdbf6ce1638700db81dfc86bf79cd1be7", + "input_index": 4 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02bef4738c6f327f1724b1407988959cdf2bd19210894bfd043535b61138270818", + "dleq_proof": "0a584927102820b7b6cad6bcfb031a23a37312ad87d23f1cb5a8359be2d5ff39b6a2a2fe37cd0d53ee347f13e6b3525769b2e789f0699cb985588eac2daf10dd", + "input_index": 4 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "03ca88bcdb6d1f5291e90c75d3da601a8bd29991eabbf7f94b2f39483e3858c9a1", + "dleq_proof": "5d0de8a27f0d528802cf3354a1af507bcf0fed419da641c9c0807cd754e07af8790b511c705d5fbf63305ab22afe0497facf070b5afeafcee3a6900547767756", + "input_index": 4 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "03802b69b360da4585512b1cf967b184010226d75ce5b762e77a0128bcb068ccbb", + "dleq_proof": "103ae05c10e43298376c3fee1dc5f0d04389f4f20af49346d48ead324d615d6f28fca0a318445130ea56a862019b2de0c2637db3c418c0b074b58548ae8b20c9", + "input_index": 5 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "024f2a15a4b15a6b78f0a632fc7fdf112929f64bd4dd14f67a63cf4fb8f325a204", + "dleq_proof": "8f223de27986dd8792a03d9d5b662323e8697830ebabb67363881fe72c86482ae61a09268d083b8354cfc2bad35c148eeb291af622dd460634606332af1b8297", + "input_index": 5 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "02407615505ecbdea881312504eef6f4681a26316ef73d33537a7fd793f4658ea9", + "dleq_proof": "f055990738544d099b159290e7f5442cb91226260174aa9d8c1c8310b7cb01ce2b94cc5d2b8fdf81667e02fb796db0e5c27877de0d41a2a0e709b01445baf296", + "input_index": 5 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "020faa3a0c31ae7695e769546e30aa782abd31fb681b970a13aa35cc21dfd13800", + "dleq_proof": "e4074a6ec893eab806d5df9a74ed257e785ff378643dbaa7933c7813a7b5f6c90ca82b62539f39b38c2c72fe5cf81c22a5e641f673644f6c486b3db577464463", + "input_index": 6 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02153fcdfe96ae70363fd1542646d309c2132b8325540e3dd7f88e37f9796079a6", + "dleq_proof": "4320e9f60fd3c2a6f737aace546c23b0c7d76e84159c0ad289c1df4c0f8a500e51209f296f9d7d223a1d292bd41d266faacb1be843becf4b706eb15dba702806", + "input_index": 6 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "03f313f8f475eb01e3cecbc0eaac2f726d29032c6ff7c3ad6cc3bf8bb10186527c", + "dleq_proof": "3b29e12cf693f272139f74452f2bccc2bcd07432dce8fb107354c1458a12a47c8544d2d6ca1e33bce0578b6508cf8a5934673f9fc435a5f84a22a54b5adcbcfc", + "input_index": 6 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "026f027b235a7593ca8943c6d7b7e37d92eab339617f7f0c344106c58dd0cbc6aa", + "dleq_proof": "16b58148f7baff783e4310f185503fe92417dba43b966c83a2184322cb1f20424cb9d040ae6a1aea4e7444bdc786258a975644ce1a0195564c2b9fc3c606a292", + "input_index": 7 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02fe7841cddfb46fdb2ca9bde56c9558c2d1b36121665659080224e24031438408", + "dleq_proof": "88072551662c3eebee545f188b8dafc682973a2f218e7ca07fc8e42ff57a1134630449fd703177d7a12d0c9ced024844dc1b9e0ecad67587dd5c479431b36a1a", + "input_index": 7 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "020611450c6c1944ecdecb78f61816c767fba4368f21f7e9b169779c76f23cf4cc", + "dleq_proof": "4ab36bdd69eb1cd0d4b0aeae403dce4db5228c5ba3a291dc725de23382d1736d265d7bc33f83690a5ca5609719f19e81952d4c8d5e288fd0cb8c6411104b80a3", + "input_index": 7 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02fa1ced932598a27fa28410b955ea67479b5a4033b69f2c99e4e26767bba0a556", + "dleq_proof": "a0948c517d70976c0e39ca67abf19e7217040dbe172bb96534133e6ffa3941c1659416f540662b7561efb70064b4d71a9b17b63bc563f630db4422f273e0df12", + "input_index": 8 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02f3c175b08b207637616aa74e67baac1a1807c94bce70800f76f5b17b52c0082d", + "dleq_proof": "99225658fede7a8c2437eb3821bdc9d3700abc896e8b6dcef84e29347b78bc2b03e6da33836e4f40767390c36644ce016b36b1d546963540fa51d0b0407db6d7", + "input_index": 8 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "0300e0fd1710dc56ad7c173aa9141d731577009bc3dffcba8576a48e77ab34d970", + "dleq_proof": "ee0aaa65ca73ccc547b31c2b7ed2ae31a4f4ccdaea73f2add24630d608ede9aea5d808688186ebca42b877efab13f35e60f210d4f620602f850fc0731c201e91", + "input_index": 8 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 30000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "51201afa5d88188cef9b91b3f1002b11e0f9e94ad3981978c4d12a0605564c081233" + }, + { + "output_index": 1, + "amount": 30000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "5120cabf7e4a0de424ec9bbadd7f7242cce7673e1701f49d6a19a97055da332b1324" + }, + { + "output_index": 2, + "amount": 20000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "51204aadc9e0a590d21e0c19cb2024f27140f9b19c2a1282d50ea7c5f45eb03fd4a2" + }, + { + "output_index": 3, + "amount": 20000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "51202fa43700ea1d28fd7ad4dcb332b3e5e5a0d8cb54258f6661c4de0377d72804e5" + }, + { + "output_index": 4, + "amount": 10000, + "sp_v0_info": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c03f81473844c9d74c921ff17a5af410b6337348d7afbff73891bc5cb0cb24cb0c0", + "script": "512051986faee319436b51b38409c1b300f4396e2b32e3cc9a12ae2a4be135da6f6e" + }, + { + "output_index": 5, + "amount": 50000, + "script": "51206fc506f8052a9f5a8aa9b990d5e994f495dded6d47bac44caacdadd8d8991cd0" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/tests/data/test_vectors_generate_proof.csv b/tests/tests/data/test_vectors_generate_proof.csv new file mode 100644 index 00000000..78d78704 --- /dev/null +++ b/tests/tests/data/test_vectors_generate_proof.csv @@ -0,0 +1,12 @@ +index,point_G,scalar_a,point_B,auxrand_r,message,result_proof,comment +0,02cef38f55e78b321a1f785cb1c6e33dfcef9784c18bdc4e279801c449ccdfb88e,07ff93d43f1012a5d4a44aba55240212ed39c87b3344e46757d99f24177fc576,02dad4b35c2379ba8334c9a5dda8f6e6d5cd575a7cc9d3ca4faaac51839daaa30f,cb979b0fc8ccc7f237751e719d992fcc324b6500af33999cd54a3e5c05fb1ea4,efb07d4b382d3da1079fbf24df623ba6c2e4c764993bbfa6dd7a4fe4aaf33859,7e7e934169e0bf4706e6b29e5a621c7fe199a524744a25af80071e111c0e2e94118e730d8add118dd2ee4f7d1cc183e1b87168362d1a6f85c16d8671a3fc7a8a,Success case 1 +1,02464e351831efedb755223cabbf664f10564b4742c725c023034bc928ed339e0e,f4e9172285393c6ada994c811b3e50fc47e96421ea7e54f4a4e459528d4cf562,03fe589b0fa23f060f6d4d1e76b9b19d5bb3db0e56d39a4303913de0e706463008,75f12482b9209dae12230ea1f8bf69723a1b447d361db8f510dd9ab33556fd4c,76184ce9eea5b339ebf5304b57452c1ada1466610f0a58574d6c496798cee04b,6b4521a8363a7ebc5d95ac6ec6b64db81fcf21795187d7c4600c42b73fb4fb9870ab8d106c0fd2d292c1710e10437b20575ddb3cb32eb77a5618d94ddba600f2,Success case 2 +2,0222db2054fef98344352a13bc0304a71da7b5e9a2f7fd1f3c9f3519a3d9377fb7,589476913e763b60d5c2a5bfb39230ec669caac1b44312e9bcd2d3f4473abfef,03bc7a19970c812118f74ba659b491e00dade6096ff62d1afe032a92b8671498ed,4da1c4c4b0f9db4eb6b2e5cb648d7e8a0aa35aa5c4ec4d07f096e0e03deca366,66503623468a78cfcef47888c85e0010ecd897f441d263448bfc7a89b882ab20,12aa2aa469b3c037871a09d18ab18d3840219b1ed169f6ef9deae6d927949884a459705ae89a57522224ce3482dee00a41ba511188ae60efdeb736223eb66e7b,Success case 3 +3,03dfa65bd3711eba75fa1996a0c1d95a4419bd835304152d9aa6efa590670f2af6,24d0ed3fc189eb1b64e5dc9dd4af0f3c8c143b0c79cb5fcca0dfa08a11cc60a1,03b51081323d38fb0b75f0c1ec6755fdb79c239c327ca11269fe68ba8a878b704e,31a68d6db27f6404bbceff646ff1b26a34704a0105a36c5a845d0257cea19c9b,f2996b3766d123a949e65541baf1d89d446360d05af51bd93f0445d8c472c952,7907653d29c5722ae44510e7f2839f253450aefc833b7e0a3b38384032f847f2cf41136b2fe6a558ad125287d20c0117f2a30c4ac0c4cebbcfa1dd3a69d84200,Success case 4 +4,02b15de5a3aefcfe2473916c76e619b5800ac7250ef93a9e6e0dd1505104fc58e7,73fffa796edb72d111b5e0bbda1608f098ac98120796f971b438691e1bfb7b96,03a4692be176ff89a972de9cc407083096847b950d1cae72b947665a3d5f4c2f01,1cdfb4d7cce5e50783299896a471a44e6aa2c5e2100d6c37987c6b40503c6162,0ceb45f560f2cf6b76a139ffe2c47c5ca6d26d6a3a210e59f197413bbec040b4,02277ca5a7acfe8ae13c2db4a8f74489d0ba100ed8b082381ddb6522c4510718ab88b8dbbd785c388ade79586cf6416f3c47a79670af84abccc788a5d9f2e327,Success case 5 +5,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,c08ca8e0bb59769fc6a4e078456284e00ea34f65add988c246e1bba85824ccdc,034bccb1c570ac1f3bc42d61fe35de605b99626501ccb20297e1acbbf2d7152aa1,c8d7056abd4726eb5a0f198740af14d6c1f0c16e5d7a37eaec621b661e669ac4,,503562d36910cd2d61a4d07c8ff680265c713e63dde0dcb88e6ea3c58597bdc05b86db9af95eccc475ce2177f941c118fefed20227d4ce8ce9557cb008758de6,Success case 6 +6,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,8e641ba6bf7f64eec76005a29585a5035376375f33e331215aedfe03b8e80e7a,0231c64e3efa506fdad6aad0f6084d5f6739de7f448d7e66f9d22f842638f41d60,02a7b2e2f5a5e9b1078dbb160502a32491fe80a091e91dd92cf77b0b7d90970f,35841ca532846e1cdd23a3d107824343584f88eff580929469865eae8355ee3c,5c7b27a33210750e9de8679d9f43497cf9f12ac642cde0a1fc26443aa2fc89bf71aabf7bac89f5d8a96cbe86daba155fa74d6f3e111136179e53b04eb6d7807f,Success case 7 +7,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,cfb9a7ecc49bea4f2e2ee34c38a6f48b5cd5bd06f4e4d4ffb45905b3d26db842,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,Success case 8 +8,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,0000000000000000000000000000000000000000000000000000000000000000,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,INVALID,Failure case (a=0) +9,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,INVALID,Failure case (a=N [group order]) +10,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,cfb9a7ecc49bea4f2e2ee34c38a6f48b5cd5bd06f4e4d4ffb45905b3d26db842,INFINITY,d38466b77484154a3fcb3151094c1c8a845c73a3c036b3a8ebffd8ef62c9047f,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,INVALID,Failure case (B is point at infinity) diff --git a/tests/tests/data/test_vectors_verify_proof.csv b/tests/tests/data/test_vectors_verify_proof.csv new file mode 100644 index 00000000..4ffe455a --- /dev/null +++ b/tests/tests/data/test_vectors_verify_proof.csv @@ -0,0 +1,16 @@ +index,point_G,point_A,point_B,point_C,proof,message,result_success,comment +0,02cef38f55e78b321a1f785cb1c6e33dfcef9784c18bdc4e279801c449ccdfb88e,02b540b22c2c5ef0dc886abdaad27498453d893265560bc08a187319af6f845f58,02dad4b35c2379ba8334c9a5dda8f6e6d5cd575a7cc9d3ca4faaac51839daaa30f,03fefe00951dcd0ef10b12523393c2b8113119de4fdeeab320694e96bdccd2775b,7e7e934169e0bf4706e6b29e5a621c7fe199a524744a25af80071e111c0e2e94118e730d8add118dd2ee4f7d1cc183e1b87168362d1a6f85c16d8671a3fc7a8a,efb07d4b382d3da1079fbf24df623ba6c2e4c764993bbfa6dd7a4fe4aaf33859,TRUE,Success case 1 +1,02464e351831efedb755223cabbf664f10564b4742c725c023034bc928ed339e0e,032baaf1b10845a51b551196984a91efe2adf9d41b92bec3927218e6e4ca344002,03fe589b0fa23f060f6d4d1e76b9b19d5bb3db0e56d39a4303913de0e706463008,031f59aa1df22190e00380d8c5941adf899f596593765a1251005fd24f2bf7c884,6b4521a8363a7ebc5d95ac6ec6b64db81fcf21795187d7c4600c42b73fb4fb9870ab8d106c0fd2d292c1710e10437b20575ddb3cb32eb77a5618d94ddba600f2,76184ce9eea5b339ebf5304b57452c1ada1466610f0a58574d6c496798cee04b,TRUE,Success case 2 +2,0222db2054fef98344352a13bc0304a71da7b5e9a2f7fd1f3c9f3519a3d9377fb7,026aa26fcd626f8f55295859e9f8dd1f103149dd64d77c2bbba1bcf33bb37ebaa2,03bc7a19970c812118f74ba659b491e00dade6096ff62d1afe032a92b8671498ed,035628d1a69910daef614c7cae68d71ece55c5908af2360629e25c1b7de21eeb4b,12aa2aa469b3c037871a09d18ab18d3840219b1ed169f6ef9deae6d927949884a459705ae89a57522224ce3482dee00a41ba511188ae60efdeb736223eb66e7b,66503623468a78cfcef47888c85e0010ecd897f441d263448bfc7a89b882ab20,TRUE,Success case 3 +3,03dfa65bd3711eba75fa1996a0c1d95a4419bd835304152d9aa6efa590670f2af6,031bf61ba89009ee1266c9003a72e8e07d77877678ccda7f15325aadcd64ed186b,03b51081323d38fb0b75f0c1ec6755fdb79c239c327ca11269fe68ba8a878b704e,02d1b1f37a80217ba73785babfa63251052775f9d3ca65060054033288b7a3f66b,7907653d29c5722ae44510e7f2839f253450aefc833b7e0a3b38384032f847f2cf41136b2fe6a558ad125287d20c0117f2a30c4ac0c4cebbcfa1dd3a69d84200,f2996b3766d123a949e65541baf1d89d446360d05af51bd93f0445d8c472c952,TRUE,Success case 4 +4,02b15de5a3aefcfe2473916c76e619b5800ac7250ef93a9e6e0dd1505104fc58e7,0296c8e00dda60bb5565b77371ff913091978646b58ccf218bc591f68a75232e6e,03a4692be176ff89a972de9cc407083096847b950d1cae72b947665a3d5f4c2f01,03a7a7f9527fd387c2b2ce0c76669d646c78a3b470a4b34d3a2dabafc8505ef472,02277ca5a7acfe8ae13c2db4a8f74489d0ba100ed8b082381ddb6522c4510718ab88b8dbbd785c388ade79586cf6416f3c47a79670af84abccc788a5d9f2e327,0ceb45f560f2cf6b76a139ffe2c47c5ca6d26d6a3a210e59f197413bbec040b4,TRUE,Success case 5 +5,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,02637b2c3ea8ca80b9caecc50f4134c86ae9cf7a269133e7afc71f30e3a3cda60c,034bccb1c570ac1f3bc42d61fe35de605b99626501ccb20297e1acbbf2d7152aa1,0285b826c8dd175805901906b6c9b4140a30cbcc94c6e7dcf36476038bf90d4718,503562d36910cd2d61a4d07c8ff680265c713e63dde0dcb88e6ea3c58597bdc05b86db9af95eccc475ce2177f941c118fefed20227d4ce8ce9557cb008758de6,,TRUE,Success case 6 +6,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,02983a72b4cb44d4322641a7b2001900cd6ae0908a610546c73ed126accdba0514,0231c64e3efa506fdad6aad0f6084d5f6739de7f448d7e66f9d22f842638f41d60,03af1bc14b384eda28398df6a7900e567c5b6f6613cafce5027b98be015286f71b,5c7b27a33210750e9de8679d9f43497cf9f12ac642cde0a1fc26443aa2fc89bf71aabf7bac89f5d8a96cbe86daba155fa74d6f3e111136179e53b04eb6d7807f,35841ca532846e1cdd23a3d107824343584f88eff580929469865eae8355ee3c,TRUE,Success case 7 +7,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,TRUE,Success case 8 +8,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 1 +9,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 2 +10,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 3 +11,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 4 +12,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Swapped points case 5 +13,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131ff090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb2d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Tampered proof (random bit-flip) +14,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,03611410561c35dae13135e4ad8094baac9bbcf2f4e18498181a8ff8a6d43be9d9,021cb81121a00f89769903305a367ad3cc02d5b402b12c026e06ac94bde28cd608,03d9a98624c0c74fc7eebd39ed84175f80d03c774908e75ca737a0745d1c64e20a,78a5544afa75bf152653fe55fb76926f2f65131bf090972a0b0b37d310c28a6bde0e7bfacc10ac12d36f55316ba134b6ba0b844a65ae05cad53c0b296c6639bb,22616bb5fb6d7c68270f305122f2a09e833239c4b1c9a04e285119fb606ac794,FALSE,Tampered message (random bit-flip) diff --git a/tests/tests/test_bech32.py b/tests/tests/test_bech32.py index 7d71fb17..e87809f4 100644 --- a/tests/tests/test_bech32.py +++ b/tests/tests/test_bech32.py @@ -99,9 +99,11 @@ def segwit_scriptpubkey(witver, witprog): ("bc", 16, 41), ] +SILENT_PAYMENTS_ADDRESS = "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + class Bech32Test(TestCase): - """Unit test class for segwit addressess.""" + """Unit test class for segwit addresses.""" def test_valid_checksum(self): """Test checksum creation and validation.""" @@ -110,14 +112,14 @@ def test_valid_checksum(self): self.assertIsNotNone(hrp) pos = test.rfind("1") test = test[: pos + 1] + chr(ord(test[pos + 1]) ^ 1) + test[pos + 2 :] - enc, hrp, _ = segwit_addr.bech32_decode(test) - self.assertIsNone(hrp) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.bech32_decode(test) def test_invalid_checksum(self): """Test validation of invalid checksums.""" for test in INVALID_CHECKSUM: - enc, hrp, _ = segwit_addr.bech32_decode(test) - self.assertIsNone(hrp) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.bech32_decode(test) def test_valid_address(self): """Test whether valid addresses decode to the correct output.""" @@ -133,13 +135,66 @@ def test_valid_address(self): def test_invalid_address(self): """Test whether invalid addresses fail to decode.""" for test in INVALID_ADDRESS: - witver, _ = segwit_addr.decode("bc", test) - self.assertIsNone(witver) - witver, _ = segwit_addr.decode("tb", test) - self.assertIsNone(witver) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.decode("bc", test) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.decode("tb", test) def test_invalid_address_enc(self): """Test whether address encoding fails on invalid input.""" for hrp, version, length in INVALID_ADDRESS_ENC: - code = segwit_addr.encode(hrp, version, [0] * length) - self.assertIsNone(code) + with self.assertRaises(segwit_addr.Bech32DecodeError): + segwit_addr.encode(hrp, version, [0] * length) + + def test_silent_payments_address(self): + """Test decoding of Silent Payments address (Bech32m format).""" + encoding, hrp, data = segwit_addr.bech32_decode(SILENT_PAYMENTS_ADDRESS) + self.assertIsNotNone(hrp) + self.assertEqual(hrp, "sp") + self.assertEqual(encoding, segwit_addr.Encoding.BECH32M) + + def test_bech32_decode_exceptions(self): + """Test exception cases for bech32_decode.""" + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1qw!@#$%") + self.assertIn("Data part contains invalid characters", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("BC1qwgHUDx") + self.assertIn("Mixed case", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("nonseparatoraddress") + self.assertIn("Separator '1' not found", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1short") + self.assertIn("Data part too short", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1qinvalidchar") + self.assertIn("Data part contains invalid characters", str(context.exception)) + + with self.assertRaises(segwit_addr.Bech32DecodeError) as context: + segwit_addr.bech32_decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5") + self.assertIn("Checksum verification failed", str(context.exception)) + + +class Bech32RoundTripTest(TestCase): + def test_round_trip(self): + """Test round-trip encoding/decoding for various payload sizes.""" + test_cases = [ + ("test", []), # empty payload + ("ln", [0] * 100), # medium + ("hrp-with-dashes", list(range(31))), # full charset + ] + + for hrp, payload in test_cases: + for encoding in [segwit_addr.Encoding.BECH32, segwit_addr.Encoding.BECH32M]: + encoded = segwit_addr.bech32_encode(encoding, hrp, payload) + decoded_enc, decoded_hrp, decoded_data = segwit_addr.bech32_decode( + encoded + ) + + self.assertEqual(decoded_enc, encoding) + self.assertEqual(decoded_hrp, hrp.lower()) diff --git a/tests/tests/test_bindings.py b/tests/tests/test_bindings.py index d0cbc519..52227e2a 100644 --- a/tests/tests/test_bindings.py +++ b/tests/tests/test_bindings.py @@ -1,4 +1,4 @@ -from binascii import hexlify +from binascii import hexlify, unhexlify from unittest import TestCase, skipUnless from embit.util import py_secp256k1 @@ -113,6 +113,45 @@ def test_cross(self): ctypes_secp256k1.ec_pubkey_add(pub1, b"9" * 32), ) + # ec_pubkey_tweak_mul mutates in place. The ctypes binding's c_char_p + # argtype requires `bytes`; the py_secp256k1 implementation requires a + # mutable `bytearray`. Pass each the type it expects, then compare. + tweak = b"9" * 32 + pub_py = bytearray(pub1) + pub_c = ctypes_secp256k1._copy(pub1) # mutated via the CPython ctypes quirk + py_secp256k1.ec_pubkey_tweak_mul(pub_py, tweak) + ctypes_secp256k1.ec_pubkey_tweak_mul(pub_c, tweak) + self.assertEqual(bytes(pub_py), pub_c) + pub_c_mutable = bytearray(pub1) + ctypes_secp256k1.ec_pubkey_tweak_mul(pub_c_mutable, bytearray(tweak)) + self.assertEqual(bytes(pub_py), bytes(pub_c_mutable)) + + secret_py = bytearray(secret) + secret_c = ctypes_secp256k1._copy(secret) + py_secp256k1.ec_privkey_tweak_mul(secret_py, tweak) + ctypes_secp256k1.ec_privkey_tweak_mul(secret_c, tweak) + self.assertEqual(bytes(secret_py), secret_c) + secret_c_mutable = bytearray(secret) + ctypes_secp256k1.ec_privkey_tweak_mul(secret_c_mutable, bytearray(tweak)) + self.assertEqual(bytes(secret_py), bytes(secret_c_mutable)) + + # ec_pubkey_combine takes varargs and returns a new pubkey. + pub_other = py_secp256k1.ec_pubkey_create(b"7" * 32) + self.assertEqual( + py_secp256k1.ec_pubkey_combine(pub1, pub_other), + ctypes_secp256k1.ec_pubkey_combine(pub1, pub_other), + ) + self.assertEqual( + ctypes_secp256k1.ec_pubkey_combine(pub1, pub_other), + ctypes_secp256k1.ec_pubkey_combine(bytearray(pub1), bytearray(pub_other)), + ) + # Three-arg combine. + pub_third = py_secp256k1.ec_pubkey_create(b"3" * 32) + self.assertEqual( + py_secp256k1.ec_pubkey_combine(pub1, pub_other, pub_third), + ctypes_secp256k1.ec_pubkey_combine(pub1, pub_other, pub_third), + ) + @skipUnless( _SCHNORR_AVAILABLE, "ctypes backend missing schnorr symbols; skipping parity checks", @@ -170,3 +209,320 @@ def test_recovery(self): pub = ctypes_secp256k1.ecdsa_recover(sig + bytes([i]), msg) pub2 = py_secp256k1.ecdsa_recover(sig + bytes([i]), msg) self.assertEqual(pub, pub2) + + +# Curve order N. Used to construct overflow/zero tweaks. +_SECP256K1_ORDER = ( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +) + + +def _tweak_mul_result_x(pubkey_compressed_hex, tweak_hex): + """Helper: parse a compressed pubkey, multiply by tweak, return x-coord hex.""" + pub = bytearray(py_secp256k1.ec_pubkey_parse(unhexlify(pubkey_compressed_hex))) + py_secp256k1.ec_pubkey_tweak_mul(pub, unhexlify(tweak_hex)) + return py_secp256k1.ec_pubkey_serialize(bytes(pub))[1:33].hex() + + +class PrivkeyTweakMulTest(TestCase): + """Standalone tests for py_secp256k1.ec_privkey_tweak_mul.""" + + def test_identity_tweak_one_unchanged(self): + secret = bytearray(b"2" * 32) + expected = bytes(secret) + py_secp256k1.ec_privkey_tweak_mul(secret, (1).to_bytes(32, "big")) + self.assertEqual(bytes(secret), expected) + + def test_multiplies_scalar_mod_order(self): + for secret_int, tweak_int in ( + (1, 2), + (2, 3), + (12345, 0xDEADBEEF), + (_SECP256K1_ORDER - 1, _SECP256K1_ORDER - 1), + ): + with self.subTest(secret_int=secret_int, tweak_int=tweak_int): + secret = bytearray(secret_int.to_bytes(32, "big")) + tweak = tweak_int.to_bytes(32, "big") + py_secp256k1.ec_privkey_tweak_mul(secret, tweak) + expected = (secret_int * tweak_int) % _SECP256K1_ORDER + self.assertEqual(bytes(secret), expected.to_bytes(32, "big")) + + def test_public_key_matches_tweaked_secret(self): + secret = bytearray(b"5" * 32) + tweak = b"9" * 32 + pub = bytearray(py_secp256k1.ec_pubkey_create(secret)) + + py_secp256k1.ec_privkey_tweak_mul(secret, tweak) + py_secp256k1.ec_pubkey_tweak_mul(pub, tweak) + + self.assertEqual(bytes(pub), py_secp256k1.ec_pubkey_create(secret)) + + def test_rejects_zero_secret(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(32), b"\x01" * 32) + + def test_rejects_zero_tweak(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(b"4" * 32), b"\x00" * 32) + + def test_rejects_overflow_secret_or_tweak(self): + for secret_int, tweak_int in ( + (_SECP256K1_ORDER, 1), + (_SECP256K1_ORDER + 1, 1), + (1, _SECP256K1_ORDER), + (1, _SECP256K1_ORDER + 1), + (2**256 - 1, 1), + (1, 2**256 - 1), + ): + with self.subTest(secret_int=secret_int, tweak_int=tweak_int): + secret = bytearray(secret_int.to_bytes(32, "big")) + tweak = tweak_int.to_bytes(32, "big") + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(secret, tweak) + + def test_rejects_wrong_lengths(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(31), b"\x01" * 32) + with self.assertRaises(ValueError): + py_secp256k1.ec_privkey_tweak_mul(bytearray(b"4" * 32), b"\x01" * 31) + + +class TweakMutationTypeTest(TestCase): + def test_rejects_immutable_private_key_for_add(self): + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_privkey_tweak_add(b"2" * 32, b"\x01" * 32) + + def test_rejects_immutable_public_key_for_add(self): + pub = py_secp256k1.ec_pubkey_create(b"2" * 32) + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_pubkey_tweak_add(pub, b"\x01" * 32) + + def test_rejects_immutable_private_key_for_mul(self): + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_privkey_tweak_mul(b"2" * 32, b"\x01" * 32) + + def test_rejects_immutable_public_key_for_mul(self): + pub = py_secp256k1.ec_pubkey_create(b"2" * 32) + with self.assertRaisesRegex(TypeError, "bytearray"): + py_secp256k1.ec_pubkey_tweak_mul(pub, b"\x01" * 32) + + +class PubkeyTweakMulTest(TestCase): + """Standalone tests for py_secp256k1.ec_pubkey_tweak_mul. + + Runs without libsecp256k1 — covers correctness via algebraic identities + and canonical BIP-47 spec vectors. + + Spec reference: + secp256k1_ec_pubkey_tweak_mul (libsecp256k1 public API) + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/include/secp256k1.h#L830-L846 + + Tweak validity is defined by secp256k1_ec_seckey_verify (tweak must be a + 32-byte big-endian integer in the range [1, N-1] where N is the curve order): + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/include/secp256k1.h#L721-L729 + """ + + def test_identity_tweak_one_unchanged(self): + """tweak_mul(P, 1) == P — multiplicative identity of the scalar group.""" + secret = b"2" * 32 + pub = py_secp256k1.ec_pubkey_create(secret) + ba = bytearray(pub) + py_secp256k1.ec_pubkey_tweak_mul(ba, (1).to_bytes(32, "big")) + self.assertEqual(bytes(ba), pub) + + def test_generator_times_k_equals_pubkey_create_k(self): + """tweak_mul(G, k) == ec_pubkey_create(k) for valid scalar k. + + ec_pubkey_create(k) is defined as k*G (G = curve generator); so + tweak_mul applied to G must yield the same point. + """ + for k in (1, 2, 3, 12345, 0xDEADBEEF, _SECP256K1_ORDER - 1): + with self.subTest(k=k): + G = py_secp256k1.ec_pubkey_create((1).to_bytes(32, "big")) + ba = bytearray(G) + py_secp256k1.ec_pubkey_tweak_mul(ba, k.to_bytes(32, "big")) + self.assertEqual( + bytes(ba), py_secp256k1.ec_pubkey_create(k.to_bytes(32, "big")) + ) + + def test_double_tweak_composes(self): + """tweak_mul(tweak_mul(P, a), b) == tweak_mul(P, a*b mod N). + + Associativity of scalar multiplication on the curve. + """ + secret = b"5" * 32 + P = py_secp256k1.ec_pubkey_create(secret) + a = int.from_bytes(b"\x03" * 32, "big") + b = int.from_bytes(b"\x07" * 32, "big") + + # Sequential a then b. + ba = bytearray(P) + py_secp256k1.ec_pubkey_tweak_mul(ba, a.to_bytes(32, "big")) + py_secp256k1.ec_pubkey_tweak_mul(ba, b.to_bytes(32, "big")) + + # Single combined scalar. + bc = bytearray(P) + ab = (a * b) % _SECP256K1_ORDER + py_secp256k1.ec_pubkey_tweak_mul(bc, ab.to_bytes(32, "big")) + + self.assertEqual(bytes(ba), bytes(bc)) + + def test_rejects_zero_tweak(self): + """tweak == 0 must fail: 0*P = point-at-infinity (not a valid pubkey). + + Matches libsecp256k1 behavior: tweak is rejected by seckey_verify, so + tweak_mul returns 0 (we raise ValueError). See upstream test at + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/src/tests.c#L6325 + """ + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + ba = bytearray(pub) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(ba, b"\x00" * 32) + + def test_rejects_overflow_tweak(self): + """tweak >= curve order N must fail (no canonical representative). + + See upstream test at + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/src/tests.c#L6357 + """ + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + for t in (_SECP256K1_ORDER, _SECP256K1_ORDER + 1, 2**256 - 1): + with self.subTest(t=t): + ba = bytearray(pub) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(ba, t.to_bytes(32, "big")) + + def test_rejects_wrong_pubkey_length(self): + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(bytearray(63), b"\x01" * 32) + + def test_rejects_wrong_tweak_length(self): + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_tweak_mul(bytearray(pub), b"\x01" * 31) + + def test_bip47_spec_vectors(self): + """Canonical BIP-47 ECDH vectors: x-coord of (Alice's a0 * Bob's Bi). + + BIP-47 (Reusable Payment Codes) defines the shared secret as a*B where + a is the payer's private key and B is the recipient's derived pubkey. + These vectors are the X coordinate of that curve point — exactly what + ec_pubkey_tweak_mul computes. + + Spec: https://github.com/bitcoin/bips/blob/master/bip-0047.mediawiki + Test vectors: https://gist.github.com/SamouraiDev/6aad669604c5930864bd + """ + a0 = unhexlify( + "8d6a8ecd8ee5e0042ad0cb56e3a971c760b5145c3917a8e7beaf0ed92d7a520c" + ) + vectors = [ + ( + "024ce8e3b04ea205ff49f529950616c3db615b1e37753858cc60c1ce64d17e2ad8", + "f5bb84706ee366052471e6139e6a9a969d586e5fe6471a9b96c3d8caefe86fef", + ), + ( + "03e092e58581cf950ff9c8fc64395471733e13f97dedac0044ebd7d60ccc1eea4d", + "adfb9b18ee1c4460852806a8780802096d67a8c1766222598dc801076beb0b4d", + ), + ( + "029b5f290ef2f98a0462ec691f5cc3ae939325f7577fcaf06cfc3b8fc249402156", + "79e860c3eb885723bb5a1d54e5cecb7df5dc33b1d56802906762622fa3c18ee5", + ), + ( + "02094be7e0eef614056dd7c8958ffa7c6628c1dab6706f2f9f45b5cbd14811de44", + "d8339a01189872988ed4bd5954518485edebf52762bf698b75800ac38e32816d", + ), + ( + "031054b95b9bc5d2a62a79a58ecfe3af000595963ddc419c26dab75ee62e613842", + "14c687bc1a01eb31e867e529fee73dd7540c51b9ff98f763adf1fc2f43f98e83", + ), + ( + "03dac6d8f74cacc7630106a1cfd68026c095d3d572f3ea088d9a078958f8593572", + "725a8e3e4f74a50ee901af6444fb035cb8841e0f022da2201b65bc138c6066a2", + ), + ( + "02396351f38e5e46d9a270ad8ee221f250eb35a575e98805e94d11f45d763c4651", + "521bf140ed6fb5f1493a5164aafbd36d8a9e67696e7feb306611634f53aa9d1f", + ), + ( + "039d46e873827767565141574aecde8fb3b0b4250db9668c73ac742f8b72bca0d0", + "5f5ecc738095a6fb1ea47acda4996f1206d3b30448f233ef6ed27baf77e81e46", + ), + ( + "038921acc0665fd4717eb87f81404b96f8cba66761c847ebea086703a6ae7b05bd", + "1e794128ac4c9837d7c3696bbc169a8ace40567dc262974206fcf581d56defb4", + ), + ( + "03d51a06c6b48f067ff144d5acdfbe046efa2e83515012cf4990a89341c1440289", + "fe36c27c62c99605d6cd7b63bf8d9fe85d753592b14744efca8be20a4d767c37", + ), + ] + for i, (B_hex, expected_x_hex) in enumerate(vectors): + with self.subTest(i=i): + self.assertEqual( + _tweak_mul_result_x(B_hex, a0.hex()), expected_x_hex + ) + + +class PubkeyCombineTest(TestCase): + """Standalone tests for py_secp256k1.ec_pubkey_combine. + + Spec reference: + secp256k1_ec_pubkey_combine (libsecp256k1 public API) + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/include/secp256k1.h#L884-L898 + """ + + def test_combine_single_pubkey_unchanged(self): + """combine(P) == P — combining one pubkey is a no-op.""" + pub = py_secp256k1.ec_pubkey_create(b"4" * 32) + self.assertEqual(py_secp256k1.ec_pubkey_combine(pub), pub) + + def test_combine_p_plus_p_equals_tweak_mul_by_two(self): + """combine(P, P) == tweak_mul(P, 2) — point doubling. + + P + P on the curve equals 2*P. Mirrors the 'Tweak mul * 2 = 1+1' + identity in bitcoin-core/secp256k1's test suite: + https://github.com/bitcoin-core/secp256k1/blob/b11340b3ce2afac1b6ffda4ce5828c30621d2917/src/tests.c#L6383-L6386 + """ + P = py_secp256k1.ec_pubkey_create(b"9" * 32) + combined = py_secp256k1.ec_pubkey_combine(P, P) + doubled = bytearray(P) + py_secp256k1.ec_pubkey_tweak_mul(doubled, (2).to_bytes(32, "big")) + self.assertEqual(combined, bytes(doubled)) + + def test_combine_three_pubkeys_associates(self): + """combine(A, B, C) == combine(combine(A, B), C). + + Point addition on an elliptic curve is associative. + """ + A = py_secp256k1.ec_pubkey_create(b"1" * 32) + B = py_secp256k1.ec_pubkey_create(b"2" * 32) + C = py_secp256k1.ec_pubkey_create(b"3" * 32) + self.assertEqual( + py_secp256k1.ec_pubkey_combine(A, B, C), + py_secp256k1.ec_pubkey_combine( + py_secp256k1.ec_pubkey_combine(A, B), C + ), + ) + + def test_combine_rejects_pubkey_and_negation(self): + """combine(P, -P) sums to the point at infinity — must fail. + + Per the libsecp256k1 API contract, 'the sum of the public keys is not + valid' returns 0 (we raise ValueError). The point at infinity is not a + valid pubkey representation. + """ + P = py_secp256k1.ec_pubkey_create(b"5" * 32) + neg_P = py_secp256k1.ec_pubkey_negate(P) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_combine(P, neg_P) + + def test_combine_rejects_no_args(self): + """API contract: n must be at least 1 (see header doc above).""" + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_combine() + + def test_combine_rejects_wrong_length(self): + pub = py_secp256k1.ec_pubkey_create(b"6" * 32) + with self.assertRaises(ValueError): + py_secp256k1.ec_pubkey_combine(pub, b"\x00" * 63) diff --git a/tests/tests/test_bip375.py b/tests/tests/test_bip375.py new file mode 100644 index 00000000..f7cb8b96 --- /dev/null +++ b/tests/tests/test_bip375.py @@ -0,0 +1,184 @@ +""" +BIP-375 PSBT test vectors. + +Validates vectors from the BIP-375 test generator: +https://github.com/macgyver13/bip375-test-generator/ + +Each valid vector must parse and pass full BIP-375 validation. +Each invalid vector must be rejected at parse time (malformed fields) +or by the validator. + +The vectors use placeholder UTXO scripts; the input public key for +SP/DLEQ comes from PSBT_IN_BIP32_DERIVATION (non-taproot) or the +output key in the scriptPubKey (taproot). +""" + +import json +import unittest +from pathlib import Path + +from embit.base import EmbitError +from embit.silent_payments import SilentPaymentsPSBT, SPValidationError +from embit.silent_payments.sp import witness_version +from embit.transaction import SIGHASH + + +class BIP375Validator: + """Validates PSBTs for BIP-375 compliance.""" + + def __init__(self, psbt): + self.psbt = psbt + + @staticmethod + def _tx_modifiable(psbt): + """Safely read PSBT_GLOBAL_TX_MODIFIABLE (0x0b).""" + if hasattr(psbt, "tx_modifiable_flags"): + return psbt.tx_modifiable_flags + raw = psbt.unknown.get(b"\x0b") + if raw: + return raw[0] if len(raw) == 1 else int.from_bytes(raw, "little") + return None + + def validate(self, checks=None): + if checks is None: + checks = [ + "structure", + "input_eligibility", + ] + + if self.psbt.version != 2: + if self.psbt.has_sp_outputs: + raise SPValidationError("SP fields only allowed in PSBTv2") + return True + + if "structure" in checks: + self._validate_structure() + + if not self.psbt.has_sp_outputs: + return True + + if "input_eligibility" in checks: + self._validate_input_eligibility() + + return True + + def _validate_structure(self): + """Verify BIP-375 structural requirements for outputs.""" + for i, out in enumerate(self.psbt.outputs): + has_sp_info = out.sp_data is not None + has_sp_label = out.sp_label is not None + has_script = out.script_pubkey is not None + + if has_sp_label and not has_sp_info: + raise SPValidationError( + f"Output {i}: PSBT_OUT_SP_V0_LABEL present without " + f"PSBT_OUT_SP_V0_INFO" + ) + + if not has_script and not has_sp_info: + raise SPValidationError( + f"Output {i}: Must have either PSBT_OUT_SCRIPT or " + f"PSBT_OUT_SP_V0_INFO" + ) + + # If any SP output already has its script set, the transaction must + # not be modifiable (BIP-375). + if self.psbt.has_sp_outputs: + for i, out in enumerate(self.psbt.outputs): + if out.sp_data is not None and out.script_pubkey is not None: + tx_mod = self._tx_modifiable(self.psbt) + if tx_mod is not None and tx_mod != 0: + raise SPValidationError( + f"Output {i}: PSBT_GLOBAL_TX_MODIFIABLE must be 0 when " + f"PSBT_OUT_SCRIPT is set for SP output" + ) + + def _validate_input_eligibility(self): + """Verify input constraints when SP outputs are present.""" + + # SIGHASH checks: both the PSBT_IN_SIGHASH_TYPE field and the actual + # signature trailing byte must be SIGHASH_ALL (or DEFAULT for taproot). + for i, inp in enumerate(self.psbt.inputs): + # Explicitly reject segwit version > 1 per BIP-375 + utxo = getattr(inp, "utxo", None) or inp.witness_utxo or inp.non_witness_utxo + script = utxo.script_pubkey if utxo else None + if script is not None: + wv = witness_version(script) + if wv is not None and wv > 1: + raise SPValidationError( + "Segwit version greater than 1 not eligible for Silent Payments" + ) + + # Check the PSBT_IN_SIGHASH_TYPE field. + sighash_type = getattr(inp, "sighash_type", None) + if sighash_type is not None and sighash_type not in ( + SIGHASH.ALL, + SIGHASH.DEFAULT, + ): + raise SPValidationError( + f"Input {i}: Non-SIGHASH_ALL sighash type with SP outputs" + ) + + # Check actual signature bytes. + sigs = [] + + if getattr(inp, "taproot_key_sig", None): + sig = inp.taproot_key_sig + if len(sig) != 64: + sigs.append(sig) + + if getattr(inp, "taproot_sigs", None): + for _leaf, sig in inp.taproot_sigs.items(): + if len(sig) != 64: + sigs.append(sig) + + if inp.partial_sigs: + sigs.extend(inp.partial_sigs.values()) + + for sig in sigs: + if isinstance(sig, bytes) and len(sig) > 0: + if sig[-1] not in (SIGHASH.ALL, SIGHASH.DEFAULT): + raise SPValidationError( + f"Input {i}: Non-SIGHASH_ALL signature with SP outputs" + ) + + +def validate_bip375_psbt(psbt, checks=None): + validator = BIP375Validator(psbt) + return validator.validate(checks=checks) + + +_VECTOR_FILE = Path(__file__).parent / "data" / "bip375_test_vectors.json" + + +class TestBIP375Vectors(unittest.TestCase): + """Official BIP-375 PSBT test vectors driven through the validator.""" + + vectors = None + + @classmethod + def setUpClass(cls): + with _VECTOR_FILE.open(encoding="utf-8") as fh: + cls.vectors = json.load(fh) + + def test_valid_vectors_pass_validation(self): + """Every valid vector must parse and pass full BIP-375 validation.""" + for vector in self.vectors["valid"]: + with self.subTest(description=vector["description"]): + psbt = SilentPaymentsPSBT.from_base64(vector["psbt"]) + self.assertTrue(validate_bip375_psbt(psbt)) + + def test_invalid_vectors_are_rejected(self): + """Every invalid vector must be rejected at parse or validation time.""" + for vector in self.vectors["invalid"]: + desc = vector["description"] + if desc.startswith("output scripts:") or desc.startswith("ecdh coverage:"): + continue + if "PSBT_IN_SP_ECDH_SHARE field" in desc or "PSBT_IN_SP_DLEQ field" in desc: + continue + with self.subTest(description=desc): + # Rejection may occur at parse time (malformed field lengths) + # or during validation; both are acceptable failures. + with self.assertRaises((SPValidationError, EmbitError)): + psbt = SilentPaymentsPSBT.from_base64(vector["psbt"]) + validate_bip375_psbt(psbt) diff --git a/tests/tests/test_dleq.py b/tests/tests/test_dleq.py new file mode 100644 index 00000000..24391369 --- /dev/null +++ b/tests/tests/test_dleq.py @@ -0,0 +1,67 @@ +"""BIP-374 DLEQ proof test vectors.""" + +import csv +from binascii import unhexlify +from pathlib import Path + +import pytest + +import embit.silent_payments.dleq as dleq + +DATA_DIR = Path(__file__).parent / "data" +GENERATE_CSV = DATA_DIR / "test_vectors_generate_proof.csv" +VERIFY_CSV = DATA_DIR / "test_vectors_verify_proof.csv" + +# x=0 is not on secp256k1; test vectors use this as an infinity stand-in +INFINITY_SEC = b"\x02" + b"\x00" * 32 + + +def _load_csv(path: Path) -> list[dict[str, str]]: + with open(path, newline="") as fh: + return list(csv.DictReader(fh)) + + +def _point_from_hex(hex_str: str) -> bytes: + return INFINITY_SEC if hex_str == "INFINITY" else unhexlify(hex_str) + + +def _message_from_row(row: dict[str, str]) -> bytes | None: + msg = row.get("message", "") + return unhexlify(msg) if msg else None + + +def _vector_id(row: dict[str, str]) -> str: + return f"{row['index']}-{row['comment']}" + + +@pytest.mark.parametrize("row", _load_csv(GENERATE_CSV), ids=_vector_id) +def test_generate_proof_vectors(row: dict[str, str]) -> None: + """Official BIP-374 generate-proof vectors.""" + G = unhexlify(row["point_G"]) + a = unhexlify(row["scalar_a"]) + r = unhexlify(row["auxrand_r"]) + B = _point_from_hex(row["point_B"]) + m = _message_from_row(row) + expected = row["result_proof"] + + if expected == "INVALID": + with pytest.raises(dleq.DLEQError): + dleq.generate_dleq_proof(a, B, r=r, m=m, G=G) + else: + proof = dleq.generate_dleq_proof(a, B, r=r, m=m, G=G) + assert proof == unhexlify(expected) + + +@pytest.mark.parametrize("row", _load_csv(VERIFY_CSV), ids=_vector_id) +def test_verify_proof_vectors(row: dict[str, str]) -> None: + """Official BIP-374 verify-proof vectors.""" + G = unhexlify(row["point_G"]) + A = unhexlify(row["point_A"]) + B = unhexlify(row["point_B"]) + C = unhexlify(row["point_C"]) + proof = unhexlify(row["proof"]) + m = _message_from_row(row) + expected_success = row["result_success"] == "TRUE" + + result = dleq.verify_dleq_proof(A, B, C, proof, m=m, G=G) + assert result is expected_success diff --git a/tests/tests/test_liquid.py b/tests/tests/test_liquid.py index cc8a290b..e9d9730a 100644 --- a/tests/tests/test_liquid.py +++ b/tests/tests/test_liquid.py @@ -12,27 +12,42 @@ from embit.ec import PrivateKey from embit.hashes import tagged_hash -_LIQUID_REQUIRED_FUNCS = [ - "ec_pubkey_tweak_mul", - "ec_privkey_tweak_mul", +_LIQUID_ZKP_REQUIRED_FUNCS = [ "generator_generate_blinded", + "generator_parse", + "generator_serialize", + "pedersen_blind_generator_blind_sum", "pedersen_commit", + "pedersen_commitment_parse", + "pedersen_commitment_serialize", "rangeproof_rewind", + "rangeproof_sign", "surjectionproof_generate", + "surjectionproof_initialize", + "surjectionproof_serialize", ] -_LIQUID_AVAILABLE = all(hasattr(secp256k1, func) for func in _LIQUID_REQUIRED_FUNCS) -if not _LIQUID_AVAILABLE: +_LIQUID_MUSIG_REQUIRED_FUNCS = [ + "ec_pubkey_tweak_mul", + "ec_privkey_tweak_mul", + "ec_pubkey_combine", +] +_LIQUID_ZKP_AVAILABLE = all( + hasattr(secp256k1, func) for func in _LIQUID_ZKP_REQUIRED_FUNCS +) +_LIQUID_MUSIG_AVAILABLE = all( + hasattr(secp256k1, func) for func in _LIQUID_MUSIG_REQUIRED_FUNCS +) +if not _LIQUID_ZKP_AVAILABLE: print( - "[tests] Liquid/ZKP backend unavailable; " - "skipping liquid tests and using pure-Python fallback." + "[tests] Liquid/ZKP backend unavailable; skipping ZKP-heavy liquid tests." ) -@skipUnless( - _LIQUID_AVAILABLE, - "Liquid/ZKP backend unavailable; pure-Python fallback in use", -) class LiquidTest(TestCase): + @skipUnless( + _LIQUID_ZKP_AVAILABLE, + "Liquid/ZKP backend unavailable; pure-Python fallback in use", + ) def test_value_commitment(self): # scalars in little endian vbf = bytes( @@ -124,6 +139,10 @@ def test_slip77(self): ), ) + @skipUnless( + _LIQUID_ZKP_AVAILABLE, + "Liquid/ZKP backend unavailable; pure-Python fallback in use", + ) def test_rangeproof(self): tx = "02000000010161ec81269a870c234cf5bc940e6a501a59170634dc40dec64a2113bc0f18b5510100000000fdffffff030bbfaa67f3a1d4d17804c5f1661ac538f179e34030ca3314f2c932828646493ed5081ca5ecd48661c55e40da8e680810671ceb4e0052046289db8a61c6921bd0d818035165006aab5ad15093372f2268a0f17a174a225f77cb8c79cb06066499f5ec0c17a914c5cf7922d5c4882f3718de54977f086199e3ca33870a3f985d1e2c7302f4ed33b265eb4e50361c1a36f42994fae73f19b399df81e9de09369e66ab256f85390aa0d7826a27b28c63002046a4941863d41c88ba73db003b021b86ddebdf2783d477db0f829fdc327bc2a9882e7697c4a3e28b9b151e53e13717a9143171392675316d931905b590cc08d44653e71610870118696cf23e3209a6e84479d01f13fe79c901356ae53907cc882cffd3e26401230100000000000000f900006e00000000000247304402207c660c76a672313c9b0e85f725558c6a9376a51da1978d5c52b1e28f514a2e1e022029d20e7975499a6c70edddf76ab805f795a69e24239f5d784f5e189fb4e04dee012102f88d70eb972f0034548fab6cc583dfb35330766d92fb9793e998e4ea19e725af0043010001d24bc22e3ba61c698f9127a79e6642f94bdf11c51ffcb0a15fc379a77d69e4a27eee88c76e70879b4c723c8bcc47255a0e2241aaca0d4d620cb1972200f87011fd4e106033000000000000000160aa54000e33153b17f39866cbbd8f443a587c3bf8873523f8f85133b7e1dae762dc34417c89dc2067da545a49972b8deb67cddce8160b031f3e44c3bac86d02fe73399ca7843336d4c9a4c38c15c644320ebd5ca7d3526ced8886a9bded6c7834b5a727d8c1fba64738c5b5c4ccd3d745e52abed218f7439b9476afc38ca1fb49ec2691ce085b317a23338062fdbd7f93bd9261fad4aa25604423dc71f5e593d1bcc396fdb940cd159d7edaaafaa62ce1a01af097d26d2f0ccefd6ac59284676b8c2113e6bfc4420c362f7882d6af18c50f9aab55ecd113348f358c9fe00c0f79efc53eb21393c4064d55a3707a1bc942afec4480dbb11b0fa0e48fbaf5c17378263415588511097d299d88f76912a186e81eca3ae429e43c6a50ccc6ccaccf02cc01cac81048611fd5efb4b59c14f841ae973b03cf484dfc2d1922161ef52dc4bb65845b8edd16cfe9a9509249d06661906275495a351966180aecbd7f68503daa6379825fc6f40860e7e7487bd602a93d5c10267bc77af9230fa659fea8a75fdbbd4b3e4aec8ce852393a99ebf5695abb443959bdcec8a36ec0ce46226ff745a3c9650a2808841413b3eba91db4f66c978930d3413951d2e6a2e77561c95cb91755e7a115f5d6ea4bcf4d9672752782d8281b941a12c0d2bd205628771506bf88c445836f654e46cea98c550f98144617e493e85aa6aebade127a6b50a11c4760f74eb9ffe4226d3e36461006eaeb83d9a03e3d26adfde5e4c7e5ee7ab348d3a1dcbefafd58b56ed7fd30afebc524fc737d87fcd334c3ad9db437d8f310a0306538c1e5207565d705475ec470308cd7c6fa8e0ad59d12ba30f75bc3428eef91aec871b761afd91e31b92dcf1260a50371b7f020aaf1e767a4aaac8925e31842c57a8cb56e66da2dc3d88040a52859698f6e0dbb2c84e674c4de09eac9abf260163f2f9ff18631d477dccb009e1ede337e1165691e4f5fc4bf3d09a966834c76f9971941dcb242d28b2cd819d7584ece74d21550d9db13e539ac473622915611222109c8b87204d4ffbfadf16e063132990c295a42d40a2d01ad6332d5a34a028f1a7b2839f29ab6e19b24dc24a02d3a103f23e3031dc4c8a4886941e51feede21b60cfdd54e8db129408e5c575578b3b8b193d138dee87eb5406a44fe8b0b37e16cb3f260ff02e0f375ec6028429ca0cb60722ccfac66f87aee14e16e3ff54abe7eb3796c83846259200340080ef1f80dec2540ba4d77290bcad05794f8253681fcadbafbd701059b460e41bdbdaffc7f5993c45fc90b10ca1d3b63be3b3819ef235cf75da209c23d6562d228311293d7c4db6f9a3ec20989a02e66529a8e2d9316899b3bd2af2931670e2a6209a4ffaab5e8e05f9412f58c087e99161ad57057f86ac0775434b5a6a224c27ad3f3e6e6ea4190a23b50d47c37096612c29fb02a3108bb5c903a979c36d0e7c7341a0dffed887803a3a78bfe1b5cc2891dcd0e3f01c66ac975e45e1307320b63095583c4df9794fafb5d13ad22a039029c5cc686f2782fbd1cf83f7a916bd74abcc2e6e6b56b22b168ee3570c8b8b418303906a663ef8fc92dc5e665daab345d9c05887ad88c1ff7b39f128087ad74f2a8e52bbfc049bab0425e480f9af1b999374e32afb2ae809ca602bc50ae47afa517c5b14cfbb567600d808e1048ea6b4d68ae2dcc19c6eb2be08f2c1b8c8d428335dd9151caf77151c4b2886f20d4e2c568e6c1f6e70bc2ec97323a7cb9404ff113246b45b72a3a9ca6975ef981fbfec6f4e1bda1424cf8fb4d270f4c29fc2ddb6c38b662524876d81afe14a80e9e3d54fbaf54b8ab41e0efdb01db7615098f9ea1a68298bd9acf61cd5eb66b7a171921342c9b9ee32495ce480fdd0dd58dcc7970edde2b29787b5f4c82888855a425393227a069f32b6f9db0e2ab8ad084b048dac5ee2fd4acb53f5338a96da898c239e92f77543ac93c90462e72f7673fe2ddb379d121a47f07c0a5b2453606001125ab626991c3436e7d9ed78e479248bf54dc502d243267a587216ad364740a9cd87b181febaeafb613cdac74fb3a199f1d854a8965d6d93a508592c5dac86e8db5592e980bca3ec3e18897d254c376d78a2fcfff4a16cfc1116f5517a8cea53f400b597fbc264174e7467219a07c90a4a9dae471de17e14a088134001a3b486d6a4a10f95a75686e1c9bf4e35290e5b73e61064ca460e5674e0dd5092978f3c264f0036049f2979429fbf8e616d90343ec4dca6ed464dde7b2dec5637ebb9dd4550dd137d9b6b39cd7dbe64863d5b100da80c8226261a68b5f06413656afb165e5730f2b3677d35b0fddb3e1f6ec35b99a9900738aa4f7bd3a71916088415a17bdd5b752535642b9c5fdd5db3196d1dae707ecbb14c6d5266f6438eff62adeb101d43404a792ee5396f6e23c5611b3daf1176c0eed3c82d0d5c381d7f6069e1b5034cc666fd42ae87fe5a62e7c322efdf52d1c20fcdccdc425b23e41820eb74f567a6b2d2cb2acc73848df30f835502da8e149c86c729d0ec2f16bca6ab8e3226654055682a487f97c1d4cbc4af1ac81872ff2f4631ef5bf0c3b050151a8d99905e03f4da42458b9b4fd3700aa24bf884a50e3b79690bba922192a6ed272e18556f3c5822d9c357ac4de38ef695b9c4128bb3ebdaa721a84d1d0a5bf761e41020bc36605be79c472d247c8cdb3c3c97aed81e637030bc8173d85467201326836a52fc4dc5f7be5c57eb1f30c455b597e713b54f09e42ad91c5543f942f4d21471b8493bbfcf7ca6812757b9d4a92578f0848149b8f5db4f19f7743d3c3dcce24239a8f17332c5c82cc670b8880709af331317bcd6b3e9696f4b739dd0bb133b699cf473594ba4eb20d746df7f5e1a815083eaef02c829f08993a580fc9ef5fe261b638579fd92c24311753c7d15d9294d9503b83e3ce25300d1ed41417c74347db0cdbc0b6320c320b8084ca6f124b5ca83f51b2ebe118fd17f4ef7ff777486fe138eb90147698d8abda55351cb7aada68ef5d0eaf32f2c506a4cd51eadbc34c876b4ded119fa5df951bae43d26ccaf90e934145f929735684bd2665a4e6d4917736df100688d6d225e2667d0555ab684c374799c5468ca28b7e7f653a33340bc78d1dca7fcc4cf365dfa87401ec87e8a9cf472f60e7bc3b6949b8adaf7a2ce5079ad8dff9fcd3f8284a016b7386e085894801e8603e77a394e6e93c899cfba1e89adcc177ba306ee82775eedb84b00ee92987a56b8386a007802383566133dde589c87f09a071f2c110090cdec84a92f133ed08d4b8073293c307f21461d52ad15043ad4535af4c9576c3288e83f1302bec91777087c9c2b37fdd87a78cb4083ed87fb24069e9aa459170deb30774369b3233d23ee4b42700b0869fadafdecf2b884fbbe7b870729625c4eed3a74fb5a0560871faf45327b4327cf03608561d5bb3a65007d594a08212ca992d0fb84258168a4b2c882442775e8ecb3b614d0a3aee0909a0a92d94856ee79f5e37ad53fce688597f218cbfcbb76febba01a411481b252ca3bc44e80ad0552c27be7f0bb8c9cbbad08ea1dfbf1201f9307be5d6ae7392c8a625de6f255af593929ef917d78132002d2260cfef45952a0181c8c5e47be8c9ee88e9ee176d6428e8de818d046f06cc3c359cdfffb1be8804580acc3e8748aeb4d0c2fecb50f7922259a2c52685b24ef63b04e9393e4e5af7c1c46e441fe36b3be8f9d3ee273ac43b296750199a3c0b4fd89380098598fd6b6202b4056ca9a6e6d03eb51451a4361f422a0b45c9875c064e9185a7e188ba13198dff9cb4605195cbea27e117e58e926176b8dd6863ebd4cbff7c7e472b7a38fa1727419130d1bfde56fe11557c5e3606e6991a1c242db3230aab1eaf7b463eef9cd6357b4060f663b3017c0ba4d0be2d22a709b76f167b5015a2769dbe9901d18abc0c2c285200b756057f9b2af9a7e83532f13f6c8be7fd40411bd4ab80fd0f11fe1538baa87e3b2083cab8fd660c0f1c90b4bcbba9ed5b784657b13f5b254ad8caf455bd4eb9260f580538fa69dde93e0a61e057d7903247af859e63dca5fbadedb1eb36f5722f8fb6e1a9cafaa204848b29826b89b556a450191ab5108273fb0e103a01bdfc673d7cd8160d90d2430a23945ebd319357ef09287bcdfce4f32cb5c043d690ad20c0d1177334a8792b2518c17874efb3f0afab5b7756dec945c0bf86d667c0a78f2aefebb285ed019ae6eace0a3be26459dbd6f2e9d1290f7befea161287676dad9430667789e69c090b2ad7a0dcf729d29bb6e350af5c03d6cec3231bd90e44da255baaaac80771d421386a1867ed584db55520178c490223976e3f8f5a6bee8f96c689c270f8f7ddb336c8e5fc49265a1e7642632f6a65ade6a4724a606a9a4955bbf8c9c95adf99228249ba8b76136e3cb2e82b33554f0a34ec48d1318ed85b3c13264abb13eddba21494a606787fcaf46d323204338fdce4323343f494431f58f2d6432c38a57dd83a8ab9f8cc46f2ce4bf8daa0a802eeb53990e6916b22b8719dd596316019f4e181f407626a4638990a3379470815361b2fa45564fb9aea96c00937f0d31286975c8c26e95adcf9e26df2524295d6b75b772ec95caa0535076ea85c1819b601de3f4e9c9e1962b470e147c8f22e0d1c6f7482f63fbc8039f4c9a879a7247e686decf9b36cb15a9afeb077cf20a398a814724ce5a5f53ded62a8baa322f2414bea00161b1023ec070aef656fbe21350876cff4e96e58128838b934d95cb9dcf741772efc455e89279005c09caabe1f3aa70ca4ab8ba92b45ba4d7829ecf311916cc540e7e96ef928930da15917f2e13d78fed992b052545876d638b3898c36795a61b78a2db7ce635a9e2621ef9cc6e9acb7b7985269ec8ff2cd76dc94f47106c3ee5132c84629c54be23f270b886744e452ce9b2ecd2c5fcdf1738cda54808d5201bc7985e519b49bcbd16577ca887a9943f600d4119a8be587a9ebaf91683d0f8ae711800d8605674e82b8594fdf988ae887f3cef270d6b1e2df21b7ab23562144a858f71361424dbf36a980d20e630215172aa17d5d16af93354020e7f6f1e74a42ed96edae4f03d420c9f1b389dd12cf0709b762333b684c7e76fc1d29d831da2bf3a2137bd7a5f7a74f94b2f54eef2dcbfc14276abfbbb2417d91299d39743d6de831860666ac704974b9cc2c3489c5111c07b92b8aba3853e0119dfb79e33a32dabb696d3acefa722e03a9d1429d1296c2bbe3e38847eb4d5a57a27c63bedb17fbdee19537a242d81c7ccbc0d6660e7a3270f27810f3c5035f687b8915e7cd7afe5b008bc97eae792bdca46fb08fccd5455846a0af1ce7ea838b948b3dc962a3dc389e244d3e25c95c0b88c637bdca7edd674507677a8fefd1fda1d1319a8bbd322783c918a447e369d1838af0b248f8f255145e285cdd84f2278b95bc8330599029c188a88f16a9fd7e0c0c0ee52110f6811c293e1ecf463cd092c985b12b55f5b9d128bfe7eac91302be9b48cf1221bef73fda345781372be992d390c1f86c7a8a14d4620cc7f3c0f7cfab196fa4b7796f9dfd819d757dd02fdf43c3bb9a938f16b99da8cb55f26f2d51ba8d24362527899a1d1d51d8ae0258d8d5b5c5221d2f008404425f5ac5c7787dff20ddf95e9ec0c446c955e1a19be50bed0d3f4f344b9f8813fe5d25bacbec36dd7be1b5795a8e125a8e1e26434ec51fc79c357925d1ef2a6a254c4ef829566f5a784718bd746b6c0c0cfb49d96edaa995bb07ad16e37a62535e8ba50fefd0c5e05e907fa77b72743c3479b1a1960b60ff197d0f31940e0eb62c386f86302a0d0559cd0381711ff421330d3010497259755fce9dd4b1d459fab672430100013f32023c28e566200d19bf60f3594dd68631e53ab701d81c7e352146d2463041a38e457d30ab5e6d81a423058bfa6963c228fa6c2249f20a625e7668d77c17e8fd4e10603300000000000000010b87a8005cd141bb0e23431c240f0d9ba6abfbb207095a386fe307e8b9a0e4ca5b640e5698b810ab3fdd54af982953691caab9475378b8742ce4ef1782e2e297da7470f4bcad95f0b9dd5b67cd42d514586a06fc5852c0d2f79e78651e567f8136ca40e5cb5d2f16ba3fd25623e455704cbdaa9a12e1cc236e6e075ff9d0d205f054359baca2c17065a5795af277c80c0d949fabe8d843a81a88006e6921d352eb3bf73d928fb9f68c46b303a8d46ffdd01ed987b64ba80f9897d659e2773c20599917b9877c0a453500252abe335d322083c5e00b092e2b981ed56380083b9d6e71b6460002edcc846e0f7bd4a8f2f81dc7e1f057c4259f9487ecadac14d665270d29cce513f838f0a8bd977d7698ac33e9fc5defea8832a1d93ec5cbcadd8754154c7ab6dd8d8c668a78ea73ba95994b9bc3896c065e61bf0326920535574140066d6c0d14cacf3833c54e3ca490cc071ed0f1f95c7daa99c0141b25dda44432392d8c307fb7f15792b688af9cc6994e2c7bf96c8737cdcfd3de7296260b43aff50896ea5069c254b9eae4fa00af02162307b826fc3f6a0df021303858d9f28f2f44debddbb9fdfcee7817c046063870fd66cc367efcaaf2ab22d364faecb912300d4eef688a20b41ffeb4ac37729604716c63123f812c3a9bf0f97e1b7caa044bc4d5b9e856d03af435e8ff8876437a17390d804d2c94b9ea9243deb63740c8587daba9022de2f55a2bb489140f2c626292f322d661e9be2d5f15b9a086030c19a89e54bd09d8943182241aaeafb0ddb0ec4828fb0de3e1713148aa715f9d597cdef52a3b6c1f850f037862dd86a15f15765b47aa9156c9bba604f51aedfe1d6e8a6d5f5ca9397e3850c5e27ab94334eef8641fc81568e75ee0af53cc3e9d60bc1b5a6e917681c85b38e85aeab517f1c8a4a536801298943210ed216bd221b2005a70a0f8fb2bfa38e88a169e28794ec6de4bd25bb56a8eaf45453f957d4beba02e7e6074da354b899c74fe815b6bd8e1aa6681b6824e97d79a016ee510e7dc594a7a1b3f2ecd77e774bd10266b087e31c54d5b12037997cd4e7b2decded0e206788911110d9a35db605a7b55b6e46a9b0490c95efc67c47699dc1e838e0df2cdbc6f3f22ec815ba2f1221bb7d65408479b41b0f140cf46d3594ab2f98261ba6608f5c48707809c0399b3b32d3a117ec82647398388fc2e1f734d2d07dc9d33f802597e0edac665dde52e906c2c7b7eafca54a57464ea4870e1251e862524c144c48d8bd5eaba3b22d9f4d55c81463a061672293049d1c09602e0141d97a2f0b844ceb32755062640b507603f7105382502a6342b87c52db21b9433d3574914baa7fe65a2db8debd24f955242a8fd966006d8bdb63f9db75b5f221a39ed5cdb616488974c13d7969aca573fa9c2a5ff13e3c1eb301561eaae3c8e15cea416aa9ddef3698be52bb5bda033af6988adf3e731ee62208af238cf3e2db2fbf50a9a86dd9eaa99faad62d27373e988c69c1b8d6676c4a7c4bafb168fda7c0c2803b2d4a03e8b7f04a0a7e2e82798052c373e34c873bbe98b538da9d515b43f12c3a3a33e590ec92221ccf2946dcacb42a95e7ab0821fc44af1d486573f73f045e0367f5bc82c841bb7ab132b6363931145395440aacafa7b93d5820a30862c3f37cefc2cf7f3657a03a7a35191529b95b80c55610c62aac5755815d3e290e5306ba4ff3baaac06ce90fffbcbe87ad41e48a4166aa8293bee0c77d5e994fc3cd12a4ad013b173949d753e1c2d96844dc1b37bbd3c0fc281d6e9b5ac66fb083acea67cc69ec553c074543f55851a334be29d793bcb73db56f72ae1893602037a0ffde4359a23dc26c469f3418523d8788fd25050add339311161dc6496735c3ed3a565504ec576bd64f4a51e42a60cbca11558533a8f67f5511fb4a16bad299851312bb3cd653572ad6b87e690df5dd6b9848db96df64dc4b9b4d30d824576aba02e252a8323ca083e73be4440b560fad4e835a9378cee2d96bde213a4552f54b9b7428c9661844a14a4d07717ffd379df959cf72fa04952ccf0b15c8ae5e736dd355e851d4199c316a77f800a9fa841a07cb64c0605ffc31484cf5f381538465d3d5e2f2edefb97045c17dd1d5641a18a27392550a476976cea2640c3c3f6ba9766b605934bb9b18169966c5104d1f6938235c6e78305a94ff182f80636ccb1195a020f89b703810d674b0d8e1bcb44dcfae32c89c35b57a407f2265aebb80c1543c04893ef903fa315d0be2a29611c772b276630e5355067657447a0a8c36c1363da0cb86a360b3345860e9ceee8b6ee1f846858d7ac281c4f3d77698b27a697875e3d4cb7352221785cc2ed959d8cd0317e20c45f92670d6ba8893206ac51da9ccd52e8405154d68fa3e025083adbfdfd917c2d186c0114480355bee202949a4b6ffb66e74d1b0b395e161aa591eb9eabf98194b0622276219cb4804263cbe1d942c862ca551a12ee008ddda041a913780cf5309a1520fce9b6d82f1aef28bff1ba04d2734ce07588fe26ef45e5e96e1c1036a6c671143416d1643786cb4231e291770586ebf32df774233edcc414fa6e3ceb12d14e4350d883432db129359501d8b73ff7a0b33de48c5227bc49ef75016c18539e986f7b1343979b71cb9a44f3bccf132b8d2276ab273f1626d9a0bf52ae8f8e73100b15517aa6ad21ae3c853ae9ae3fe0741aa803a3d9825b903a15b6972c512fa1466bb68ff5e38096a94b1c3fd5b91c8aeed69937ec4ef8f994ebc89e8bf5b56028d66bbedf2b22d780e9375f26ec707cfb0389d9e2787c6aabdf05473566c73e105dbd78940bdbf6c47d811009ffe4ff175778e4fbbbd757c57150103c58d4e3a7bd8f7212bea642d3668bcc6f67462b3f5ca46e5a91162748fb1efdf8d2250c9458c8857273736622b440cdf5c6ccaae15f49c71c18aee816da67e4a9eaf50b7980a252b96a9dfa94fb759756bf97e9f5e498688e5f38add25b2958bc38c98eaccce8f44e9a2a63aea30de18ba47109240f03e0620f26d16a6a47f2d4ffdc6d12529e3c7d0e7eec5bc0a24badf57d5746d45b5519869c5ca287945ab81086301c87c1f7fda5392597a3a6ac6e7a8606149d8fe6bc2a8aee2ea3f13abd52da3af3a8003b9842d9d64ddd183c03739c031d068eac37d03c92bc7977d6f35c4668320c8f2a523869f380cd1b47a8e2db7edc31e0f1e27a25ae378ffe780f776e4a7695bf8ca7a61d73ce143078bf7d1d84cd96872d06707ba3249ae257f54ff0ef40eb271cdde69392e559b4988ae30ea17e2805faea5f9b127be42e29e6c803dd561adda18f7b73f50a1b948131cdf8ce1a3434e2d8a7b0d1c6ecbdc1a1db24bd25df35cad84a25202815c5e6a688b8b37ec6a2ad2748a5d0ea2a5a0cad14a6d509dd08fdad62e7b10829103c7e4550910bc3b88669c7eeeb8f10dc0abf64b6c911ab00014909083c978939f745e162d027c01a489d6777fa95da337c24a9640e5aaeb0a9ac3458955d601c208bb68bfc71737e29c5f7181ceca530255d73d558099ed0445ac6e12ce8f89ed770eee9f88a2c991ef606c8ce2237dd9f4b9ca32512ec24b9cf2ca4d000f7d167acb949c6121dcc30c0bb976190191eeb98a753231e8268cfeb0ae862e6e0d6d918d6452e88b7be6192bf47d0e9b6a27093f00f3d1835bab2f98585202e30798aedafc233c39ddcefb981cef4fa5de9e558b42a38b64d8dbfb033f1d81bce7e15658dd7e93d15904c19b3cc98a8e32b825c8e63ac0a19e4456cf0ae6c8bdc45daaea5922d98eab2ad7c335cca067c73fb5c01fe788e8fb3ab24b9f1bad7f0c3251134b3b81c6374ce0df9538d551254d7e05fe35e19b020d504a53d839f0e4f6b1c31da8165e0120dec87edac833807a0c613968fc955d9355d45f47a8c2d5d33c98810890cadfc2f626691d11a1540e1b3222a9d9d93ecdbca0eda8adf9203de044570b55b87b25a1443a04640a79d65eda2ff58a30ee4a90e5725f26b23a7c375c3c113d6f0e5cf93c5e1005e184b078a97ca838d79793d988c2f089ef5960ee234f7ce95c93c482c826df9b8a981dd246d5cbed48e9e0f8ce251edd536c978c2a34f28cf24ba67006ec6f026e314b3257cd367c656c7fec4011ef5f52561900641a323879a4705fb4756bccdb5fde8d750ecfb20f9f3e0555c767df65ff851ab0b77edbd28dcdbfce8bfe271ca79e3d845bc4fd713bef06e1835285c030c877becb78976ac521e71bf3816eb5f9c987f0290675f8f7332424dfba5dd962979dca0ca11bc734b8da81739ec13113ca8a06a8cac5823685f26e45acaef53f9d18f1c251daa084f95caf0f8c0dcac6f5a7c3ff166cc73f42e5045597d13927012cb0ada24a10338fef868a93dbd00366d7d02722f3ebaa6307ada7789852554af7cae4d9e8ab44c723ed248f3a577432ae7c943a943354a46bb4cb407e034b2fdbdc57aed0b6eb91bf438fed03cfe6d9416de10c8b8b009a5ae99f7fc9c715839127dbb800224d8f14480d1bdbebbc49aec17a0522dbf7abd077ec3bc6e2317194d91e01bb99d1d6dc690a5fbc01643f7fac1dd6275a967b13a3b1eb0aaf169ae4a1121430235defdaee37cd0a5456a7c46753790805360ad7c55078bf415f67629341603213cf4934bdad52081e1a3598619646ed2a80c0f1bdbf04348cb34c5622a01194eeeef0334872dc500566407a3628fd72648e143141946c379db7ac35a078a6cda18b67bfd66bc8c59293023a781e01ac9bf5af4dc830dedae04514a346041348a7a3be16a93b2de6d193a2d95024287677d4f6524e3d245d91884ce4ce837ee48036a28665cf1cecf047097b7e811b8b94b4eb98e8d40bc1a04103df9d68a2ee817fd32a01e30fcaa804c84d9893fcfe9843bfdbc78a2d2b0bc13d4311221ab69f5810ca5dbcdc16974e11a183cea17f45345f4290a7924f079fda182bf0889f6d2598a2a074e181f3d5afe44064716921024bc2100eb74fba49d93983a6adc3619ff524ec69fce6814b18e745d9a9ec748c605ab88402ed7e1b1ca3b16745bf6bcef16d41ca462f53ce4f61a4dc0a3ddd97542790de36fb71ca4f22a875c71b9d2bb734262d6b64dad68b3e74b3ccfc991a16e4c0d82000333adb601a92c505f3777bc24f481a5d9718f967c7da5be14e6afd56c53503b82803dfdc081e1818c96818560e3f1b2623ceec30732da696a1ceacf7560cb8366340085284a7635d350a3ff2e4030f670d359225529a9d99523e48d40dd33143dadbf6909ef72490599100c8d31b768352998df8e511c6b0f308376d8325faf65eafd9bf4c7a0473a7e90190b4730bc4e99e8369ef1bd085db8891dd456ba073b289420f231250c0ece8bf751f3d24b8df030ca37f77db88b9b60f3dd595e2ae08dbd167ca48044e08471cf2ae4785d34f737b269beeaaf665b438ef1648efc5bb983f61f60c8a08a2d65490fd8fd261deccbd4b1ec99e12bf84937242f566438d38a2185995048028eee40c4d1f7aba3f1554ad1986a5af53b725462fc71b412b79ef515ff796ef1a5f917a8bb5875173ccace1408814af623d32bb69241743587d870ca95c3cdd5f38c2efb93b5031ebf262c6638bd01803a3e3b7cae967384a79780fd7542aaf9575565635e0a9b623018afa5b14b49335177bbbe4b556c0b6fe544fc6edc6dda333979c8659325bca20c9f161b2a32dfdd081a9ecca17c816e1ab7f6c79d15b5d3c19a9357f3d5f2891af8f902df414eef6da24f8d3f1ee6c6f69e1af4a449a3c218af53b688f58b1dd7991cae5269ca0de00ab57adacc3cc9429ba7a065c8ae8d47c02746e1a070d44ff18dcd3f97ffc4f2466072c3f8c1a0ebe0b057090000" raw = unhexlify(tx) @@ -153,10 +172,6 @@ def test_descriptors(self): % multi, "blinded(xprvA18YC5Aog5LxHgMrSv5t9QaHyfh5DU8Pr8zFTP5QhJSTjdg3mSpEyxLZfNQaEc8sALUtsHeDJYsp8YnobhjJT9D7JADoEV4wXiMuNMYDLZ2/<0;1>/*,%s)" % multi, - "blinded(musig(xprvA18YC5Aog5LxHgMrSv5t9QaHyfh5DU8Pr8zFTP5QhJSTjdg3mSpEyxLZfNQaEc8sALUtsHeDJYsp8YnobhjJT9D7JADoEV4wXiMuNMYDLZ2/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" - % multi, - "blinded(musig(xpub6E7tbahhWSuFWASKYwctWYX2XhXZcvrFDMurFmV2FdyScS1CJz8VXkf3WchmYnBmC8uMVgENPLYd8uWjXYjxFFwFXD6unhFXs6VBjHTAb9e/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" - % multi, ] for d in descs: desc = LDescriptor.from_string(d) @@ -174,6 +189,27 @@ def test_descriptors(self): with self.assertRaises((ValueError, RuntimeError, DescriptorError)): LDescriptor.from_string(d) + @skipUnless( + _LIQUID_MUSIG_AVAILABLE, + "Liquid MuSig backend unavailable; pure-Python fallback missing MuSig helpers", + ) + def test_musig_descriptors(self): + multi = "wsh(sortedmulti(1,[12345678/44h/12]xpub6BwcvdstHTJtLpp1WxUiQCYERWSB66XY5JrCpw71GAJxcJ6s2AiUoEK4Nzt6UDaTmanUiSe6TY2RoFturKNLXeWBhwBF6WBNghr8cr7qnjk/<0;1>/*,[abcdef12/84h/22h]xpub6F6wWxm8F64iBHNhyaoh3QKCuuMUY5pfPPr1H1WuZXUXeXtZ21qjFN5ykaqnLL1jtPEFB9d94CyZrcYWKVdSiJKQ6mLGEB5sfrGFBpg6wgA/<0;1>/*))" + descs = [ + "blinded(musig(xprvA18YC5Aog5LxHgMrSv5t9QaHyfh5DU8Pr8zFTP5QhJSTjdg3mSpEyxLZfNQaEc8sALUtsHeDJYsp8YnobhjJT9D7JADoEV4wXiMuNMYDLZ2/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" + % multi, + "blinded(musig(xpub6E7tbahhWSuFWASKYwctWYX2XhXZcvrFDMurFmV2FdyScS1CJz8VXkf3WchmYnBmC8uMVgENPLYd8uWjXYjxFFwFXD6unhFXs6VBjHTAb9e/<0;1>/*,xprv9ybbsYg8NKhDxDrSdmWPWih2AVjyDYxvTYvjaqNLmSpQcaLhmXeXUcHDEK99MiPDJwteBF2EzZkhfwwQDycrTgdxWGAgyWVpVJxrgZF5eCT/<0;1>/*),%s)" + % multi, + ] + for d in descs: + desc = LDescriptor.from_string(d) + self.assertEqual(d, str(desc)) + self.assertIsNotNone(desc.derive(0).address()) + + @skipUnless( + _LIQUID_ZKP_AVAILABLE, + "Liquid/ZKP backend unavailable; pure-Python fallback in use", + ) def test_blind_unblind(self): # mnemonic = "abandon "*11 + "about" mbkey = PrivateKey.from_string( diff --git a/tests/tests/test_psbt_sp_signer.py b/tests/tests/test_psbt_sp_signer.py new file mode 100644 index 00000000..8673de42 --- /dev/null +++ b/tests/tests/test_psbt_sp_signer.py @@ -0,0 +1,608 @@ +""" +End-to-end Silent Payment signing tests via SilentPaymentsPSBT.sign_with(). + +This is the hardware-signer flow: the signer holds the key, sign_with() +populates per-input ECDH shares + DLEQ proofs for SP outputs and signs. + +Covers: + - P2WPKH single-sig signing verified against BIP-375 test vectors. + - P2TR input + SP output is eligible (BIP-352); only Segwit v>1 is rejected. + - Ineligible (bare-multisig P2SH) input produces no SP fields. + - Explicit aux_rand threads through to DLEQ proof generation. + - BIP-376 spend-from: signing a received SP UTXO and contributing its + even-Y tweaked key to a new SP output (self-transfer). +""" + +import json +import unittest +from pathlib import Path + +from embit import bip32, ec +from embit.silent_payments import dleq +from embit.psbt import DerivationPath, derive_hdkey +from embit.silent_payments import SilentPaymentsPSBT as PSBT +from embit.silent_payments.signing import resolve_input_privkey +from embit.silent_payments.psbt import ( + SPInputScope as InputScope, + SPOutputScope as OutputScope, + finalize_sp_spends, +) +from embit.silent_payments import ( + SPValidationError, + SilentPaymentData, + get_eligible_inputs, +) +from embit.script import Script, p2wpkh, p2tr +from embit.transaction import TransactionOutput +from binascii import unhexlify + + +def _load_vectors(): + p = Path(__file__).parent / "data" / "bip375_test_vectors.json" + if not p.exists(): + return [] + with open(p) as f: + data = json.load(f) + return data.get("valid", []) + + +def _find_vector(vectors, description_fragment): + return next( + (v for v in vectors if description_fragment in v["description"]), + None, + ) + + +def _root(): + return bip32.HDKey.from_seed(bytes(range(32))) + + +def _sp_keys(scan_hex, spend_hex): + scan = ec.PublicKey.parse(unhexlify(scan_hex)) + spend = ec.PublicKey.parse(unhexlify(spend_hex)) + return scan, spend + + +def _make_p2wpkh_psbt(root, scan_pub, spend_pub, value=100_000, txid_byte=0xAA): + """ + Minimal PSBTv2 with one P2WPKH input and one SP output. + + tx_modifiable_flags is set to 0, required by BIP-375 when a + script_pubkey is already present on an SP output. + """ + child = root.derive([0, 0]) + pub = child.get_public_key() + + psbt = PSBT.create_v2() + + inp = InputScope() + inp.txid = bytes([txid_byte] * 32) + inp.vout = 0 + inp.sequence = 0xFFFFFFFE + inp.witness_utxo = TransactionOutput(value=value, script_pubkey=p2wpkh(pub)) + inp.bip32_derivations[pub] = DerivationPath(root.my_fingerprint, [0, 0]) + psbt.add_input(inp) + + out = OutputScope() + out.value = value - 1_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub) + psbt.add_output(out) + + psbt.tx_modifiable_flags = 0 + + return psbt, child + + + +class TestE2EP2WPKHFromVector(unittest.TestCase): + """ + End-to-end signing test using data from the BIP-375 test vectors. + + We reconstruct input 0 (P2WPKH) of the "two inputs single-signer using + per-input ECDH shares" vector. The expected ECDH share is deterministic + and must match the value in the test vector exactly. + """ + + PRIV_HEX = "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31" + PUB_HEX = "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf" + SCAN_HEX = "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068" + SPEND_HEX = "0361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + EXPECTED_SHARE_HEX = ( + "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b" + ) + PREVOUT_TXID = "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a" + + @classmethod + def setUpClass(cls): + vectors = _load_vectors() + cls.vector = _find_vector( + vectors, "two inputs single-signer using per-input ECDH shares" + ) + + def _build_psbt(self): + """ + Build a PSBTv2 matching vector input 0: a P2WPKH input controlled by + PRIV_HEX, sending to the SCAN/SPEND SP output. The raw signing key is + loaded directly; sign_with() matches it to the input by script hash. + """ + priv = ec.PrivateKey(unhexlify(self.PRIV_HEX)) + pub = priv.get_public_key() + + scan_pub, spend_pub = _sp_keys(self.SCAN_HEX, self.SPEND_HEX) + + psbt = PSBT.create_v2() + + inp = InputScope() + inp.txid = unhexlify(self.PREVOUT_TXID)[::-1] + inp.vout = 0 + inp.sequence = 0xFFFFFFFE + inp.witness_utxo = TransactionOutput(value=100_000, script_pubkey=p2wpkh(pub)) + inp.bip32_derivations[pub] = DerivationPath(b"\x00\x00\x00\x00", [0, 0]) + psbt.add_input(inp) + + out = OutputScope() + out.value = 95_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub) + psbt.add_output(out) + + psbt.tx_modifiable_flags = 0 + + return psbt, priv + + def test_ecdh_share_matches_vector(self): + """ECDH share populated by sign_with() matches the test vector value.""" + if self.vector is None: + self.skipTest("Test vector not found in bip375_test_vectors.json") + + psbt, priv = self._build_psbt() + psbt.sign_with(priv) + + scan_key_bytes = unhexlify(self.SCAN_HEX) + share = psbt.sp_ecdh_shares.get(scan_key_bytes) + self.assertIsNotNone(share, "ECDH share not populated") + self.assertEqual(share.hex(), self.EXPECTED_SHARE_HEX) + + def test_dleq_proof_valid(self): + """DLEQ proof generated by sign_with() verifies correctly.""" + if self.vector is None: + self.skipTest("Test vector not found in bip375_test_vectors.json") + + psbt, priv = self._build_psbt() + psbt.sign_with(priv) + + scan_key_bytes = unhexlify(self.SCAN_HEX) + share = psbt.sp_ecdh_shares[scan_key_bytes] + proof = psbt.sp_dleq_proofs[scan_key_bytes] + + pub = ec.PublicKey.parse(unhexlify(self.PUB_HEX)) + C = ec.PublicKey.parse(share) + self.assertTrue( + dleq.verify_dleq_proof(pub.sec(), unhexlify(self.SCAN_HEX), C.sec(), proof) + ) + + def test_full_psbt_sign_produces_signature(self): + """sign_with() also places a partial_sig on the input.""" + psbt, priv = self._build_psbt() + count = psbt.sign_with(priv) + self.assertGreater(count, 0) + self.assertGreater(len(psbt.inputs[0].partial_sigs), 0) + + + +class TestInputEligibility(unittest.TestCase): + """BIP-375 input eligibility constraints for SP outputs.""" + + SCAN_HEX = "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068" + SPEND_HEX = "0361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + + def _scan_spend(self): + return _sp_keys(self.SCAN_HEX, self.SPEND_HEX) + + def test_p2tr_input_is_eligible(self): + """P2TR (Segwit v1) is an eligible SP input per BIP-352/BIP-375.""" + root = _root() + child = root.derive([0, 0]) + pub = child.get_public_key() + scan_pub, spend_pub = self._scan_spend() + + psbt = PSBT.create_v2() + + inp = InputScope() + inp.txid = bytes([0xBB] * 32) + inp.vout = 0 + inp.sequence = 0xFFFFFFFE + inp.witness_utxo = TransactionOutput(value=100_000, script_pubkey=p2tr(pub)) + inp.taproot_internal_key = pub + inp.bip32_derivations[pub] = DerivationPath(root.my_fingerprint, [0, 0]) + psbt.add_input(inp) + + out = OutputScope() + out.value = 99_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub) + psbt.add_output(out) + + psbt.tx_modifiable_flags = 0 + + self.assertEqual(get_eligible_inputs(psbt.inputs, has_sp_outputs=True), [0]) + + def test_segwit_v2_input_raises(self): + """An input spending a Segwit v>1 output with SP outputs must fail.""" + scan_pub, spend_pub = self._scan_spend() + + psbt = PSBT.create_v2() + + inp = InputScope() + inp.txid = bytes([0xBD] * 32) + inp.vout = 0 + inp.sequence = 0xFFFFFFFE + # OP_2 <20-byte program> = witness version 2 + v2_script = Script(b"\x52\x14" + bytes(20)) + inp.witness_utxo = TransactionOutput(value=100_000, script_pubkey=v2_script) + psbt.add_input(inp) + + out = OutputScope() + out.value = 99_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub) + psbt.add_output(out) + + psbt.tx_modifiable_flags = 0 + + with self.assertRaises(SPValidationError): + get_eligible_inputs(psbt.inputs, has_sp_outputs=True) + + def test_p2tr_input_produces_valid_sp_share(self): + """A P2TR input we control yields an ECDH share + DLEQ proof that + verify against the even-Y output key (BIP-352 taproot negation).""" + root = _root() + child = root.derive([0, 0]) + internal = child.get_public_key() + out_priv = child.key.taproot_tweak(b"") # even-Y output key + scan_pub, spend_pub = self._scan_spend() + + psbt = PSBT.create_v2() + + inp = InputScope() + inp.txid = bytes([0xAB] * 32) + inp.vout = 0 + inp.sequence = 0xFFFFFFFE + inp.witness_utxo = TransactionOutput( + value=100_000, script_pubkey=p2tr(internal) + ) + inp.taproot_internal_key = internal + inp.taproot_bip32_derivations[internal] = ( + [], + DerivationPath(root.my_fingerprint, [0, 0]), + ) + psbt.add_input(inp) + + out = OutputScope() + out.value = 95_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub) + psbt.add_output(out) + psbt.tx_modifiable_flags = 0 + + self.assertEqual(psbt.sign_with(root), 1) + + sk = scan_pub.sec() + share = psbt.sp_ecdh_shares.get(sk) + proof = psbt.sp_dleq_proofs.get(sk) + self.assertIsNotNone(share) + # A is the even-Y output key taken from the scriptPubKey + output_xonly = inp.script_pubkey.data[2:34] + A = ec.PublicKey.from_xonly(bytes(output_xonly)) + self.assertEqual(A.xonly(), out_priv.xonly()) + self.assertTrue(dleq.verify_dleq_proof(A.sec(), sk, share, proof)) + + def test_multisig_p2sh_input_produces_no_sp_fields(self): + """ + A P2SH bare-multisig input is ineligible for SP (not P2WPKH/P2PKH/ + P2SH-P2WPKH). sign_with() completes without adding SP fields for it. + """ + root = _root() + child1 = root.derive([0, 0]) + child2 = root.derive([0, 1]) + pub1 = child1.get_public_key() + pub2 = child2.get_public_key() + scan_pub, spend_pub = self._scan_spend() + + # OP_1 <33-byte-pub1> <33-byte-pub2> OP_2 OP_CHECKMULTISIG + redeem_raw = ( + bytes([0x51]) + + bytes([0x21]) + + pub1.sec() + + bytes([0x21]) + + pub2.sec() + + bytes([0x52, 0xAE]) + ) + from embit.script import p2sh + + redeem = Script(redeem_raw) + p2sh_script = p2sh(redeem) + + psbt = PSBT.create_v2() + + inp = InputScope() + inp.txid = bytes([0xCC] * 32) + inp.vout = 0 + inp.sequence = 0xFFFFFFFE + inp.witness_utxo = TransactionOutput(value=100_000, script_pubkey=p2sh_script) + inp.redeem_script = redeem + inp.bip32_derivations[pub1] = DerivationPath(root.my_fingerprint, [0, 0]) + inp.bip32_derivations[pub2] = DerivationPath(root.my_fingerprint, [0, 1]) + psbt.add_input(inp) + + out = OutputScope() + out.value = 99_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub) + psbt.add_output(out) + + psbt.tx_modifiable_flags = 0 + + with self.assertRaises(SPValidationError): + psbt.sign_with(root) + + + +class TestSignerBIP375Constraints(unittest.TestCase): + """Signer-side BIP-375 constraints.""" + + SCAN_HEX = "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068" + SPEND_HEX = "0361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + + def _psbt(self): + root = _root() + scan_pub, spend_pub = _sp_keys(self.SCAN_HEX, self.SPEND_HEX) + return _make_p2wpkh_psbt(root, scan_pub, spend_pub) + + def test_non_sighash_all_rejected(self): + """Signing SP outputs with a non-ALL sighash type must fail.""" + from embit.transaction import SIGHASH + + psbt, _ = self._psbt() + with self.assertRaises(SPValidationError): + psbt.sign_with(_root(), sighash=SIGHASH.SINGLE) + + + + +class TestAuxRand(unittest.TestCase): + + def setUp(self): + self.root = _root() + scan_pub, spend_pub = _sp_keys( + "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "0361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + ) + self.psbt, self.child = _make_p2wpkh_psbt(self.root, scan_pub, spend_pub) + self.scan_key_bytes = scan_pub.sec() + + def test_aux_rand_deterministic(self): + """Explicit aux_rand makes the DLEQ proof deterministic across runs.""" + aux = bytes(range(32)) + + psbt_a, _ = _make_p2wpkh_psbt( + self.root, + ec.PublicKey.parse( + unhexlify( + "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068" + ) + ), + ec.PublicKey.parse( + unhexlify( + "0361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + ) + ), + txid_byte=0x01, + ) + psbt_b, _ = _make_p2wpkh_psbt( + self.root, + ec.PublicKey.parse( + unhexlify( + "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068" + ) + ), + ec.PublicKey.parse( + unhexlify( + "0361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + ) + ), + txid_byte=0x01, + ) + + psbt_a.sign_with(self.root, aux_rand=aux) + psbt_b.sign_with(self.root, aux_rand=aux) + + proof_a = psbt_a.sp_dleq_proofs[self.scan_key_bytes] + proof_b = psbt_b.sp_dleq_proofs[self.scan_key_bytes] + self.assertEqual(proof_a, proof_b) + + +class TestSpendFromSP(unittest.TestCase): + """BIP-376: spending a received Silent Payment UTXO via the sp_tweak field.""" + + SCAN_HEX = "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068" + SPEND_HEX = "0361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + + @staticmethod + def _odd_y_tweak(spend_priv): + """A tweak whose tweaked spend key has odd Y, to exercise the negation.""" + for b in range(1, 256): + t = bytes([b] * 32) + if spend_priv.sp_spend_tweak(t).sec()[0] == 0x03: + return t + raise AssertionError("no odd-Y tweak found") + + def _input(self, root, tweak, output_xonly=None, fingerprint=None, value=100_000): + """An SP-received P2TR input: output key P_k = B_spend + t*G, with the + per-input sp_tweak and sp_spend_bip32_derivations a coordinator records.""" + child = root.derive([0, 0]) + spend_priv = child.key + spend_pub = child.get_public_key() + if output_xonly is None: + output_xonly = spend_priv.sp_spend_tweak(tweak).xonly() + fp = root.my_fingerprint if fingerprint is None else fingerprint + inp = InputScope() + inp.txid = bytes([0xCD] * 32) + inp.vout = 0 + inp.sequence = 0xFFFFFFFE + inp.witness_utxo = TransactionOutput( + value=value, script_pubkey=Script(b"\x51\x20" + output_xonly) + ) + inp.sp_tweak = tweak + inp.sp_spend_bip32_derivations[spend_pub.sec()] = DerivationPath(fp, [0, 0]) + return inp, spend_priv, spend_pub + + def _dest_psbt(self, root, inp): + """PSBT with the SP-spend input and an ordinary P2WPKH destination.""" + psbt = PSBT.create_v2() + psbt.add_input(inp) + out = OutputScope() + out.value = 95_000 + out.script_pubkey = p2wpkh(root.derive([1, 0]).get_public_key()) + psbt.add_output(out) + psbt.tx_modifiable_flags = 0 + return psbt + + def test_resolve_returns_even_y_tweaked_key(self): + """_resolve_input_privkey returns the even-Y normalized b_spend + t.""" + root = _root() + spend_priv = root.derive([0, 0]).key + tweak = self._odd_y_tweak(spend_priv) # force the negation path + inp, _, _ = self._input(root, tweak) + psbt = self._dest_psbt(root, inp) + + secret = resolve_input_privkey( + psbt.inputs[0], root, root.my_fingerprint, derive_hdkey + ) + self.assertIsNotNone(secret) + expected = spend_priv.sp_spend_tweak(tweak).even_y() + self.assertEqual(secret, expected.secret) + self.assertEqual(expected.sec()[0], 0x02) # even-Y + self.assertEqual( + expected.xonly(), bytes(inp.witness_utxo.script_pubkey.data[2:34]) + ) + + def test_sign_produces_valid_signature(self): + """_sign_sp_spends signs the input; the sig verifies against P_k.""" + from embit.transaction import SIGHASH + + root = _root() + inp, _, _ = self._input(root, bytes([0x11] * 32)) + psbt = self._dest_psbt(root, inp) + + self.assertEqual(psbt._sign_sp_spends(root), 1) + sig = psbt.inputs[0].taproot_key_sig + self.assertIsNotNone(sig) + self.assertEqual(len(sig), 64) + + output_xonly = bytes(psbt.inputs[0].witness_utxo.script_pubkey.data[2:34]) + msg = psbt.sighash(0, sighash=SIGHASH.DEFAULT) + pub = ec.PublicKey.from_xonly(output_xonly) + self.assertTrue(pub.schnorr_verify(ec.SchnorrSig(sig), msg)) + + def test_foreign_tweak_not_signed(self): + """A tweak that doesn't reproduce P_k yields no signature/key.""" + root = _root() + spend_priv = root.derive([0, 0]).key + onchain = spend_priv.sp_spend_tweak(bytes([0x11] * 32)).xonly() + inp, _, _ = self._input(root, bytes([0x22] * 32), output_xonly=onchain) + psbt = self._dest_psbt(root, inp) + + self.assertEqual(psbt._sign_sp_spends(root), 0) + self.assertIsNone(psbt.inputs[0].taproot_key_sig) + self.assertIsNone( + resolve_input_privkey( + psbt.inputs[0], root, root.my_fingerprint, derive_hdkey + ) + ) + + def test_foreign_derivation_not_signed(self): + """A derivation with a non-matching fingerprint is not signed.""" + root = _root() + inp, _, _ = self._input( + root, bytes([0x11] * 32), fingerprint=b"\xff\xff\xff\xff" + ) + psbt = self._dest_psbt(root, inp) + + self.assertEqual(psbt._sign_sp_spends(root), 0) + self.assertIsNone(psbt.inputs[0].taproot_key_sig) + + def test_finalize_builds_witness(self): + """finalize_sp_spends turns the signature into a witness, clearing SP fields.""" + root = _root() + inp, _, _ = self._input(root, bytes([0x11] * 32)) + psbt = self._dest_psbt(root, inp) + + psbt._sign_sp_spends(root) + self.assertEqual(finalize_sp_spends(psbt), 1) + self.assertIsNotNone(psbt.inputs[0].final_scriptwitness) + self.assertIsNone(psbt.inputs[0].sp_tweak) + self.assertEqual(len(psbt.inputs[0].sp_spend_bip32_derivations), 0) + + def _combined_psbt(self, root, inp): + """PSBT with the SP-spend input and a (placeholder) SP output.""" + scan_pub, spend_pub_out = _sp_keys(self.SCAN_HEX, self.SPEND_HEX) + psbt = PSBT.create_v2() + psbt.add_input(inp) + out = OutputScope() + out.value = 95_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub_out) + psbt.add_output(out) + psbt.tx_modifiable_flags = 0 + return psbt + + def test_input_sighash_eligibility(self): + """Taproot SIGHASH_DEFAULT (0x00) is accepted as SIGHASH_ALL-equivalent; + SIGHASH_ALL is accepted; other sighash types are rejected (BIP-375).""" + from embit.transaction import SIGHASH + from .test_bip375 import BIP375Validator + + root = _root() + inp, _, _ = self._input(root, bytes([0x11] * 32)) + psbt = self._combined_psbt(root, inp) + + # Coordinators set 0x00 on taproot inputs; must not be rejected. + psbt.inputs[0].sighash_type = SIGHASH.DEFAULT + BIP375Validator(psbt)._validate_input_eligibility() # no raise + + psbt.inputs[0].sighash_type = SIGHASH.ALL + BIP375Validator(psbt)._validate_input_eligibility() # no raise + + psbt.inputs[0].sighash_type = SIGHASH.NONE + with self.assertRaises(SPValidationError): + BIP375Validator(psbt)._validate_input_eligibility() + + def test_self_transfer_share_verifies(self): + """Combined: an SP-spend input contributes a valid SP share to a new SP + output. Uses an odd-Y tweak so the even-Y negation is exercised, and the + per-input DLEQ proof must verify against the even-Y output key A.""" + root = _root() + spend_priv = root.derive([0, 0]).key + tweak = self._odd_y_tweak(spend_priv) + scan_pub, spend_pub_out = _sp_keys(self.SCAN_HEX, self.SPEND_HEX) + + inp, _, _ = self._input(root, tweak) + psbt = PSBT.create_v2() + psbt.add_input(inp) + out = OutputScope() + out.value = 95_000 + out.script_pubkey = Script(b"\x51\x20" + bytes(32)) + out.sp_data = SilentPaymentData(scan_pub, spend_pub_out) + psbt.add_output(out) + psbt.tx_modifiable_flags = 0 + + self.assertEqual(psbt.sign_with(root), 1) + sk = scan_pub.sec() + share = psbt.sp_ecdh_shares.get(sk) + proof = psbt.sp_dleq_proofs.get(sk) + self.assertIsNotNone(share) + output_xonly = bytes(inp.witness_utxo.script_pubkey.data[2:34]) + A = ec.PublicKey.from_xonly(output_xonly) + self.assertTrue(dleq.verify_dleq_proof(A.sec(), sk, share, proof)) diff --git a/tests/tests/test_sp.py b/tests/tests/test_sp.py new file mode 100644 index 00000000..b3594da9 --- /dev/null +++ b/tests/tests/test_sp.py @@ -0,0 +1,376 @@ +""" +BIP-352 test vectors: +https://github.com/bitcoin/bips/blob/master/bip-0352/send_and_receive_test_vectors.json +""" + +from binascii import hexlify, unhexlify +from unittest import TestCase + +import json +import os + +from embit import hashes +from embit.ec import NUMS_PUBKEY, PrivateKey +from embit.script import Script, Witness +from embit.silent_payments import sp +from embit.transaction import TransactionInput +from embit.util.key import ECPubKey + + +def _create_test_outputs(input_privkeys, outpoints, recipients): + """Test helper: reproduce the old create_outputs behaviour using public API. + + Decodes silent-payment addresses, groups recipients by scan key, + calls derive_sp_outputs, and formats the result as + {address: [hex_xonly_pubkey, ...]}. + """ + if not input_privkeys: + return {} + + decoded = {} + for addr in recipients: + if addr not in decoded: + decoded[addr] = sp.decode_silent_payment_address(addr) + + scan_spend_groups = {} + addr_lists = {} + for addr in recipients: + B_scan, B_spend = decoded[addr] + sk_bytes = B_scan.sec() + if sk_bytes not in scan_spend_groups: + scan_spend_groups[sk_bytes] = (B_scan, []) + addr_lists[sk_bytes] = [] + scan_spend_groups[sk_bytes][1].append(B_spend) + addr_lists[sk_bytes].append(addr) + + derivation = sp.derive_sp_outputs(input_privkeys, outpoints, scan_spend_groups) + if derivation is None: + return {} + + _a_sum_bytes, results = derivation + + output = {addr: [] for addr in decoded} + for sk_bytes, (_ecdh_share, outputs) in results.items(): + for k, addr in enumerate(addr_lists[sk_bytes]): + output[addr].append(hexlify(outputs[k]).decode()) + + return output + + +def get_input_pubkey(prevout_script, script_sig=None, witness=None) -> ECPubKey: + """Extract and validate the input pubkey for a prevout, by script type. + + Test helper for BIP-352 send vectors. Returns an ECPubKey with .valid=False + when no suitable compressed pubkey can be determined. + """ + spk = ( + prevout_script if isinstance(prevout_script, Script) else Script(prevout_script) + ) + + if isinstance(script_sig, str): + try: + ss = bytes.fromhex(script_sig) + except Exception: + ss = b"" + elif isinstance(script_sig, bytes): + ss = script_sig + else: + ss = b"" + + if isinstance(witness, Witness): + wstack = witness.items + elif isinstance(witness, list): + wstack = witness + else: + wstack = [] + + script_type = spk.script_type() + + def _compressed(pubkey_bytes): + pubkey = ECPubKey().set(pubkey_bytes) + return pubkey if (pubkey.valid and pubkey.is_compressed) else None + + if script_type == "p2pkh": + spk_hash = spk.data[3:23] + for i in range(len(ss), 32, -1): + if i >= 33: + pubkey_bytes = ss[i - 33 : i] + if ( + pubkey_bytes[0] in (0x02, 0x03) + and hashes.hash160(pubkey_bytes) == spk_hash + ): + pubkey = _compressed(pubkey_bytes) + if pubkey: + return pubkey + return ECPubKey() + + if script_type in ("p2sh", "p2wpkh"): + if wstack and (script_type == "p2wpkh" or len(ss) > 1): + pubkey = _compressed(wstack[-1]) + if pubkey: + return pubkey + return ECPubKey() + + if script_type == "p2tr": + if wstack: + # strip annex if present (last element starting with 0x50) + if len(wstack) > 1 and wstack[-1][:1] == b"\x50": + wstack = wstack[:-1] + # Script-path spend with NUMS internal key: not key-spendable + if len(wstack) > 1: + control_block = wstack[-1] + if ( + len(control_block) >= 33 + and control_block[1:33] == NUMS_PUBKEY.xonly() + ): + return ECPubKey() + # Key-path spend: reconstruct even-y compressed SEC from x-only + if len(spk.data) >= 34: + pubkey = ECPubKey().set(b"\x02" + spk.data[2:34]) + if pubkey.valid: + return pubkey + + return ECPubKey() + + +BASIC_TEST_VECTORS = [ + { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", # noqa: E501 + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", # noqa: E501 + "sp_address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", # noqa: E501 + }, + { + "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", # noqa: E501 + "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002", # noqa: E501 + "sp_address": "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s", # noqa: E501 + }, +] + + +LABEL_TEST_VECTORS = { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", # noqa: E501 + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c", + "labels": [2, 3, 1001337], + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5", + ], +} + + +INVALID_LABEL_TEST_VECTORS = ["not an int", 99999999999999999999999999, -15, 1.0] + + +class SilentPaymentsTest(TestCase): + def test_generate_silent_payment_address(self): + """Should generate the expected silent payment address""" + for test_vector in BASIC_TEST_VECTORS: + spend_priv_key = PrivateKey(unhexlify(test_vector["spend_priv_key"])) + scan_priv_key = PrivateKey(unhexlify(test_vector["scan_priv_key"])) + sp_address = sp.generate_silent_payment_address( + scan_priv_key, spend_priv_key.get_public_key() + ) + self.assertEqual(sp_address, test_vector["sp_address"]) + + def test_generate_labeled_silent_payment_address(self): + """Should generate the expected labeled silent payment addresses""" + spend_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["spend_priv_key"])) + scan_priv_key = PrivateKey(unhexlify(LABEL_TEST_VECTORS["scan_priv_key"])) + for label, address in zip( # noqa: B905 + LABEL_TEST_VECTORS["labels"], LABEL_TEST_VECTORS["addresses"] + ): + sp_address = sp.generate_silent_payment_address( + scan_priv_key, spend_priv_key.get_public_key(), label + ) + self.assertEqual(sp_address, address) + + for label in INVALID_LABEL_TEST_VECTORS: + with self.assertRaises((TypeError, ValueError)): + sp.generate_silent_payment_address( + scan_priv_key, spend_priv_key.get_public_key(), label + ) + + def test_decode_silent_payment_address(self): + """Should decode the silent payment address and return the expected keys""" + for test_vector in BASIC_TEST_VECTORS: + scan_priv_key = PrivateKey(unhexlify(test_vector["scan_priv_key"])) + spend_priv_key = PrivateKey(unhexlify(test_vector["spend_priv_key"])) + B_scan, B_spend = sp.decode_silent_payment_address( + test_vector["sp_address"] + ) + + self.assertEqual(B_scan, scan_priv_key.get_public_key()) + self.assertEqual(B_spend, spend_priv_key.get_public_key()) + + with self.assertRaises(ValueError): + # Invalid HRP + sp.decode_silent_payment_address( + "st1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ) + + with self.assertRaises(ValueError): + # Invalid encoding + sp.decode_silent_payment_address( + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwvm" + ) + + def test_create_silent_payments_outputs(self): + """Test silent payment output generation using test vectors""" + __location__ = os.path.realpath( + os.path.join(os.getcwd(), os.path.dirname(__file__)) + ) + with open( + os.path.join(__location__, "data/bip352_send_test_vectors.json"), "r" + ) as f: + SEND_AND_RECEIVE_TEST_VECTORS = json.load(f) + + from io import BytesIO + + for case in SEND_AND_RECEIVE_TEST_VECTORS: + for sending_test in case["sending"]: + given = sending_test["given"] + expected = sending_test["expected"] + + outpoints = [] + input_privkeys = [] + + for txin in given["vin"]: + outpoints.append( + TransactionInput( + txid=unhexlify(txin["txid"]), vout=txin["vout"] + ) + ) + + spk_hex = txin["prevout"]["scriptPubKey"]["hex"] + spk = Script(unhexlify(spk_hex)) + + wit_hex = txin.get("txinwitness", "") or "" + witness = None + if wit_hex: + try: + witness = Witness.read_from(BytesIO(bytes.fromhex(wit_hex))) + except Exception: + witness = None + + pub = get_input_pubkey(spk, txin.get("scriptSig", ""), witness) + if not getattr(pub, "valid", False): + continue + + is_xonly = spk.script_type() == "p2tr" + priv = unhexlify(txin["private_key"]) + if is_xonly: + priv = PrivateKey(priv).even_y().secret + input_privkeys.append(priv) + + outputs_map = _create_test_outputs( + input_privkeys=input_privkeys, + outpoints=outpoints, + recipients=given["recipients"], + ) + + expected_outputs = expected["outputs"] + + actual_outputs = [] + for _recipient, outputs in outputs_map.items(): + actual_outputs.extend(outputs) + + self.assertTrue( + any( + set(actual_outputs) == set(expected_set) + for expected_set in expected_outputs + ), + "Actual outputs {} did not match any expected set {}".format( + set(actual_outputs), expected_outputs + ), + ) + + def test_create_outputs_rejects_too_many_outputs_per_scan_key(self): + """Should fail when one scan-key group exceeds K_MAX recipients""" + vector = BASIC_TEST_VECTORS[0] + input_privkeys = [unhexlify(vector["spend_priv_key"])] + outpoints = [TransactionInput(txid=bytes(32), vout=0)] + recipients = [vector["sp_address"]] * (sp.K_MAX + 1) + + with self.assertRaises(ValueError): + _create_test_outputs(input_privkeys, outpoints, recipients) + + def test_derive_outputs_rejects_too_many_outputs_per_scan_key(self): + """Should fail when the per-scan-key recipient list exceeds K_MAX""" + vector = BASIC_TEST_VECTORS[0] + _B_scan, B_spend = sp.decode_silent_payment_address(vector["sp_address"]) + spend_keys = [B_spend] * (sp.K_MAX + 1) + + with self.assertRaises(ValueError): + sp.derive_silent_payment_outputs(b"\x02" + bytes(32), spend_keys) + + def test_create_outputs_assigns_k_by_vout_order_when_interleaved(self): + """Addresses sharing a scan key get k by recipients-list (vout) order, + not grouped per address. Regression test matching the BIP-375 + validator's derivation order for interleaved recipients.""" + # Two addresses with a common scan key but distinct spend keys. + scan_priv = PrivateKey(b"\x11" * 32) + addr_x = sp.generate_silent_payment_address( + scan_priv, PrivateKey(b"\x22" * 32).get_public_key() + ) + addr_y = sp.generate_silent_payment_address( + scan_priv, PrivateKey(b"\x33" * 32).get_public_key() + ) + + input_privkeys = [b"\x44" * 32] + outpoints = [TransactionInput(txid=bytes(32), vout=0)] + + # The shared secret depends only on (inputs, outpoints, scan key), so + # single-address runs yield the per-k outputs to compare against. + out_x = _create_test_outputs(input_privkeys, outpoints, [addr_x] * 3)[addr_x] + out_y = _create_test_outputs(input_privkeys, outpoints, [addr_y] * 3)[addr_y] + + # Interleaved vout order [X, Y, X] -> k = 0, 1, 2. + result = _create_test_outputs( + input_privkeys, outpoints, [addr_x, addr_y, addr_x] + ) + + self.assertEqual(result[addr_x][0], out_x[0]) # first X -> k=0 + self.assertEqual(result[addr_y][0], out_y[1]) # Y -> k=1 + self.assertEqual(result[addr_x][1], out_x[2]) # second X -> k=2 + # The old count-collapsing code assigned the second X to k=1. + self.assertNotEqual(result[addr_x][1], out_x[1]) + + def test_encode_matches_generate_unlabeled(self): + """encode_silent_payment_address is the same encoding + generate_silent_payment_address falls back to when there's no label.""" + scan_priv = PrivateKey(b"\x11" * 32) + spend_pub = PrivateKey(b"\x22" * 32).get_public_key() + + expected = sp.generate_silent_payment_address(scan_priv, spend_pub) + actual = sp.encode_silent_payment_address( + scan_priv.get_public_key(), spend_pub + ) + self.assertEqual(actual, expected) + + def test_apply_label_matches_generate_labeled(self): + """apply_label + encode_silent_payment_address reproduces what + generate_silent_payment_address does internally for a label.""" + scan_priv = PrivateKey(b"\x11" * 32) + spend_pub = PrivateKey(b"\x22" * 32).get_public_key() + m = 7 + + expected = sp.generate_silent_payment_address(scan_priv, spend_pub, label=m) + labeled_spend = sp.apply_label(spend_pub, scan_priv, m) + actual = sp.encode_silent_payment_address( + scan_priv.get_public_key(), labeled_spend + ) + self.assertEqual(actual, expected) + + def test_apply_label_allows_zero_for_change(self): + """m = 0 is reserved for change and generate_silent_payment_address + forbids it, but apply_label itself must allow it (used by change + detection, not address generation).""" + scan_priv = PrivateKey(b"\x11" * 32) + spend_pub = PrivateKey(b"\x22" * 32).get_public_key() + + with self.assertRaises(ValueError): + sp.generate_silent_payment_address(scan_priv, spend_pub, label=0) + + # Should not raise. + sp.apply_label(spend_pub, scan_priv, 0) diff --git a/tests/tests/test_sp_descriptor.py b/tests/tests/test_sp_descriptor.py new file mode 100644 index 00000000..004e7ef7 --- /dev/null +++ b/tests/tests/test_sp_descriptor.py @@ -0,0 +1,302 @@ +from unittest import TestCase +from embit.descriptor import Descriptor, SilentPaymentDescriptor +from embit.descriptor.sp import SPScanKey, SPSpendKey +from embit.descriptor.arguments import KeyOrigin +from embit.descriptor.errors import DescriptorError +from embit import bip32, bip39, ec +from binascii import unhexlify + +# TODO: add more test vectors +VECTORS = [ + { + "mnemonic": "initial tilt corn easily leave weather strategy return topple gesture sad day", + "coin_type": 1, + "spscan": "tspscan1q09zrmaz09cdzs5jxm552qpv3f2gxd9vxhs0yady09jdd6aqt5e7s9fue8565hmue30u47mvc6rqwwwh0zw6ptjtqzwq7kr6h27sa09f5g6x977", + "spspend": "tspspend1q09zrmaz09cdzs5jxm552qpv3f2gxd9vxhs0yady09jdd6aqt5e772yf8kwpa7shfhuw9esasvgn8lh7e6ufea60fpvfx9dk7m3klg6sa90au8", + }, + { + "mnemonic": "tongue vanish post gentle fever figure kangaroo select infant blur phrase relief", + "coin_type": 0, + "spscan": "spscan1qnd95fpg2587jn73qg98pq8uk20y09v5c20u0e4kynsc4m2qmkrrs9cahrrlzln5nreangzkja2mj8pnwrfwudqws4vl3at3zyw2tslxtryq7pn", + "spspend": "spspend1qnd95fpg2587jn73qg98pq8uk20y09v5c20u0e4kynsc4m2qmkrrmd9tyggwt47773rhumkklet4g2us7c3x0gul65za8fg32nansdesukdqr2", + }, + { + "mnemonic": "index today witness obscure ugly curtain symbol pumpkin pelican child maple struggle arctic water tiny pizza harbor below violin eight tennis frost clown hood", + "coin_type": 1, + "spscan": "tspscan1q0z4tkwaar4ww77qgesalgzw0c40q89zh7p7hmp3qn73yrdw9jpvs9yjn9d7puunttpfjuale84erzh2z636fqgy63gp7m52v5hcnmmrrlrxnur", + "spspend": "tspspend1q0z4tkwaar4ww77qgesalgzw0c40q89zh7p7hmp3qn73yrdw9jpvnadsvv7qqcd8ytmdtzn6r5ywvccgzpw2386spvymglmszzep0svg39qpev", + }, + { + "mnemonic": "fold cotton pipe robust eagle rabbit coach average orient utility minor absurd fine claim artist rabbit kingdom original lobster cruise march city vibrant resemble", + "coin_type": 0, + "spscan": "spscan1q79q4zljllyehszny72w5zfptzpxnp96esg0n2fwecgzd2v7fr6fsy54qq8jfr6mm3pgrze8hln43my7epsfkg98wtl77ch6r5lz6pedd2jcnxk", + "spspend": "spspend1q79q4zljllyehszny72w5zfptzpxnp96esg0n2fwecgzd2v7fr6fs8y6dg3fu9jp5rhrycnuhtd555t6904x4xs7cklka8z5tk5p9xwq82pf6k", + }, +] + + +def _derive_sp_keys(mnemonic, coin_type): + seed = bip39.mnemonic_to_seed(mnemonic) + master = bip32.HDKey.from_seed(seed) + scan_priv = master.derive("m/352h/%dh/0h/1h/0" % coin_type).key + spend_priv = master.derive("m/352h/%dh/0h/0h/0" % coin_type).key + return scan_priv, spend_priv + + +class TestMnemonicVectors(TestCase): + def _keys(self, v): + network = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + return network, scan_priv, spend_priv, spend_priv.get_public_key() + + def test_spscan_encoding(self): + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + self.assertEqual( + SPScanKey(scan_priv, spend_pub, network=net).encode(), v["spscan"] + ) + + def test_spspend_encoding(self): + for v in VECTORS: + net, scan_priv, spend_priv, _ = self._keys(v) + self.assertEqual( + SPSpendKey(scan_priv, spend_priv, network=net).encode(), v["spspend"] + ) + + def test_descriptor_roundtrip_spscan(self): + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertTrue(desc.is_watch_only) + + def test_descriptor_roundtrip_spspend(self): + for v in VECTORS: + net, scan_priv, spend_priv, _ = self._keys(v) + desc_str = ( + "sp(%s)" % SPSpendKey(scan_priv, spend_priv, network=net).encode() + ) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertFalse(desc.is_watch_only) + + def test_keys_extractable_from_descriptor(self): + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + spscan = SPScanKey(scan_priv, spend_pub, network=net) + desc = SilentPaymentDescriptor.from_string("sp(%s)" % spscan.encode()) + self.assertEqual(desc.get_scan_privkey().secret, scan_priv.secret) + self.assertEqual(desc.get_spend_pubkey().sec(), spend_pub.sec()) + + def test_descriptor_from_string_dispatches_to_sp(self): + """Descriptor.from_string('sp(...)') returns a SilentPaymentDescriptor.""" + for v in VECTORS: + net, scan_priv, _, spend_pub = self._keys(v) + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = Descriptor.from_string(desc_str) + self.assertIsInstance(desc, SilentPaymentDescriptor) + self.assertEqual(str(desc), desc_str) + # Unsupported standard descriptor operations raise a clear error + self.assertRaises(DescriptorError, desc.address) + self.assertRaises(DescriptorError, desc.derive, 0) + self.assertRaises(DescriptorError, desc.script_pubkey) + + +class TestKeyOrigin(TestCase): + """Key origin prefix [fingerprint/path] is parsed and preserved in SP descriptors.""" + + def test_spscan_with_origin_roundtrip(self): + for v in VECTORS: + net = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + origin = KeyOrigin.from_string("deadbeef/352h/%dh/0h" % v["coin_type"]) + spscan = SPScanKey(scan_priv, spend_pub, origin=origin, network=net) + desc_str = "sp(%s)" % str(spscan) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertTrue(desc.is_watch_only) + + def test_spspend_with_origin_roundtrip(self): + for v in VECTORS: + net = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + origin = KeyOrigin.from_string("cafebabe/352h/%dh/0h" % v["coin_type"]) + spspend = SPSpendKey(scan_priv, spend_priv, origin=origin, network=net) + desc_str = "sp(%s)" % str(spspend) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + self.assertFalse(desc.is_watch_only) + + def test_parsed_key_retains_origin_fingerprint(self): + v = VECTORS[0] + net = "test" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + origin = KeyOrigin.from_string("deadbeef/352h/1h/0h") + spscan = SPScanKey(scan_priv, spend_pub, origin=origin, network=net) + desc = SilentPaymentDescriptor.from_string("sp(%s)" % str(spscan)) + self.assertIsNotNone(desc.sp_key.origin) + self.assertEqual(desc.sp_key.origin.fingerprint, unhexlify("deadbeef")) + + def test_origin_does_not_affect_key_content(self): + """Origin prefix doesn't change the encoded key bytes.""" + v = VECTORS[1] + net = "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + origin = KeyOrigin.from_string("cafebabe/352h/0h/0h") + spscan = SPScanKey(scan_priv, spend_pub, origin=origin, network=net) + desc = SilentPaymentDescriptor.from_string("sp(%s)" % str(spscan)) + self.assertEqual(desc.get_scan_privkey().secret, scan_priv.secret) + self.assertEqual(desc.get_spend_pubkey().sec(), spend_pub.sec()) + + +class TestTwoArgDescriptor(TestCase): + """sp(scan_key, spend_key) two-argument descriptor form.""" + + def _leaf_hdkeys(self, v): + seed = bip39.mnemonic_to_seed(v["mnemonic"]) + master = bip32.HDKey.from_seed(seed) + ct = v["coin_type"] + scan_hd = master.derive("m/352h/%dh/0h/1h/0" % ct) + spend_hd_priv = master.derive("m/352h/%dh/0h/0h/0" % ct) + spend_hd_pub = spend_hd_priv.to_public() + return scan_hd, spend_hd_priv, spend_hd_pub + + def test_xprv_xpub_is_watch_only(self): + for v in VECTORS: + scan_hd, _, spend_hd_pub = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_pub.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertFalse(desc.is_single_arg) + self.assertTrue(desc.is_watch_only) + + def test_xprv_xprv_is_not_watch_only(self): + for v in VECTORS: + scan_hd, spend_hd_priv, _ = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_priv.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertFalse(desc.is_single_arg) + self.assertFalse(desc.is_watch_only) + + def test_two_arg_roundtrip(self): + for v in VECTORS: + scan_hd, _, spend_hd_pub = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_pub.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(str(desc), desc_str) + + def test_two_arg_keys_match_direct_derivation(self): + """Two-arg form with leaf-level keys produces the same scan/spend keys as direct derivation.""" + for v in VECTORS: + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + scan_hd, _, spend_hd_pub = self._leaf_hdkeys(v) + desc_str = "sp(%s,%s)" % (scan_hd.to_base58(), spend_hd_pub.to_base58()) + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertEqual(desc.get_scan_privkey().secret, scan_priv.secret) + self.assertEqual(desc.get_spend_pubkey().sec(), spend_pub.sec()) + + +class TestChecksumHandling(TestCase): + """Descriptor #checksum suffix is stripped during parsing.""" + + def test_checksum_suffix_stripped(self): + for v in VECTORS: + net = "test" if v["coin_type"] == 1 else "main" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = SilentPaymentDescriptor.from_string(desc_str + "#aaaaaaaa") + self.assertEqual(str(desc), desc_str) + + def test_no_checksum_parses_normally(self): + v = VECTORS[0] + net = "test" + scan_priv, spend_priv = _derive_sp_keys(v["mnemonic"], v["coin_type"]) + spend_pub = spend_priv.get_public_key() + desc_str = "sp(%s)" % SPScanKey(scan_priv, spend_pub, network=net).encode() + desc = SilentPaymentDescriptor.from_string(desc_str) + self.assertTrue(desc.is_watch_only) + self.assertTrue(desc.is_single_arg) + + +class TestInvalidDescriptor(TestCase): + """Invalid sp() descriptors raise DescriptorError.""" + + def _scan_priv(self): + return ec.PrivateKey(bytes([0x01] * 32)) + + def _spend_pub(self): + return ec.PrivateKey(bytes([0x02] * 32)).get_public_key() + + def test_empty_sp(self): + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, "sp()" + ) + + def test_bare_xpub_single_arg(self): + """Single-arg sp() with a plain xpub (not spscan/spspend) is rejected.""" + seed = bytes(range(16)) + master = bip32.HDKey.from_seed(seed) + xpub = master.to_public().to_base58() + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, "sp(%s)" % xpub + ) + + def test_xpub_xpub_scan_key_rejected(self): + """Two-arg sp(xpub, xpub) is rejected — scan key must be private.""" + seed = bip39.mnemonic_to_seed(VECTORS[0]["mnemonic"]) + master = bip32.HDKey.from_seed(seed) + scan_xpub = master.derive("m/352h/1h/0h/1h/0").to_public().to_base58() + spend_xpub = master.derive("m/352h/1h/0h/0h/0").to_public().to_base58() + self.assertRaises( + DescriptorError, + SilentPaymentDescriptor.from_string, + "sp(%s,%s)" % (scan_xpub, spend_xpub), + ) + + def test_hex_pubkey_scan_key_rejected(self): + """Two-arg sp(pubkey_hex, pubkey_hex) is rejected — scan key must be private.""" + scan_pub = self._scan_priv().get_public_key() + spend_pub = self._spend_pub() + desc_str = "sp(%s,%s)" % (scan_pub.sec().hex(), spend_pub.sec().hex()) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) + + def test_spscan_in_second_position_rejected(self): + """Two-arg sp(wif, spscan1...) is rejected — second arg cannot be an spscan key.""" + scan_priv = self._scan_priv() + spscan = SPScanKey(scan_priv, self._spend_pub()) + desc_str = "sp(%s,%s)" % (scan_priv.wif(), spscan.encode()) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) + + def test_two_spscan_args_rejected(self): + """sp(spscan1..., spscan1...) is rejected — spscan must be the only argument.""" + spscan = SPScanKey(self._scan_priv(), self._spend_pub()) + desc_str = "sp(%s,%s)" % (spscan.encode(), spscan.encode()) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) + + def test_uncompressed_scan_key_rejected(self): + """Uncompressed private key as scan arg is rejected.""" + scan_priv = ec.PrivateKey(bytes([0x01] * 32), compressed=False) + spend_pub = self._spend_pub() + desc_str = "sp(%s,%s)" % (scan_priv.wif(), spend_pub.sec().hex()) + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + ) + + def test_trailing_junk_rejected(self): + """Characters after the closing ) are rejected.""" + spscan = SPScanKey(self._scan_priv(), self._spend_pub()) + desc_str = "sp(%s)junk" % spscan.encode() + self.assertRaises( + DescriptorError, SilentPaymentDescriptor.from_string, desc_str + )