From 788770113783245837fb81aec64a3e120055984d Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:04:34 -0500 Subject: [PATCH 01/13] nit: mark bitcoind dependent sign tests --- testing/test_sign.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/test_sign.py b/testing/test_sign.py index 9f1dd445..96b612a5 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -764,7 +764,7 @@ def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, use_re end_sign(True) - +@pytest.mark.bitcoind def test_wrong_p2sh_p2wpkh(bitcoind, start_sign, end_sign, bitcoind_d_sim_watch, cap_story): sim = bitcoind_d_sim_watch sim_addr = sim.getnewaddress("", "bech32") @@ -1335,6 +1335,7 @@ def value_tweak(spendables): @pytest.mark.parametrize('addr_fmt', ADDR_STYLES_SINGLE) @pytest.mark.parametrize('num_ins', [1, 17]) @pytest.mark.parametrize('num_outs', [1, 17]) +@pytest.mark.bitcoind def test_txid_calc(num_ins, fake_txn, try_sign, dev, decode_with_bitcoind, cap_story, txid_from_export_prompt, press_cancel, num_outs, addr_fmt): # verify correct txid for transactions is being calculated @@ -2927,6 +2928,7 @@ def random_nLockTime_test_cases(num=10): (1748671747, "2025-05-31 06:09:07"), *random_nLockTime_test_cases() ]) +@pytest.mark.bitcoind def test_timelocks_visualize(start_sign, end_sign, dev, bitcoind, use_regtest, bitcoind_d_sim_watch, nLockTime, sim_root_dir): # - works on simulator and connected USB real-device @@ -2983,6 +2985,7 @@ def test_timelocks_visualize(start_sign, end_sign, dev, bitcoind, use_regtest, @pytest.mark.parametrize('in_out', [(4,1),(2,2),(2,1)]) @pytest.mark.parametrize('partial', [False, True]) +@pytest.mark.bitcoind def test_base64_psbt_qr(in_out, partial, scan_a_qr, readback_bbqr, goto_home, use_regtest, cap_story, fake_txn, dev, decode_psbt_with_bitcoind, decode_with_bitcoind, From d50ab5669847d94c7f33bb55a526f71109689180 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:08:56 -0400 Subject: [PATCH 02/13] bump submodule versions external/ckcc-protocol external/libngu --- external/ckcc-protocol | 2 +- external/libngu | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/ckcc-protocol b/external/ckcc-protocol index 6d9f7193..48f407eb 160000 --- a/external/ckcc-protocol +++ b/external/ckcc-protocol @@ -1 +1 @@ -Subproject commit 6d9f7193b336ab1097c7f941ce8c7e2ae80bfe29 +Subproject commit 48f407eb107d098f9807a0c56b1c331ddf33a795 diff --git a/external/libngu b/external/libngu index b0ce9acf..e1542fb0 160000 --- a/external/libngu +++ b/external/libngu @@ -1 +1 @@ -Subproject commit b0ce9acffa455d9630c64d3614d0fb9b913c919e +Subproject commit e1542fb07ea842c453aedd18f05ad99ebf10d8df From b243727d7604c82c707f6e81b25edc22554d44a3 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:38:21 -0500 Subject: [PATCH 03/13] BIP-352: Implement Silent Payment Cryptographic Primitives update libngu - add interface for s_ec_pubkey_tweak_mul - add interface for s_ec_pubkey_combine add bip352 crypto primitives / helpers add bip352 tagged hashes --- shared/precomp_tag_hash.py | 10 ++- shared/silentpayments.py | 179 +++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 shared/silentpayments.py diff --git a/shared/precomp_tag_hash.py b/shared/precomp_tag_hash.py index 7ba0a7c9..a5f9f990 100644 --- a/shared/precomp_tag_hash.py +++ b/shared/precomp_tag_hash.py @@ -9,4 +9,12 @@ # SHA256(TapTweak) TAP_TWEAK_H = b'\xe8\x0f\xe1c\x9c\x9c\xa0P\xe3\xaf\x1b9\xc1C\xc6>B\x9c\xbc\xeb\x15\xd9@\xfb\xb5\xc5\xa1\xf4\xafW\xc5\xe9' # SHA256(TapSighash) -TAP_SIGHASH_H = b'\xf4\nH\xdfK*p\xc8\xb4\x92K\xf2eFa\xed=\x95\xfdf\xa3\x13\xeb\x87#u\x97\xc6(\xe4\xa01' \ No newline at end of file +TAP_SIGHASH_H = b'\xf4\nH\xdfK*p\xc8\xb4\x92K\xf2eFa\xed=\x95\xfdf\xa3\x13\xeb\x87#u\x97\xc6(\xe4\xa01' + +# BIP-352 tag hashes +# SHA256(BIP0352/SharedSecret) +BIP352_SHARED_SECRET_TAG_H = b'\x9fm\x80\x11X\x1e\xb6-r\xe6\x13`L3\r\xca*\x0b\xd3I\xe2JF\xd9\xa2\xef$\xb9\xa9\x8fA\xbd' +# SHA256(BIP0352/Inputs) +BIP352_INPUTS_TAG_H = b'\x1e{\x96\xeb\x16\nh\x81\x9f\x97vKC\xd5\xd7~fY\xd7Xw\x9dC\xa8\xa7u_[\xe4Z~3' +# SHA256(BIP0352/Label) +BIP352_LABEL_TAG_H = b'\x03I\x19F5\xc2\xd0>b\xd4\x13\xba\x8c\xcdQ\x98\x91\x90\x17\xa1\xe9\x9c\xbei\x1fZ4\xa9\x93w\xe0\x95' diff --git a/shared/silentpayments.py b/shared/silentpayments.py new file mode 100644 index 00000000..af768279 --- /dev/null +++ b/shared/silentpayments.py @@ -0,0 +1,179 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# silentpayments.py - BIP-352/BIP-375 Silent Payment Logic +# +# Consolidates cryptographic primitives and PSBT handling logic for Silent Payments. +# + +import ckcc +import ngu +from precomp_tag_hash import BIP352_SHARED_SECRET_TAG_H, BIP352_INPUTS_TAG_H + +SECP256K1_ORDER = ngu.secp256k1.curve_order_int() + +# ----------------------------------------------------------------------------- +# Silent Payments Cryptographic Primitives +# ----------------------------------------------------------------------------- + +def _compute_ecdh_share(a_sum, B_scan_bytes): + """ + Compute BIP-375 ECDH share for silent payments + + Formula: ecdh_share = a_sum * B_scan + + Args: + a_sum: Combined private key as scalar + B_scan_bytes: Scan public key (33 bytes compressed) + + Returns: + bytes: ECDH share as compressed public key (33 bytes) + """ + privkey_bytes = a_sum.to_bytes(32, "big") + try: + ngu.secp256k1.pubkey(B_scan_bytes) + except Exception as e: + raise ValueError("Invalid scan public key") from e + + return ngu.secp256k1.ec_pubkey_tweak_mul(B_scan_bytes, privkey_bytes) + +def _compute_shared_secret_tweak(shared_secret_bytes, k): + """ + Compute BIP-352 shared secret tweak for output index k + + BIP-352 formula: t_k = hash_BIP0352/SharedSecret(shared_secret || ser_32(k)) + + Args: + shared_secret_bytes: Combined shared secret (33 bytes compressed point) + k: Output index (0-based) + + Returns: + bytes: Shared secret tweak as 32-byte scalar (reduced mod curve order) + """ + # Concatenate shared_secret || k + msg = shared_secret_bytes + k.to_bytes(4, "big") + tweak_bytes = ngu.hash.sha256t(BIP352_SHARED_SECRET_TAG_H, msg, True) + + # Convert hash to scalar (reduce by curve order) + return (int.from_bytes(tweak_bytes, "big") % SECP256K1_ORDER).to_bytes(32, "big") + +def _compute_input_hash(outpoints, A_sum_bytes): + """ + Compute BIP-352 input hash + + BIP-352 formula: input_hash = hash_BIP0352/Inputs(smallest_outpoint || A_sum) + + Args: + outpoints: List of (txid, vout) tuples, where txid is 32 bytes and vout as (4 bytes little-endian) + A_sum_bytes: Sum of all eligible input public keys (33 bytes compressed) + + Returns: + bytes: Input hash as 32-byte scalar (reduced mod curve order) + """ + # BIP-352: use only the lexicographically smallest outpoint + smallest = min(outpoints, key=lambda x: (x[0], x[1])) + msg = smallest[0] + smallest[1] + A_sum_bytes + + input_hash_bytes = ngu.hash.sha256t(BIP352_INPUTS_TAG_H, msg, True) + + return (int.from_bytes(input_hash_bytes, "big") % SECP256K1_ORDER).to_bytes(32, "big") + +def _combine_pubkeys(pubkeys): + """ + Combine a list of public keys into a single public key + + Args: + pubkeys: List of public keys (33 bytes compressed) + + Returns: + bytes: Combined public key (33 bytes compressed) + + Raises: + ValueError: If list is empty or keys are invalid + """ + if not pubkeys: + raise ValueError("No public keys to combine") + + combined_pk = pubkeys[0] + try: + for pk in pubkeys[1:]: + combined_pk = ngu.secp256k1.ec_pubkey_combine(combined_pk, pk) + except Exception as e: + raise ValueError("Failed to combine public keys") from e + + return combined_pk + +def _compute_silent_payment_output_script( + outpoints, A_sum_bytes, ecdh_share_bytes, B_spend, k=0 +): + """ + Compute the P2TR scriptPubKey for silent payment with output index k. + + BIP-352 formula: P_k = B_spend + t_k * G + where input_hash = hash_BIP0352/Inputs(outpoints || A_sum) + t_k = hash_BIP0352/SharedSecret(ecdh_share * input_hash || ser_32(k)) + + Args: + outpoints: List of (txid, vout) tuples from eligible inputs + A_sum_bytes: Sum of eligible input public keys (33 bytes compressed) + ecdh_share_bytes: ECDH share point (33 bytes compressed) + B_spend: Recipient spend public key (33 bytes compressed) + k: Output index for this recipient + + Returns: + bytes: P2TR scriptPubKey (34 bytes: OP_1 <32-byte x-only pubkey>) + + Raises: + ValueError: If B_spend or output_pubkey is invalid + """ + try: + ngu.secp256k1.pubkey(B_spend) + except Exception: + raise ValueError("Invalid spend public key") + + # Compute shared secret using input hash and ecdh_share + input_hash_bytes = _compute_input_hash(outpoints, A_sum_bytes) + shared_secret_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(ecdh_share_bytes, input_hash_bytes) + + # Compute shared secret tweak + tweak_bytes = _compute_shared_secret_tweak(shared_secret_bytes, k) + + # Compute t_k * G using the generator point + G = ngu.secp256k1.generator() + tweak_point = ngu.secp256k1.ec_pubkey_tweak_mul(G, tweak_bytes) + + # Derive output pubkey: P_k = B_spend + t_k * G + output_pubkey = ngu.secp256k1.ec_pubkey_combine(B_spend, tweak_point) + + if len(output_pubkey) != 33: + raise ValueError("Invalid pubkey length") + x_only = output_pubkey[1:] + + return b"\x51\x20" + x_only + + +# ----------------------------------------------------------------------------- +# Input Eligibility (BIP-352) +# ----------------------------------------------------------------------------- + +def _is_p2pkh(spk): + # OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG + return ( + len(spk) == 25 + and spk[0] == 0x76 + and spk[1] == 0xA9 + and spk[2] == 0x14 + and spk[-2] == 0x88 + and spk[-1] == 0xAC + ) + +def _is_p2wpkh(spk): + # OP_0 OP_PUSHBYTES_20 <20 bytes> + return len(spk) == 22 and spk[0] == 0x00 and spk[1] == 0x14 + +def _is_p2tr(spk): + # OP_1 OP_PUSHBYTES_32 <32 bytes> + return len(spk) == 34 and spk[0] == 0x51 and spk[1] == 0x20 + +def _is_p2sh(spk): + # OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL + return len(spk) == 23 and spk[0] == 0xA9 and spk[1] == 0x14 and spk[-1] == 0x87 From c51661050e453c9f18c8e305ea97fd85583a2c58 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:38:42 -0500 Subject: [PATCH 04/13] BIP-374: Implement DLEQ Proof Generation and Verification add bip374 tagged hashes add dleq bip374 reference generate_dleq_proof, verify_dleq_proof functions --- shared/dleq.py | 213 +++++++++++++++++++++++++++++++++++++ shared/precomp_tag_hash.py | 8 ++ 2 files changed, 221 insertions(+) create mode 100644 shared/dleq.py diff --git a/shared/dleq.py b/shared/dleq.py new file mode 100644 index 00000000..275d3ff7 --- /dev/null +++ b/shared/dleq.py @@ -0,0 +1,213 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# dleq.py - DLEQ (Discrete Logarithm Equality) proofs for BIP-374 +# +# Implements non-interactive zero-knowledge proofs that prove: +# log_G(A) = log_B(C) +# where: +# - G is the secp256k1 generator +# - A = a*G (sender's public key) +# - B = (recipient's scan key) +# - C = a*B (ECDH share) +# +# This proves the ECDH computation is correct without revealing the private key. +# +import ckcc +import ngu +from precomp_tag_hash import DLEQ_TAG_AUX_H, DLEQ_TAG_NONCE_H, DLEQ_TAG_CHALLENGE_H + + +def xor_bytes(a, b): + """XOR two byte strings of equal length""" + assert len(a) == len(b), "Byte strings must be equal length" + return bytes(x ^ y for x, y in zip(a, b)) + + +def dleq_challenge(A_bytes, B_bytes, C_bytes, R1_bytes, R2_bytes, m=None, G_bytes=None): + """ + Compute DLEQ challenge using BIP-374 tagged hash + + Args: + A_bytes: A_sum, A = a*G (33 bytes compressed) + B_bytes: B_scan, B = scan public key (33 bytes compressed) + C_bytes: C_ecdh, C = a*B (33 bytes compressed) + R1_bytes: Commitment R1 = k*G (33 bytes compressed) + R2_bytes: Commitment R2 = k*B (33 bytes compressed) + m: Optional message (32 bytes or None) + G_bytes: Generator point G (33 bytes compressed) + + Returns: + int: Challenge value e + """ + # BIP-374: e = TaggedHash("BIP0374/challenge", cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m) + challenge_input = A_bytes + B_bytes + C_bytes + G_bytes + R1_bytes + R2_bytes + + # Append message if provided + if m is not None: + challenge_input += m + + challenge_hash = ngu.hash.sha256t(DLEQ_TAG_CHALLENGE_H, challenge_input, True) + return int.from_bytes(challenge_hash, 'big') + + +def generate_dleq_proof(a_sum_scalar, B_scan, aux_rand=None, m=None): + """ + Generate DLEQ proof (BIP-374) + + Args: + a_sum_scalar: Input private key a (scalar) + B_scan: Scan public key B (33 bytes compressed) + aux_rand: Auxiliary randomness r (32 bytes), if None uses hardware RNG + m: Optional message (32 bytes or None) + + Returns: + bytes: DLEQ proof (64 bytes: e || s) + + Raises: + ValueError: If inputs are invalid + """ + # Validate inputs + if not isinstance(a_sum_scalar, int) or a_sum_scalar <= 0: + raise ValueError("Invalid private key") + + if len(B_scan) != 33: + raise ValueError("Invalid scan pubkey length") + + if m is not None and len(m) != 32: + raise ValueError("Message must be 32 bytes") + + # Validate scan_pubkey is a valid point + try: + ngu.secp256k1.pubkey(B_scan) + except Exception: + raise ValueError("Invalid elliptic curve point") + + # Validate privkey is in valid range + if a_sum_scalar >= ngu.secp256k1.curve_order_int(): + raise ValueError("Private key must be less than curve order") + + # Compute public key A = a*G + G_bytes = ngu.secp256k1.generator() + privkey_bytes = a_sum_scalar.to_bytes(32, 'big') + A_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(G_bytes, privkey_bytes) + + # Compute ECDH share C = a*B (this is what we're proving knowledge of) + C_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(B_scan, privkey_bytes) + + # Generate aux_rand if not provided + if aux_rand is None: + aux_rand = bytearray(32) + ckcc.rng_bytes(aux_rand) + aux_rand = bytes(aux_rand) + else: + if len(aux_rand) != 32: + raise ValueError("aux_rand must be 32 bytes") + + # t = a XOR TaggedHash("BIP0374/aux", r) + aux_hash = ngu.hash.sha256t(DLEQ_TAG_AUX_H, aux_rand, True) + del aux_rand + t = xor_bytes(privkey_bytes, aux_hash) + + # rand = TaggedHash("BIP0374/nonce", t || A || C || m) + nonce_input = t + A_bytes + C_bytes + if m is not None: + nonce_input += m + rand = ngu.hash.sha256t(DLEQ_TAG_NONCE_H, nonce_input, True) + + # k = int(rand) mod n + k = int.from_bytes(rand, 'big') % ngu.secp256k1.curve_order_int() + if k == 0: + raise ValueError("Generated nonce k is zero (extremely unlikely)") + + # R1 = k*G, R2 = k*B + k_bytes = k.to_bytes(32, 'big') + R1_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(G_bytes, k_bytes) + R2_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(B_scan, k_bytes) + + # e = TaggedHash("BIP0374/challenge", A || B || C || G || R1 || R2 || m) + e = dleq_challenge(A_bytes, B_scan, C_bytes, R1_bytes, R2_bytes, m, G_bytes) + + # s = (k + e*a) mod n + s = (k + e * a_sum_scalar) % ngu.secp256k1.curve_order_int() + + # proof = e || s + proof = e.to_bytes(32, 'big') + s.to_bytes(32, 'big') + + # Verify the proof before returning (sanity check) + if not verify_dleq_proof(A_bytes, B_scan, C_bytes, proof, m=m): + raise ValueError("Generated proof failed verification (internal error)") + return proof + + +def verify_dleq_proof(A_sum_bytes, B_scan_bytes, ecdh_share_bytes, proof, m=None): + """ + Verify DLEQ proof (BIP-374) + + Verifies that the prover knows a value a such that: + - A = a * G (pubkey) + - C = a * B (ecdh_share) + without revealing a (the private key). + + Args: + A_sum_bytes: Input public key A (33 bytes compressed) + B_scan_bytes: Scan public key B (33 bytes compressed) + ecdh_share_bytes: ECDH share C (33 bytes compressed) + proof: DLEQ proof (64 bytes: e || s) + m: Optional message (32 bytes or None) + + Returns: + bool: True if proof is valid, False otherwise + """ + # Validate proof length + if len(proof) != 64: + return False + + if m is not None and len(m) != 32: + return False + + # Parse proof + e_bytes = proof[:32] + s_bytes = proof[32:] + + try: + # Validate scalars are in valid range + s = int.from_bytes(s_bytes, 'big') + if s >= ngu.secp256k1.curve_order_int(): + return False + # Note: e can be >= n since it's a hash output reduced mod n + + # Validate points + ngu.secp256k1.pubkey(A_sum_bytes) + ngu.secp256k1.pubkey(B_scan_bytes) + ngu.secp256k1.pubkey(ecdh_share_bytes) + except Exception: + # Invalid points + return False + + # Get generator point + G_bytes = ngu.secp256k1.generator() + + # Reconstruct R1 = s*G - e*A + # We compute this as s*G + (-e)*A using point negation + sG = ngu.secp256k1.ec_pubkey_tweak_mul(G_bytes, s_bytes) + eA = ngu.secp256k1.ec_pubkey_tweak_mul(A_sum_bytes, e_bytes) + # Negate eA by flipping the y-coordinate (change 02<->03 prefix) + eA_neg = bytearray(eA) + eA_neg[0] = 0x03 if eA[0] == 0x02 else 0x02 + R1_bytes = ngu.secp256k1.ec_pubkey_combine(sG, bytes(eA_neg)) + + # Reconstruct R2 = s*B - e*C + sB = ngu.secp256k1.ec_pubkey_tweak_mul(B_scan_bytes, s_bytes) + eC = ngu.secp256k1.ec_pubkey_tweak_mul(ecdh_share_bytes, e_bytes) + # Negate eC + eC_neg = bytearray(eC) + eC_neg[0] = 0x03 if eC[0] == 0x02 else 0x02 + R2_bytes = ngu.secp256k1.ec_pubkey_combine(sB, bytes(eC_neg)) + + # Recompute challenge e' + e_check = dleq_challenge(A_sum_bytes, B_scan_bytes, ecdh_share_bytes, + R1_bytes, R2_bytes, m, G_bytes) + + # Verify e == e' + e = int.from_bytes(e_bytes, 'big') + return e == e_check \ No newline at end of file diff --git a/shared/precomp_tag_hash.py b/shared/precomp_tag_hash.py index a5f9f990..3d328dcc 100644 --- a/shared/precomp_tag_hash.py +++ b/shared/precomp_tag_hash.py @@ -18,3 +18,11 @@ BIP352_INPUTS_TAG_H = b'\x1e{\x96\xeb\x16\nh\x81\x9f\x97vKC\xd5\xd7~fY\xd7Xw\x9dC\xa8\xa7u_[\xe4Z~3' # SHA256(BIP0352/Label) BIP352_LABEL_TAG_H = b'\x03I\x19F5\xc2\xd0>b\xd4\x13\xba\x8c\xcdQ\x98\x91\x90\x17\xa1\xe9\x9c\xbei\x1fZ4\xa9\x93w\xe0\x95' + +# BIP-374 DLEQ proof tag hashes +# SHA256(BIP0374/aux) +DLEQ_TAG_AUX_H = b'\xfc\xe2u Date: Thu, 19 Mar 2026 14:08:45 -0400 Subject: [PATCH 05/13] SP: Add SilentPaymentsMixin extend psbt handling with silent payment specific functions incorporate BIP375 test validation functions - psbt_structure, input_eligibility, ecdh_coverage --- shared/silentpayments.py | 571 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 571 insertions(+) diff --git a/shared/silentpayments.py b/shared/silentpayments.py index af768279..46095ed8 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -7,8 +7,15 @@ import ckcc import ngu +from dleq import generate_dleq_proof, verify_dleq_proof +from exceptions import FatalPSBTIssue from precomp_tag_hash import BIP352_SHARED_SECRET_TAG_H, BIP352_INPUTS_TAG_H +from serializations import SIGHASH_ALL, SIGHASH_DEFAULT +from ubinascii import unhexlify as a2b_hex +from utils import keypath_to_str +# BIP-341 NUMS point (Nothing Up My Sleeve) - x-only (32 bytes) +NUMS_H = a2b_hex('50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0') SECP256K1_ORDER = ngu.secp256k1.curve_order_int() # ----------------------------------------------------------------------------- @@ -177,3 +184,567 @@ def _is_p2tr(spk): def _is_p2sh(spk): # OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL return len(spk) == 23 and spk[0] == 0xA9 and spk[1] == 0x14 and spk[-1] == 0x87 + + +# ----------------------------------------------------------------------------- +# PSBT Mixin +# ----------------------------------------------------------------------------- + +class SilentPaymentsMixin: + """ + Mixin class for psbtObject to handle Silent Payments logic. + + This class assumes it is mixed into psbtObject and has access to: + - self.inputs + - self.outputs + - self.get() + - self.my_xfp + - self.parse_xfp_path() + """ + + # ----------------------------------------------------------------------------- + # Input Helper Functions + # ----------------------------------------------------------------------------- + + def _is_input_eligible(self, inp): + # Check if input is eligible for silent payments per BIP-352 + # Returns (bool) + spk = inp.utxo_spk + if not spk: + return False + + if not (_is_p2pkh(spk) or _is_p2wpkh(spk) or _is_p2tr(spk) or _is_p2sh(spk)): + return False + + if _is_p2tr(spk): + if inp.taproot_internal_key: + tap_ik = self.get(inp.taproot_internal_key) + if tap_ik == NUMS_H: + return False + + if _is_p2sh(spk): + if inp.redeem_script: + rs = self.get(inp.redeem_script) + if not _is_p2wpkh(rs): + return False + else: + return False + + return True + + def _pubkey_from_input(self, inp): + """ + Extract the BIP-352 contributing public key from an input. + + Note: + P2TR: use PSBT_IN_WITNESS_UTXO to fetch (x-only -> compressed with 0x02) + non-taproot: use BIP32 derivation pubkey (first 33-byte key) + + Returns 33-byte compressed pubkey or None. + """ + # TODO: BIP-376 use sp_spend_derivation when available + spk = inp.utxo_spk + if spk and _is_p2tr(spk): + return b"\x02" + spk[2:34] + else: + if inp.subpaths: + for pk_coords, _ in inp.subpaths: + pk = self.get(pk_coords) + if len(pk) == 33: + return pk + + return None + + + # ----------------------------------------------------------------------------- + # Validation Functions + # ----------------------------------------------------------------------------- + + def _process_silent_payments(self, sv): + """ + Core SP workflow: validate, compute shares, compute scripts if ready. + + Returns True if output scripts were computed (ready to sign). + """ + if not self.has_silent_payment_outputs(): + return + self._validate_psbt_structure() + self._validate_input_eligibility() + self._validate_ecdh_coverage() + + if not self._compute_and_store_ecdh_shares(sv): + return False + + if self._is_ecdh_coverage_complete(): + self._compute_silent_payment_output_scripts() + return True + + return False + + def _validate_psbt_structure(self): + """ + Validate PSBT structure requirements for silent payments + + Raises FatalPSBTIssue if any structural requirements are violated + """ + for i, outp in enumerate(self.outputs): + has_sp_info = bool(outp.sp_v0_info) + has_sp_label = bool(outp.sp_v0_label) + has_script = bool(outp.script and self.get(outp.script)) + + # Output must have script or SP info + if not has_script and not has_sp_info: + raise FatalPSBTIssue( + "Output #%d must have either PSBT_OUT_SCRIPT or PSBT_OUT_SP_V0_INFO" % i + ) + + if has_sp_label and not has_sp_info: + raise FatalPSBTIssue( + "Output #%d has SP label but missing SP_V0_INFO" % i + ) + + if has_sp_info: + if len(outp.sp_v0_info) != 66: + raise FatalPSBTIssue( + "Output #%d SP_V0_INFO wrong size (%d bytes, expected 66)" + % (i, len(outp.sp_v0_info)) + ) + + # Validate ECDH share sizes (33 bytes) and DLEQ proof sizes (64 bytes) + if self.sp_global_ecdh_shares: + for _, share in self.sp_global_ecdh_shares.items(): + if len(share) != 33: + raise FatalPSBTIssue( + "Global ECDH share wrong size (%d bytes, expected 33)" + % len(share) + ) + + if self.sp_global_dleq_proofs: + for _, proof in self.sp_global_dleq_proofs.items(): + if len(proof) != 64: + raise FatalPSBTIssue( + "Global DLEQ proof wrong size (%d bytes, expected 64)" + % len(proof) + ) + + for i, inp in enumerate(self.inputs): + if inp.sp_ecdh_shares: + for _, share in inp.sp_ecdh_shares.items(): + if len(share) != 33: + raise FatalPSBTIssue( + "Input #%d ECDH share wrong size (%d bytes, expected 33)" + % (i, len(share)) + ) + if inp.sp_dleq_proofs: + for _, proof in inp.sp_dleq_proofs.items(): + if len(proof) != 64: + raise FatalPSBTIssue( + "Input #%d DLEQ proof wrong size (%d bytes, expected 64)" + % (i, len(proof)) + ) + + # TX_MODIFIABLE must be cleared when output scripts are finalized + for outp in self.outputs: + if outp.sp_v0_info and outp.script: + if self.txn_modifiable is not None and self.txn_modifiable != 0: + raise FatalPSBTIssue( + "TX_MODIFIABLE not cleared but SP output script is set" + ) + + def _validate_input_eligibility(self): + """ + Validate input constraints for silent payments (BIP-375) + + Raises FatalPSBTIssue if any input constraints are violated + """ + for i, inp in enumerate(self.inputs): + if not inp.utxo_spk: + continue + + spk = inp.utxo_spk + # No segwit v>1 inputs when SP outputs present + if len(spk) >= 2 and 0x52 <= spk[0] <= 0x60: + witness_version = spk[0] - 0x50 + raise FatalPSBTIssue( + "BIP-375 violation: Input #%d spends Segwit v%d output. " + "Silent payment outputs cannot be mixed with Segwit v>1 inputs." + % (i, witness_version) + ) + + # SIGHASH_ALL required when SP outputs present + if inp.sighash is not None and inp.sighash not in ( + SIGHASH_ALL, + SIGHASH_DEFAULT, + ): + raise FatalPSBTIssue( + "BIP-375 violation: Input #%d uses sighash 0x%x. " + "Silent payments require SIGHASH_ALL." % (i, inp.sighash) + ) + + def _validate_ecdh_coverage(self): + """ + Validate ECDH share coverage and DLEQ proof correctness (BIP-375) + + Raises FatalPSBTIssue if any ECDH / DLEQ requirements are violated + """ + scan_keys = self._get_silent_payment_scan_keys() + if not scan_keys: + return + + for scan_key in scan_keys: + has_global = ( + self.sp_global_ecdh_shares and scan_key in self.sp_global_ecdh_shares + ) + has_input = any( + inp.sp_ecdh_shares and scan_key in inp.sp_ecdh_shares + for inp in self.inputs + ) + + # Check if any output with this scan pk has a computed script + scan_key_has_script = any( + outp.sp_v0_info and outp.sp_v0_info[:33] == scan_key and outp.script + for outp in self.outputs + ) + + if scan_key_has_script and not has_global and not has_input: + raise FatalPSBTIssue( + "SP output script set but no ECDH share for scan key" + ) + + # Verify global DLEQ proof + if has_global: + if ( + not self.sp_global_dleq_proofs + or scan_key not in self.sp_global_dleq_proofs + ): + raise FatalPSBTIssue("Global ECDH share missing DLEQ proof") + + ecdh_share = self.sp_global_ecdh_shares[scan_key] + proof = self.sp_global_dleq_proofs[scan_key] + + # Sum all eligible input pubkeys + pubkeys = [] + for inp in self.inputs: + if not self._is_input_eligible(inp): + continue + pk = self._pubkey_from_input(inp) + if pk: + pubkeys.append(pk) + if not pubkeys: + raise FatalPSBTIssue("No public keys found for DLEQ verification") + combined_pk = _combine_pubkeys(pubkeys) + + if not verify_dleq_proof(combined_pk, scan_key, ecdh_share, proof): + raise FatalPSBTIssue("Global DLEQ proof verification failed") + + # Verify per-input coverage and DLEQ proofs + if scan_key_has_script and not has_global: + for i, inp in enumerate(self.inputs): + eligible = self._is_input_eligible(inp) + has_share = inp.sp_ecdh_shares and scan_key in inp.sp_ecdh_shares + + if not eligible and has_share: + raise FatalPSBTIssue( + "Input #%d has ECDH share but is ineligible" % i + ) + if eligible and not has_share: + raise FatalPSBTIssue( + "Eligible input #%d missing ECDH share" % i + ) + + if has_share: + if not inp.sp_dleq_proofs or scan_key not in inp.sp_dleq_proofs: + raise FatalPSBTIssue( + "Input #%d ECDH share missing DLEQ proof" % i + ) + + pk = self._pubkey_from_input(inp) + if not pk: + raise FatalPSBTIssue( + "Input #%d missing public key for DLEQ verification" % i + ) + + ecdh_share = inp.sp_ecdh_shares[scan_key] + proof = inp.sp_dleq_proofs[scan_key] + if not verify_dleq_proof(pk, scan_key, ecdh_share, proof): + raise FatalPSBTIssue( + "Input #%d DLEQ proof verification failed" % i + ) + + def _is_ecdh_coverage_complete(self): + """ + Check if all eligible inputs have ECDH shares for all scan keys + + Returns: + bool: True if coverage is complete, False if any eligible input is missing a share for any scan key + """ + scan_keys = self._get_silent_payment_scan_keys() + if not scan_keys: + return True + + for scan_key in scan_keys: + if self.sp_global_ecdh_shares and scan_key in self.sp_global_ecdh_shares: + continue + # Check per-input: every eligible input must have a share + for inp in self.inputs: + if self._is_input_eligible(inp): + if not inp.sp_ecdh_shares or scan_key not in inp.sp_ecdh_shares: + return False + return True + + + # ----------------------------------------------------------------------------- + # Process Output Functions + # ----------------------------------------------------------------------------- + + def has_silent_payment_outputs(self): + """ + Check if PSBT contains any silent payment outputs + + Returns: + bool: True if any output has PSBT_OUT_SP_V0_INFO field + """ + for outp in self.outputs: + if outp.sp_v0_info: + return True + return False + + def _compute_and_store_ecdh_shares(self, sv): + """ + Compute ECDH shares and DLEQ proofs for our inputs, store in PSBT fields + + Notes: + For single-signer: sum all private keys, store as global fields. + For multi-signer: store per-input for owned inputs. (Contribute shares) + Sets self.sp_all_inputs_ours for callers. + + Returns: + bool: True if shares were computed, False if we have no signable inputs + """ + # Collect per-input private keys for eligible inputs we own. + # Track foreign ownership: if _derive_input_privkey returns None for an input + # that has derivation paths, that input belongs to another signer. + has_foreign = False + input_privkeys = [] # list of (inp, privkey_int) + for inp in self.inputs: + if not inp.sp_idxs or not self._is_input_eligible(inp): + continue + + privkey_int = self._derive_input_privkey(inp, sv) + if privkey_int: + input_privkeys.append((inp, privkey_int)) + elif inp.taproot_subpaths or inp.subpaths: + has_foreign = True + + # Detect foreign eligible inputs (different XFP, no sp_idxs) + if not has_foreign: + for inp in self.inputs: + if inp.sp_idxs: + continue + if self._is_input_eligible(inp) and self._pubkey_from_input(inp): + has_foreign = True + break + + if not input_privkeys: + return False + + self.sp_all_inputs_ours = not has_foreign + all_inputs_ours = self.sp_all_inputs_ours + + scan_keys = self._get_silent_payment_scan_keys() + if not scan_keys: + return False + + for scan_key in scan_keys: + if all_inputs_ours: + # Single-signer: sum all private keys, one global ECDH share and DLEQ proofs + combined_sk = 0 + for _, privkey_int in input_privkeys: + combined_sk = (combined_sk + privkey_int) % SECP256K1_ORDER + + ecdh_share = _compute_ecdh_share(combined_sk, scan_key) + aux_rand = bytearray(32) + ckcc.rng_bytes(aux_rand) + dleq_proof = generate_dleq_proof(combined_sk, scan_key, bytes(aux_rand)) + + if self.sp_global_ecdh_shares is None: + self.sp_global_ecdh_shares = {} + if self.sp_global_dleq_proofs is None: + self.sp_global_dleq_proofs = {} + self.sp_global_ecdh_shares[scan_key] = ecdh_share + self.sp_global_dleq_proofs[scan_key] = dleq_proof + else: + # Multi-signer: per-input ECDH shares and DLEQ proofs for owned inputs + for inp, privkey_int in input_privkeys: + ecdh_share = _compute_ecdh_share(privkey_int, scan_key) + aux_rand = bytearray(32) + ckcc.rng_bytes(aux_rand) + dleq_proof = generate_dleq_proof(privkey_int, scan_key, bytes(aux_rand)) + + if inp.sp_ecdh_shares is None: + inp.sp_ecdh_shares = {} + if inp.sp_dleq_proofs is None: + inp.sp_dleq_proofs = {} + inp.sp_ecdh_shares[scan_key] = ecdh_share + inp.sp_dleq_proofs[scan_key] = dleq_proof + + return True + + def _iter_input_xfp_paths(self, inp): + """ + Iterate over all BIP32 derivation paths for an input, yielding parsed xfp_path tuples + + Note: + For taproot inputs, yields paths from taproot_subpaths (path_coords[2]). + For non-taproot inputs, yields paths from subpaths (path_coords). + + Returns: + Generator of parsed xfp_path tuples for all derivation paths in the input + """ + if inp.taproot_subpaths: + for _, path_coords in inp.taproot_subpaths: + yield self.parse_xfp_path(path_coords[2]) + elif inp.subpaths: + for _, path_coords in inp.subpaths: + yield self.parse_xfp_path(path_coords) + + def _path_to_privkey(self, xfp_path, sv): + """ + Derive private key from a parsed xfp_path tuple + + Returns: + int: The derived private key as an integer + """ + node = sv.derive_path(keypath_to_str(xfp_path, skip=1), register=False) + return int.from_bytes(node.privkey(), "big") + + def _derive_input_privkey(self, inp, sv): + """ + Derive the BIP-352 contributing private key for an eligible input + + Note: + For taproot inputs, uses the internal key's derivation path (ik_idx), XFP-checked + For non-taproot inputs, uses the first matching BIP32 derivation path + + Returns: + int | None: The derived private key as an integer, or None if not eligible + """ + # TODO: BIP-376 sp_spend_derivation should go here + spk = inp.utxo_spk + if spk and _is_p2tr(spk): + if inp.ik_idx is not None and inp.taproot_subpaths: + _, path_coords = inp.taproot_subpaths[inp.ik_idx] + xfp_path = self.parse_xfp_path(path_coords[2]) + if xfp_path[0] == self.my_xfp: + return self._path_to_privkey(xfp_path, sv) + else: + for xfp_path in self._iter_input_xfp_paths(inp): + if xfp_path[0] == self.my_xfp: + return self._path_to_privkey(xfp_path, sv) + return None + + def _get_outpoints(self): + """ + Get a list of outpoints (txid, vout) for all inputs + + Returns: + list[tuple[bytes, bytes]]: A list of tuples containing the transaction ID and output index for each input + """ + outpoints = [] + for inp in self.inputs: + if inp.previous_txid and inp.prevout_idx is not None: + outpoints.append( + (self.get(inp.previous_txid), self.get(inp.prevout_idx)) + ) + else: + raise FatalPSBTIssue("Missing outpoint for silent payment input") + if not outpoints: + raise FatalPSBTIssue( + "No eligible inputs for silent payment output computation" + ) + return outpoints + + def _compute_silent_payment_output_scripts(self): + """ + Compute and set the scriptPubKey for each silent payment output based on the ECDH shares and input pubkeys. + + Note: All validations must be done before calling this function. + + No return value; modifies self.outputs in-place. + """ + outpoints = self._get_outpoints() + + # Track k per scan key + scan_key_k = {} + + for out_idx, outp in enumerate(self.outputs): + if not outp.sp_v0_info: + continue + + scan_key = outp.sp_v0_info[:33] + B_spend = outp.sp_v0_info[33:66] + k = scan_key_k.get(scan_key, 0) + + ecdh_share, summed_pubkey = self._get_ecdh_and_pubkey(scan_key) + if not ecdh_share or not summed_pubkey: + raise FatalPSBTIssue("Missing ECDH share for output #%d" % out_idx) + + outp.script = _compute_silent_payment_output_script( + outpoints, summed_pubkey, ecdh_share, B_spend, k + ) + + scan_key_k[scan_key] = k + 1 + + def _get_ecdh_and_pubkey(self, scan_key): + """ + Get ECDH share and summed pubkey for a given scan key + + Returns: + (ecdh_share_bytes, summed_pubkey_bytes) or (None, None) + """ + # Global share: return ECDH share directly, sum all eligible input pubkeys + if self.sp_global_ecdh_shares and scan_key in self.sp_global_ecdh_shares: + ecdh_share = self.sp_global_ecdh_shares[scan_key] + # Sum all eligible input pubkeys + pubkeys = [] + for inp in self.inputs: + if self._is_input_eligible(inp): + pk = self._pubkey_from_input(inp) + if pk: + pubkeys.append(pk) + if pubkeys: + return ecdh_share, _combine_pubkeys(pubkeys) + + # Check per-input ECDH shares — combine via EC point addition + summed_ecdh = None + pubkeys = [] + for inp in self.inputs: + if inp.sp_ecdh_shares and scan_key in inp.sp_ecdh_shares: + share = inp.sp_ecdh_shares[scan_key] + if summed_ecdh is None: + summed_ecdh = share + else: + summed_ecdh = ngu.secp256k1.ec_pubkey_combine(summed_ecdh, share) + pk = self._pubkey_from_input(inp) + if pk: + pubkeys.append(pk) + + if summed_ecdh and pubkeys: + return summed_ecdh, _combine_pubkeys(pubkeys) + + return None, None + + def _get_silent_payment_scan_keys(self): + """ + Extract unique scan keys from silent payment outputs + + Returns: + list: List of unique scan_key bytes (33 bytes each) + """ + scan_keys = set() + for outp in self.outputs: + if outp.sp_v0_info: + scan_key = outp.sp_v0_info[:33] + scan_keys.add(scan_key) + + return list(scan_keys) From 0b2b60d5929d50ac33b6fd84c4b937e25a24807d Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:34:33 -0400 Subject: [PATCH 06/13] fix: (psbt v2) structural changes only store key_data for short_values - remove key_type only serialize PSBT_OUT_SCRIPT if self.script has a value adjust v2 script assert to not required if sp_v0_info is present --- shared/psbt.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/shared/psbt.py b/shared/psbt.py index 97c5b46f..1273a8c8 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -248,7 +248,8 @@ def parse(self, fd): # storing offset and length only! Mostly. if kt in self.short_values: actual = fd.read(vs) - self.store(kt, bytes(key), actual) + # only store key data for short_values + self.store(kt, bytes(key[1:]), actual) else: # skip actual data for now # TODO: could this be stored more compactly? @@ -506,7 +507,8 @@ def serialize(self, out_fd, is_v2): wr(PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS, v, k) if is_v2: - wr(PSBT_OUT_SCRIPT, self.script) + if self.script is not None: + wr(PSBT_OUT_SCRIPT, self.script) wr(PSBT_OUT_AMOUNT, self.amount) if self.proprietary: @@ -1835,7 +1837,9 @@ async def validate(self): if self.is_v2: # v2 requires inclusion assert o.amount - assert o.script + # Silent Payments: if not spending to silent payment then script must be provided + if not o.sp_v0_info: + assert o.script, "v2 script required when not silent payment output" if o.amount == 0 and o.script == b'\x6a': null_data_op_return = True else: From 03197adbc3727be54077a3a7b93eb83741d3e38d Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:29:11 -0400 Subject: [PATCH 07/13] fix: (usb_test) Use single namespace dict so globals and locals are unified provides silent payments testing framework with access to MockPSBT --- shared/usb_test_commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shared/usb_test_commands.py b/shared/usb_test_commands.py index 4747bf8e..b76d6a96 100644 --- a/shared/usb_test_commands.py +++ b/shared/usb_test_commands.py @@ -33,7 +33,9 @@ def do_usb_command(cmd, args): if cmd == 'EXEC': RV = uio.BytesIO() - exec(str(args, 'utf8'), None, dict(RV=RV)) + ns = globals().copy() + ns['RV'] = RV + exec(str(args, 'utf8'), ns) return b'biny' + RV.getvalue() except BaseException as exc: From e31c9d3a7a9a3d23afa054225d36607f55d87f37 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:49:15 -0500 Subject: [PATCH 08/13] BIP-375: Add Silent Payments Test Vectors migrate test_silentpayments to use simulator and bip375_test_vectors.json - add devtest/unit_silentpayments.py - add devtest/verify_sp_outputs.py mirror psbt fields in testing/psbt correct input eligibility ecdh share assumptions --- shared/silentpayments.py | 138 +- testing/bip375_test_vectors.json | 1717 ++++++++++++++++++++++++ testing/devtest/unit_silentpayments.py | 559 ++++++++ testing/devtest/verify_sp_outputs.py | 227 ++++ testing/psbt.py | 50 + testing/sp_helpers.py | 144 ++ testing/test_silentpayments.py | 200 +++ 7 files changed, 3013 insertions(+), 22 deletions(-) create mode 100644 testing/bip375_test_vectors.json create mode 100644 testing/devtest/unit_silentpayments.py create mode 100644 testing/devtest/verify_sp_outputs.py create mode 100644 testing/sp_helpers.py create mode 100644 testing/test_silentpayments.py diff --git a/shared/silentpayments.py b/shared/silentpayments.py index 46095ed8..e1187cb9 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -202,6 +202,97 @@ class SilentPaymentsMixin: - self.parse_xfp_path() """ + def encode_silent_payment_address(self, output): + """ + Encode a human-readable Silent Payment address + + Uses current chain's HRP for encoding + + Args: + output: Output object from self.outputs + + Returns: + str: bech32m-encoded Silent Payment address (e.g., "sp1...") + """ + if not output.sp_v0_info: + raise ValueError("Output is not a silent payment output") + + scan_key = output.sp_v0_info[:33] + spend_key = output.sp_v0_info[33:] + + # Get Silent Payment HRP from current chain + import chains + + hrp = chains.current_chain().sp_hrp + version = 0 # Currently only v0 supported + return ngu.codecs.bip352_encode(hrp, scan_key, spend_key, version) + + def preview_silent_payment_outputs(self): + """ + Computes ECDH shares and output scripts for silent payment outputs if we have the necessary information. + + Notes: + - This function is intended to be called during the preview phase, before signing. + - Single signer should be able to generate output and preview immediately. + - Multi-signer should generate shares and prompt user to collect all shares if not complete. + + Returns: + bool: True if output scripts were computed and are ready for preview + False if we generated shares but are waiting on others, or if we don't have necessary info to proceed + """ + try: + import stash + + with stash.SensitiveValues() as sv: + result = self._process_silent_payments(sv) # TODO: should False raise an exception instead? + self.sp_processed = result + return result + + except FatalPSBTIssue: + raise + except Exception as e: + print("SP preview failed: %s" % e) + return False + + def process_silent_payments_for_signing(self, sv, dis): + """ + Reference notes for preview_silent_payment_outputs + + Notes: + - This function should skip share generation but checks for completeness before attempting to sign. + + Returns: + bool: True if coverage is complete + False if we generated shares but are waiting on others, or if we don't have necessary info to proceed + """ + dis.fullscreen("Silent Payment...") + + if not self.sp_processed: + self._process_silent_payments(sv) + + if self._is_ecdh_coverage_complete(): + dis.fullscreen("Computing Outputs...") + self._compute_silent_payment_output_scripts() + return True + + return False + + def render_silent_payment_output_string(self, output): + """ + Render a human-readable Silent Payment output string for displaying on screen + + Args: + output: Output object from self.outputs + + Returns: + str: Human-readable Silent Payment output string + """ + if not output.sp_v0_info: + raise ValueError("Output is not a silent payment output") + + return " - silent payment address -\n%s\n" % self.encode_silent_payment_address(output) + + # ----------------------------------------------------------------------------- # Input Helper Functions # ----------------------------------------------------------------------------- @@ -267,7 +358,7 @@ def _process_silent_payments(self, sv): Returns True if output scripts were computed (ready to sign). """ if not self.has_silent_payment_outputs(): - return + return False self._validate_psbt_structure() self._validate_input_eligibility() self._validate_ecdh_coverage() @@ -425,8 +516,6 @@ def _validate_ecdh_coverage(self): # Sum all eligible input pubkeys pubkeys = [] for inp in self.inputs: - if not self._is_input_eligible(inp): - continue pk = self._pubkey_from_input(inp) if pk: pubkeys.append(pk) @@ -536,7 +625,8 @@ def _compute_and_store_ecdh_shares(self, sv): elif inp.taproot_subpaths or inp.subpaths: has_foreign = True - # Detect foreign eligible inputs (different XFP, no sp_idxs) + # FIXME: is this the right approach? + # Detect foreign eligible inputs (different XFP, no sp_idxs) if not has_foreign: for inp in self.inputs: if inp.sp_idxs: @@ -563,9 +653,7 @@ def _compute_and_store_ecdh_shares(self, sv): combined_sk = (combined_sk + privkey_int) % SECP256K1_ORDER ecdh_share = _compute_ecdh_share(combined_sk, scan_key) - aux_rand = bytearray(32) - ckcc.rng_bytes(aux_rand) - dleq_proof = generate_dleq_proof(combined_sk, scan_key, bytes(aux_rand)) + dleq_proof = generate_dleq_proof(combined_sk, scan_key) if self.sp_global_ecdh_shares is None: self.sp_global_ecdh_shares = {} @@ -577,9 +665,7 @@ def _compute_and_store_ecdh_shares(self, sv): # Multi-signer: per-input ECDH shares and DLEQ proofs for owned inputs for inp, privkey_int in input_privkeys: ecdh_share = _compute_ecdh_share(privkey_int, scan_key) - aux_rand = bytearray(32) - ckcc.rng_bytes(aux_rand) - dleq_proof = generate_dleq_proof(privkey_int, scan_key, bytes(aux_rand)) + dleq_proof = generate_dleq_proof(privkey_int, scan_key) if inp.sp_ecdh_shares is None: inp.sp_ecdh_shares = {} @@ -689,10 +775,16 @@ def _compute_silent_payment_output_scripts(self): if not ecdh_share or not summed_pubkey: raise FatalPSBTIssue("Missing ECDH share for output #%d" % out_idx) - outp.script = _compute_silent_payment_output_script( + computed = _compute_silent_payment_output_script( outpoints, summed_pubkey, ecdh_share, B_spend, k ) + if outp.script: + existing = self.get(outp.script) if isinstance(outp.script, tuple) else outp.script + if existing != computed: + raise FatalPSBTIssue("SP output #%d: output script mismatch" % out_idx) + + outp.script = computed scan_key_k[scan_key] = k + 1 def _get_ecdh_and_pubkey(self, scan_key): @@ -714,23 +806,25 @@ def _get_ecdh_and_pubkey(self, scan_key): pubkeys.append(pk) if pubkeys: return ecdh_share, _combine_pubkeys(pubkeys) + return None, None - # Check per-input ECDH shares — combine via EC point addition - summed_ecdh = None + # Per-input shares: combine shares and pubkeys from eligible inputs + combined_ecdh = None pubkeys = [] for inp in self.inputs: if inp.sp_ecdh_shares and scan_key in inp.sp_ecdh_shares: - share = inp.sp_ecdh_shares[scan_key] - if summed_ecdh is None: - summed_ecdh = share + ecdh_share = inp.sp_ecdh_shares[scan_key] + if combined_ecdh is None: + combined_ecdh = ecdh_share else: - summed_ecdh = ngu.secp256k1.ec_pubkey_combine(summed_ecdh, share) - pk = self._pubkey_from_input(inp) - if pk: - pubkeys.append(pk) + combined_ecdh = ngu.secp256k1.ec_pubkey_combine(combined_ecdh, ecdh_share) + if self._is_input_eligible(inp): + pk = self._pubkey_from_input(inp) + if pk: + pubkeys.append(pk) - if summed_ecdh and pubkeys: - return summed_ecdh, _combine_pubkeys(pubkeys) + if combined_ecdh and pubkeys: + return combined_ecdh, _combine_pubkeys(pubkeys) return None, None diff --git a/testing/bip375_test_vectors.json b/testing/bip375_test_vectors.json new file mode 100644 index 00000000..7b9a6273 --- /dev/null +++ b/testing/bip375_test_vectors.json @@ -0,0 +1,1717 @@ +{ + "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+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgfCTD+UpVH5TLj1wOA8yaFwh/sU1xSl0vTp/Ux4nT02YBCgQBAAAAAA==", + "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": "psbt structure: empty 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+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEAAA=", + "supplementary": {} + }, + { + "description": "ecdh coverage: only one ineligible P2MS input when PSBT_OUT_SCRIPT set for sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiBPa2xI91PQstsuV3qY9lLMD4wBpxZ0Jhhhb5ag+ezmZwEPBAAAAAABASughgEAAAAAACIAIOr39e5jDu+omlGHtlFF4GmTiHzQv1oD8JZFgXkxw/hiAQVHUiEDaGidhEg6mw9BaLYu3+hU5d5PRyw42+0S4nN0susnfjohArPI8nEqzmYLPLqq/KShgFDY2qzp8YMkjzj0SWjwUgPjUq4BEAT+////AAEDCJBfAQAAAAAAAQQiUSDN452LBbSW+PGPILJ9CvmjIzMJ4EciGeCBP/O103h+KQEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "", + "public_key": "", + "prevout_txid": "4f6b6c48f753d0b2db2e577a98f652cc0f8c01a716742618616f96a0f9ece667", + "prevout_index": 0, + "prevout_scriptpubkey": "52210368689d84483a9b0f4168b62edfe854e5de4f472c38dbed12e27374b2eb277e3a2102b3c8f2712ace660b3cbaaafca4a18050d8daace9f183248f38f44968f05203e352ae", + "amount": 100000, + "witness_utxo": "a086010000000000220020eaf7f5ee630eefa89a5187b65145e06993887cd0bf5a03f09645817931c3f862", + "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 input single-signer", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+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 + } + ], + "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": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + } + ] + } + }, + { + "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/fFsorLVDpocHYA486Jgi7jICEAAQMIoIYBAAAAAAABBCJRIHwkw/lKVR+Uy49cDgPMmhcIf7FNcUpdL06f1MeJ09NmAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoAuJBKtaGGMwJH8G/tuRd4l0b3BZXsljCbRYdEtLosDafAQoEAQAAAAABAwhw8wUAAAAAAAEEFgAUOMGPNd2AY+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": "51207c24c3f94a551f94cb8f5c0e03cc9a17087fb14d714a5d2f4e9fd4c789d3d366" + }, + { + "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/U3bEJHv8Fw2bJjsOPSBrZoyH6Us03ur6AKF5KwR614h1uwABAwighgEAAAAAAAEEIlEgjkFVhZ37qH3uVCd4eSDGlYuRbXrHP02lCyzrTAnwYY8BCUICi3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWgD8+Tq+VWJ7fdfgshaz/nQvOSCnhaA85Xsfq1iaXfwhRMBCgQDAAAAAAEDCDDmAgAAAAAAAQQiUSDj5vwH7Pa/a1m0ubaqUxM11laKeL6AjUVOAg1+jH/aUwEJQgKLeQ4Jh/3p8NEjmOtr4wxzdnk8ZHhloqAvLvyWDZZlaALcqxqjkFTkguCjoiOo8Sgs1xGXZTHGeaoggF9/X6TxawEKBAEAAAAA", + "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": "51208e4155859dfba87dee5427787920c6958b916d7ac73f4da50b2ceb4c09f0618f" + }, + { + "output_index": 1, + "amount": 190000, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", + "sp_v0_label": 1, + "script": "5120e3e6fc07ecf6bf6b59b4b9b6aa531335d6568a78be808d454e020d7e8c7fda53" + } + ] + } + }, + { + "description": "can finalize: two mixed input types - only eligible inputs contribute ECDH 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///8AAQMIkF8BAAAAAAABBCJRIDJt9Q/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 + }, + { + "input_index": 1, + "private_key": "", + "public_key": "", + "prevout_txid": "d222262f268729bb7c84c558129a5d6986185cef535ec149c0df60dd4a92c204", + "prevout_index": 0, + "prevout_scriptpubkey": "a914f45f8cca268ec26e4bbe899170f138250d7cf55e87", + "amount": 150000, + "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 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": "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", + "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, + "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/testing/devtest/unit_silentpayments.py b/testing/devtest/unit_silentpayments.py new file mode 100644 index 00000000..20a9b373 --- /dev/null +++ b/testing/devtest/unit_silentpayments.py @@ -0,0 +1,559 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# Unit tests for BIP-352/BIP-375 Silent Payments implementation. +# Runs inside the simulator via sim_execfile('devtest/unit_silentpayments.py'). +# Success = no output; failure = assertion traceback. +# +import ngu +from ubinascii import hexlify as b2a_hex +from ubinascii import unhexlify as a2b_hex +from uhashlib import sha256 + +from dleq import generate_dleq_proof, verify_dleq_proof +from exceptions import FatalPSBTIssue +from silentpayments import ( + _compute_ecdh_share, + _compute_input_hash, + _combine_pubkeys, + _compute_shared_secret_tweak, + _compute_silent_payment_output_script, + _is_p2pkh, + _is_p2wpkh, + _is_p2tr, + _is_p2sh, + NUMS_H, + SilentPaymentsMixin, +) +from precomp_tag_hash import ( + BIP352_SHARED_SECRET_TAG_H, BIP352_INPUTS_TAG_H, BIP352_LABEL_TAG_H, + DLEQ_TAG_AUX_H, DLEQ_TAG_NONCE_H, DLEQ_TAG_CHALLENGE_H, +) +from public_constants import ( + PSBT_GLOBAL_SP_ECDH_SHARE, PSBT_GLOBAL_SP_DLEQ, + PSBT_IN_SP_ECDH_SHARE, PSBT_IN_SP_DLEQ, + PSBT_OUT_SP_V0_INFO, PSBT_OUT_SP_V0_LABEL, +) + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +TEST_PRIVKEY_INT = 0xa5377d45114b0206f6773e231861ece8c04e13840ab007df6722a3508211c073 +TEST_PRIVKEY2_INT = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +TEST_SCAN_KEY = a2b_hex('03af606abaa5e29a89b93bf971c21e46dd2797aee31273c47f979a102eb51c3629') +TEST_SPEND_KEY = a2b_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5') +MY_XFP = 0x12345678 +TEST_DERIV_OURS = (MY_XFP, 44, 0, 0, 0) +FOREIGN_XFP = 0xDEADBEEF +G = ngu.secp256k1.generator() +P2WPKH_SPK = b'\x00\x14' + b'\xab' * 20 + +# --------------------------------------------------------------------------- +# BIP-352 Crypto Primitives +# --------------------------------------------------------------------------- + +# ECDH share computation +ecdh_share = _compute_ecdh_share(TEST_PRIVKEY_INT, TEST_SCAN_KEY) +assert len(ecdh_share) == 33 +assert ecdh_share[0] in (0x02, 0x03) + +# Input hash computation (order-independent) +outpoints = [ + (a2b_hex('0000000000000000000000000000000000000000000000000000000000000001'), b'\x00\x00\x00\x00'), + (a2b_hex('0000000000000000000000000000000000000000000000000000000000000002'), b'\x00\x00\x00\x01'), +] +spk = a2b_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') +ih = _compute_input_hash(outpoints, spk) +assert isinstance(ih, bytes) and len(ih) == 32 +assert int.from_bytes(ih, 'big') > 0 +assert _compute_input_hash(outpoints[::-1], spk) == ih + +# Shared secret tweak (different k -> different tweak) +es = a2b_hex('03ccffacf309a1570d01449966bbc0f876d23ee929e88a68968e0a606e31efcc35') +t0 = _compute_shared_secret_tweak(es, 0) +t1 = _compute_shared_secret_tweak(es, 1) +t2 = _compute_shared_secret_tweak(es, 2) +assert t0 != t1 and t1 != t2 and t0 != t2 +assert isinstance(t0, bytes) + +# --------------------------------------------------------------------------- +# Tagged Hash Constants +# --------------------------------------------------------------------------- + +assert BIP352_SHARED_SECRET_TAG_H == sha256(b"BIP0352/SharedSecret").digest() +assert BIP352_INPUTS_TAG_H == sha256(b"BIP0352/Inputs").digest() +assert BIP352_LABEL_TAG_H == sha256(b"BIP0352/Label").digest() +assert DLEQ_TAG_AUX_H == sha256(b"BIP0374/aux").digest() +assert DLEQ_TAG_NONCE_H == sha256(b"BIP0374/nonce").digest() +assert DLEQ_TAG_CHALLENGE_H == sha256(b"BIP0374/challenge").digest() + +# --------------------------------------------------------------------------- +# PSBT Field Constants (BIP-375) +# --------------------------------------------------------------------------- + +assert PSBT_GLOBAL_SP_ECDH_SHARE == 0x07 +assert PSBT_GLOBAL_SP_DLEQ == 0x08 +assert PSBT_IN_SP_ECDH_SHARE == 0x1d +assert PSBT_IN_SP_DLEQ == 0x1e +assert PSBT_OUT_SP_V0_INFO == 0x09 +assert PSBT_OUT_SP_V0_LABEL == 0x0a + +# --------------------------------------------------------------------------- +# DLEQ Proofs +# --------------------------------------------------------------------------- + +pubkey = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY_INT.to_bytes(32, 'big')) + +proof = generate_dleq_proof(TEST_PRIVKEY_INT, TEST_SCAN_KEY) +assert len(proof) == 64 +assert verify_dleq_proof(pubkey, TEST_SCAN_KEY, ecdh_share, proof) + +# Tampered proof rejected +tampered = bytearray(proof) +tampered[0] ^= 0xFF +assert not verify_dleq_proof(pubkey, TEST_SCAN_KEY, ecdh_share, bytes(tampered)) + +# Wrong ECDH share rejected +wrong_ecdh = a2b_hex('02ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') +assert not verify_dleq_proof(pubkey, TEST_SCAN_KEY, wrong_ecdh, proof) + +# Deterministic with same aux_rand +p1 = generate_dleq_proof(TEST_PRIVKEY_INT, TEST_SCAN_KEY, b'\x00' * 32) +p2 = generate_dleq_proof(TEST_PRIVKEY_INT, TEST_SCAN_KEY, b'\x00' * 32) +assert p1 == p2 + +# --------------------------------------------------------------------------- +# Address Encoding +# --------------------------------------------------------------------------- + +sk = a2b_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') +spk2 = a2b_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5') + +addr = ngu.codecs.bip352_encode('sp', sk, spk2) +assert isinstance(addr, str) and addr.startswith('sp1q') and len(addr) == 116 + +addr_tn = ngu.codecs.bip352_encode('tsp', sk, spk2) +assert addr_tn.startswith('tsp1q') and len(addr_tn) == 117 + +assert ngu.codecs.bip352_encode('sp', sk, spk2, 0) == addr + +for v in [0, 1, 15, 30, 31]: + a = ngu.codecs.bip352_encode('sp', sk, spk2, v) + assert isinstance(a, str) and a.startswith('sp1') + +for v in [32, 33, 100, -1, -10]: + try: + ngu.codecs.bip352_encode('sp', sk, spk2, v) + assert False, "Should raise for version %d" % v + except ValueError as e: + assert 'version must be 0-31' in str(e) + +# Different keys -> different addresses +sk2 = a2b_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5') +assert ngu.codecs.bip352_encode('sp', sk2, spk2) != addr + +spk3 = a2b_hex('021b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec97b6eda4116f') +assert ngu.codecs.bip352_encode('sp', sk, spk3) != addr + +# Invalid key sizes +for bad_hex in ['79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', + '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ff', + '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16', + '']: + try: + bad_key = a2b_hex(bad_hex) if bad_hex else b'' + ngu.codecs.bip352_encode('sp', sk, bad_key) + assert False, "Should raise for bad key" + except ValueError as e: + assert '33 bytes' in str(e) + +# Uncompressed key rejected +uncompressed = a2b_hex( + '04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5' + '1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a') +try: + ngu.codecs.bip352_encode('sp', sk, uncompressed) + assert False, "Should raise for uncompressed key" +except ValueError as e: + assert '33 bytes' in str(e) + + +# --------------------------------------------------------------------------- +# Mock Infrastructure +# --------------------------------------------------------------------------- + +class MockInput: + def __init__(self): + self.sp_idxs = None + self.sp_ecdh_shares = None + self.sp_dleq_proofs = None + self.subpaths = None + self.taproot_subpaths = None + self.previous_txid = None + self.prevout_idx = None + self.witness_utxo = None + self.taproot_internal_key = None + self.utxo_spk = None + self.ik_idx = None + self.sighash = None + self.redeem_script = None + +class MockOutput: + def __init__(self): + self.sp_v0_info = None + self.sp_v0_label = None + self.script = None + +class MockPSBT(SilentPaymentsMixin): + def __init__(self): + self.inputs = [] + self.outputs = [] + self.my_xfp = MY_XFP + self.sp_global_ecdh_shares = {} + self.sp_global_dleq_proofs = {} + def get(self, x): + return x + def parse_xfp_path(self, coords): + return coords + def handle_zero_xfp(self, xfp_path, my_xfp, _): + return xfp_path + def _path_to_privkey(self, xfp_path, sv): + return sv[xfp_path] + + +def _make_eligible_input(pk, deriv, txid, vout_bytes): + inp = MockInput() + inp.utxo_spk = P2WPKH_SPK + inp.subpaths = [(pk, deriv)] + inp.previous_txid = txid + inp.prevout_idx = vout_bytes + inp.sp_idxs = [0] + return inp + + +def _make_test_keypair(): + privkey_bytes = TEST_PRIVKEY_INT.to_bytes(32, 'big') + pk = ngu.secp256k1.ec_pubkey_tweak_mul(G, privkey_bytes) + es = _compute_ecdh_share(TEST_PRIVKEY_INT, TEST_SCAN_KEY) + dp = generate_dleq_proof(TEST_PRIVKEY_INT, TEST_SCAN_KEY, b'\x00' * 32) + return TEST_PRIVKEY_INT, pk, TEST_SCAN_KEY, es, dp + + +def _make_mock_psbt_with_global_proofs(): + _, pk, scan_key, es, dp = _make_test_keypair() + psbt = MockPSBT() + inp = _make_eligible_input(pk, (MY_XFP, 44, 0, 0, 0), b'\x01' * 32, b'\x00\x00\x00\x00') + psbt.inputs = [inp] + outp = MockOutput() + outp.sp_v0_info = scan_key + TEST_SPEND_KEY + psbt.outputs = [outp] + psbt.sp_global_ecdh_shares = {scan_key: es} + psbt.sp_global_dleq_proofs = {scan_key: dp} + return psbt, scan_key, es, dp, pk, TEST_SPEND_KEY + + +def _make_sp_output(scan_key): + outp = MockOutput() + outp.sp_v0_info = scan_key + TEST_SPEND_KEY + return outp + +# --------------------------------------------------------------------------- +# Mixin: ECDH Coverage +# --------------------------------------------------------------------------- + +# Global coverage complete +psbt, _, _, _, _, _ = _make_mock_psbt_with_global_proofs() +assert psbt._is_ecdh_coverage_complete() is True + +# Global missing +psbt.sp_global_ecdh_shares = {} +assert psbt._is_ecdh_coverage_complete() is False + +# Per-input coverage +_, pk, scan_key, es, _ = _make_test_keypair() +psbt = MockPSBT() +psbt.sp_global_ecdh_shares = {} +inp = _make_eligible_input(pk, TEST_DERIV_OURS, b'\x01' * 32, b'\x00' * 4) +inp.sp_ecdh_shares = {scan_key: es} +psbt.inputs = [inp] +psbt.outputs = [_make_sp_output(scan_key)] +assert psbt._is_ecdh_coverage_complete() is True + +# Per-input missing +inp.sp_ecdh_shares = {} +assert psbt._is_ecdh_coverage_complete() is False + +# No SP outputs: vacuously true +psbt = MockPSBT() +psbt.sp_global_ecdh_shares = {} +psbt.inputs = [MockInput()] +assert psbt._is_ecdh_coverage_complete() is True + +# --------------------------------------------------------------------------- +# Mixin: DLEQ Validation +# --------------------------------------------------------------------------- + +# Global valid proof +psbt, *_ = _make_mock_psbt_with_global_proofs() +psbt._validate_ecdh_coverage() + +# Global tampered proof +psbt, scan_key, _, dp, _, _ = _make_mock_psbt_with_global_proofs() +tampered = bytearray(dp) +tampered[0] ^= 0xFF +psbt.sp_global_dleq_proofs = {scan_key: bytes(tampered)} +try: + psbt._validate_ecdh_coverage() + assert False, "Should raise FatalPSBTIssue" +except FatalPSBTIssue: + pass + +# Global missing proof +psbt, _, _, _, _, _ = _make_mock_psbt_with_global_proofs() +psbt.sp_global_dleq_proofs = {} +try: + psbt._validate_ecdh_coverage() + assert False, "Should raise FatalPSBTIssue" +except FatalPSBTIssue: + pass + +# Per-input valid proof +_, pk, scan_key, es, dp = _make_test_keypair() +psbt = MockPSBT() +psbt.sp_global_ecdh_shares = {} +inp = _make_eligible_input(pk, TEST_DERIV_OURS, b'\x01' * 32, b'\x00' * 4) +inp.sp_ecdh_shares = {scan_key: es} +inp.sp_dleq_proofs = {scan_key: dp} +psbt.inputs = [inp] +outp = MockOutput() +outp.sp_v0_info = scan_key + TEST_SPEND_KEY +outp.script = b'\x51\x20' + b'\xcc' * 32 +psbt.outputs = [outp] +psbt._validate_ecdh_coverage() + +# Per-input tampered proof +_, pk, scan_key, es, dp = _make_test_keypair() +psbt = MockPSBT() +psbt.sp_global_ecdh_shares = {} +tampered = bytearray(dp) +tampered[0] ^= 0xFF +inp = _make_eligible_input(pk, TEST_DERIV_OURS, b'\x01' * 32, b'\x00' * 4) +inp.sp_ecdh_shares = {scan_key: es} +inp.sp_dleq_proofs = {scan_key: bytes(tampered)} +psbt.inputs = [inp] +outp = MockOutput() +outp.sp_v0_info = scan_key + TEST_SPEND_KEY +outp.script = b'\x51\x20' + b'\xcc' * 32 +psbt.outputs = [outp] +try: + psbt._validate_ecdh_coverage() + assert False, "Should raise FatalPSBTIssue" +except FatalPSBTIssue: + pass + +# --------------------------------------------------------------------------- +# Mixin: Compute and Store ECDH +# --------------------------------------------------------------------------- + +pk_ours = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY_INT.to_bytes(32, 'big')) + +# Single signer -> global +psbt = MockPSBT() +psbt.sp_global_ecdh_shares = None +psbt.sp_global_dleq_proofs = None +inp = _make_eligible_input(pk_ours, TEST_DERIV_OURS, b'\x01' * 32, b'\x00' * 4) +psbt.inputs = [inp] +outp = MockOutput() +outp.sp_v0_info = TEST_SCAN_KEY + TEST_SPEND_KEY +psbt.outputs = [outp] +sv = {TEST_DERIV_OURS: TEST_PRIVKEY_INT} +assert psbt._compute_and_store_ecdh_shares(sv) is True +assert len(psbt.sp_global_ecdh_shares) == 1 +assert TEST_SCAN_KEY in psbt.sp_global_ecdh_shares +assert len(psbt.sp_global_dleq_proofs) == 1 + +# Idempotent +psbt._compute_and_store_ecdh_shares(sv) +assert len(psbt.sp_global_ecdh_shares) == 1 + +# Multi-signer -> per-input +psbt = MockPSBT() +inp_ours = _make_eligible_input(pk_ours, TEST_DERIV_OURS, b'\x01' * 32, b'\x00' * 4) +foreign_deriv = (FOREIGN_XFP, 44, 0, 0, 0) +inp_foreign = _make_eligible_input(b'\x02' + b'\xbb' * 32, foreign_deriv, b'\x02' * 32, b'\x01' * 4) +psbt.inputs = [inp_ours, inp_foreign] +outp = MockOutput() +outp.sp_v0_info = TEST_SCAN_KEY + TEST_SPEND_KEY +psbt.outputs = [outp] +assert psbt._compute_and_store_ecdh_shares(sv) is True +assert not psbt.sp_global_ecdh_shares +assert len(inp_ours.sp_ecdh_shares) == 1 +assert TEST_SCAN_KEY in inp_ours.sp_ecdh_shares + +# --------------------------------------------------------------------------- +# Mixin: Ownership +# --------------------------------------------------------------------------- + +# All owned +psbt = MockPSBT() +psbt.sp_global_ecdh_shares = None +psbt.sp_global_dleq_proofs = None +inp = _make_eligible_input(pk_ours, TEST_DERIV_OURS, b'\x01' * 32, b'\x00' * 4) +pub2 = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY2_INT.to_bytes(32, 'big')) +inp2 = _make_eligible_input(pub2, (MY_XFP, 44, 0, 0, 1), b'\x02' * 32, b'\x01\x00\x00\x00') +psbt.inputs = [inp, inp2] +outp = MockOutput() +outp.sp_v0_info = TEST_SCAN_KEY + TEST_SPEND_KEY +psbt.outputs = [outp] +psbt._compute_and_store_ecdh_shares({ + TEST_DERIV_OURS: TEST_PRIVKEY_INT, + (MY_XFP, 44, 0, 0, 1): TEST_PRIVKEY2_INT, +}) +assert psbt.sp_all_inputs_ours is True + +# Foreign input +psbt = MockPSBT() +inp_ours = _make_eligible_input(pk_ours, TEST_DERIV_OURS, b'\x01' * 32, b'\x00' * 4) +inp_foreign = _make_eligible_input(b'\x02' + b'\xbb' * 32, (FOREIGN_XFP, 44, 0, 0, 0), b'\x02' * 32, b'\x01' * 4) +psbt.inputs = [inp_ours, inp_foreign] +outp = MockOutput() +outp.sp_v0_info = TEST_SCAN_KEY + TEST_SPEND_KEY +psbt.outputs = [outp] +psbt._compute_and_store_ecdh_shares({TEST_DERIV_OURS: TEST_PRIVKEY_INT}) +assert psbt.sp_all_inputs_ours is False + +# No signable inputs +psbt = MockPSBT() +inp = _make_eligible_input(b'\x02' + b'\xbb' * 32, (FOREIGN_XFP, 44, 0, 0, 0), b'\x01' * 32, b'\x00' * 4) +psbt.inputs = [inp] +outp = MockOutput() +outp.sp_v0_info = TEST_SCAN_KEY + TEST_SPEND_KEY +psbt.outputs = [outp] +assert psbt._compute_and_store_ecdh_shares({}) is False + +# --------------------------------------------------------------------------- +# Mixin: Compute Output Scripts +# --------------------------------------------------------------------------- + +# Single input, single output +psbt, scan_key, es, _, pk, spend_key = _make_mock_psbt_with_global_proofs() +psbt._compute_silent_payment_output_scripts() +outp = psbt.outputs[0] +assert outp.script is not None and len(outp.script) == 34 and outp.script[0] == 0x51 +expected = _compute_silent_payment_output_script( + [(b'\x01' * 32, b'\x00\x00\x00\x00')], pk, es, spend_key, 0) +assert outp.script == expected + +# Missing ECDH share +psbt, _, _, _, _, _ = _make_mock_psbt_with_global_proofs() +other_scan = a2b_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') +psbt.outputs[0].sp_v0_info = other_scan + TEST_SPEND_KEY +try: + psbt._compute_silent_payment_output_scripts() + assert False, "Should raise" +except Exception as e: + assert 'Missing ECDH share' in str(e) + +# Per-input share combining +pub1 = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY_INT.to_bytes(32, 'big')) +pub2 = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY2_INT.to_bytes(32, 'big')) +share1 = _compute_ecdh_share(TEST_PRIVKEY_INT, TEST_SCAN_KEY) +share2 = _compute_ecdh_share(TEST_PRIVKEY2_INT, TEST_SCAN_KEY) + +psbt = MockPSBT() +psbt.sp_global_ecdh_shares = {} +inp1 = _make_eligible_input(pub1, (MY_XFP, 44, 0, 0, 0), b'\x01' * 32, b'\x00\x00\x00\x00') +inp1.sp_ecdh_shares = {TEST_SCAN_KEY: share1} +inp2 = _make_eligible_input(pub2, (MY_XFP, 44, 0, 0, 1), b'\x02' * 32, b'\x01\x00\x00\x00') +inp2.sp_ecdh_shares = {TEST_SCAN_KEY: share2} +psbt.inputs = [inp1, inp2] +outp = MockOutput() +outp.sp_v0_info = TEST_SCAN_KEY + TEST_SPEND_KEY +psbt.outputs = [outp] +psbt._compute_silent_payment_output_scripts() + +combined_share = ngu.secp256k1.ec_pubkey_combine(share1, share2) +summed_pk = _combine_pubkeys([pub1, pub2]) +outpoints = [(b'\x01' * 32, b'\x00\x00\x00\x00'), (b'\x02' * 32, b'\x01\x00\x00\x00')] +expected = _compute_silent_payment_output_script(outpoints, summed_pk, combined_share, TEST_SPEND_KEY) +assert outp.script == expected + +# k counter per scan key (skips non-SP outputs) +psbt, scan_key, es, _, pk, _ = _make_mock_psbt_with_global_proofs() +spend_key_a = a2b_hex('022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4') +spend_key_b = a2b_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5') +spend_key_c = a2b_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') + +sp0 = MockOutput(); sp0.sp_v0_info = scan_key + spend_key_a +reg1 = MockOutput() +sp2 = MockOutput(); sp2.sp_v0_info = scan_key + spend_key_b +reg3 = MockOutput() +sp4 = MockOutput(); sp4.sp_v0_info = scan_key + spend_key_c +psbt.outputs = [sp0, reg1, sp2, reg3, sp4] +psbt._compute_silent_payment_output_scripts() + +ops = [(b'\x01' * 32, b'\x00\x00\x00\x00')] +spk_sum = _combine_pubkeys([pk]) +assert sp0.script == _compute_silent_payment_output_script(ops, spk_sum, es, spend_key_a, k=0) +assert sp2.script == _compute_silent_payment_output_script(ops, spk_sum, es, spend_key_b, k=1) +assert sp4.script == _compute_silent_payment_output_script(ops, spk_sum, es, spend_key_c, k=2) +assert reg1.script is None +assert reg3.script is None + +# --------------------------------------------------------------------------- +# Mixin: Simple Methods +# --------------------------------------------------------------------------- + +psbt = MockPSBT() +outp = MockOutput() +outp.sp_v0_info = b'\x02' + b'\xaa' * 32 + b'\x02' + b'\xbb' * 32 +psbt.outputs = [outp] +assert psbt.has_silent_payment_outputs() is True + +psbt = MockPSBT() +psbt.outputs = [MockOutput()] +assert psbt.has_silent_payment_outputs() is False + +# _get_silent_payment_scan_keys +psbt = MockPSBT() +scan1 = b'\x02' + b'\xaa' * 32 +scan2 = b'\x03' + b'\xbb' * 32 +o1 = MockOutput(); o1.sp_v0_info = scan1 + b'\x02' + b'\xcc' * 32 +o2 = MockOutput(); o2.sp_v0_info = scan2 + b'\x02' + b'\xdd' * 32 +o3 = MockOutput() +psbt.outputs = [o1, o2, o3] +keys = psbt._get_silent_payment_scan_keys() +assert len(keys) == 2 and set(keys) == {scan1, scan2} + +# Dedup +psbt = MockPSBT() +o1 = MockOutput(); o1.sp_v0_info = scan1 + b'\x02' + b'\xbb' * 32 +o2 = MockOutput(); o2.sp_v0_info = scan1 + b'\x02' + b'\xcc' * 32 +psbt.outputs = [o1, o2] +assert len(psbt._get_silent_payment_scan_keys()) == 1 + +# --------------------------------------------------------------------------- +# Script type predicates +# --------------------------------------------------------------------------- + +assert _is_p2wpkh(b'\x00\x14' + b'\xab' * 20) is True +assert _is_p2wpkh(b'\x00\x14' + b'\xab' * 19) is False # wrong length +assert _is_p2tr(b'\x51\x20' + b'\xcd' * 32) is True +assert _is_p2tr(b'\x51\x20' + b'\xcd' * 31) is False # wrong length +assert _is_p2pkh(b'\x76\xa9\x14' + b'\xab' * 20 + b'\x88\xac') is True +assert _is_p2pkh(b'\x76\xa9\x14' + b'\xab' * 20 + b'\x88\xad') is False # wrong suffix +assert _is_p2sh(b'\xa9\x14' + b'\xab' * 20 + b'\x87') is True +assert _is_p2sh(b'\xa9\x14' + b'\xab' * 19 + b'\x87') is False # wrong length + +# P2TR with NUMS output key is still valid P2TR shape (ineligibility is checked separately) +assert _is_p2tr(b'\x51\x20' + NUMS_H) is True + +# --------------------------------------------------------------------------- +# _combine_pubkeys edge case: empty list raises ValueError +# --------------------------------------------------------------------------- + +try: + _combine_pubkeys([]) + assert False, "Should raise ValueError" +except ValueError: + pass diff --git a/testing/devtest/verify_sp_outputs.py b/testing/devtest/verify_sp_outputs.py new file mode 100644 index 00000000..411f703d --- /dev/null +++ b/testing/devtest/verify_sp_outputs.py @@ -0,0 +1,227 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# Crypto verification for silent payment integration tests. +# Runs inside the simulator via sim_execfile('devtest/verify_sp_outputs.py'). +# +# Interface: main.SP_VERIFY dict with hex-encoded parameters. +# Success = no output; failure = assertion traceback. +# +import ckcc +import main +import ngu +import struct +import ujson +from ubinascii import hexlify as b2a_hex +from ubinascii import unhexlify as a2b_hex +from dleq import generate_dleq_proof, verify_dleq_proof +from exceptions import FatalPSBTIssue +from silentpayments import ( + _combine_pubkeys, + _compute_ecdh_share, + _compute_silent_payment_output_script, + SilentPaymentsMixin, +) + +if isinstance(main.SP_VERIFY, str): + with open(main.SP_VERIFY) as f: + params = ujson.load(f) +else: + params = main.SP_VERIFY + +mode = params.get('mode') + + +# --------------------------------------------------------------------------- +# MockPSBT infrastructure for firmware method testing +# --------------------------------------------------------------------------- + +class MockInput: + def __init__(self): + self.utxo_spk = None + self.taproot_internal_key = None + self.redeem_script = None + self.sighash = None + self.previous_txid = None + self.prevout_idx = None + self.sp_ecdh_shares = {} + self.sp_dleq_proofs = {} + self.subpaths = [] + self.taproot_subpaths = [] + self.sp_idxs = None + self.ik_idx = None + + +class MockOutput: + def __init__(self): + self.sp_v0_info = None + self.sp_v0_label = None + self.script = None + + +class MockPSBT(SilentPaymentsMixin): + def __init__(self): + self.inputs = [] + self.outputs = [] + self.sp_global_ecdh_shares = {} + self.sp_global_dleq_proofs = {} + self.txn_modifiable = None + self.my_xfp = 0 + + def get(self, x): + return x + + def parse_xfp_path(self, coords): + return coords + + +def _build_mock_psbt(params): + psbt = MockPSBT() + psbt.txn_modifiable = params.get('txn_modifiable') + + global_ecdh = params.get('global_ecdh') or {} + global_dleq = params.get('global_dleq') or {} + psbt.sp_global_ecdh_shares = {a2b_hex(k): a2b_hex(v) for k, v in global_ecdh.items()} + psbt.sp_global_dleq_proofs = {a2b_hex(k): a2b_hex(v) for k, v in global_dleq.items()} + + for d in params.get('inputs', []): + inp = MockInput() + if d.get('witness_utxo'): + wu = a2b_hex(d['witness_utxo']) + # Extract scriptPubKey: 8 bytes value + varint length + script + script_len = wu[8] + inp.utxo_spk = wu[9:9 + script_len] + if d.get('taproot_internal_key'): + inp.taproot_internal_key = a2b_hex(d['taproot_internal_key']) + if d.get('redeem_script'): + inp.redeem_script = a2b_hex(d['redeem_script']) + inp.sighash = d.get('sighash') + if d.get('previous_txid'): + inp.previous_txid = a2b_hex(d['previous_txid']) + if d.get('prevout_idx') is not None: + inp.prevout_idx = struct.pack(' 1 else pubkeys[0] + RV.write(b2a_hex(result).decode()) + +elif mode == 'aggregate_ecdh_share': + # Combine per-input ECDH shares via EC point addition + shares = [] + for h in params['shares']: + shares.append(a2b_hex(h)) + result = shares[0] + for s in shares[1:]: + result = ngu.secp256k1.ec_pubkey_combine(result, s) + RV.write(b2a_hex(result).decode()) + +elif mode == 'validate_sp': + # Run firmware validation methods; write error string to RV on failure + psbt = _build_mock_psbt(params) + try: + psbt._validate_psbt_structure() + psbt._validate_input_eligibility() + psbt._validate_ecdh_coverage() + psbt._compute_silent_payment_output_scripts() + except FatalPSBTIssue as e: + RV.write(str(e)) + +elif mode == 'get_ecdh_and_pubkey': + # Call _get_ecdh_and_pubkey and return "ecdh_hex,pubkey_hex" + psbt = _build_mock_psbt(params) + scan_key = a2b_hex(params['scan_key']) + ecdh_share, summed_pubkey = psbt._get_ecdh_and_pubkey(scan_key) + ecdh_hex = b2a_hex(ecdh_share).decode() if ecdh_share else '' + pubkey_hex = b2a_hex(summed_pubkey).decode() if summed_pubkey else '' + RV.write(ecdh_hex + ',' + pubkey_hex) + +elif mode == 'get_outpoints': + # Call _get_outpoints and return "txid:vout,txid:vout,..." + psbt = _build_mock_psbt(params) + outpoints = psbt._get_outpoints() + parts = [] + for txid, vout in outpoints: + parts.append(b2a_hex(txid).decode() + ':' + b2a_hex(vout).decode()) + RV.write(','.join(parts)) + +elif mode == 'get_combined_privkey': + # Sum private keys for eligible inputs; return as 64-char hex + SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + psbt = _build_mock_psbt(params) + combined = 0 + for pk_data in params.get('private_keys', []): + i = pk_data['input_index'] + inp = psbt.inputs[i] + if psbt._is_input_eligible(inp): + combined = (combined + int(pk_data['key'], 16)) % SECP256K1_ORDER + RV.write('%064x' % combined) + +elif mode == 'get_input_pubkey': + # Call _pubkey_from_input for a specific input; return pubkey hex or "" + psbt = _build_mock_psbt(params) + i = params['input_index'] + pk = psbt._pubkey_from_input(psbt.inputs[i]) + RV.write(b2a_hex(pk).decode() if pk else '') diff --git a/testing/psbt.py b/testing/psbt.py index b553d15f..26e85ebe 100644 --- a/testing/psbt.py +++ b/testing/psbt.py @@ -25,6 +25,9 @@ PSBT_GLOBAL_INPUT_COUNT = 0x04 PSBT_GLOBAL_OUTPUT_COUNT = 0x05 PSBT_GLOBAL_TX_MODIFIABLE = 0x06 +# BIP-375 Silent Payments +PSBT_GLOBAL_SP_ECDH_SHARE = 0x07 +PSBT_GLOBAL_SP_DLEQ = 0x08 # INPUTS === PSBT_IN_NON_WITNESS_UTXO = 0x00 @@ -58,6 +61,9 @@ PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a PSBT_IN_MUSIG2_PUB_NONCE = 0x1b PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c +# BIP-375 Silent Payments +PSBT_IN_SP_ECDH_SHARE = 0x1d +PSBT_IN_SP_DLEQ = 0x1e # OUTPUTS === PSBT_OUT_REDEEM_SCRIPT = 0x00 @@ -71,6 +77,9 @@ PSBT_OUT_TAP_TREE = 0x06 PSBT_OUT_TAP_BIP32_DERIVATION = 0x07 PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08 +# BIP-375 Silent Payments +PSBT_OUT_SP_V0_INFO = 0x09 +PSBT_OUT_SP_V0_LABEL = 0x0a PSBT_PROP_CK_ID = b"COINKITE" @@ -141,6 +150,8 @@ def defaults(self): self.musig_pubkeys = {} self.musig_pubnonces = {} self.musig_part_sigs = {} + self.sp_ecdh_shares = {} + self.sp_dleq_proofs = {} self.others = {} self.unknown = {} @@ -171,6 +182,8 @@ def __eq__(a, b): a.musig_pubkeys == b.musig_pubkeys and \ a.musig_pubnonces == b.musig_pubnonces and \ a.musig_part_sigs == b.musig_part_sigs and \ + a.sp_ecdh_shares == b.sp_ecdh_shares and \ + a.sp_dleq_proofs == b.sp_dleq_proofs and \ a.unknown == b.unknown if rv: # NOTE: equality test on signatures requires parsing DER stupidness @@ -260,6 +273,10 @@ def parse_kv(self, kt, key, val): aggregate_key = key[33:66] tapleaf_h = key[66:] self.musig_part_sigs[(participant_key, aggregate_key, tapleaf_h)] = val + elif kt == PSBT_IN_SP_ECDH_SHARE: + self.sp_ecdh_shares[key] = val + elif kt == PSBT_IN_SP_DLEQ: + self.sp_dleq_proofs[key] = val else: self.unknown[bytes([kt]) + key] = val @@ -318,6 +335,12 @@ def serialize_kvs(self, wr, v2): if self.musig_pubkeys: for agg_k, pk_lst in self.musig_pubkeys.items(): wr(PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS, b"".join(pk_lst), agg_k) + if self.sp_ecdh_shares: + for k, v in self.sp_ecdh_shares.items(): + wr(PSBT_IN_SP_ECDH_SHARE, v, k) + if self.sp_dleq_proofs: + for k, v in self.sp_dleq_proofs.items(): + wr(PSBT_IN_SP_DLEQ, v, k) if self.musig_pubnonces: for (pk, ak, lh), pubnonce in self.musig_pubnonces.items(): @@ -349,6 +372,8 @@ def defaults(self): self.taproot_tree = None self.script = None # v2 self.amount = None # v2 + self.sp_v0_info = None + self.sp_v0_label = None self.proprietary = {} self.musig_pubkeys = {} self.unknown = {} @@ -365,6 +390,8 @@ def __eq__(a, b): a.proprietary == b.proprietary and \ a.taproot_tree == b.taproot_tree and \ a.musig_pubkeys == b.musig_pubkeys and \ + a.sp_v0_info == b.sp_v0_info and \ + a.sp_v0_label == b.sp_v0_label and \ a.unknown == b.unknown def parse_kv(self, kt, key, val): @@ -404,6 +431,10 @@ def parse_kv(self, kt, key, val): for i in range(0, len(val), 33): pk_list.append(val[i:i + 33]) self.musig_pubkeys[key] = pk_list + elif kt == PSBT_OUT_SP_V0_INFO: + self.sp_v0_info = val + elif kt == PSBT_OUT_SP_V0_LABEL: + self.sp_v0_label = val elif kt == PSBT_GLOBAL_PROPRIETARY: self.proprietary[key] = val else: @@ -435,6 +466,10 @@ def serialize_kvs(self, wr, v2): if self.musig_pubkeys: for agg_k, pk_lst in self.musig_pubkeys.items(): wr(PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS, b"".join(pk_lst), agg_k) + if self.sp_v0_info is not None: + wr(PSBT_OUT_SP_V0_INFO, self.sp_v0_info) + if self.sp_v0_label is not None: + wr(PSBT_OUT_SP_V0_LABEL, self.sp_v0_label) for k in self.proprietary: wr(PSBT_GLOBAL_PROPRIETARY, self.proprietary[k], k) @@ -463,6 +498,8 @@ def __init__(self): self.outputs = [] self.txn_modifiable = None self.fallback_locktime = None + self.sp_global_ecdh_shares = {} + self.sp_global_dleq_proofs = {} self.unknown = {} self.parsed_txn = None @@ -478,6 +515,8 @@ def __eq__(a, b): all(a.inputs[i] == b.inputs[i] for i in range(len(a.inputs))) and \ all(a.outputs[i] == b.outputs[i] for i in range(len(a.outputs))) and \ sorted(a.xpubs) == sorted(b.xpubs) and \ + a.sp_global_ecdh_shares == b.sp_global_ecdh_shares and \ + a.sp_global_dleq_proofs == b.sp_global_dleq_proofs and \ a.unknown == b.unknown def is_v2(self): @@ -530,6 +569,10 @@ def parse(self, raw): num_outs = self.output_count elif kt == PSBT_GLOBAL_TX_MODIFIABLE: self.txn_modifiable = val[0] + elif kt == PSBT_GLOBAL_SP_ECDH_SHARE: + self.sp_global_ecdh_shares[key[1:]] = val + elif kt == PSBT_GLOBAL_SP_DLEQ: + self.sp_global_dleq_proofs[key[1:]] = val else: self.unknown[key] = val @@ -588,6 +631,13 @@ def wr(ktype, val, key=b''): if self.txn_modifiable is not None: wr(PSBT_GLOBAL_TX_MODIFIABLE, bytes([self.txn_modifiable])) + if self.sp_global_ecdh_shares: + for k, v in self.sp_global_ecdh_shares.items(): + wr(PSBT_GLOBAL_SP_ECDH_SHARE, v, k) + if self.sp_global_dleq_proofs: + for k, v in self.sp_global_dleq_proofs.items(): + wr(PSBT_GLOBAL_SP_DLEQ, v, k) + if self.version is not None: wr(PSBT_GLOBAL_VERSION, struct.pack(' 0 + + for scan_key, proof in p.sp_global_dleq_proofs.items(): + ecdh_share = p.sp_global_ecdh_shares.get(scan_key) + assert ecdh_share is not None, \ + "valid[%d]: global ECDH share missing for scan key" % vi + _, summed_pubkey = _sim_get_ecdh_and_pubkey(sim_exec, sim_execfile, p, scan_key) + _sim_verify_dleq(sim_exec, sim_execfile, + summed_pubkey, scan_key, ecdh_share, proof, expected=True) + + + for inp_idx, inp in enumerate(p.inputs): + if not inp.sp_dleq_proofs: + continue + pubkey = _sim_get_input_pubkey(sim_exec, sim_execfile, p, inp_idx) + if pubkey is None: + continue + for scan_key, proof in inp.sp_dleq_proofs.items(): + ecdh_share = inp.sp_ecdh_shares.get(scan_key) + assert ecdh_share is not None, \ + "valid[%d] inp[%d]: ECDH share missing" % (vi, inp_idx) + _sim_verify_dleq(sim_exec, sim_execfile, + pubkey, scan_key, ecdh_share, proof, expected=True) From 0a40e1ec942fef8031db2f819a56288c18cf0918 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:50:51 -0500 Subject: [PATCH 09/13] BIP-375: Implement Silent Payment PSBT Signing integrate preview silent payment address as output in auth workflow integrate silent payments in signing workflow add sp_hrp property to chains add silent payments fields to psbt --- shared/auth.py | 13 ++++++ shared/chains.py | 3 ++ shared/psbt.py | 104 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index f28259bd..4c1cf7c8 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -430,6 +430,11 @@ async def interact(self): ccc_c_xfp = CCCFeature.get_xfp() # can be None args = self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp) + + # Silent Payments: Validate and pre-process Silent Payment outputs + if self.psbt.has_silent_payment_outputs(): + self.psbt.preview_silent_payment_outputs() + self.psbt.consider_outputs(*args, cosign_xfp=ccc_c_xfp) del args # not needed anymore # we can properly assess sighash only after we know @@ -700,6 +705,8 @@ def output_summary_text(self, msg): total_change = 0 has_change = False + # TODO: Test pay to silent payment change address + for idx, tx_out in self.psbt.output_iter(): outp = self.psbt.outputs[idx] if outp.is_change: @@ -714,6 +721,9 @@ def output_summary_text(self, msg): else: if len(largest_outs) < MAX_VISIBLE_OUTPUTS: rendered, _ = self.render_output(tx_out) + # render silent payment address + if outp.sp_v0_info: + rendered += self.psbt.render_silent_payment_output_string(outp) largest_outs.append((tx_out.nValue, rendered)) if len(largest_outs) == MAX_VISIBLE_OUTPUTS: # descending sort from the biggest value to lowest (sort on out.nValue) @@ -1733,6 +1743,9 @@ def yield_item(self, offset, end, qr_items, change_idxs): outp = self.user_auth_action.psbt.outputs[idx] item = "Output %d%s:\n\n" % (idx, " (change)" if outp.is_change else "") msg, addr_or_script = self.user_auth_action.render_output(out) + # render silent payment address + if outp.sp_v0_info: + msg += self.user_auth_action.psbt.render_silent_payment_output_string(outp) item += msg qr_items.append(addr_or_script) if outp.is_change: diff --git a/shared/chains.py b/shared/chains.py index afed090b..87dfce96 100644 --- a/shared/chains.py +++ b/shared/chains.py @@ -58,6 +58,7 @@ class ChainsBase: curve = 'secp256k1' menu_name = None # use 'name' if this isn't defined ccc_min_block = 0 + sp_hrp = 'sp' # BIP-352 Silent Payment address HRP (default mainnet) # b44_cointype comes from # @@ -341,6 +342,7 @@ class BitcoinTestnet(ChainsBase): } bech32_hrp = 'tb' + sp_hrp = 'tsp' # BIP-352 Silent Payment testnet HRP b58_addr = bytes([111]) b58_script = bytes([196]) @@ -353,6 +355,7 @@ class BitcoinRegtest(BitcoinTestnet): ctype = 'XRT' name = 'Bitcoin Regtest' bech32_hrp = 'bcrt' + sp_hrp = 'tsp' # BIP-352 Silent Payment regtest HRP def get_chain(short_name): diff --git a/shared/psbt.py b/shared/psbt.py index 1273a8c8..8d295991 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -41,9 +41,13 @@ PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, MAX_SIGNERS, PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS, PSBT_IN_MUSIG2_PUB_NONCE, PSBT_IN_MUSIG2_PARTIAL_SIG, PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS, + PSBT_OUT_SP_V0_INFO, PSBT_OUT_SP_V0_LABEL, + PSBT_IN_SP_DLEQ, PSBT_IN_SP_ECDH_SHARE, + PSBT_GLOBAL_SP_DLEQ, PSBT_GLOBAL_SP_ECDH_SHARE, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AFC_SEGWIT, AF_BARE_PK ) +from silentpayments import SilentPaymentsMixin psbt_tmp256 = bytearray(256) @@ -402,12 +406,15 @@ def parse_subpaths(self, my_xfp, parent, cosign_xfp=None): # Track details of each output of PSBT # class psbtOutputProxy(psbtProxy): - no_keys = { PSBT_OUT_REDEEM_SCRIPT, PSBT_OUT_WITNESS_SCRIPT, PSBT_OUT_TAP_INTERNAL_KEY, PSBT_OUT_TAP_TREE } + no_keys = { PSBT_OUT_REDEEM_SCRIPT, PSBT_OUT_WITNESS_SCRIPT, PSBT_OUT_TAP_INTERNAL_KEY, PSBT_OUT_TAP_TREE, + PSBT_OUT_SP_V0_INFO, PSBT_OUT_SP_V0_LABEL } + short_values = { PSBT_OUT_SP_V0_INFO, PSBT_OUT_SP_V0_LABEL } blank_flds = ('unknown', 'subpaths', 'redeem_script', 'witness_script', 'sp_idxs', 'is_change', 'amount', 'script', 'attestation', 'proprietary', 'taproot_internal_key', 'taproot_subpaths', 'taproot_tree', 'ik_idx', - 'musig_pubkeys') + 'musig_pubkeys', 'sp_v0_info', 'sp_v0_label', + ) def __init__(self, fd, idx): super().__init__() @@ -423,6 +430,8 @@ def __init__(self, fd, idx): #self.script = None #self.amount = None #self.musig_pubkeys = None + #self.sp_v0_info = None + #self.sp_v0_label = None # this flag is set when we are assuming output will be change (same wallet) #self.is_change = False @@ -473,6 +482,14 @@ def store(self, kt, key, val): elif kt == PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS: self.musig_pubkeys = self.musig_pubkeys or [] self.musig_pubkeys.append((key, val)) + elif kt == PSBT_OUT_SP_V0_INFO: + # Silent payment address (scan_key + spend_key) + # val contains 66 bytes: scan_key (33) + spend_key (33) + self.sp_v0_info = val + elif kt == PSBT_OUT_SP_V0_LABEL: + # Optional label for silent payment output + # val contains 4-byte little-endian integer + self.sp_v0_label = val else: self.unknown = self.unknown or [] pos, length = key @@ -511,6 +528,13 @@ def serialize(self, out_fd, is_v2): wr(PSBT_OUT_SCRIPT, self.script) wr(PSBT_OUT_AMOUNT, self.amount) + # Silent Payment fields + if self.sp_v0_info: + wr(PSBT_OUT_SP_V0_INFO, self.sp_v0_info) + + if self.sp_v0_label: + wr(PSBT_OUT_SP_V0_LABEL, self.sp_v0_label) + if self.proprietary: for k, v in self.proprietary: wr(PSBT_PROPRIETARY, v, k) @@ -519,6 +543,7 @@ def serialize(self, out_fd, is_v2): for k, v in self.unknown: wr(None, v, k) + def determine_my_change(self, out_idx, txo, parsed_subpaths, parent): # Do things make sense for this output? @@ -644,7 +669,7 @@ def fraud(idx, af, err=""): class psbtInputProxy(psbtProxy): # just need to store a simple number for these - short_values = { PSBT_IN_SIGHASH_TYPE } + short_values = { PSBT_IN_SIGHASH_TYPE, PSBT_IN_SP_ECDH_SHARE, PSBT_IN_SP_DLEQ } # only part-sigs have a key to be stored. no_keys = {PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, PSBT_IN_SIGHASH_TYPE, @@ -660,7 +685,8 @@ class psbtInputProxy(psbtProxy): 'taproot_merkle_root', 'taproot_script_sigs', 'taproot_scripts', 'taproot_subpaths', 'taproot_internal_key', 'taproot_key_sig', 'tr_added_sigs', 'ik_idx', 'musig_pubkeys', 'musig_pubnonces', 'musig_part_sigs', 'musig_agg_idx', - 'musig_added_pubnonces', 'musig_added_sigs' + 'musig_added_pubnonces', 'musig_added_sigs', + 'sp_ecdh_shares', 'sp_dleq_proofs', ) def __init__(self, fd, idx): @@ -1165,6 +1191,16 @@ def store(self, kt, key, val): elif kt == PSBT_IN_MUSIG2_PARTIAL_SIG: self.musig_part_sigs = self.musig_part_sigs or [] self.musig_part_sigs.append((key, val)) + elif kt == PSBT_IN_SP_ECDH_SHARE: + # Silent Payments: Per-input ECDH share - uses short_values - key and val as bytes + if self.sp_ecdh_shares is None: + self.sp_ecdh_shares = {} + self.sp_ecdh_shares[key] = val + elif kt == PSBT_IN_SP_DLEQ: + # Silent Payments: Per-input DLEQ proof - uses short_values - key and val as bytes + if self.sp_dleq_proofs is None: + self.sp_dleq_proofs = {} + self.sp_dleq_proofs[key] = val else: # including: PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS self.unknown = self.unknown or [] @@ -1261,14 +1297,23 @@ def serialize(self, out_fd, is_v2): if self.req_height_locktime is not None: wr(PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, pack(" Date: Tue, 23 Dec 2025 15:34:24 -0500 Subject: [PATCH 10/13] BIP-376: Handle spending silent payment outputs refactor _derive_input_privkey use sp_tweak when spending silent payment output --- shared/psbt.py | 68 ++++++++++++------ shared/silentpayments.py | 96 ++++++++++++++++++++++---- testing/devtest/unit_silentpayments.py | 17 +++++ testing/devtest/verify_sp_outputs.py | 2 + testing/psbt.py | 16 +++++ 5 files changed, 163 insertions(+), 36 deletions(-) diff --git a/shared/psbt.py b/shared/psbt.py index 8d295991..5446ef6f 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -44,10 +44,11 @@ PSBT_OUT_SP_V0_INFO, PSBT_OUT_SP_V0_LABEL, PSBT_IN_SP_DLEQ, PSBT_IN_SP_ECDH_SHARE, PSBT_GLOBAL_SP_DLEQ, PSBT_GLOBAL_SP_ECDH_SHARE, + PSBT_IN_SP_TWEAK, PSBT_IN_SP_SPEND_BIP32_DERIVATION, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AFC_SEGWIT, AF_BARE_PK ) -from silentpayments import SilentPaymentsMixin +from silentpayments import SilentPaymentsMixin, _compute_silent_payment_spending_privkey psbt_tmp256 = bytearray(256) @@ -686,7 +687,7 @@ class psbtInputProxy(psbtProxy): 'taproot_subpaths', 'taproot_internal_key', 'taproot_key_sig', 'tr_added_sigs', 'ik_idx', 'musig_pubkeys', 'musig_pubnonces', 'musig_part_sigs', 'musig_agg_idx', 'musig_added_pubnonces', 'musig_added_sigs', - 'sp_ecdh_shares', 'sp_dleq_proofs', + 'sp_ecdh_shares', 'sp_dleq_proofs', 'sp_tweak', 'sp_spend_bip32_derivation', ) def __init__(self, fd, idx): @@ -1030,7 +1031,9 @@ def determine_my_signing_key(self, my_idx, addr_or_pubkey, my_xfp, psbt, parsed_ lhs, path = lhs_path[0], lhs_path[1:] assert not lhs, "LeafHashes have to be empty for internal key" assert self.sp_idxs[0] == 0 - assert taptweak(xonly_pubkey) == addr_or_pubkey + # Silent Payments (BIP-376): sp_tweak replaces taptweak + if not self.sp_tweak: + assert taptweak(xonly_pubkey) == addr_or_pubkey else: self.is_miniscript = True @@ -1201,6 +1204,10 @@ def store(self, kt, key, val): if self.sp_dleq_proofs is None: self.sp_dleq_proofs = {} self.sp_dleq_proofs[key] = val + elif kt == PSBT_IN_SP_TWEAK: + self.sp_tweak = self.get(val) + elif kt == PSBT_IN_SP_SPEND_BIP32_DERIVATION: + self.sp_spend_bip32_derivation = (key, val) else: # including: PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS self.unknown = self.unknown or [] @@ -1305,7 +1312,14 @@ def serialize(self, out_fd, is_v2): if self.sp_dleq_proofs: for k, v in self.sp_dleq_proofs.items(): wr(PSBT_IN_SP_DLEQ, v, k) - + + if self.sp_tweak: + wr(PSBT_IN_SP_TWEAK, self.sp_tweak) + + if self.sp_spend_bip32_derivation: + k, v = self.sp_spend_bip32_derivation + wr(PSBT_IN_SP_SPEND_BIP32_DERIVATION, v, k) + if self.unknown: for k, v in self.unknown: wr(None, v, k) @@ -2797,6 +2811,9 @@ def sign_it(self, alternate_secret=None, my_xfp=None): oup = self.outputs[out_idx] + if oup.sp_v0_info: + continue # SP change already verified in _detect_sp_change_outputs + good = 0 for i in oup.sp_idxs: # for multisig, will be N paths, and exactly one will @@ -3032,24 +3049,34 @@ def sign_it(self, alternate_secret=None, my_xfp=None): if xonly_pk == internal_key: # internal key is our key -> easy - # BIP 341 states: "If the spending conditions do not require a script path, - # the output key should commit to an unspendable script path instead of having no script path. - # This can be achieved by computing the output key point as Q = P + int(hashTapTweak(bytes(P)))G." - tweak = xonly_pk - if inp.taproot_merkle_root: - # we have a script path but internal key is spendable by us - # merkle root needs to be added to tweak with internal key - # merkle root was already verified against registered script in determine_my_signing_key - tweak += self.get(inp.taproot_merkle_root) - - tweak = ngu.hash.sha256t(TAP_TWEAK_H, tweak, True) - kpt = kp.xonly_tweak_add(tweak) - digest = self.make_txn_taproot_sighash(in_idx, hash_type=inp.sighash) if sv.deltamode: digest = ngu.hash.sha256d(digest) - sig = ngu.secp256k1.sign_schnorr(kpt, digest, ngu.random.bytes(32)) + if inp.sp_tweak: + # BIP-376: spending a silent payment output + # sp_tweak already encodes the full output key derivation, + # so we skip the normal taproot internal key tweaking + tweaked_sk_int = _compute_silent_payment_spending_privkey(sk, inp.sp_tweak) + tweaked_sk = tweaked_sk_int.to_bytes(32, 'big') + sig = ngu.secp256k1.sign_schnorr(tweaked_sk, digest, ngu.random.bytes(32)) + stash.blank_object(tweaked_sk) + else: + # BIP 341 states: "If the spending conditions do not require a script path, + # the output key should commit to an unspendable script path instead of having no script path. + # This can be achieved by computing the output key point as Q = P + int(hashTapTweak(bytes(P)))G." + tweak = xonly_pk + if inp.taproot_merkle_root: + # we have a script path but internal key is spendable by us + # merkle root needs to be added to tweak with internal key + # merkle root was already verified against registered script in determine_my_signing_key + tweak += self.get(inp.taproot_merkle_root) + + tweak = ngu.hash.sha256t(TAP_TWEAK_H, tweak, True) + kpt = kp.xonly_tweak_add(tweak) + sig = ngu.secp256k1.sign_schnorr(kpt, digest, ngu.random.bytes(32)) + del kpt + if inp.sighash != SIGHASH_DEFAULT: sig += bytes([inp.sighash]) @@ -3057,13 +3084,10 @@ def sign_it(self, alternate_secret=None, my_xfp=None): # 'omitting' the sighash byte, resulting in a 64-byte signature with SIGHASH_DEFAULT assumed inp.taproot_key_sig = sig self.sig_added = True - # debug - # ngu.secp256k1.verify_schnorr(sig, digest, kpt.xonly_pubkey()) - - del kpt elif isinstance(self.active_miniscript.to_descriptor().key, MusigKey): # internal key is musig + assert not inp.sp_tweak, "MuSig2 + SP tweak spending not yet supported" agg_k = self.active_miniscript.to_descriptor().key.node.pubkey() digest = self.make_txn_taproot_sighash(in_idx, hash_type=inp.sighash) diff --git a/shared/silentpayments.py b/shared/silentpayments.py index e1187cb9..606039a0 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -157,6 +157,45 @@ def _compute_silent_payment_output_script( return b"\x51\x20" + x_only +def _compute_silent_payment_spending_privkey(b_spend_bytes, sp_tweak_bytes): + """ + Compute private key for spending a silent payment output + + BIP-352 formula for spending: d_k = (b_spend + t_k) mod n + where: + - b_spend is the base spend private key + - t_k is the combined tweak (shared secret + label if applicable) from PSBT_IN_SP_TWEAK + - n is the secp256k1 curve order + + Args: + b_spend_bytes: Base spend private key + sp_tweak_bytes: 32-byte scalar tweak from PSBT_IN_SP_TWEAK + + Returns: + int: Tweaked spending private key + + Raises: + ValueError: If sp_tweak or spending_privkey is invalid + """ + if len(sp_tweak_bytes) != 32: + raise ValueError("SP tweak must be 32 bytes") + + sp_tweak_int = int.from_bytes(sp_tweak_bytes, 'big') + b_spend_int = int.from_bytes(b_spend_bytes, 'big') + + if b_spend_int == 0 or b_spend_int >= SECP256K1_ORDER: + raise ValueError("Spend private key is out of valid range") + if sp_tweak_int >= SECP256K1_ORDER: + raise ValueError("SP tweak is out of valid range") + + # Compute tweaked key: d_k = (d_spend + t_k) mod n + spending_sk = (b_spend_int + sp_tweak_int) % SECP256K1_ORDER + + if spending_sk == 0: + raise ValueError("Resulting spending key is zero (invalid)") + + return spending_sk + # ----------------------------------------------------------------------------- # Input Eligibility (BIP-352) @@ -325,16 +364,27 @@ def _is_input_eligible(self, inp): def _pubkey_from_input(self, inp): """ - Extract the BIP-352 contributing public key from an input. + Extract the BIP-352 contributing public key from an input Note: + Spending SP: use PSBT_IN_SPEND_BIP32_DERIVATION P2TR: use PSBT_IN_WITNESS_UTXO to fetch (x-only -> compressed with 0x02) non-taproot: use BIP32 derivation pubkey (first 33-byte key) Returns 33-byte compressed pubkey or None. """ - # TODO: BIP-376 use sp_spend_derivation when available + if not self._is_input_eligible(inp): + return None + spk = inp.utxo_spk + if inp.sp_spend_bip32_derivation: + _, val_coords = inp.sp_spend_bip32_derivation + xfp_path = self.parse_xfp_path(val_coords) + if xfp_path[0] == self.my_xfp: + pk = self.get(val_coords) + if len(pk) == 33: + return pk + if spk and _is_p2tr(spk): return b"\x02" + spk[2:34] else: @@ -709,25 +759,43 @@ def _derive_input_privkey(self, inp, sv): Derive the BIP-352 contributing private key for an eligible input Note: + If inp.sp_tweak is set (BIP-376), derives from sp_spend_bip32_derivation + and applies the tweak: d_k = d_spend + t_k (mod n) For taproot inputs, uses the internal key's derivation path (ik_idx), XFP-checked For non-taproot inputs, uses the first matching BIP32 derivation path Returns: int | None: The derived private key as an integer, or None if not eligible """ - # TODO: BIP-376 sp_spend_derivation should go here - spk = inp.utxo_spk - if spk and _is_p2tr(spk): - if inp.ik_idx is not None and inp.taproot_subpaths: - _, path_coords = inp.taproot_subpaths[inp.ik_idx] - xfp_path = self.parse_xfp_path(path_coords[2]) - if xfp_path[0] == self.my_xfp: - return self._path_to_privkey(xfp_path, sv) + privkey_int = None + + if inp.sp_tweak and inp.sp_spend_bip32_derivation: + _, val_coords = inp.sp_spend_bip32_derivation + xfp_path = self.parse_xfp_path(val_coords) + if xfp_path[0] == self.my_xfp: + privkey_int = self._path_to_privkey(xfp_path, sv) + privkey_int = _compute_silent_payment_spending_privkey( + privkey_int.to_bytes(32, 'big'), inp.sp_tweak + ) else: - for xfp_path in self._iter_input_xfp_paths(inp): - if xfp_path[0] == self.my_xfp: - return self._path_to_privkey(xfp_path, sv) - return None + spk = inp.utxo_spk + if spk and _is_p2tr(spk): + if inp.ik_idx is not None and inp.taproot_subpaths: + _, path_coords = inp.taproot_subpaths[inp.ik_idx[0]] + xfp_path = self.parse_xfp_path(path_coords[2]) + if xfp_path[0] == self.my_xfp: + privkey_int = self._path_to_privkey(xfp_path, sv) + if inp.taproot_internal_key: + privkey_int = self._normalize_p2tr_privkey( + privkey_int, self.get(inp.taproot_internal_key) + ) + else: + for xfp_path in self._iter_input_xfp_paths(inp): + if xfp_path[0] == self.my_xfp: + privkey_int = self._path_to_privkey(xfp_path, sv) + break + + return privkey_int def _get_outpoints(self): """ diff --git a/testing/devtest/unit_silentpayments.py b/testing/devtest/unit_silentpayments.py index 20a9b373..9562b204 100644 --- a/testing/devtest/unit_silentpayments.py +++ b/testing/devtest/unit_silentpayments.py @@ -17,6 +17,7 @@ _combine_pubkeys, _compute_shared_secret_tweak, _compute_silent_payment_output_script, + _compute_silent_payment_spending_privkey, _is_p2pkh, _is_p2wpkh, _is_p2tr, @@ -177,6 +178,20 @@ except ValueError as e: assert '33 bytes' in str(e) +# --------------------------------------------------------------------------- +# Spending Privkey +# --------------------------------------------------------------------------- + +result = _compute_silent_payment_spending_privkey((1).to_bytes(32, 'big'), (42).to_bytes(32, 'big')) +assert isinstance(result, int) +assert result != 1 +assert 0 < result < ngu.secp256k1.curve_order_int() + +try: + _compute_silent_payment_spending_privkey((0).to_bytes(32, 'big'), b'\x00' * 32) + assert False, "Should raise" +except ValueError as e: + assert 'out of valid range' in str(e) # --------------------------------------------------------------------------- # Mock Infrastructure @@ -193,6 +208,8 @@ def __init__(self): self.prevout_idx = None self.witness_utxo = None self.taproot_internal_key = None + self.sp_tweak = None + self.sp_spend_bip32_derivation = None self.utxo_spk = None self.ik_idx = None self.sighash = None diff --git a/testing/devtest/verify_sp_outputs.py b/testing/devtest/verify_sp_outputs.py index 411f703d..98fbb32b 100644 --- a/testing/devtest/verify_sp_outputs.py +++ b/testing/devtest/verify_sp_outputs.py @@ -47,6 +47,8 @@ def __init__(self): self.sp_dleq_proofs = {} self.subpaths = [] self.taproot_subpaths = [] + self.sp_tweak = None + self.sp_spend_bip32_derivation = {} self.sp_idxs = None self.ik_idx = None diff --git a/testing/psbt.py b/testing/psbt.py index 26e85ebe..709a5837 100644 --- a/testing/psbt.py +++ b/testing/psbt.py @@ -64,6 +64,9 @@ # BIP-375 Silent Payments PSBT_IN_SP_ECDH_SHARE = 0x1d PSBT_IN_SP_DLEQ = 0x1e +# BIP-376 Silent Payments +PSBT_IN_SP_SPEND_BIP32_DERIVATION = 0x1f +PSBT_IN_SP_TWEAK = 0x20 # OUTPUTS === PSBT_OUT_REDEEM_SCRIPT = 0x00 @@ -152,6 +155,8 @@ def defaults(self): self.musig_part_sigs = {} self.sp_ecdh_shares = {} self.sp_dleq_proofs = {} + self.sp_tweak = None + self.sp_spend_bip32_derivation = {} self.others = {} self.unknown = {} @@ -184,6 +189,8 @@ def __eq__(a, b): a.musig_part_sigs == b.musig_part_sigs and \ a.sp_ecdh_shares == b.sp_ecdh_shares and \ a.sp_dleq_proofs == b.sp_dleq_proofs and \ + a.sp_tweak == b.sp_tweak and \ + a.sp_spend_bip32_derivation == b.sp_spend_bip32_derivation and \ a.unknown == b.unknown if rv: # NOTE: equality test on signatures requires parsing DER stupidness @@ -277,6 +284,10 @@ def parse_kv(self, kt, key, val): self.sp_ecdh_shares[key] = val elif kt == PSBT_IN_SP_DLEQ: self.sp_dleq_proofs[key] = val + elif kt == PSBT_IN_SP_TWEAK: + self.sp_tweak = val + elif kt == PSBT_IN_SP_SPEND_BIP32_DERIVATION: + self.sp_spend_bip32_derivation[key] = val else: self.unknown[bytes([kt]) + key] = val @@ -341,6 +352,11 @@ def serialize_kvs(self, wr, v2): if self.sp_dleq_proofs: for k, v in self.sp_dleq_proofs.items(): wr(PSBT_IN_SP_DLEQ, v, k) + if self.sp_tweak is not None: + wr(PSBT_IN_SP_TWEAK, self.sp_tweak) + if self.sp_spend_bip32_derivation: + for k, v in self.sp_spend_bip32_derivation.items(): + wr(PSBT_IN_SP_SPEND_BIP32_DERIVATION, v, k) if self.musig_pubnonces: for (pk, ak, lh), pubnonce in self.musig_pubnonces.items(): From 45507e9f0bcce1d23834782cc8b96daa642b8060 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:26:02 -0500 Subject: [PATCH 11/13] SP: Add integration tests - test_silentpayments_sign.py create end-to-end tests for silent payments handle sp_tweak tweak parity when matching private key introduce mine vs theirs seed construct to fake_txn --- shared/silentpayments.py | 45 +++- testing/test_sign_silentpayments.py | 374 ++++++++++++++++++++++++++++ testing/txn.py | 5 +- 3 files changed, 417 insertions(+), 7 deletions(-) create mode 100644 testing/test_sign_silentpayments.py diff --git a/shared/silentpayments.py b/shared/silentpayments.py index 606039a0..12fbb9d2 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -9,7 +9,7 @@ import ngu from dleq import generate_dleq_proof, verify_dleq_proof from exceptions import FatalPSBTIssue -from precomp_tag_hash import BIP352_SHARED_SECRET_TAG_H, BIP352_INPUTS_TAG_H +from precomp_tag_hash import BIP352_SHARED_SECRET_TAG_H, BIP352_INPUTS_TAG_H, BIP352_LABEL_TAG_H, TAP_TWEAK_H from serializations import SIGHASH_ALL, SIGHASH_DEFAULT from ubinascii import unhexlify as a2b_hex from utils import keypath_to_str @@ -396,6 +396,38 @@ def _pubkey_from_input(self, inp): return None + def _tweak_p2tr_privkey(self, privkey_int, inp): + """ + Tweak a P2TR private key according to BIP-352 + + Args: + privkey_int (int): The internal private key as an integer + inp: The input object containing Taproot information + + Note: + keypair.xonly_tweak_add().privkey() returns the tweaked key without normalizing the output to even Y + + Returns: + int: The tweaked private key as an integer + """ + G = ngu.secp256k1.generator() + # normalize internal key to even Y, extract x-only. + internal_pub = ngu.secp256k1.ec_pubkey_tweak_mul(G, privkey_int.to_bytes(32, 'big')) + if internal_pub[0] == 0x03: + privkey_int = SECP256K1_ORDER - privkey_int + internal_xonly = internal_pub[1:] + # compute TapTweak hash (with merkle root if script tree present). + tweak_data = internal_xonly + if inp.taproot_merkle_root: + tweak_data = internal_xonly + self.get(inp.taproot_merkle_root) + t = ngu.hash.sha256t(TAP_TWEAK_H, tweak_data, True) + # add tweak scalar to get output private key. + d = (privkey_int + int.from_bytes(t, 'big')) % SECP256K1_ORDER + # negate if tweaked pubkey has odd Y (so d*G matches 0x02||x(Q)). + output_pub = ngu.secp256k1.ec_pubkey_tweak_mul(G, d.to_bytes(32, 'big')) + if output_pub[0] == 0x03: + d = SECP256K1_ORDER - d + return d # ----------------------------------------------------------------------------- # Validation Functions @@ -777,6 +809,12 @@ def _derive_input_privkey(self, inp, sv): privkey_int = _compute_silent_payment_spending_privkey( privkey_int.to_bytes(32, 'big'), inp.sp_tweak ) + # BIP-352: normalize to even-Y so contributing privkey matches + # 0x02||x(P_k) convention used by _pubkey_from_input + G = ngu.secp256k1.generator() + P_k = ngu.secp256k1.ec_pubkey_tweak_mul(G, privkey_int.to_bytes(32, 'big')) + if P_k[0] == 0x03: + privkey_int = SECP256K1_ORDER - privkey_int else: spk = inp.utxo_spk if spk and _is_p2tr(spk): @@ -785,10 +823,7 @@ def _derive_input_privkey(self, inp, sv): xfp_path = self.parse_xfp_path(path_coords[2]) if xfp_path[0] == self.my_xfp: privkey_int = self._path_to_privkey(xfp_path, sv) - if inp.taproot_internal_key: - privkey_int = self._normalize_p2tr_privkey( - privkey_int, self.get(inp.taproot_internal_key) - ) + privkey_int = self._tweak_p2tr_privkey(privkey_int, inp) else: for xfp_path in self._iter_input_xfp_paths(inp): if xfp_path[0] == self.my_xfp: diff --git a/testing/test_sign_silentpayments.py b/testing/test_sign_silentpayments.py new file mode 100644 index 00000000..de563874 --- /dev/null +++ b/testing/test_sign_silentpayments.py @@ -0,0 +1,374 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# test_silentpayments_sign.py - Simulator-based integration tests for BIP-375 Silent Payments +# +# Tests the full firmware signing path: PSBT construction -> upload to simulator -> +# firmware computes ECDH shares, DLEQ proofs, and output scripts -> download -> verify. +# +# Requires simulator running. Run with: pytest test_sign_silentpayments.py -v +# + +import pytest +import struct +import time +from binascii import unhexlify +from bip32 import BIP32Node +from ckcc_protocol.protocol import CCProtoError +from constants import simulator_fixed_tprv +from psbt import BasicPSBT +from pysecp256k1 import ec_pubkey_parse, ec_pubkey_serialize, ec_pubkey_tweak_add, tagged_sha256 +from pysecp256k1.extrakeys import xonly_pubkey_from_pubkey, xonly_pubkey_serialize +from sp_helpers import ( + _sim_sp, _sim_get_ecdh_and_pubkey, _sim_get_outpoints, + _sim_verify_dleq, _sim_compute_output_script, +) + + +# --------------------------------------------------------------------------- +# Test SP recipient keys (any valid secp256k1 points; these are the recipient's +# keys, not the signer's). Taken from BIP-352 test vectors; sorted lexicographically +# so multi-output tests can use them in correct k-assignment order. +# --------------------------------------------------------------------------- + +SCAN_KEY = unhexlify('03af606abaa5e29a89b93bf971c21e46dd2797aee31273c47f979a102eb51c3629') +SCAN_KEY_2 = unhexlify('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') + +# Sorted: SPEND_KEY_A < SPEND_KEY_B < SPEND_KEY_C (lexicographic) +SPEND_KEY_A = unhexlify('022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4') +SPEND_KEY_B = unhexlify('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5') +SPEND_KEY_C = unhexlify('02fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556') + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _add_sp_outputs(psbt, sp_outputs): + """Set sp_v0_info on specified outputs and clear script for firmware to compute.""" + for idx, scan_key, spend_key in sp_outputs: + psbt.outputs[idx].sp_v0_info = scan_key + spend_key + psbt.outputs[idx].script = None + + +def _verify_sp_outputs(sim_exec, sim_execfile, tp, scan_key, spend_keys_in_order): + """Verify SP output scripts match expected derivation for a given scan key.""" + assert scan_key in tp.sp_global_ecdh_shares, "Missing global ECDH share" + assert scan_key in tp.sp_global_dleq_proofs, "Missing global DLEQ proof" + + ecdh_share = tp.sp_global_ecdh_shares[scan_key] + dleq_proof = tp.sp_global_dleq_proofs[scan_key] + + assert len(ecdh_share) == 33 and ecdh_share[0] in (0x02, 0x03) + assert len(dleq_proof) == 64 + + _, summed_pubkey = _sim_get_ecdh_and_pubkey(sim_exec, sim_execfile, tp, scan_key) + _sim_verify_dleq(sim_exec, sim_execfile, summed_pubkey, scan_key, ecdh_share, dleq_proof) + + outpoints = _sim_get_outpoints(sim_exec, sim_execfile, tp) + sp_outs = [o for o in tp.outputs if o.sp_v0_info and o.sp_v0_info[:33] == scan_key] + assert len(sp_outs) == len(spend_keys_in_order) + + for k, (outp, sk) in enumerate(zip(sp_outs, spend_keys_in_order)): + assert outp.script is not None and len(outp.script) == 34 + assert outp.script[0] == 0x51 + expected = _sim_compute_output_script( + sim_exec, sim_execfile, outpoints, summed_pubkey, ecdh_share, sk, k) + assert outp.script == expected + + +def _compute_foreign_share(sim_exec, sim_execfile, privkey_int, scan_key): + """Compute ECDH share and DLEQ proof for a foreign input via simulator.""" + rv = _sim_sp(sim_exec, sim_execfile, 'compute_foreign_share', { + 'privkey_hex': '%064x' % privkey_int, + 'scan_key': scan_key.hex(), + }) + parts = rv.split(',') + return bytes.fromhex(parts[0]), bytes.fromhex(parts[1]) + + +# --------------------------------------------------------------------------- +# Basic Tests +# --------------------------------------------------------------------------- + +def test_sp_signing_story(dev, fake_txn, start_sign, end_sign, cap_story): + """SP output: approval story shows SP address, no unknown-script warning.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt='p2wpkh', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + + time.sleep(.1) + title, story = cap_story() + assert title == 'OK TO SEND?' + assert 'not well understood script' not in story + assert 'sp1' in story + + end_sign(accept=True, finalize=False) + + +def test_sp_p2wpkh_input(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Single P2WPKH input signing to a single SP output.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt='p2wpkh', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + + +def test_sp_p2tr_input(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Single P2TR input signing to a single SP output.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt='p2tr', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + + +def test_sp_mixed_inputs(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """P2WPKH + P2TR inputs combined produce a single SP output from summed privkey.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + psbt_bytes = fake_txn([['p2wpkh'], ['p2tr']], 1, xp, psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + + +# --------------------------------------------------------------------------- +# Mixed output tests +# --------------------------------------------------------------------------- + +def test_sp_three_outputs_same_scan_key(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Three SP outputs with same scan key: k=0,1,2 all produce distinct scripts.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [ + (0, SCAN_KEY, SPEND_KEY_A), + (1, SCAN_KEY, SPEND_KEY_B), + (2, SCAN_KEY, SPEND_KEY_C), + ]) + + psbt_bytes = fake_txn(1, 3, xp, addr_fmt='p2wpkh', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_A, SPEND_KEY_B, SPEND_KEY_C]) + + sp_outs = [o for o in tp.outputs if o.sp_v0_info and o.sp_v0_info[:33] == SCAN_KEY] + scripts = [o.script for o in sp_outs] + assert len(set(scripts)) == 3, "All three k-indexed scripts must be distinct" + + +def test_sp_two_outputs_different_scan_keys(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Two SP outputs with different scan keys each get their own ECDH share.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [ + (0, SCAN_KEY, SPEND_KEY_B), + (1, SCAN_KEY_2, SPEND_KEY_A), + ]) + + psbt_bytes = fake_txn(1, 2, xp, addr_fmt='p2wpkh', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY_2, [SPEND_KEY_A]) + + assert tp.sp_global_ecdh_shares[SCAN_KEY] != tp.sp_global_ecdh_shares[SCAN_KEY_2], \ + "Different scan keys must produce different ECDH shares" + + +def test_sp_mixed_output_types(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """SP output alongside a regular output: normal output script is preserved.""" + xp = dev.master_xpub + original_normal_script = None + + def sp_hacker(psbt): + nonlocal original_normal_script + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + original_normal_script = psbt.outputs[1].script + + psbt_bytes = fake_txn(1, 2, xp, addr_fmt='p2wpkh', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + + assert tp.outputs[1].script == original_normal_script, \ + "Normal output script must not be modified by SP processing" + + +# --------------------------------------------------------------------------- +# Multi-signer - complete coverage scenario tests +# --------------------------------------------------------------------------- + +def test_sp_all_owned_multi_input(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Scenario (a): all inputs owned -> global ECDH share -> compute scripts -> sign.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + psbt_bytes = fake_txn([['p2wpkh'], ['p2wpkh']], 1, xp, + psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + + assert tp.sp_global_ecdh_shares and SCAN_KEY in tp.sp_global_ecdh_shares + assert tp.sp_global_dleq_proofs and SCAN_KEY in tp.sp_global_dleq_proofs + for inp in tp.inputs: + assert not inp.sp_ecdh_shares, "Per-input shares must not exist in single-signer path" + + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + + for i, inp in enumerate(tp.inputs): + assert inp.part_sigs or inp.taproot_key_sig, f"Input {i} missing signature" + + +def test_sp_partial_owned_coverage_complete(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Scenario (b): some owned + foreign with pre-existing shares -> coverage complete -> sign.""" + xp = dev.master_xpub + FOREIGN_SEED = b'\xaa' * 32 + foreign_mk = BIP32Node.from_master_secret(FOREIGN_SEED) + foreign_sub = foreign_mk.subkey_for_path("0/1") + foreign_privkey_int = int.from_bytes(foreign_sub.privkey(), 'big') + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + ecdh_share, proof = _compute_foreign_share(sim_exec, sim_execfile, foreign_privkey_int, SCAN_KEY) + psbt.inputs[1].sp_ecdh_shares = {SCAN_KEY: ecdh_share} + psbt.inputs[1].sp_dleq_proofs = {SCAN_KEY: proof} + + psbt_bytes = fake_txn([['p2wpkh'], ['p2wpkh', None, None, False]], 1, xp, + psbt_v2=True, psbt_hacker=sp_hacker, foreign_seed=FOREIGN_SEED) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + + assert not tp.sp_global_ecdh_shares, "Global ECDH shares must not exist in multi-signer path" + + assert tp.inputs[0].sp_ecdh_shares and SCAN_KEY in tp.inputs[0].sp_ecdh_shares + assert tp.inputs[1].sp_ecdh_shares and SCAN_KEY in tp.inputs[1].sp_ecdh_shares + + sp_outs = [o for o in tp.outputs if o.sp_v0_info] + assert len(sp_outs) == 1 + assert sp_outs[0].script is not None and sp_outs[0].script[0] == 0x51 + + # Verify output script by combining per-input ECDH shares via firmware + ecdh_share, summed_pubkey = _sim_get_ecdh_and_pubkey(sim_exec, sim_execfile, tp, SCAN_KEY) + outpoints = _sim_get_outpoints(sim_exec, sim_execfile, tp) + expected = _sim_compute_output_script( + sim_exec, sim_execfile, outpoints, summed_pubkey, ecdh_share, SPEND_KEY_B, 0) + assert sp_outs[0].script == expected + + assert tp.inputs[0].part_sigs, "Owned input must have signature" + assert not tp.inputs[1].part_sigs, "Foreign input must not have signature" + + +# --------------------------------------------------------------------------- +# BIP-376 Silent Payments spend sp output test +# --------------------------------------------------------------------------- + +def test_sp_spend_silent_payment_output(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Spend from SP output with correct proof -> sign.""" + xp = dev.master_xpub + + SP_TWEAK = bytes.fromhex( + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2' + ) + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + inp = psbt.inputs[0] + base_xonly = inp.taproot_internal_key # 32-byte x-only + + # Derive full compressed B_spend with correct Y parity from BIP32 path + tap_val = inp.taproot_bip32_paths[base_xonly] + xfp_and_path = tap_val[1:] # strip leaf-hash count byte + path_data = xfp_and_path[4:] # skip 4-byte XFP + path_ints = [struct.unpack_from(' Date: Wed, 25 Mar 2026 16:58:22 -0400 Subject: [PATCH 12/13] SP: Introduce partial share contribution workflow add test to validate 'Contribute Shares?' UX --- shared/auth.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 4c1cf7c8..5813dd64 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -433,8 +433,31 @@ async def interact(self): # Silent Payments: Validate and pre-process Silent Payment outputs if self.psbt.has_silent_payment_outputs(): - self.psbt.preview_silent_payment_outputs() - + if not self.psbt.preview_silent_payment_outputs(): + # Coverage incomplete: shares computed but waiting on other signers. + # Skip normal approval flow — prompt user to contribute shares, then save. + del args + ch = await ux_show_story( + "Silent payment ECDH shares will be added to this transaction.\n\n" + "Other signers must contribute their shares before signing can proceed.\n\n" + "Press %s to contribute shares. %s to abort." % (OK, X), + title="CONTRIBUTE SHARES?" + ) + if ch != 'y': + self.refused = True + await ux_dramatic_pause("Refused.", 1) + del self.psbt + self.done() + return + try: + await done_signing(self.psbt, self, self.input_method, + self.filename, self.output_encoder, + finalize=False) + self.done() + except BaseException as exc: + return await self.failure("PSBT output failed", exc) + return + self.psbt.consider_outputs(*args, cosign_xfp=ccc_c_xfp) del args # not needed anymore # we can properly assess sighash only after we know From ecbd31fedb26d6fade03210d20ed7810424c1d94 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:29:21 -0400 Subject: [PATCH 13/13] SP: Introduce UI change output validation add silent payments label change tests - detect presence or absence of 'Change back:' in UI story add multi-signer incomplete coverage scenario tests fix sp_spend test failure when B_spend has odd Y parity --- shared/auth.py | 12 ++- shared/silentpayments.py | 59 +++++++++++++ testing/test_sign_silentpayments.py | 128 ++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 4 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 5813dd64..a323ca37 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -728,15 +728,16 @@ def output_summary_text(self, msg): total_change = 0 has_change = False - # TODO: Test pay to silent payment change address - for idx, tx_out in self.psbt.output_iter(): outp = self.psbt.outputs[idx] if outp.is_change: has_change = True total_change += tx_out.nValue if len(largest_change) < MAX_VISIBLE_CHANGE: - largest_change.append((tx_out.nValue, self.chain.render_address(tx_out.scriptPubKey))) + addr = self.chain.render_address(tx_out.scriptPubKey) + if outp.sp_v0_info: + addr += '\n' + self.psbt.render_silent_payment_output_string(outp) + largest_change.append((tx_out.nValue, addr)) if len(largest_change) == MAX_VISIBLE_CHANGE: largest_change = sorted(largest_change, key=lambda x: x[0], reverse=True) continue @@ -765,7 +766,10 @@ def output_summary_text(self, msg): largest.pop(-1) if outp.is_change: - ret = (here, self.chain.render_address(tx_out.scriptPubKey)) + addr = self.chain.render_address(tx_out.scriptPubKey) + if outp.sp_v0_info: + addr += '\n' + self.psbt.render_silent_payment_output_string(outp) + ret = (here, addr) else: rendered, _ = self.render_output(tx_out) ret = (here, rendered) diff --git a/shared/silentpayments.py b/shared/silentpayments.py index 12fbb9d2..92256b93 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -196,6 +196,31 @@ def _compute_silent_payment_spending_privkey(b_spend_bytes, sp_tweak_bytes): return spending_sk +def _apply_label_to_spend_key(B_spend, b_scan, label): + """ + Apply BIP-352 label tweak to spend key + + BIP-352 formula: B_m = B_spend + hash_BIP0352/Label(b_scan || m)*G + + Args: + B_spend: Base spend public key (33 bytes compressed) + b_scan: Scan private key (32 bytes) + label: Label integer (0 for change, >0 for other purposes) + + Returns: + bytes: B_spend_labeled public key (33 bytes compressed) + """ + msg = b_scan + label.to_bytes(4, 'big') + tweak_bytes = ngu.hash.sha256t(BIP352_LABEL_TAG_H, msg, True) + tweak_scalar = int.from_bytes(tweak_bytes, 'big') % SECP256K1_ORDER + + # Compute tweaked pubkey + G = ngu.secp256k1.generator() + Tweak_pubkey = ngu.secp256k1.ec_pubkey_tweak_mul(G, tweak_scalar.to_bytes(32, 'big')) + + # Apply tweak: B_m = B_spend + Tweak + return ngu.secp256k1.ec_pubkey_combine(B_spend, Tweak_pubkey) + # ----------------------------------------------------------------------------- # Input Eligibility (BIP-352) @@ -450,10 +475,44 @@ def _process_silent_payments(self, sv): if self._is_ecdh_coverage_complete(): self._compute_silent_payment_output_scripts() + self._detect_sp_change_outputs(sv) return True return False + def _detect_sp_change_outputs(self, sv): + """ + Mark SP outputs as change if sp_v0_label is present and keys match our wallet. + + Note: + Must be called while SensitiveValues context is open. + + No return value; modifies self.outputs 'is_change' in place. + """ + import chains + coin_type = chains.current_chain().b44_cointype + + scan_node = sv.derive_path("m/352h/%dh/0h/1h/0" % coin_type, register=False) + spend_node = sv.derive_path("m/352h/%dh/0h/0h/0" % coin_type, register=False) + + b_scan_bytes = scan_node.privkey() + B_scan_bytes = scan_node.pubkey() + B_spend_bytes = spend_node.pubkey() + + B_spend_labeled = _apply_label_to_spend_key(B_spend_bytes, b_scan_bytes, 0) + + for outp in self.outputs: + if not outp.sp_v0_info or not outp.sp_v0_label: + continue + label_val = int.from_bytes(outp.sp_v0_label, 'little') + if label_val != 0: + continue + if outp.sp_v0_info[:33] != B_scan_bytes: + continue + if outp.sp_v0_info[33:66] != B_spend_labeled: + continue + outp.is_change = True + def _validate_psbt_structure(self): """ Validate PSBT structure requirements for silent payments diff --git a/testing/test_sign_silentpayments.py b/testing/test_sign_silentpayments.py index de563874..8f08535b 100644 --- a/testing/test_sign_silentpayments.py +++ b/testing/test_sign_silentpayments.py @@ -22,6 +22,7 @@ _sim_sp, _sim_get_ecdh_and_pubkey, _sim_get_outpoints, _sim_verify_dleq, _sim_compute_output_script, ) +from constants import simulator_fixed_tprv # --------------------------------------------------------------------------- @@ -39,6 +40,26 @@ SPEND_KEY_C = unhexlify('02fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556') +# --------------------------------------------------------------------------- +# Derive labeled SP output keys for use in change output tests. +# Simulator's BIP-352 keys (testnet: coin_type=1), derived from fixed test seed. +# Used for SP change output tests where sp_v0_info must match our wallet keys. +# --------------------------------------------------------------------------- + +def _derive_sim_sp_keys(): + root = BIP32Node.from_wallet_key(simulator_fixed_tprv) + scan_node = root.subkey_for_path("352h/1h/0h/1h/0") + spend_node = root.subkey_for_path("352h/1h/0h/0h/0") + b_scan = scan_node.privkey() # 32 bytes + B_scan = scan_node.sec() # 33 bytes + B_spend = spend_node.sec() # 33 bytes + label_tweak = tagged_sha256(b"BIP0352/Label", b_scan + b'\x00\x00\x00\x00') + B_spend_labeled = ec_pubkey_serialize(ec_pubkey_tweak_add(ec_pubkey_parse(B_spend), label_tweak)) + return B_scan, B_spend_labeled + +SIM_B_SCAN, SIM_B_SPEND_LABELED = _derive_sim_sp_keys() + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -294,6 +315,64 @@ def sp_hacker(psbt): assert not tp.inputs[1].part_sigs, "Foreign input must not have signature" +# --------------------------------------------------------------------------- +# Multi-signer - incomplete coverage scenario tests +# --------------------------------------------------------------------------- + +def test_sp_partial_owned_coverage_incomplete(dev, fake_txn, start_sign, end_sign, cap_story): + """Scenario (c): some owned + foreign without shares -> coverage incomplete -> save PSBT.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + psbt_bytes = fake_txn([['p2wpkh'], ['p2wpkh', None, None, False]], 1, xp, + psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + + time.sleep(.1) + title, story = cap_story() + assert title == 'CONTRIBUTE SHARES?' + assert 'ECDH shares' in story + assert 'Other signers' in story + + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + + assert not tp.sp_global_ecdh_shares, "Global ECDH shares must not exist in multi-signer path" + + assert tp.inputs[0].sp_ecdh_shares and SCAN_KEY in tp.inputs[0].sp_ecdh_shares + assert tp.inputs[0].sp_dleq_proofs and SCAN_KEY in tp.inputs[0].sp_dleq_proofs + assert not tp.inputs[1].sp_ecdh_shares + + sp_outs = [o for o in tp.outputs if o.sp_v0_info] + assert len(sp_outs) == 1 + assert not sp_outs[0].script, "Output script must NOT be set when coverage is incomplete" + + for i, inp in enumerate(tp.inputs): + assert not inp.part_sigs, f"Input {i} must not have signature" + assert not inp.taproot_key_sig, f"Input {i} must not have taproot signature" + + +def test_sp_partial_owned_coverage_incomplete_refused(dev, fake_txn, start_sign, end_sign, cap_story): + """Refuse to contribute SP shares -> CCUserRefused.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + psbt_bytes = fake_txn([['p2wpkh'], ['p2wpkh', None, None, False]], 1, xp, + psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + + time.sleep(.1) + title, _ = cap_story() + assert title == 'CONTRIBUTE SHARES?' + + end_sign(accept=False) + + # --------------------------------------------------------------------------- # BIP-376 Silent Payments spend sp output test # --------------------------------------------------------------------------- @@ -372,3 +451,52 @@ def sp_hacker(psbt): start_sign(psbt_bytes) with pytest.raises(CCProtoError, match="SP_V0_INFO wrong size"): end_sign(accept=False, finalize=False) + + +# --------------------------------------------------------------------------- +# Silent Payments Change Detection Tests +# --------------------------------------------------------------------------- + +def test_sp_spend_to_labeled_change_address(dev, fake_txn, start_sign, end_sign, cap_story): + """Two SP outputs: output 0 is a send (no label), output 1 is change (label=0, wallet keys).""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [ + (0, SCAN_KEY, SPEND_KEY_B), + (1, SIM_B_SCAN, SIM_B_SPEND_LABELED), + ]) + psbt.outputs[1].sp_v0_label = b'\x00\x00\x00\x00' + + psbt_bytes = fake_txn(1, 2, xp, addr_fmt='p2wpkh', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + + title, story = cap_story() + assert title == 'OK TO SEND?' + assert 'not well understood script' not in story + assert 'sp1' in story + assert 'Change back:' in story + + end_sign(accept=False, finalize=False) + + +def test_sp_labeled_change_wrong_keys_not_change(dev, fake_txn, start_sign, end_sign, cap_story): + """SP output with label=0 but non-wallet keys must not be detected as change.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [ + (0, SCAN_KEY, SPEND_KEY_B), + (1, SCAN_KEY, SPEND_KEY_B), + ]) + psbt.outputs[1].sp_v0_label = b'\x00\x00\x00\x00' + + psbt_bytes = fake_txn(1, 2, xp, addr_fmt='p2wpkh', psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + + title, story = cap_story() + assert title == 'OK TO SEND?' + assert 'Change back:' not in story + assert 'sp1' in story + + end_sign(accept=False, finalize=False)