From 5f2db0c3dfcd4916893aa363c955fff530d9f1a7 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/32] nit: mark bitcoind dependent sign tests --- testing/test_sign.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testing/test_sign.py b/testing/test_sign.py index 9f1dd445..13e2e106 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -765,6 +765,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 +1336,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 +2929,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 +2986,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 e004001dffb8c51f50c88c74e1839801e8f18c31 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:34:58 -0400 Subject: [PATCH 02/32] bump submodule version external/ckcc-protocol - Add bip375 and bip376 constants external/libngu - Expose ec_pubkey_combine - Expose ec_pubkey_tweak_mul - Add bip352_encode --- 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..da36586b 160000 --- a/external/ckcc-protocol +++ b/external/ckcc-protocol @@ -1 +1 @@ -Subproject commit 6d9f7193b336ab1097c7f941ce8c7e2ae80bfe29 +Subproject commit da36586bdb55ae7c98d7b98bc0f8b50ce996259c diff --git a/external/libngu b/external/libngu index b0ce9acf..05158533 160000 --- a/external/libngu +++ b/external/libngu @@ -1 +1 @@ -Subproject commit b0ce9acffa455d9630c64d3614d0fb9b913c919e +Subproject commit 051585338644e46339b19319fadfc1690bb4eb68 From 4be2f2d4073280d17f7dbf25bf03c0412e9503db 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/32] BIP-352: implement Silent Payments cryptographic primitives Add bip352 crypto primitives / helpers Add bip352 tagged hashes --- shared/precomp_tag_hash.py | 10 ++- shared/silentpayments.py | 142 +++++++++++++++++++++++++++++++++++++ 2 files changed, 151 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..f917ad65 --- /dev/null +++ b/shared/silentpayments.py @@ -0,0 +1,142 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# silentpayments.py - BIP-352/BIP-375 +# +# Consolidates cryptographic primitives and PSBT handling logic for Silent Payments +# +import ngu +from precomp_tag_hash import BIP352_SHARED_SECRET_TAG_H, BIP352_INPUTS_TAG_H + +G = ngu.secp256k1.generator() +SECP256K1_ORDER = ngu.secp256k1.curve_order_int() + +# ----------------------------------------------------------------------------- +# Silent Payments Cryptographic Primitives +# ----------------------------------------------------------------------------- + + +def _combine_pubkeys(pubkeys): + """ + Combine a list of public keys into a single public key + + Args: + pubkeys: List of public keys to combine (33-byte compressed) + + Returns: + bytes: Combined public key (33-byte compressed) + """ + if len(pubkeys) == 1: + return pubkeys[0] + return ngu.secp256k1.ec_pubkey_combine(pubkeys) + + +def _compute_ecdh_share(a_sum_bytes, B_scan_bytes): + """ + Compute ECDH share (partial shared secret) + + Formula: ecdh_share = a_sum * B_scan + + Args: + a_sum_bytes: Combined private key (32-byte scalar) + B_scan_bytes: Scan public key (33-byte compressed) + + Returns: + bytes: ECDH share point (33-byte compressed) + """ + return ngu.secp256k1.ec_pubkey_tweak_mul(B_scan_bytes, a_sum_bytes) + + +def _compute_input_hash(outpoints, A_sum_bytes): + """ + Compute BIP-352 input hash + + Formula: input_hash = hash_BIP0352/Inputs(smallest_outpoint || A_sum) + + Args: + outpoints: List of (txid, vout) tuples, where txid is 32-byte and vout is 4-byte little-endian + A_sum_bytes: Sum of all eligible input public keys (33-byte compressed) + + Returns: + bytes: Input hash (32-byte scalar) + """ + 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) + input_hash_int = int.from_bytes(input_hash_bytes, "big") + if not (0 < input_hash_int < SECP256K1_ORDER): + raise ValueError("Invalid input hash: not in valid scalar range") + return input_hash_int.to_bytes(32, "big") + + +def _compute_shared_secret_tweak(shared_secret_bytes, k): + """ + Compute BIP-352 shared secret tweak + + Formula: t_k = hash_BIP0352/SharedSecret(serP(shared_secret) || ser_32(k)) + + Args: + shared_secret_bytes: Shared secret point (33-byte compressed) + k: Output index per scan key group (int) + + Returns: + bytes: Shared secret tweak (32-byte scalar) + """ + msg = shared_secret_bytes + k.to_bytes(4, "big") + tweak_bytes = ngu.hash.sha256t(BIP352_SHARED_SECRET_TAG_H, msg, True) + tweak_int = int.from_bytes(tweak_bytes, "big") + if not (0 < tweak_int < SECP256K1_ORDER): + raise ValueError("Invalid shared secret tweak: not in valid scalar range") + return tweak_int.to_bytes(32, "big") + + +def _compute_silent_payment_output_script(outpoints, A_sum_bytes, ecdh_share_bytes, B_spend_bytes, k): + """ + Compute the P2TR scriptPubKey for silent payment + + Formula: P_k = B_spend + t_k * G + + Args: + outpoints: List of (txid, vout) tuples from eligible inputs + A_sum_bytes: Sum of eligible input public keys (33-byte compressed) + ecdh_share_bytes: ECDH share point (33-byte compressed) + B_spend_bytes: Recipient spend public key (33-byte compressed) + k: Output index per scan key group (int) + + Returns: + bytes: P2TR scriptPubKey (OP_1 <32-byte x-only pubkey>) + """ + 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) + tweak_bytes = _compute_shared_secret_tweak(shared_secret_bytes, k) + # Derive output pubkey: P_k = B_spend + t_k * G + tweak_point = ngu.secp256k1.ec_pubkey_tweak_mul(G, tweak_bytes) + output_pubkey = ngu.secp256k1.ec_pubkey_combine([B_spend_bytes, tweak_point]) + x_only = output_pubkey[1:] + return b"\x51\x20" + x_only + + +# ----------------------------------------------------------------------------- +# Input Eligibility +# ----------------------------------------------------------------------------- + + +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 e556787d431c90a8d7a329aae59f0b4128dea299 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/32] 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 | 157 +++++++++++++++++++++++++++++++++++++ shared/precomp_tag_hash.py | 8 ++ 2 files changed, 165 insertions(+) create mode 100644 shared/dleq.py diff --git a/shared/dleq.py b/shared/dleq.py new file mode 100644 index 00000000..b3f8c6f8 --- /dev/null +++ b/shared/dleq.py @@ -0,0 +1,157 @@ +# (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 public key) +# - C = a*B (ECDH share point) +# +# This proves the ECDH computation is correct without revealing the private key +# +import ngu +from precomp_tag_hash import DLEQ_TAG_AUX_H, DLEQ_TAG_NONCE_H, DLEQ_TAG_CHALLENGE_H + +G = ngu.secp256k1.generator() +SECP256K1_ORDER = ngu.secp256k1.curve_order_int() + + +def xor_bytes(a, b): + """XOR two byte strings of equal length""" + return bytes(x ^ y for x, y in zip(a, b)) + + +def dleq_challenge(A, B, C, R1, R2, m=None, _G=None): + """Compute DLEQ challenge using BIP-374 tagged hash""" + + # BIP-374: e = TaggedHash("BIP0374/challenge", cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m) + challenge_input = A + B + C + _G + R1 + R2 + 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, B_scan, aux_rand=None, m=None): + """ + Generate DLEQ proof (BIP-374) + + Args: + a_sum: Input private key a (32-byte scalar) + B_scan: Scan public key B (33-byte compressed) + aux_rand: Auxiliary randomness r (32-byte or None) + m: Optional message (32-byte or None) + + Returns: + bytes: DLEQ proof (64-byte: e || s) + + Raises: + ValueError: If inputs are invalid + """ + a_sum_int = int.from_bytes(a_sum, "big") + if not (0 < a_sum_int < SECP256K1_ORDER): + raise ValueError("Invalid input private key: not in valid scalar range") + + # Compute public key A = a*G + A_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(G, a_sum) + + # Compute ECDH share C = a*B + C_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(B_scan, a_sum) + + if aux_rand is None: + aux_rand = ngu.random.bytes(32) + 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(a_sum, 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") % SECP256K1_ORDER + if k == 0: + raise ValueError("Generated nonce k is zero") + + # R1 = k*G, R2 = k*B + k_bytes = k.to_bytes(32, "big") + R1_bytes = ngu.secp256k1.ec_pubkey_tweak_mul(G, 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=G) + + # s = (k + e*a) mod n + s = (k + e * a_sum_int) % SECP256K1_ORDER + + # 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): + 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) + + Args: + A_sum_bytes: Input public key A (33-byte compressed) + B_scan_bytes: Scan public key B (33-byte compressed) + ecdh_share_bytes: ECDH share point C (33-byte compressed) + proof: DLEQ proof (64-byte: e || s) + m: Optional message (32-byte or None) + + Returns: + bool: True if proof is valid, False otherwise + """ + if len(proof) != 64: + return False + if m is not None and len(m) != 32: + return False + + e_bytes = proof[:32] + s_bytes = proof[32:] + + s = int.from_bytes(s_bytes, "big") + if not (0 < s < SECP256K1_ORDER): + return False + + # 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, 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=G) + + # Verify e == e' + e = int.from_bytes(e_bytes, "big") + return e == e_check 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/32] SP: add SilentPaymentsMixin Extend psbt handling with silent payment specific functions Incorporate validation functions from bip375 test vectors - psbt_structure, input_eligibility, ecdh_coverage Add private key derivation helper functions --- shared/silentpayments.py | 514 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 514 insertions(+) diff --git a/shared/silentpayments.py b/shared/silentpayments.py index f917ad65..0983da83 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -5,9 +5,16 @@ # Consolidates cryptographic primitives and PSBT handling logic for Silent Payments # 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 G = ngu.secp256k1.generator() +# BIP-341 NUMS point (Nothing Up My Sleeve) - x-only (32-byte) +NUMS_H = a2b_hex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") SECP256K1_ORDER = ngu.secp256k1.curve_order_int() # ----------------------------------------------------------------------------- @@ -140,3 +147,510 @@ 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 psbt as self + """ + + def _process_silent_payments(self, sv): + """ + Core SP workflow: validate, compute shares, compute scripts + + Returns: + bool: True if output scripts were computed + """ + 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 + + # ----------------------------------------------------------------------------- + # Input Helper Functions + # ----------------------------------------------------------------------------- + + def _get_ecdh_and_pubkey(self, scan_key): + """ + Get combined ECDH share and summed pubkey for a given scan key + + Returns: + tuple: (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] + 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) + + # Per-input shares: combine EDCH shares and sum all eligible input pubkeys + 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 combined_ecdh is None: + combined_ecdh = share + else: + combined_ecdh = ngu.secp256k1.ec_pubkey_combine([combined_ecdh, share]) + pk = self._pubkey_from_input(inp) + if pk: + pubkeys.append(pk) + + if combined_ecdh and pubkeys: + return combined_ecdh, _combine_pubkeys(pubkeys) + return None, None + + def _is_input_eligible(self, input): + """Check if input is eligible for silent payments per BIP-352""" + spk = input.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 input.taproot_internal_key: + tap_ik = self.get(input.taproot_internal_key) + if tap_ik == NUMS_H: + return False + + if _is_p2sh(spk): + if input.redeem_script: + rs = self.get(input.redeem_script) + if not _is_p2wpkh(rs): + return False + else: + return False + return True + + def _pubkey_from_input(self, input): + """ + Extract the contributing public key from an input + + Note: + P2TR: use PSBT_IN_WITNESS_UTXO to fetch x-only compressed pubkey + non-taproot: use BIP32 derivation pubkey from subpaths + + Returns: + bytes: Input public key (33-byte compressed) or None if not found + """ + spk = input.utxo_spk + if spk and _is_p2tr(spk): + return b"\x02" + spk[2:34] + if input.subpaths: + for pk_coords, _ in input.subpaths: + pk = self.get(pk_coords) + if len(pk) == 33: + return pk + return None + + # ----------------------------------------------------------------------------- + # Validation Functions + # ----------------------------------------------------------------------------- + + 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 + + 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 share / 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) + + # ----------------------------------------------------------------------------- + # Modify PSBT Field Functions + # ----------------------------------------------------------------------------- + + def _compute_and_store_ecdh_shares(self, sv): + """ + Compute ECDH shares and DLEQ proofs for our inputs, store in PSBT fields + + Notes: + 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_material = [] # list of (inp, privkey_int) + for inp in self.inputs: + if not inp.sp_idxs or not self._is_input_eligible(inp): + continue + + input_sk = self._derive_input_privkey(inp, sv) + if input_sk: + input_material.append((inp, input_sk)) + 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_material: + 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: combine all input private keys, one global ECDH share and DLEQ proofs + combined_sk = 0 + for _, input_sk in input_material: + input_sk_int = int.from_bytes(input_sk, "big") + combined_sk = (combined_sk + input_sk_int) % SECP256K1_ORDER + + ecdh_share = _compute_ecdh_share(combined_sk.to_bytes(32, "big"), scan_key) + dleq_proof = generate_dleq_proof(combined_sk.to_bytes(32, "big"), scan_key) + + self.sp_global_ecdh_shares = self.sp_global_ecdh_shares or {} + self.sp_global_dleq_proofs = self.sp_global_dleq_proofs or {} + 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, input_sk in input_material: + ecdh_share = _compute_ecdh_share(input_sk, scan_key) + dleq_proof = generate_dleq_proof(input_sk, scan_key) + + inp.sp_ecdh_shares = inp.sp_ecdh_shares or {} + inp.sp_dleq_proofs = inp.sp_dleq_proofs or {} + inp.sp_ecdh_shares[scan_key] = ecdh_share + inp.sp_dleq_proofs[scan_key] = dleq_proof + return True + + def _compute_silent_payment_output_scripts(self): + """ + Compute and set the scriptPubKey for each silent payment output + + 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 + + # ----------------------------------------------------------------------------- + # Utility 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 _get_outpoints(self): + """ + Get a list of outpoints (txid, vout) for all inputs + + Returns: + list: Outpoints as (txid, vout) tuples + """ + 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("Did not find any outpoints") + return outpoints + + def _get_silent_payment_scan_keys(self): + """ + Extract unique scan keys from silent payment outputs + + Returns: + list: Unique scan public keys (33-byte compressed) + """ + 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) + + # ----------------------------------------------------------------------------- + # Key Derivation Functions + # ----------------------------------------------------------------------------- + + def _derive_input_privkey(self, input, sv): + """ + Derive the 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: + bytes: Derived private key as (32-byte scalar) or None if not eligible + """ + # TODO: BIP-376 sp_spend_derivation should go here + spk = input.utxo_spk + if spk and _is_p2tr(spk): + if input.ik_idx is not None and input.taproot_subpaths: + _, path_coords = input.taproot_subpaths[input.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(input): + if xfp_path[0] == self.my_xfp: + return self._path_to_privkey(xfp_path, sv) + return None + + def _iter_input_xfp_paths(self, input): + """ + 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: Parsed xfp_path tuples for all derivation paths in the input + """ + if input.taproot_subpaths: + for _, path_coords in input.taproot_subpaths: + yield self.parse_xfp_path(path_coords[2]) + elif input.subpaths: + for _, path_coords in input.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: + bytes: Derived private key (32-byte scalar) + """ + node = sv.derive_path(keypath_to_str(xfp_path, skip=1), register=False) + return node.privkey() From 05ada7c1957a376b3bf9b1729de8cbb6957e83eb 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/32] 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 52720fe78dd744e9229520ff56b884d2d30348c1 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/32] fix: (usb_test) Unify local and globals dict to single namespace 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 c5413088bde89953480687a49d8e442cc6ddc64d 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/32] BIP-375: add test vectors Use devtest/verify_sp_outputs.py for simulator testing Add testing/test_bip375_vectors.py and testing/bip375_test_vectors.json - Correct input eligibility ecdh share assumptions Add devtest/unit_silentpayments.py to perform unit testing via simulator Mirror psbt fields in testing/psbt.py --- shared/silentpayments.py | 118 +- testing/bip375_test_vectors.json | 1759 ++++++++++++++++++++++++ testing/devtest/unit_silentpayments.py | 608 ++++++++ testing/devtest/verify_sp_outputs.py | 208 +++ testing/psbt.py | 50 + testing/sp_helpers.py | 289 ++++ testing/test_bip375_vectors.py | 236 ++++ 7 files changed, 3250 insertions(+), 18 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_bip375_vectors.py diff --git a/shared/silentpayments.py b/shared/silentpayments.py index 0983da83..1b3ece63 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -4,6 +4,7 @@ # # Consolidates cryptographic primitives and PSBT handling logic for Silent Payments # +import chains import ngu from dleq import generate_dleq_proof, verify_dleq_proof from exceptions import FatalPSBTIssue @@ -17,6 +18,7 @@ NUMS_H = a2b_hex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") SECP256K1_ORDER = ngu.secp256k1.curve_order_int() + # ----------------------------------------------------------------------------- # Silent Payments Cryptographic Primitives # ----------------------------------------------------------------------------- @@ -115,13 +117,86 @@ def _compute_silent_payment_output_script(outpoints, A_sum_bytes, ecdh_share_byt 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) tweak_bytes = _compute_shared_secret_tweak(shared_secret_bytes, k) - # Derive output pubkey: P_k = B_spend + t_k * G - tweak_point = ngu.secp256k1.ec_pubkey_tweak_mul(G, tweak_bytes) - output_pubkey = ngu.secp256k1.ec_pubkey_combine([B_spend_bytes, tweak_point]) - x_only = output_pubkey[1:] + x_only = _compute_silent_payment_spending_xonly(B_spend_bytes, tweak_bytes) 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 + + Formula: d_k = (b_spend + sp_tweak) mod n + + Note: sp_tweak = combined tweak (shared secret + label if applicable) + + Args: + b_spend_bytes: Base spend private key (32-byte scalar) + sp_tweak_bytes: Tweak from PSBT_IN_SP_TWEAK (32-byte scalar) + + Returns: + bytes: Tweaked spending private key normalized to even Y (32-byte scalar) + + Raises: + ValueError: If sp_tweak or spending_privkey is invalid + """ + sp_tweak_int = int.from_bytes(sp_tweak_bytes, "big") + b_spend_int = int.from_bytes(b_spend_bytes, "big") + if not (0 < b_spend_int < SECP256K1_ORDER): + raise ValueError("Invalid spend private key: not in valid scalar range") + if not (0 < sp_tweak_int < SECP256K1_ORDER): + raise ValueError("Invalid tweak: not in valid scalar range") + spending_sk = (b_spend_int + sp_tweak_int) % SECP256K1_ORDER + if spending_sk == 0: + raise ValueError("Invalid computed spend key: result is zero") + return _negate_if_odd_y(spending_sk.to_bytes(32, "big")) + + +def _compute_silent_payment_spending_xonly(B_spend_bytes, sp_tweak_bytes): + """ + Compute x-only pubkey for spending a silent payment output + + Formula: P_k = B_spend + sp_tweak * G + + Args: + B_spend_bytes: Recipient spend public key (33-byte compressed) + sp_tweak_bytes: SP tweak (32-byte scalar) + + Returns: + bytes: x-only public key (32-byte) + """ + tweak_point = ngu.secp256k1.ec_pubkey_tweak_mul(G, sp_tweak_bytes) + output_pubkey = ngu.secp256k1.ec_pubkey_combine([B_spend_bytes, tweak_point]) + return output_pubkey[1:] + + +def _negate_if_odd_y(privkey): + """ + Normalize a private key so its corresponding public key has even Y (0x02 prefix) + + Returns: + bytes: normalized private key (32-byte scalar) + """ + pubkey = ngu.secp256k1.ec_pubkey_tweak_mul(G, privkey) + if pubkey[0] == 0x03: + privkey = (SECP256K1_ORDER - int.from_bytes(privkey, "big")).to_bytes(32, "big") + return privkey + + +def _sum_privkeys(privkeys): + """ + Sum list of private key (32-byte scalars) + + Returns: + bytes: summed private key (32-byte scalar) + """ + total = 0 + for sk in privkeys: + total = (total + int.from_bytes(sk, "big")) % SECP256K1_ORDER + if total == 0: + raise ValueError("Invalid private key sum: result is zero") + return total.to_bytes(32, "big") + + # ----------------------------------------------------------------------------- # Input Eligibility # ----------------------------------------------------------------------------- @@ -204,8 +279,9 @@ def _get_ecdh_and_pubkey(self, scan_key): pubkeys.append(pk) if pubkeys: return ecdh_share, _combine_pubkeys(pubkeys) + return None, None - # Per-input shares: combine EDCH shares and sum all eligible input pubkeys + # Per-input shares: combine shares and pubkeys from eligible inputs combined_ecdh = None pubkeys = [] for inp in self.inputs: @@ -215,9 +291,10 @@ def _get_ecdh_and_pubkey(self, scan_key): combined_ecdh = share else: combined_ecdh = ngu.secp256k1.ec_pubkey_combine([combined_ecdh, share]) - pk = self._pubkey_from_input(inp) - if pk: - pubkeys.append(pk) + if self._is_input_eligible(inp): + pk = self._pubkey_from_input(inp) + if pk: + pubkeys.append(pk) if combined_ecdh and pubkeys: return combined_ecdh, _combine_pubkeys(pubkeys) @@ -258,6 +335,9 @@ def _pubkey_from_input(self, input): Returns: bytes: Input public key (33-byte compressed) or None if not found """ + if not self._is_input_eligible(input): + return None + spk = input.utxo_spk if spk and _is_p2tr(spk): return b"\x02" + spk[2:34] @@ -411,8 +491,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) @@ -477,6 +555,7 @@ def _compute_and_store_ecdh_shares(self, sv): elif inp.taproot_subpaths or inp.subpaths: has_foreign = True + # FIXME: is this the right approach? cosign_xfp? # Detect foreign eligible inputs (different XFP, no sp_idxs) if not has_foreign: for inp in self.inputs: @@ -499,13 +578,9 @@ def _compute_and_store_ecdh_shares(self, sv): for scan_key in scan_keys: if all_inputs_ours: # Single-signer: combine all input private keys, one global ECDH share and DLEQ proofs - combined_sk = 0 - for _, input_sk in input_material: - input_sk_int = int.from_bytes(input_sk, "big") - combined_sk = (combined_sk + input_sk_int) % SECP256K1_ORDER - - ecdh_share = _compute_ecdh_share(combined_sk.to_bytes(32, "big"), scan_key) - dleq_proof = generate_dleq_proof(combined_sk.to_bytes(32, "big"), scan_key) + combined_sk = _sum_privkeys(sk for _, sk in input_material) + ecdh_share = _compute_ecdh_share(combined_sk, scan_key) + dleq_proof = generate_dleq_proof(combined_sk, scan_key) self.sp_global_ecdh_shares = self.sp_global_ecdh_shares or {} self.sp_global_dleq_proofs = self.sp_global_dleq_proofs or {} @@ -548,7 +623,14 @@ 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(outpoints, summed_pubkey, ecdh_share, B_spend, k) + 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 # ----------------------------------------------------------------------------- diff --git a/testing/bip375_test_vectors.json b/testing/bip375_test_vectors.json new file mode 100644 index 00000000..3f16f51b --- /dev/null +++ b/testing/bip375_test_vectors.json @@ -0,0 +1,1759 @@ +{ + "description": "BIP-375 Test Vectors", + "version": "1.1", + "notes": [ + "Generated by https://github.com/macgyver13/bip375-test-generator/", + "Each vector includes: base64-encoded psbt and description", + "Supplementary material (inputs, outputs, sp_proofs) is included for diagnostics and should not be used for validation", + "'checks' overrides the validation sequence, e.g. skip structure checks to test input eligibility" + ], + "invalid": [ + { + "description": "psbt structure: missing PSBT_OUT_SP_V0_INFO field when PSBT_OUT_SP_V0_LABEL set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgImjPUplZ8wrI96VHHKTGdTalHw5bForwPu1HFe3McuABCgQBAAAAAA==", + "supplementary": {} + }, + { + "description": "psbt structure: incorrect byte length for PSBT_OUT_SP_V0_INFO field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUECekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84AA=", + "supplementary": {} + }, + { + "description": "psbt structure: incorrect byte length for PSBT_IN_SP_ECDH_SHARE field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GggA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QoiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaECKE7OYVUX3K9bolxSuuQmz41SoQqm7i1bNDt7SHfihmVmLMSKKSeC9fpXOEFP3xbKKy1Q6aHB2AOPOiYIu4yAhAAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": {} + }, + { + "description": "psbt structure: incorrect byte length for PSBT_IN_SP_DLEQ field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4Gg/ihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgAAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": {} + }, + { + "description": "psbt structure: PSBT_GLOBAL_TX_MODIFIABLE field is non-zero when PSBT_OUT_SCRIPT set for sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAQABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": {} + }, + { + "description": "psbt structure: missing PSBT_OUT_SCRIPT field when sending to non-sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAA=", + "supplementary": {} + }, + { + "description": "ecdh coverage: only one ineligible P2SH multisig input when PSBT_OUT_SCRIPT set for sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiDNOBpFUI/j2cq2O34PcEoPVp2/1VqYEC2eozruYht19AEPBAAAAAABAFMCAAAAAXyoMfPUiQn+aMsOqqoR+Hc97n6McwhTN1kOJLsGOQ20AAAAAAD/////AaCGAQAAAAAAF6kU2OZohxTu7Qqi7dSJqhyjwXFwHk2HAAAAAAEER1IhAr5Ar99ismPLfe4rmF0cOP5BsZlNKwUHU0PRcsIY6rHEIQJ6R5JT0AogWhCpQ483IBm582TgqDaAC/g529KOfOa5VVKuARAE/v///wABAwiQXwEAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "", + "public_key": "", + "prevout_txid": "cd381a45508fe3d9cab63b7e0f704a0f569dbfd55a98102d9ea33aee621b75f4", + "prevout_index": 0, + "prevout_scriptpubkey": "a914d8e6688714eeed0aa2edd489aa1ca3c171701e4d87", + "amount": 100000, + "non_witness_utxo": "02000000017ca831f3d48909fe68cb0eaaaa11f8773dee7e8c73085337590e24bb06390db40000000000ffffffff01a08601000000000017a914d8e6688714eeed0aa2edd489aa1ca3c171701e4d8700000000", + "sequence": 4294967294, + "signed": false + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_IN_SP_ECDH_SHARE field for input 0 when PSBT_OUT_SCRIPT set for sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBHxAnAAAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0BAwQBAAAAIgYCQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsIAAAAgAEAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghAuV6etywGNj7MVToDBS5elCZS0I1aEHqn8ExcGFEAOY3Ih4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhA9/o4HeILO0WcbkJsYhPUF1MI8kgqROMyhCMmJNTa/Vcymb868WqzAsViY2OjfUSHB/fnoevqBke8zUhiA7srpgABAwgYcwEAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 10000, + "witness_utxo": "10270000000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02e57a7adcb018d8fb3154e80c14b97a50994b42356841ea9fc13170614400e637", + "dleq_proof": "f7fa381de20b3b459c6e426c6213d4175308f2482a44e33284232624d4dafd573299bf3af16ab302c5626363a37d448707f7e7a1ebea0647bccd486203bb2ba6", + "input_index": 1 + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_IN_SP_DLEQ field for input when PSBT_IN_SP_ECDH_SHARE set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbAAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": null, + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_GLOBAL_SP_DLEQ field when PSBT_GLOBAL_SP_ECDH_SHARE set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWwABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEiBgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwgAAACAAAAAAAEQBP7///8AAQMIGHMBAAAAAAABBCJRIDJt9Q/goHt6y3IHC+s7Yy65rRW6aVzB5apqoAe2FG/YAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b" + } + ] + } + }, + { + "description": "ecdh coverage: invalid proof in PSBT_IN_SP_DLEQ field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAbvHreDwFl9ZoVl4787j95b7SgVKV9D4XAVEk1EXcy9rdKkne51lhePEhbl1NuRdP/dpjNON8kDEQjAjOE3TeDwABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "6ef1eb783c0597d668565e3bf3b8fde5bed2815295f43e17015124d445dccbdadd2a49dee7596178f1216e5d4db9174ffdda6334e37c9031108c08ce1374de0f", + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: invalid proof in PSBT_GLOBAL_SP_DLEQ field", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIIAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQOfI/UQrlNiKKmoskrQ0ZI+gB8n+CARyozeA0LUjKETQm9iLP+6G0S1DhsRlf7t4Xi0vFL/PkYV0P+EqyCUPldkAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfoIYBAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/RzBEAiAJB3pqpkrBSuehqk/mjAKs4gbJ6scjSQYa52A5ALkibgIgbxQypXj44zCvC5ACVnRryfTlKqLozHkCncjnajwpSwYBIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEDCBhzAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "2430479d2da1310a024d2aab8e4c7b49d76cd51f45ee8127c159b0393a675b6f75c581ebe3d0817ddaf3bb598cfd7a022f1a87ed479664f6b947a47db5941cb2" + } + ] + } + }, + { + "description": "ecdh coverage: missing PSBT_IN_BIP32_DERIVATION field for input when PSBT_IN_SP_DLEQ set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIAkHemqmSsFK56GqT+aMAqziBsnqxyNJBhrnYDkAuSJuAiBvFDKlePjjMK8LkAJWdGvJ9OUqoujMeQKdyOdqPClLBgEBAwQBAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIGHMBAAAAAAABBCJRIDJt9Q/goHt6y3IHC+s7Yy65rRW6aVzB5apqoAe2FG/YAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: output 1 missing ECDH share for scan key with one input / three sp outputs (different scan keys)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEDAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR9ADQMAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdArlFYVfpRu8H90tU+gm+Sn6Tjcr7+ye0s3LFc8BZ8SqwIQLChkR0sISqyB9uMKxe30ZnOTzmBNgI0/SZoJhOi1JJdCIdA2UdLAc/ywKk2C3aU/HVAdd6A1BmZ5hUZHOyEmts0afeIQN2iSMnxOeAru7UzhvJK7ASK3t5ENlvCtUUi0lvnD6S+yIeArlFYVfpRu8H90tU+gm+Sn6Tjcr7+ye0s3LFc8BZ8SqwQPCxHVHD/ROzjgpsjvwu4Mpxdzr2dLMVHmiSFJJcS1fIFojJ4Hi3UIhabKsoa1d+k6JJfrSWNrGvG2vILnyRElEiHgNlHSwHP8sCpNgt2lPx1QHXegNQZmeYVGRzshJrbNGn3kCY8UrEWvZFmrDITUfaU2TPsUT/kL2scy+cq8bbAA3PGpXjsx97cKBCmKUbb9RRbIHXFsmVT4rxDD1ZgJruK704AAEDCJBfAQAAAAAAAQQiUSDTlxRqgoZbVOsSuepx1oJP6rD5KkgTf3p5OmhZazNnpAEJQgNlHSwHP8sCpNgt2lPx1QHXegNQZmeYVGRzshJrbNGn3gPycOClYyGDtqsBB06HfmgA2wD1x8XS+vu2j5wo1IVFJAABAwgQJwAAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUIDUteMQTkAMq2RgWppf8dA2OuQnPBNcIhSZLBR8jheJewC4MTBh7chX5l8hIWnW0WRtsKEd7laESO5lpItMVSGC/gAAQMIIE4AAAAAAAABBCJRINLSQezkGFFKoY45kuyVPE1K1exwSmk+IQa6IsOwWnqDAQlCArlFYVfpRu8H90tU+gm+Sn6Tjcr7+ye0s3LFc8BZ8SqwAhJoHj8J6uOKjy8/7NfdVvxYbPoCjoqzP3KpD4QKbSJcAA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de", + "ecdh_share": "0376892327c4e780aeeed4ce1bc92bb0122b7b7910d96f0ad5148b496f9c3e92fb", + "dleq_proof": "98f14ac45af6459ab0c84d47da5364cfb144ff90bdac732f9cabc6db000dcf1a95e3b31f7b70a04298a51b6fd4516c81d716c9954f8af10c3d59809aee2bbd38", + "input_index": 0 + }, + { + "scan_key": "02b9456157e946ef07f74b54fa09be4a7e938dcafbfb27b4b372c573c059f12ab0", + "ecdh_share": "02c2864474b084aac81f6e30ac5edf4667393ce604d808d3f499a0984e8b524974", + "dleq_proof": "f0b11d51c3fd13b38e0a6c8efc2ee0ca71773af674b3151e689214925c4b57c81688c9e078b750885a6cab286b577e93a2497eb49636b1af1b6bc82e7c911251", + "input_index": 0 + } + ] + } + }, + { + "description": "ecdh coverage: input 1 missing ECDH share for output 1 with two inputs / two sp outputs (different scan keys)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsIQMJH9SAORd/RNe3npYWiEnGLrP4eADEZ2fFCo84+Y79FSIdA2UdLAc/ywKk2C3aU/HVAdd6A1BmZ5hUZHOyEmts0afeIQN2iSMnxOeAru7UzhvJK7ASK3t5ENlvCtUUi0lvnD6S+yIeA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsQL4acjrvVeiYG5lZmpvBKzj4froBWMxwZokPImnK3kY7dzQal9KScZZNAcAkHMMi/VOQwpuZFg+CbzKTZYmMvwIiHgNlHSwHP8sCpNgt2lPx1QHXegNQZmeYVGRzshJrbNGn3kCY8UrEWvZFmrDITUfaU2TPsUT/kL2scy+cq8bbAA3PGpXjsx97cKBCmKUbb9RRbIHXFsmVT4rxDD1ZgJruK704AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0BAwQBAAAAIgYCQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsIAAAAgAEAAAABEAT+////Ih0DZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp94hAnUEQAf+LQMNV3wymRzyo+deU1BCRbPL5bMFOufF8iBaIh4DZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp95AOLnjgGtJvr7vxs7gZcKwZv2wCuzs8ei1sqr0fPBBeyma4KJW7Xy2yCcC63E31zv1eSccQoTQBuO1nXCpuWfYiQABAwiQXwEAAAAAAAEEIlEg9Jwu8ZtnBhG4bd7+iQzvlCaKgq4zHkb+GVHWxuNyPLEBCUIDZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp94D8nDgpWMhg7arAQdOh35oANsA9cfF0vr7to+cKNSFRSQAAQMIkF8BAAAAAAABBCJRID2h4XbGT7RZU+xG4FA2NLx+jjQ6do2+77sKPojMSkyaAQlCA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsAuDEwYe3IV+ZfISFp1tFkbbChHe5WhEjuZaSLTFUhgv4AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 100000, + "witness_utxo": "a0860100000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de", + "ecdh_share": "0376892327c4e780aeeed4ce1bc92bb0122b7b7910d96f0ad5148b496f9c3e92fb", + "dleq_proof": "98f14ac45af6459ab0c84d47da5364cfb144ff90bdac732f9cabc6db000dcf1a95e3b31f7b70a04298a51b6fd4516c81d716c9954f8af10c3d59809aee2bbd38", + "input_index": 0 + }, + { + "scan_key": "0352d78c41390032ad91816a697fc740d8eb909cf04d70885264b051f2385e25ec", + "ecdh_share": "03091fd48039177f44d7b79e96168849c62eb3f87800c46767c50a8f38f98efd15", + "dleq_proof": "be1a723aef55e8981b99599a9bc12b38f87eba0158cc7066890f2269cade463b77341a97d29271964d01c0241cc322fd5390c29b99160f826f329365898cbf02", + "input_index": 0 + }, + { + "scan_key": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de", + "ecdh_share": "0275044007fe2d030d577c32991cf2a3e75e53504245b3cbe5b3053ae7c5f2205a", + "dleq_proof": "38b9e3806b49bebeefc6cee065c2b066fdb00aececf1e8b5b2aaf47cf0417b299ae0a256ed7cb6c82702eb7137d73bf579271c4284d006e3b59d70a9b967d889", + "input_index": 1 + } + ] + } + }, + { + "description": "ecdh coverage: input 1 missing ECDH share for scan key with two inputs / one sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR9QwwAAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4gvp1lyDkV+wkkirvfyoyj7ubONQxVT2Xo1Sbl31J6tSIBDwQAAAAAAQEfUMMAAAAAAAAWABRGM9WcrRNDR5ly4MmWkh0KXa22XSIGAkOg1A6d4IgnlBYXUrrkK6kJUnhCCfBjQw8K8lY1SoW7CAAAAIABAAAAARAE/v///wABAwgYcwEAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 50000, + "witness_utxo": "50c3000000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 50000, + "witness_utxo": "50c30000000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ] + } + }, + { + "description": "input eligibility: segwit version greater than 1 in transaction inputs with sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABZSFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIHLzeIUvuENyYHLyUHbw53Vg7UwuBHFm7mpibHW/2znWAiAhKq5fCVdONB9YhvX/8y9XFuq5AwpKU3nmAWtqTULcJAEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "5214229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000165214229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ] + }, + "checks": [ + "input_eligibility" + ] + }, + { + "description": "input eligibility: non-SIGHASH_ALL signature on input with sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIHLzeIUvuENyYHLyUHbw53Vg7UwuBHFm7mpibHW/2znWAiAhKq5fCVdONB9YhvX/8y9XFuq5AwpKU3nmAWtqTULcJAEBAwQCAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ] + } + }, + { + "description": "output scripts: P2TR input with NUMS internal key cannot derive sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASsQJwAAAAAAACJRIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAAQMEAQAAAAEQBP7///8BFyBQkpt0waBJVLeLS2A16XpeB4paDyjsltVHv+6azoA6wCIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIHCUAAAAAAAABBCJRICdoyIruoiMiW6OthOeGNS+YKdLT6TeKOToL/1mvh83QAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "amount": 10000, + "witness_utxo": "102700000000000022512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 9500, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51202768c88aeea223225ba3ad84e786352f9829d2d3e9378a393a0bff59af87cdd0" + } + ] + } + }, + { + "description": "output scripts: PSBT_OUT_SCRIPT does not match derived sp output", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIGHMBAAAAAAABBCJRIM3jnYsFtJb48Y8gsn0K+aMjMwngRyIZ4IE/87XTeH4pAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120cde39d8b05b496f8f18f20b27d0af9a3233309e0472219e0813ff3b5d3787e29" + } + ] + } + }, + { + "description": "output scripts: two sp outputs (same scan / different spend keys) not sorted lexicographically by spend key", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCID9iBcRocGpGJF8XE6u4uLmjp7/YOsdF98ByDUQxtM38AiBxWKv7gg8r9a1nRfVvwHCcmVzMrP4XCY2KobYfjJ/y/wEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwiQXwEAAAAAAAEEIlEgdUe4Fj1bvFSYQL5MwUaq0JUgc2e565RwbeZwjUCPk/kBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgCi4D5lDNIleEbx2x1q254/iwluzMvIXshHEAXvx9nfYYAAQMIECcAAAAAAAABBCJRIGziw1OV8XDyS20tgNCkDEb0a6S0hD9qsxGpNLMcXYTcAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA6r4Yq7amM+SU5r33DN10dpfsGsSHvCh8DGp9zgv1T27AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068028b80f994334895e11bc76c75ab6e78fe2c25bb332f217b211c4017bf1f677d86", + "script": "51207547b8163d5bbc549840be4cc146aad095207367b9eb94706de6708d408f93f9" + }, + { + "output_index": 1, + "amount": 10000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe06803aaf862aeda98cf92539af7dc3375d1da5fb06b121ef0a1f031a9f7382fd53dbb", + "script": "51206ce2c35395f170f24b6d2d80d0a40c46f46ba4b4843f6ab311a934b31c5d84dc" + } + ] + } + }, + { + "description": "output scripts: k values assigned to wrong output indices with three sp outputs (same scan / spend keys)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEDAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIFg36hg2U7tRrGNQq+bDhHvokGogZdwwSNbSCOp8DkyCAiAQ+0/6sv7Vqmy2KAqTxHnPsE5Gyp7fF6owXNFnSiQsSQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwiQXwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkAAQMIECcAAAAAAAABBCJRIJcUQsifBHhuCCcorhFu6rgC8/qelf2w03U6aEAgIAbeAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AAEDCBAnAAAAAAAAAQQiUSA/y66kkIf44D2d79Ory9bejAMrWD+Fpl0FeIHExEPNwAEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + }, + { + "output_index": 1, + "amount": 10000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120971442c89f04786e082728ae116eeab802f3fa9e95fdb0d3753a6840202006de" + }, + { + "output_index": 2, + "amount": 10000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51203fcbaea49087f8e03d9defd3abcbd6de8c032b583f85a65d057881c4c443cdc0" + } + ] + } + } + ], + "valid": [ + { + "description": "can finalize: one P2PKH input single-signer", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiBSJ0jrF3ZNKMpJSBXsjUnn0w1SvHNCLHyG63TjlwVylAEPBAAAAAABAFUCAAAAAfTCEtWu0ef2/2M/LOCcZHxXvt2TAxTZjed1A9WOlAszAAAAAAD/////AaCGAQAAAAAAGXapFB4q14ctMpQTpW3wlovjOCIngxY7iKwAAAAAIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIDnBDcvHz0XG2UNW/1DBK42GqVUM8DcXPZzr94cU5nx1AiBxlVpC7SBTJDIHI8TwFCXc6J9CX4NwKEy0J2z9tt6jrAEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwgYcwEAAAAAAAEEIlEg4UDSh7RbRs1OqvpDdwYVcLq+g9G4vJUKdKn+oJIz16YBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "522748eb17764d28ca494815ec8d49e7d30d52bc73422c7c86eb74e397057294", + "prevout_index": 0, + "prevout_scriptpubkey": "76a9141e2ad7872d329413a56df0968be338222783163b88ac", + "amount": 100000, + "non_witness_utxo": "0200000001f4c212d5aed1e7f6ff633f2ce09c647c57bedd930314d98de77503d58e940b330000000000ffffffff01a0860100000000001976a9141e2ad7872d329413a56df0968be338222783163b88ac00000000", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120e140d287b45b46cd4eaafa4377061570babe83d1b8bc950a74a9fea09233d7a6" + } + ] + } + }, + { + "description": "can finalize: two inputs single-signer using global ECDH share", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQLTJicjNSYH6E/K92UfjS1jfgIK6obfrYDTBs1PSM/MYiIIAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQMZTtx+UaBipW9w58JGERbyQOgh2tzgGDp5dsdzsMKvfVOC18XhXP1EZ3nGqY60ZtEnjvLZUY5Nsi67qTZFzkrkAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfoIYBAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/RzBEAiAXBfzaBiZu2zKxaYZ3s39nu25F1JLr/OZWqIDufxOOtgIgPc3jI53ZBjavZPskEspB45fJWKAwd3z/nIlAucd310wBIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAUE5NQEXxXeUGphYArue3vwROTK8ciAgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3EcwRAIgA4mx4K7v1/q2Ce+rAYv7AWrP8hu4EPPEjDi6ckQYDpUCIBUP7GDiVvQMR1dN/RKaCOr3rB2Xq+4x9AaZ+Hk1RhnzASIGAvW1n6XkkiIev1W6eK1EJgW+rpUWa6HroyUNC7qsfi7cCAAAAIABAAAAARAE/v///wABAwgYcwEAAAAAAAEEIlEgEz1qT1CQlbV5T3SR+8PxLDxFvC2bYK8luLunBMgHKq8BCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 100000, + "witness_utxo": "a086010000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02d3262723352607e84fcaf7651f8d2d637e020aea86dfad80d306cd4f48cfcc62", + "dleq_proof": "c653b71f946818a95bdc39f0918445bc903a0876b738060e9e5db1dcec30abdf54e0b5f178573f5119de71aa63ad19b449e3bcb65463936c8baeea4d917392b9" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120133d6a4f509095b5794f7491fbc3f12c3c45bc2d9b60af25b8bba704c8072aaf" + } + ] + } + }, + { + "description": "can finalize: two inputs single-signer using per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIBcF/NoGJm7bMrFphnezf2e7bkXUkuv85laogO5/E462AiA9zeMjndkGNq9k+yQSykHjl8lYoDB3fP+ciUC5x3fXTAEiBgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwgAAACAAAAAAAEQBP7///8iHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCED7KT/Ebco4uD2DOYiKUOm/1W52V9ie/mpnQhLyHLVClsiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaECKE7OYVUX3K9bolxSuuQmz41SoQqm7i1bNDt7SHfihmVmLMSKKSeC9fpXOEFP3xbKKy1Q6aHB2AOPOiYIu4yAhAAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAUE5NQEXxXeUGphYArue3vwROTK8ciAgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3EcwRAIgA4mx4K7v1/q2Ce+rAYv7AWrP8hu4EPPEjDi6ckQYDpUCIBUP7GDiVvQMR1dN/RKaCOr3rB2Xq+4x9AaZ+Hk1RhnzAQEDBAEAAAAiBgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3AgAAACAAQAAAAEQBP7///8iHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCEDNNLO907e1EZCJ2B/xZrB3oDBKiQzpWJxUo7cz1D6bCsiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaEA3wAwLQqBz9rZfDEtuVuBndLOAwd894otkCxIe4LO0z3hQ30VuC12OGXXd/yUmLIOB4g+FLNhwveP2qDKmMlvMAAEDCBhzAQAAAAAAAQQiUSATPWpPUJCVtXlPdJH7w/EsPEW8LZtgryW4u6cEyAcqrwEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 100000, + "witness_utxo": "a086010000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + }, + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "0334d2cef74eded4464227607fc59ac1de80c12a2433a56271528edccf50fa6c2b", + "dleq_proof": "37c00c0b42a073f6b65f0c4b6e56e06774b380c1df3de28b640b121ee0b3b4cf7850df456e0b5d8e1975ddff25262c8381e20f852cd870bde3f6a832a6325bcc", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120133d6a4f509095b5794f7491fbc3f12c3c45bc2d9b60af25b8bba704c8072aaf" + } + ] + } + }, + { + "description": "can finalize: two inputs / two sp outputs with mixed global and per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQECAQYBACIHAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQNmz8f9zgbgjtSbGzlSuNFRqPfgdf5fbdWN1o+dre7KlCIIAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQF6riwhtSFD+nRyflhqR+Ze2HKjQhQr8BqeL3zx0nvVAUOaStT+1sIbl54eT8B5203R/ieMXen/v9vjBYX8bBuMAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfQA0DAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/SDBFAiEA1k6e8CREAEbpyjkwLrh7LfYN/FHJwKkX1oLzz4EFkIwCIB6+EqXx/SGW8u+11Tr+V850W5Af6ImXitWa7+FKT1A3AQEDBAEAAAAiBgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwgAAACAAAAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECED+9Fz2cSNWMUCJCbQUni8oXuajiKCZmkBobALngS3mUkiHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEEDAXaQQSHz67DeP6+avUAXjrmyGI2gYsOVbIUAX61Bz7e9AF5V1tdzaqQY0kayLg/0GJNQXek0t1JlMRkDWuMzeAAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH0ANAwAAAAAAFgAUE5NQEXxXeUGphYArue3vwROTK8ciAgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3EcwRAIgWoJ5pshwRPECyFkwyJ7C/wGUKGJBraOMeFTy6DZR1fICICvMZkqflwTVcxLbwryDaGcIDyLEdYuR27ktF6HkEeqpAQEDBAEAAAAiBgL1tZ+l5JIiHr9VunitRCYFvq6VFmuh66MlDQu6rH4u3AgAAACAAQAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECEDpqibG5hqmVUz4XvtZFALVBuw1zkHHv4q4rfvsKDKa88iHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEEDDSmopQLvppzVsHMb/41jdREHbWIl8upnCEut1KF59oeyhHHksl6OASTNFqyJN6dlMKzbuhSauI1KBcaMjHeczAAEDCJBfAQAAAAAAAQQiUSCA65gxi89t1hGsF6lksJFzbATK6t8ubodOXO+Gx8YFFwEJQgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKAMSZlgePoQcu5TeYupzVYOiMsTIiPpu3vLsL4cHifwd1AABAwiQXwEAAAAAAAEEIlEgELYvxakKvKAWeWHb/0zq6aoyTvssfyRmJ+i1oz3jfIQBCUICqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRACSkjXbiDUC89fLhpcP/6z3CqLO4jXmkhtVMnZ6qj6GI4A", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 200000, + "witness_utxo": "400d030000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "0366cfc7fdce06e08ed49b1b3952b8d151a8f7e075fe5f6dd58dd68f9dadeeca94", + "dleq_proof": "5eab8b086d4850fe9d1c9f961a91f997b61ca8d0850afc06a78bdf3c749ef54050e692b53fb5b086e5e78793f01e76d3747f89e3177a7feff6f8c1617f1b06e3" + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03fbd173d9c48d58c5022426d05278bca17b9a8e2282666901a1b00b9e04b79949", + "dleq_proof": "c05da410487cfaec378febe6af5005e3ae6c86236818b0e55b214017eb5073edef40179575b5dcdaa9063491ac8b83fd0624d4177a4d2dd4994c4640d6b8ccde", + "input_index": 0 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03a6a89b1b986a995533e17bed64500b541bb0d739071efe2ae2b7efb0a0ca6bcf", + "dleq_proof": "c34a6a2940bbe9a7356c1cc6ffe358dd4441db58897cba99c212eb75285e7da1eca11c792c97a380493345ab224de9d94c2b36ee8526ae23528171a3231de733", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "512080eb98318bcf6dd611ac17a964b091736c04caeadf2e6e874e5cef86c7c60517" + }, + { + "output_index": 1, + "amount": 90000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "512010b62fc5a90abca0167961dbff4ceae9aa324efb2c7f246627e8b5a33de37c84" + } + ] + } + }, + { + "description": "can finalize: one input / one sp output with both global and per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBACIHAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQMKGLm7QZm4kzDrF/juWseG4IU9zS88zfF0S5fOTt4FPiIIAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQCf7kMXWHHjjEVIA/9GD0o0KtIOAYslUGTTGQW1CZ84xvUKH6TWUUsCBevDvTVDP9XhDaw0o+e2PIbeaRHZAgI4AAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfQA0DAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/RzBEAiA3eo+fWjPY+iVcM7OSJtO88CKd+tlgRw74AWpxQvIzWAIgCBvWlIh63us2Ia9aLeTw05yxNpLQQb99vM00oysFQekBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQMKGLm7QZm4kzDrF/juWseG4IU9zS88zfF0S5fOTt4FPiIeAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQAATg9RmG+hyXvKQSPSMkR+J9fNDF9fAwKzGDvZfG1K//3qW2DObYl34Orq1eqJz7c2aNceU2suxyGEXG7JnB18AAQMIkF8BAAAAAAABBCJRINxNRNnjYeXnQ2V5x44lklVB4in9wVOtWYmtiuawYkx4AQlCAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oAxJmWB4+hBy7lN5i6nNVg6IyxMiI+m7e8uwvhweJ/B3UAA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "030a18b9bb4199b89330eb17f8ee5ac786e0853dcd2f3ccdf1744b97ce4ede053e", + "dleq_proof": "27fb90c5d61c78e3115200ffd183d28d0ab4838062c9541934c6416d4267ce31bd4287e9359452c0817af0ef4d50cff578436b0d28f9ed8f21b79a447640808e" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "5120dc4d44d9e361e5e7436579c78e25925541e229fdc153ad5989ad8ae6b0624c78" + } + ] + } + }, + { + "description": "can finalize: three sp outputs (different scan keys) with multiple global ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEDAQYBACIHAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQLC2d9g7mmlqGYE3ZpigsIu+OLJJ22m5ZWlRj83Gq3uJCIHAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQLcfS7KxWiwEVF1ujzVSW/0xLbZCUItvMrrjyUvpU+L0CIHA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQJwvvXAapqFX8Y7/yvQvi1wAzyCC2FTQTyNbtpR8lobKSIIAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQOyCQr97WIHO4GBWbH8TzEfkcQyJomRYwgMBswMeSBc7wL6gFoDTN1616AYRqw1Qix5Jq7VxsYeAMqrknwsC99IiCAMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEDqQ0cOXhvjKRtIoLy1P1WVLGSGDnY177pQA8yt7XfdEo4hZpew9SO+IX7Gh1B3l8zOvFzZQ2KvG0bYA2Qr1flAIggDjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxAG9/lTtSioG//mwAJrk49gPxHYWR1r98hJEz+PG0QTfJHQah1UU14ZU+5yhEY5tPlUvOPjKujVS6DM8UBvwy9PwABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR8goQcAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCLg3ekbsHGTj/QlvLhIwW7hrrfzKDP2b9X3JYim55whAIgfn3H3eyoVVwXfr7/yUnqTDNSD+W+7Hun4nJ+NPvUjf0BIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////AAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0iAgJDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFu0gwRQIhAPmSLe+tUsYSCQS6pacAUymDbsHD2uFsoGjAFrJrBNLYAiBw+V8k+cklbkGWlGnDUqz6yOiUMjUC6s0WPJSi42nuHwEiBgJDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFuwgAAACAAQAAAAEQBP7///8AAQMIkF8BAAAAAAABBCJRIKFPr9x+36QEQnF0sSasoOmZMTP+F+JCCurtIO9U/DjcAQlCAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oAxJmWB4+hBy7lN5i6nNVg6IyxMiI+m7e8uwvhweJ/B3UAAEDCJBfAQAAAAAAAQQiUSB99fJJWUQ0FLvv6J0tqJSGSlYotoAzuSPoaGvcw96dlAEJQgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEAJKSNduINQLz18uGlw//rPcKos7iNeaSG1UydnqqPoYjgABAwiQXwEAAAAAAAEEIlEgsvEqf1cMxQV4QPpjk/cfBpABQgiBgYEMNLuIdh2SlQ0BCUIDjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwD+BRzhEyddMkh/xelr0ELYzc0jXr7/3OJG8XLDLJMsMAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 500000, + "witness_utxo": "20a1070000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 100000, + "witness_utxo": "a0860100000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02dc7d2ecac568b0115175ba3cd5496ff4c4b6d909422dbccaeb8f252fa54f8bd0", + "dleq_proof": "ea43470e5e1be3291b48a0bcb53f55952c64860e7635efba5003ccaded77dd128e216697b0f523be217ec687507797cccebc5cd94362af1b46d803642bd5f940" + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02c2d9df60ee69a5a86604dd9a6282c22ef8e2c9276da6e595a5463f371aadee24", + "dleq_proof": "ec8242bf7b5881cee060566c7f13cc47e4710c89a26458c20301b3031e48173bc0bea01680d3375eb5e80611ab0d508b1e49abb571b1878032aae49f0b02f7d2" + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "0270bef5c06a9a855fc63bff2bd0be2d70033c820b6153413c8d6eda51f25a1b29", + "dleq_proof": "1bdfe54ed4a2a06fff9b0009ae4e3d80fc47616475afdf21244cfe3c6d104df24741a875514d78654fb9ca1118e6d3e552f38f8caba3552e8333c501bf0cbd3f" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "5120a14fafdc7edfa404427174b126aca0e9993133fe17e2420aeaed20ef54fc38dc" + }, + { + "output_index": 1, + "amount": 90000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "51207df5f24959443414bbefe89d2da894864a5628b68033b923e8686bdcc3de9d94" + }, + { + "output_index": 2, + "amount": 90000, + "sp_v0_info": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c03f81473844c9d74c921ff17a5af410b6337348d7afbff73891bc5cb0cb24cb0c0", + "script": "5120b2f12a7f570cc5057840fa6393f71f06900142088181810c34bb88761d92950d" + } + ] + } + }, + { + "description": "can finalize: one P2WPKH input / two mixed outputs - labeled sp output and BIP 32 change", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR8goQcAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCspcmETsmSLIHmcIF+My2i3RXSxdOpRiq21l5TtoKJXgIgTfMs6jgucsQ8BYQE/a5wWVHmuj3jPYM8g6lbsBg4CscBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIoIYBAAAAAAABBCJRICJoz1KZWfMKyPelRxykxnU2pR8OWxaK8D7tRxXtzHLgAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA+qPIovitxbaze/LKBAHKHTwCkdvXAe9BRkXEmzGSz3vAQoEAQAAAAABAwhw8wUAAAAAAAEEFgAUOMGPNd2AY+OApw/+tPmP9O80gXciAgLIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvwwAAAAAAAAAAAAAAAEA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 500000, + "witness_utxo": "20a1070000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "sp_v0_label": 1, + "script": "51202268cf529959f30ac8f7a5471ca4c67536a51f0e5b168af03eed4715edcc72e0" + }, + { + "output_index": 1, + "amount": 390000, + "script": "001438c18f35dd8063e380a70ffeb4f98ff4ef348177" + } + ] + } + }, + { + "description": "can finalize: one input / two sp outputs - output 0 has no label / output 1 uses label=0 convention for sp change", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR/gkwQAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCICJa45kPVF365uoUKpN0arSqgSfjyBNY63wha08anrtiAiBl42BSGRArJQdeOVXz80zjvPKjXR4QFd1dw7/tPs81qQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0CXw7Bs42ZhKfrYvFb5AMCBBDxQu301p42P1YVr9Kmts0hA84eNnXZDZkmSfqd2xH5AlSbs7AvVvbonMCgTIuMN92UIh0Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWghAlWBv6pzMVb2uY5xom9w/XazZ4ZJ9isfxed7jpq45hBQIh4CXw7Bs42ZhKfrYvFb5AMCBBDxQu301p42P1YVr9Kmts1ADolhPR9rJTbbj6kkIA2zpvjvyhciVWTVaxEuUIZbTTAuBY+m9hwp4jZqSnJp1FVaXGHWVVnb+/rJ4urf5TY31SIeAot5DgmH/enw0SOY62vjDHN2eTxkeGWioC8u/JYNlmVoQHlAgZ626fJYwVg4fApkzBb1F8fvRAkSLM6qlPTi/1N2xCR7/BcNmyY7Dj0ga2aMh+lLNN7q+gCheSsEeteIdbsAAQMIoIYBAAAAAAABBCJRIKBIDZZnnBk25au9SLqw4B/6yx7n0cnhaC8VQBaZZYXZAQlCAot5DgmH/enw0SOY62vjDHN2eTxkeGWioC8u/JYNlmVoA7RDRzNcY/UF5V4mirKA8xi0pqoUSS/zecNmih9U74ybAAEDCDDmAgAAAAAAAQQiUSB2Hb00kpiEpmcp69zbrPFnSC32LBTwmOTvGNcP4Grv0AEJQgJfDsGzjZmEp+ti8VvkAwIEEPFC7fTWnjY/VhWv0qa2zQK+EFEqlVNkHkB62OUJo0GEUqkC6FVqDboumD9eAPe8UQEKBAAAAAAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 300000, + "witness_utxo": "e093040000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "025f0ec1b38d9984a7eb62f15be403020410f142edf4d69e363f5615afd2a6b6cd", + "ecdh_share": "03ce1e3675d90d992649fa9ddb11f902549bb3b02f56f6e89cc0a04c8b8c37dd94", + "dleq_proof": "0e89613d1f6b2536db8fa924200db3a6f8efca17225564d56b112e50865b4d302e058fa6f61c29e2366a4a7269d4555a5c61d65559dbfbfac9e2eadfe53637d5", + "input_index": 0 + }, + { + "scan_key": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d966568", + "ecdh_share": "025581bfaa733156f6b98e71a26f70fd76b3678649f62b1fc5e77b8e9ab8e61050", + "dleq_proof": "7940819eb6e9f258c158387c0a64cc16f517c7ef4409122cceaa94f4e2ff5376c4247bfc170d9b263b0e3d206b668c87e94b34deeafa00a1792b047ad78875bb", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", + "script": "5120a0480d96679c1936e5abbd48bab0e01ffacb1ee7d1c9e1682f154016996585d9" + }, + { + "output_index": 1, + "amount": 190000, + "sp_v0_info": "025f0ec1b38d9984a7eb62f15be403020410f142edf4d69e363f5615afd2a6b6cd037cd462358c69c40525247a8793944d9e67484b248fd84a1e7ee4f920b4da41b5", + "sp_v0_label": 0, + "script": "5120761dbd34929884a66729ebdcdbacf167482df62c14f098e4ef18d70fe06aefd0" + } + ] + } + }, + { + "description": "can finalize: two sp outputs - output 0 uses label=3 / output 1 uses label=1", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR/gkwQAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCICJa45kPVF365uoUKpN0arSqgSfjyBNY63wha08anrtiAiBl42BSGRArJQdeOVXz80zjvPKjXR4QFd1dw7/tPs81qQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWghAlWBv6pzMVb2uY5xom9w/XazZ4ZJ9isfxed7jpq45hBQIh4Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWhAeUCBnrbp8ljBWDh8CmTMFvUXx+9ECRIszqqU9OL/U3bEJHv8Fw2bJjsOPSBrZoyH6Us03ur6AKF5KwR614h1uwABAwighgEAAAAAAAEEIlEgyvcy9B51UZCclKM10VeOp7dmAnuIbN0cFwHZv8a3hIgBCUICi3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWgDliRhtuNd91TQDUhtw53swgJxhFB2YU5cDtLJDyPT6b4BCgQDAAAAAAEDCDDmAgAAAAAAAQQiUSBNB6DTsPYafF7+NsLDxYfItphBhUYAjPgZu9pM4xgSIQEJQgKLeQ4Jh/3p8NEjmOtr4wxzdnk8ZHhloqAvLvyWDZZlaANaVzfUxngmF8xrDElrsrxiVVoyslQkT7ycnANd9GnlqwEKBAEAAAAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 300000, + "witness_utxo": "e093040000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d966568", + "ecdh_share": "025581bfaa733156f6b98e71a26f70fd76b3678649f62b1fc5e77b8e9ab8e61050", + "dleq_proof": "7940819eb6e9f258c158387c0a64cc16f517c7ef4409122cceaa94f4e2ff5376c4247bfc170d9b263b0e3d206b668c87e94b34deeafa00a1792b047ad78875bb", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", + "sp_v0_label": 3, + "script": "5120caf732f41e7551909c94a335d1578ea7b766027b886cdd1c1701d9bfc6b78488" + }, + { + "output_index": 1, + "amount": 190000, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", + "sp_v0_label": 1, + "script": "51204d07a0d3b0f61a7c5efe36c2c3c587c8b698418546008cf819bbda4ce3181221" + } + ] + } + }, + { + "description": "can finalize: two inputs using per-input ECDH shares - only eligible inputs contribute shares (P2SH excluded)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCLTLcGPIp7P5Ia4ABZbNd4jlRA4dY+8e4bzsjCrJQMogIgNw2OmoWgzI3SWwuwgfMaotzFugoiSOqGCoFlHKNKDGgBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4g0iImLyaHKbt8hMVYEppdaYYYXO9TXsFJwN9g3UqSwgQBDwQAAAAAAQBTAgAAAAFA5vqmEmjxehAQHWJVNYKIKuSFi+4TNj24D98oH4Jf/AAAAAAA/////wHwSQIAAAAAABepFPRfjMomjsJuS76JkXDxOCUNfPVehwAAAAABBEdSIQKHfKAvFEBZvYLQDhs5muN094pSzvehyjfyjXiLNA0veSEDTIekSHL14fAG002IoAlZKCvUU150d8PWjDFlmR6hs5ZSrgEQBP7///8iHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCECYototkRuaocCWXPZOvsZTkIKEPOhzLOK3YLUZKiUpCgiHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEDCJBfAQAAAAAAAQQiUSAybfUP4KB7estyBwvrO2Muua0VumlcweWqaqAHthRv2AEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "", + "public_key": "", + "prevout_txid": "d222262f268729bb7c84c558129a5d6986185cef535ec149c0df60dd4a92c204", + "prevout_index": 0, + "prevout_scriptpubkey": "a914f45f8cca268ec26e4bbe899170f138250d7cf55e87", + "amount": 150000, + "non_witness_utxo": "020000000140e6faa61268f17a10101d62553582882ae4858bee13363db80fdf281f825ffc0000000000ffffffff01f04902000000000017a914f45f8cca268ec26e4bbe899170f138250d7cf55e8700000000", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + } + ] + } + }, + { + "description": "can finalize: two inputs using global ECDH share - only eligible inputs contribute shares (P2SH excluded)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBACIHAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIIAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQCQwR50toTEKAk0qq45Me0nXbNUfRe6BJ8FZsDk6Z1tvdcWB6+PQgX3a87tZjP16Ai8ah+1HlmT2uUekfbWUHLIAAQ4gGKcXZjsLqxSxKhp3EyP/HkB53VMuXdE+KOoQgccAmEoBDwQAAAAAAQEfoIYBAAAAAAAWABQimnLTSmRb00lru/ULu4HJBj9PlCICAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/SDBFAiEAi0y3BjyKez+SGuAAWWzXeI5UQOHWPvHuG87IwqyUDKICIDcNjpqFoMyN0lsLsIHzGqLcxboKIkjqhgqBZRyjSgxoASIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///wABDiDSIiYvJocpu3yExVgSml1phhhc71NewUnA32DdSpLCBAEPBAAAAAABAFMCAAAAAUDm+qYSaPF6EBAdYlU1gogq5IWL7hM2PbgP3ygfgl/8AAAAAAD/////AfBJAgAAAAAAF6kU9F+MyiaOwm5LvomRcPE4JQ189V6HAAAAAAEER1IhAod8oC8UQFm9gtAOGzma43T3ilLO96HKN/KNeIs0DS95IQNMh6RIcvXh8AbTTYigCVkoK9RTXnR3w9aMMWWZHqGzllKuARAE/v///wABAwiQXwEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "", + "public_key": "", + "prevout_txid": "d222262f268729bb7c84c558129a5d6986185cef535ec149c0df60dd4a92c204", + "prevout_index": 0, + "prevout_scriptpubkey": "a914f45f8cca268ec26e4bbe899170f138250d7cf55e87", + "amount": 150000, + "non_witness_utxo": "020000000140e6faa61268f17a10101d62553582882ae4858bee13363db80fdf281f825ffc0000000000ffffffff01f04902000000000017a914f45f8cca268ec26e4bbe899170f138250d7cf55e8700000000", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "2430479d2da1310a024d2aab8e4c7b49d76cd51f45ee8127c159b0393a675b6f75c581ebe3d0817ddaf3bb598cfd7a022f1a87ed479664f6b947a47db5941cb2" + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + } + ] + } + }, + { + "description": "can finalize: two mixed input types - only eligible inputs contribute ECDH shares (NUMS internal key excluded)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASsQJwAAAAAAACJRIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAARAE/v///wEXIFCSm3TBoElUt4tLYDXpel4HiloPKOyW1Ue/7prOgDrAAAEOIL6dZcg5FfsJJIq738qMo+7mzjUMVU9l6NUm5d9SerUiAQ8EAAAAAAEBH6CGAQAAAAAAFgAURjPVnK0TQ0eZcuDJlpIdCl2ttl0iAgJDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFu0gwRQIhAPRhsN/2Q/wKVsFSIUU2OBYiEkh+UGpZ4GnXXtHwq4T2AiBSxx6U6TEuqKkAH80mNyvnCZfdwqpinFqBFtxR9bfpogEBAwQBAAAAIgYCQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsIAAAAgAEAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghAuV6etywGNj7MVToDBS5elCZS0I1aEHqn8ExcGFEAOY3Ih4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhA9/o4HeILO0WcbkJsYhPUF1MI8kgqROMyhCMmJNTa/Vcymb868WqzAsViY2OjfUSHB/fnoevqBke8zUhiA7srpgABAwgcJQAAAAAAAAEEIlEgBXZyJSSDmoybydLZc46Yu0DdLsUCO9x9p/cGI818hfsBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "amount": 10000, + "witness_utxo": "102700000000000022512050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 100000, + "witness_utxo": "a0860100000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02e57a7adcb018d8fb3154e80c14b97a50994b42356841ea9fc13170614400e637", + "dleq_proof": "f7fa381de20b3b459c6e426c6213d4175308f2482a44e33284232624d4dafd573299bf3af16ab302c5626363a37d448707f7e7a1ebea0647bccd486203bb2ba6", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 9500, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51200576722524839a8c9bc9d2d9738e98bb40dd2ec5023bdc7da7f70623cd7c85fb" + } + ] + } + }, + { + "description": "can finalize: three sp outputs (same scan key) - each output has distinct k value", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEEAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+AGgYAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIA3+9XB4r8pV2yK0ze42yxF/h2HkeAgptCHRBkisr62PAiBk5vWJD0/QW3emSnGDHJiGTZyYTJ7x/r6afFC08VkHIQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GghA+yk/xG3KOLg9gzmIilDpv9VudlfYnv5qZ0IS8hy1QpbIh4Cekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GhAihOzmFVF9yvW6JcUrrkJs+NUqEKpu4tWzQ7e0h34oZlZizEiikngvX6VzhBT98WyistUOmhwdgDjzomCLuMgIQABAwighgEAAAAAAAEEIlEgMm31D+Cge3rLcgcL6ztjLrmtFbppXMHlqmqgB7YUb9gBCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkAAQMIoIYBAAAAAAABBCJRID/LrqSQh/jgPZ3v06vL1t6MAytYP4WmXQV4gcTEQ83AAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AAEDCKCGAQAAAAAAAQQiUSCXFELInwR4bggnKK4Rbuq4AvP6npX9sNN1OmhAICAG3gEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQABAwiQXwEAAAAAAAEEIlEgee9WiKP6f+/DkidFWx8cLwjHAIdCTWoA+EwxQhrKjaQA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 400000, + "witness_utxo": "801a060000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + }, + { + "output_index": 1, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51203fcbaea49087f8e03d9defd3abcbd6de8c032b583f85a65d057881c4c443cdc0" + }, + { + "output_index": 2, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120971442c89f04786e082728ae116eeab802f3fa9e95fdb0d3753a6840202006de" + }, + { + "output_index": 3, + "amount": 90000, + "script": "512079ef5688a3fa7fefc39227455b1f1c2f08c70087424d6a00f84c31421aca8da4" + } + ] + } + }, + { + "description": "can finalize: three sp outputs (same scan key) / two regular outputs - k values assigned independently of output index", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEGAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR8goQcAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQD333KAYwDd2+SyTNcz9TsypkayNsKUYFfeR65QOyqxaAIgTGlAW3e+2vOyB+3ll0m7o9VMRhWoi7025Ugkr3fJ5zEBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQMIoIYBAAAAAAABBCJRIDJt9Q/goHt6y3IHC+s7Yy65rRW6aVzB5apqoAe2FG/YAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AAEDCFDDAAAAAAAAAQQiUSCcaj0jgY6CDtHQz965Y2V8jsMne32bhlx411V/Xe3WogABAwighgEAAAAAAAEEIlEgP8uupJCH+OA9ne/Tq8vW3owDK1g/haZdBXiBxMRDzcABCUICekh/wZ+3aYd7h0LW6hgRjzxOcrHqjG3mAqetSkHb4GgDYeGx6d5eQssgB/fKVLng1X7ROTj61W0/GeV1E6j84DkAAQMIUMMAAAAAAAABBCJRIHnvVoij+n/vw5InRVsfHC8IxwCHQk1qAPhMMUIayo2kAAEDCKCGAQAAAAAAAQQiUSCXFELInwR4bggnKK4Rbuq4AvP6npX9sNN1OmhAICAG3gEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQABAwjILAEAAAAAAAEEIlEgb8UG+AUqn1qKqbmQ1emU9JXd7W1HusRMqs2t2NiZHNAA", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 500000, + "witness_utxo": "20a1070000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120326df50fe0a07b7acb72070beb3b632eb9ad15ba695cc1e5aa6aa007b6146fd8" + }, + { + "output_index": 1, + "amount": 50000, + "script": "51209c6a3d23818e820ed1d0cfdeb963657c8ec3277b7d9b865c78d7557f5dedd6a2" + }, + { + "output_index": 2, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "51203fcbaea49087f8e03d9defd3abcbd6de8c032b583f85a65d057881c4c443cdc0" + }, + { + "output_index": 3, + "amount": 50000, + "script": "512079ef5688a3fa7fefc39227455b1f1c2f08c70087424d6a00f84c31421aca8da4" + }, + { + "output_index": 4, + "amount": 100000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120971442c89f04786e082728ae116eeab802f3fa9e95fdb0d3753a6840202006de" + }, + { + "output_index": 5, + "amount": 77000, + "script": "51206fc506f8052a9f5a8aa9b990d5e994f495dded6d47bac44caacdadd8d8991cd0" + } + ] + } + }, + { + "description": "in progress: two P2TR inputs, neither is signed", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASughgEAAAAAACJRIMgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/AQMEAQAAAAEQBP7///8BFyDIF7t1Ia/DXqlvO/snDm61Dd/6VWBie5Yf7ADymWUIvyIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4gK5A3/0HdF05VIvD0BFw/QENiMq8mANTaXJ6hPMPKtLUBDwQAAAAAAQErECcAAAAAAAAiUSBDoNQOneCIJ5QWF1K65CupCVJ4QgnwY0MPCvJWNUqFuwEDBAEAAAABEAT+////ARcgQ6DUDp3giCeUFhdSuuQrqQlSeEIJ8GNDDwryVjVKhbsiHQJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaCEC5Xp63LAY2PsxVOgMFLl6UJlLQjVoQeqfwTFwYUQA5jciHgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaED3+jgd4gs7RZxuQmxiE9QXUwjySCpE4zKEIyYk1Nr9VzKZvzrxarMCxWJjY6N9RIcH9+eh6+oGR7zNSGIDuyumAAEDCLj5AgAAAAAAAQQiUSDLH766UNBuv/tpC9qdX/Oc4vrbT87clmMu/xukHMDAewEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "5120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "amount": 100000, + "witness_utxo": "a086010000000000225120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "2b9037ff41dd174e5522f0f4045c3f40436232af2600d4da5c9ea13cc3cab4b5", + "prevout_index": 0, + "prevout_scriptpubkey": "512043a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "amount": 10000, + "witness_utxo": "102700000000000022512043a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + }, + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "02e57a7adcb018d8fb3154e80c14b97a50994b42356841ea9fc13170614400e637", + "dleq_proof": "f7fa381de20b3b459c6e426c6213d4175308f2482a44e33284232624d4dafd573299bf3af16ab302c5626363a37d448707f7e7a1ebea0647bccd486203bb2ba6", + "input_index": 1 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 195000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "script": "5120cb1fbeba50d06ebffb690bda9d5ff39ce2fadb4fcedc96632eff1ba41cc0c07b" + } + ] + } + }, + { + "description": "in progress: one P2TR input / one sp output with no ECDH shares when PSBT_OUT_SCRIPT field is not set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEBAQYBAAABDiAT8Qa2S1e1sTdvn2xHGQ9AzAazvty4qrqXhQqz1/Z9AgEPBAAAAAABASughgEAAAAAACJRIMgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/ARAE/v///wEXIMgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/AAEDCBhzAQAAAAAAAQlCAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoA2HhseneXkLLIAf3ylS54NV+0Tk4+tVtPxnldROo/OA5AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "13f106b64b57b5b1376f9f6c47190f40cc06b3bedcb8aaba97850ab3d7f67d02", + "prevout_index": 0, + "prevout_scriptpubkey": "5120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "amount": 100000, + "witness_utxo": "a086010000000000225120c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [], + "outputs": [ + { + "output_index": 0, + "amount": 95000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + } + ] + } + }, + { + "description": "in progress: two inputs / one sp output, input 1 missing ECDH share when PSBT_OUT_SCRIPT field is not set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQIBBQEBAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoIQPspP8Rtyji4PYM5iIpQ6b/VbnZX2J7+amdCEvIctUKWyIeAnpIf8Gft2mHe4dC1uoYEY88TnKx6oxt5gKnrUpB2+BoQIoTs5hVRfcr1uiXFK65CbPjVKhCqbuLVs0O3tId+KGZWYsxIopJ4L1+lc4QU/fFsorLVDpocHYA486Jgi7jICEAAQ4gvp1lyDkV+wkkirvfyoyj7ubONQxVT2Xo1Sbl31J6tSIBDwQAAAAAAQEf8EkCAAAAAAAWABRGM9WcrRNDR5ly4MmWkh0KXa22XSIGAkOg1A6d4IgnlBYXUrrkK6kJUnhCCfBjQw8K8lY1SoW7CAAAAIABAAAAARAE/v///wABAwhADQMAAAAAAAEJQgJ6SH/Bn7dph3uHQtbqGBGPPE5yseqMbeYCp61KQdvgaANh4bHp3l5CyyAH98pUueDVftE5OPrVbT8Z5XUTqPzgOQA=", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 1, + "private_key": "586b0810bbc3c7266971aeb7f8778b41707e092a1aeaf0d953b211f956e35541", + "public_key": "0243a0d40e9de0882794161752bae42ba90952784209f063430f0af256354a85bb", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "00144633d59cad1343479972e0c996921d0a5dadb65d", + "amount": 150000, + "witness_utxo": "f0490200000000001600144633d59cad1343479972e0c996921d0a5dadb65d", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe068", + "ecdh_share": "03eca4ff11b728e2e0f60ce6222943a6ff55b9d95f627bf9a99d084bc872d50a5b", + "dleq_proof": "8a13b3985545f72bd6e89714aeb909b3e354a842a9bb8b56cd0eded21df8a199598b31228a49e0bd7e95ce1053f7c5b28acb543a68707600e3ce89822ee32021", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 200000, + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039" + } + ] + } + }, + { + "description": "in progress: one input / two sp outputs, input 0 missing ECDH share for output 0 when PSBT_OUT_SCRIPT field is not set", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR9ADQMAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsIQMJH9SAORd/RNe3npYWiEnGLrP4eADEZ2fFCo84+Y79FSIeA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsQL4acjrvVeiYG5lZmpvBKzj4froBWMxwZokPImnK3kY7dzQal9KScZZNAcAkHMMi/VOQwpuZFg+CbzKTZYmMvwIAAQMIkF8BAAAAAAABCUIDZR0sBz/LAqTYLdpT8dUB13oDUGZnmFRkc7ISa2zRp94D8nDgpWMhg7arAQdOh35oANsA9cfF0vr7to+cKNSFRSQAAQMIkF8BAAAAAAABBCJRIENYHfbIefmqrcU2YN6xVrnxIr0a7KczIDvOh/zx84buAQlCA1LXjEE5ADKtkYFqaX/HQNjrkJzwTXCIUmSwUfI4XiXsAuDEwYe3IV+ZfISFp1tFkbbChHe5WhEjuZaSLTFUhgv4AA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 200000, + "witness_utxo": "400d030000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": false + } + ], + "sp_proofs": [ + { + "scan_key": "0352d78c41390032ad91816a697fc740d8eb909cf04d70885264b051f2385e25ec", + "ecdh_share": "03091fd48039177f44d7b79e96168849c62eb3f87800c46767c50a8f38f98efd15", + "dleq_proof": "be1a723aef55e8981b99599a9bc12b38f87eba0158cc7066890f2269cade463b77341a97d29271964d01c0241cc322fd5390c29b99160f826f329365898cbf02", + "input_index": 0 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 90000, + "sp_v0_info": "03651d2c073fcb02a4d82dda53f1d501d77a0350666798546473b2126b6cd1a7de03f270e0a5632183b6ab01074e877e6800db00f5c7c5d2fafbb68f9c28d4854524" + }, + { + "output_index": 1, + "amount": 90000, + "sp_v0_info": "0352d78c41390032ad91816a697fc740d8eb909cf04d70885264b051f2385e25ec02e0c4c187b7215f997c8485a75b4591b6c28477b95a1123b996922d3154860bf8", + "script": "512043581df6c879f9aaadc53660deb156b9f122bd1aeca733203bce87fcf1f386ee" + } + ] + } + }, + { + "description": "in progress: large PSBT with nine mixed inputs / six outputs - some inputs signed", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQkBBQEGAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+ghgEAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9IMEUCIQCLNThMVlDzsjgXC0oPt/Sorr8CiDHHC50NMY3FUceJxgIgMZmwKWN19OdCfS+4KMjLfkHblItuYaaIKCeY8x+RIogBAQMEAQAAACIGAsgXu3Uhr8NeqW87+ycObrUN3/pVYGJ7lh/sAPKZZQi/CAAAAIAAAAAAARAE/v///yIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQP70XPZxI1YxQIkJtBSeLyhe5qOIoJmaQGhsAueBLeZSSIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQMKGLm7QZm4kzDrF/juWseG4IU9zS88zfF0S5fOTt4FPiIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQNzctF59PYo2oLMvU5CF72wgJ8UF8mr6qNfqAXUitZVTCIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQMBdpBBIfPrsN4/r5q9QBeOubIYjaBiw5VshQBfrUHPt70AXlXW13NqpBjSRrIuD/QYk1Bd6TS3UmUxGQNa4zN4iHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEAAE4PUZhvocl7ykEj0jJEfifXzQxfXwMCsxg72XxtSv/96ltgzm2Jd+Dq6tXqic+3NmjXHlNrLschhFxuyZwdfIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxApoctTrEZ/ir3CVP3UqIiNrVKO1f3DlEfEL2LQv9Wtaz2NlCTxIDqnxN3Z4SUW1kxisKSfZFariRM+2g/QYijpwABDiC+nWXIORX7CSSKu9/KjKPu5s41DFVPZejVJuXfUnq1IgEPBAAAAAABAR+ghgEAAAAAABYAFBOTUBF8V3lBqYWAK7nt78ETkyvHIgIC9bWfpeSSIh6/Vbp4rUQmBb6ulRZroeujJQ0Luqx+LtxHMEQCIGBe6cE9y5EoaPHeTsHHUaxLRis49eXxBj+EHthVABbpAiBHqaafS5jOmNbyz3jQjoWlVWhRC3tc+65oAn9FjH5+BQEBAwQBAAAAIgYC9bWfpeSSIh6/Vbp4rUQmBb6ulRZroeujJQ0Luqx+LtwIAAAAgAEAAAABEAT+////Ih0CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRAhA6aomxuYaplVM+F77WRQC1QbsNc5Bx7+KuK377CgymvPIh0DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bSghAvaue4qsmg+cnwTqtIQGIXLfLjjZ8vbsMbQEu1ooaqvwIh0DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwhArHT7S+kvDFqCOsUhfp17X3VTKeprhjQxKF9qT8ww4XVIh4CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRBAw0pqKUC76ac1bBzG/+NY3URB21iJfLqZwhLrdShefaHsoRx5LJejgEkzRasiTenZTCs27oUmriNSgXGjIx3nMyIeAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQDNKztgQbrqkKH2spWzKJnlfLSpzl4TPa3NNozcHV/SZ4SDA5AvkBDbNdMTvJpBXBvUvp/pujnZFHoWDeDBRqEMiHgOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobEDYQaNzrSiWuXK4z+oWNP8Uow+A2Ez9bvGIkzl7jzRsgFGcmxYaciRNDptk5UNsmM6d1OF6Byj8mXtce885zstoAAEOINBBx0LKNkRy0aGMeo6uB+bpBKnqG4XxmFJfuD7yduNDAQ8EAAAAAAEBH6CGAQAAAAAAFgAUIwf8d0yhdLQq2oFLwubkRP89gwwiAgJlXvXa8F0o0cpzuo4hEwDbK3BBZzcYPg+ow1XuTBy6xkcwRAIgKTOk1x6k+7U8mzjYjLK+bqIQKQBrzEx5rJgFGKrIYxkCICj9GcotjGhqD9CjsK6gmpqVgMw4oisFIgS+RXXwNa0/AQEDBAEAAAAiBgJlXvXa8F0o0cpzuo4hEwDbK3BBZzcYPg+ow1XuTBy6xggAAACAAgAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECED33Bv8m1nL+UMb1aiyh5xdOdQVckgu9M34afWvygm9twiHQMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKCEDv7hiU765b1i4xGnkQs/Pn3ilQTqVQjvo5oZ2Ty+nvT8iHQOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobCEDhgSqjP8wnd30mx82b4nCaWFlh/xB22CFQPlDFTb2ZG8iHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEED9LowiubZy07ueJtS22i3D8SVFWpaf2OKywbp5EHatcbOMAfkhjD1ndv2JXS8wZ0hJltheJoOQwrQz5sSyGJHxIh4DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bShAUt/78AlfKbHeJMnWjVSdHFS85pAEVP43/RD1yQPf7oX3utinKtqOUs2QkvUdnpC1p+TzbKBN+Y53+60TRzBbxyIeA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsQJPh5dSIi8HP4C7s+p3qFE6thPI6EPwaMBEDweVjk1J7Y0F8bEpa9FNZ/4tuF5r3OXjk1i6GLu3Soob1J45Ms0AAAQ4gUiYs1dQCvzJhbQf8Ayhqj3/nivesQR3/9BZV3cEBHm8BDwQAAAAAAQEfoIYBAAAAAAAWABRwGLzpZSiNi/DM8+DsSLbx27/lSCICAwOg9FKNJk6OlSVmNLlEFOJ0j3KWJwT+V+iwosxE0jg9SDBFAiEA2yW73RvPhQMJZj64j7uw62ETCVeF1/bVM42du8cFAycCIHmV0Cb1bqmeBz39CnlsbnT8Ye0I7NJGcM7zQNrVCueaAQEDBAEAAAAiBgMDoPRSjSZOjpUlZjS5RBTidI9ylicE/lfosKLMRNI4PQgAAACAAwAAAAEQBP7///8iHQKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVECEDgCmZjHPCsTnbdVV+wwEexl7NISRHljAGVxhSo+kiicwiHQMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKCECmAuscmThS/ggzENbaaeN9ybBd+sCbxuBtS3oEScvWOsiHQOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobCEDon2fH6U+Q/cINaaZCVS3ijX3RwZve4K0qeqC6LteBqYiHgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEEA4HOPBrJ/H9qUVNkViUphHaOj70BQHAjn0yxOxZ5PHTMXtZ3yKfqzBc+mI01Oj8onH4hQdt6S0FFxUmeyDfzauIh4DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bShAOOA0Y7ZFDb0FEGWoEqbyYxCH95UeRqyE5m2oEXVXF8CTGA5NSYJ7jAl8pwk/6S05P5Wl162EmkGALcTkVYq96SIeA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsQHMqoJV6nbLHWeOayl0BZ+8T0qNKQ8gS6cydcS1Fj3sihdp1EesW9h+WANIL5OlN/dK8OuN/2UP8NJR9/D/cZ5cAAQ4g4pjq8k/hymi0Hktrg8w9kc7ZnG541bmSz7FXcbVFcGQBDwQAAAAAAQEfoIYBAAAAAAAWABSTMrsCDLA9SetNBHEsHMCOKwbY2CICA6iRG+taSP4RnLAVCIzww6EYzwJnckSJctG3DHZKxZQSRzBEAiBr1hL2RzkhB3F8V6+V7oRzl08sMjurcHnoGRbsuQIcjgIgWlToLfcW0eQKehOGkN9w6tSjzILOEImugKMj+1/ui1UBAQMEAQAAACIGA6iRG+taSP4RnLAVCIzww6EYzwJnckSJctG3DHZKxZQSCAAAAIAEAAAAARAE/v///yIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQK+9HOMbzJ/FySxQHmIlZzfK9GSEIlL/QQ1NbYROCcIGCIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQJ+t8HEr81+Yqtq09K6JzOjdFIgZB+RkD6/57zF29xrXSIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQPKiLzbbR9SkekMddPaYBqL0pmR6rv3+UsvOUg+OFjJoSIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQApYSScQKCC3tsrWvPsDGiOjcxKth9I/HLWoNZvi1f85tqKi/jfNDVPuNH8T5rNSV2my54nwaZy5hViOrC2vEN0iHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEB3sVcjNtm9Ti9zQR649qliNaEw9ETL6wekifRHkh43SJ1x/T2yigu6o1uIw3t1eezb9s4WOHANuB38hr95zRvnIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxAXQ3oon8NUogCzzNUoa9Qe88P7UGdpkHJwIB811Tgevh5C1EccF1fv2MwWrIq/gSX+s8HC1r+r87jppAFR3Z3VgABDiAXMIM+y1vgE2LXFJUiYESEIW4cexjPMT0c8VQ++F3ztgEPBAAAAAABASughgEAAAAAACJRINFVcTxSDOLYJ2FkIV1lmrYjxfcwb4ZSwXVuJoM+iG4BAQMEAQAAAAEQBP7///8BFyDRVXE8Ugzi2CdhZCFdZZq2I8X3MG+GUsF1biaDPohuASIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQJPKhWksVprePCmMvx/3xEpKfZL1N0U9npjz0+48yWiBCIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQOAK2mzYNpFhVErHPlnsYQBAibXXOW3Yud6ASi8sGjMuyIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQJAdhVQXsveqIExJQTu9vRoGiYxbvc9M1N6f9eT9GWOqSIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQI8iPeJ5ht2HkqA9nVtmIyPoaXgw66u2c2OIH+cshkgq5hoJJo0IO4NUz8K601wUjuspGvYi3UYGNGBjMq8bgpciHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEAQOuBcEOQymDdsP+4dxfDQQ4n08gr0k0bUjq0yTWFdbyj8oKMYRFEw6laoYgGbLeDCY32zxBjAsHS1hUiuiyDJIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxA8FWZBzhUTQmbFZKQ5/VELLkSJiYBdKqdjByDELfLAc4rlMxdK4/fgWZ+Avt5bbDlwnh33g1BoqDnCbAURbrylgABDiDpNTLg7aOcTTnaLT+R0UUpvENfwu+5GW4l+6rEtOibgQEPBAAAAAABASughgEAAAAAACJRINAOFftian7+6FxbzmdtY2tgSJFOmjSs4LTLxEvJSAd6AQMEAQAAAAEQBP7///8BFyDQDhX7Ymp+/uhcW85nbWNrYEiRTpo0rOC0y8RLyUgHeiIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQIVP83+lq5wNj/RVCZG0wnCEyuDJVQOPdf4jjf5eWB5piIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQIPqjoMMa52ledpVG4wqngqvTH7aBuXChOqNcwh39E4ACIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQPzE/j0desB487LwOqsL3JtKQMsb/fDrWzDv4uxAYZSfCIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQEMg6fYP08Km9zeqzlRsI7DH126EFZwK0onB30wPilAOUSCfKW+dfSI6HSkr1B0mb6rLG+hDvs9LcG6xXbpwKAYiHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEDkB0puyJPquAbV35p07SV+eF/zeGQ9uqeTPHgTp7X2yQyoK2JTnzmzjCxy/lz4HCKl5kH2c2RPbEhrPbV3RkRjIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxAOynhLPaT8nITn3RFLyvMwrzQdDLc6PsQc1TBRYoSpHyFRNLWyh4zvOBXi2UIz4pZNGc/n8Q1pfhKIqVLWty8/AABDiC2Nou8Mwzd/p4lUO2338Fa50ysm8x9ln/oHm8n+Fl7HQEPBAAAAAABAFMCAAAAAf6hTz0WmysEANsUmWIhsxGCu56GyhaekbnHvZP5xz6IAAAAAAD/////AaCGAQAAAAAAF6kUDfEBNX9z5mWFAM9W86Ey17uK+92HAAAAAAEBIKCGAQAAAAAAF6kUDfEBNX9z5mWFAM9W86Ey17uK+92HIgICBQDxZ89mnn2sOeauzaHyjGa3jscJfu/vJdVPe+eF5/ZHMEQCIA134+usWENCsMr5fopp9YcHxKsAzK2+hsRKoYe1Zfv/AiBubgVutYtmJeL++r0axbRapqEto5ypCKEaIcY396JcMAEBAwQBAAAAAQQWABTGvIEF7G22DE2lOsNc2iBAaekOiiIGAgUA8WfPZp59rDnmrs2h8oxmt47HCX7v7yXVT3vnhef2CAAAAIAHAAAAARAE/v///yIdAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQIQL+eEHN37Rv2yypveVslVjC0bNhIWZWWQgCJOJAMUOECCIdAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oIQJvAnsjWnWTyolDxte3432S6rM5YX9/DDRBBsWN0MvGqiIdA47TIg5zNE+R4tMzkePhaR2zna8G5CN+GX7Q0Ae/JyhsIQIGEUUMbBlE7N7LePYYFsdn+6Q2jyH36bFpd5x28jz0zCIeAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQQIgHJVFmLD7r7lRfGIuNr8aClzovIY58oH/I5C/1ehE0YwRJ/XAxd9ehLQyc7QJIRNwbng7K1nWH3VxHlDGzahoiHgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKEAWtYFI97r/eD5DEPGFUD/pJBfbpDuWbIOiGEMiyx8gQky50ECuahrqTnREvceGJYqXVkTOGgGVVkwrn8PGBqKSIh4DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGxASrNr3WnrHNDUsK6uQD3OTbUijFujopHccl3iM4LRc20mXXvDP4NpClylYJcZ8Z6BlS1MjV4oj9DLjGQREEuAowABDiADvOulN51z2ur+Z3lysD3PfvMLfxDqqDrQS8C/ANiy9wEPBAAAAAABAFUCAAAAASo0rBsNt3qBmMHikALMR7v81u2DCWAwTSyaOrVISGtdAAAAAAD/////AaCGAQAAAAAAGXapFFab8xHmijg3Z7Q/gwQt+r5gCT7eiKwAAAAAIgIDGZ0vFV8y67FnrrDMym5a+3NAiY3xt6uZNlAPbUORkCpHMEQCIGIn/SRuyIcF32NUW0i+PRLD1KVITUtOxDRAmsuEETxaAiA8y8m0eI77f7hg2oVYqUx+TEJa11XoaOLY8Fz7RhJflwEBAwQBAAAAIgYDGZ0vFV8y67FnrrDMym5a+3NAiY3xt6uZNlAPbUORkCoIAAAAgAgAAAABEAT+////Ih0CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRAhAvPBdbCLIHY3YWqnTme6rBoYB8lLznCAD3b1sXtSwAgtIh0DM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bSghAvoc7ZMlmKJ/ooQQuVXqZ0ebWkAztp8smeTiZ2e7oKVWIh0DjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwhAwDg/RcQ3FatfBc6qRQdcxV3AJvD3/y6hXakjnerNNlwIh4CqfH6wsxFdoTzPekAc/4g0iyUfJIWO4YrFNCrod9HFRBAmSJWWP7eeowkN+s4Ib3J03AKvIlui23O+E4pNHt4vCsD5tozg25PQHZzkMNmRM4Bazax1UaWNUD6UdCwQH221yIeAzNh0/9y805rTnp775bluBUeZql5rc51PDnFg8vldm0oQKCUjFF9cJdsDjnKZ6vxnnIXBA2+Fyu5ZTQTPm/6OUHBZZQW9UBmK3Vh77cAZLTXGpsXtjvFY/Yw20Qi8nPg3xIiHgOO0yIOczRPkeLTM5Hj4Wkds52vBuQjfhl+0NAHvycobEDuCqplynPMxUezHCt+0q4xpPTM2upz8q3SRjDWCO3prqXYCGiBhuvKQrh376sT815g8hDU9iBgL4UPwHMcIB6RAAEDCDB1AAAAAAAAAQQiUSAa+l2IGIzvm5Gz8QArEeD56UrTmBl4xNEqBgVWTAgSMwEJQgMzYdP/cvNOa056e++W5bgVHmapea3OdTw5xYPL5XZtKAMSZlgePoQcu5TeYupzVYOiMsTIiPpu3vLsL4cHifwd1AABAwgwdQAAAAAAAAEEIlEgyr9+Sg3kJOybut1/ckLM52c+FwH0nWoZqXBV2jMrEyQBCUIDM2HT/3LzTmtOenvvluW4FR5mqXmtznU8OcWDy+V2bSgDEmZYHj6EHLuU3mLqc1WDojLEyIj6bt7y7C+HB4n8HdQAAQMIIE4AAAAAAAABBCJRIEqtyeClkNIeDBnLICTycUD5sZwqEoLVDqfF9F6wP9SiAQlCAqnx+sLMRXaE8z3pAHP+INIslHySFjuGKxTQq6HfRxUQAkpI124g1AvPXy4aXD/+s9wqizuI15pIbVTJ2eqo+hiOAAEDCCBOAAAAAAAAAQQiUSAvpDcA6h0o/XrU3LMys+XloNjLVCWPZmHE3gN31ygE5QEJQgKp8frCzEV2hPM96QBz/iDSLJR8khY7hisU0Kuh30cVEAJKSNduINQLz18uGlw//rPcKos7iNeaSG1UydnqqPoYjgABAwgQJwAAAAAAAAEEIlEgUZhvruMZQ2tRs4QJwbMA9DluKzLjzJoSripL4TXab24BCUIDjtMiDnM0T5Hi0zOR4+FpHbOdrwbkI34ZftDQB78nKGwD+BRzhEyddMkh/xelr0ELYzc0jXr7/3OJG8XLDLJMsMAAAQMIUMMAAAAAAAABBCJRIG/FBvgFKp9aiqm5kNXplPSV3e1tR7rETKrNrdjYmRzQAA==", + "supplementary": { + "inputs": [ + { + "input_index": 0, + "private_key": "7e31eeeb1aa2597b6d63b357541461d75ddae76b7603d24619f5ebed9e88ec31", + "public_key": "02c817bb7521afc35ea96f3bfb270e6eb50ddffa5560627b961fec00f2996508bf", + "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", + "prevout_index": 0, + "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 100000, + "witness_utxo": "a086010000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 1, + "private_key": "295c2eedddd8331d20b5d4cf9e69bb523ed85cb0bf35ab12e04fea66fe6d4a4a", + "public_key": "02f5b59fa5e492221ebf55ba78ad442605beae95166ba1eba3250d0bbaac7e2edc", + "prevout_txid": "be9d65c83915fb09248abbdfca8ca3eee6ce350c554f65e8d526e5df527ab522", + "prevout_index": 0, + "prevout_scriptpubkey": "0014139350117c577941a985802bb9edefc113932bc7", + "amount": 100000, + "witness_utxo": "a086010000000000160014139350117c577941a985802bb9edefc113932bc7", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 2, + "private_key": "e6db4a04e2fc29745ea1a9dcf1fed7449c8228ffe0503d8171e64126d5399800", + "public_key": "02655ef5daf05d28d1ca73ba8e211300db2b70416737183e0fa8c355ee4c1cbac6", + "prevout_txid": "d041c742ca364472d1a18c7a8eae07e6e904a9ea1b85f198525fb83ef276e343", + "prevout_index": 0, + "prevout_scriptpubkey": "00142307fc774ca174b42ada814bc2e6e444ff3d830c", + "amount": 100000, + "witness_utxo": "a0860100000000001600142307fc774ca174b42ada814bc2e6e444ff3d830c", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 3, + "private_key": "f5208ed67b0019ba4e306aff029d247931e0dfacd1fe6e69091bc6bc70a42619", + "public_key": "0303a0f4528d264e8e95256634b94414e2748f72962704fe57e8b0a2cc44d2383d", + "prevout_txid": "52262cd5d402bf32616d07fc03286a8f7fe78af7ac411dfff41655ddc1011e6f", + "prevout_index": 0, + "prevout_scriptpubkey": "00147018bce965288d8bf0ccf3e0ec48b6f1dbbfe548", + "amount": 100000, + "witness_utxo": "a0860100000000001600147018bce965288d8bf0ccf3e0ec48b6f1dbbfe548", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 4, + "private_key": "0c19c34dc1005feb5ca60104d20472f7f2f97bd4d521264618196bf71155b20a", + "public_key": "03a8911beb5a48fe119cb015088cf0c3a118cf026772448972d1b70c764ac59412", + "prevout_txid": "e298eaf24fe1ca68b41e4b6b83cc3d91ced99c6e78d5b992cfb15771b5457064", + "prevout_index": 0, + "prevout_scriptpubkey": "00149332bb020cb03d49eb4d04712c1cc08e2b06d8d8", + "amount": 100000, + "witness_utxo": "a0860100000000001600149332bb020cb03d49eb4d04712c1cc08e2b06d8d8", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 5, + "private_key": "5aaa151cbdeb566911784d166b01ce57f35993b98f96846efec1cfefcc0d8f56", + "public_key": "02d155713c520ce2d8276164215d659ab623c5f7306f8652c1756e26833e886e01", + "prevout_txid": "1730833ecb5be01362d7149522604484216e1c7b18cf313d1cf1543ef85df3b6", + "prevout_index": 0, + "prevout_scriptpubkey": "5120d155713c520ce2d8276164215d659ab623c5f7306f8652c1756e26833e886e01", + "amount": 100000, + "witness_utxo": "a086010000000000225120d155713c520ce2d8276164215d659ab623c5f7306f8652c1756e26833e886e01", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 6, + "private_key": "d92c1506167b3a00a21f4d90d7b5ccb4713c00194aa66f9c7deb162c467f9643", + "public_key": "02d00e15fb626a7efee85c5bce676d636b6048914e9a34ace0b4cbc44bc948077a", + "prevout_txid": "e93532e0eda39c4d39da2d3f91d14529bc435fc2efb9196e25fbaac4b4e89b81", + "prevout_index": 0, + "prevout_scriptpubkey": "5120d00e15fb626a7efee85c5bce676d636b6048914e9a34ace0b4cbc44bc948077a", + "amount": 100000, + "witness_utxo": "a086010000000000225120d00e15fb626a7efee85c5bce676d636b6048914e9a34ace0b4cbc44bc948077a", + "sequence": 4294967294, + "signed": false + }, + { + "input_index": 7, + "private_key": "d8bc8d0e1233a97133f3573d6dceb6736e8b6d2e4839925145022ea3efdad8f8", + "public_key": "020500f167cf669e7dac39e6aecda1f28c66b78ec7097eefef25d54f7be785e7f6", + "prevout_txid": "b6368bbc330cddfe9e2550edb7dfc15ae74cac9bcc7d967fe81e6f27f8597b1d", + "prevout_index": 0, + "prevout_scriptpubkey": "a9140df101357f73e6658500cf56f3a132d7bb8afbdd87", + "amount": 100000, + "witness_utxo": "a08601000000000017a9140df101357f73e6658500cf56f3a132d7bb8afbdd87", + "sequence": 4294967294, + "signed": true + }, + { + "input_index": 8, + "private_key": "81e30f5b5d049bdf5c5bea9c3329bf57cc8b473240182a9f2a6eee07435bd54a", + "public_key": "03199d2f155f32ebb167aeb0ccca6e5afb7340898df1b7ab9936500f6d4391902a", + "prevout_txid": "03bceba5379d73daeafe677972b03dcf7ef30b7f10eaa83ad04bc0bf00d8b2f7", + "prevout_index": 0, + "prevout_scriptpubkey": "76a914569bf311e68a383767b43f83042dfabe60093ede88ac", + "amount": 100000, + "non_witness_utxo": "02000000012a34ac1b0db77a8198c1e29002cc47bbfcd6ed830960304d2c9a3ab548486b5d0000000000ffffffff01a0860100000000001976a914569bf311e68a383767b43f83042dfabe60093ede88ac00000000", + "sequence": 4294967294, + "signed": true + } + ], + "sp_proofs": [ + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "030a18b9bb4199b89330eb17f8ee5ac786e0853dcd2f3ccdf1744b97ce4ede053e", + "dleq_proof": "001383d4661be8725ef29048f48c911f89f5f34317d7c0c0acc60ef65f1b52bfff7a96d8339b625df83abab57aa273edcd9a35c794dacbb1c861171bb267075f", + "input_index": 0 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03fbd173d9c48d58c5022426d05278bca17b9a8e2282666901a1b00b9e04b79949", + "dleq_proof": "c05da410487cfaec378febe6af5005e3ae6c86236818b0e55b214017eb5073edef40179575b5dcdaa9063491ac8b83fd0624d4177a4d2dd4994c4640d6b8ccde", + "input_index": 0 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "037372d179f4f628da82ccbd4e4217bdb0809f1417c9abeaa35fa805d48ad6554c", + "dleq_proof": "a6872d4eb119fe2af70953f752a22236b54a3b57f70e511f10bd8b42ff56b5acf6365093c480ea9f13776784945b59318ac2927d915aae244cfb683f4188a3a7", + "input_index": 0 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02f6ae7b8aac9a0f9c9f04eab484062172df2e38d9f2f6ec31b404bb5a286aabf0", + "dleq_proof": "334aced8106ebaa4287daca56cca26795f2d2a739784cf6b734da3370757f499e120c0e40be40436cd74c4ef26905706f52fa7fa6e8e76451e8583783051a843", + "input_index": 1 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03a6a89b1b986a995533e17bed64500b541bb0d739071efe2ae2b7efb0a0ca6bcf", + "dleq_proof": "c34a6a2940bbe9a7356c1cc6ffe358dd4441db58897cba99c212eb75285e7da1eca11c792c97a380493345ab224de9d94c2b36ee8526ae23528171a3231de733", + "input_index": 1 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "02b1d3ed2fa4bc316a08eb1485fa75ed7dd54ca7a9ae18d0c4a17da93f30c385d5", + "dleq_proof": "d841a373ad2896b972b8cfea1634ff14a30f80d84cfd6ef18893397b8f346c80519c9b161a72244d0e9b64e5436c98ce9dd4e17a0728fc997b5c7bcf39cecb68", + "input_index": 1 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "03bfb86253beb96f58b8c469e442cfcf9f78a5413a95423be8e686764f2fa7bd3f", + "dleq_proof": "52dffbf0095f29b1de24c9d68d549d1c54bce6900454fe37fd10f5c903dfee85f7bad8a72ada8e52cd9092f51d9e90b5a7e4f36ca04df98e77fbad1347305bc7", + "input_index": 2 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "03df706ff26d672fe50c6f56a2ca1e7174e75055c920bbd337e1a7d6bf2826f6dc", + "dleq_proof": "fd2e8c22b9b672d3bb9e26d4b6da2dc3f125455a969fd8e2b2c1ba791076ad71b38c01f9218c3d6776fd895d2f3067484996d85e268390c2b433e6c4b21891f1", + "input_index": 2 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "038604aa8cff309dddf49b1f366f89c269616587fc41db608540f9431536f6646f", + "dleq_proof": "93e1e5d4888bc1cfe02eecfa9dea144ead84f23a10fc1a301103c1e56393527b63417c6c4a5af45359ff8b6e179af73978e4d62e862eedd2a286f5278e4cb340", + "input_index": 2 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02980bac7264e14bf820cc435b69a78df726c177eb026f1b81b52de811272f58eb", + "dleq_proof": "38e03463b6450dbd051065a812a6f2631087f7951e46ac84e66da811755717c093180e4d49827b8c097ca7093fe92d393f95a5d7ad849a41802dc4e4558abde9", + "input_index": 3 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "038029998c73c2b139db75557ec3011ec65ecd212447963006571852a3e92289cc", + "dleq_proof": "381ce3c1ac9fc7f6a51536456252984768e8fbd014070239f4cb13b16793c74cc5ed677c8a7eacc173e988d353a3f289c7e2141db7a4b4145c5499ec837f36ae", + "input_index": 3 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "03a27d9f1fa53e43f70835a6990954b78a35f747066f7b82b4a9ea82e8bb5e06a6", + "dleq_proof": "732aa0957a9db2c759e39aca5d0167ef13d2a34a43c812e9cc9d712d458f7b2285da7511eb16f61f9600d20be4e94dfdd2bc3ae37fd943fc34947dfc3fdc6797", + "input_index": 3 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "027eb7c1c4afcd7e62ab6ad3d2ba2733a3745220641f91903ebfe7bcc5dbdc6b5d", + "dleq_proof": "77b1572336d9bd4e2f73411eb8f6a96235a130f444cbeb07a489f447921e37489d71fd3db28a0bbaa35b88c37b7579ecdbf6ce1638700db81dfc86bf79cd1be7", + "input_index": 4 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02bef4738c6f327f1724b1407988959cdf2bd19210894bfd043535b61138270818", + "dleq_proof": "0a584927102820b7b6cad6bcfb031a23a37312ad87d23f1cb5a8359be2d5ff39b6a2a2fe37cd0d53ee347f13e6b3525769b2e789f0699cb985588eac2daf10dd", + "input_index": 4 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "03ca88bcdb6d1f5291e90c75d3da601a8bd29991eabbf7f94b2f39483e3858c9a1", + "dleq_proof": "5d0de8a27f0d528802cf3354a1af507bcf0fed419da641c9c0807cd754e07af8790b511c705d5fbf63305ab22afe0497facf070b5afeafcee3a6900547767756", + "input_index": 4 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "03802b69b360da4585512b1cf967b184010226d75ce5b762e77a0128bcb068ccbb", + "dleq_proof": "103ae05c10e43298376c3fee1dc5f0d04389f4f20af49346d48ead324d615d6f28fca0a318445130ea56a862019b2de0c2637db3c418c0b074b58548ae8b20c9", + "input_index": 5 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "024f2a15a4b15a6b78f0a632fc7fdf112929f64bd4dd14f67a63cf4fb8f325a204", + "dleq_proof": "8f223de27986dd8792a03d9d5b662323e8697830ebabb67363881fe72c86482ae61a09268d083b8354cfc2bad35c148eeb291af622dd460634606332af1b8297", + "input_index": 5 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "02407615505ecbdea881312504eef6f4681a26316ef73d33537a7fd793f4658ea9", + "dleq_proof": "f055990738544d099b159290e7f5442cb91226260174aa9d8c1c8310b7cb01ce2b94cc5d2b8fdf81667e02fb796db0e5c27877de0d41a2a0e709b01445baf296", + "input_index": 5 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "020faa3a0c31ae7695e769546e30aa782abd31fb681b970a13aa35cc21dfd13800", + "dleq_proof": "e4074a6ec893eab806d5df9a74ed257e785ff378643dbaa7933c7813a7b5f6c90ca82b62539f39b38c2c72fe5cf81c22a5e641f673644f6c486b3db577464463", + "input_index": 6 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02153fcdfe96ae70363fd1542646d309c2132b8325540e3dd7f88e37f9796079a6", + "dleq_proof": "4320e9f60fd3c2a6f737aace546c23b0c7d76e84159c0ad289c1df4c0f8a500e51209f296f9d7d223a1d292bd41d266faacb1be843becf4b706eb15dba702806", + "input_index": 6 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "03f313f8f475eb01e3cecbc0eaac2f726d29032c6ff7c3ad6cc3bf8bb10186527c", + "dleq_proof": "3b29e12cf693f272139f74452f2bccc2bcd07432dce8fb107354c1458a12a47c8544d2d6ca1e33bce0578b6508cf8a5934673f9fc435a5f84a22a54b5adcbcfc", + "input_index": 6 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "026f027b235a7593ca8943c6d7b7e37d92eab339617f7f0c344106c58dd0cbc6aa", + "dleq_proof": "16b58148f7baff783e4310f185503fe92417dba43b966c83a2184322cb1f20424cb9d040ae6a1aea4e7444bdc786258a975644ce1a0195564c2b9fc3c606a292", + "input_index": 7 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02fe7841cddfb46fdb2ca9bde56c9558c2d1b36121665659080224e24031438408", + "dleq_proof": "88072551662c3eebee545f188b8dafc682973a2f218e7ca07fc8e42ff57a1134630449fd703177d7a12d0c9ced024844dc1b9e0ecad67587dd5c479431b36a1a", + "input_index": 7 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "020611450c6c1944ecdecb78f61816c767fba4368f21f7e9b169779c76f23cf4cc", + "dleq_proof": "4ab36bdd69eb1cd0d4b0aeae403dce4db5228c5ba3a291dc725de23382d1736d265d7bc33f83690a5ca5609719f19e81952d4c8d5e288fd0cb8c6411104b80a3", + "input_index": 7 + }, + { + "scan_key": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28", + "ecdh_share": "02fa1ced932598a27fa28410b955ea67479b5a4033b69f2c99e4e26767bba0a556", + "dleq_proof": "a0948c517d70976c0e39ca67abf19e7217040dbe172bb96534133e6ffa3941c1659416f540662b7561efb70064b4d71a9b17b63bc563f630db4422f273e0df12", + "input_index": 8 + }, + { + "scan_key": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510", + "ecdh_share": "02f3c175b08b207637616aa74e67baac1a1807c94bce70800f76f5b17b52c0082d", + "dleq_proof": "99225658fede7a8c2437eb3821bdc9d3700abc896e8b6dcef84e29347b78bc2b03e6da33836e4f40767390c36644ce016b36b1d546963540fa51d0b0407db6d7", + "input_index": 8 + }, + { + "scan_key": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c", + "ecdh_share": "0300e0fd1710dc56ad7c173aa9141d731577009bc3dffcba8576a48e77ab34d970", + "dleq_proof": "ee0aaa65ca73ccc547b31c2b7ed2ae31a4f4ccdaea73f2add24630d608ede9aea5d808688186ebca42b877efab13f35e60f210d4f620602f850fc0731c201e91", + "input_index": 8 + } + ], + "outputs": [ + { + "output_index": 0, + "amount": 30000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "51201afa5d88188cef9b91b3f1002b11e0f9e94ad3981978c4d12a0605564c081233" + }, + { + "output_index": 1, + "amount": 30000, + "sp_v0_info": "033361d3ff72f34e6b4e7a7bef96e5b8151e66a979adce753c39c583cbe5766d28031266581e3e841cbb94de62ea735583a232c4c888fa6edef2ec2f870789fc1dd4", + "script": "5120cabf7e4a0de424ec9bbadd7f7242cce7673e1701f49d6a19a97055da332b1324" + }, + { + "output_index": 2, + "amount": 20000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "51204aadc9e0a590d21e0c19cb2024f27140f9b19c2a1282d50ea7c5f45eb03fd4a2" + }, + { + "output_index": 3, + "amount": 20000, + "sp_v0_info": "02a9f1fac2cc457684f33de90073fe20d22c947c92163b862b14d0aba1df471510024a48d76e20d40bcf5f2e1a5c3ffeb3dc2a8b3b88d79a486d54c9d9eaa8fa188e", + "script": "51202fa43700ea1d28fd7ad4dcb332b3e5e5a0d8cb54258f6661c4de0377d72804e5" + }, + { + "output_index": 4, + "amount": 10000, + "sp_v0_info": "038ed3220e73344f91e2d33391e3e1691db39daf06e4237e197ed0d007bf27286c03f81473844c9d74c921ff17a5af410b6337348d7afbff73891bc5cb0cb24cb0c0", + "script": "512051986faee319436b51b38409c1b300f4396e2b32e3cc9a12ae2a4be135da6f6e" + }, + { + "output_index": 5, + "amount": 50000, + "script": "51206fc506f8052a9f5a8aa9b990d5e994f495dded6d47bac44caacdadd8d8991cd0" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/testing/devtest/unit_silentpayments.py b/testing/devtest/unit_silentpayments.py new file mode 100644 index 00000000..71a1c8f7 --- /dev/null +++ b/testing/devtest/unit_silentpayments.py @@ -0,0 +1,608 @@ +# (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 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 = a2b_hex("a5377d45114b0206f6773e231861ece8c04e13840ab007df6722a3508211c073") +TEST_PRIVKEY2 = a2b_hex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") +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, 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) + +proof = generate_dleq_proof(TEST_PRIVKEY, 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") +try: + assert not verify_dleq_proof(pubkey, TEST_SCAN_KEY, wrong_ecdh, proof) +except Exception as e: + assert isinstance(e, ValueError) + +# Deterministic with same aux_rand +p1 = generate_dleq_proof(TEST_PRIVKEY, TEST_SCAN_KEY, b"\x00" * 32) +p2 = generate_dleq_proof(TEST_PRIVKEY, 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 + + @property + def is_sp_spend(self): + return bool(self.sp_tweak and self.sp_spend_bip32_derivation) + + +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(): + pk = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY) + es = _compute_ecdh_share(TEST_PRIVKEY, TEST_SCAN_KEY) + dp = generate_dleq_proof(TEST_PRIVKEY, TEST_SCAN_KEY, b"\x00" * 32) + return 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) + +# 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} +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) +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, + (MY_XFP, 44, 0, 0, 1): TEST_PRIVKEY2, + } +) +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}) +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) +pub2 = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY2) +share1 = _compute_ecdh_share(TEST_PRIVKEY, TEST_SCAN_KEY) +share2 = _compute_ecdh_share(TEST_PRIVKEY2, 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, 0) +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 cases +# --------------------------------------------------------------------------- + +try: + _combine_pubkeys([]) + assert False, "Should raise ValueError for empty pubkey list" +except ValueError: + pass + +# Intermediate sums should not fail if final sum is non-zero. +A = ngu.secp256k1.ec_pubkey_tweak_mul(G, (1).to_bytes(32, "big")) +neg_A = bytes([0x03 if A[0] == 0x02 else 0x02]) + A[1:] +assert _combine_pubkeys([A, neg_A, A]) == A + +# Final point-at-infinity should still fail in phase 1. +try: + _combine_pubkeys([A, neg_A]) + assert False, "Should raise ValueError for point-at-infinity" +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..35e09397 --- /dev/null +++ b/testing/devtest/verify_sp_outputs.py @@ -0,0 +1,208 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# ruff: noqa: F821 +# +# 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 +# RV (uio.BytesIO) is injected into globals by the simulator's exec namespace. +# +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("utxo_spk"): + inp.utxo_spk = a2b_hex(d["utxo_spk"]) + elif 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 == "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 == "pubkey_from_input": + # 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(' Date: Thu, 9 Apr 2026 21:23:34 -0400 Subject: [PATCH 09/32] BIP-352: add test vectors Add testing/test_bip352_vectors.py and testing/bip352_test_vectors.json - Verify SP sending and receiving test vectors --- testing/bip352_test_vectors.json | 5729 ++++++++++++++++++++++++++ testing/devtest/verify_sp_outputs.py | 40 + testing/test_bip352_vectors.py | 349 ++ 3 files changed, 6118 insertions(+) create mode 100644 testing/bip352_test_vectors.json create mode 100644 testing/test_bip352_vectors.py diff --git a/testing/bip352_test_vectors.json b/testing/bip352_test_vectors.json new file mode 100644 index 00000000..3a189757 --- /dev/null +++ b/testing/bip352_test_vectors.json @@ -0,0 +1,5729 @@ +[ + { + "comment": "Simple send: two inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ], + "shared_secrets": [ + "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ], + "tweak": "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", + "shared_secret": "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Simple send: two inputs, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ] + ], + "shared_secrets": [ + "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f438b40179a3c4262de12986c0e6cce0634007cdc79c1dcd3e20b9ebc2e7eef6", + "pub_key": "3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1", + "signature": "74f85b856337fbe837643b86f462118159f93ac4acc2671522f27e8f67b079959195ccc7a5dbee396d2909f5d680d6e30cda7359aa2755822509b70d6b0687a1" + } + ], + "tweak": "024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", + "shared_secret": "028158aff7d61ea66b2fa7f555bc3c5937d1debbde16423d630f9aa7943e14d80d", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ] + ], + "shared_secrets": [ + "03aa707f7b5e94b448abd28aa217e3d7a7cc6bb07f1a8d07be4de91bf7b1417469" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 3, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 7, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "4851455bfbe1ab4f80156570aa45063201aa5c9e1b1dcd29f0f8c33d10bf77ae", + "pub_key": "79e71baa2ba3fc66396de3a04f168c7bf24d6870ec88ca877754790c1db357b6", + "signature": "10332eea808b6a13f70059a8a73195808db782012907f5ba32b6eae66a2f66b4f65147e2b968a1678c5f73d57d5d195dbaf667b606ff80c8490eac1f3b710657" + } + ], + "tweak": "03aeea547819c08413974e2ab2b12212e007166bb2058f88b009e082b9b4914a58", + "shared_secret": "03aa707f7b5e94b448abd28aa217e3d7a7cc6bb07f1a8d07be4de91bf7b1417469", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Simple send: two inputs from the same transaction, order reversed", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ] + ], + "shared_secrets": [ + "03054f5c84b07182ba2a2e10a35e088778f95c04f059f4574b024c372eb8ce5468" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 7, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 3, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "ab0c9b87181bf527879f48db9f14a02233619b986f8e8f2d5d408ce68a709f51", + "pub_key": "f4c2da807f89cb1501f1a77322a895acfb93c28e08ed2724d2beb8e44539ba38", + "signature": "398a9790865791a9db41a8015afad3a47d60fec5086c50557806a49a1bc038808632b8fe679a7bb65fc6b455be994502eed849f1da3729cd948fc7be73d67295" + } + ], + "tweak": "024cad5180a093d3af0f49f586bdf37f890920178e68e80561ed53351d0fa499ad", + "shared_secret": "03054f5c84b07182ba2a2e10a35e088778f95c04f059f4574b024c372eb8ce5468", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Outpoint ordering byte-lexicographically vs. vout-integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + }, + "private_key": "93f5ed907ad5b2bdbbdcb5d9116ebc0a4e1f92f910d5260237fa45a9408aad16" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ] + ], + "shared_secrets": [ + "02cb25a6e7c9b7c6d550e0413da63834678465b5e80853a51d0335d318296ac182" + ], + "input_private_key_sum": "7ed265a6dac7aba8508a32d6d6b84c7f1dbd0a0941dd01088d69e8d556345f86", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 256, + "scriptSig": "48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac" + } + } + } + ], + "outputs": [ + "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "c8ac0292997b5bca98b3ebd99a57e253071137550f270452cd3df8a3e2266d36", + "pub_key": "a85ef8701394b517a4b35217c4bd37ac01ebeed4b008f8d0879f9e09ba95319c", + "signature": "c036ee38bfe46aba03234339ae7219b31b824b52ef9d5ce05810a0d6f62330dedc2b55652578aa5bdabf930fae941acd839d5a66f8fce7caa9710ccb446bddd1" + } + ], + "tweak": "031f9a80d0938cf980b51f7cc4fad713d49037f430646dff129c0570d75a40d8f0", + "shared_secret": "02cb25a6e7c9b7c6d550e0413da63834678465b5e80853a51d0335d318296ac182", + "input_pub_key_sum": "032562c1ab2d6bd45d7ca4d78f569999e5333dffd3ac5263924fd00d00dedc4bee" + } + } + ] + }, + { + "comment": "Single recipient: multiple UTXOs from the same public key", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ] + ], + "shared_secrets": [ + "02f6b40ff17f4010fe732ac4b0f2f211281aa09c9a5fb41f1c151ec2606fee9ec2" + ], + "input_private_key_sum": "d5b8f02cbfe3f1d5295af9fb8a9320e859e9cb07115856486ab1a4e4fb89a621", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + } + ], + "outputs": [ + "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f032695e2636619efa523fffaa9ef93c8802299181fd0461913c1b8daf9784cd", + "pub_key": "548ae55c8eec1e736e8d3e520f011f1f42a56d166116ad210b3937599f87f566", + "signature": "f238386c5d5e5444f8d2c75aabbcb28c346f208c76f60823f5de3b67b79e0ec72ea5de2d7caec314e0971d3454f122dda342b3eede01b3857e83654e36b25f76" + } + ], + "tweak": "0319949463fc6a2368d999a2a6a2bcb2dbf64a2ac6e00b3ba5659780c860a6d9e0", + "shared_secret": "02f6b40ff17f4010fe732ac4b0f2f211281aa09c9a5fb41f1c151ec2606fee9ec2", + "input_pub_key_sum": "03e40664e222ba71e29b80efc907fa22a3c6c64f45e89dbb8511dc7a3712b0a186" + } + } + ] + }, + { + "comment": "Single recipient: taproot only inputs with even y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ] + ], + "shared_secrets": [ + "02de9719785c6d09f71571dadf44bca59edba2af3e689c65cbc3bb5a4a387732ef" + ], + "input_private_key_sum": "e7638ebfda3ab3849a5707e240a6627671f7f6e609bf172691cf1e9780e51d47", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "02782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + } + ], + "outputs": [ + "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3fb9ce5ce1746ced103c8ed254e81f6690764637ddbc876ec1f9b3ddab776b03", + "pub_key": "de88bea8e7ffc9ce1af30d1132f910323c505185aec8eae361670421e749a1fb", + "signature": "c5acd25a8f021a4192f93bc34403fd8b76484613466336fb259c72d04c169824f2690ca34e96cee86b69f376c8377003268fda56feeb1b873e5783d7e19bcca5" + } + ], + "tweak": "02dc59cc8e8873b65c1dd5c416d4fbeb647372c329bd84a70c05b310e222e2c183", + "shared_secret": "02de9719785c6d09f71571dadf44bca59edba2af3e689c65cbc3bb5a4a387732ef", + "input_pub_key_sum": "038180a2125f9d6dd116e1a6139be4d72fd5057dab6aaabaa5654817c11baeb3ba" + } + } + ] + }, + { + "comment": "Single recipient: taproot only with mixed even/odd y-values", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ] + ], + "shared_secrets": [ + "030e7f5ca4bf109fc35c8c2d878f756c891ac04c456cc5f0b05fcec4d3b2b1beb2" + ], + "input_private_key_sum": "cda4ff9a3480e1fbfc6edd61b222f280f9baa0652002c1ffdb612efcc45d2ff2", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "028c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + } + ], + "outputs": [ + "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "f5382508609771068ed079b24e1f72e4a17ee6d1c979066bf1d4e2a5676f09d4", + "pub_key": "77cab7dd12b10259ee82c6ea4b509774e33e7078e7138f568092241bf26b99f1", + "signature": "ff65833b8fd1ed3ef9d0443b4f702b45a3f2dd457ba247687e8207745c3be9d2bdad0ab3f07118f8b2efc6a04b95f7b3e218daf8a64137ec91bd2fc67fc137a5" + } + ], + "tweak": "03b990f5b1d90ea8fd4bdd5c856a9dfe17035d196958062e2c6cb4c99e413f3548", + "shared_secret": "030e7f5ca4bf109fc35c8c2d878f756c891ac04c456cc5f0b05fcec4d3b2b1beb2", + "input_pub_key_sum": "020f0ab50f420ab1249bc2a21659c607f2873400853035aad0ca6d0ded04d62623" + } + } + ] + }, + { + "comment": "Single recipient: taproot input with even y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ] + ], + "shared_secrets": [ + "021cd92ff153e638d0a97bcd11fafc81c321b111f5ba1efff593371b7b688efdd3" + ], + "input_private_key_sum": "7823ca0d4895515315a8e3bf602c080b6b732117272429e94751eb9b13a01943", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "b40017865c79b1fcbed68896791be93186d08f47e416b289b8c063777e14e8df", + "pub_key": "30523cca96b2a9ae3c98beb5e60f7d190ec5bc79b2d11a0b2d4d09a608c448f0", + "signature": "d1edeea28cf1033bcb3d89376cabaaaa2886cbd8fda112b5c61cc90a4e7f1878bdd62180b07d1dfc8ffee1863c525a0c7b5bcd413183282cfda756cb65787266" + } + ], + "tweak": "0233c2a447b8b244e4ffcfb59fe365eaa3bb22288b31e2113b9998861f40d4d6da", + "shared_secret": "021cd92ff153e638d0a97bcd11fafc81c321b111f5ba1efff593371b7b688efdd3", + "input_pub_key_sum": "031ecda9c64faaa6cd57c9f3d7c62bcfc0763c2627ed8dc0e2c3018e9ff37a0bf0" + } + } + ] + }, + { + "comment": "Single recipient: taproot input with odd y-value and non-taproot input", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + }, + "private_key": "1d37787c2b7116ee983e9f9c13269df29091b391c04db94239e0d2bc2182c3bf" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ] + ], + "shared_secrets": [ + "03d9437eb3676cf5cc00feebe68bc44c4567332e4b89788dec9eceb3779054442b" + ], + "input_private_key_sum": "700fd97abd324179e8bcc72587bbd9a40b43f67535ce95a0b80175b2dc73a314", + "input_pub_keys": [ + "028c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4", + "03e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "01400a4d0dca6293f40499394d7eefe14a1de11e0e3454f51de2e802592abf5ee549042a1b1a8fb2e149ee9dd3f086c1b69b2f182565ab6ecf599b1ec9ebadfda6c5", + "prevout": { + "scriptPubKey": { + "hex": "51208c8d23d4764feffcd5e72e380802540fa0f88e3d62ad5e0b47955f74d7b283c4" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "463044021f24e010c6e475814740ba24c8cf9362c4db1276b7f46a7b1e63473159a80ec30221008198e8ece7b7f88e6c6cc6bb8c86f9f00b7458222a8c91addf6e1577bcf7697e2103e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9148cbc7dfe44f1579bff3340bbef1eddeaeb1fc97788ac" + } + } + } + ], + "outputs": [ + "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "a2f9dd05d1d398347c885d9c61a64d18a264de6d49cea4326bafc2791d627fa7", + "pub_key": "359358f59ee9e9eec3f00bdf4882570fd5c182e451aa2650b788544aff012a3a", + "signature": "96038ad233d8befe342573a6e54828d863471fb2afbad575cc65271a2a649480ea14912b6abbd3fbf92efc1928c036f6e3eef927105af4ec1dd57cb909f360b8" + } + ], + "tweak": "02d4e4f2c4cdb71c9c39a700a9ee1a0fc05b98362a441183f5770af7d6e2b3038c", + "shared_secret": "03d9437eb3676cf5cc00feebe68bc44c4567332e4b89788dec9eceb3779054442b", + "input_pub_key_sum": "03bc118b1c8178915b716d6137633722c71adfe721551ec7b3938054691de6a2b9" + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "d97e442d110c0bdd31161a7bb6e7862e038d02a09b1484dfbb463f2e0f7c9230", + "pub_key": "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "signature": "29bd25d0f808d7fcd2aa6d5ed206053899198397506c301b218a9e47a3d7070af03e903ff718978d50d1b6b9af8cc0e313d84eda5d5b1e8e85e5516d630bbeb9" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs: multiple outputs, multiple recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "scan_pub_key": "02062d49ffc02787d586c608dfbec184aa91a6597d97b463ea5c6babd9d17a95a3", + "spend_pub_key": "0381eb9a9a9ec739d527c1631b31b421566f5c2a47b4ab5b1f6a686dfb68eab716" + }, + { + "address": "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "scan_pub_key": "02062d49ffc02787d586c608dfbec184aa91a6597d97b463ea5c6babd9d17a95a3", + "spend_pub_key": "0381eb9a9a9ec739d527c1631b31b421566f5c2a47b4ab5b1f6a686dfb68eab716" + } + ] + }, + "expected": { + "outputs": [ + [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2", + "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9902c3c56e84002a7cd410113a9ab21d142be7f53cf5200720bb01314c5eb920", + "scan_priv_key": "060b751d7892149006ed7b98606955a29fe284a1e900070c0971f5fb93dbf422" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn" + ], + "outputs": [ + { + "priv_key_tweak": "72cd082cccb633bf85240a83494b32dc943a4d05647a6686d23ad4ca59c0ebe4", + "pub_key": "2e847bb01d1b491da512ddd760b8509617ee38057003d6115d00ba562451323a", + "signature": "38745f3d9f5eef0b1cfb17ca314efa8c521efab28a23aa20ec5e3abb561d42804d539906dce60c4ee7977966184e6f2cab1faa0e5377ceb7148ec5218b4e7878" + }, + { + "priv_key_tweak": "2f17ea873a0047fc01ba8010fef0969e76d0e4283f600d48f735098b1fee6eb9", + "pub_key": "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "signature": "c26f4e3cf371b90b840f48ea0e761b5ec31883ed55719f9ef06a90e282d85f565790ab780a3f491bc2668cc64e944dca849d1022a878cdadb8d168b8da4a6da3" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Receiving with labels: label with even parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0259352add837b6686e8d22b87017814a46b3ad308702167c65bd5c8599cd28d1c" + } + ] + }, + "expected": { + "outputs": [ + [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "51d4e9d0d482b5700109b4b2e16ff508269b03d800192a043d61dca4a0a72a52", + "pub_key": "d014d4860f67d607d60b1af70e0ee236b99658b61bb769832acbbe87c374439a", + "signature": "c30fa63bad6f0a317f39a773a5cbf0b0f8193c71dfebba05ee6ae4ed28e3775e6e04c3ea70a83703bb888122855dc894cab61692e7fd10c9b3494d479a60785e" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Receiving with labels: label with odd parity", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0208a144a18433a83f633c822c1bf5ee4c8c8e24601d6ca75e20a7dc57a0ff9280" + } + ] + }, + "expected": { + "outputs": [ + [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "6024ae214876356b8d917716e7707d267ae16a0fdb07de2a786b74a7bbcddead", + "pub_key": "67626aebb3c4307cf0f6c39ca23247598fabf675ab783292eb2f81ae75ad1f8c", + "signature": "a86d554d0d6b7aa0907155f7e0b47f0182752472fffaeddd68da90e99b9402f166fd9b33039c302c7115098d971c1399e67c19e9e4de180b10ea0b9d6f0db832" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Receiving with labels: large label integer", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03d85092bbe3468f684ce1d8a2a66ebec96a9e6e09e7110720a5d5faa4aa7880d0" + } + ] + }, + "expected": { + "outputs": [ + [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 2, + 3, + 1001337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjex54dmqmmv6rw353tsuqhs99ydvadxzrsy9nuvk74epvee55drs734pqq", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqsg59z2rppn4qlkx0yz9sdltmjv3j8zgcqadjn4ug98m3t6plujsq9qvu5n", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgq7c2zfthc6x3a5yecwc52nxa0kfd20xuz08zyrjpfw4l2j257yq6qgnkdh5" + ], + "outputs": [ + { + "priv_key_tweak": "e336b92330c33030285ce42e4115ad92d5197913c88e06b9072b4a9b47c664a2", + "pub_key": "7efa60ce78ac343df8a013a2027c6c5ef29f9502edcbd769d2c21717fecc5951", + "signature": "c9e80dd3bdd25ca2d352ce77510f1aed37ba3509dc8cc0677f2d7c2dd04090707950ce9dd6c83d2a428063063aff5c04f1744e334f661f2fc01b4ef80b50f739" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled and labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs with labels: multiple outputs for labeled address; same recipient", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + } + ] + }, + "expected": { + "outputs": [ + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj" + ], + "outputs": [ + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "9d5fd3b91cac9ddfea6fc2e6f9386f680e6cee623cda02f53706306c081de87f", + "pub_key": "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "signature": "db0dfacc98b6a6fcc67cc4631f080b1ca38c60d8c397f2f19843f8f95ec91594b24e47c5bd39480a861c1209f7e3145c440371f9191fb96e324690101eac8e8e" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Multiple outputs with labels: un-labeled, labeled, and multiple outputs for labeled address; same recipients", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "03a6739499dc667d308baefea4de0c4a85cc72aece181bc05712d3919662610ff1" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0244baa5cf5db444a9e922832ff2c88716b566a85d62e8235aebd91884d4f64942" + }, + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "0244baa5cf5db444a9e922832ff2c88716b566a85d62e8235aebd91884d4f64942" + } + ] + }, + "expected": { + "outputs": [ + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + [ + "3c54444944d176437644378c23efb999ab6ab1cacdfe1dc1537b607e3df330e2", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "3edf1ff6657c6e69568811bd726a7a7f480493aa42161acfe8dd4f44521f99ed", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "7ee1543ed5d123ffa66fbebc128c020173eb490d5fa2ba306e0c9573a77db8f3", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + [ + "602e10e6944107c9b48bd885b493676578c935723287e0ab2f8b7f136862568e", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca" + ], + [ + "83dc944e61603137294829aed56c74c9b087d80f2c021b98a7fae5799000696c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "e976a58fbd38aeb4e6093d4df02e9c1de0c4513ae0c588cef68cda5b2f8834ca", + "f4569fc5f69c10f0082cfbb8e072e6266ec55f69fba8cffca4cbb4c144b7e59b" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [ + 1, + 1337 + ] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqaxww2fnhrx05cghth75n0qcj59e3e2anscr0q9wyknjxtxycg07y3pevyj", + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjyh2ju7hd5gj57jg5r9lev3pckk4n2shtzaq34467erzzdfajfggty6aa5" + ], + "outputs": [ + { + "priv_key_tweak": "4e3352fbe0505c25e718d96007c259ef08db34f8c844e4ff742d9855ff03805a", + "pub_key": "006a02c308ccdbf3ac49f0638f6de128f875db5a213095cf112b3b77722472ae", + "signature": "6eeae1ea9eb826e3d0e812f65937100e0836ea188c04f36fabc4981eda29de8d3d3529390a0a8b3d830f7bca4f5eae5994b9788ddaf05ad259ffe26d86144b4b" + }, + { + "priv_key_tweak": "43100f89f1a6bf10081c92b473ffc57ceac7dbed600b6aba9bb3976f17dbb914", + "pub_key": "39f42624d5c32a77fda80ff0acee269afec601d3791803e80252ae04e4ffcf4c", + "signature": "15c92509b67a6c211ebb4a51b7528d0666e6720de2343b2e92cfb97942ca14693c1f1fdc8451acfdb2644039f8f5c76114807fdc3d3a002d8a46afab6756bd75" + }, + { + "priv_key_tweak": "bf709f98d4418f8a67e738154ae48818dad44689cd37fbc070891a396dd1c633", + "pub_key": "ae1a780c04237bd577283c3ddb2e499767c3214160d5a6b0767e6b8c278bd701", + "signature": "42a19fd8a63dde1824966a95d65a28203e631e49bf96ca5dae1b390e7a0ace2cc8709c9b0c5715047032f57f536a3c80273cbecf4c05be0b5456c183fa122c06" + }, + { + "priv_key_tweak": "736f05e4e3072c3b8656bedef2e9bf54cbcaa2b6fe5320d3e86f5b96874dda71", + "pub_key": "ca64abe1e0f737823fb9a94f597eed418fb2df77b1317e26b881a14bb594faaa", + "signature": "2e61bb3d79418ecf55f68847cf121bfc12d397b39d1da8643246b2f0a9b96c3daa4bfe9651beb5c9ce20e1f29282c4566400a4b45ee6657ec3b18fdc554da0b4" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Single recipient: use silent payments for sender change", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + }, + { + "address": "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr", + "scan_pub_key": "03b4cc0b090b6f49a684558852db60ee5eb1c5f74352839c3d18a8fc04ef7354e0", + "spend_pub_key": "03ecd43b9fdad484ff57278b21878b844276ce390622d03dd0cfb4288b7e02a6f5" + } + ] + }, + "expected": { + "outputs": [ + [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ] + ], + "shared_secrets": [ + "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "037d12c02c3aed482658a28b8d1be030dac1daf995551491d74c00543af98572fb" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "b8f87388cbb41934c50daca018901b00070a5ff6cc25a7e9e716a9d5b9e4d664", + "scan_priv_key": "11b7a82e06ca2648d5fded2366478078ec4fc9dc1d8ff487518226f229d768fd" + }, + "labels": [ + 0 + ] + }, + "expected": { + "addresses": [ + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqauj52ymtc4xdkmx3tgyhrsemg2g3303xk2gtzfy8h8ejet8fz8jcw23zua", + "sp1qqw6vczcfpdh5nf5y2ky99kmqae0tr30hgdfg88parz50cp80wd2wqqlv6saelkk5snl4wfutyxrchpzzwm8rjp3z6q7apna59z9huq4x754e5atr" + ], + "outputs": [ + { + "priv_key_tweak": "80cd767ed20bd0bb7d8ea5e803f8c381293a62e8a073cf46fb0081da46e64e1f", + "pub_key": "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "signature": "7fbd5074cf1377273155eefafc7c330cb61b31da252f22206ac27530d2b2567040d9af7808342ed4a09598c26d8307446e4ed77079e6a2e61fea736e44da5f5a" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "037d12c02c3aed482658a28b8d1be030dac1daf995551491d74c00543af98572fb", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + }, + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "be368e28979d950245d742891ae6064020ba548c1e2e65a639a8bb0675d95cff", + "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "33ce085c3c11eaad13694aae3c20301a6c83382ec89a7cde96c6799e2f88805a", + "pub_key": "f207162b1a7abc51c42017bef055e9ec1efc3d3567cb720357e2b84325db33ac", + "signature": "335667ca6cae7a26438f5cfdd73b3d48fa832fa9768521d7d5445f22c203ab0d74ed85088f27d29959ba627a4509996676f47df8ff284d292567b1beef0e3912" + } + ], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "Single recipient: taproot input with NUMS point", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + }, + "private_key": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + }, + "private_key": "8d4751f6e8a3586880fb66c19ae277969bd5aa06f61c4ee2f1e2486efdf666d3" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ] + ], + "shared_secrets": [ + "036f040608cd1e5ee79c54e78bea85904c895591f547beae080d0c5f6946c2730d" + ], + "input_private_key_sum": "fc8716a97a48ba9a05a98ae47b5cd201a25a7fd5d8b73c203c5f7b6b6b3b6ad7", + "input_pub_keys": [ + "02782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0440c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b22205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5ac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00150", + "prevout": { + "scriptPubKey": { + "hex": "5120da6f0595ecb302bbe73e2f221f05ab10f336b06817d36fd28fc6691725ddaa85" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140bd1e708f92dbeaf24a6b8dd22e59c6274355424d62baea976b449e220fd75b13578e262ab11b7aa58e037f0c6b0519b66803b7d9decaa1906dedebfb531c56c1", + "prevout": { + "scriptPubKey": { + "hex": "5120782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "0340268d31a9276f6380107d5321cafa6d9e8e5ea39204318fdc8206b31507c891c3bbcea3c99e2208d73bd127a8e8c5f1e45a54f1bd217205414ddb566ab7eda0092220e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85dac21c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + "prevout": { + "scriptPubKey": { + "hex": "51200a3c9365ceb131f89b0a4feb6896ebd67bb15a98c31eaa3da143bb955a0f3fcb" + } + } + } + ], + "outputs": [ + "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3ddec3232609d348d6b8b53123b4f40f6d4f5398ca586f087b0416ec3b851496", + "pub_key": "79e79897c52935bfd97fc6e076a6431a0c7543ca8c31e0fc3cf719bb572c842d", + "signature": "d7d06e3afb68363031e4eb18035c46ceae41bdbebe7888a4754bc9848c596436869aeaecff0527649a1f458b71c9ceecec10b535c09d01d720229aa228547706" + } + ], + "tweak": "02213b872c9a6ee28a0d861384a1b3e3ec7257f4855ed09b4323e3899f3b028989", + "shared_secret": "036f040608cd1e5ee79c54e78bea85904c895591f547beae080d0c5f6946c2730d", + "input_pub_key_sum": "02782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + } + } + ] + }, + { + "comment": "Pubkey extraction from malleated p2pkh", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ] + ], + "shared_secrets": [ + "034773b97ccad9791cb4213964ff9896ccd6581ee69345de5d114786d9d86b03a2" + ], + "input_private_key_sum": "610e0f75fd05e5e80e088b57af0a46da06cb0700c0c5907aa6d29c6b4ce46348", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "02e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "0075473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "5163473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187372102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d67483046022100c0d3c851d3bd562ae93d56bcefd735ea57c027af46145a4d5e9cac113bfeb0c2022100ee5b2239af199fa9b7aa1d98da83a29d0a2cf1e4f29e2f37134ce386d51c544c2102ad0f26ddc7b3fcc340155963b3051b85289c1869612ecb290184ac952e2864ec68", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914c82c5ec473cbc6c86e5ef410e36f9495adcf979988ac" + } + } + } + ], + "outputs": [ + "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "10bde9781def20d7701e7603ef1b1e5e71c67bae7154818814e3c81ef5b1a3d3", + "pub_key": "4612cdbf845c66c7511d70aab4d9aed11e49e48cdb8d799d787101cdd0d53e4f", + "signature": "6137969f810e9e8ef6c9755010e808f5dd1aed705882e44d7f0ae64eb0c509ec8b62a0671bee0d5914ac27d2c463443e28e999d82dc3d3a4919f093872d947bb" + } + ], + "tweak": "028d6617f9bfe08604beb2188f4eebec923f5f8cc436fa6d14e4256e49bc32e7c8", + "shared_secret": "034773b97ccad9791cb4213964ff9896ccd6581ee69345de5d114786d9d86b03a2", + "input_pub_key_sum": "038b0d201fe111bdc0e6953772bd02a41959d25d5b2f66bcbe348af27bbdd42735" + } + } + ] + }, + { + "comment": "P2PKH and P2WPKH Uncompressed Keys are skipped", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + }, + "private_key": "72b8ae09175ca7977f04993e651d88681ed932dfb92c5158cdf0161dd23fda6e" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ], + "shared_secrets": [ + "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64" + ], + "input_private_key_sum": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 1, + "scriptSig": "", + "txinwitness": "02473045022100e7d26e77290b37128f5215ade25b9b908ce87cc9a4d498908b5bb8fd6daa1b8d022002568c3a8226f4f0436510283052bfb780b76f3fe4aa60c4c5eb118e43b187374104e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d6fe8190e189be57d0d5bcd17dbcbcd04c9b4a1c5f605b10d5c90abfcc0d12884", + "prevout": { + "scriptPubKey": { + "hex": "00140423f731a07491364e8dce98b7c00bda63336950" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ], + "tweak": "02b04034f00da0678507d1345b7d56fecef825a1151f9dc7d8ca6946452a9e1f43", + "shared_secret": "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64", + "input_pub_key_sum": "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + ] + }, + { + "comment": "Skip invalid P2SH inputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ] + ], + "shared_secrets": [ + "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64" + ], + "input_private_key_sum": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "16001419c2f3ae0ca3b642bd3e49598b8da89f50c14161", + "txinwitness": "02483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d621025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "prevout": { + "scriptPubKey": { + "hex": "a9148629db5007d5fcfbdbb466637af09daf9125969387" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 1, + "scriptSig": "1600144b92ac4ac6fe6212393894addda332f2e47a3156", + "txinwitness": "02473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "prevout": { + "scriptPubKey": { + "hex": "a9146c9bf136fbb7305fd99d771a95127fcf87dedd0d87" + } + } + }, + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 2, + "scriptSig": "00493046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d601483045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b97014c695221025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be52103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233382102e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d53ae", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "a9141044ddc6cea09e4ac40fbec2ba34ad62de6db25b87" + } + } + } + ], + "outputs": [ + "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "688fa3aeb97d2a46ae87b03591921c2eaf4b505eb0ddca2733c94701e01060cf", + "pub_key": "67fee277da9e8542b5d2e6f32d660a9bbd3f0e107c2d53638ab1d869088882d6", + "signature": "72e7ad573ac23255d4651d5b0326a200496588acb7a4894b22092236d5eda6a0a9a4d8429b022c2219081fefce5b33795cae488d10f5ea9438849ed8353624f2" + } + ], + "tweak": "02b04034f00da0678507d1345b7d56fecef825a1151f9dc7d8ca6946452a9e1f43", + "shared_secret": "0295a54c359da5b2640601ddbedb26e040cb97b6a3432e60b76d1258e85f72fa64", + "input_pub_key_sum": "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + ] + }, + { + "comment": "Recipient ignores unrelated outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgrz6j0lcqnc04vxccydl0kpsj4frfje0ktmgcl2t346hkw30226xqupawdf48k8882j0strrvcmgg2kdawz53a54dd376ngdhak364hzcmynqtn", + "scan_pub_key": "02062d49ffc02787d586c608dfbec184aa91a6597d97b463ea5c6babd9d17a95a3", + "spend_pub_key": "0381eb9a9a9ec739d527c1631b31b421566f5c2a47b4ab5b1f6a686dfb68eab716" + } + ] + }, + "expected": { + "outputs": [ + [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8" + ] + ], + "shared_secrets": [ + "03dd5fd04d3c8863be750a1bd7474df06161461d38d3ce1397a5c78cee112cdcd2" + ], + "input_private_key_sum": "ee55616ce5a93e508f03f21949ecbe70a2a0b107b6e1df5d98b4e4da4adaca1b", + "input_pub_keys": [ + "025a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5", + "03782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "", + "txinwitness": "0140c459b671370d12cfb5acee76da7e3ba7cc29b0b4653e3af8388591082660137d087fdc8e89a612cd5d15be0febe61fc7cdcf3161a26e599a4514aa5c3e86f47b", + "prevout": { + "scriptPubKey": { + "hex": "51205a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b972103782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9147cdd63cc408564188e8e472640e921c7c90e651d88ac" + } + } + } + ], + "outputs": [ + "841792c33c9dc6193e76744134125d40add8f2f4a96475f28ba150be032d64e8", + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [], + "tweak": "0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8", + "shared_secret": "038efbcbc1b0938fba3bf59fea1219a3c54b6d6f9107560da05001407adc13f413", + "input_pub_key_sum": "03853f51bef283502181e93238c8708ae27235dc51ae45a0c4053987c52fc6428b" + } + } + ] + }, + { + "comment": "No valid inputs, sender generates no outputs", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + }, + "private_key": "eadc78165ff1f8ea94ad7cfdc54990738a4c53f6e0507b42154201b8e5dff3b1" + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + }, + "private_key": "0378e95685b74565fa56751b84a32dfd18545d10d691641b8372e32164fad66a" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [] + ], + "shared_secrets": [ + null + ], + "input_pub_keys": [] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + "vout": 0, + "scriptSig": "483046022100ad79e6801dd9a8727f342f31c71c4912866f59dc6e7981878e92c5844a0ce929022100fb0d2393e813968648b9753b7e9871d90ab3d815ebf91820d704b19f4ed224d641045a1e61f898173040e20616d43e9f496fba90338a39faa1ed98fcbaeee4dd9be5c61836c9b1688ba431f7ea3039742251f62f0dca3da1bee58a47fa9b456c2d52", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a914460e8b41545d2dbe7e0671f0f573e2232814260a88ac" + } + } + }, + { + "txid": "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + "vout": 0, + "scriptSig": "473045022100a8c61b2d470e393279d1ba54f254b7c237de299580b7fa01ffcc940442ecec4502201afba952f4e4661c40acde7acc0341589031ba103a307b886eb867b23b850b974104782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c3799373233387c5343bf58e23269e903335b958a12182f9849297321e8d710e49a8727129cab", + "txinwitness": "", + "prevout": { + "scriptPubKey": { + "hex": "76a9144b92ac4ac6fe6212393894addda332f2e47a315688ac" + } + } + } + ], + "outputs": [ + "782eeb913431ca6e9b8c2fd80a5f72ed2024ef72a3c6fb10263c379937323338", + "e0ec4f64b3fa2e463ccfcf4e856e37d5e1e20275bc89ec1def9eb098eff1f85d" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [], + "tweak": null, + "shared_secret": null + } + } + ] + }, + { + "comment": "Input keys sum up to zero / point at infinity: sending fails, receiver skips tx", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + }, + "private_key": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb" + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + }, + "private_key": "592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456" + } + ], + "recipients": [ + { + "address": "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s", + "scan_pub_key": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5", + "spend_pub_key": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + } + ] + }, + "expected": { + "outputs": [ + [] + ], + "shared_secrets": [ + null + ], + "input_pub_keys": [ + "02557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "03557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "024730440220085003179ce1a3a88ce0069aa6ea045e140761ab88c22a26ae2a8cfe983a6e4602204a8a39940f0735c8a4424270ac8da65240c261ab3fda9272f6d6efbf9cfea366012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + } + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402204586a68e1d97dd3c6928e3622799859f8c3b20c3c670cf654cc905c9be29fdb7022043fbcde1689f3f4045e8816caf6163624bd19e62e4565bc99f95c533e599782c012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + } + } + ], + "outputs": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ], + "key_material": { + "spend_priv_key": "0000000000000000000000000000000000000000000000000000000000000001", + "scan_priv_key": "0000000000000000000000000000000000000000000000000000000000000002" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqtrqglu5g8kh6mfsg4qxa9wq0nv9cauwfwxw70984wkqnw2uwz0w2qnehen8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq6lqj3s" + ], + "outputs": [], + "tweak": null, + "shared_secret": null + } + } + ] + }, + { + "comment": "Input keys intermediate sum is zero but final sum is non-zero", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "0247304402203e5537fa8c876b3475e7efe4f1474b0f48b7a6e4169179db5de9bb5b55ad1bd10220200e06f8f4d29dbc48bbcdf90df3278e798ce6cbf3c9fbf90427599fde147867012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + }, + "private_key": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb" + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402207fdad0faf46edc54f5a5c67d33b2fa8d3f1fdc869381fd96e659f9e0c470ab1e022044f0d973339618b18667cef9a6251817f7f431f7f2b252a8cb760ccb40e7d823012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + }, + "private_key": "592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456" + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 2, + "scriptSig": "", + "txinwitness": "0247304402203e5537fa8c876b3475e7efe4f1474b0f48b7a6e4169179db5de9bb5b55ad1bd10220200e06f8f4d29dbc48bbcdf90df3278e798ce6cbf3c9fbf90427599fde147867012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + }, + "private_key": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb" + } + ], + "recipients": [ + { + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + "scan_pub_key": "0220bcfac5b99e04ad1a06ddfb016ee13582609d60b6291e98d01a9bc9a16c96d4", + "spend_pub_key": "025cc9856d6f8375350e123978daac200c260cb5b5ae83106cab90484dcd8fcf36" + } + ] + }, + "expected": { + "outputs": [ + [ + "7e88a7536c90770be4d2693a84ed03abe3fdcc5a29f96ec3433effec3b0c2194" + ] + ], + "shared_secrets": [ + "037dc4e5904ab4770dbdbb628860b54265fdbb7810b8afdf9f582fedaabfdebef0" + ], + "input_private_key_sum": "a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb", + "input_pub_keys": [ + "02557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "03557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "02557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 0, + "scriptSig": "", + "txinwitness": "0247304402203e5537fa8c876b3475e7efe4f1474b0f48b7a6e4169179db5de9bb5b55ad1bd10220200e06f8f4d29dbc48bbcdf90df3278e798ce6cbf3c9fbf90427599fde147867012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + } + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 1, + "scriptSig": "", + "txinwitness": "0247304402207fdad0faf46edc54f5a5c67d33b2fa8d3f1fdc869381fd96e659f9e0c470ab1e022044f0d973339618b18667cef9a6251817f7f431f7f2b252a8cb760ccb40e7d823012103557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149860538b5575962776ed0814ae222c7d60c72d7b" + } + } + }, + { + "txid": "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e", + "vout": 2, + "scriptSig": "", + "txinwitness": "0247304402203e5537fa8c876b3475e7efe4f1474b0f48b7a6e4169179db5de9bb5b55ad1bd10220200e06f8f4d29dbc48bbcdf90df3278e798ce6cbf3c9fbf90427599fde147867012102557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975", + "prevout": { + "scriptPubKey": { + "hex": "00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792" + } + } + } + ], + "outputs": [ + "7e88a7536c90770be4d2693a84ed03abe3fdcc5a29f96ec3433effec3b0c2194" + ], + "key_material": { + "spend_priv_key": "9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3", + "scan_priv_key": "0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c" + }, + "labels": [] + }, + "expected": { + "addresses": [ + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ], + "outputs": [ + { + "priv_key_tweak": "3d5b7a284108f93b9fe78f2c300d2ca9ef3b4e0cd0de673fc72990f2a4f417b9", + "pub_key": "7e88a7536c90770be4d2693a84ed03abe3fdcc5a29f96ec3433effec3b0c2194", + "signature": "f8d5222f1a682215c40ab677f2104606f2a0e6c5cb1a2d248d970fb61d4eadb31702353e6b41ea5a9b24817efa0eaf535552eeee8a794b662c3cf303b6c86672" + } + ], + "tweak": "039c68bacb7efbf2175d781822f460afc4839a5798fabb055b50d939231bf57bb6", + "shared_secret": "037dc4e5904ab4770dbdbb628860b54265fdbb7810b8afdf9f582fedaabfdebef0", + "input_pub_key_sum": "02557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975" + } + } + ] + }, + { + "comment": "Maximum per-group recipient limit K_max is exceeded (2324 matches): sending fails, receiver doesn't scan beyond limit", + "sending": [ + { + "given": { + "vin": [ + { + "txid": "3cde0e7ad8adf24a4e52fd18efafffb94f73f4c7facf2627a5b588ad1c9db87f", + "vout": 0, + "scriptSig": "", + "txinwitness": "01407ce01bc5ef2e4aae353e7f291fec32623d6a491b5421be6dccc70345f32ebae5baa6654c60c074e72da76fd06b343474370e56d3ec0413156ec6261757125ad4", + "prevout": { + "scriptPubKey": { + "hex": "5120bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + } + }, + "private_key": "0000000000000000000000000000000000000000000000000000000000001337" + } + ], + "recipients": [ + { + "address": "sp1qqtnvg7hxjck9ag24naytgd7pjwsmevwh95ydwht58w3uh7uw0ta7kqmk4a3pm24z0yepw6aw27pku56md0hq5ace0l3p3jstkqaajwrsjqc5dde5", + "scan_pub_key": "02e6c47ae6962c5ea1559f48b437c193a1bcb1d72d08d75d743ba3cbfb8e7afbeb", + "spend_pub_key": "0376af621daaa27932176bae57836e535b6bee0a77197fe218ca0bb03bd9387090", + "count": 2324 + } + ] + }, + "expected": { + "outputs": [ + [] + ], + "shared_secrets": [ + null + ], + "input_private_key_sum": "0000000000000000000000000000000000000000000000000000000000001337", + "input_pub_keys": [ + "02bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + ] + } + } + ], + "receiving": [ + { + "given": { + "vin": [ + { + "txid": "3cde0e7ad8adf24a4e52fd18efafffb94f73f4c7facf2627a5b588ad1c9db87f", + "vout": 0, + "scriptSig": "", + "txinwitness": "01407ce01bc5ef2e4aae353e7f291fec32623d6a491b5421be6dccc70345f32ebae5baa6654c60c074e72da76fd06b343474370e56d3ec0413156ec6261757125ad4", + "prevout": { + "scriptPubKey": { + "hex": "5120bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + } + }, + "private_key": "0000000000000000000000000000000000000000000000000000000000001337" + } + ], + "outputs": [ + "5cfb6fd7a7e5d2af0fee276f2104ce2e5c86cde7955b98f2f1fb32811b448f2f", + "da290e0d79ead5cd2e5d150aac80dcab54dbd0e68f241a6c34dc30906eabd13d", + "d4a2b1458f0c8b20d756bde8e545560d7e7a65f6b4158f01ed34a913ba3a4603", + "24752e555b0edcca123f769113692224bf1e6db137cddeb7d69ae52e51144e9e", + "c911f722ef9a3594d6b85c585d4f3ed8dc437f850591dc627a0154723644114b", + "ef91ddaa83a0b748d638dc9be8c9771b049a04815f03967b7ee993bde6c260ab", + "ee4ed334fb38af95f52c6bbadcf2f491e646b9cc8ed31da67635f36c3cfdaba0", + "a17d61b4772e453b32782744902261e051510cbafc04f086f53480d1ceee1a9c", + "44d89f5e903739072e3aff8a3eb2f351520f77ff288ac8be277be3f28834bdd2", + "3eff02a4a04d66d63f6bc93a693f23db703f351c0a1ff1656b907ed348a9a35f", + "628c23a6e21e6a47b80274fc80e4decb98ff79b8040c7f9ea5972e4920b62b6d", + "e72190c0a5ea167c2663b18d378520b9228da61ebbc99c3fa2af39e01fb9501e", + "24183edbc5b80588e0e5b803e924e77fc1cab1afca54cfd285dad9bfc2a34f6d", + "c45bad25a223f0f617e8167f858037907e4454210a745f3b9798c8913646dfc6", + "9f9cec85011d7f718f96ee1a2940df21920a857cd1a545255579cb615f285a42", + "8c60891f9bc2f772efc51d44f7cc885161412835b6fd5aac3b0cf57c3c2f9154", + "85ac46a6ac9569ed67d1bc5342ed3b591af5e01581d1b09bcb2c71deb9b2c2c9", + "8a6bfe47e616c6854fc797a4af53ef51c942b99eae18f316594fc8eb8869ca05", + "ed5e1aa6a8b4e805273367b8ec3ade16ed6494270fc2169ba920b89770efe8eb", + "2448503c55a1a7035c069e1d5461b81774c43bf6061421fdd0157009bfe96a8a", + "d0dcb3d7b551eac8d31fceccb83df415c554860d5541dd08ff5fa0028ce5704f", + "f11a298b54b650613e0b0183552c9c9e42c3812e03cf713d351ef71106b6ee3f", + "49749206f23791f858c453cb60160d48710075bdbfd33907b0046d10f0b0295e", + "3fbfdb52cbf0ea9e6655a211748bf2ac2dc92ff3cfeeef0f0a26be9987016de0", + "d4a29144511981919cadbe8227fc68379e85d3fe399f177c2ae36c304b6ad6b0", + "c586cc196ff56f42396f426a0200afee3a6c41058d81f19caf6536ec0efd5605", + "c666994f027ceab025256af28d8ea6aec452fb8b0e1f69e7e13a4f3c5119f4ae", + "5c9216cc4529c3f9c62caf70b04f8354ce9ec2cabaef745092bea1b160328fda", + "d9aea54323503d78c026a8cd465c19a6df54b32ee866714d190e8260054361da", + "ba71f87ca1d74eee115d8e8a26293fe2c88d9d66cff185a7a079282f02f931ad", + "b8b930666b2df74c33dc8963221080c115246070e3fcd51f39bee71312db4fc4", + "b7f42d2f18a3ac61f570b64ae6864f7dbff774a370ba27358d76797e841d5304", + "d636d179055241a0f8bb7db193912da513a8999684d9829b266985a8083819a9", + "be9622aec125f9605bcee193a99ca27c16bc591122ff8b44c511a85e6f63ec9d", + "af5beb25776f84fadd6079c3ffd85775a35ad74096a58229e1d5076d836ca25d", + "75fb55ec31e304456f663c9a66321e67f9d9bd8f43749494acc5189ed57b0d4a", + "8dfebff33c514486d06eeb4ab8eb8471496556858c4382f73780620101a77f71", + "93bfd4e75706951de4a59c7f70c5d2ce4c617b4bde0b0b23ae046df096116726", + "b6eeca22f43bd62a4fcb3eee719e07d39ea703fa0707970fc197916f2efebbb5", + "0437c3971f73342e5c41178590e1cb1364cb855b2d20b7b6c1c1ed0e7da5b57b", + "d284c831cd9d52793977925ce3111b68f36d1ea456e46716158fe6947fb6d635", + "d95b8d2789fd06cc764d47cc7e0697404220ae8f938ef47dcc9a1bba299fbb9f", + "33599fb1fe313b5a2680b126bfee93cb097d938915d425a63e307e5834479b95", + "05ba86903fac2f02ef2e152072bc6067008ee8ee34ce55ad460801303373eaab", + "0b757802e56728b6d5b28e671a90de573d7251a47fc1bcd9319b617cac8e1463", + "b4a2451e725e4cbcdf47fc736139cfe29c847e2f75cbb7653d80c8522bd48a6c", + "8dd800c3a7cb1ee60f52db3bb69413c4c4247b0b134576552c675970f9903c9b", + "4de21129c6fdf5568040346bbdda272caaadd58218ec6f1f152518068dac4d4d", + "22f6927eae0c3717395df6483d756028839fbf0814f8cf0391159efa65dd2854", + "192d26fe0bc772c705c2657dd5c5033ac41ba408793e6c721f74c53747be16e8", + "9527b8e19570a16ce2d8dd3620dfd915e461bccc82340fe682f7f811b978814d", + "f84039ff95cd44f842d7cf6982a726957a89d90bb151a368e62770720d3efbee", + "b5b36dd84b352cddf715246d0463984b9d7894f50b566a20042c4d485df41946", + "592e1bd733a1095b9e5b770021769090df0c001260803fce9a9b761ba697a5e7", + "dd2fdbd6932d8b56d6ff91db3ed28f282a6ca01588ee5a552d061da215580411", + "82f95ef71d4d0b32e3f0cef89e6e90a8c271cd5536b38c507fc69b354ab51062", + "1800903b2d5f8483a12f0b98a5b35ed39d6cf8f4a52f495395f1816d6e3c139c", + "121a2c2ea70a6986af8bde71b7f75ac9f9e1189eeb9756395e7281702f01936d", + "8578df7e8ec4b068f64dca679310894afec095dbbedbefef5944c3a319809b8b", + "5cebde1dbb352ff62841d7c50c3c45cd0fd1b7b3ebe884a7cf29cef6686695ab", + "362f7a73f2620ea061586b5edbee01f9c026ae6e0d95131c570c44933fc41f10", + "3a19b5976236d44a3d1d6920b348fcef9ec830aac56aa672353c711471ebc36a", + "842c43c5293b03811165f1ff7e04c189ccd355fe08d37808a843a0995541c075", + "0ab1fdbf7117c88aed3f882d5f7dee8964c9246030086b9ee3e2ebed4eb2c731", + "7cf8ca4463d4bd500cc12623ac9d3984cc48e7fbb98bebb4a498c2fd66d54cde", + "631676f3369eb479686256cef33c4fb061c212cc215ab442d8f7837c039ddacc", + "2fff6de5386b65efe6062cc2242b692a1e811a083612ab79fa7f7517f1488767", + "3c7d8b7f2a56e22ade186a70a3039b4d4e4e3c0be6e661770b0a7b2ef18b178b", + "e030882188281a754c00706f621084f96869bf95bb5d005ccc7b2649fbfc747e", + "072cb899c3eb621240792a39bf727bf59398329748317d5ca1addfd8ee2f7e92", + "37cfa6b7214e99bbec6323802115b9615b10851ac85cdc26f213b9532fdefcb8", + "68b14b71ad6b5e2d41ef47fe1b9229a0b8297ede0812d1b9cb95aac185d1a5b8", + "652ef549580cf412bb217fb15b2a0a1db8feb3c79969b95364a80dd97d565f09", + "dd3504b7563a65115a050505bc88ee23a50e37c354773cac329355c16c43d839", + "6d7f05d6ee71459d2ea982f7a5cd4a251d7466d4892e6c8b6aaf45eec66472d7", + "14c28adbf9008d0cbcc978006b6c1ec7be51a134bd5587e94ee33844b62c1ffb", + "f9c20067f16feaaf3f8daa8456dd9334725ebdc8b49b4f74f5a02c38ef5f2804", + "e2c537948a01054b7a0d630952c2ddd395a3a265d618e22ee9d99d4597f82322", + "70e3a71945fee29350b5b10a8ee8dcbd19c2f734e22407bb6b50ec3acb1035a5", + "effbced848df72cd467b1ada7353b1ddc0b7f3f08190c0f2fc747f18303ed9f8", + "02d6eedc5abc295bcfbd4c30a8fa3406c8256d8e51f4714800b4aecb795a976b", + "92134c6158993a0390af92565e166418561c9b157989d02304b68c2ee1babe14", + "6065e3e3aff3e2b2f33bf4af9256e9368b8a303335b0587c8cc9e31cb9db42bd", + "732fd2ba9efabc6d79bd7404c1c6f1d5c1f14c9bfe02ee1320621824847935c2", + "209f9c958b5f4813b32205a44d05c83aa1009cb7b9c7343f0ca3717ae069defc", + "1d274fcb65b6d4c846e95030c37851a97d161f926ed88f6c4dec4ed7007fc23b", + "1d3787106cf47e1e9d08761a6c6f427ef6c07d4a571f23d340ae2d31d34b1d8e", + "5697a3f56a0a2a6bd6991ba05559df9b526d5c42e19604c398548a7cb0dec0b5", + "8c0f581f38238215f10f8017e3d234cbda9d94ca017bbd6b076ac8666014b3d3", + "78f061b0fc912fbbeb0d207b92528ad78ded55ec145c8f18133991847fb9cbe9", + "1e3555081a869f3aafa3d74d6b5afa942baace60869ecd4cd33635f84602b715", + "4f0e36ac0c4813db3fdf79d85ef0db89150dc1be2c879ba06299c049288ab8be", + "6b048c38924b7a1f970cdff081124b739202b703db5c17bafa4ad23a5bcb97e9", + "a5f5e9238aec20bd706262f64671ca2d05961f55ca383953d489edf857e4b2a4", + "a047b08e2af7ae5bfb9e54f98051ff7cd5e7dc88a25afacdd9a324526cd33c63", + "66ae2b9513a0e0ad125001bcd5f3a36e18b4ec009db4020ea4afc4951cb39a34", + "621d8269c8f93b6336f8b7480516d2191cfb15a1d9501463771d0fb70c1afa33", + "76b94079066bff52a077ba1d1ba8cdec4d6484d1ce2f7454b2b04aa3d9cf52ed", + "33c6a108f9d4fe054c085991d89d2a5c32b8f108c95b0e688c5763424c283e54", + "52575ed2ad84295a070a99e7785ff0e4dd32a93d3830be7f7c2811804f3df2f3", + "158dfe014c271c148b38459ef16b49cc3b5c6bdfe052795a6f6a6aacbb3e47b4", + "d471729cb231deeb2f1a67f91962ecfcb9e8c66f66b6b79fe1117d34ae86a7c0", + "e086b1a09ab5307d953e25366b1acf7c41047a926aa6a4d122c5a34e3e0d93d4", + "d541d825401a1734d9961fdb4f0e9ce7af220c6929648e0aa2e885cd28696343", + "a555af7a52ac07e681c247f1d6597f8240d28e76036bbf3b2be1f7cfab90dfd6", + "8598fe7a4a6c6fa865c0ca8e8e65c96301c80965e19e48f5d4ca29ac073551c7", + "70c9ac61ab3508a7419ead73503d8fe51b4ecba73a55d4b167cd4db6c9b0cb8a", + "e594eb83b38ad916176abff3f0ec16d7a814a461012c8f9bc8286274af3f1856", + "fa4e0df6df82f21050394fc2caaa8a8713b131d0d1f7d21eb92b56796095bf17", + "891711c535579b555105e5ec18ea0f2f610c6fe5126ff0f6a4257e99a24033c5", + "f192c9b2c49dab7515d405052f825abead30ef9a870ba3bdad98c1d6256be461", + "70c22f6ceb81744c83eb8dae69980f4583c75431457dbb14f5ecd6c9fae3e63d", + "7d563f44bdfcb2a10ee053f4c05b8d46fa90d44822775f97ee8cfbdeda7a4ca1", + "c09b3df69eaa232a9119e2fb805cdccacb658b2fb9c7f3a2106c8399aa928033", + "15b4b3d50fef72de80fef6e3351dae0f203902e4c4db8ce8d8737b67aed9104d", + "c9b7718633714b3ce1b489f13e53fc647be53cba7703f07a35a8815245b23ff3", + "2d344af9852fc2451ca5d2ed7155e074efd2c53ca9cbed7f48c74658967aeca6", + "c8b778b5c1fc378365d8b342915215117c3d365e12f61f3ccefbe4091879e373", + "1deb076e44c029027c1aa10000305c3eded5d401a036c21a4dd3d3455c0c33a1", + "d2d2dfe410cefbae2309e746690818e8d52dcd80ebd192cddaa00204b6833d05", + "d18a0a0bcc678908ff00b236296f0a955a9aa52a802ee4175849d0bcb3d6738a", + "7af51e08357e455e4dda86d60da75899b5ffce4bebc7f96037d1e404b1260ebe", + "65b930580f9738e1a7af2ac56faad5bdc6a417e75cb872b7f62513f4e8e36b33", + "71c8e88c7c478a4d919d0c80d351331102584d032d82aa25510801e18bb5d6b9", + "2166bad16a6e4a1f77a22b0fb13f7f11d618a123c3c077dbf1ed1504ed68cbb4", + "717a3a5903c2b12aea8be15599ea9864259a964c0139125e649fefbec763890e", + "0d042f5767f6cb2f94526b5b6da8ef986e4a5927eb5f734bdeb6c6ec3bb8211a", + "bb67d586774ae9f3653c609dbdd98f216315b54c0ce24ad9e359d0c2c21614a3", + "945b2962ec44c54ea25e3e35e32a2362c130fc3a9495b16871e8a2eadbe457e7", + "7d5324e5e158c82388cba0d497530c6de0c1ba97131688931c03dbfc60ca7541", + "21805a8f8d43aede3e927b80ef04924d388e96f3cc3947a0dbc1bd1b812539f0", + "27ec492fde5fc3d3784359d9a270b735c1820bc4a6f15ac87797d97de047a6b5", + "4fda2f855ab08a8d4dcba1874402e1662315c872e3ad55681fc4d6c5a89665a3", + "b0dc69204deaa17ac4e0f197f8b23211418b5d54f5bac55414445a66c57fd6f2", + "209262521ab45331639a57523f2e3bbf8250052b84fba81c50c990166a6f58b6", + "b02857949ebddecc58aaba80b9d635ae95cf71f4bfc5ea8fbe05fc55d4c73e96", + "5194133163b28182a261ce0473b1f3ab991a53a08d0e813d3bfb533cfaf1255b", + "c56fe98848fe79124ae1ee09d1b214ee87da51c1440134210c67fe15bf6d1bb0", + "851d949ac91f439ec84f07c481598728ca93acf79cb6505bd77ef776559275f1", + "eeccaf0cd29cb002e2140cffa9fe2c08c53ffd73b99167f4b5e6ab5c94481066", + "821f1058ae54732723937c5ae535cdc0a9ce673eec7fc02bd03c240c93aff24c", + "de836fd21ce91d4a6add661f5d259d0ade4e0ff927d03f67ab5d0b24bff37242", + "fdac0470c61d81d1498e4e0c6fc4ca5ccd02f727413e33c84478ebe844feba13", + "469b4f5af2b2377dc09e5824af17c57f1dd8fbe02294059e16641c1307eff125", + "b77348b40d52d531378dead61cf2eb6fafb4c89f85285a98cd4b6032c0e30a16", + "2cf9616aefee83eddb2df0dda1a3331185f0b772c3a8a62ea3ffbb325290f32b", + "2d23a9035ec627187108b9d13b24134d69ad7cb6f77d2c3ab939db17cc6f129c", + "4925a7008584e04fa769661dd9eb766fe2ea3b66622691249142ca155423f368", + "69090019b3c1635da9d9e4661f55863cb3ab561bcedf2632360745e62b06afc5", + "5cecb744c3e65889c22e14a58e42bb69d068c721f1c547c8c704fbc688c6c6e2", + "2f7371ba97f17c64c4c487742f83755ab61aeb1ee537bcd5f16beb4440d0233a", + "9d1e3dcad79b153296a67238771b91a92c430432a9cc9f7b2ad7667fd67b4de3", + "e9a34ca0067dbb6dbba48e736e1d32337cca5e93f2b76229fafef501979d84ae", + "4d5d36724a2baa0d73657a140493a9ba95e1de95cad74c9753eaad785ac18cbd", + "d135fe3d44b6b0875c1662ecfe33556be0f1b39bc8279dc8730d5e9e351f36ae", + "1fb8439b54ead68f72b7ba4fcaa5041f38d313d92826b57dbda299dab06ab716", + "b96e8d59a7c1006f15b0ac10cd35c08a8008e0c2ecba26d87db3f08c0f6fc991", + "73300dd04c53d270420023957c50410147ea4a2814085d3b98ef25d8d44afeed", + "6941323a74081c1ee28846c4ffe3b4023b29eb3a3b64d0d2dea0f47349fc2138", + "f7cebcfc608a982a3e11b3941abf482d8108a7d59c3719279563788173a2cc6c", + "b76c687ff464c50b58e4231379c5105470264de2826574e8d7fc58d6889d0753", + "f0148c800120bd34159684196bdfb34f08d1af93c74232dd9f59e2ca0a66aed4", + "51e0334c027b5e43e7cd557e301bceb0189903a189dadec2a11c1e2d675e3e9c", + "32a3f110862c9340106f2dbba85c8af5d7ed575635d55cf2af7637a59c83103d", + "b35bc38a430ebd8c7584d0c90bdaaa15aa252b3977fcac456646e839746f9c0a", + "87ea74b899c0b929e83bf31a8d146faea3d26140ea99a3d0d3e0be93a4d6eeb4", + "e222ed1dfcf6505fad77b9cf2e3e38d0ea86fe89a969b88e729ebdf2e6c63f0e", + "d5e92da54ae3432603ea96cd770f4da442f7dcbd0a4f2d82774a33675690297e", + "01e372d5dee08beefd03317d0897d5ef1c5126bbfe00c8967ec9aa05523c5e25", + "369df1d577f66ea1c93b58b770783a74606907713b14ad86b316319d94f84ac7", + "ef7e648860a8b3490440f2a8d541a268b4613f4a94c0dbf685df5973b55f0e88", + "f19f4b29d4d7b70abc4624c01a4ef7ab772bd4feae8cb33bb502f30a11dab29f", + "8ccbea64791275ea047a3648b092ed281acfa21f62a4b50ed111b954e534a697", + "d60a6b4846f218a8369db8deb45972760a1df33c7532d0ef1ae679ba0294216e", + "7aa4638be336fb77bc1ac5dfcd3f5ae002aeed8c42bb46475f718e3c278f0097", + "06a82a57d7951a5629a1153240f6b1f09c150f0b9b777d7c64a0a628e24a0bdc", + "d23d2ce73e24b434f2fe5a6eef2b88eaed4e57848cf357055da6914bb234851d", + "0dfbe791dfc86570004538c64195a7702e33ed121490fee5819537676a021932", + "8e2a510bc84d4600f25812994dfd5b4566c281a153e459cc9460c006ababd093", + "cf73a8cdabecf4323a194f3249cb5b1674037f779c747690eb0d9c9f703f7c8c", + "a8999928014db24e80a27cfb572eacf902b50cdac259b5be80229fe73260ce08", + "f2a13b90ffda09f999ca5d7f9a2cd171ac33ee2a9a889c70a2e2d454dee0ddeb", + "a5fa0377e644e743a38f0b427adefab90d218ba8e9e2894fc1f1f71cd815a92d", + "1a27dc6f452362eca3745218915d24726b72f5d6963a9c41a34b9b7a12aeedda", + "74657a8b361a7d235d94878cf2ac2cf3e18dbd76fa1a88440e3be7ac7a8dc54a", + "15059499248e579d66827fb6700d3c31ab3c1e824fd0b0de583668631f31abe1", + "21dab269f60f67bbce69447b7fec674662bbfe2bf1fb998e164d44e92086a89a", + "a266bfdd44a5969f46985337359fe5ebb6083b8a21cf310ebdf4a62aa16dcb89", + "612ebf41822f25ab638867c1232596a83de60c5bc1a62aea81464cd87c50a53e", + "7c7e8506946118e3af1f5c36f94fc5cddb945e6346a009d21934ea0a384778ac", + "82cc3dcfa10188e9cee8d84a60ab86cef7b564208734ebe8d1c6f4fd583e5466", + "7ec2dce8c901de5c5b5da218d4ca6c304c4a8691568631b1d9d625deab025e8a", + "baf8884aec756bba04cafe11f82e84d5721bc66689a59418e24dba0471e5ed9c", + "2033109b98dec88046390619d056fba7ab1e207591b049b5ca4e5ca7905a0736", + "04337d0bc9948530c0f6bd541442a14cb8da9342cbaae84ffd21f49a3a4ff9b5", + "95a390c9ff01592aefaab09d6409dd618263bd596d5134e024d2e04fc080f9d2", + "c314b11e8ab0957c8c11770b6dd3bf9ba8a281c312f0c25d0dbdb0dab2bf6176", + "9372c5731f8e38ca30845e277b499e77dd3f5fa442f5496c4bdde0512de1e922", + "84ff2e4e6c23f0953aec51c9d8642b1d71c653731d7585fdf021e045ebaa0c11", + "c4472b40a9ec2b01de30d79bafcff2cd4853edb77724a5ae02f4387597deaf53", + "de59785aaed498deb953dc239d06847baed05797fe3dd2420a5c6f35a4ddaa1b", + "9622558b5b744223868445c32fd92ff2ba0d9c56f5b34bbf9de7f3e26206a277", + "80887a104ef050b442514ff8292132f050bfda9b640a840c3258f01893e75f42", + "cbfb1b0ad1df2f84e6ddfa190362225f9306eb62a9f5541cc5deb5da781fa213", + "b01734822f1598f9edceacfe3d828203c629313c7ea0f51111f6b10474e070f9", + "a12cab7ec7e781e90a49f837e6efab253d896dab615aba3f6327b264df5b91be", + "ff54a2a3cae5ac505347c5577e010f713411b5401bff7491cebc18b34592683c", + "99529fea4b50b9b8bd91aee8f8a25cf88dd3612ede12610acb24e73a1fda35ae", + "679c55a753495d26c5ff8ee7e9dd2f6329fe76bc3bba1324f27ff5f3ecb7f8c5", + "c805e4ae5d2358af561cce2ab0ae13a83b68a80886fd11ce5e8200b0194a0085", + "5fbec47c66e62ecc3c483c440f0e39711f414dcc8c0ad72b17325bedb8bce577", + "c8e43b97b4aae188ac288e16f856f16e839e67ee149d8bc8f8bb6aa05a186704", + "ff4748fce9a654b3876905aef033f19030938880da461ae9e05bfe84698658ff", + "c6e39da9593ab92827ce0e81d29fd85003d110bb2e85d530769eaeb0b3fcb122", + "0a1cd405a53aeeb8c03e69eda64aca2dc6df3d599c41551e0e71067c2c3299d2", + "41a3b0883541b8e28e321b9cf326c99f07fe0bf8354e080539511dc27d382728", + "47f3ee76ee3a53b8b6c84de48dbfe40d4487c0f1e024be7ad735b4cb01b240e3", + "4605541e0a8b64ebfb694d092456455e366312df0fdb6edb975d5df6738d5f64", + "f140f701e73403c39925a8ec3bf6fc033df1de9c5d013a0b866b71808f77c3b2", + "0a9f71c5ea3656600032d72147b1fb795f1ec515eb0c0e9b142a51b63ccdd236", + "773a797fa4bb49ec882c2c972b71e509bcc094b89083340df1ebdd64a68f6045", + "bd004ac4dc73dd5304e33abe88de5b3889f233d1b27327b14814f2941f47131e", + "5c2b636057904e70179efc5366acf56b238780b1bd50d5733f3cfd06e4a894f8", + "30cd462a2b16211e365728808b2dea5591fdae5f8e7f816300440e255839d9aa", + "388cd1a37c37a2e7fc75da6363e894618c7e011f6db275cf64011312f3d377ed", + "f88beb132412107250f73babe32b820fe6b277b2cf21035bb4c36e7b60a9585a", + "c38eb4323358d7ae5f9aa8d459dee525427a9a490650246a2e8104d1346599dd", + "212947c08a738977407b769756f1f4d192eca956b07413bb3f702402aeff1307", + "c6bb090f1ea6e4350d6e71cba3c096adca356ce16d997f48dd081eaeeac50267", + "2da0a61f2510fe2fbafb40b27ba5aa8d0fac8254d7d69e475fdff6f3777b4d6a", + "785fc19dee1f3e84307dc836550f5ace30b672460267751e6fcd5e33ac46c296", + "370020c80d7e1f3f53566b5f51931a1297fa1a4efdc81ccf7c5ab72e2577ae40", + "259bc6129f4c136073173a83e0d5d5954203beb997ce140d8f69d7b950e0187c", + "11cb148948a9c92a2c962f6f2da0bc9a024ce63c12e62aab3f0693847c23ad0e", + "a09df031dc5d362e71b730b91a0e84677353406309aa06fce96f83eb77671b4c", + "046bd72f9bedc26c0ffa2bb7bb9679b7b1afc7c9cc72a0de2da2fd9e9f08837e", + "9ee8b7df8898711a797fd8816a32c181e641ae77f9d04f69fbccdcf5916cb668", + "98813c8870bd227cb607e97a296195e673829e66f6c18434e852f36719fe823e", + "5ff5c13d156c6c20c0018848eb761cc24a1739b1a2964e279e1841d587ce3441", + "11caa777f57c824055e6cfec2281deeccea4cc6a9a0cd971ec1b9ea7de49cbca", + "3a8e126522f84bb01239c8d1c51bada77fb0e3e76d51ca82245cbed04dcb7739", + "860a27fef1b6b537a6be688753599f84f29a2600f462bddb5180f0575cf10bf7", + "0bfb8295a95da4b08f084135ce5b221f4e5ea4cb7e63d96410e1b0161f0eb5ec", + "684b917015396fe12a0b84845c805f3e07534070e1a0410bd6319f40704fe392", + "5809b9f126cc43329050dec45c6ec7215c588ae85a0647a23816a73e2eee7e8b", + "21ce0014bae97d4599230abcf322b031d49fee0f48e578f173b317478f0098a2", + "79192c7dec85ace4a9cbb6daa82e76105d10e988ffabf1050a0b27e1eddc706b", + "9a4545f77055b0320345d7f147adae0552191e0b77ba53fcd82749b5ef45e334", + "e154afdc575fcfec7acc74e634eb767a87354b7e4c9277d6218174919f21fd7e", + "12c94ad4e5fccd6a8a24229298b1aee2efa0f231fb209aaa4581a5ae970e4636", + "cd089347c67b34ab0d80db569233b6f42792d9fbb08eddb769c5d2c06c03b78f", + "c95de3ac861cb2435096b17b6a6e96880331ccfe730b15fb14771ce6414fbf36", + "b355cc801fd8712315d0247517eb2ff1ffcf90ce66e786f66c0b70b05c3aac13", + "ead6b1e3c8ce5380567394714a8d3e71b739a128872a4310fd52ccc0cd015bca", + "b2aa10099a3d806f3b5fefc4cf5aade2c91e518a93a03eff4b4bfed37aa98bc1", + "ba727d2be80bdb3b47b860b7588a46ac93984db5de47ee999ad946aedc411a3b", + "5de5016468e7f8b981bd5f1b04b270eb7c5ba3665481b9f2215607137c5d6ecb", + "44a38331d4f01c48fd89cb306be7d40910f61e85c01efa64be56007e168eb704", + "876407f1f2289e2212c3f09fbdddd40e4d99331e618fe1fe76e54c0007146cca", + "3c12bc0d8d061735b0b03371c4869f9ebb423eb84ba6a80bdddf863601287190", + "35426258f74c650aac3cacfe5294871a989306eec1f93c85f34648d3e1e64b57", + "24b0f6118578f557278af2deaea6bcff73b7c1fc2c034f0529b804b5a3667f7c", + "a853095e6abb3da36dab40eba98107044c9bd706a59569c27df4956af6a2867a", + "7bc75c278a522c3f2fe32b83c4e5ac1f002b970424ea9534ecb70519e2ffe1f5", + "968a9fabed55c4ffe4df25e7aa61da56e67bb39f407d6c138d8949f0ebd2d654", + "c01b876bfce242254ed98ebe8999d5bce9d6aa8a0afcd643f192c9c58ba05697", + "0f25ddf33af868397a40586b9b29c759b5f48c5d5ecffd65c8025eae40cd455c", + "9b2aa176b8978b1a6ed7c1a7ea238fa59414085797fcc91a2f0bcd4ba2a56b17", + "454807437182e5bd112654fa4dd079fa0ec3d701f61075d1671c6300e77cc3a2", + "dfb43a5c2c31241a80a316403612fc16007a8bb04652eb3316bbcf1a0f8783ef", + "f0c2b72b9e3f3e0dda04e2c94ad826890cb8fa891b519b63a1646fb00690772b", + "8fc8c2fb737c13eee55751ce1dc798fa291d428858e1cd862eabd89507dd0f65", + "a84481a8966b29dcade6a3e58692ad8868a0b729eeeffa1e25cfc95f7b3912db", + "91c8e0ebbee601828719aa29d7664b5d94e3501f3778999c60a975ea74bb00f8", + "86862070933c5b4cbe7d1b29e15c728766079109dada7f51ab02b03796ffea61", + "2aca32ca5da86a14878e221dd132cf11e9f858a1694eb66c29cd245ed49a81c4", + "621a41b83f8e9299fa89f2f5245ccbe34ddf352e1270c6ef094161708f86edcf", + "23984583b0ab9321f11a5c054ca891c1b5940ca0ee0174a701ed91d8f7c52d33", + "69dc06a85efe0e51884bb0e8215d50ea2d2cd59ff9708079e90434bb2412b217", + "96bd0d2f1ff8360f4846e79d20a31b6eab3705f52fd737741bda0568e629acd0", + "7df4dd373b3fac18a0ec775c111b47f96484f3270a6d5d9b1455d4337c031b62", + "65b585500d0e4f021bf4f625c7f48bfb02d32e9e532ec9cbc0c264247f9c9446", + "28e2b11230c728131c2312f86434b934b1e34e985c1601fa17740b7820775fc1", + "f1eec6722e3a8a3626a3b9cb45ce9c80fd08bb7e71c38d7b637aa986c6206273", + "cd5c00f5aa138760efd51fabfba83c0964c47eaa2e54f95b952b437ed66a5a80", + "16557e57e3b29f120e52c5d9c08f07f93340bc79910a29953df8a4fcda6b745e", + "683fca73c707ae662b516a2c0e2793e6a764b990af2f2ce8c3151ea75e9379af", + "b77d4a94dbc5ce109210288aa0cd3edc83f671ae9a9172495e705ea902ba90e7", + "5506fe1e6126cabdbfdeb4cb59bf30bf21edc868046b263d692345649b3fd6c2", + "e94b83fb44f81d60f82a9844edb2559563ffc876bc584f4fc4e11049237ac60e", + "395cc58f6dc01018cd334bf6ac29b8f5ec3385ec325470b408bc2e8d9af11755", + "3463442e45c1c1df952e9760f91b99ea8a0fb604b203c27f03a955fea0381ceb", + "41f4c041586f3e7468fa44de1ea745e3d41b9942dc19cf35b5565978449a78dd", + "31b31a285469f516015a5045dd8434642b2f55f19373b9e0d5530e843deeccf7", + "943b282f0be57bc8c9b5ad4f47aadfc0a439af58522a4495b7cb36e0ad43a6ca", + "f1d7d6d9dc97dd43e3b76bc13c6973e31572fa1792335ce0387ca171236838b5", + "01cc0fadd0cbb9e8aaa0dd49291e39f3f325030943c8041178738bfb662a716a", + "d96ec821e29cbeeb8b9f25f7123c8d5d708159daf8acd12e7a800c72521a4efa", + "b5395de909f1cade846176880210f4a560ccd3180f8b70d2d4bf15325f196ddb", + "e447d8ecd54443d4674c84cc84763a18b9161c76399eadaa82dd74cf31535064", + "8f47c23e998213388a6abc7d3b5754336bd0eacf4eb545b0825abade1ae48feb", + "b2b553a0af737c3912207071469517222d3d3704c7c45bed01f3cae1b50c54d4", + "ebd2711b2330377b5d7fd2f39a720b896a7eef7d7f0ac2a1d0739002a1511a00", + "d62a3142e4c4a2c87e8014012b804805dfa6f7d5da1a270f7c3401479a3b2f51", + "0142c36fee839b588918bdadaf8c737f91d61b920dfc581e05335f6f2cbc3310", + "e729c42028460ee33d01a15c2e369048bb79f8ddfd70a22dccd43b1c58d41933", + "6fd227ca8a63cd5aae1cb590c6d3baccc3518306e22db68095de25f0ccb4b49f", + "80ece14b164adb418170ba9feaa4d352cfc6cb1e31e47b3239163d2cdb54eaf8", + "04205ac29beea2aad32008fe47c9acc02e3ebc9f66c16db685c8f0976f3562aa", + "071d3e6a4659c37fec52601fe99ccb4d2a2480f3a4a7bcd16f12ff21fe84a666", + "57f2fce9dc52bccdec3de4763a02e3700d13953003190d560a8ab306b81c1221", + "076c6ea3b84d7f2535d034e5371378e9345dee62947ffe4de3185d02a2152340", + "0e386ca6f2161a1c64a4072ce7a507e1131edbd90446723d84a41dcddaed4324", + "48042f945a825bc8cea4e8fef1da5c85b252054137adb5e123dbefbcc189db93", + "410d27dddbea610ff9c4e6354b187fe8a2f7c5b19c5563e2a1bca1a4b4b8c6f1", + "453ce5c10b3de7a4fd1b0bd5f96512d05fba41117299d588a4740585fc295590", + "677e34874e3dde27ba4a987778de7b1a64b6c63051f3cfc774ebd4c3fc605b7a", + "c9bfc40e1be4befc1b44e38b471167667f6b12457d5c82662ef3a0a0e9f66e2e", + "4cd169e13f4aca41b158924d9f1b972aa4d167d6d9c116e7094a7a06d96e6387", + "aad5a84ac08dcbcd06b4fd5a0c5d2c1af8eb0d6568f17b3d8cc47d064b72a3de", + "4d495f70907d93c7c1142990927f1a3d59e3218a34db881714250ea6e9aea84b", + "8c43d66f0d23b88c17d19aa01c64ab1d2af7c43704952326129b0dab4453cfcc", + "c12581dc707a3a6da47886aca1c2aabb5babcc0e0fb21808c27c6d5a939841db", + "2c63aef84a0c812b53e37656cc6d5e8f64f8e4f8552e4e502dbb1aa0319e13c1", + "86886a41f2639a791085b6a2be4f2ac4f76d951880eff5cafc722785ab4d64f7", + "73bb646c6363f96e1ecc19f5ddf7df417327b9089c1b70a867f799af03cb3d66", + "ebe518fccbdf53bfed56fd1f8f3b114b5849a8fb6e14ad7bf3f2a2baf29775c2", + "db82684a4b314c554fa6139c4c7ede0245f68b32a885c819efee12ab0c027414", + "5a985ff0be58aa5a46a0f4122cb70c7ebb937f7802a33a32d8262e63b3771796", + "938ffa6e22fccdc71d6ca1544cece54518b9fb75cac5736a0468420cb9f504b3", + "4726ef072888e9d5ad65342790b285c7ff024d60b084c32377eae6faf67db536", + "57292f406904048855148e3dc04cdb08ce05a58f174867de688b252e448096f0", + "ef8e25e005dda6d6cac7947006e1f688c743efacb7633c34ebbed7f87707a3d7", + "b90e5ada245515dc90c2fec028ce8359708a610927c3f9cb92bd93eff2fb59de", + "32ded7b174c9528307b8042692bd00480d31db9683c9476a4f50085aa6810893", + "532a40b49048d2695ab6ce15572377ea0befb57bf6311e5bc89a3b579561837f", + "587782fd3913fdd01f9f475f25e0704ab834d89fac46a0be1d88e54de6b50701", + "b91389a5cf8a1c10e890ee4c740fe9994d48e999b59a2c6320397a389ec7a316", + "6e737036b6c0c9d72a72c8545cc835d4cf7da70c5ea33f6c6003fb844565354f", + "2f239f75caba3909f2b6f059a782c2624a71b591a7e132ef72b4add3c9c2d738", + "95d4dd64bcc89d80efe84c9fb5d4a17ac7115d1b38a0ac0262e7687905eb1c4f", + "4487d612260e1a6c676d2967cedd84cc5adee2ac845ab2900aa193a1b86fade2", + "97edff35d1872b6a95f43606f084de35aef0292cd411541243bf7336a905a938", + "9001c5d627af968d68f12200662585921aab14619935c8be4775c457e2a40867", + "ba959223c1636c7b0d10b43b78c1821e7af6f6aee1f80337c555a470867c5ad8", + "9f8b4937db8172a5e01b31310cb3b74824910f9c52c356c17035d70173794089", + "1750c07b09dfc52f0cb0f09f4560095db135127cab4c511c525f560c42025abd", + "a51ee98093bf11eb0455cf106b3a4e027135e870e3e5fb212c42d62e190f153f", + "35501a82a47804cf7718d6bd2e4bb6fd67d7639984eade0c1232bc2c74588343", + "9c3192c2435a9cf815b717ecbc6c539cac03c7f582dbda889b39485895f3e174", + "ddf04cdfd5366d08b5886ef736dbf547a403f583cdd7c089f9f3c4239e554210", + "2675b30bcda3da6a2cb6837e614ada858c1bddbd42e6c3963eb220c290fbf967", + "dbd9134ad3f09406a76eba91323ab3ad2a4fc0bff4975b2cc480bd4403ac7560", + "2929520c43b14ce24a94bf56870520ddfe4d29bbcba0d0e1c4624292d8719832", + "f71513cd36ec057435881587013b2fea181cb57df1cd9798e9b4bc2b0457e778", + "b83971a9cbba7903239a1d2a5907351c61028b952d4dd207dd49f6fcc9abc13c", + "c3b7e679bd4d0dab1a2e7938b0d5cd8c28200c5da9534bda97ea0f1ea81c4aaf", + "77ccaeb09a35c5a1df5bece681062f6ce9fea08bbc2c2075ae235946e8e1d9d4", + "51001f07d1462cd9312b077be915dfd5225b85c3ba760e2a414275aba5fee655", + "ab0f6f8f5e58803093fff168c38de2710c2aff8b47661eaa3ae2a337c0e71ecf", + "96e9ce518bea74432b7aa4c857b2e2423a4b3f48275fc4b48f87caec87800b87", + "60c0b7a0ccbe7792b9332ad6072636d9790ff736e2d83a12f50e7dfec65a2680", + "38175abae6191a78d957e3c63121036688cb6b8c46d42d026551ad37a6aeccb1", + "48d82a8ce43a65f567584bfe7fb28cf8c84bc725c73413b8768d2e6688ae77f9", + "18621d94f2733bf9289e0353f066c53d95ef8dde449b2e160e28232caf17df74", + "e86c65449e6b871ac6886526107b8d7bfaca468ef270172576bd9f39fd208aad", + "c32bde208f9087259fab6b053db25b1b136b38276942e2420482855a5d7ad17a", + "dfb55a5293f934d40ff7c0a6749aad57806e571df185f592044e69e680e995a2", + "8c2a1c5eea4c8dad313bbc6beae0c4d94828fcb2a4877b19761e75c74fc10164", + "eed8a70bc0d94fdd32629373b50eb9ef830282b42dcb1d8b96fdb9b8a1797df9", + "2c4735a60cfae0273c4f309be579a51c6ebbed0200715a826048b1ee568d4934", + "f7e56418539529401d0780879ad1266aee0f0354da07486ceab82011a069deef", + "aca86fb3d7d9fb64e920ee5ddccfa78227a0a3a3e40bb3b275f114186e488c78", + "bd4b2c02f07305164010353383415b08f570fbf3fb297df74a38d6f6e3ae99ab", + "589908e264b0c91b1358f1a51f020daf473b7e4d5faf493b3625782866b476ff", + "70360b520b95938174c5b0c2400da3b047743e60d837bf567767732d748e991a", + "a54ed87456ed361b714da5c56fae7c4cfd92f8ac4a6e8e901a565241a39569dc", + "9df7afdf237605066f98cf95611f49f5c5f383a080c86dc3753390c5627efca1", + "6281424793e144590ddb68a85972c45272a5ca39227f9d24d0bfe78e2bb82580", + "13657e27b82c768942fab407deeb645ecfd958f506a6e02c95d119620cac86ec", + "cc67906e35cda000740c83f50b24b92bd04c58f2a0c45580e9a134c284580804", + "44e92cefa2c869560d185d18baafa780bf16f148d1a7ea186f05597b18783606", + "8ae261dd18679d8e203e7b4c8dd241a51634a84f83855b559f7ba55728d9c9de", + "0269bb4c3079709fb80f3aa451a895b639e3b7db83360a14f078f26816979dae", + "2faf079e75b3e9e0298fb15ef8c6d53715cba427c8c551e98d44a3c46a67414e", + "745a15e40bf06483cf58188b9c7df99e796754dc4bf3eb0547b11fd8016c5c66", + "70dbd06038f269ead55d560b7f960d24c7cb3a9c8864fe68c0daebf315cfcc9e", + "1dda0994af0142dc5f71646735ed3bbdb4f6eb40955836db16fc7524eff22ed7", + "492af127ff75fa55f94ed919386917778ed9c51e4cd193b75d82524c186a1b5d", + "5031aec77ba13f73b21e5cf92708f44c0edfd1081c89b4630a0ee3a50b91794b", + "859b7197bd1a5d6b8dbca9d6c96321e706708cba23866d44bf3f014deba6a4bf", + "3108f998d14696b3e945569ca9b47346b26f8a3700cdd444f545fd0a03968891", + "b7cde7af47f0df23419ccba57da19a5ff7645fa160e136d43c827f41b460decc", + "18cb7236904f9784d611eb67b53daa31efac1a8a9a7d2ff47dc34255ba276a57", + "1670703aaef6d8c722bdacacc31a80dcbac0326dcee64becba1339bcf630e330", + "6ae67bdc91167c7efd005be6881b58d24abde9f8c3a71e6aecdb8e6738f3d169", + "3efdadf9ad79dd00eeee1d80e40a8297254f249a9dde4b4d94b656d58338fc5f", + "d14ad9887a66067658afeb25a64866bf90debccb49cb8213184b807e9f1284aa", + "7f2bb055f5dd834bedc5ba9d63b3c83322e4325de7866b2af7ff8692bb2b52ec", + "729e0ab27d04bf1c14dc28c42ea56cd7fc1a8207ef207ac34e676577fc32006e", + "3aea585e172df036a10a0a3c26ebf4705972eb3efb5b61602231b8a2b2942b19", + "a3b01753e934a16eea44aed4a756b7582aa1b469ac462ccfe4c92268705591b4", + "d5af668caeda6f6af27a778359ce9196ba091cc04d9f9b3928f8f5203fc3fc88", + "a6319eb545257cd4c849e2cca4c6faabea9490eafa3723434800331f222b86fb", + "a4f001d05640267e07caac4cad7122771451f3f997caee435965cbd19e64a621", + "f852577eacf926a5db6921da93b4a78f00f57e06fc064ac02633976e35d9e00a", + "c48da9db46193fcda552476b8c08fe35a73ff1bc1950a41f3b1ec098d2f75a47", + "7b26f8d832edc5d6c025be9189216766b58ab27f59e7b253d0ece4d28f51e2d8", + "cc5b9121f74988f1bafbe4ebdb7296d2255a4af6f01f6967c7391568c32377ce", + "29fe88933391cedc886475d8aa7b68415a7f3e80e153d8837cacbaec3f47d8c1", + "392cf2ab942625fad754d738f33732d3172f4213a8606368330f72373ec99c64", + "b9ca5c8d1d64334e586ba1f0303d3fe802a03eca9c2aa12d2f57e79836d0b64a", + "c47d0f78a14f36ea4a9d1fdca74596a88f93dd9a0e746d97c91a79762420390e", + "74a1f274dda992663e03a2a10f29c84d41e2e202bf56e658c1a42ebed46db202", + "dfa9dcc132701d8b1f4332e2a6bc7faae3c7cf11ac0909a83c4c7e30fcbe7dcb", + "35d4395190d570f8288d255533eca2fd7bb13e720d6e8f5a7113094f97538c5f", + "70a16244e7480d092b617242786deee67056d07cb15365fe51d0e8ab1e8932a1", + "a26843f48f4f2a218b8f5b219147641ca236a1abd96905d8534350e570b164e2", + "5d9eed350175b4d1217b25e671c2f3c9868c8732b6dfa80e62e4a9bc7010dcf6", + "72db3913aa066e04662cc170f3363578fa47799b64561d3b9e84598b43ee8c26", + "e406db4deb9cc79c8eb7d658c382c2291668d7917d73c652c415f37a3f713b0f", + "a0faece933213756d22239d1a066c1730fe58b3e815df52c2de1fca102b03caf", + "9d6bc10478d9345260c53c40de93ccd7b1f344dc25e6e4cc8199e1ed9c6dc824", + "ff1a79b9d954943147a0b29d67ce6fcae1f9f8d5476c2b8eaf78ac575f95247c", + "ba31c30e4a28d5ad22a364042b7155e5f4ee008929f22eceb3fb7371d0c1c2a0", + "797bb553ef79e09eb1aa5400a889bcaa513583662c3708614e29b202cfc3b80d", + "3df07ae6890b2c332556c247ee91abf341c5dce226e38df09f7010b1c21615f6", + "30de2051725d9f0fc371cfa55aaded73b12e0668b1624775b657ae0f41ed8daf", + "035e448afdafd5069a783abcd6e268a82f43a52d9ea805c1141d329557d2029f", + "1cd59a8ac8c61f8f97cfcd1e43d633f41f0b3b3782e3deb3d0a9d80942b52077", + "b1bd938d8288167f4a985abf342f977cfd1c2f1da70c6037c9bdc4295a705726", + "418297a2f0c0e5c3d6b2c4d80824e1a909a61fce6c18bc937720eecb1ce3682f", + "06031df90b06aa84d58da9de78808f7b2cd7d563f41dbed683e74bcd7884720d", + "28961517d508b8e963f98272dcbe636af5f99e6fbf436d14b9b64273b17857a0", + "bac113e3e3572e90dad38b0c2b32deb78f9027e59b75db03d0fb86c49c2d6d9d", + "26a5e2f817b0596d95c8a82cbbfeec6971da2247ce1cfa52bc5098ebdfd3c084", + "ba7bae49a577e15e14568cec5f779c4700c8ecbd21bc8cfbd3aa56eb0948dd27", + "90bdd504836627c40d4c14d54da1b3dc5cf5b6b1cacfe2e220d1aa44850f6fba", + "02f1553ab9b4f4486fbee6f5735ffe0d9b305feae9a120df2ea311fde4cc6076", + "2a9ec46cca5184106d4df225de39422136c6cc995fb9d124bdd640ea45667a13", + "28a25bb686696fcfcdeed1a996ff4f8956dd4c41587d599eb78a83ab6b1358c8", + "422bb78c5107f8f0a5e1a253347a72894704cc4d8d1f60a49a49f8c5dbaaf298", + "74f3c43fcc95c26354129e18241a1e747c78975c4bfbb0b90a2b67c117dd85b5", + "8c0f5a2aeb81ef4e5805c1070bfb8667ae185b59705c3cab2aac50363327daed", + "87deb9c6b62b237a5f83b7333776baeebb4eaf804ef78b9ca2c62ab92388a697", + "42897cf080c2e405a77fd501360a7a86d0a1cb7d573f5fb07e094bb13a942f03", + "0d3b80df67851ee640b906d761fd95ad7e47d0186cdcddc59a10a22e011816ed", + "dd222577e63a176e688a09834ba7dfbc7d25af9cecda7b130e48a25fec558e2c", + "f6f7bd7315404bee4a9611e69fe5d3387de5e4577ac28267d04d69946f42f18c", + "03cce690c88d34689a3fa66b6f8c9da75747ed568e63fb20715def88e7228b38", + "d9a4eced4646f53c7840f817b4296b5bf16e7b3d5e906953b6302d7b8a848dd9", + "319741447aa9b6c819cab13368125a52f3c35d264249d5b4068d2f379010f4d6", + "0a0f3eab30a538a23ac52e4f251d383d418bccc96d5975446e093cb424e8c247", + "550942f02799dc998561d8695fdafc4554c5a89f12333403cfd7c4c9cb15bbec", + "1b0d7536d6774149b4ed8db577268f176e4c1f76e88c9f956d51577d7349cfa6", + "5d27bef5a988fb678c4e178b4b1063c4151c5d3780c1dff1ec2ca898f8f8cb5a", + "6bb4b71ce2a72c465e83586c39a2c1d2f77a47ecfca14b45af365a4ec29448ca", + "938e0c409a837260a9fc27ad5878f3b8936ca0e6219ecd3f9bd4bef6c402acd2", + "d8aea1404c3d5fca7289e2512982bda873437b43e7b8b2a912f383340f821c8f", + "1f4cd386950035ca3cec81cba5d6a506620863056da90c7dc9a5afe47d0d9dd1", + "dac066c881d458341c7db30fde7d9197e498ee577bb4f0e0e48fb859f3790143", + "09381cdc27896ab6c73c341673a61927099eaf2d300d3e00ea37ea6cecb262b8", + "65f65598a48e7b2e38efa51d454ef1f26be016c6a70181e26bf35921998671ea", + "98d991cab4e708f9281d1f56b1a507026a114ab0733ec2dd572bbeaa22fa4887", + "1c2a4e2bf0dc9a09e511baff42c297b321cb8da45c18ec50e4c39ca2465d7cd5", + "327c7a6d05d522536e61aa1fbe8fa19f781f915e20a66a647ea90abed8879fb4", + "cb46cc08d32b5bc7ae9480f30884c7670b8e8694d66d5438e5f8fe81785dcd1c", + "537fa4c41690903a48b7a632249144f154c5dc4686f51868df0159a0350a4ea5", + "75a5e4322582d7e240de8e8b0ab9ad73b7e283428537338b41b35107dd63ea2c", + "1f87054001c06863b66f55b3a897ac0d76f361908900b30e1d8ffa46742bfd16", + "22197f4735d51aa39ef48c88c4d60818245fca73b98df8b9669dc434c6ac8796", + "a2e14dc37362b89a5b85c57b3b48169adb2e6dd5360b8030de7a0e028a6db3b2", + "676f7706eb7a57397f6c9f69de04cb037d3a1045281b8ad8390244b9afc7a2d1", + "8defc187fb1f0d7bfea657619f9a020f303f0cede255b473d18817b500b94b75", + "be1166434669922a764541789705f9d86af0587241cf3da9e33a7983d048177b", + "c5adb9840c2cba41c6e49f9e97c3b78d8d0a1c4af88929e89af205f75f61cebe", + "a83b4a5b3282ab0a2dfa4b1620dd0e3ecefd89cd43fcee45a1a6e4760453e9a8", + "ae2117b3fadabe7e3d94153da7e68c6d7b371efc20373aa8e1d9326ed9a3b369", + "a0b5970a81570a1b2b4d058b32ba5f207273765be398986490ed5e9109d07863", + "9ea221fe02ecd3c974957ec24796afb677672fd1ca770a13ec9c3a94135ef06c", + "a8316aa74116d533973b74f528dac5bc257a3e9cfdeae39db7d8d32f38085f4e", + "efc5250bdfe42346a1d032ef4c7189cfeaf303af28a12b3be074daeea810b171", + "9aa7194fdc5f39e9ea59a13d04158913df4c87ed385b32a37f216c17abfc69c1", + "a17f4cce7a3aa86d9bd5d51f3c38f235fcc4b086a746030af939997d05e54fe5", + "98b049494b468b0c6cdb3cf382283110668d919005678e4e8a5550a892ce133b", + "1ff264905c3f040c3fcaa107b7df3c38ec0dc7d59ddf9ff8780d6aff0381da57", + "ef6077b9e143f01f8b500e2eb6b6ff02043ec00befeb78ee44db5f00d037bbf9", + "ca1af382997fd751c70fa1fea1cdee6ee1d2a22246463ed7867d56e57619aaa5", + "be8045324dcbd46d31a1b24be314d5f0ed7bd1d1ca67bf71ba5cc43043d5c9e3", + "70455e4b87d4b01585564a77a6a414a53338c05a984de6dcd14d7126306e22af", + "5bb2bfe0be18cf76e3f70ab3c6ef2816dd988074d43389a3c4479703d1a61041", + "3c95b917d6409a12360186d7c8839b33d6fb9a7f66f2e0e3a887601927d14043", + "0259cb83d1129316ee620361341d083c43690ae542e5fec01f887acaf2fb04b9", + "9aa9b67c720b3b2fe3eff8732fdc359ab15ed4fe1092e289fc27b7e55a9c5b50", + "1151155d7be1017544c0c2b7432ea1f67cf3191baee64771e9364f3bd1da3708", + "9c45b3560ce8419825ae84affed5223683fcccfd6e35247d1c03f4b988e8f48d", + "a3242aa0a01b82c6d5faf748c22a616b950ecd30ef0f72685aa5860f9320761c", + "0f07485d78018a506a69021080b41861f801ddad09666090e53fff3bff0cfa92", + "e819c861150099438a5ef2613bbc8e4c854cb8410521e9699fe90500a9127443", + "4e78cd42978462aaedb993b6a49a5ba1315519db360522a26add6a17c8adac8e", + "b21ef6dd5976ca60b6e335e78e479a655409da184e68c10e0b4010ff9d208c64", + "e8bd4c3f3e0a8345ae5b2b5748060b15ef0c7529fc3eff080bec1a9a77584592", + "3c8d2c54c7d5d826498d730a289d36f903720b7e2ce1f699ccfa152999b5a0fe", + "fb6e025eec367e2bf4b44af67ea674d767b77397d0568528e7fe3aa7574ae730", + "9236e92010421a2c1674eb0a8044ac2b215b605c0b94b903f3a2872d0fabd344", + "9ea25f2f9472292aba590e298fc5ff15c4cb22608b2f0f2ce1bf26e148920007", + "d4d18a3b2db8540a7bf72e0cb14b176919f995e70f0df607489d5adeedb0a932", + "e6f6e272b36731484782bcd4844087cf45678a05d8b84819f9d41f5376f0f8ca", + "5088a2b878336dd33c3a7c55c09aa70bfb1d34815f031d4b3e92402c7633b400", + "3d12743c540c798ec2587301a3fb2b4f0f6e1278e4bb15280c247272ffed0651", + "52c17f712792472ca80df50f7b9f96f51a6eb155c872ba9ef460aa6d4e10fe39", + "b97f17e90b9fc70044f8789379639e52f97707eac2863a35e9a19376555f52dc", + "19701f39368b26bd3c71a2f0630b5e89586f7249431843eb6c890045f7789280", + "49ea123cea990f6fb2029e8cbc9ff7ba7e049c32db1f6519306ab980d74d0d38", + "fd574068f0c0df88faf7b54341a310446c0ec94931bc459748e11c58435d6ea4", + "fdd6cc2c790b22471f03a32b906635b4028debfd691a0a57f847e15061a2904c", + "b7a173017905bf55dfe03c1640a4e808dd6a1d6bdaeb27722a7c839766b8465e", + "74b799aae53dafe2131b183068d8cf5fa8f2a3006c9bb9944aa493c7491b94cc", + "cef9c06a87ada326bb03e6a301f53fe5fe6d68df015b4d72f1965581545c7fd8", + "db390feffd9cdd7f8a34acafa77d80889a820c9275095e84b439ff5f05781ceb", + "57e17c1b71a226c7bf02687565e92689e5c966f5c864e64d9a621bb6757f4f2f", + "b8c8dc2624b8aa73874647bb89be2765fda8223fa7055796a59c7f4d1918e8ff", + "08a2553e3004a255c9a4135930bd9f8724423747135c0dfe45747285549e4ea2", + "4d4966d172bd8c367d828eb8a14f853999df1394fda96fdcdf5e41d70725ba21", + "23888052d915017c4b47b5307d25158fd13daa02bef1d4a2c11bfef79a851470", + "52a97f2b532139784f4a6a6250b2e61a3989a2c94534c2434041aefa13af2871", + "73c82b469fce6ce8b6b2828441bd31b66784b0994e57b82d331be9e03cdccca8", + "cdbb2bec43b864ec0d7caf5def1222c3cbe20c79d97c7642350dab42b5d1af3e", + "e047c075c048467400806f61489cc503dfcdffc73d4b9b6bfa470427f95127f2", + "82de0f386c63555542e8a34ece6dd42f2a4671d437e00aca7da175e7d18bbc85", + "fee469874ca3fb72f0871a24f410284c04311446f4c5f04480d7b3ceb4e78efa", + "830d8aa08ae29fa0941efc576cb59975d035139cc27213d0a04a0153e45207d4", + "f444da313f6e7bd9e27396f9a95129c83977e1462f86e7a9b5addb1a7ff30580", + "fa601478a412743bdf29bcf44ecd5fdedc09792c8e469a7399bc17c64e134585", + "cec5ef97aa6a39f3c146c5e3f955bc9028798591d4e38094f38f70e981566d58", + "6bbf46cb35bc96854a8fae0e20935f2ed32ab5a940e7891fa7f268d28ce5d549", + "955d9741503c2e5244f513e2c2430bf814201a2d7dc74e2e5c9c9aeed401ebc5", + "9f2e6590991e25b4fbcacbee5bf7381092cfcd7d4f3e258bf4b41b84102fc179", + "647c657c3c930537bc13047cf886869f5a0699fc52496ab30c997e5fea1b3dbd", + "9802afd3ddb4d5067fd0db7ead8c66ffb3ad1c29391ce56321c4b8f34174eeb5", + "60f782f178f30f3d9321017b2c6591f25a82f957ef1c704c6901fe2fc92ed76f", + "109992589b06b8eeba15fc2f6104140fec043b2031c8df3e9025fb094978ddaf", + "091adb2a123d0cbeae7dd399a39f4ca4a3b68086410e6c7aef0526bb2ad2fb82", + "64143c36d10d8d0659f9431dcf30cec804f4487452d5376639157354bb7ee637", + "9602ec038d85b2cc617dbdbbff0a3293ec62048f70b16a6b38702e26b99074a6", + "1c6b6508f0f0c772137ed1a53cb559fb1aa4b3690f6e1f6fd3ddbe5b74da7676", + "a4df5d4c393b255674bfd64aea4ae82da5175732d39591fa671a233004d2d0bf", + "928a67d956422b29189df8c6e88e345ed2cfc8ac941d06f15a7c24cf30bc5296", + "79abadbacc8e8bcf8930e8295dbf6626e3c2c71b8c4c6883318dd1c72e03ecba", + "ddfc0d445cc013734947279fdf7c6f0d474a622da65db4ab1c0c9cea7f60d0fd", + "61ce62f67c3526d4183f74ef0d457216c7ba9913c9a0243944c972cac6dbf42c", + "9cbe363f9bc69639efc73b9bc02264e29f7d0f8ea3b1f35ba16640a5767c5db2", + "d7a25bfdc1e8bfba013bbcdc8afe41d81511e15cece155e9be7bf1ce81af27c3", + "93f9632756d3b0d8bc72c4718dffaac5ef81630d6bd79364b7bbcc9ffbc302d5", + "2311829f9842404d9cc2243dd149bdb26d210031ea78dfb066e90381a392e641", + "cd08a8a153ded8be5d705390e3bf53cb56d7b8c20e0e08c39adb3689a68b9602", + "8d310a900176ade581ad42108605cc6bf867f21cc115e6abbf2a5e083c2bbae5", + "43c5cb40e94874eb80ec26b7df732cea255453f511992fd61ddbf4de739f3b35", + "a2a9abf1c835b824a9d311105a4c92b3d6ca8eb2761b45da7241f5879d05bcab", + "ec48aab645d64498938781156b6daffadc0ec9f3556b7e995c8dbd63299c3121", + "05f6c84dd8ff3468d8992d9dfcb4e31597f7a378828cc0108320c009ef16ceaf", + "d4d39f9d3724cad98bf1466577b79c4dcdca104ecb489e877fa8f44adb1b2ba2", + "b23a123b0bb6c11060658fd14786dd345ad2a2c699e4fdc7c8de79b1eba41fc1", + "f27d04707f4b1110ffd978f8610b2f094ac0f24c1dc819b59b70e1f64cd86a99", + "42895aa8dc4c0ca50b98c7157fbaafa53fc3d8a5221b660dc86837eed7e6d319", + "ea9f13a6e1cbe9c308bf85b50ddc729abe112c46d74b50ef53d0bd7ece84121d", + "e8d250858b5de70d138213342a6765948419b81b8e8ed65b3763b20ddf009fdf", + "827b1279e3ea50b1b7aeda4a7969cdf18bed45fa018e552190e8341d2b8be2a1", + "3584a32e4fa98cc72c35e0050fc00f46462b7286e9d6ad675c340a8d1e56fed2", + "196435304b5e92922c3453ad9f31a8d1b2821634ee6a29c07543b05eafd7b061", + "a86b7271d28b011a2749b302fcdbbf712f89d9f3efa24b9a169e97e67a57e594", + "d40ed1342bca6c536e736f1fa15aa5f5a01274b4208ac4690a4f8c293dd40b0c", + "513256a2ae5c50af825420724fadd88aa0a77d93f64e1425f768f33bc56bf862", + "83381a3fb9ec95f2fccc410ae48e0581ee4f492881cd0c20c90cf19823ffca7e", + "0d51b0210f527afde630887455f5d231425f8a8aab5d8567e5ccd49441f5effb", + "c584a68f8f6a5d8841d9b51fa9cff1a3aee73b1b4f8c8f378843c79ba5ce5902", + "b5396ef2974c2a1040b8e8ff023a82ed4ddbff2b69e5569249d139ec01dafaef", + "e52c0b2f5da5c4c5f68f4c1b7e3dc81e9572bcb6299d3386d54bf881c43dcec4", + "ba60878ab1ffd5e5b85d843ba049d669ec4d0094330147c3942a62f0394c32e5", + "e412b797b242a5619823b6b33008f45bb0b11bac0ff365b4f8c73c9f3fe6e590", + "50933f5388c97812aff9aa52db014d71556383c63c6b1b7ce294195c8efcb540", + "643ee4638b7d9c626522f9178bce2a53a875453252c46ee3ed4d524ad2710a4e", + "e72b8739dbff60185cfe3da8783fd9af56b7da9974610428a11adaf5c433b2c7", + "48c3cb8b5c7a90728fbc104f8b191a98119f8a2777c00f8c1000244013745527", + "34845f23b0bcf4e5e7fac5f82c059cd08d9bd7cabb11edbd62fdf1b40df95758", + "33bcac68cb4ae201b3d3fd356c62feac888ea3f7e0f59ff0dfa2fef97f1eb558", + "778d833969b28ccda7711e0f6e37345fa598ac1a9fd1c6a497dcfbb0d2a9d590", + "5fca36d19c656d347e71c9ca43df0d701852d9811b758d63b3ae6522dd4c0b68", + "74f0e0b61425240202486eb26a74e53bdf8460203352f1c1adf698be9c01dda5", + "d2b4e304a065b74641f42966a43398a96e85c14cb209ff06d18e6fdd8a30e1bd", + "df6401c07b51c72f6ea2654aa3069fb6bf0d8a3adbf00d926d534400d88733b0", + "3850acc410d21b19110d9f229c63d67e36e2484915693f21569b627b0f3395bb", + "6143ef6a8f8edfb6b40ad2314c0f9595fbed1f55fac3b8abbacd72f691da7780", + "b14ae396d6da8ee43a3eaf5e9a06333ce6515cbf1819bb5283d73a68cd39d2c2", + "2ccb1721e1ed7add382cf8838f7cd82106232c7b0bb7fbbc5f36291936fc8f52", + "cbde7da6d0dbf518da6923c7d82749def1260eb6632f03a7e3eaa62cb3a4eecf", + "9a9df0550f1629c3cc3c539ec83ca7e815ab8c9319ceb91f42b6b9dfc3384ede", + "f48737960c6bfd1b4631cd39048b55cad33bd0fa925a8ae094456466e16b7791", + "2514869fcc6ff50736da7f509dcc55cfe8372f82beb46529d757c0ad98fca10e", + "b21124fa2ab9868f3e7375cebd5cf95a48d0d62ed6fb62ef0e0f08f7337bb212", + "84835fd51e9bd4624f4b6feaf2387bf3652f0b03efd56cde086079f86bcf9cc2", + "868492cf4941bc0fd0ca8c37a30008fabf58e56bb3b41f7bd91e09b43914618d", + "00a8ba1a607be27447cfd6f60447a22ff6e603e347eaad21604bf56cfd3b1434", + "e50a1c9ab7655b55cda75ff82bc1d28ea48cc7fd292e5f7c72e0f48acee712de", + "249df64d683697b530c6013692fba966655c6d4e38ba40fd05a282f60570ee67", + "1e66d4f9bf24724b6ebeeba7f2ccce22cc039cba53b7e7fa76723f1dbb934e34", + "db6767231847109ee8ae86b459f0303e08e2fcf5b4165faf7edc33c66909735a", + "406ee377180093d25cc204105aaa3030473ab112f209ae1dd608ebe115a3f95b", + "0763bf2c13cdd4525a95f123d5a42ddcc9b3aad27c90dfac1f19fe2cc643f636", + "3cb4c58f6597a2e937c8bcc870bbef98fe4a2ef16089c94a0afcd0ecbbca140b", + "1a448036689115f5ad50e0587d114cd77901f23a07070b4d18ae816256d8811e", + "4bc718cc25242feeced0c3f07c5459d30e5ecd91011237dd869a2b1d32c390b9", + "127d4282b45be11fe780f20db6c1737192912449c0ba6b44ac499d9fcf546a48", + "2882d864f8aadec1babadf7ea312ca3774fd0202a4702779415a7296501e5828", + "2c1a8fb6d739f4ebfd01044c2ca11b484de6933d33c6d120c5bf36475ab7b457", + "d4224f53e199e315ffba0bb317dcf7743d764642d93770e8f6eff12506df888a", + "b8d8b26045f477d58aa21e5bd835883ae1a7bf47e416032080750e80ea7b008a", + "fad3f5f3c9d29a2165efdd3e33f0aae9e58365b379f0ac61f41ff8912f7e9281", + "c05aa5280e7bb5dc66265a8862932aeb268162c25ac2be8fca44949f94ec1ee7", + "af2c85d7085cbd5ff2760f744d22e1e5be0bb8db8817deaa14c06ba9e4cdf516", + "00305a938c19feb580e7945aaf55f217d5089185176561c5896d3643551c1aa9", + "8ac01d950862d0598424e3488ede8e9a285dd068c1d401759ecfa3c97f9bdc85", + "33f3b9159541a44f2198e6e7bff59160bb52f81c35c9f80433992edb5ed0a340", + "31244c0e5b1f7c934f92aefaab2e06b0c2b88c98576777d3e5c514a6b8ba97ad", + "72ed3556b3a25c229bb436b6bf809173540368bcc434c74548373c043f4f8602", + "00947670a458adcae6c83f223cc0b2b05cc1debeed514ea4011044ce375c8eab", + "123376bfe0702b06b98915e38ad3c8d3d8d270f2c81db595922c298d980a7991", + "b9ea637bee6f3f3bee40fb76dd375b6419fcce5fbab8523785d04b1c4956570f", + "601b25e3e686fcc9c3470bdbf4fd2af9d008ea1858cef028180e3f62df0fca7b", + "6d5a1265a95ee04afd7df5c9aee4341f31de35b9e87f2fc9e02d2fd7e0d5fa2d", + "77bef4f6fde9b94b5397e63d1690c6bcd553ca53b58c2bfcdf92726590246317", + "27f110882479c6cf64c0bb38d121e389d5ff17db4b216597c75151b78b24b2be", + "3d1d9cc9b494b22ab1df806d7b1e9745d132dd6d72ba4726f55af9210f3df4b7", + "84d60c1d787bc5d15a952183acad447856c0501c2bd5809145f8d8582c97decf", + "b79fa13459e7f875488cf30ccd2f0431cbe8dffc671880c10cd72d7e2be7e8cd", + "061c36fb310210ab07b22f39178b7f451d5142a395ececa1ec783af9c52b730b", + "fa16976c19becae31248c8477ff0e746c4e28ddcea5ee36408c05be86a142471", + "82235891577986898debf0cc0bab31fa765e925d01a27c27ec694103320d82a3", + "39989431d3ac81a61b0311181d499018fb4cddfb74b1afe4a9260b5841135989", + "a846acdc0cf349a8d2b352d3dfa8784893f5038c05277d327341d3ef7de41bce", + "f3cd88ff101e9dcaa9e05998db425b590dc45dadfcd8b8a6bc0ac7e932372055", + "a159443648d0796956f92ec4efc5aa44af0cc2fb7ba97de7e6a8e0dd16638530", + "9e381ca3bd9488a656e965de17a44487f8c0da8896ebf9c0af4c0146ce034951", + "4cfafb85f2faa88a88034c4e04b7a37be20eff7e02d3e21bdb79d5aa7de833df", + "eeae101eeff05cb6a0dee3bd70b7999858b898b8c0cc7fa96a5e9235d1377381", + "786ad41ea86eda551d0bfd3e702622e118b21e619c604772befe546e55d4cedc", + "2c2f151ad2b9d7bb8f52dc4b060d194bcc63848754d0d2983def1eb992f194dc", + "530b5035473e95e2bf117e3ba2d88d6de1e648f8672baa0c9dcb83e80ef47580", + "b34801289f8a80277b7d5fdb602ed3df305d9e915138761eb20cdd42b338509f", + "eff8301ccf9032fb1398c62896f8502a564889242c9cce7cfddf1b9b87dc9189", + "1babe594d5c84b3e67237b6abcf14b9a091368c587958eccedd4ab0367601a6f", + "b2e8b56a474d41acb1a53646d825bec845cb12c7b96dff588d6d1051136dae11", + "7b400ad7fa14af9182259b51e810e16029c947f2509fb129ebe111fdd977af3a", + "842d63a701a80a9799357ac704f2288ab62af3a62d476fd740c0f8f5c35a46b6", + "dd146653c3aabc3747df891c3a6685d24e9cfe4d61e52f5c7fc7df6f079d59ee", + "2564c5ffb78c4c132cf8c6dc06591661f2d7ee12004af6ec078af8aca04b42aa", + "8c01eb1dcc1e13892a782e9826288711f28ca7c716bb9756e49b41c8d9946694", + "f68743f290c5002b746d912702c1084cee82bdd3c323200dc0710381d98a41c1", + "292a6383fd16ec5afa4cffbf7a6fcc8c6553d4cbd2574886f623b00bf12b1e4b", + "4980e074baa3623551a56263d46933f5fca5465615467ea914fcf6e389e5eb6b", + "4cf461bc4b7f8e1104cb225bae1f3be5dc70ea47a3879257d6652b42ee302a6c", + "79f626c481d62c454a9a8e5d008141befb190c0f4e78c0f9796cdfd8b490c2d0", + "8b4ab8a45964bdb5ab237c0e014c840e224ab1ef50cf2afe79f8810613de8c9d", + "6daef9ecc84a1d29833c0edbfd38663b0e5b76f6ae1d5a22cb081984be8a1ba1", + "7cf7620117fda4ba2e7e1392a39c2c0467e3b90ef96ab021f90708e46df97e94", + "44f6020ce6f1831ef60cb293f456a5ed770aa92b1837fb571ecbf00963e77249", + "b53bc8f19e29660d7c2c9622fc080eb3839eee4b601df47eb3ac20aff8ef8343", + "114f8884785cb1ea54087bf3d19418129fbac44f8c496677778714743e678c72", + "640e38abc942b35d60027a7ca1671ef96da9dbda8d3aac61917189e0f1613a1f", + "23bc1dfe86d5be148155bcb7aea39b8cb68a887a07c7159a8832d7fa8af8d449", + "8ad3f2e44ac576c4a9c364fe497b840bae3a5673a29e69f0806208499332a001", + "ba11731e009801147b11770a0822e2f1e6cab3923726ed79efb3ed5675289c05", + "409e700d67c2541afa301f2c09a4a98de150ff30662f25dcce0bec82da44a1bb", + "74aebd9823146c25c615aff0cac7a2a7b43a3678fd4acdba428bc874c2de2351", + "1ab29e97945c3ead550846ae8af77e6f02d77d24fa8d6af4a866c45568055be2", + "729944aa7565a697548f162e0ec9f39a634fec17615a4b1b65c7b68d06da8ec7", + "dbff2d93d089c5fc366cb89c702f99022ccc53ceb39418e2a9921b1f9d895c95", + "170f0177e2fd6e2ad5e96bf1c7d5fe8443f824ca80c51cb19fe985020bac4dc6", + "44321c4c2e53ab3faa16443e0dd56d0bb18ad444224c09a686285528c04333fd", + "2af9827f953129f8eec520d1409be4d9747fe2213e877ccb06a82e9bfae761bb", + "65831a1f6d27ecf91d452140be9219d010695af0fcb2ef3f19764a4f6b3e07f9", + "103f4a32f26764273b7ab1abed25b768e7994a6bcfe4c029d2eb5a30c4e506ed", + "545d4fd5dcc11f7c29a6ba552562cf4d23890761c76ff86d6940dc707368a0ad", + "4ff9f091da95e9e8892bbe017a8fe29a05955c5c62992a78e23d3bfa4b3c7d15", + "8e82f5c6062204966bfff0f299b26b5921358e22e0880a5130004280c418827f", + "1a6df2e84c39918881ec151d620cc74007c0d122020f6c501dcaff8822f46891", + "01ff674c4dc5d40f0a53318e4a210f418dd2e504077f80e640360db4d2470be0", + "fb4b597769166e58a3b28a698f18ad0bd4c1e24d6c0e3cf58cb800b131f4fea5", + "dbef3658df7ce530752ca69e5248cbf2832c1c458f0b85918f2a4f3666d7b301", + "9b651ab03463dbfb3e7dff410e758430fe535d409805506018bb3f9ba98d726f", + "0795034bae75b616aa3d94df004f4b80717c593aae42e0eeea2bda508fd091c6", + "52d8998c0c3ea0a42e6271a43660f24f17e6201dcbe28a4e1374dd20758327b9", + "a04159ad9cdb943b39160af9eba203f3aeff9bbfd933843bf6cc32f40bbc1f54", + "cf250507d4d9f066221a2c5ce190da42e2c8e9afce5a539f09305af5074b759f", + "5fca2bf5cbda4284eb2a9aa26b55f54d43e28d0efb97e429482a11b66cbc7f8f", + "a740d86574c321a0d3ffec5124431c9a2e52882f85520f296bdb230778e74201", + "5543633ba06069b459f10a9174dff488575830c86bd32c1da6724847791ea055", + "8cb684f24f0a95ad5f4376aa8f0d9bdda6e413bf04a3c8821a547fdf46dd1938", + "b167590ad1040def2161bb854d08fc62b8e4ff4a2d60376c1692c87cb2008e7b", + "a00f55c70f539a98a23b51888c892f2b5a9b86328ec99cd56a11f0e5d8d4d49c", + "73dc98bc6b3862a513320e0e4cd61f5021b30170a2d054abbb62e87036f4ea08", + "ac0f7cc6fbc94a64b50565a929ed6aebd4c6c36000dd40b4f826052b60e98103", + "e24d3edf80ad414a6e5ffca904a259c6a28f1058c0488a524908d1ca8a2faa26", + "38d020a513200b0a6d7bad5fe1e87e39efc7f80b940698ba23a78bbe2cde0fda", + "ffb2bdb644990f4751b4795b94784b5e3c00a5d40afa2b110822eb7dd0cf2d63", + "40a6543d5faecc7fd797d8aaf091d5e4ddf5f075e7f4798b36b6ff2669afbfca", + "07ee87b5c221a21104881b0a6255c2354abfeca80be635803a387a3ef9032f3b", + "d3fe1eefcaa0d03434e6759f8b48fc50f4223edec547c94bf072d49427e2f418", + "0ce4d7ed2eb07f798588df1df044d9e124604f0178ca8941e189854daf64f26c", + "4e5657ffdeb3b88cee74452cd24a8d78ea33013f600d0a6e97c56eef47c36819", + "8e5a2d6f8b4d0878a438d7b33ed8bb7c32294968f71ebf45ab51ba45bc3836e3", + "0f81dcd7f649bbfdd0db6fc69ae5f5a7d89b99388e7d20312705ba07d16a4ab8", + "1cc3486005fa6151d957d0cb6e2599df2680055949b81d4dab85562622ded07d", + "78c8021936527768d5cd7e1dcc30ce55ab13ceb6a1209996a829e3cff8e7239d", + "ab1747694e6e07051b6739e3df2916e09d7cd8a135753c9136a8dbba0449e83a", + "25d6f0d435679e7d84147da65006a48a5a0f893ed2e956518bbcd6b98f40d368", + "3554c7682f565a20b336c40129b5f7182239f2b0b1169f39f8421f25b9ad52cd", + "51b263e67a4421f6b858eb3cafe303af0b3339e0b0509eb068fe3092d394f530", + "2bdcbc0df01dbdab2c91c4658bb9b160fe3efbb5dfecdd85ab525dc21033d90a", + "aa0105f9628907cdeebaa743c45a458553942c3e089d64a02bdcaef293581337", + "d4636ae50f908fb11cfa1f338314260f587f26c29c86cd682e5003fccb554efa", + "31ae9547640c8ebad28019e8eb66cf608074d5c1a95aefa065db97da4c1e1044", + "a228c94293d05874a00c3aa46ee128a41d10405c28e5bed3f7e7f548243fafd2", + "a7d5f3e4630be48f88cfe45147b94b9010718e55d6eaae6462a41aac52e866c3", + "56d4a383be9efe8694a572a143649be7bf89b1eb12842438162532827f7bf101", + "79f7177cf2655951fe0fe7c98cd1cca252e7eafc0b81066aab0841f7cd7895f5", + "abc1746768383ad59c647846b13dbdf51d50f9a5f2c50fac9f676531ed6d08a6", + "a1ad8a55e14acf37cd4eb5085405b62a53948065dcd3bbf299477d03b1d8cc6f", + "c731487e9086642d4e92ecbada3dc31b72049583d446d65bda3a09b4370cafdb", + "729c914190c753c6ab1f648ed04889baba5adc43e05102587672a7a0051b279d", + "3fcbf09cd64d4070062dd2eaf9ff595d3e1fefaea6996ab134e9f394e0814f56", + "2efc8bdc74b41138f0075e2603ec227f7265d3eb6e7ca16a7e1696aa83cc3635", + "cf05b470683e4090959a5e6222aabb1026d60907f9df98256ed0dc62103eff79", + "cae8a98b1c421ba34af46610134a71153d525636c02cccad8e0362b7109d7410", + "cbf00526ae885899747bad4c395b9f2c02e62bbf39fcee82565fecb1224bc311", + "b6ddac871381353848dbb438f0b7ef14fca4eb39fbda7b4eb1747ea454b3155a", + "375b66c16668308f1e768c09bc2dbb0def9dd04d2f90c42c49453f558220a338", + "a2387da7be7ec4a0ce7ef03cb1bd701330e5e950be120854efac13d903367248", + "21f4efbd857d838626a5b6eb935c1d8dc3a0a6c8defd23928c8018c4956663c1", + "ac96b176999b37719fe3a67231b811a7a435ee7b5e807fd1430803a29cab70ff", + "9c06f32a7f016a0e702b928f7de9a82297498400654109ec3a17a1c2531a36d0", + "fa932de504e54776e5b8fe75a2aa7067cff79865b766882542608eb31de1f3af", + "d994c1c85ab3611ad12fbbd4ee7e7afb30ff9ce3c67ca619d9786553b0383668", + "2e5789ccd625cf20f42a72de7cdc246ab92a1cd85ee619570549a8effa476ff9", + "ec4145c429fecc6b5109d5a4e1434c4418925fc65f53bda6cd06620dbd2d1b69", + "0978239a7a14bb768e1ca71e1d40e37a102091ad01a75f5f92c5da900761c79d", + "3382546aefe321d644bd3626c7b767bbf940df0cebfd3917c05f69c7973136cc", + "227e8c8045928af96e794585a87d6869c3ced7dab3689c5565cec54e168641a9", + "c0a3f1b8c27d0234fc64fd8d1ff9f6d3ca157b03033e505bd443f1b7bba62f7e", + "2e848b33465df0fa0a2b75d23c4df4f02824f431e78e80c0221ff5aa61348a91", + "14656b6da232af334beffd02ffb86992637993ce4aa530acf8bbc2081c5dd2eb", + "1697567eea595c835310638672cc7ff2067a568198731128280bf4167b2e45ee", + "749a784a544085ed3fe56e3ce9643d0e13aab8f3480f2fdfe7c16e15ef08d0b3", + "0b2b27579b615e086959fb35f7741791615e25c611f8ab7851c83bdfeda7d7c9", + "e444de3a6fc90b1d2626987fe98c956c1e0db2008cdbe6d863ad410e9fe0a8a3", + "89a757ea85e9d24b3e584538cded1e5db3a90bb01066af609d3401d1bb5b205f", + "b00b5a1ccbc1ec142c5ef063daa88aaac1d3e52af12256cd892803aa348c3f30", + "0ea22dee4f62ac35e5b45a3beeff82c8c32231193bcc33ba7575916dacbdebde", + "5c428762fa847e9a97fce0f306c7f0d8c4b4abdf9defcb0df0dafec41590ebb8", + "8eacd0d7d08645ad8366807a5cc9f91a4b22fd52ab4d8c74efaeb9fba398f24d", + "648ed141936c9497702be348e260b9a4aa9ff5dadc6b654381c9e54160a07f4b", + "682cc4ae34101c80a98fe4a674ef0b16d11ce70b56b3a7fc5b191b360deeaecd", + "2b82b8fa930e14bcc38f1b9bf6a99f3987b32c7e74363d20acf6bd05057cfb5e", + "1d2c04136aa4ce34040a9756ba9f16af6a3fad26b7a9669a62c3563d66a4d4a5", + "04ab01586e7e4615656127d95cd558e318457ed1641c2229b75ed676f21ae1d5", + "a7a5c84706284a3c98121e35e89e3361c39ab8b5ce26c3e479cdb4c2f30aaea9", + "d12b3299cd334b9899ce855a088577d8d757dc32125921f2ee561daa94814b7b", + "48714a6b92c6e98e3406ee38539721a3ba60254ace52c741925222955a89a8bb", + "27597f719f1a11522733a0db69462fd7d28f0a64d6cb508af5dbfee291b52f81", + "44c2fc25551c204419d798aafacde9e75b4aed663d47c0cca8c5c3482b7660ac", + "9cfc7a76eafe717ecd56501bc16aa644bac189cab58dbb90bbb0cf3a4e3053ef", + "029223d300c7186ab0c823474f67590a86e7d08a9d1ed39588307def876ad59b", + "533054cb85955768daaedd98ccd79495e4726b2e5e7d345a3cc70beb19538a57", + "60e7f410dd3c32e1c88638ca4e758db9a2a8f7e52e2faee358fdbf5dd3fce27e", + "3ed8f47b67c5cc36b0e6745a29251d0470795cb14f0ad9b30170c0e5c8efcbe6", + "1f889c28d68e869bf711e214dbe739acc35780e8b19ef9576dede8e2e91bdb9d", + "dbf00c8bd10f5ae12f38c625c8af8089cd28ff92f34a1d949dbdc20d7a80d797", + "960ef9daa1b246bb656aed819cbe898dc14ca8e558070c9468f840487c05db92", + "fe1b43ac2709550840c682ecc23d2699e9b3fcf55851999d9f09b02370aad502", + "9f58ad99bdc049a7cc29f7bc482629302d56b32ba3b4a703223945c8a61f4254", + "af61383d241fa4cee95f8065527537a237914d9b1494ac8fd3ed476a604882af", + "503e10b0bd880beb400e91fce69c4fc9f36e3c816ae86b7b8d9729d00d08efc7", + "beb9c475c020f4af8659aa27ef0d1dd143479029a8ef2f605adae61bd693c9ce", + "73b93de4843e323d7db0726e0bc301ff0194b9656bb40bd3baae073b9a4df96b", + "3f3a78847d9964cae0df94acbd73ba24eb042f44e5a473b1dc665a4db518dcbd", + "240d37aeb82815e4a61db5729ca10d069d9554e3f209eecd881d12719a47dab6", + "87f8197177364e5b345abc75771b0418890afaa0c109e2f6da58df071868c7de", + "9d0f1a386ab8b2192f4531281d50df5d354a96afca98b9122198982e0120d384", + "0bd411fccce089fbbd2eb04da0cecc3f030d036a44b78f37352816bd2bec9c1d", + "a5587eb1bb907b021e206cf17beaaeeff2c34c22a9857c0ead14d68f62b1818c", + "ed96c8097a4fa7debe2db8b1aff9baea5ebad839b60b7ac878fc93dd704f9633", + "c244897280ea22de3494642c6a8219500b884ccc77802b953f71cf12de445e28", + "e11b64f780d2e2314fe969a74a86129b8cdaf4d223376843b94f4e64e9be7eec", + "1a7771432e6782bf9a5ef436874bb9aa44a5f46d1a5a7aa785dd1fb2311d9d58", + "215be42fb8c3511dbcbccd1634895857d48d2f26de9f9f167ffa86f141819298", + "5635725b8e48404be37a8c9db3274195cd7e36a651c65d09ee8419c571d1d233", + "3bbe9d510574073e795b4f843f272a1900f31510f9ad36a522525845c90075ee", + "1b19a47569f1803ecc66d1d5163c4c6e7ab9f7e99abe3e8bcf55f8cdbb00ef7a", + "c155578db721fb204f9759119f8ff004d78ba068869c938d9ce0a57e650e085c", + "017d7f31830417c7a0ef240d7837ef9cf047ed0a8414eb085da37fe2cddbdf64", + "78ea7551cf28cb954259a28ec1340ee66d715f02c5eb7fec48145298581464f1", + "b89ee6474841186f5afab538f9ca17b4171c303d5dcc89247b79ce38389bc0da", + "f15ceaf2a19e8eb599bac5959aba3518688d4791321e618b5ff4af1700fc969f", + "6c0be8eac8a473b665f89bc3a310c033c093f5b062527a227921b072aa3123be", + "60950a25415add062c9bd5427cf5e0eb75d6fd7363af9d44656831d04cdcf7fe", + "c8ee6f1b5cafc14571642946e8d8e56191c3a9a020e5322ec30a698258c24a17", + "fd5a24fb185292c845fb8759d924a72f24c18758fe9ac5e0df99d1365e0657a4", + "85db138de6940bcd58f37836e431c6d0dda1722d21f7fe2e519d08ec8d990d7b", + "13a18fb4b02a808771e8a5eb2a64829bf27c7e6fb82b2adf8d44a920fa6a5900", + "cf37460c8ad19a604cd4d1475e8258ddbf72e3163d07bad676a0db39248f298d", + "16aaf7854629a4612442c7c7b67836990ce60a78b95bb0221646cb9d27454e2a", + "f3ff2a33d32daff5a2606ad41d8b9fa469fed175ab2d12cba2432d2cec320bcd", + "3f84dcab7582376089e959b42b47ed237ad52f5e633093bf7fcbff6164b74cbd", + "1776d3f2ca165cc20e0736e7ba69ce320b1e19575245e0e78bf628bf8a3b4c90", + "4b815273385785fe9e57dbe6bbab75423b6302f8daef245c12bc4b8572696893", + "f9daf148d2ece99869e00ed44ffc1a740792569a36d6dc21a94388453c31a08b", + "0a7f0cc9bc54b816a21785f84414bdb99f34a49a3e33e5fa1d105ba026540627", + "05ec349ece9f47e92ec0bada62dc00a7896abe818b213914b95d3cc301afa0cf", + "3f652f63459aba4684390a90687e117bfd79e09b42542733acb734adbb0a01cf", + "d7be18e61d11c4e5757f2ad5fb6b8eb70ab1f621f7785d048baec87e3c81ddb7", + "cb4ef5ac7ff2c34808e9398ff7e313cfb5b73e8b3cf9c0c3ab8059983159213c", + "c1f66f86c3dc26b2212cd7536bd40bde14015a19108d997e24d0f5b1532c5201", + "2e8766d4dd56646f8fca1cc12fe66785bd90f42d27074c71e3380732f5616558", + "feb95b15d26976b6b31bd488430da464d87102d5b35cec8bb578cffc91bc7933", + "bef956f6a785e9937e4e3bad0b1468c428534fc06658738d5e02c1a604c77003", + "fb471f8b762add49e6d91eb46cc018a6990ca7bf1fb15d26aec90f6ba968f6cd", + "d4f35e4a4e006546af1bf09f5e49b5dda60ae5dd526b85ba92d871f4f09df77b", + "795fda42bece1d711437fe504f78e940f309c89830bdb5e9dd46afe3fbb1b150", + "abaf0bc879b70ace0a82af4b9d4f32085ddc7c5d920a673c47e83199959b4de6", + "ae69a784b18524bf95e7b2349cd1c36709eb810b0168e7e140a7a411443a02dd", + "7858fa7acf40bbc2168ebf05e3389ae646d1fd4567b17470506596baa5e00ea9", + "d9f1d5f5ba2a228915342e41510bcdaac533e10025531eb9a1960b93ae667332", + "5a0def567978daf9e17209cf27f261256f11d0f8cc946c888ba3bd15e56c786a", + "589c40f1e0e442e1e227c1d45ddbd799c41cc38d025ae871bbbad98e0b65d2c4", + "d25e897ed507bc4dcbbf349cc2de3d2240ad6bede49c89fb07c9b5dcc138363c", + "b480a7829783c502490abec998b2d0253aeeee68e54bc165a5f1e98cb6a9f4c5", + "ae8c6d400c2d517eb69379c486e1ad35497e692292bbe495f0d01b4d4e48e265", + "27fe2f66e35f873b8094d7e2ced46e55811f5a936f9019e5e2db41b7ae5e819b", + "1d767698a7e6a1d6adfb55c1584880350f2c4e80f40e51776bd33a6a2e47ba8a", + "70114481a06394db7de45673ff702a2ca4047d2715d780884f3f841a493790b3", + "404487dd890566585291dbf1a22cbbb7fd70e4870b36f39ee7c710320aa0c270", + "b908e1a132f73bc705594db6e08b82fb277a87456142f6e6145fa4701d7de09c", + "7f0734a0c77fbb88b5a5a4d49f995bad567f0a676cc9326c3267211d06ffffe7", + "204e168e47177bf41e3066b1ffa5f0779d98fdc7a0ff8ac5399f1c3abd6998cd", + "fae75f8b1c0302a15813cdacc724fb9c9d06d6ac7c6a358b1f1c33a9df3da5c3", + "c18e90a57bd273f5d27751fe81ea9302f5604f24c2a2a9d306e2b0b26ed65cdc", + "dc084d1ec5c777f8cb78fa955903adb7bcc68b0d18cd2de66f9c32acfe9e9948", + "4fbad69a041e89833b72a21907459d2a9985bef82d05f70a8740f4d1abbac471", + "f0d752ca1604efdc9102728c70fc2198ed466b21acdafe6a234f7124cdc3c531", + "96a291d08e64c21d74c94982a2d1a146ca25ef24b4c2ffcdb3a8bcd77288072b", + "d84f7a140461dc63b72d66dc97af315127f8260a6176159f7120ad15e0ffbb42", + "e323b02b16317a133f695b892900cbed463c146310f13847cf52f5e8a1556403", + "56f9986c62059e091f741f5febd25cd26400acfd51302e09e853c49815345372", + "4a91c13e08448237509eefdc09ed41470f72309aeb0f3577bfbdaf2f45b359ca", + "3f1214f03f2bb53d779d05f2a3a647d0bc2550b5540ef159c2917cbab8abe006", + "c00d7a898094e86d4659ecd1ebead956c2d74969a9c7ea05b2ef5daf456cbfde", + "c9f7b7bd863e12133688f089abc3846578a8c50f8fe95d33ffae1472cd61d396", + "9c4cb359614595f16ba7a76a3a5f7a78205c32a23653a935400f147206eb4a2e", + "f81f1b1b6ec3af3a09c79607ff09a025162ddfc14cf85ddaf4ed678599031bcb", + "07518a7b6771f27678761c04c584da6475af520f2358b0886ac0f792e5b07e79", + "3d8edf9ff36d26b12f22af02685c72a8f9106295c7cd06cfe74fd59967501ef9", + "957afb2e2abda010d44ee4eda67a2b70b61c56594a31964f860b26a28cb8454f", + "f136ce60520308496e1802b6de253f4911afe61e910358b2ead474a9cf48ee6c", + "7c90e7fd57edfe1fd3504153c5fc31342a7b354e93038b45140b67fb6fd18e65", + "629c3a127f091ebee42b4d487d029bd4c3fa2ec72dbecc715e6c8c9d2671b736", + "41e8646e02f9bb0537b627ae5f269154b03d916fc490d38abbcfc3ebf6d9dda4", + "72d66600ed0cc1faa6d91c2a0da81bd90d4538cee893aae4bb0cc14eeb4dc280", + "81889ef1c6d2c81067e366ab7185eb2199be8789f114ddd6a3b693ef7821bbec", + "8adb60319e326ca0afa70e7f8ce7209bef89e1ec820a5d70f13d606add534ab6", + "ddcebfd51afd77e78f06d5a5dccbfb4d6dc61977c47ac9b1a2f6402940d9a156", + "b4cc18127051cfd93b1a5eb0358153c03f1d44b841a41cf3c2a7a23a14dec4f4", + "a3d44b5e13a0c0e6eb22da6e8811d4995d495d4d780e840ee85c83f2b1656082", + "eb69269adfc48cad8200b8e31807e9454ec615b8d33b67e7191cead52549007a", + "cc6bed8de65dc9b905e2aa95ad5fd57bb82ee21c49968cf30a6cb4b002be5f6d", + "0ee85d0f8f0f5bfb77f957de73fcb2da0ff2691b2971ea681432a67ae5c7c378", + "d8a2af3e7a30804574e74ed97275804fcc22c19607ea30251f7fcecfc86c8cd4", + "be4f45e912a7cc5a174d6cd1c058412042308381f9882d2a74c43cef9a93e51c", + "b2137011b2206e9737392b735787c1861d5df47690b5682cbd432fcac4c05198", + "f4fcc63e7a123783b643adbf0b97580b789def70d8ce5af5955b35b263f4a4fe", + "1cdf85f46a7dd1937cd43c941f1b555d582f36d3091097231f86602c73c84c7d", + "76cd77e06f946eab3f59cdccd3a3e20395f04d5e61ebc5ae6cfebbf72d96214a", + "f080f232fbf74a104a25ff2df6958e0008662596f81cfb84cd72c2a940c9b5fd", + "70a13b34f284353ecae70355403e6297fbab263f207707860ec22dc8a2da4c94", + "2d1bceb4417eabef500af978c975f18ab1b01b12946ff411832af267652f5d49", + "74847cb2b631547a13df246f10f21053e1d412b4bcc83d2a28285033c75a2839", + "0df6e0d42f19ca95d31d9b762fb58b9ee4ef0a2037cab32e49509c575c6ed19b", + "e9b6acc84e087163ed1ea23f7b7aa0fa4ea37f15473a47af6618d7f30e8f56a7", + "3578384eae5e7a10731d6de0972072f1f568a764ee9a2c2c6152eee141fdd3d7", + "69653aa6632c7fb7c69a8129861e9f3226b726a8804471165642481b598dd099", + "a296c05fca5e44afc5c2cd583cbde4a1791bdb7a8461c6676b97ea63c11fe393", + "077900cf366702fe8add4f0265cc52273b343a14218ea59ab26cd628a38240fa", + "a63d694c0e6728166e578d90061d24146d56ccda4ef281c41ce6076ad51727bc", + "1623e53bff3af6346b27b38cfc0d98f6f2ec80830789f88bbccfbecb4ce1162c", + "ce5c3ddd1852bb75590b6d8db16b4e6264a8993a3aad89f1731753565d5e54fb", + "3dc87d48cddba8c9aea71e8307e9d803925c9e4c4f55d7730d3e651dbfc7ba45", + "1cd04f320cca7fe33c2f43dd9e0098cda82edf20162a67320e68ebaf3fd74fe5", + "8d63274c4a44170e5008d2d1b790d5f468269abfa91a54c329152433a30665b9", + "89ff3946f2111436afd2b41932c475a0d09223e23b6b9e9a9ccf164d3cbe0b11", + "db63caf3eda1222a308deb317a0bc8f1deb95a6b3b0108f71d2d16691244927b", + "3d1ba03634ffaeb5d906032273191fe2595bd655a70dd8c22680fa4929012c6f", + "0d355563eac60a85c52b4b2a54c21ee7610819a9a3b30e69a9a37b5655b9b5db", + "373a44ab3936104a3d9d26a246d2365127c1726c34d2f1d7e87d7d7f6058f248", + "16022145fe055769433d4c3cd9639ce0f8f668e157bf9711e2c7bd7907178df4", + "6b34e7d77649a72887fa46cb34618d813a32fbb77e4ebdd6691527eacf5fd84c", + "df915fc7ca49225b34f2d419554d2594c8b2ffd834b2bb92f49397fcff381eac", + "6002c0ef73c781b20d7d4d9e01322f8efa8aa78e5e9792bce29a9169c7032f57", + "3a0debbc0ee42304a9edb10db770d0b8180c403971f42dad8832cc16e395c11c", + "6d76b23d46aae2e574ec2947c490f2ead9acf9592e417069e72050721224fba4", + "593602ee2803bcdd743fce93962f672cb877edc7d009be65c02a562b8604a74c", + "64759e44d4e426d1c147b405330e2fe1ed1afe168b8db60a87c935cf07ea10f6", + "d8f1eea3b2c56c9bdeeea5cc57d5fb38cda7c2dcef9276ed5602d27b9788c194", + "0da1328dfa5efb29034215279a87bf026e70aa7ceb2793e8c0ba9354580d552b", + "8bf631c56ecb7ab31c0d30bf31dd28a62877402cd6825d0ecc9c71e26f4be643", + "ae29941b70abb79f690de45e32c8b55f8114b30cae4376e991b963d4e03d970d", + "0b87ec546259a15a8c8752634c3ef219459c382d383f155f219c04d9d5bd7f01", + "87605721bbed02e5f279a29dbf5fb857a5e1d41ffbc50689ec4e1ad1ed3f2b76", + "64d33c169662adb9ae01cc1a902b8725ea84162226d860b05b22c1cf8b716b88", + "dd3e22b9aaf98f165f20c449618ebf776727386daf5695e68847462a1573f1fa", + "2eed687fa82a0df68fb2a1ccd9c7e0679f5ada28b831b105718dbd0580c0aeb6", + "3748437a03b2e086825a1b26b9a363495cb04ee3b5c6125f16fa7153d388b38d", + "d6f69cb9ff2388175b1e549df23171ea6cf0f181c52902e90740dec0fef941b5", + "81e173033c052c2a8b6c2ce928c7d9e7f7b956e9744156c8b40cf815f093de69", + "627027e95f078f86d70df8b97d509bd556c6397693025367ae4b43edc9b6fdf2", + "9c8bee1ae58091342dac7901e5846df4acbbc409656457b03e1702eeabbfc8f5", + "ee387d8e66f5bf75dfdcdeb97d28b4dbbcb1302bf20d6bf8465b7b41832550ad", + "681f0145ff4dd8a84e42b02ca5bdf4bbe8adfdff11e3ae57673740a4f98f0c67", + "9f7d29a27a80e64603c98125d6f77e23afac3ae85ab0fbbe91e74e498fa69c89", + "f23c72f39810dfa196ddac4a2b30fc97f3e05cfd67d91ef2e6ab063ee29483a9", + "cb02c55cbe1a20d200bd53e718c78f9f2307d0476082bb425d4476a70ef47172", + "2c56d7a46e2e989c71d754a9ed2ed22285adb9bc7ea0ce85e7ad0146798836e0", + "161f8ede4832a65d9d082699e93299189916a9feae06703aabba7aa7100c8078", + "c14f24845530dbca3372c7d7453441113682d76ca7c5ee93a86f8705dfa6754b", + "98e2302a8c27aff16705eac3c9b4861e0d670571ca493dd0077280785d1d32a3", + "1af7252bf32de08559d47dc9273c82a7e6cf89c2ad0d755084116bdc30e2dcb9", + "b662f05da0258a7218659df10926cb716fc6d092654ebdf9d8a0efcab72f311e", + "ad8807bdc9c0db6fff8ba8963af51f14ed809ff5cff5944d27b1923bc5cec68e", + "4502bbe76bba11effb1f91a822732bb4d1c6cbd20a12722cf7cf2276bcb224ef", + "bd100da2cd60b60a2923299e505c6c0fd23cfdeebefbcec8119567fee2b8d5ac", + "2725060e6d944dceab24729b82d3a64275ebd82cb42ab330af43f53cefa27893", + "9cad96ee381a6d63d841569e475eeff6dee67836e2d05113a8f18aee590673b2", + "c22c8e48d846ad43fc8319c4d3ed8c4af31ea7c3e079595cbb557b78e50f0a60", + "24a9bcf359c2345fdc35812a10f9f08229d11f17c5951120cda1264adb2e6d71", + "54658722a2c31e13616e4a38a817927c01d2df37830dab7e76ec16e7ec93e298", + "fdf9d5820ce67e076d78db09766a5827c6d14678d1336c8f936608d80ae83d72", + "f3c55939c47563aecd9a0be2fd16c8d839331c811403ea22d47a8dedb636f389", + "9a62ce2c9a3238370ab983857b27a6ebba407540c6a55b8d3e62fb012b9177d5", + "32ea29fa721d7727b0ad77f534bba0ccd4f08b16b1c809c60da2e83c37cd052f", + "0c8eab2d814623966122a83e079ae8629897d405fc471fe70eb8ecf63d53a6a4", + "b5f81d6be6c5ed881db8e195b850c3ebfa2cb455d0a1d14df79740eebe5d549e", + "ff39acb8667a42faa265d97a90fa7d5802c6a581dfa6bc868fc6e953b4fe3b10", + "cd1a9a932859370e971587fa26255782a1aee9a1efeafd17badd1aca3c822363", + "780034cf738e520971387345b73f42cd13770dc8cf68948ec9e8e1f6a6327f61", + "0d97a70b467fbf4af625a6b539035b19e86d011782bb7a9ce538bb89d81438f5", + "ca8dcf9e3454d6c2a346a01372c798cf2cf8c49c38f9581d705c42e4f1e7c4b4", + "0c21834cdce637a85f931e8d2d961573d1dba0ab7bee93189f74c6331995c914", + "c8aa251601172c9479b4ab2c1382a2d6608b547cf6b7a4a04a75a91fde4c08b3", + "14d36f3db5b2cfac7a99df066016eba59a21ef651e991d631c04acb11d1d2b51", + "89006b74bc61637b0808bf786df16039d0f5a26d1cde1348de0f33718d47e75c", + "098621c303cb1b9dda387a0ee66ecd267172a70defc1b403887ea74c53d5b911", + "f9428c1dc527138be5c9ddbfeed43077d06f2826b75edf9218e7b90df8b5ec29", + "245270fc3442e19475818ca0d5cd32a4728a25b5c4f2231d571eafa281e61e18", + "753e79f5b9edd2e49c30c17d3d5050a2c0265027abdf8e661281d22e6cacb701", + "7ac9b5239a33b2689c63739de793dfbbd0955c044fe062bab28f7b9e72af4755", + "ba6c405725d69b2c74661f3e4259242b054d13f197db5700d1d7728110e56955", + "a7ff3309d156a8d4af88c92e5959c98617e0c1b95a10caa544fd761f93715b80", + "4185a1fd84ac173ac31493ea80b4a6d38d37325e8d82d07ee31bbd7f5c661e17", + "78826fde40d9fed3355c37bcaaf0656a4236d58404a892d1da402955698e2904", + "9f591f6bb853601a315de1edebc0927bde202e6cdacacace8dd32bc824a8a094", + "3ae950afe6ce59737c943cd90dc8f100de10de1ee952b2d295514c4f710aa7d2", + "a75089fa658b1206f03622608239f2de628b4a4693548c864d6096e2cef76490", + "efa7ea258d706b04547b8d4a791b1113fc77a4e5547c8a308a89bcbbf8161e56", + "f0c8cd991b168fa30685d9adb508e9aeb4f2b63dc08a153b814b0cf1f989183b", + "6f1946a94c3138276073dcfe0d0d48d3cff0452993b34965a8fefe90161f0fcd", + "2bc40489956125fa47c9cff896091977b8dd0b7a0e0150f614592b11a2e4d1ff", + "b2f96fc311567ddbef1bf0fc487f973145f717f283883606ee03b4384da74003", + "afb571e0e140ddda917b23d993474493d61a8414b0bd191092b6780bed309756", + "687a64c739ccc3941a0eb74554bcd0135e0063922933ff892c7b90b88e21c7b8", + "5ddc34627e55a68c4fd82c789c10d7fecf3e380b458ecd18eb8c5ae041c0b288", + "ee7584bbd58e279c2667ffc07c1c4977654fc948a63e33633a563c757f0dd9fd", + "3b7fb2e668fbef730803f10eea0e6cd9a2819eee7c1c1418431d1dc683def7f2", + "b4e18431bfedbd98c924a64497c60ca9e4a82f22084fa1ad24c09f79b2ea6a94", + "a5073a937e8b5cbbabb8915cfd2f2fe7307e4e334d5f434077995815475f0df2", + "455b0de46808bf01d625c22f0cea27120780c8c69c0c11cf837e98823faf58ff", + "a2b26681847ad348da10f7b578cae28087a3f5aabc5f84de188ae7ba9dc754f4", + "f03694a80d8dcf0b98d4c4ae69a797218cff7688f80ea97e627e817ad36f3795", + "006670166e382b2bf369907cb2b4834d58ccdc55a4bbd6494e8f9c129f5140e6", + "ba02ab95f8cdf7e40f03a629e47a0f7d5b77096549f17a158c73e2b96c6cf0ed", + "e1180c4ae2e094ff0ef2dd8107e5f2b02bebfad6501b18965b75ce8689efea2a", + "a8114db88256707afdb0f589c4e375a7b165380a0f8c2795c0c0dc0d02f5684c", + "a8ff66f47c414f56d8129e33a45a0bd9b9d1a640a3a3454d3496ba3a800b2d1f", + "f36ca0f95e603f806380dce52d91036d8e3a5545e1dd7620a19196397b91359b", + "1c207b576a1e9e765209ac6756c7948fa3f252a4687696d0f8e831f14ca52b92", + "cc5eb95f7759dfb6cc55027d49a5fe9c52f3006568cd97a0c544638be7a887ee", + "4e15d5885837d4e5e79bff21930dcacba97d2bac39e506896e005fd55aad7e67", + "5e2e62808fbe25793a6917dfbfd06029ea30eee0892116fe6ea4696fcf62ef53", + "334d575dcfb7f6b606866af045d0a378f53447ef51d5b3d1afd83d78c0aee1eb", + "591c6dc98541b9fa044ab89f78856374c3f42ca5f8021bfa0e119d8a04a28646", + "c502d6853c8eb8a2657e069659c1f3b06878498d1b1a15f771fffaf886a1ae52", + "2d4b16c2b6cee606af3c02ae8d6a86560e2393306fed2ac0b72456a8e35e24b6", + "b2ee8ad11dbd4ddbca245b403ada57354b5df94a9de54bdf574bf0eb41645c89", + "a895e7d8adaec8c44b603648f36774980707d30ede4f5dbaf2bd3d2e3d4e90b4", + "1e76fe8a5aa1b516db5d04f3496e69170d3a6910aea79ad7554e2527b81b6b5a", + "1576d0f08a566bc422049efcdd942ba455e61a1bb2d6e1ad470e098899128f75", + "ffc42cbcc82b0b23c5616301e90aa6ed76f485cd63506ba52097a42b5c17420e", + "27fa1edc6938b7d3658c5c9d894136857fc0be8fa743924a86ee4baf583599bf", + "4c8a502fb162a9580502f4f2618be83041782f245c6023e1d6c3ecd7bbf2cbd2", + "5b72391f63f61ba48fbf8fd308ca6afb7d72b9545d1812538f2e4d1414c6fb1d", + "1968aab09560c49509dc7cdac8c5c919629a3da0a39d7628b172d5e7bde99c20", + "97656482e3f4c9dbd01cf1fd91f0deb0b319c15a89b5deec6a57fac647a81753", + "d1392d6f39252c12b76aca6762fd95086785433913e5960b3c14a708401c2897", + "c11244439c754d91aa30a2fcddeef5a0be7e300b94a5c98967785f112cdbcfff", + "21c529765b6a1048c8c946578be14e63750eeae28521fd845bf7a3ae669f203b", + "c2f22169832b5de185aaa5642f125738f164001083438328d2541c15a7f986af", + "293583e0b2b86f041204e4ddc9546ca74431407f88ce36665337a5602dbe3bbd", + "1c270bcff184b5efcdec7f630e707c3e7c90a519302233fdc08aa744d4cc0f9f", + "88a9d5a81d358a14cd92c14d669903032c93371915725d764fb01c7c1673d4e0", + "c733a54fa8d1ef07070c52ce77898eb93f148b13cdd1b6fa69884dae4b62144c", + "acc7546cee77ad0a641bd33253869e0459325db8e6e3a9c7e3812916939e5345", + "edb6a177be83d61f8e67db1b3837db0316d0205cc7303c242c697fb3c2a1af25", + "8e1e002d811734ae8033333794c2bdbe826f1c69f4dd6197e595e75837a3ca64", + "316f26c9d7b2dc523838fd4d8a102e7b817a44d00a4f31dcd30bd6529ca3070b", + "0ae34d23335c3138e2ad4a282b3bdb89542bdc29a4df69d202ac41f140e15aa9", + "6c6d48776be2e5a30e91af97bd6b0bc5a5eb76a5fd713adc399f8664918cee9f", + "dfd7c371eef529314fa853918ed4fac86e473d5bfcd9dbe261e57201d70c082f", + "0046ae85e6230a07ef27a9c7c75dc6f6ccb7742433cf8be17343ea8666512259", + "dd9fe83a560f0897bd19f23474fe744c8eade026cdb1f048e84f3459e0f19303", + "3e6a71ec2a652defe81127e60e22728c5c2b6b7ebd3f8946e347d662ff3a868b", + "a4229eb078f80905f012a474545e0e2786f98bf7fb2073dcead7ec73c65fbe9b", + "7dcad9638f5473ae40af1a5d9334ca763a09c66812806f67e75bb44e457f87c3", + "374ff61877eb866ee324a87077eb71d0a269e9845847e55a5e10fe5f3c77aade", + "ef6f393a0d140250e071d3b82f5086c13bb429266cf944bae8cdaa28834cef8b", + "9651ad41b55c6e8d6abb345d4ea06e4399cf2902d2c1cf13ebbfba0b7140c8bc", + "9d0540e58a6445fd868ea65c40185a7da7858524f419797084a602427ace7477", + "5d812312bfa56dced2d010ddbd98b79ec946a4c13fc545088c80d037e4df3c3b", + "c75d28a33c292a50e293d4e88f25421bc03c83fdbafaba4f526b8c6a7e5c2e2e", + "53cc98e7b41fa8cf94ae5ddb9807fcf8c73c209d910c892f266621fa05782ece", + "f6b589decfffc6c33957cd061404dea6482435abcabbad3485266cf4a8e7fc05", + "e4e03d0c467c2420923815fdef774f530d07c0c415e9975bed75db2ce4227dbf", + "7178a3cf86b9b27023e576a0f112ddbd3e240289bf8c4256ec384962207d749d", + "58c29327189acf5670bf364f2c9023439eab2dff643a4ff36f3d35b43263111e", + "d9b365bae902827f415288ab30e5e75d93038ffc1466356d6f5280863f18dad2", + "ffd371a7a95b47d197bc947e0e1c3cd50a96657ca14116fad6fb7ec96aec3bf1", + "10ab513a9aa33ce2ea4cf293dd0c435b5aad5a1adff65604dd3f07cfa1f340ee", + "40863aa43e50ba80bdafdba66ae053896370421d48f59c2aa962ec5e2914bd96", + "0363498ed651cf53bd469fd0dd8ffdcee6c6b4641a67bf52fd177ee7e9e81324", + "7804a9ccc78fcc0e6b6583e2fe28a32d3b409a08dc48061233f8faf88dbcfa03", + "68ae59987180df7089fe2566c5d2a61d554b267332a24dc44ffba97c1ab1b596", + "f2c29ca8dd0b0429df3fe25fe3e36e6fdef1170dbed8baa30cde80f13cd093f5", + "06d4400c68d16ce0b1b70f8fef726fdf633af85eac4e285694db95c0f8e503cd", + "8db9481d31cd20f8ebf15b2ac559ee46796f0da83b8a94139ab998e2be19f035", + "4c4a93fcff164e070a9eac32c8b7a582a00c6d32f8dd146cc603520d91fb11dd", + "3d70cb493fee1f8575dc7da9c7627d31ebf42c9842275f772ce6f0d42e06db0a", + "9f28537ea916ceebf76f1a42544c43e13e9075929228f32190d9ba4f5bb5c9b0", + "673af69eacb58312c01c9b4e93167bf8e5bec2ccb4450ab833baee81cdbbd95b", + "0d0b61e12e81bed69380c9b8eb23a2384a48bb13a4b7808a21ebdb00960e7e4c", + "284b4b1d0f126cc76b743eed1d255b181a4ea45839ef0c1dd2749ce5ae83270c", + "70cbb1998efc52d6b6aecca67551e42bb44eb6dd6e400241d2e2bab34ef9c1f5", + "a47582681ac364f031f9d19c42e0b68edc0749c5b3da9581009b5187b8b503f4", + "808c8d746109d74eae54b97f45ab7b9ed24f32835cf8a1ec9d7d46c3fae5178b", + "ab0e3e550b11a95ebe15edb506a534d70e4bdfbbc9172b251668cb8eaffa5c7b", + "8d24d23ec2012ff57024b1241dd401eea2ec104645e5dd4088b32546fd8ff904", + "523fc99da5bd881da326c94b303a852c0796792c1f3abfc1fdc7590c9a1ad80a", + "bef023158f2dfff9af92f2aa64e7da3087afe08b2cd5bed4d6a0933e46efd411", + "bdb056bd0ae30d8b926688b56a6e06029905ecb5b25922fe4f6f2004d4adfa11", + "94a65dc8805bbee393a49a95ce8c95574ab280d6cfcc4a1490cec8352754c6a4", + "50410fb63b9759e697bbb5c62d39820ce5d1f76ae9697e064578a15e0621b792", + "90bc57b7055d84741c13f3a7707770de07ac2118015e3f0d08ac9ca3a4a6e177", + "4b703ad6aaf1cbbd6f4fcaeb9745fa82f6fb055611d7164c572892b0fb786114", + "0ab836aa1a9ab4467708652e8dbec2885119358e37b32f51b46d45f33c88046c", + "1a118c7d922133ccd430b9f9857b62f8f2ce4df7d648a197f6edd3fefc07cecd", + "40e846cc774ddf6392d673f69ce95245690f25786770051ba5ad49461310b0df", + "5aadcafbcf0b1b4723d39c5d4048a8d9f3889a3f68aab66cac0ea4f0ef7d549d", + "acdd2f2a548cd325fc06b49c257bfbc9b09c016ab0859cb947aac64c4570f6e9", + "0b3ab537044f2e6142ff425079d25fcd87e0dd8187f03bf0dfc479142d8b9abd", + "497c6521a301865863f7dfcd03316ae0db690e98546116c14df05d98e8cdc893", + "ccdf5949457b2394df193f21d8cacd4971942ea3a1467e2ab13fc0cf38a2aca6", + "b38079f35e81d62e2ec89a5511537abd020e0d49f47cfeacb1fdf0000e3898c3", + "b5477b70b3e6f62fc281e349303e8e160a5cbf6af135bf3c991955b3cb963039", + "64e8bcf3fdb18248ead8b6c5a23b156d76cbc6ab5789815a2b45c365ab20d353", + "86c981eecb0af6e8ada8e68a9ba9a6cb93d0ed527a366121ab0dcf9f44d2a38d", + "93ff8bbd4dced9f09d343b2227099d63a28c121389dda386c8b58acac7e020aa", + "b9b7cc4a4e8b9f5a1ec1116d378e3f90ab3e8e4605d4cdc95b3e4b7df2815a1c", + "f2f37eaae0ce72d792c65a906b6c7334c2377c3c31e454063305e7e63e5329e0", + "cdfcf71c42e3d8d056cab0fbf3625c1b13704870709a4e0850006447f128efc0", + "83566f6ad1536d4aa2543258bdee98989807ffa133d36af3a26672708ead8410", + "68b89c27a8254d5fa39096943bd9bb0074e950fac7e686209c0113df088ae054", + "0e8c8d00c7e71968371f4597da934aeaedc4142586c6454790fff7a4e61af30e", + "0bc7e2ff2af89d45812349f03973406f27a4475c44e71589c4121a9cae9cf37e", + "53738bebafb5a06ce8140b83ef922a31a05521c07c42cb09fbcf6619bf959697", + "809f6e691647916082cde741c82df020254de10148b4b712744f1db345bc2694", + "82238f89893476756a48eaa42cffb4fcf07c8cf4cdbe12a1c43145d2b67aac07", + "ee995fdc7633e803741fffcad712a48aeafef16e5a91aba899e4295bcfa3b577", + "b65b0489ecfa62b9725b7a4c4150caf0f01350de9cfd613c90c60d0fa4875ff2", + "7988a77872e593e831989c6ca5e5e6861b63e1c2a42030d684b51fb88ab73004", + "914ea37de5dc3b2f9035cdb7fb80e6e692a0a5934756beb6d8e2ea8eee7a3895", + "d9c28dc8bc385283650fb2aeb1efe10caf848c39de37a78dbe6f47b8f3bd8b51", + "49a2581f6ebc753d70dc30d3b093882153fef2d0825252354e5beb1b35eb90aa", + "7791d37475e1e15bb9b817b180811d784963d2398c5e736a851af2ef8e622e77", + "3957f179aa7676eadd2c312cd6096f84b7a0424bc99f05c8997bac9fed057ae5", + "9d954ab5224ffd2e0301ff9b33ef834d019596a6c3f56fb634ccc9779b9c694f", + "e6235eda52b721bcc3b997bc8da554f1998b5c7ff43cb86c2d7f1915150c4ded", + "c0684e197ace1cb57ab553203b16fbccce5c993b876b90499e96d94ce308b956", + "70cb7be7ca2d01bb41c09c06fefc04e8d6dbfcdbd9b8df4bd38caa3c7fbb6001", + "96235ac39c397550e28b63f63a4bebf549215c5e72cc78488efeef131b0cdcf8", + "f3bc2994745fcbbc792d905ce1858978814bcec9a5a0a3eea4913bb87939378e", + "caf6345a1bc6013c428933fcf151ee366a11ee41070c3de2a3e5036a65145194", + "ce92dd8170fc721b8bb69c261cd868b82a2d1ccc556bef3999128a3c20bd7c5a", + "45e73df58de1b26bdc1b842b5a951da84381708cd15b40eab87ad44641777c6f", + "1a57c8e01d94f7455719c85c662f76580b9f4f530aabbecee9c5abe00fd7def4", + "c92b4679c9376df3e41d79131f42961fddcdf99afd2c476168daf9657f769373", + "2a2893a114df2630e4b32c47bd60612b4f3d9b04891e944af55fd2767d260314", + "98dfd86f3bb1b56d0e5b3d0a9b3227642b09ee0a2ae78f19eb504edd1bd91ba1", + "2bca879ad4553c1406e38ab072cda33740cf1681f18e7cf548ce040528fa5d05", + "5c3039135e50309be3292559fc0f0d6a5de7ab2803c83f206c8cfcc06683b54e", + "7b46b23f8ca2c45baf6124165030a1ee5fc27f1ae3d1226defb2574177cf728f", + "b37b504ccbcc5ae9f8b787fcc8da194fdda3a25b94fbf217444c13fa4a0863d5", + "36d5d669f4afaf603a54902269087a0f65f89962f87ba03be56b552bf04ce81f", + "f57f94affbe464d876235351bd20660ad5896e5f5bd38978d64346564a089754", + "4637eacec37dc577f7d0c780ce46a57de6da9b5173f4d0626963af480b95213b", + "e22295a0995837f730cf99e9465b0e5c41c1f4a99456f6b27d26d66b8ef2c4bf", + "33d01fca1bb71c53d131b757afe6d97342006694ae752c17747a461717f42f8d", + "ed239b46bcf7f8a66b92c15135488e1249f668625c6fd0a08a0f49c733806de0", + "324c9a8fbc527d9e9357595b1b3509847847f340cd557b9305e78fa2ba963be4", + "2fc5f4089b8b6375a41bb70257be3281cdd32778db05663c55f96cbeb976ef1c", + "691fc9f8e0e0fd9c0a0f42f17f33ea83c3462e35d777dd54734108bf273b5a4f", + "e21e3a565655f1f58ebdb421c94110e19a55030777dbbe455e350868020a574a", + "2c3e5c5791cb68bb2177332f5b79c8f7d1cf4a577a903acf225a0d11e45aa5c5", + "5707100bd5a978ef7fe1def6ba895ad388466a4db13679a899c93bcb802d3a7f", + "fb537a7e26384ea9867ad9ac845067553c1461f5c75a5bd50df1afe40b063b1c", + "afea4fc11996d2552a83a4b7bec586c9dbf3346cf2b3ee7a862b8f6ccf163522", + "67a83eb66b438519cd92e6962566f3d1277395d2edfcecdb06565e33b9b4200e", + "a763a731e9fd04d5ef02aef4e7cdc1726a34c7a048cd78c45ddf9f05c52773be", + "c992b26e5d9f56c3f1615fe30fd5d4a70d639e192583e5792909baad8695e90f", + "5f63a2abf7fde874a260f469f56f7392c715f43581fa0e0ad99522dde7c047da", + "2d92ebb998b264b0c7fe1d9dc94c8f3ac171f31a38867eefd8687216342ad4ac", + "36758fd2cddfa3f6c1658a649508727ede3db00c36b5a619260212b50b3e5da7", + "19237f8184a40216542e1f326527f7e512ada4ff9923380f9a7a7e73a0c4aba1", + "3f2d551638a07d4c6b03265f131adae2639d1600a9f31a337d5561bc22c41af6", + "212a3841885d5890d53c698bfc2ee6114c13ed4636483f65bf9f43e2a23c2dc7", + "30c6c4fede61dd964588586338cacee4e0ef8977b556b4fc104fcb6120d45d3e", + "413981cdcd8428b9e76a34010c95f810ed5f39ee66edfcda92ab0ad94f030871", + "c70a24bad26cd871d82aa4563199ce82ef8d00cebc48a1cd698ee1933ac8b6a1", + "d228077b8f08942f4f5c445f5431f6a388d8dec122471f8c24a56ac158c90742", + "6d25f50ee65fc7f46a86e1742f30afc9b9536b65762f75d9bfd1f9ca30df1d75", + "2a7ac6f4b98849eb4d6962c424e91537967350ccb60c5cad40400d46fb0dc3d6", + "002721d6c430ad953c9125dac7b4235a3f5edf80309b23fe9e197d0ef12ab0e2", + "5464573c76aa26e98ba45180d7783bf83a1ec7939c8fd8a210d635225ea1ef42", + "18395b65b1a46358ec085bafcc873706fb482866f54e02e042522ca09ff0452b", + "f4ff0049dc8e5a4f06c6274a1bcdbf8b5f8b841f921a264ea5bff329378fd620", + "19bdc09f26645d82ea05cd1fa0ffaa68885c66530b3f0eb05caa55b88aee4efa", + "6e749013aca97a5ef3b156da4dc621bfefa7e0186e5e4780c31332f9c62ac7f7", + "83800ecc6922e2a6ec0c34c1736a12f2dfcd94eb9f27c6948a5b28d961bc4ae9", + "86b6fe2935f0206083c952c07ea38c25388955eb8a806c10b69d3e52d6ec26e5", + "c7db870f8b09b9498d39b22d828b60a806f7a1de694c43d6bd29f3d76a11677d", + "b0a5809735c83af8e2a3eeb93da682bce95a699392c0c4abc8d39188563e381f", + "63af21f2f1f82d3120e4d8d98e7fda2d9f39d102c57d9c4111d07079a9ffcd47", + "ad654b3ed5a11b9a2a52fbb8dda47e91a72e4b7bac3a4ad75c074786f14c046f", + "6d9c4a44bf22648c07881e2e92a81ffe002c0177d9c7892416ce01ed1f693872", + "16f279dbb7b3f6c3d4f190b36fe2283efd5fe4669a8f70d209eeed96bf55907c", + "f79a83867614dc4d2e47353f53cb5a833582446329dbe4d5e8df941b0926dcb5", + "0cebcc9ea3729487c1b20adb8eafc6fb1398710ff6179a7fdafb1985e5f4a8e8", + "5a53e473fda88a9fd818defdc2c8e68bcea0c5aeded0c1910d6004b0e77ace89", + "f97965894c4a296fca42905242a27228e5a9a0150d31a55e65d637dcb9e60ac1", + "ec72e9e2fe2f4ab9db10b7075012c9e7cd2a4ec02b4c3b76df17f6eb27348ca2", + "a4e92900f9f5490b66e21fb66c0ae094d106eed5366a85418a86a8a3f8f87a28", + "225f9b50f64a97b9eadb625662b1a3e1b400eb86cee50fec8c62260027b4d390", + "7cd4971b0e5453d210c097904480403bd53c689c19d52e4c6a37925d14e4272d", + "3edaf61be8b6cfa06e1d42d7abd2df32033bf63cd14df8bb3ce337e62a822f16", + "89fd8d0e59d8d757b7b8a2c3a2b826e799c028df166b6897a42f08f2284d3bdd", + "0aa2fcdd4e83a4395cb5f514e1ef61ad4bb517d7c60f81bea030d5191c3c79b7", + "80a275ddf6caa4bef4039860d4c5b0c4bd1009953c19ec2c63294ad2c5c56d1d", + "e85d450859d702a0473363070a6e56d5021ebd1111232f2af28a60963acdac16", + "54d48012bd8af661baae940fb6f0d10348137cbb13c650626f42051ede55ec8f", + "bdb3a9bed24420afc6eb80930694b4afe0992318e49428156acdba5762c69e16", + "cd277985e4b4c30a1999e5bf7bf7d1cc903640a04678a93efaac1f9bb5482dea", + "d1319c6862feb76be11c089c753f62873519ec29a0c83b186efe57f50c4e342b", + "1662d8e6db309700071a801a3d066ce97f942abce241c8085f3c023258927eb9", + "e179e018705eb4a49ffa4e5271ddaa4fe1273029b521aea7a69f8f3b764ae017", + "0e7347cdb154f6d96fc8f9eb8991d4149fbb21aa3270c21fa5df61ca140a4fe1", + "34f182c2f44fab5ac9403543493be5006814b2379e0e6b7603d9e0fe76390367", + "5d710a970ba007bffbecdf12d1c920fbda1f57023719d506825e0895a2affc49", + "b99e0f318386f90df96c1d1cd4dc5bbe4726410f327ecc108108ad898e450eb5", + "7eb9ae1f66a6bdc4482175d031bd6af270fe4ff8ed1138200d6eb49e13c0b8e8", + "c508f259179f67bb6e05f2b9c5c5ea751f3a013eb925c6bfcc2a2624b962e059", + "f3d0dab6f1d30ce74c97b89f551ce0820bebad2bc1cbf4a449b11d5408cbc140", + "c116f6b0a0852a30c8a913f9e0f1680ff1be06165e19eac4d00c4e426ca9b6b3", + "25df122cffe53ab88ecb94abae69e0a1d4323eadb7537c5e4894c0e082d58529", + "a30d97485d4162e910b12effc549374221217c216b0a9487e5a07ff1fca835b8", + "5dffc63818983f6a7db2ab65d621819be9f9edac58df38571887cba22aaaf1cf", + "f042fb4eb01dde4b285f2df328f191d20621b66d7630d7691e7c69d9dcc04aaa", + "d4190e2bf10aa9800a1c2395c0c4a38737c21f8bc42dffb23addf762ff309aa6", + "4745eb0e771c9f2144ffd4abf39e63cf6d0ebe122df5f32ae399cc15fe3f7011", + "806c6fabb4059c51865f723ae619e0c55718f973680ce7eb84c49b935f59ff02", + "3352add492d3a7fa9ee046040a3f7795ec375af875c87edfdb0bc876ba14fb3b", + "27dacac03594cae3db57550410bd0f8d166863992895548f6ab782a8332462c6", + "1708396a0792c2a33b8174dd950c4a59ff06e969db873eaf2a7d82453e233b99", + "5680174e172ddbe6943cede98f8d854dabb66ba65e3349889cd6262cdf462a80", + "4960f0a4c46286d62e5bae021da9236ed0258f6c4c779e1797a9e66a47803f0a", + "06af18499315d7dd1c8d16533f0e13465d606fea2946115f23735f3fe2f6d882", + "73b0ee02de1c3c8a9b2bb8a9c48705c94df76c9c2d68a222c8493848c75327ad", + "42044a69f91d56cf8b993166dc1c724ec7d311eff01c8255987cf563d58fa5a2", + "187382209cf9b45c1df5655801a651463854a6495a7aa303887d65ad0e5b915f", + "3e44b609a8f97ea0b412abf88d7748edfa62b7ceaa174bae008e508b1b133e7e", + "d7b1c0b1bb02a6411eeb888ae26bef44c20364113bcfc1a4a3410396cb52b950", + "5534bafcac199f54a48fa713c6f7ce0568abb11b341e38b4a968bddbffffa507", + "84044786d9f286e241174796073fa47ce25c154616ba27bcca69fee1482942e1", + "45eb4756d3895fffeb913bfe149746f56e184f4c336dad29f6f81fc2a3c0168f", + "b583374fc47629a3f17476f4c9c751e851b7c4881eda50e4bead35afb77808b5", + "4a99c32e81aa36e824bc4be78495458020b1faa47d558e7263f2c0477a16e6d8", + "888b7d3a32f4566f21407c251b753b3890604001db91f88d45773bd8a53b2541", + "53eb65e5b425e7acd56ee6234ebf3d40d8fc45a3dab9968ef4273cee307c38a9", + "4bec2659f865dfae69862e128e710680f6114866b3478de257aa5ac596a66555", + "543cbc31d271c10cabc9417a4d4b6dce2dac73c20ff29bfffbbe55584e5329a1", + "4970e6469f1ace57971103d1cb2324b595ff12e34504ef8249971c76296bf578", + "010810a21205384741812fe98489cd7369d9f6fd9e6818b11dea07c78a9073b1", + "6114e32fefd54c1c2352ddf9327727bb6a34a9d8b3b7d01ce59c6e0d32700c7f", + "ecc7248b7093678e7e0ae58333dcef0eaa18e3af261b2d1722d29acacce2b8f5", + "0dc74b6f19b2bbf2f0e7f511440d4dbbc67952050bd1d962516b683ff6f9ec9b", + "fb8447ff1d5200c3acc768992705d2108c37d61b0451c7474304803e76cc09e2", + "f7f222b44c49d2d927fc2e345d64e2f0ed5f9a602758fc5f5ccc73d3b087f8bc", + "295fcc7e7fcb601b3e79c1fe403f920b622bde0f4975419c3641c4d432de8f50", + "e016f042fea1d93fcd4b2326b3ef6051e293b4abd066b3fa5c430ee7773e443c", + "7c8347faa1ad96169f5650a1d138cb10bfb60dcf861d5e2dfce6a986997d2dab", + "060928467611f28b99eaf284bc6c09d6760904ab5a6f9e072a44915a646e3ec8", + "ba582c64b17221c46d27aaf6ce1e2818562639b321666c9513c1e85727a623e6", + "e73793b5e2f33f60887c6ab428339d40d21fcf75530640977215a18db34fd7b4", + "0ea32380009a639b705fda94d93ff032d88c3126503c0d153bd0f104b039bf60", + "13bc02f7f971910299b8c61d1a16782726f22f63b31b2c0c34faece873f4418e", + "a99b3ff6413262f46cf9a44c3fd5d0424109a6373e2580fe97938c3dc04e60df", + "72b149e7ff672ec6b23352493cbcac166062282911e46d2b7ae5228d5814b720", + "58d67077a2b7c836dbc19457eef1b1e1f734370a800388bdfb1012840fb7a240", + "32beefd48538abe6eed6ae137199e03e05229d124628205cce7deb53f16f8f16", + "3e466fb18e2a437292f00acf9f6e662fdc6e0b7cb9c0606e613f81dea3393e39", + "9e537b504916eb6e9838f13eccf0c753869351778201c0813ee35cc19205409c", + "404c0560dbcd46d1aa9cd76246654bd01f62ecafd40ad2b364c426f849941c5e", + "89e5cbab667b81f08eeee6203be0af9cbe9dc0655af3272b0cb3fa722116c200", + "2fac933a1b980ba75339703bd02f055199d057355a8dbe94eda724b79c2ac534", + "c0200b5442f41779316e0e250b7bd011e12551b2babc96c9228f3187fbd17eb3", + "f3269d2e80bfa7e80397a8d32815f8fb8fc0f8dc01dd1c75cd57028a23e5d462", + "d6be8612904259aa6ff43ff30a2bbeba9d58e02f479f56480a8affc2cc9bee64", + "a67a26fdd90d624fff7e501a2a946681134e32e3afe3094a0c3776229e7db123", + "43234eef33f6bad45358374d23ded2731a35c12f472cce58d05e6d9ee56c9535", + "f0217ee0507b13f56f8417d0c92e3c76026da461c22dbb7fdb653442dcf65ef5", + "d9572135cdc5ba7afbf9871de3ffb68a1ca933e71b80e8766e678bde59ad2b52", + "9d32d8b859188827a48fbbc7660900fa51bdb1bbf3dca2ecac1392a1cbbf4757", + "daedaf79fde7f4e49a736f5b81f8b6f505884a073c5aad83997b93bb9685ee52", + "fe7b3ac50841778d1283fd836996b1865f45392d53b95f3bef90ff657a49c498", + "b89b62e4130fe4d40232744fb0182d477047acd40d32bcd15fb6181ddc890a6a", + "0ae8786dcd5bffb824b81fa038b10c9127b811664e0d9655d537d95377d6d93c", + "8608661859b838a7e8ec16e15db1114aa9db6b141877851e09439c591b46d830", + "f5351c6f978f600be1a71c95abb638bdda6a7f6587e3f66d53bda682c7aa631f", + "b6bcd591af74caa9970d39e0747bf20b56590878cc5b76da411525fd34b71cab", + "bff91200c32ad862bfee68ac1fac4b85a5e6dbba4fd00a7d09e217cd226ce777", + "40e5ebfabd309ba228262431317a2ffa268074cdf83556682d5f5e8c77660c5f", + "c379dedb960b327f684e794ec212b11fd1eb8f20a6847676554be85aa4527dd2", + "e405b5be582345aec0595f1a8a4aac6f938b539426fc7910d1271f3e2d1ba81d", + "e27a84e97deb5450a38bffa685d0461dd90e2fc9c42aa1261da9d3b9fc8fa0b0", + "c8b7e2506c4a067bacda36ed1bba81327b18a08df3fd373be25690754b9d8439", + "5b8d011d0493a5217b74d086f4b8db0bb38cb357c76b1d59441af36806ead1f5", + "4ad67654188a9c920bf0bfe25d22c433035d876e28f79a7c09c5649ae7b970eb", + "2a0abcd054b303b2077ba72f2681a7832712b526b145b62c601741efecb360db", + "bff88f3f33650251f8b9240b1bd94621460baf5d7b224030ff9619a6c708c56a", + "d8846d0b025e2eb7eb528eabb478902ad5990d507837cff5c90b1113addf4c75", + "e57728788dee9191e55c5588236680bec3f0d2cb8791da18064a6cb341b5b7a5", + "f291bfd6644103b66aaa5208ecfb536a28d6209462f98493847ba6454896a369", + "8b0a1b5d290d1069f771d50369e662c5f3c777c2ec3efee722d1a0ea58c6d9f8", + "cdc255d80343d5967d7418ad4edb8dc99ac7138c1621bb8beb0869dafce44cb3", + "cf363923e12340b2e95d7397df7775e3d6d98539001d8262048c67539be6a7c4", + "aa8e17f4657e2aa01025d8c99112d7002fdb1abb086cecb23d428081c87de6dc", + "59faa9c953bfd7f7d5bbfd363c25ed4089fbcce464b2958c9aedcd5729d4883b", + "fd0e1bdf4b5130bfaff244ebd4654e4e0df288abf7673df010bb909286f93ce2", + "f354946364f1449f4cb93847742ad2c2ec82163ccd7fb4e0d5687fccf1a2a018", + "8be151375abca57a5df2d52428068eff6f82dad14d5bfd08a699f8e747ddcf99", + "14a1cb09f11e6401ca5b119808a3176f893db83a0c23fdf9c393ac78139ba429", + "1043a8492b8a8b6a8b9355669a29fc3f2e1e01df59741458bfd3014420df60c8", + "338caae1b0d36e70fb8bbfb3e230a25fc9eab8a1c5b727e1313d61174c878411", + "8e1509bd3f536a9e263d5bbd5cb5d892aa0a860c104f0ac7af74493b9d164bd6", + "de5f3b69cd11a453b41b1595ee919dc2175ef867541c66571e6e71abba2661e4", + "653934f935d080105495891b1889aaf0403223077af687feab1570d03cc08d29", + "0cfc8f472eb4c7e30b17ecf91ba335ac1b479a01154e442d33fd0ef3c0e5dc4d", + "fbc10b32be63288c096a64a70c75dfc76f162707be9f3b09675eeb08d98a4f52", + "d76bf859e647ec2d77a7e5b5f726e8eeef59c6ab7fc1bb01c981b5cde26f3eb3", + "64fe376e3defceb8149411cc4cdffdf3dac15aedb2ae45263a0b94877c8b9ac4", + "3faacaf41e61750af0ac6df1b5a79927dba509d293a7fa2fa9e76bd3a574fd3c", + "a7a80a4cd46db9010cfe1fa8859d658d7b4534832e7b44fddee51c53d6be6408", + "c0268812c0e6cad694d5632f819b5623383fbc3e551122cad33b82536f733791", + "a5a2a39a433131759be0f1f6339c4730de3d401c54c71813066def40e6cfb046", + "61f4ea199859377dd681fd26610068f87e99081866cbf9aeaaaa1163e68fd950", + "65e2e5e77b37be178505f7f40de4e64fd4e86acb9c6236ffec62392e8184692f", + "bc82892fc9c6dc5d7bbf16ba4b2bea2cb9666815bf3f680f72be395384a299e7", + "db5139acccf8dc1294eeb56fae06397f17a040e093d307a9e7b0d62ad6b42ae2", + "43e63ea9672a65432e1b1c21baac7741c9a5f1d6c4e60ae0b4618587415c5a9f", + "75e8ad0712bf9f279d89b252e7fc17ee0add1587df3f355568999fda4dfb0a2a", + "83491196e6dfdec6dc2d09e12380cf55fca8acd4ed8af12012e41f3ac2270978", + "b31d6592a4bedfc5e5d93565a4824f2917326566070ea981e45bdaadaf3022ba", + "ad3b86deac6998141aaaf372f299bc1c39e8bbee4807cb2b6cab7cff32749815", + "e2ea5f557fea88d002756c080dff0350e2ae9c15dbbd423a2be45d660107813b", + "7750f2575cf347fc1b24fdd5b149eba76a438109f7ae02a515e2451179b2c98e", + "c34cafe9369b18ed88c2936cd7cfaa2dad9ff105a9c3a53e49da5e7ad4ae4b4d", + "c7542b4b36dab508ea0de2e1957d451160fb3616c4c81ab967d2aaa97cc0d8c8", + "9da5cd367d3dc450b2c76dfa1ea0ad1e52db56eb7426fa031f2d08d919eb39d6", + "340a3d854262aaf7a8a6bf005c21ae7ef62d58d052c1d108e7e09e708ac3f0d1", + "dc2baad94c21cf7e37a4208a6dddd74bcc84aec852eb308a39ce9f81f6fb883c", + "d21cb6a6f34d9a66c167c4939619a8823be213ceac2bf401721495628d2ea3ff", + "55f3144838496089eb2af67adccd4b27340155cf2b1cc885427f4c7d4e7a8666", + "86ac6e18b9931bd435b38f96d97aed092e1fa8be47089f00bc44b6f7897e64ff", + "b4044a883dfb2eb9f4baa42d259f87d5ab39821470f977548a435cc65897a162", + "d75f0fca387134162a37b5037c5006c3e25c991fd5c0d3eecaed3e75df90b30a", + "4ef8fd941ecc624d1f13ec8c0004744cf907bf91f0a25ea0986294b81d5e45ea", + "015e5322f9f2153091b70b0341912ea37a0da3ed20cfe9b8356c1ce882c00900", + "65f0ffa646fdd7a98f1dde849611f19b2f443703a11ff5a61568b0ba7f85b0ef", + "d9515617ba3fddf4595e395e0e033e9eae484810a478be85bb0eb3851000459b", + "e3858e031fe47a5edc3ec572173de5ea77e9ed292968af2c6ff2fbd98e5d7996", + "4fcaaf279411aa84d92f548b1dfc05458af2afe9ff577221e4b1d82a2e37368b", + "269b84393f62de52b612e2f9922a67d06e523e27d4d2fe6d3aa68caed7a2a190", + "4d179db7af1ec1b6094dd55ce21d26a54e395f23cc3428c0de6ef171d6b015ea", + "1a508533186b12799e418ae46978c8bb8bc88fbd8a478fbea38b86987d197ce1", + "ca4e5990f8a59ddaa61ea319c9ecba88cf321558b8735fc262aaa172185801b1", + "0d48ac9f64fd82bf2e95ffb1eb9f1f73a8dc778417672fbdff374e5093a35f23", + "c2b88c4f59cea3fd60369fc386ac89d22f6998a69b24f4c8ab7fee63247851a9", + "75fa241c04b786f0f2670e0b1e45134aa76445efb26280cf6dfd926f0e77446a", + "b6b80a7ed9aa4d50ec56812cd5a04fa290591976b0adce14e6657815764d1645", + "c29d8b54a265d59ba2e8eb1bed27450e411ee34ccfda853d3a83c1da2fb75b3c", + "180681a90fddc13a6f216b87128055a0947736248d967a5170149a50ec8020f8", + "f65b08dc2c498a0f01adf70e45c5ba21205e259186f928305961718fd086e08b", + "43e8f556219e8e6a7508a2c5ae19d6de0cda7b9f6e893bc19bb68e361c37c786", + "367709af13a857bd2b97701b27c88a7c430a0e791a76c72896b575e017d8d9d6", + "c9207c31bf4517e18513602801a0780242fdb91c1ef85924c2ad4ded7bbf1ada", + "9e48f7bd1d77dc7269d121584eb914303aacf25a336d42914300659e0f67871a", + "8a2b8b728756feef55e9c9bacc138eacf609589afb4422ba2ec2bf9aaaa6b0fb", + "5440c68bcfe6a804c5865c3bbf033033bcabe29e20dbd9f36f5968e2eb0bef02", + "432af6a45d6f2a44d5d6de8c82c4b43ebfc8b289b08866d0c4b4dbfe7337b31f", + "4fb77ac98b549ebda2ba41cf0ee99b55c23386dde4cb505ccc497c4337b243be", + "3f1af916ee3982bb79c31921da5c90a315c9719ef93d87be323bdfb13c6e2770", + "4268889b9776502d948382bc1c9e065793104a2ed0e8aa75b2153b62b4ccc097", + "e14719c99770cf507552d3d64eb822c8c7a6c4d12c769d6cecb6e6e05db85e47", + "70adecdd90fccabd0f39e9d41dc19c6ba9afd11ddb7f09c6ca9c3e568b9e7194", + "c9a94d170bbcb06d9170badf89f2f3533daf8e15b633dbe7ea1d02c62fd2d847", + "538c3cff924df23105e51cb100c844f356d0d05d06e3aacb7c30f98c91292f72", + "bee2c32fb72004ceb24d207bd890ba8fb4126848d2a7bb02ba580c2669f7d27d", + "98db2db86a3da56f16411bf6c2e27f2755afd38673bccc15af8991fca283cd8b", + "fd8f89cfc412008ec0752fabaec3ac72d6c89ad81286980e8138edf6ff4cd992", + "009704c84f0954d45036969fd15c7293048156ff97a76d9c72a08248a4dcd725", + "593b8d1e74c299da61950db52fd3b08f44d6cf972ec05a5c5914da9f230cacc5", + "e8a6d9cff900567aef290b62a57d8d5528b7468adbe2ee0bcef8ac47e2dc1d7c", + "9fbba3e66b12c7c926b88afb5b4a9eb03d1afaac94f673d45639ce9d4dd51416", + "b7f9c83320d1b04ecb9b4f714da462b7bb4e4ab7cf3625a5f0bb497f5405a038", + "4da28a828ab79336ff2e766d3f32e3a5ad11af8203c4428d97ce5c0054aaeca6", + "d1d9ee683589c16d6d043f383ec2023953c482f9c32704fe1a461ba17da05ea2", + "0706b6349613201f5bbb9c6125c48306ff53d4688f64dd994405abad1872d450", + "b22580d4aeda8ed6d73c59bf74efd26c3edbdeb0865c0c4b6ca61db39fc6fcdd", + "978e4abd23ead67962bb223e372c8873f283f2d6bc3b0b0ef6a21fdedbf48420", + "d37e209428acd60797e1e77636bdda5204c56af262d487469b6eb069dc5ba0d9", + "44622d438823091e0823695e5c08b633b3f0c46af0286989798e27530a20273a", + "8902b51c808d5143cf6e368faa11e4e86ff0d373cef041553662da373376ff59", + "8250faa8be69d0ccbc2df66b1046d1ccd6750503855943542b6610a9a6e9cfc2", + "e75d7f1982478da0379f975f964410f4e5645f954ace65f0ba0eeb1ba7d7b190", + "5836da561d17e30faac4342fc555878d144b1108f27112af7f425aaf90f42efb", + "b0d06739420bc6d96ccbae8336bb9b6a28adcac61f1f5712915ef42456222332", + "c8839deeb39bf4e9743d617cf5efc29e9a59f39d681128baa4e900203acab6bc", + "9fca90ccfc4d102cb60b2a6d992c9086a71b1f742dd805cb40b945e598bd255f", + "b08651ed30172874bdb63711d32ef549b43861bae4cee530da74b7874d0deffb", + "f59d3938a18249d47eae4be568d3098688df1c5eee393a913203af87f92ab871", + "68b30e55a656422b4f7757e887a7356781f17ec91acd25d2f062bb9012741423", + "ba79b09ddea8bbdbe7723a906c367b4a51cd8aa47fbe2836c169ec28b4e51af4", + "51b2803fc4d01eb5e9f011c74c5e6b294327f44052e9e56fd0b75475b3f2167b", + "2a40ad807c8ec22752fd4886570008b62762d208a5ea24140813808b64792c48", + "f2abe05e84099e996c1f9a2be789b77259322c5b28a1a24ec418038e77a7c252", + "e39996b22b171c30c0d9d7f3f9b89c8cdde606af69dbf6936ab50d4a8540a43c", + "6782c67e3849c9ce43967ee303d29b12545af84fea12c5cc4cf1f973214eb2d1", + "3477e62c947ab134c3242522425c234318e22998b4f6d7b08422427e36d01004", + "c2020358b71b1f043bff47e117027bc8112d4dd0d65131063e667da9e0974357", + "5e3c95247d735f46b45940288d0e8c0016ee553c4939aa4192ba7e52cbe63b63", + "f3ef5179680400a2a83fa5b01d823bc3906d68ffd29ef18923262472afb8c685", + "5a2f1f9be05ce5402fc0728614f98e6c5d8a30a7008fd7114d2e32f128b99468", + "80489be62cf13a2efe0f72ae08422eb8b9a033bb24783bb075e5a030b12f4d12", + "01aa376514be84131de256c9fd1f3cfa05edd4408a332e699c2f0512e8653203", + "6a59eb72f291d9209aa9ee2580a977707b28e3714d04859352826b7003759f1a", + "1ed1699d9c8af25571a1280b6f5251cf690d0005bbc222829ed1b5c8368fb7d0", + "2b8966e5eafc875e7df9b828674d7adfca2225a5c5458efe5d3490932c0011be", + "d0f30db6d5b92d763aa39c910bfd007d8ce08f58cabdfdb67e38b12478851a81", + "2b5370d6abbb3e0c0cf3d9187e5173d7784622b770dfba6c33b8d672e22e12c9", + "7a874cc8981afc43943b02be3428c2150ca2d94dc2735904be475872fd48bf4b", + "ab8bc740762787a90716c3f1fee072e57031a1525d52ce20214a984e793307b1", + "40983e94cc324c141ba33a7cc5682f3f187d832dd665a2503c4101ce3710b2d1", + "ea73153725b2ef43b2e10a05b7426aa8e241284b1702214e8dd9cd04b38df68d", + "4ab724b8efa9a325df30695daea6894d5da08aaef739f20e716619f4e595885e", + "cceb8bbf3b3bbef90da4b219fe547ad6a3edd9dcd2ae48066f2511a8769e286b", + "b8c442596d32a409e8fa7b4401d52cc8cd2e82ac216d374933dd9013535b443b", + "0a7d2aaff8434564372ad339e2ed316650f490f9932afbc9cfb5f9726edd227d", + "117e018ab57319440ca476a5799a3eb87eb7738a793fde1b95c34d32cbeb4522", + "7c84152e37659e4be0e0806e11fa7ab154e5dde6bba50409c8c475f480016120", + "4408ece10877a6ac3f4320941617b72c27fe122bf0a0b94932bec6f7f6c5953f", + "5b425b6d0c9bdb4fe93fdcb1314b2ecf3cafb77cadea5a3fbc1a377c04c77aa3", + "2352dff4400aeaefaaaa61cf37512d2076e3f5bea92d2ad51aa99f630ef25a67", + "08c9db753634d99a25264104c5b816ebc15b95e0d2f84855d32e52aa90aa5aa7", + "91d5571d7b2e0566426cef2498a5690d32e30268c0a3fc17d81e4d3acdaf65f4", + "71b408acc816438a0acf055260a433b5b0fca4a71e627e54ed7dc96079846c44", + "4c925a3c7a5c74d7ddc2095fd3c303fe9af708cc54ec1992f89719a45dd2e98e", + "60f56a735c8f2f31efd7580c90234e9fc622efcae366849c63a796bf80006a12", + "43961c4f8130c54f32b45f9ee39fe88faab957a734501e7b49e7d7ac8f81ba91", + "5a503cee0a815b1fcc4ee6000b8d88e7fa176f7fff7ebf9abe82379a9b87037c", + "b19e6f323d3ce96402d8ced832944a02728d9df0674a9a7bd8e18dc8896de94c", + "a743c955a7b559a34cd8907b9eb7a40d86f8f15a58e90b91c9fd87414a91b3ef", + "b40cb00eaa805c0d27e86cc289e34dd354d820453382954ed2ac24b27d38c2d4", + "e6016c78523ce714a360f77ad95b39bb8a724277430e6d2ea5a2130334011727", + "e8b448a80c197809e559b30fbff6a7a99d1bae7409ec00c766a129ab70e47f5f", + "4b42f9bb88185504e200703c9fa8dfbaf850978a60827cfe7688c434c502de0f", + "d48a1aaa1f36222f4acbaaa8218e249d5774acf8abea087c9e2ee9e21dfb2ab1", + "cd11b4f441f834863a99988fbecdf8b19be665edfd19ad4db62e9d290a98bd02", + "2d6152c003c48f8f6a1c2693dbb5cc5528e7e164a16beb798c772dc85bf2ee21", + "0f9af43eaaeadb7118498e562e45be1ee1e9b7f3787cbec3aeadf671a2c3138b", + "e87a2dab8e4945b8ad5fb7866efc8dc76a990638822542050506d1d0ce78fcb9", + "190a2cb2ea8d06d989d23ff075bcf61b236841a27975f89bdc085cff823e55b5", + "9b09d9ad2f3a069a4c9b33e77a833fdd6605a2095e8062f8b79a5fe197dda466", + "06dd4f3da3273bd8fe869a976006745235347ab53612247d3e0428fe152787a6", + "8dd9a7dcd863f7a8fe98ed2f452615a74560ab7ab964ff161834a6a6d5b21b4e", + "f68bdbccd9955c40848cd3acd5133f991becbcfbcf167e5d2018633229d1c0ee", + "d1990ee38345d528f0115a99aad45cf41d3a579ee1be601bee3820f181819f4a", + "b09339818aa6b2bd5bf955109969d4ed43830a672c479cf34bca1661e31eacf1", + "c5dc68711d6a27092ee6db8a779fb1972d769a4d571b759e6bca257493b1031e", + "6f7e4d57051949b2abee3b329b299d5a6f555d06cdd83811fdc7d47531ff7179", + "45862ace8f36c0a3a6d4326817f0f80f3041622e85e055e2eb89dbd20d0df324", + "b57841327c49f6c475534d0cd18ff578958790ec3c819fa5b97d8971252138e6", + "e83615f0779683ae32d0f019b20cbf2da935403f509bbb9d2856dc6201b613bb", + "be524d2863d41baa171561328c9435163eca92d6e085ebbc29a4e1e3cc6753ca", + "9dd3c020ae3e8be0f6fcc97e6fb0e8c76ce9694dd5ca8a2e8a122862f552a421", + "a7e5ca5d0be1d24c69f4992a3c1a8f6acee7929afb2538045cf737a5da62898b", + "0cdda68b16cf1e43b41e2d2eedd6547972eeb5491930f9057a3c07743219c1db", + "5c83bb522e9a09d6c81af19b7802ea30ad22484b2ef0c609ade92089e7cc7248", + "078a6ea5a4b2be65c7c423f7ad5e4ad909248729b08264b8f880ac5d90f009f3", + "47a73b39d4c6c631d148b9cc31dd43311d20d91592a9ed38d87579491cd9d855", + "7c625f9d16a27bd30064682fc896a15c4b504819233610fc4a88cccd271059a4", + "5f4c8d2cc5c67a0c393493474e74f770f8c09751cf4d712f0c906d9d3903fcc0", + "60a628f0347932c8679436a3990fe3dcf4d02fb9c24fe4fa9754717c176591e0", + "2fb12765c5e1eb877c55f574c7dc61a4123ebf7c7e72cec7d7e83420fa578853", + "2ef3b7fb03472c1f5a850b40e9b35d9a35d87f05292c1d0d43e863d7e7ec6b4d", + "16c54fff31b4da393130f6eb18111392a96e85421fcd1e928e17ae6359172221", + "56ba9c544d0ca5de806803a36a1a19f53ba914f286d92bce307f91eca1bf5652", + "e8df51a49ab47130db5332e1dd3f1ddafcc2ce80150ae3c37eb100f5627b6bf7", + "3df4f7f14f87f1ea6df31b9b8269cbdcac2d8590a89a2753d7bb11fb8ec576ce", + "ed2560a20088fa40572837ba321394fb3665cc60c15b19a2fd07333101636953", + "a5ecff5434cd0aaa835073066abb2b9f9e493dc4cb7b26983f93c94016c09297", + "af3ef059dd49490d483b8b9a99b245695483aaedbbab25bd1a54852a26b28802", + "39d2d8d4da53e0c373a1e39b4847821317f10d9fff8b7ac598b59b7e79e2be6f", + "b1b6815684cc275278a6a3b0a7de9d0c7d4216f8f1b8c8f1d075be64f70b79f4", + "fb2abc840ce8866537feb12531f420dc89ca9de12b4e192e1ef7cbe765cc4816", + "c7b6c8d3d4ec9457b4caf29f6a6198f893c51263d73d0b12e8bcf5630fea1e7f", + "aa5ae0d35bbba924165abccb826604c80f6ad55781c49e6016bd8207196f66bf", + "650d0eedab56b6492743e7c5692651e8aad37d1a9939963141a67482a686e9c3", + "22d0b3451a44adb8ef87a116097e3ac46b5f8345ebf0e5dcec960d2d55a72d93", + "e3110bee29e6162f431aa68c170d622f819bc4e289f6c3e1d695cc444c8dc171", + "92fc6ae91caffeefc1e16e816a0ef299e17c63a968d41bb91b7a6f587d34f8f2", + "76d986dbb1668f0069867df25251328e1aa15e6f31233eacc856e61b5d328e6c", + "560b821428968d6304aa517c6b0df4469059a097171e7180c0bdc1de4c8c2dfb", + "199db247cd1e61526d67823aa89c7d3e9a849a1c48983902f4e732d37491ac95", + "f3000de6fbdf5997bcd5067bddbebd4c1057346a69164446e4a2fd0fe575461d", + "a36d62775f7abac51834f25572e05a0238de20394f0b8a5cc1f33b47984c4a02", + "b6df530ded76cd3cac5eb5f6516b7b5aad5ee43beb0876c8f1eddc38f4e173af", + "a939753b3d570927cc8d90e512239cfb6e52d81b5d56ea80eb4d0c46572d9ffb", + "915cf3bab38606e7266a5f7c2179ad469b07b134765900ff6ca2a5ab83fbe80c", + "c084076c1d75861928f6a28f566527ad2913a2bf829489f57dfc8947c368c50c", + "b71e574bfd980be24d08395d32430a000953a021556ffc4d7693190c00e74e77", + "4cb8e212261c1ec6fbc52df8cac337d6f5d189537645f7d5a7f911db882db716", + "533abcd8780f59049a8462ab7ef505f5ccf1d342150c30dbab5949fb084cdf21", + "db533c3913044c03977baf092f0156071c2628208d14d81872162436b87123fa", + "ca8ac0dbf15bef994221f78c0248b9a6408ead1166e623c6db7652a16c016c06", + "0990b3a317a95d36b6b6ff1fc6736fea01529a39c0fe06e49b7fd564080b6df4", + "787469298f138d1c490251895a85d6080e52aa1df375643d195dbe7e87847949", + "68a371372ba0f489c8ab8672e257f265a418e50c6c969bbec4b24105850c371b", + "f8ba30581f3cf31050db47b7a43a5551d13b9ce7001c896bb79409f4f1d0a31f", + "23a5c21151808ad600e97139099b79c3e1ce23ac1252db87b2d4cb88359cde4a", + "9886c12c93ec0ea5d14478fa4a9a38c4a1025f8b9d595deb371d6aff79f93b04", + "4e4f81cf1628b6f43db1db218773db5f7a405311aa7ee5ae2072148a867a1437", + "a019556336364cf6a38501be26e917b0aa2cc2c37c137fc269659aa44a3e5ee8", + "7c6b6a1324fc40c2373c6215ad699f723b826f02397eb6f6326c1a5ed558a115", + "bd21001934e50a47ade3d713cec8dec036d54da0a976ed3b690ac080a7bc9ac4", + "4db0292758deb6ab35aac12fd6387276b2f32cdc859a15642f01931c845fb0ef", + "5431a87a0cd32b11db5ced545effb22f220ffc69a60ac552844db86bc7998e84", + "c6f343782af6f6ad784f9f85c1123fba0b82ac07119a61ac39ed2fbd92fc0a90", + "c412d2dea34ebbda964b17cc12c1da70e7dc428c92eaf494e17100e9ac65ffa4", + "e9b947aba6755de52a67725be3849e3383e187099aeb1bef5315d22609368b23", + "0b2d1949bead9e9995b717a821f6e5dc3136857b178638350a16c1a32149c6ff", + "dffb51c4bc98c9f0c07ce18a88877d48947706cf57f56cc6a29ce82cf2f74c07", + "97dbf9a48f3c92fd4674b20ed6f635bbd92af003de523feb5ecc4758169e5857", + "cf701be33d1b3abd3357a74ab2f7f03fa78f7ebe0362994256020fb1fad75251", + "fb26930bfdbbdd6f69be65c3c01536e09a30cedb747b69719035e31636e47937", + "5725b8ebf2345fe7e660579ae94b84e251259487da4428a5f1f0cfec19db0639", + "61b92f8043e5a035b2144fad7b1df3ca7130c56cabbe0dd2757dd737719719fc", + "35a2f85adbd0b6a818d84a16d8ccbc0947bda389b6cf43ce6ee9aa9b4aea19f2", + "69dbaa6d8a1d44259bf095b55311dd39ce028ab20de30ca49b1a173d6171109e", + "3b68203efda022be691480952c5132129d514b2cdceb0d3a8f137595be7f1c25", + "388d3dd82eecf369794186b42c964871575c03fa9772d57d31280d678f87a636", + "6b493f049d19b3d1e268babf0211d48abd018c912787a913ff71457d54ac3606", + "53261340605f5e63ecea85eabc5716f31be1d16d1c91c04f3a090773b288e62f", + "7bc9e1a6b36320bf97e60da91091c0ea848653a56cfda13f129e42719623c500", + "7347ad263d90e20635eca6fe1bc89df06a4ec273ad50e1ef78a27fb9fcffca13", + "fd286c0a1d9e4a0e0aaf97b7f2da4b219e385d7a9afc4982698574d72ae49daf", + "0f42f9f37bf77fc47e1c75e0206654e0059703f2b7dd261f91719e49e586e7ad", + "5c1eb8efa7c26ebb6a3e2cb19148783df0b7d5dac0e80b3c793ecb556166a88a", + "0fd0ace6df8a7ba03aa6d52f78c3e58a4720a3852ad936bba80e08bf7c61a287", + "020d473e1c9504366a2204d566b1ca6c968320e30382129f95a40c739cba7f19", + "ff7051698eaa84a3ffc25d5dbcf923933a1a59730db33e8161698160a090a45d", + "df0b1d015cf77e47512cda3d49be662782e52715fb747cd0f73934109a066ff1", + "930edae2fb341f18849c9b8aace01d5ed2cfaa013f59647cb542daf33ee60db6", + "acd5d0212d8d1bed17253050038a481d968cbb8007c5c69f35d328cc887723c1", + "17f76ba6af39c2d64b95554ba27b12818e60284d166a9da60a3110091c1dc3ec", + "e03914821b7b6185e239eae45598d504558175443c4f34b700c2b329beaf14cc", + "cbde6cf03818faf19d93a082a509c3601a6b043a66a222d389cd44d4a91d38cc", + "c42abe41dfa793e60e2ab797e626bd8a5904e03633b61c513c9f80c2be9ef0d0", + "b47f9040a5561c94bb5c11ac8042f34d80e144a3f5a8b7c0e47d7aa3c10812ec", + "07f44825dbef5d2c5fb089411e2eb2d4f7e7eeef6697852084acc7b1f48aa77e", + "ab143316522be7e11b267b89677251a5a1d48ce7c90ee27a6c66e1857d71e576", + "3e03c42ebff10fddf113e731d91c96cba59d8d29acb8f6e63e0fd171657414d2", + "12a073e1548bd7551d477f3ffaa790d42105596f7c161a623650f1a053c6f053", + "4882fb1b192fe817b836fff3baf788d89442f75d3ae7a529297febc3ea896779", + "a3b5c64eb6d70663ce13bad27c28cfe46ecbf721aa12d667f3903e155f37471f", + "19283ceafa4e91731909817f264f997836d529c523881913fef99c04c23f636a", + "2c9ebde51c783478616c534f8cf30c1d9bcba627e82873678054f8c05f72cd20", + "6addbacf1779166778a472eafa5388de1e51765c9264db7f44297808f30cc940", + "33c39d4cf80eddc5b93cbf11f5cf6de02023709b71c3388dfffd485becf93de5", + "73d7856eb00d8f0eef1cc6ebab299384aba68e5e7c90e4526c4a33a3f7b9725b", + "f45968304cd3eaaf430b7744dac3bd2c287c51ab1418e02973fb2f6bfd3d7847", + "1e18a797ff5a58a2a3251358d7ea2e84895a939dd63fb5a3c77753f324b9c86e", + "3ff531919376e2ad060436f9d27129b7ef17fb8ff29cfce1ef3726833aed96dd", + "92a1626588c06d5a3d74a59f23e506460f99323c09d35bf5d49bd73450803f50", + "be288f4cfe34bc80521860940d76e5a42c85f68ca190a4ad71dcdc18c4198103", + "9339b6786bf2c8c1dd4830c384e03dedac22e92bc1aa9cdc3449c674bef72ca6", + "1b22a0e578c641613e3602764129e027e101e2d423d9fde2a6718a7dbd726eb7", + "c6be938d80860b8ccc96ba087fdb2a00d980808a61dc79483710ba87f1529dde", + "4bbb241fdc1feccd155ac9feb2ab91cbe714030cab20dfbf34318ae72ecd4a6c", + "706c4c5215f54df3c0c256e9d6c54de897716618824d1fc630d00d3af059352c", + "93e883284fa263b442a033252e84dd637a6943f49f9db224855327139dc8417c", + "689dc122cf1c3b550ac11e0041b45844b794a45c31a2896a4238242422b23819", + "1aeed96414641bbf27a32c1a94b5bc224947f87bb915a53dd2362a5baaac9ba0", + "0c0f53761b2c3f168b01cb9cc05894cb86ee53a02ff0c5fd5f160a8d0d15ae76", + "da8b72cfb9146b496da6719d87da1c52f295086935e1f15da60a86b24bf29b97", + "2389058fa5e0174f217260522f49d07b76c7758b0d3f7296809104cf78561b40", + "0a59439df43ceb691a13038b87b7ed5ed2d05f7acddeb2cc502d6df537b71311", + "ad9989dc2e5e4d1a473e2f30f9581ad5dd161f68765adb6d2e70c8effb23f3ff", + "466309e21fe5c8555a231e5214596e196ad44683a8e8376e453daf42b8085d9c", + "a6ceea7c560556cdba4d7b433400688ffeffcbf27bc7d05ea507dd2d4ce948c2", + "b77e4ed4bee931d43571d6f22f0ae253ae2dc63871c5e4845c6676c7dd3f0eb7", + "7ebef185856fed824ebc3f38b79f82746b2983f8106c2ca6a8b64e410040de10", + "e9a0458703175d02b8fce2b8ca87d2a6062270e97499130aa81aea621b42567d", + "ae17cd0fa9460c9caf2c158e1ac809d0baa510933aca2413c316840ca42d69dd", + "0d485a3ddf329e8b8dd47b569c7ba6faf17d4dbe280521a520ece56136ecc218", + "20241e5f5bea83a9efe54520cb4e18b2bbda3eb83b0875f44c62994cc2f4a481", + "ecd706d3f483600f37f3b2b268cf38033db1290e0359a089e7dd44d35300eb39", + "326a9ece9955f2f872f0f6e401ff4281f1992d9f22f247ecaf25a44f5661c732", + "36f0d608c831ea7365b0ab0f2d78d44f9b6cf0a3edda625d02a0e8db1250c644", + "fb476cab0554ccbd17ef87ab086fb70310f1b468e61050e4c47a9c003f71a88f", + "eae0d52bc60c959f73cbf3ff7a84f1005b935e3d71b64dd076f23851b61e19df", + "c8320f5862f6c97350570d41ac318ab729aac137043d0ca73f467eca670fd2cd", + "ad83f844f80d1bdc54559399d55f09160e3041f63bd85f9482ef0e0159e576eb", + "faca90225e0b75e9ecdb380a4f45937016f843f188f48a956b24601dbe2e7eeb", + "da4e04a00f8b7a2724c60b78a8df4caf6d3f65a8f5b8a86561ceb9933eb87a72", + "c17975d1893a8b62915a085e714f9c6f85d9442f7916ddec6a822090c8dc655b", + "7b7f3cb7bf35478e8dab8f33acc8746aa6ed70189e0daa8e893783deb55fc50d", + "d0311b9d76278b80d7f55b8c4bacaa8dca293cccea20a784ea9fe254abe93b5a", + "91d99863528e63d01fd353457a910ad683d11614458708b31fae2454fa224b5b", + "b0ae1e07f65c9dd764db153aa46cf42f68d780e4b28e642a4559ec90e8e3cc39", + "43fea848ab8ea34e591ac9c9f98783b3f83a812a9bef6339e81bd380d4d0b0d9", + "80695fc224bc3d9f16e1be9078e62d91b1769c4147dc44421531a213b4a69460", + "ebb6cfe7e39f666efb4e256af94b5b21aac14902ef3c23e6788ffb32d16da24c", + "ef5b6beeb6cf57ad290d3be9fe6790221fe4685d99ced85082f66cea6b2e2d96", + "a4556671f393544c118880d5e2b6e0b6a9d571325e35972c8bfa5ae70a9aec8a", + "0df55165d1eb0fdd4b62a25222e060b21a4e5be7b8388e29380b86b2940efdad", + "19b60ec413d47f58a62173098d1591609281e701dd079656a09512e8684badaf", + "52e4284b527671369be86b4b20545217254f85faaa8abe39953b59fa1557442d", + "8eb1a4325c5783a09e0febbc365313f3c4d35a2c1618ee6532572ab2ef1124e3", + "48a251c051cc0c8b051b806600f00342ab81e3aa111e066e74adc044502379f9", + "97fefae91a85838a8b93a4a8cc0edadc2e50b43c1ac0e36a259a40fdf8a4e213", + "aa1342bfc905f3f255c44aa09df477228c41977c060fb5f936fc6b4838c5be7a", + "b9e6594d64492899962352ee5c918c3ade3efdf92e16bd4320e0da27ab3ce2ae", + "1a5db6b7d6da5386f76ef77cd6ddc99ff67de917819afa746dc3c5378a95f30c", + "bf64bc337a886d5d799d839f03eee210acab95f613a17db30165d3e249d07e87", + "37b794fdf56c4ab8d2b4d00506ae67f02060625d55adf6c208cd2f53b6787cb8", + "7df41b919abd69e7acee85a552ed81023aad7a4f1773b35f104af8d253e9020e", + "71be6114d2cd31bea00baa69981edc59bfaa7891ed5849178f3711f37e5a9223", + "7e8908c6f9e14300d44c2254d022578f689d8c0cab56ad73ab97c748a71e7371", + "06a70fc0c9e0e0a3a67dfec26eb5367836dc932f20f65770cd27511f76a4ef7f", + "5f902ed069cac194a12c903fd9231b3b9b73ae499e47206179a5db78053b7d48", + "c4587b59006d177ecb76f64a083c8bafc926b243a6e9ee6ef12bf0f3fae367ff", + "eeff57364b4cee2e43ed7811728c6e0dc155c94bd8630567981c00e7983c202f", + "d9ed39b27fc2cd3e1c3bdda317c856557795363bac22e5475f58614be9da5f0a", + "5fe8295650a3ca6dceb6adda98a9b2a52c8ff5aa2b879dbe74dc479c14df783e", + "bb74a5396ca5059aee5f6973f760285858dc7ee84f34631100b641d8f48b89b3", + "bc357b63fb90baaff808482eb645d5523323bf52d98c3fe493ac8eaaa4773677", + "2d052d15ac1ce4eae8e7988c586779ca83b7f20134899d1a72bc05bc94c0b35c", + "ec20f72f981ef2d1397d3f05c0cdb32110e4c7bb9b876a9aa8296f5daeee6cc4", + "039cb433d2315ab0011d4f1b3d040ec7e1f1006fc3278b96e9a8e0491d1a9d60", + "2dcdb4800462a1c11c0e832d04a1c49d9f62103f2da332286c48fe94b456b92e", + "4a8b6e2279a7392cefb1e7da03b42371a678f84c856de77cf13ff9c0891d9b00", + "57bcdcfc0ded16c30a50204f409dfc81df4ad4b1fe673d99920d9f79a397d8b7", + "0261b6d4b9c1c5a585ab93951761c05ae9cd62c56359dd821486d2b1fbbb3177", + "2a71c8ea5108d9a459c6ccb1a614cc2546ddbd85efa1eb39ab3ce0a504cdb916", + "251698409786d137dbce1f4490f83fecedaf384809eaea793c3e688bbf61811f", + "2ace8689de6c19ba00afddca95b6c4d32b9929390c628b9339a77ed21dd9ec97", + "10e81239fd71cd6fc6a7b858986c7c4e1d064811ffa0cedff96ec34e3fa9f454", + "1981916f528ccbc3547bd0daed092031c4045e1d1426398f88298f7d29e61f8b", + "271cf2cafb0c86c1058d1247771cf90cd398916ee1eb4988b19765673a2c03e6", + "9d086afd05abd5ed8e554891229752b2bd98bb124d83e5c517c727a6e41750fa", + "5f2c7ea24040a74f3badd941fc602bb8ba2c2f3dc8a6f12bf3565579587b5ac9", + "0cd14ff3e26eb0253503592d3c2231f067c0d34c9a08478b3ccb6f9708e63bb3", + "7d8c41eb7c106d34679fda959f40538e268441214561488988384d52bc4959bb", + "ad844d292ee314dcfe7b9673493aa46fc09437a3215984514f7d6e25516d82e1", + "1a2a86d0c46010632d2cc42225c28e81d33831a762904c07fc165696e0b21f56", + "dd5549881dea36dc21d79af4fe8fd523ea7198354b09f11d45cdbc16f2f1a593", + "f163f190fa2603da18b5c4d6a8ce38135027e1e27765786dda3d4aea2a9cb80b", + "6e09fafb8b6e87d6189f46e244bfac72ac097f881fd3e20548bc9b2b77a06494", + "050e25827579544b11a0659b33779e36091a9386e10529de15661476f621b4be", + "2e106b4ee4214443a32a625a8050bfbe23c994d5d11f7c06604d67449dc4b65c", + "f46f7dc3ed0045f94584f0638fed6b4a1428e757d0286e64a7d09b90bd379f44", + "4a2c43aa7fa22812393b64ef4735c4ce9dad96b2324d06204d17cd489d7ce412", + "98862e2f2fbafaf00755c99a60aa0476cb449041ee71556e1e7c6442e149e51b", + "4c118375e7aac9cd504042c81c94f58f123924bcc2dbdd07efdf3ac283bd06ec", + "bebddec9428e7802423acb00e58d998f5daa5a26bfdae57697deb463e9e86feb", + "53b03c24b8c7b3c56e56ee1e708df3eeb032bf383d7cb1b2d3fd0bb11cb984b2", + "784badbf46476520b5c6635401f74e99ef7c7a003a393aa1fd57867f185dd721", + "2f3caaeb46d182b613605bcb8bedd3831bfd1ea4e7a041c4f76e0d4718aa9eef", + "1b3aa9204c47e2d2ba2657e13382926d2ead35d868353551eaf9a66a70c669ec", + "c9f2a7d878c3f80d7627ae2b832007dababd27fd2fc13709901faebcb14b2932", + "5a39dc81cb0b6518f00f5c66ac659dbd72f881a78e0c0b13dc88ade430abb90f", + "2f0ee5fedfa55f94832fb2744a80af32bdbf7708ba65a3e0d08b446358da1499", + "1d07c7dda8607fe8e06cefe30123fb4a38302ca4e091f5ee9df4013778e13fb3", + "3fabeb2e7e8cb221b62d4e11ded6d5eaa1db1380b7639c943f60a1c5c30cad09", + "51a1fd08466d6b37cbe7a6bab184c21ca50c9feb5938283d58d89fdc1035fc9f", + "0708c2dc87f7ffaecd46d3c7dac910700bbdd5f9331f262819ec9fc90545d3c8", + "2d8f59f27fbf1306ba8453c032a2d8bc6a18ad2d8f3fec4ec260af16f6213dde", + "3a7455c90f216f2692b108f4c907cde0f64872ed13e6f75b44e5f857695ad824", + "5f1fe505ad82e5de86f16977be619103b511b0ca09393daed964ec61962cb942", + "3decaf9907eda934c124f603d3252929ccb9b99aeacf745018f2cd612ba3dcdb", + "4d232fa8d81cb6abd1fb6806b45c069b7fa7011248f55d1873daa335312dddab", + "55000d6b9fd4132be58fdfcc87b55b28dbb2bc259c33d50e03a9d470e503553a", + "225d9a9f991b12fab890ccf06ee654afad4878a3a8c0bb3ddfdd54af3ba74ee3", + "40df7f285dccf966031f1e8815bec7473a748f37c9405ad98b0ccd40c87f986d", + "c4156b9659801913909aa5955548725ed2a251e9d3b3a587786094dc299a8f61", + "5dc3a95d6f96b946237a9977b4c1e05b24799729f03c191b084c6118106fd712", + "118bf0b7868f02b1154198107b4a86873ef471153967ec7bb11341caf70c4355", + "1ad3a5310730b0b698512c8c7d3e7a8552f75b5c7eec4546860bfdc622f75e7d", + "fb74a1c13b1eb19423dd805a4d85ee771ecf52e3956b51b0409cc5bbeec34569", + "7405d158d51c83cfb8dabb3201f692e3bc089f020cae6b62a2af2c88dd8f03ed", + "08f20e5a8dea1f3217c990bc112fe510ca5aacdefabd5a2a6958339abb88ad7a", + "dec3f7bbee141e55a68d1d164e891d9bf832cab148061a1551475fe33c1bc868", + "0a311678a6266de62ceb20154e6ba5ba5560507d58d8c5798474967b4924651c", + "b4f48fd17a7cd9e0b26e90a8adfa2f9a150d47e9a203cb89df114585645c87b6", + "26a5c76a25d02b0feb4b22e549ac88447abb8fe7c86196a73292f13d8eea61aa", + "2f41b58911b04c3418bcf76dfcc500d03a55fbf1749ee83a4e981fce4e32eb4a", + "411a18b6785d5cabf9b666a9a70cdfc5a1e29228f92f80b9f8d613face0195f1", + "b17d0a2cb46dcc365e065267bbc2cc36e92e20bc0ce24d144089d69282e6fa19", + "30ede26b25bcc2e6cc42f248386c61499ae477d4fb392ee289238c40c88ab5ab", + "f1bbaafdc5f0e00a5dad95c9ddf659747c74d093e1cba8a0e72f1db1d5cd904e", + "09e411ef140daba69cfdc2c3c32fd84cd572a842f53388124b51aa2eb1e40fa5", + "2c408b685afb2574e9fe2a212e92c638090e851026ac80c9a639daf234fdb984", + "a342b54ed7092264595569a37a5e5140a48d5117f62833de3dc1923516d5b917", + "b90af846ef17cbdfdc06399f2d6790194fd2d781babbcb2c3ba005031a278650", + "16c65a8054f7c7c6beedb172498972f2c3dcf0e8da240cc8f120dcee7439c95f", + "8979da949593c80f9164828107f04602d70ca494947d6ca7722180ca85c8c0d2", + "eb4ef64a1eb138e6ec7414b3d841d24e61d6aca94a870d3568f89bfeec28ce5b", + "8db828055ca1154a1c1b60b992299b2a3cc9f9ff217e15a329b642c8d5515344", + "5813ff09a79cd74b6fb3307c277caf7e80f3506afb677eeec65555b7fdb8bccb", + "e15883831714acb3d5c08a3898ddc6280114f1b41c450f6e93a27bb36e7c6766", + "95ee3e0acc204a6bd5f5b484e907056695cf5ace25449fabe87f25d61b1262f1", + "8eb0d5da9c247c962eba9081b9b58413b8f7aaec03df2813c77ce44573a3ab1f", + "afb841db4f6c64fcfda654cf058f0a883166b4066fba6be6df5dad564da30515", + "68fce267894ffd76f551a169c9508e53810bb12e773015aa4c547a56a5806fee", + "6e614d1afb783e6b45301b2fa03788a1fe27f2808cd2b513eabeb1226e6241dd", + "6bb0cdb7d8686d79abb5453c3f5045c1acc2e268b3f2b32444e60796a18d4c12", + "9b99c863068f24297df3431025ec6564b66f2b9f64c1678b528a1dbc4bf71b16", + "204b6f2f3ec5bd0f1907fdb460e4ed6fb0d25de5cc12f777a1ce10aacbed40c4", + "615fba5e47ab0d959fbcef39996936da656e2615b954f03b61fbfaa8e1a4f500", + "529895a7de063cf6e2497d30d849a8df3c4f7339e22a486044cbb3e7496fec22", + "b7fd2e4c0b845850f044f32c5d5123af9d5eaaa693d4350c4ca8758bfb857b19", + "059e015cff4b33f019921f9242533cb45cd65e507ed882205367b746b27f4442", + "1b79153a3a25bc82c7f52a577c140d85854d76b3268e332b5871a4de4eaaa47e", + "b9730c36bf7402a5d06c8226dc11ebec9c0a4bd6a23762a6e9e78d1b996a4630", + "d22427bf91dccfacba55357d4a8038f797eeff2c4dab56220cb00d9ae897b783", + "da19f42f6dc60fcc3d075d6f38671949ce2dad31b37496a638d341757822e92d", + "78132589eb98ac63e53a329f7cee32584dd447bb9fbbbc01e27245ca29c23a23", + "e467ea63a727b9c36e666d435664875c822f94f047f60ac0d89f2733bd4d1f01", + "f4f89149b1a5779dfdfa3a9b7dc5b88791deb32c20db6d611fa013b9a2eade6b", + "7b40c196aef97aa032f8a1ba0b88aed12baf4331acacae6513e8f4fbb102dc98", + "dc87f4206941dfd0431dbae606358b03741e52c9c9830017847042f99caa1a89", + "293c650a8f2b442da199ba6dc31cbc7616c09bdfcb685d216fe5f6964ebdcc19", + "cf3736739c87082a71653540be254cad9dd2b80c788910ff51833053de0f6833", + "b7904a9d282ee147c41a87ab1de0c68f6fe6e8681b728a50b07dbf82ab61990f", + "ff43760bacfbd8a86b77ee50524df729bfc322d221bf39d73fbdcc5172be25cf", + "94b19511a901d6c9863b4e698bd3203bb45f459a706f17d5fd0217b7d4d2e880", + "5fb42d90123e7d17bdf84717dba17f8d6e2e4cb9856ebcb5dbf6892fd227d4f8", + "d403da452541afc98ea45671522db8605d28a1373baa96279149d70fec56e8db", + "c515c1f3132d7124546c2149d2f96bf8899954ed28db737d81149cbd9f84ba87", + "7559f506a7dfdd3ac7f13dbc616629b56b88f8e43e6039205182b17136e45e6d", + "6b3f153c83caaa5f7bff608f4fbc514feee1ad49652691393b26e60a3860ac49", + "b25dc960443d800d107491ed43a224db6df66c12b02b7dbd3415ad4cf38c1f25", + "a70e10b5d457c0c10ae765d7696e96d1721bbd0557386cd00db27f7dd61c8039", + "c12df355cfaa94c9154a29848e53dfb2d1bcc7b68cb1506d067f5feebeca2134", + "c9a697b7434305777c8068af8b5bfd654d74bc5ad82220f84fe16e1cc9cb2173", + "261dab494a2e8adcbdfa76b171a1dfeae0a70cfee528d781883537c35ad37546", + "df2aababde59eec1b7b499858984c1204557f599659f05f6671d1d66d2273d68", + "36bbadcddfe80758e7b91dc8bbd2d2cfca89aa1c50ad41a706c99bd192c1042f", + "bbbd1e0d041cf1b340f9b0873d876ead1b76c60f593e8ba82e2d700309ea5b4c", + "07708bd4113b5e2773ac0d65323a793e5dcc366b1260eb55f6ed80f6fb3eece5", + "b7fd39d21af1c3ff30225230a86cb382680bab72cd83c081495df252bbd31dca", + "a01f7348d23cfc9c72d4c6c8d41d00f25331a9f6b28f704e314f46b9a5fe8836", + "e14b6869725e9f7c8b4d0fbd88d1cbf9a969e800bddeb3b426ab13a9e1c6873f", + "ad16ff69baf5690783aba8de17d09a92af4fe62a985e33c0969ed734d09bede2", + "d3edfb7f2ee7cd654793fce245666efe24655095b9098152093b3e5a12cc41f7", + "50cdf981025f02190aef3c09b5aded26472fe5b155c689acd1600b6f163b0aef", + "d944eee476fcd32ecf4d85d5796e17ec2adca8f9f6ce1c1f66687e2d99a29f2c", + "6eae57da9514dd54a3f5920e1891529982d88c17e7ad6f6d8d24c731e1507992", + "ba0b8bdba07211878e29c71e21bb5aba5186e50fd45b4303f846897b8c409ae5", + "16828f8c425552366cedc41a7ca7f0df1c5e5a9dc543bfd9b0df65021d087a48", + "338222d8ae91833de9008c65cf519b1d021c6072d09cfc08f78d002300b6dbf3", + "c0a24805f980cbf60892cd89af249fb06c079c694e3cd84f0cba7f21f9e6fc9b", + "fbdde53f5541ec1feec1a381409d5ec9c6baffe97ca0c7e06f24e841d2b1161a", + "1752a73a37c14d5080efc1ec13bb1dccb9bcc0281a4d95cc2ef4b853ecfa6c31", + "fd2e0c2b50b472b90d7f4bd119085224219ee533a99922ebd8cb0f2cbee9cd7e", + "3a1d28fe96af222b8bcbbd2e937ad0d1f4128c7de8c7ae4ddf7015b07a1f6bc8", + "7919fe4c1265bc194272f2d3e0a0a0dd4853b9f318baf6287484549b01404b68", + "10f6bf39d6329af3916a571ea4ae1de70cc23a9818a866be45c267284d3abfd5", + "cb0c691d7266460c4af283861919c3ee427c09831e70161c0b7485b3f420c051", + "69d7916a1618b94a9794dbfffbdafe155f921b57b40c6a55e8ca17405d1183b0", + "0c59dc8c3caf16b480847e086cc53f4bcc55c0ad0b2d6954f943eedab457c86b", + "8accbc6c5c4abd38dd2574cf0a84286709bb377aeae9d50668b2145190ff94eb", + "66213a6bb4469587379b204233f5c4cb9213c9443c6ac8a8f5748f339223bcf8", + "1512294a5bacf0e03dd013bd22c0469b02c6a56ae6ab03d089d1b59f9bf47ba9", + "a5ed327b1392f514a7ca393657194e0cc1f5f454aefdba63e867a3905c9f3ba6", + "af21c0215b6b801617c9b590a5bbca4874f5a23e7fff46e2fc380b4e501b1b42", + "48d97a310cffb3c3f576b5615ca7d30491461980e9e6ddba01e9ab8b69c6fb87", + "c23b5db245f1cc4791a6a7f57dcbd3a72750431e1a67f5f783668c8a2825a0ea", + "ea2c2b9190a3a2e7bca55d33bdd1566863284587cbcfcdad74a617a571d006da", + "f00c701d6e9eb10e8ef79276cdc154e8ba8c4422f1d08ff330ef3f55d054d337", + "43a9d6c9ab48dc22ca3c9cf8250db069e7513537e53cd632424035f96371b52a", + "9668811e88849fe80a78b79fe33a05252ec3274a8c4c924609b8c85281ccd46b", + "f2f93449bf57bbd79c852a412dc55922802044e40a10d5b70b940a585ee720ee", + "d20bda52ea5a7e785ab94177f1d90f5688d21b3968326e9909c8d26cfe160b6c", + "1eee37e54171be9e11fdae188b2edfb0bc024b5f94de55dda4b175596d449bde", + "0af11c745a8c4891e888eed30309edc37748d712e154a078d4bdd5b13cf220b1", + "15982ded9a62a5482a3acc1ad4c33159fe6c42f09aa14365a292d1f8e4434861", + "429e21c28b9f0e415e2211ff953a01b408b9399dd2b67d0cbf212653aa9e3de5", + "9b21c3118ea6bd778beb58728a3e319c2bdc18fe9bb2218024e4d7d537758988", + "730d819c74a968d95a749664b659fece7f28f85ebb9e860542969b1f83f7135c", + "07734cbfaf9de00aa8bfdda5f205dd887fd7df72535ab729d873a5ea23080c8c", + "472f6c62614c64e30fa642fe8370b2108fc6c624bfc3652ddcabdc943b93d12e", + "41161aea39995afac088fa514c8df9905a21984920ab7ed9f5ef52f603932f4a", + "ff0ac19b15cf71021ba255bb88b9b48b8d813446364defcbd3648719c0ab4a4c", + "d9836a3268981bfe94ad1f77f2b3ced20b7079f09025391357de906d03c40e83", + "7e55b991f5f34ac4389f5574bb493fe43493e65668918329489fcd9e473eadad", + "d32bc70e6246bb9345bb5efb2ffb68f6173a0b675a8cd4c7ca74b354adaa2909", + "b9d8bbf671e794da22b3d2318c9ba690448e3a41f406b903f638b5025f7564d8", + "b28a5816f46942f4270bc625ec876659d37c5e40aaa913a6223e49e7576235d2", + "43781bb2014fa51a94bb3498c99623b76eb963f48238f9b1df9b6f31322c0970", + "8dc87e8f4df3ae436fae06c170909b87542352d349384b9c7027e950b402f65a", + "339cbb45be7f44b72f31f96503860c5a25c5f2abde498a93561fb59b433f293f", + "f2e6d476a7d47acc13d2eabd4d309398684ca739f8bce57eb55b1e79f3d1e519", + "56af0198847aa027beeb25867b66c211c78fddbf74d32c1d833d87c778120fac", + "9ac6f60c776459541ec6bc8801fb2fd02e36585debfcf3212fb0b1614ae2f6cd", + "e1792f3d1d3d161adf761720889612dd3cc96eedbbe2a432a03f7539b0cf405b", + "0618cceaafb6f8e91a6c592127ea2b5b0cb1194c3210c3d643cbd25e047a2c5c", + "c7deb398d09de0de32c114870e0843d7700252917eb69fb0546830437308e970", + "e0ac7eb0b844399d04bdf65c93459a19b00c9a23bfbee05b3deca40a05b9d389", + "23a16f8f604e45d067ae5a3b94755fd327505b4a0959844aab30ff5808f79b46", + "488b6589b506e1a4501b2485091e717411867900783807af78f87dff7a46cf0c", + "56c965124e66ae2c716bf67050bdb320f38385280e7a7c6cd51bd7f7ce43d32b", + "19edef6e3e69990ec222877913340628049da39409aab79c93af2b6948a93d58", + "128eab2b73e6a6e9c535e15b22559826c3e9d678859d92653a5b0611acd12d52", + "b60504a4e6fb685fefeb7053d02d89bff2c41c86795c4061a89d0c3685da8e84", + "6038bf6c7728e0927950e6865113a34a6646cf0ec0a2d15e1f1015b87ed6b091", + "1bc443a013d98a82943adba3b89cbdbbb38d8aabbc0d4df57d218717d50f50c1", + "9765ae2f7bd51d2631470a84f4acb6d9669f3e211ba7b3d1480b80a0cb53dea5", + "cdbb6c307084183baa8b44cd6a93f014687b2e8f6aaae063fec6eb2c61d3a331", + "d386012578fa5b91e761a714209d38301129c8ca4d1d393549a4801a9b1e0094", + "05387de7db27a628101147c5e01cf481a68866c5f4be570f35a50e5a96dd8976", + "ad2775261f5a12d7fcaea9666b48dac6ffec096d643df0bdb53dab400abc9fa9", + "c95adaffc299856ba5bdceb376860a43c0a55383ccda166614af9333986f18b5", + "2db34fa42ab7e9970883aa5744fd4ae926463a3acbd35ecabd5854f369bf3f5e", + "068881adabd87a5d568f27064212ab1bb362583ab9fcae18fdb03dbc0f3185aa", + "9ef7cd94ebb5f2b74db7933a06459ad972f43f21260ce84baf26904eae9090a6", + "99afd610f6087a27df4c16d1faa3e80af5dba4194e7aad1347dc282f493951d8", + "d036a9a86f34c9eda2f4d54a8f9df4c470c0f97695c4e5dc9de60e85af56a1e2", + "7f0137290a2f6b51638f1a439091c26c66ddd7afc9a8525c86d0490952957f5e", + "a755f2fa40c875c9592313dfad65fbe58d17940dd1fd5ffc38f9520f91b8fa63", + "6690697131f9aeea3f3a74f83c6e34475cf3c1d6c63e1555174ad875a908ece5", + "26f83c084041837eafe3647ce38404a2e24b2c3f5a870fbc57ee2f75fe863156", + "aa90255d432e68be319e26ab5b1423400d2013117121636cb27ec494a887ce49", + "72a7f09acd5ab6283df40bd92802a67c66c5308f661e98f3c12b2dd3c26817ce", + "de85e7574117c4c773bec442121ac01cdcdecd533946624f5956258644eed440", + "51e4500dcd4477c425cf9276a556041162c6d9978d56f394d32a10c963f3049e", + "7a37ec2c99e082dac260c89e7fb72c23a1f958c31e50624217391cb31b715ef4", + "1611fa2786e97a786d6f0a9eed8bf4b9a30a2158c6b9722eba150b4d53398e1e", + "0ce07e0ba171681afaa50f91d3ba9e1c1801f6c9d49d7d695d2bee3ca563dab2", + "4a9cfeba7938b7c64cd17a04e6191e9ee3b07eb39720f76794518b264817eaed", + "ba2e9a35c933057f7775202da766bd3686c4268f32d9287d8cd3e0a371002d76", + "41e92e37ed2717cac094610c3da3c64079d81b0f18adfbfbcdc46860082d05ba", + "1dd6a4446b8615ea23597f2f1fb4f01e8bb999c5cd098099694dd2d9fba09e71", + "a32552daccda15299cf311e0bb4c7ef3378f303997e5b2f3892cf3265c9aafd0", + "4a17194413577d14d26f1c1fce778081c7cedee904af0ed3a25abbde34774885", + "2d5b0ed95502bae6f4bd6738def6edf4e7e351f879b8c034646fa7421d0bf675", + "0c32676576aa6862d224cbc313ee6576fdcfb71c8041e83a789380c58413354e", + "f1c53b8e6989c4f0d55a5a08239393a9b8a5517244d8f04577300048f9e21e87", + "4c6e479e828fbd26116908338cf72758557d523d190b7cbe3c33cdb00f91f281", + "fcbab6fae7d8bacee71d82f2c1ab679c48a85ea750247f93478baafe4d8682f9", + "f79fced898dfdd08f18178825490803c4203a029faf29cbf08d8f1602f032430", + "d2f0731c4ff11f4cb6234e6f22717d0a2ae395409329cac53785269a7c26ec3a", + "535b3030171da4c461763c488d7da9e89c8cfd7c1c2dadbd360bb99edeb9b0aa", + "9f5e861a8376f92be1917f3157180c119f5b2bea66ef40bdfa77ec8f6136df7b", + "9a7f91c1a05612ddc81567d6637a6279b614dc9fba3cd0322777f437f6cf11ee", + "3e1474061b17403e96e750b3ad725b570d4ca96f98f583e0cd5dd26b84877cbe", + "c046e99c6ceb99a1ee02f92bf5099e2c44a5adebd5290dee742cc5ae9da7076f", + "0736d3d6b42c0b68cea5a9c774183db0d7ac138e6303036e46da245dda405f44", + "2677862a927266eedf5f926a1844495ed7f31f671dac9f58996ae17c637ca140", + "df622da8693db1d4245400e8bdf8a28190c1d29c27d30046f9fd1178a7554c67", + "b0adea932e628a977282825083c96a26a276dc13e623380a45a590fccd543be1", + "1c54ee5a29b8ed8013b00cdcbdd654272965ce4be0e38d7afb413e65cc8525e3", + "62b605565cc85abfa0861daebbdb0b13d58732e59bc1163d9ee5b411fc6f7af3", + "7b7226d49d687fd471e0eae0f54d9529f330d58b40a255aa871ccd36c8b048c4", + "9ea175109678095977395a2337781a4b7a0bad3984d030b6bb0223772487d27e", + "949862062b71287fb7eba6eb72efed9a06402d4372e8d4c32de15961298b9455", + "4200d4862ab89d7e11f465dbb15e722eea2ead1b6eadcf55eb76cdee2dcaaeb2", + "daeb4b3ae41eab84db5c30d4032541872bd9910f36c2f3708467b640f3d56217", + "8340ac58b1ea529e8f4f0bcf297dc402d89f412252deb830d9e6ea626ec2221b", + "b1a09ab896bccb3a2b2dd487d97392e5a3de1f2a8937cb30091a5be8320e43d6", + "772812821e2409ab4b668507b3cf42fe3bef220d4970081db69cfbb992c54a0b", + "af59ff44fe6084dcb0f148dd58429a66985c8f445d58bf7d734ee5d6cf80002c", + "308def2d531cbe674b75dcc421bc3a5ef9d1ab5c8df737243643b2692a7280a6", + "e207877fa88deb5ffae9b19ebc61d0ee6453d6c7bf911aa01263030cfa64f8a2", + "eea9b387577ce6c76eae8f5d2d4fac9de61dbfe4b03382024a752c7f90a8e366", + "ebd0fa6d638bbf5ef8948054b2c2424d490b16dcf19ba35801c25cb388a4004f", + "0f41ea73a5debb480585af132f3910f3c7b98a577fd595882121631431af470a", + "4dd11303be6ed6afbdc8a9017827c6fbf2fa8ea263117a9f08f09284559bd3a2", + "77dce608339c3e4933ebda49d99d52c3eba9e585f81bb6b94de1eeb6c4f19aff", + "e6bca493413498ac5b2ecb083cd17687e02f2fbfad5da93d4c31a02c9eb72c81", + "4b7eaef4ba7578f43422618c729ce5b1d362a11c437f4d9693da8eee9bbe6b01", + "0fffe2e60e034ee0092da0c22f560bcdce5553fc95eaf8478c3aed2d9713dcf1", + "d7dc0f79e6077711fbd11be2c49a57d984dfe43e2357de902f694f561cce6691", + "51cfd0c3aa1b96da6be92c65ef7dc25ac5f10236b515403cb7f8492791f3bbc3", + "309d74e89ffd04cc8f050dfc4be50085f197f6c23ac5f84c22f79a2729ca62b1", + "a139b028838d489dce5fa08da18ba5ea10969860dd813eb64c755f9f8a0c856e", + "2987cd385ac6ee4a84b0c2c472fea3828306b7addd11cdd28aa416ad9950a9f2", + "97e9199855dfed3d4fc6a8746ed9dee32586d73066e024e8a96bb124a187b3c6", + "d5aca4fca7529a7917cd26c7710c20eefad94ab118b592db8fa22a4c258e99ff", + "08a67a8a0f262f70e4039e10091a296fcc196e9520d5ac37f836ab109205181c", + "e71586f3b63c70b6d65672a7d84781aa67ce0d70f41e4240f56066ab67d1baeb", + "f2bfba19ad8573d2c7d48527ab2cc275f91fe08d82829908013f53dacb9478e1", + "8af272ab08b9367911a97394a580c59d6e6ffe763aadc619edef7c3b827bc927", + "8980fd4836c9ff27cfd5a999fa5a535ca557e32adce854752addef70c4144aa3", + "5d976c7296257e3a0b8411bfba3f372f8d35bd7e73ed92bc26af88e322177178", + "a01565d4915e7fb6689db3f1fb7fbddfc9e8647381c115e1c0a7b1490e8f755b", + "a8fcedb1c3ed358fc5305bdc8b618bc0f354781472eccbb8960c8ca3a0f3233c", + "094ac7a83a427bfab8eff10ecbce65c26670bacd70c941f623ff0298d354edc3", + "d7b2b22315b3ee15e124dcf4560d3bea585842f4163daf0b879a8b0c55390637", + "e6e8072a24ab7bffa9ce6e9a359c33b210e8b66172e1d35a5c8ed28871c40029", + "39c1bcabe1c30d0798d8bbc275d83a503299b08fa2935552892ce03bad2c0bc9", + "2ce057410a13bb152f276afa305eb94e0e31c5dd82a2eb828d0a01710a276756", + "86b8f99ed5240df548dcd85fd87fb2dbc78e21e117524eacebe7ed88d7340a8f", + "0ace53d368852d27daea0ec59bef8f1d5088ceda03719add6a02bc70e3aac785", + "48c7e89b19636c77e186a093b1f2946ddd692f964ba6f2d2ed136b8c2f490566", + "6d9e0dcbe46da467509a5a8a2aa709a31d47795f9f3bdb3a01812ced98dddd5c", + "1526564dfcf5b6ae528444ee7a260771bbf30aa846da37d07dafb1d743c9f68f", + "2987aef1d875722ea6f1684883eefb0ad566fd0017b7ef6094a1752e2ba68550", + "22bc33eea0b2fdfd447021a90c90d10d24652add142991676418a72e8151682c", + "0d72148b5f75fe9f6e36c5f042244bb5e35a27b62777db7ca1e1072eee9a8e48", + "ef71137c514cfa65fa5fda9b517e3c267a9c7a7a7fcd4b9177dad57d0fc2add5", + "11efa0f3f84cd47c1cb0c2d4634f317202ce00e52fa18468910a68cb317326fa", + "750711ec221178fb1ee7549333d5dfa060283226953f9096782c24cf78771345", + "9d621cbeb49423a46c56581dd621246766d6c1d2e885f38cd36ca18f88c6966e", + "e459b29d49a394e84000903ccad0edde2b83096f73e93d144cf8a4fd041d37bc", + "902e39fc44a6a9a712c9d42e0b98a08f793f9eb468ab13c120c87bf6309b68f6", + "5fd3dd562fb5d0c97a66fc5d6f2d047b7c12e9518b95d6d4b96d3cc649e893c4", + "ae5a35203af76d78f8d4c506b18fb4e1c67e5798706d7a1b98912e9fbf6669ed", + "8806e940bc2274296217b392b69987b4772bac5aa5a96980a848c1ec769bb8d5", + "bad155b7d45404ede0f07382e8d74d344dbbfe462c8b3658dc6ead106d418e9a", + "fcf3012d4e101fd7affd972b1768ea8200fddce6e0fb7e403e4fd9ba3f01bac0", + "8ce68b64c7b5675e0663e81e61c4b4379cc5f8845dbdf191031b591a0e10914e", + "ec2253aabaa2731500b94ca5b60c8226e405ea23831e87882fa127cacbed1e70", + "8333c77cd6b63b5e538dde44a5f1c37822c5e1259d7c6674ebc98d17431720af", + "abecc9a68c658a5c31008c6c594d581d36fee313f8ec2aaac469f2fdf4525569", + "e4b493bfaeeb85f35f83e84a318b3a1df7df1497ba1b03b498099e747956ef52", + "8dda479f521d6dfee106e861380824d6d06c770d1472a189bcf251a1bf0a0f55", + "e38a654a52e216ff4a200af142e4a8b9c38c448876b70b84dfc9de5211ca7024", + "12b0bf0288ad6eaad0392192143f5765bddc84d7c99c3ce7e33711083251e878", + "98055e28a5af0dbe2cdc674820e18c92a38ac89cd2b6d3e2f87a589e1f054eab", + "8e05d03fef5e1e2f69f9d4fac674d82e351e34106ad29073ae9490e9ffc15b25", + "bcbb675310f133824ce336a3c43f36f8f6bc1dc9be8efbfe3e7270fa0e7eb5af", + "20af3852b4aef5ea3fabd3847cd03c064cd822b30b5c1652b91b414d08704fb8", + "cc18a3362023966193c072630da0afe74cc08d9eb767866d85c557e6a6f26387", + "950e603915e7fcedf0dc158daff822db034c0a240f7cdc5063bbb428d1408414", + "12658fe8bebcd857c22f584c8e31d6172a87f089e1d883130b7dcb4c1600518b", + "661d8c02f86e16355681f3a682aa0efea86a985ad29e2b64b343f5315a9268b2", + "617a333e2cd2482f83715845421a1e25506724d4737f7bf8dbe9f3faa5a94aa9", + "1cf208a9abaff984c259300f9345c92683eadd83d11a6a4024ee47b3b4d1d045", + "db15ccc93275a34f2728c92076086f0f4c7b2068fab2b009adf7f0d48bdb888a", + "befb08a1da6b7c3136dca3d29c7c0c853c2d91644e55e4303cfc8be64f699538", + "ca3755a1d62e3b4a74fc458daa9a7faa3316fe7f6fb40a62282e9128882d4675", + "4fe123bbaa5d874195f63e85aea06ab08355e0e57f2204308439917ffb080a92", + "cae10b8938dd5806cfe5ea1944b39296a0a73b5957a82f308d9141558b21c6eb", + "124dfeb77b60b21d782aeb2e4998ef15ca8755a3809fc3420b1f4a7125a1f4f5", + "406e044140ce58e8cdf36d57a41892fe4b15a16871f8318afb4ba0ce43365b5a", + "59f62af4c197b3ab5b58998677a39f42ae62738fb962f775529889cd7db10925", + "7eaa2867fa01e79b9e64e17f41df246f099a3f1ec05f9feef690274f55b00182", + "3c0125bf287ec0efa250f7dfdc4f784a4b11475e7bdc5ecccbf7c6dc591d0284", + "06b9701560e87fcad9f9f9eb761b9b78bed1eb979bcbc252b9ebaaa465319a2b", + "64bf409c31578efb2717df97ff8125c218e5b2740cce771186288de418110e2c", + "91adc1e2ecabaa94988334b107b23a78af49d6cd6a8a7fa45dd0a3de7e7a5bb5", + "99ae7e7cbf973d9eee35901a95212ced0cacccdea07eac0d88b5cd29e980b956", + "563e70183d98c899d0f0fca96634f77ec89ad4bacb3f52ed2f736555149779e0", + "7f01f980bfd6c3c60f3628d61afd9bc5c92f50998b3ebb0813db0487d2c23f56", + "aaa5cfc4028fa4a0b2f1303372ab35e5c4e4a6d5816bd6b819926e8949a4bd73", + "52968b2efd496a5de2dcf26a6502e8c6c74dd1249a40723a17e84bd331dae9b6", + "9137b4da3041a7e4a3b88deffe35f371c0d8f746867c9959eca986d4a24034eb", + "1d81a4be4ab49d3baa09a94794061d770bd224d3701fe00b76680297ca819bee", + "628cc2eb26c7278894ed8ec317b4c60afe8ad5fda359f151300e1501d6b27091", + "cc9378c99c54db8ea7cfa4261e74349b30e53fea0232dfc65e595419b7311c78", + "3f3a959ed7bd378b9ea2822b166760ececc99ff65c340e9a99b550b4e233bc38", + "234aeab4fff850bdd013574828def84a96ebf53a59187fae764d691c1e16fc9b", + "b065e4e7d29530028fe92e1f48fd4944cfbcd4a4bb1aeedab43866b694f0fba5", + "5df39fea0e235a747476259f34f8b66d739f9d7bf47f50c19891222fb66ed6f3", + "67fef34becf3a89191220819e985761682583e9e2fac7fcce2181843e567b13c", + "587293d74305af4ad1bcc2d88f6ac16c47ba265e3ac5ffa5f0f6235ea8cac952", + "36a9a03e9fbc0237bd52ecf7fa23626d8ac83e91d54cc0471cc9277929582879", + "78a673152ae87540f2b3a38ba47ec5bd5cf047da62cd6be3682a723bc87ee1a9", + "8b48afe535024d84514d81fdea58934ca0f23e031565d7f74d05ec7a7d946801", + "d81ee7e407ad1311d68f0b53994e6fb68131c596735329f76cb4d18c4ddbfb77", + "16be4607f8bee1e191857372b0263ed401bfa173f2752736fbe50abb7e58da98", + "d05083dd5e6d5e0a2c320a1752c724d3a13428b9dbd2833fc67c86628f6eb1c3", + "0b2f6370271a17242d09d0292a1e0344d2806cc89e8e6e125c72ed47317171c9", + "aa1332229c78544ada666a1e7cc54fb5fa31c586370de5388bfadd86176b2250", + "d605e3d0ca62ce111f73d2ea48c5fbb56c5d47c2bc28eb68c398515703119072", + "9e0549f039e6c530dc1432adcaa40a3ee2d69329fe1a2245c6c0461c16931eaa", + "cb2f3789ae09d724f9eddb3f104e755ed2ddc3f5bac87aeb91f76fa53784e881", + "6e3b3835f989decf5441e8c524aa95b6b312d5732d614fe24d80f9dd39bad1c5", + "318440cfef57053d806d6f9c03c1331947018cf2f479b71d9f5ff4d1bf940ae9", + "51050b2569885e0a61fc2487c8428e6fdb8861727de7e8d97f7901e6f583acc7", + "02e37462d082a033c382c04ec6d90c629bd96cd0379eac6399a718d9988bf3b9", + "3515eaa553cf9f35d85fdeb367eb3eb930a79dcdefcdaf2e6eb00c550deea9f5", + "1321c47c060f371589bf9a1dc6557b9b49434a4ad6ad15f6749a2bf71e26ce14", + "627cf526b8c336e3a3d8c3e82e5503fea6338f14a438e8de6bb3dc5674a10da6", + "7cd3b2289dc1190b6ea21b9d011ec5574fa251d222af63a52bebefecbd89d7c1", + "9bb3ad4efd34c6839bf4b641d1f745d8ce60c829f22b773de0fedd566c3ca60d", + "679c477e999ae78bde65ce99acb6c2bacbcea8214ba4b7fd477ae91b1d21792d", + "bd3ec90f50cdbfa74372cfcee0f017fe7342935eb93d9367ca277dff7b02eaff", + "6a73b3ac5c74db3e940df13ea5e049432e2488b3997767dfd700e9aeb34f9cfa", + "a642e0ef2b4c17cbacb868ec0ca7d43ae9cefee5e0dc29e59e561b44aff05615", + "1a77398f0d8673a9fccd3342338f6e5fc1e96800beebe04f4aba48afdd41f6a3", + "d2bf9e00bf6050ad68e7a3ea1261b3c9f9b6394d53da2f9f1712199ff5fa96ac", + "d17c0276250825f6c8ab121c999c61cc892874488a4bc95a104dc0382e511aaa", + "7be2cb478a857b81957d7169a01c6692a254e7b029b42995b6c262e6d30f0038", + "2af54e9f62193195b0b927462e63a661c5abe69fff22037e047257dd41089646", + "120849c155437bbec39e71267d3320eae464e949c7ed1e812c91f3c5ead6ba1d", + "4d77309ed2522ec89b7e8e0f0189cfe9124babae465007ab5c905a9541d63a4b", + "4eebf5a0cca448832d9a5224a39250ecac4a18c650baafe2d8804241c274dfc1", + "5d29b8c14ac83e531b87534ef17531cfec84f49c764e6410c8d5682fd80204a7", + "625d9a10f136b103e85f0e7863df74db8049f77a75e52361748873fbdb2a3f73", + "eb21a9d33e58daa0ead2dad3a71d4e5eaa7b0321c6cc2d4e86464c70cf366634", + "6b9066c0d9049b0634edf92b34140d02a49e7a6d76483d14c32125f3ea41d8f4", + "b68f5ebb20554bddbcbd92371c4e536210a0674e61577d552b6b405939acddfc", + "2f8fc98373422a6dce98f183f25488520794b45b947c18ab0e1b924dee353b61", + "64643340086927f78905c52784692557869bcf723b2ad19b69fd0349601aa7e5", + "39c72e17874ba3a3850231ad77319c564e5fc1f23414c80d0531f8e618895ac6", + "9e2afd30de5bfa30e237d9f2f773766a82e20d1a7efa10d34e37048b172d2e91", + "c0888a46377fe1194a84313d3e9f64e90cb94648cbadc93ca1c077960b2946bf", + "c7a54836aa914f27df8424e40d4c42183c7dce32798f7964fade258768beaa4d", + "485865c169e3e60c2b0cd97fe7caf04bb5592a9a08c5abf53de92f5fa745845a", + "77038185b0024d9e2e0881b8d57e4dea0b59fcbd2bc2a776c8d7b29b5356a240", + "19be080d3f5d828b2826c221f35a07200540c47c5bd5c374175f8d1b55dcc8e9", + "32d5a77433130085014cfa02eb0b0ff36be96763b8f100502ee09a93fa4a62bd", + "9572c95caca053688f8cde1436a7d5652f1bb5bb4da92dd200df11d2a27da378", + "b3db618f9e7680be0c485377b1f9e874798b5b9cb2800a098053b025c47e70f6", + "67a77a0cdf101d5fca5d17bf2b49f3c00632bb4a53c4ef06617762f36c8c8e07", + "1fff3805be7779014868c91462838db4e8f88e7759179f144008534a1724fcda", + "efd453c249b898711874f643c4b66efd58e91ad5b3aee1aa6ad7a477b7248a94", + "bf94e5f1c7b5c8483285a8dea4345e7519581d486fcb27a8da43a2b48be22346", + "7ccb34279395a288bc3ce100245c6de77fd9fc35956f9ece35e6475bbd6f6ef8", + "810ce536aa750b60cfc6acff3ff65b36515f60c1abe5816aa5e38549b0cf21ad", + "be1c2775730ae524f44e05fc989ea33b24d262db5cb630cd13d56a2324f8fe37", + "7b8bf85e45285fa4b800e544da002c0c43c2b87644dfb62c42090631c5abd1d5", + "96e286ddeb65c5e54b4a8325cd695b53f4aff1fda635ce72c7b14ce1e926c7d4", + "12e0310edca52b08cb8a18dd2e382da62908b476f14b1a8e0aa993a945e84399", + "e8ceee44ab4241cb19d5da582abc8b9c7ec4d71bbfa456bbda2d3dfb120c1e82", + "e3951d2637df7a24c3fb48360da87247d998c9f092d7ebe349bf5b493cf89034", + "2657c928cb139ac30cb495a790e8a125d452a541267e71ca1635cb69d830c349", + "4d0fab3685c9e14d8382253670d0453430010fdb95da9a0cf5427760ff8f4bf5", + "85a65d69081dc88743067a1ae0f5abef0adf79ba23500954217f990bdd17246d", + "b3d6e83386b11a0d736d43f1eb3235388abdfaf2c90e94ba02d4c50a3c830a66", + "f1f207e702a02318635f35a70141ff96d50b1058f57c01f84fd5973112eec5bd", + "10e412a4f7c37f47b63d39cbde4721f65670bc2085ec8db8a6ebd2db0f19531c", + "a7679d3b0d86f7d19b865f9c25aa91c1bbcfcc89cac3415138393f68958699f2", + "9be5b69545813b368cb6fd8e8e2f0843a3299c268f3c78691d62f14cc08e1fd5", + "c7ef2594ead75dd56e8effb151e72c72efd2e3151c22f113a9952c2d2a094ef8", + "43406fa30813bf114ad791f19f91783ee623dbf5882e9361dbb9dd42dcc124a9", + "f45c2618d639e25d60a6ff4e3d3c54aca3afda8887feeda48b3931d5fb3a890d", + "021d36f9b63ae9509a139f5919a2c7c6daaa8b775b67d175d9fdc2caf1545247", + "221662e3384acfda6b419aca64194a7e2b515c470eb515641a43ad280f5d8c64", + "93a2280c52123349f7143d8f90b8862985eab8856ce01c1e65847f74e82f3585", + "97595d2f3e15492623c1180c3448d91e27e8caa91a2286ffdde6e0e01d73487c", + "554e51f4e422d00765369712a73dbe5e18c4f59aa55546b9aff3f1b94c8f75ee", + "373c9b4a6fa3fc747881123d342ad6fd4bfe280c1550517f329e7b5fd9c90824", + "5466a29fb42efa17c090eedb46bb213988ef7656634eff61d399c4540bf185d4", + "68882db32387229d3c096e4f075c9075d1128759c3fdf5e5d8bcdfe24072d559", + "f703f98c5b75380e73f861b3ceb685094aee6341309d5161b761fcca78acda0e", + "59f93b2d63f4643f566199a92d955bd78caaf7ec7e3078fd5264f5b478ac09b2", + "639731cfd7ab582be092da8ec8da53718c206b8f0fa5c75953fcff3cb116a172", + "2bfe2a097a074fdff140fef54c104953996a032dc162edf32dc357e09abb3fcc", + "466dd64bfa5468a071705d8b3f86206a481673ced10e0a4f172d7486cd29e8ce", + "a40e511b719b6275dedb1b46f2c5f8b0cd725312ab239132c5b9b29547f6f764", + "6583f450fd9001e5355551acb58a3c95ac663f8f58a82cc3f9f31135fe492442", + "2a818d069c34b05be52769437dba3051f931aae1b1453e950c591b3920c3b3ee", + "0b26d32e1e31400f4c3a5e2750fee52239b2dc3728f927ec455ba49fcc16bbbd", + "7d90705556fd11248be8e53c04bafeb395b123c3b42d3e0e07f02ae5bba74681", + "79ad2329c40c96cd2e4d694d51d2610b8a28971d6b253f5a54c22301afdb293d", + "147eed7b0ff213c3ba36f679ba864cb2b050de968fdedcdcb48e6916bc264574", + "d027dfcaaf6e04f676c619244515b823e7adff3dd01da6730342421168a61c45", + "e2199dbc29264ddf0edc446706d684cc19ab7c81fd19f2c25a6aedfaf38bff61", + "74219656debd84f572508135fb03c4ddb80a992c629a1483a4b32535156ee1b7", + "cc6b74e9616a1facb82a483e19039348ccbc6a387a3c6991f50133ca844bb499", + "3084b541105fe892ddfad7efe4d428a8cfb5890c6970d4c4d05bc1b1e7ea38ef", + "a0b5c69bf883147418969ffca6cd45e9f710223b8da9bd93d77ce8aec76d4bea", + "e08c7e844d6fa6cdcabb04975fb6c66ceda4cadef3b1e4fad7367207d674e973", + "6033e2342b470e18a442cc73eebfc5e77303cd4ecb3507930f777cd2fb740a15", + "3c20d132ea8e829fd8fb7b18cf5f9a5a71dd88bf35bc81f7cf37b4ab4cc7b961", + "0ee98db4b01bea147bbcb1bd492a66c85abe9e20985738bb5a035d587badff8d", + "bd78400ee797aba19c82c3bf060a13d720be4cab6358a39075e1ad721cfd0b99", + "91c0f7efc5e43eefa3e587d1e2685bbbf68a7b064c06da9b21f3e38be2f8915e", + "f0c2ff1abe61e4c0c3092afba482e7eef6f0744c267b5334e060485d01974b18", + "dd4d830bd248066832624a2a5ceca133b9f5b9fa66c1d3a41eb4cc29169f955d", + "04175c02b43883fa4b3c4b990334e2c3e23e85ddc36edcf04bee9f86e8a116af", + "2eafcbc1db5005dbf74c90a56554ba71201e21d21ef4a77094f4b1c6c6d10497", + "f6c826a659eef7eca7685ce1e574066a007906163041aaba2ac90a12f0315660", + "2dc4d5de794e832b499f493c99fa948e10079f74552f23fc5d3700722e8d0f8d", + "c02e4905c3876a3fa382c1a7f90cf91496d66da1b2495cb3f5a345993360280a", + "5a375be2718ac37a38ee1c84b84476695a384e2a2ff43068c8d7989b4fd3cea5", + "b70b2e6d667be606bd1c511f7e2cf631f533948cbb5af26db8650d4101253f63", + "35a49d20ab277ac8c092985c9faad58320d1af7427124c4828888f4bd7491b36", + "2a67df5cdd3e8c2a1241dfd24554af46feee6b138b4809663f86e9012ec47b27", + "b50c7e29386970d39258f395e05f3caeb8648d9a40a0c14a603ba1e729c1782e", + "0db1d79fc1686e0388b8439fe7331862314b77ced506dfdaccc2a21faf74f44f", + "6b3393eacda7dc18a2ce451f2541681f5dd4e9af21a2624956542b60268a9405", + "8e17ed5e690277e1156294c6904f6899de1497d15d8862a98cfe349c5296ec13", + "e44b9eb141e8ff07acd5bc5a2fdd6d334192e27b27b5f4c26a225ad28cd9ab9a", + "02b6fe45acb7af83932640c81519a7cf4d33af40e8c66510855286f1b869df7f", + "c8aff4d8670456d4e64051e65f018226f7abcb40dc77b7a36a934f4afeb42888", + "aa4a4dabb9038bb1bc8dafa93397b77b81e263fcfff59eb86c07920e6c8c218f", + "3b8203b9ed708044720313b67f9b98cf9e96544e95ada0200bc4ea0e54a3c633", + "524aced0165da7aa4aed8d02720276c3895a37fcfd8db9cffbc9d353ccc960c4", + "553fcf8e9e53d37da153a4f8bfd16a2c5379da0472076719b05a26d10100801b", + "fbb7b3e31f1d0d1606dc34bf1c624db7c62133e173203dad5cf4c3792351fbbb", + "e62eddd4a815b35bf6916c615a52f185395baceaa52cee8a836de3a4db0c5381", + "9ea285ecbb3cc9189511d2148e3d732bd98582324d2a597ccb942e8b0158548c", + "6c9ecf00a9f605bd50375d0793e8ebad382060daebbbad0ec1427d5501b95186", + "00a5392f9bf5114694596652d3f47debb758a1ef4a408490688a8b3b6f4d076d", + "c9d5bf16c017b12a95d62a92b270c9c100fee78fc85c91f830c56be00235ce7f", + "68ea43f029fa915aafc14bfa5eb8ab1ab1e6e20c467c0985dfeeb72af1859659", + "172c4fada590aedd1eb45a16ca2ac669cc0371f12aceecf1467e0dd6e2af387c", + "0e8c60669d5b505ff9f6fb941f3247bee6458fa1d6f1829c1444135c55b5b58a", + "83e174c96ba972dd05b14f9e7d8dc0923ad96cdf6b2926c81baece8c7c3ccbc0", + "55699232a0c96bd45cf5bbb81ae2e033d22b534b2fcec3b70ab661d22bc25b0f", + "9d4ba8d5cb7404ae253587e6a583c595214407007c049988a08b51e8c05a67ef", + "1d63978b718b86d8d84bd1f6e7cd053fec76dbc922c597abeeee64066d02b57a", + "6d03e10f0787dd16296266414cb6bacb961387fbec686348f586a4d053718f02", + "e4ae9203d1b61a37a98b21769f110e46de7ab6772d15b4af93a9f39cb2d096a8", + "a5de66a7fa9666e03e6dd9e114d3cbed11e14454758fb20f42586685509f315e", + "f0accba58611444a9eb7c3d41218740ffed85556762d5245f4d1bbd6d958d488", + "95de4a732eaee2996786c28c3ae80a7bbfd34ad276694cdd64194e7d45d6b1b8", + "59b03bd978a4a5ed212fd094e90048aca406d16c196410eb8be8122f2856d1d0", + "2b6c3018cfd7cc55746864434c9b2c610a4971e60d9128a338451d46c78bfc86", + "2428da7b24364be8aa93c6a255543d7da27f4cbe57dde5f14a8512fa5ffb981d", + "bce26d655a0efb6f287be49da07393f30feee6bb6c2306bbad88e04c8984e7d0", + "0fe2ad6dbadcd9c423b5eebde12b78365ff1c4fbc85e933206523afc2a2f8a48", + "67482decfd58bdc49de9ec5a8147945130a928b98202457647c66865ca336c71", + "7d22130b398f4085766f4909a68b4f7fa2c4b26db1e107f3a2ef29417ee5cfae", + "98778612b5124e3d7d455f725f4f2462ef780f0323e0c36cb9bc81811c00a045", + "51e7511c0f2ef6bfc7e17c7906fd604e42e862f3f4475d50d86bfc1401099d0d", + "f120fe5e0ef2b223977b4519069cac7e4e77c587c5b011663d06aa08f80fd477", + "9d00ace943765ba7f2a3a414f534d8e751af83fed0f8e872c36b311a15ae21bf", + "93cc266fd13aaa341f03d20115f066cb126497bc0d6d5de719c9f80604a2e57c", + "ddddd5d3636ab27be531f09a9a93e5d8b39fb2c03a79d4b7908f2b40b52bd1f2", + "626c9f4c4e3bad8d65523b0f4b820f4e59a720007fc35aa925bc99d9deae3e00", + "af9d9060c20e16bb1f94b3da86b8f4c074c472f2d402ebcadb879d9d5853dc78", + "309db5e72eed85d70fb56caffd8bfc1dffb79fb9b599916468a68982c975dca9", + "0738d677af3228b95cf444baba3469dd67aaeba54a93c91ae62845d2ab12bb3b", + "720666a5e21383c6546a87b8cc93be7044c87c015ee2995f87b55bb291e9924b", + "21d91ecc9946962e60ae1119f45687a670eb725a392425e3dca05d108808a314", + "253c1ba525d54f4075c8aa01ee5383d0ea6a41b62d243fb06d4f90987e557578", + "73429a7a5981f25624a1ea521c95c1aa4e744bca69c6b63f495e3c72571d403f", + "e92100e5825a11d06c55a544f83e71a1cb86516805549d0d16c085a690645682", + "3969b8d71699943cd01e410fb2b072638ffdc59d10eb3316ee12a98783c09181", + "3dd6b9d720af724e3bba14e7fc41213a0c6b7fd5c4a6d6081cdde8b05941023e", + "a8c6b92481b9b52f50fc996f3fc023f720b832f79f306aebe9e1db5d5d680f33", + "84bde27cda62db77fac212cc9a399467cf466e0d5af32984b04f389c28e6b0b6", + "d416c534ceaa07ce856d3fe9fb6d8cae21f28aa45eadd4fee27b791e69c8f045", + "0b370fe3369b09d22ddd43f870a428c4b65974430a18261c59781ae77d941480", + "906ad75351144066d6d0d914c5145664825d2004d778aeb41aec56644f81ba25", + "3c57221b786a6de68d8d8db9db8ed3928a20eff9e688ad1121d73085a0fe9d75", + "e1c262992728e3ede684e77f4a06efde3e2eebf8f12a0406dcede1aafe299648", + "c60819a050b2d4e6ac4d72ea6683fde04dbaf6b66bd74ff988fbc7d2b29b9bf2", + "e8468896b3f9734cd0f7c721ee0896ad1548902340242b7fdd16e39b361c4ac5", + "6a8f24b12c66adc9e771cc6b52392688bb8535c1ba026ca7f79d26ce2c7ed9a2", + "f5d607b7d774d7c5d81d6c8d58b4a7ed576403a4dfce8f2e63e1b7ce9c7f8a02", + "011d439d089fcb3c7d44c2d5a6420cf60cf1b8716db31286f7c9ce390a0f2fa6", + "d6fe9c609aefadabb0efff44e5375ea4eacc5237c21b58a2e199cc2899dbfbb0", + "3a0b54bd93d3305354b07b6bb7807bace50476c4e120a0b327de1cdb93617079", + "41e55404a5998f71571a200c032127a6241b2e7b232e82147fb6769d8256bd29", + "5756ac43c1df14927d3af8ef161c403ee1f92b1ed296d0adc72e2cb45d98d7de", + "f82cf2b7895ba7802d0478d2b20b069d3c8500637fe98d4194da02c884cf9d17", + "3a10b6cc7cc3bb244fbd0d49910e588abe7dbaf759d4cf44783a7139af064824", + "5a9b9b31eaedbb651f42d207a14b502b4b73885330c37f8e615ad89e3e61213b", + "e61688578b28b999d906798525736d64a5aac1b5544b7507badb20b34b556d8c", + "45b27a6ef06340c3ed81433083ac81ce99abcc951f5e334919f3ff56cd1c75d6", + "fceb7a9bd727f1e8438f617d02674039902f195dcbbfb01b43d5f72edea02831", + "d19c5e3b8aeb8f8b4c08ee8b61f34277bfe579398a76e28e58b972cd69089b61", + "a32c7ab77af334e2d5c4eb3caf95d3aaad689f28a2dc3a7557f2528be402407e", + "b6c17c21cecca5ac92a1fdc1032369e4c6d9cf2fd4f1d5b4406e1d7e7606b2ac", + "be1b81276adee2c0f4df4cdc6d1b82df87dceaf516b7e4d994e023a937479f9f", + "439251ab04c231c48b891702a2061921fa88cfef44741ee173853b6bf8490430", + "26b7068c7272d9feaab03fef3083e50d49885e375f77792d59ff0cd502c03df1", + "85dce2eba1a9ae5a41d06854f4db88dd7fe022727be35ad6b18e6087c8b91f1b", + "e4f7178d36429096577518bf47e2d296c021ae7349aca78e2c0a4c606acca5a8", + "e8d88f547187b278275c799b25a94d814f3174bac46077df9820a338b4d9c267", + "ac8f48385b06e1c43da7fb74e522d347603b7b973ac3c3d06412ad72f5762fd6", + "22f39a936e8e401a404457cbbef921c3568ce016a5c75de9256011b988c252c6", + "77fa7038e7ded01577a2ae592cdc9663311b5e1e42f5f87ad8a66ae0e9a54c28", + "87f6e5ae2adc65a736c9b1746ec1f18340bbee45481d7c1f5509ad71d5f3c9c2", + "180e9ee8a321a1e91c490cd90afd74d556ae2a9bae223ec6c094587f359870b0", + "f6c87366cdd0be058ebf9295d8b6b42729be58a0f7f057fe3243f9b2949ada26", + "d45c27211e8a4b77a9e76c12db25e6e9ba9d03647f3691d158edbd8b7c38ab69", + "8ebac15b55c4a2c5d3f4866858fe3ab9324bd785036dc0ce8ababa396419eb35", + "6a80d86e69694ebeec5f6f5a5d37d12c2f813147c1116d5d26538d25f8882729", + "2cf7e4c33c2a63ef54c444020bac3648738bee48779db4a3a0f9e72ca5b4e380", + "9d101afcd85b7331d4008230f29b761b9f798e8814d123140e8b648b230261f9", + "c6ee0ade266785f9dadcf6892bf4b98d7bbc292aaf2bfe5b7dd484785760375b", + "94d239b78781096e3e8e7e6a7d9ad70d0804adeb6a6675fc5da4f05d1152c19d", + "55ca49e0e313f14ae0f214ac0a51843158ee07aa5f381cb9d596b8612dbd3f36", + "e3e3672131fe9a71f54a41b0e8143c63bc0aeb823288527bda069a1616a41529", + "ad1b386b668b79c7cada251a6c2bd19c53e51045e4a4562b4b9212852d334556", + "bd08a3ba2d8603332b31b2394a556edf6ef949ec8d49e5d54a13318213a0da65", + "b34731e855ce7fc834aa0846c72ce7f455f40564b11181b340ec07a003787e88", + "ae292c8d03de63af642539e6559960770d80dc4e9590c46f54ab8a74b68f145c", + "10a28c04e90016bc744986e50d2a240ff553cea32c8ccbfc218445d6f9e32271", + "182df763c8b4dc2c1698567546dac90ed5de00c033322c9f731a64f08cf7ae61", + "eaad2e6f6dbc000fe3b8f60fc7ce7011c22c4d062ae2c33a3621098190027381", + "1e7d630f59f8c968c54a803f8c70e15d85a1214be4e0f8e82cab9b2056df4ef6", + "af485e0990350c9814bf8a8b3db91f44d4d459f513f9e0635ff54a444a45055e", + "c5339ed11420ef44af6d7eec085a7495ac1f59264119184d3183a937d7012295", + "ee4c98e7b5e1b5fc19c8e5e75b9be39690ebf15bc9b1e5fbb3ecb75a2ec89d3a", + "c88b24689438c8f097cec81f2178fd162d4c53ba51f1b835c9b7d1f33f3bfed4", + "b3f12ee06f773536bc4882441d7b40153fd02ff4abd97a5686bc8ec294de5303", + "74b69c91d7b24cbc734d5bbfccf3442b10aa94f835dd49e84aeb9fb93d48f984", + "7249af9eedd2ff198a93c9fd96d3847d05fd24e4a2dee3213f8b0157028a7a1e", + "4c663e23616d31135cf9b4c33a89556d9973b2df0fe8b5a851163ce3e770df33", + "4afede394dff0a2b6f0b1b3dedf82614ca5133814c9466e7856a53cf33a83359", + "b7f3d596f87080e6fe4b0c5e17364dc87ba269ca561689d20085c9890270145a", + "a91c666326b9198e9954c4461da9e173c21042edbc8e63e6527f69aa06144ad5", + "c11375b5ba44d9be64cf8ccc8eaba4689ce8d0a1d75f4b932d14b4e26b962ba8", + "c737cddd051af963d4109063a7c98bfbbe500abdf2761545fb2e3f592f1c3183", + "9b09f03742db7491a7a96f20d2735d669383b59570c919c5f8f4d804ccf552af", + "112ba278ae78125b0ee2a9ae9d4c857d7b09d3636d8afd9e175a3557917409f2", + "3ec2366bdb151bc04838ad4459790d217d7e70104dbb6a13de48c8e97873c874", + "e592d021807bc768bc2eb1b6b7832d7326565c39b82658ce9c340ee50a443626", + "446de54c9e312644e2fd20f49b8d9926edeb2c995e627414da25b664f14e4fb2", + "c0e6c970fb25f5ec11f0c76316e7eff5bb180acd21bc777616ad3212edf5cb3c", + "32c3a1e954262361dfea8dcd6d4e74c436d53e6518a2bfd080302187e454ef3d", + "af6c5f72dbd65e75a91a40ba9c6abad575f12b7fd8b66c41d88948fa8e026f3d", + "2f858194e1da082ac8ba94373768de9fa1842a1adba13b2bf6c67474d0a233c8", + "74b19f6456e34582f963dba6d7faebb6289050227f55d5bead6aed9f6394eb41", + "e249aa1cc573b722bfb4e6613dfc70e2a5d6cb7df94ba1002e7f549f57aedcef", + "0ad80c6bbea362b8bdb966cbc50135bfa321bb8cbb0179f76f445dde3d156c8b", + "898f699a5c52bdada6a62d24ac05ec8cb043f5679b554ae5a9b4d666a440fb76", + "305cf536a22fcbdee3353cdf59ddbd42e24ea1a8b91967d5038ca3f9094868c5", + "2847f3395f902b011e2eb0ce5e995cd97af861c925e04021a5bd893447025060", + "91f09953dda135db571d5e159e07a495b0ca9d2eb6ba584b6bff019ae5693624", + "648126d6266a94fd567040da2a9becb5e6be4055f9f4176d252ea8b29915b8a1", + "46451db7b8576c094fa561a81e6d1300220b42b886e3e3b5faeb95c78090e6e0", + "97817465e9219742eaf8121d0d053224793083991313a7b376093f5cdf7329b2", + "4e5c52de7f2d110910f12126b36f9ee712f4a70ec48a86ef10b21767b876396f", + "6510078a1522e2f137600f9183802c1581f882bd9fa0f8f1391484f84cf0fb43", + "2b687ea5a740865b89abc5657546a55b7d5f7fe4e4796354b4b4266f0c0ec10c", + "879d6960613b3138e2a92c821026f5858a99338db1f769923ccb7f32ec4afcd6", + "6c37cd022e745b6666c0388b81296be90dce442972de52400fa3adb8e734d2ee", + "e58f22310f59fdddb90479cc36c763c1fcf60d6e09501d51c495dcb95c033d05", + "70ac02944106daddc97b53db0a0f9bdc1cdf7e789b698b4234a88d8acb945233", + "e797d50c461ed06e255f8106694f1e3c3dff45ca342b631c9d93fd6868ee7862", + "d645d1ce9d365a5ea79ccaa9b24ec597a029e07e667bdd5461a7cec83344f83a", + "2fdc7e98eaa0910fd04914e2ac4c1216e2a4d37621e23c69daa7539b6d403ad6", + "3ea00f497980193dac3261c3e12d7593b1668465227775c53b01e0029a3dbaee", + "eff07e9c0aa7f889f74bd0daabfa20017aeac105fb09d11647059f37eb1027eb", + "69d3cce33ba2f60e895201dd8d0a92db55e4db99b04de54976d3f11ca280dfe4", + "64aef37268e77bcbbda83cb568ba365aa650c4363e22f08ebd9ea8dd4c5ec3ba", + "837f82d3bed0341335bd5ab9d633bb5b1117a0b71664315643f710321451b08d", + "52b1677f38980e1ba9eee33faf8a60f50d76a9020fc18aadad44ef12d102c630", + "c5727a2a22170ed9a40b4e003e7039bb6f51ef719faa103159869f715cd79d64", + "2bd8376008ef3339ad49383a12754f63710ab01657cf717aa945f8ebeb351d26", + "617d5e3734a176c6aaa970e78f412532263398023f922604d69d73a34d7c9600", + "b508b6f323862d1393522ce67c9f267f8f7a08c4f17ec2d7e9230df6a7dcc83d", + "003b1c8857c2366b415d5b1eb23ab8c0705065bd87fe08e7c25114098bc2d020", + "9c18e71750ab5a52520fa5c6f4b48047d34af42b9feedfcfa96a9b9cf6c2231d", + "c3d69220994caf490d6d8f4c7f7689da6e533bdbf775309af7b13b25359c15db", + "64945a074fe5d593413f740fbb0fb036efc1585073a5c769e6b7bba5083bff67", + "da64240fb5aefe994b80dcbf86c6775e1d8b9827c90ea914dc89ed96994c1b06", + "f3dcceebebff82171d500aa5bfa42d923af47fd9d935ff3b24e4246c2c240bb0", + "5e9c33d78cdd3d5f71a74981aee540c199f9928c132f047ff975f9c4528929ed", + "e83e08ca018747334d5bef54eff1527d03fc478a9a2ce3bc1d08eb74b1e05c5a", + "2b4404ceb208a4cbcca4eca597cc25e50c156d1d4320b937795c201bf62e8e27", + "6cf6e54742a3e3a2469296b51863123d6939a3e4f18973bed3350f361a0a7fa1", + "173648ce3c3faebf66b1b400e2ba1d09a6f7a0e90c4f6eb259cf141cf2702868", + "1602f84cd08034833d4d6b6f0a63017840b0517fcd716dc5bb82071f81cacf22", + "5c9a5a89db7ce103784cd0a185ef589967930fa6e92aa90fb8c02031694a9404", + "a99a83be39be7d0d247f3805d7c78f47bf16a3babf9d63cb68e28c0f630e15c2", + "9a92a01899a649676ac792b2a2be8db3ab24f1f464a1af6b3c03b7f67e572510", + "e0448af4bfdabe13a044362ab133a5560a77e8e6605df7d9b36f22a9ccdd806f", + "8f675662f98a9961e9a67114a98f442359b8797f729c94c5a041ebdb937e0b57", + "6dd8437315fb9be06fcbb22ebc82103648da5b142622857175dd75a0f78c7a8f", + "6c4a96d440dbfc0f26f1bdf1e1c0ac273b573c36f14e55342fd98435e806b676", + "700725449099dc98933ea1c8f1b9fdaf21ccb7f7439451cbdcf763e7b633e52d", + "22c986a29a24c02703a8be6fc3a17f00975fe6f323366da8284a4c1070b18160", + "037036f85de49e6136d1f54c2b1f754d61c262bb88c806b72a156e272d60c535", + "8813017f3878a97b15c272982b95b3534a4475dbc3a6f40aaee3eb9dd593eb2a", + "403a7a97d3c47c57e89982086de458cc1c62d01449945e9adeb160bdb487e7c8", + "e1ae2a3ca0a102d9d7b80462c5c75ce1fd49a341a3ae60a497a98c9e84dc4d2c", + "7d3cf5fdda0516c200a2a5d622c256d481f9764d6a72799995e01b31c97f4f26", + "a080f0e1bf33dc8190c844c3b13c56c058ea08af44e1c7363d55e8fd8a7a3afc", + "a611f43917fd4c23e5bb65ba8dcbde6db510142346cad2b9ab596fa75635d9e3", + "ba068f56418356e2c0ee98119cc3030fa7a9b8c27d5530c93f134e899afedc02", + "81a7d27fa4e8d911410a733248f102e5a9eff4d8f5617114e97c1267cbf3d348", + "ea711ec0f66cec18c17b3bfa39967b00480a0df0575412c0e8a7aa8869ab93ac", + "dc2efe412c0c7cf5d7e838641bdbe2d59877ca74357536da58aaa0fd1b711ac0", + "93e321c3371d69fade1451f2f537579c54934aba5271a018540f70d4ac91a78e", + "8f1368dfc99618f3e1b2302ebee88319371106bef78471b0eb876f2f06a3a818", + "5fffc415587f815bf9645acdd481294fc4d83ac5b9a37275e8725921eaeed7f7", + "3f29e067de0e036efd2684b9c54e0e6b2787fb9d83504c1201c4d0a017a0785f", + "07f4a000178a78a4a8faa370d5aea0aa5628ff669522fbf6ee0d016ce016f8dc", + "773ac8e936b1618243407a82d1ec9a6feaa5ebd9cbd82619d19b5bd7f0297536", + "af56203cef3a9126c3442d964fe09bfeba50d9243fd9bb87ec38a961c59b5960", + "fbbdfc94f38b514470de4290bba2ebdff7aac5523d97a0abbb20859ebc82065a", + "471ab6bea6f7cae837a9fc601f70c7c8e53e84a544e43008d945875d61df04ad", + "e20fa6fa46cd4d80d54d393c86e49b282ea1526c5f339e579bad5b39304498b8", + "cac629d37126684710d0bb74fb52d529f2d16c73734f2005de6de9a504ad84ff", + "a953032ffba659a809d5f733daa9db681082aa65dd08c0b05664dbefd87707ba", + "5bab2244233e9f9f2d7191dfbe1ac968652ea1bc0fac48a5557b01c40d260f4a", + "9d09dc8281294423fd516835ab30f15416cfd011e74dff4bf5e05f87416f0cf6", + "893776d2cccda1f86b0e405d13a75a11a96528a45e0872e4ffc0b87513594100", + "758a1e6461aeb3ffb55c231caacce8f663d83eef4d12f8cf94d69b09252b6e06", + "0dee497dd3a7bbc26c1b2b2f807e55bc62765b3557cd50960e8b913a2e3acdba", + "35a08cc32bdce97e91ba274bde6d6f19c74ae3fd3566cf1c40002a0775872505", + "689104a13715e4e881a4640fd5e4bcd46687c67f53c91db4b8cae8d21cf90722", + "e48d19e8a8a211358e7e7ffe964e981d3a1d11ae76a2b44a44faa856566abc5b", + "28d7c2498774be7540d3b85cd67e873db198267ebd59cefb1164969d56544940", + "8440cbb16acf418689ac4fe6d2b65ddd949e96458542539d30bfe2125e3a88ca", + "6f837c7e4a9f4c4c1b17fb202a5bc7479add8cc0d26564766328f77455627961", + "fb786f9dc125141f04857fd1393abe9172ad9c3aade782bd4d8dc23018c662bb", + "be101e3278740014287a572b2b27fe3e20712bf589db356e800fad1e633d34b6", + "02770ff50283c20a9a9de291de391fe7ebb4744da607a7780b10a20c49a753c2", + "84801d626b5998746e0865aec4fcd1378bb376edcc763a80ee96373b01cbf72c", + "1da1dd3aa77a4e2d8aa0857ccd857d4952e58ab49c26a1f0cb60275030f6607e", + "04fefbda5ec9f182c2a22ad6bbbc42ffa322338e3562976b8d32b3406c2e38c9", + "e8ccdcb248078a482359f4f7973d3b2069794b9777c4e2ac21558fbffee59c8c", + "51507a05fbdfcc522a1f29099cc06458591bcd5809d2b210901e275dc1daed7d", + "05e6b311d3a3241022b0152924627706edf857161004dc785983c5c8e4059b19", + "986ccd7ef43c8085b738a0e2eb84b98aa920fd19f372c34c37569a5c7354d6df", + "37c4370297131fcef6101bb43e37ada63ee4c63eadd53c683cdffda2c22b6a5f", + "c8a736669a5a1c69c6088735306b2aa6da1fa685584446b27c53612344a28d39", + "0c829d4a74a40e4338a406a268de64a9ac1b58ad2f0655cf1412cfe10a81f128", + "ea8aeeeb0fcfa66319b5d1560c1c7e805cb445b678f60735a5450233a08f5146", + "0c03e2902e4374dc2ae6e9d46b27028af86c461ec65b9708829f6e341c5dfa86", + "427623b7f21f13216b47d64b3cce69d8695a12a8b76ceda909038e8ed1ad7ad6", + "309bd0bf85e4b9bb130a17df032a6d057d2f4774abca3a8af6fdbf25de9182d9", + "7f3da6402da41b66b0e5e957a40a787c53a3a31228f06d45f6ddfc275c06e815", + "9453352b0b6541e28b5c36d1ef833b7f49dad472ac7bc01712f04a1d0bd8cb3d", + "35b6a024cb364ee9e7ac55b3ee07e137b7dc7756c6e7131d02d603613f876578", + "6ce8b20bf01933590374ff85f4a2fcab43cde2bbf9bf7b56c7e4c21acce02af1", + "f02acb1e0f786b78b042e5d5e4d25fe711ecc05b2cbdc37077a4a1b66d925808", + "b5697acfcc9d3d4b58bf01ee959bb519b165d6e2b3ff799dcd8581f2e4ecc4b4", + "e5dcd86553008584de8d4e67edb1049198922900801bf51da55efcb7de45c37b", + "e0c3e5997bd114db9c09728c21b8b35748eee68f8eb305b5cc099be3c265d327", + "6c1e0ef8496d1c39fcde030239772ddf1182807dde25d54fea68085434a08b02", + "4fbd6bcb31a607f792cca7f514d8259a05f684092be8ac91b242bc90169a7042", + "9dbd18a509716c53037d8713d0353094d55b48253cb0d38cd7bc08959d7ff014", + "a444e2b82333cbd5dfe250e393e245d5db8b78f91bbb6a572761a0a43ed56b38", + "b163cba8c2d0441ac59033f7742733cb77812bd74de1147ae8173faae4f9269b", + "0ac9c6fc35728d1317490b51b36277e825a49ba95be2a6783ef7af99e49228f7", + "abe44cdbc6aea6876b8f009d6279479dca2c4f17c650c51bd7de410bbf23cd24", + "4b9370d4ae2fe08db33f468ffceca5d9fe6b0273ef2887a4ea8522293870e0a1", + "98b3b29996faf548b808efa1fd4ca779882bb75cc2ab3304f049b429a3638a97", + "dfb57e8a34f4911773133529e0e222f6a5cf691b25a65c288f8d6baa68b5f406", + "d1e35d54b3df21193102e9dcbb194a294323522ae35c68597b4b08f53cbf36ec", + "53e8bb244f3c2c90fcc8ceb1662e03aab3e1a781fa1697f7383e0bdc59831195", + "255874e4ced532485777b878f2d779c62853bd8d4d05ddadfa6163465703dc8c", + "6b61c176da93e56226cb6fb7a4f8acf1df87d02be81f823dc851680ae64f80b6", + "f035af0a109e467fd4ecd7c45bed043dd1ba0d02164c2b2aa0ab1989d8eaeff7", + "81b239e1213841934e989034959c9ca0448ad64ac3ab919751c2289600a20723", + "251b4f7823c8449e2e154cbab4aa72fb2cc55495da5fdc2d31075aa5647ba4cd", + "0769a11a881bfd2f02a087803235d344ad2f005010e40ecd7af5db96245dd719", + "f4963f675eefacf4cdd14ff7b759d5752aecee4cd704744e58b9bbbe078e0faa", + "28916a97e55790f8e0a95c1203c1f8f1eac99b866aac5800445ef2d888b4be87", + "575a46ecc4c7757656b9371e9b8dc058f99f9d9ad20a3e6de2d4e6cbc193711c", + "935994c1df9323d2bdcbab8762904e48edf6298904f3ae7e234599366bfd86ef", + "dec5bac62e0ef64db4936f818e325e12aed29ee0933277259185da1298dd0a28", + "3ba063cbea967fc0b7bb9a25c5a12ee801eaa92f5570d1d1a977a229834fd0a0", + "3327cc41ebebc9e82f06f836d5868f9c5023b05498bd73d836f04179d24fc7b9", + "8d70f449bda2223d0d647d7543bc3a2c29fedffaef789dc5bd73d25a6f0afd38", + "9902826a10b226265f357562bb20774358dbc636e790257a57af7f791b600acf", + "c5821a347232865496580de7f3043822ca56eea4f8266c51e3be4a16016cdeed", + "2817d6dee3e6706b92989f2dcef7cd0fef5d2263ed3d1c5dab8ba99550b1e5d5", + "b918f2ce206701dfe60eee1d40fad119540e928b8da813fe29d77a2e26c2738b", + "7f836479ecd2c4aec31c96935e2ce932135249c6077184b837f724fbdd425408", + "1381c39fb5a68c134b948aacf72b5127a7b0d5a721ed2091da25a3856b9a9af8", + "80e6ab5756de5eba8551f62d8943e5f650f4ae8864ea6bdffc3257ef1072679a", + "ae7dd5fcf4688844f5ef72471cc0273e0e2f8f43ba4fc3dd41208f70b880e5d4", + "e0f1d8c1a745ede1198fefbd0555bc118f7ccbd9ee783d8ed7a148f54fb1903f", + "33c8a75fad49fd3b3bb1e031fb283858f79b05a8b461eb1f8f0e9d41c53474bc", + "3864e97cd6f47c2eaf3fdcbb105e014accb4eb902be8a43dfca77dfe0d92761e", + "e524e7c40053d3d1da35ac8a584ad7034cd30de0e9513bf7731f8e5f51f7178e", + "12e121ad8a9be231659d08746b1a8b2b9247304dddc69cec074711970986cc6d", + "149a0d197a6c2dba3044e43f5fac90f6a797e0d4c8d91e241725e963083bb472", + "6a062ba02e79d2b2c001e1689daa1b3ade7c917ef26b30987798123818b202bd", + "99e47c124d128f741f30e7c5bc7575f11a005befaa26ab60b560fe4a65717aa0", + "147961e3d28573540582cb4a6e266f3a677d9e5b24da19c29fa611d3f8fba8a9", + "59261c94c9000fe26b3b050c4cf7ab06343d4ffcbe3934da65f12d8a7b0c6862", + "7428bc174ef7fd68911d46b35da30bbf8a292bb6b9ea99489b0c28bb55876e54", + "6fbff71f7187e67e5cc11f85ecc7d063284c6829ffc2b647d9439cf6d80c71b3", + "d49db17aa1e54596333d1962db0050b54a96abc6184f28b46778ac3eb9bc2290", + "082436ca15376750587883056c2b5fceafd232f6fd91d839dcf7f138583565d0", + "2d6c7614ddf121cedd9db989f73da61c3e5d5d7a876139d2d441d8dc28bef0ab", + "10b5eb9db4b22b9047b2c9a2b40abefc9786d8f0b33514bcef81c462f241ca60" + ], + "key_material": { + "scan_priv_key": "000000000000000000000000000000000000000000000000000000000000dead", + "spend_priv_key": "000000000000000000000000000000000000000000000000000000000000beef" + }, + "labels": [ + 0 + ] + }, + "expected": { + "addresses": [ + "sp1qqtnvg7hxjck9ag24naytgd7pjwsmevwh95ydwht58w3uh7uw0ta7kq6xcdal4dazgg2txp472hdz4svms98328lqdflmpxppmc6ykamcrv9s7w9g", + "sp1qqtnvg7hxjck9ag24naytgd7pjwsmevwh95ydwht58w3uh7uw0ta7kqmk4a3pm24z0yepw6aw27pku56md0hq5ace0l3p3jstkqaajwrsjqc5dde5" + ], + "n_outputs": 2323, + "tweak": "022c333a8938ca0c7433c5e39d440b469f58374d0363b5bffc66aca1cc10d7b609", + "shared_secret": "03abe0979dbbe4e1bb2cd2e06524a411c0b829c1d2282052da817b46f1d6e598e4", + "input_pub_key_sum": "02bca87f72e604e8850064552bedf380ca4584227057efe12a6cc238470658aaa3" + } + } + ] + } +] diff --git a/testing/devtest/verify_sp_outputs.py b/testing/devtest/verify_sp_outputs.py index 35e09397..0e9ac8bf 100644 --- a/testing/devtest/verify_sp_outputs.py +++ b/testing/devtest/verify_sp_outputs.py @@ -19,7 +19,11 @@ from silentpayments import ( _combine_pubkeys, _compute_ecdh_share, + _compute_input_hash, _compute_silent_payment_output_script, + _compute_silent_payment_spending_privkey, + _negate_if_odd_y, + _sum_privkeys, SilentPaymentsMixin, ) @@ -200,9 +204,45 @@ def _build_mock_psbt(params): parts.append(b2a_hex(txid).decode() + ":" + b2a_hex(vout).decode()) RV.write(",".join(parts)) +elif mode == "sum_privkeys": + # Sum a list of normalized privkeys; return as 64-char hex + privkeys = [a2b_hex(h) for h in params["privkeys"]] + result = _sum_privkeys(privkeys) + RV.write(b2a_hex(result).decode()) + elif mode == "pubkey_from_input": # 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 "") + +elif mode == "compute_input_hash": + outpoints = [(a2b_hex(t), a2b_hex(v)) for t, v in params["outpoints"]] + A_sum = a2b_hex(params["A_sum"]) + result = _compute_input_hash(outpoints, A_sum) + RV.write(b2a_hex(result).decode()) + +elif mode == "compute_shared_secret": + ecdh_share = a2b_hex(params["ecdh_share"]) + input_hash = a2b_hex(params["input_hash"]) + result = ngu.secp256k1.ec_pubkey_tweak_mul(ecdh_share, input_hash) + RV.write(b2a_hex(result).decode()) + +elif mode == "pubkey_from_privkey": + # Compute pubkey = privkey * G; return 33-byte compressed hex + privkey = a2b_hex(params["privkey"]) + G = ngu.secp256k1.generator() + pubkey = ngu.secp256k1.ec_pubkey_tweak_mul(G, privkey) + RV.write(b2a_hex(pubkey).decode()) + +elif mode == "compute_spending_privkey": + b_spend = a2b_hex(params["b_spend"]) + tweak = a2b_hex(params["tweak"]) + result = _compute_silent_payment_spending_privkey(b_spend, tweak) + RV.write(b2a_hex(result).decode()) + +elif mode == "negate_if_odd_y": + privkey = a2b_hex(params["privkey"]) + result = _negate_if_odd_y(privkey) + RV.write(b2a_hex(result).decode()) diff --git a/testing/test_bip352_vectors.py b/testing/test_bip352_vectors.py new file mode 100644 index 00000000..ac429067 --- /dev/null +++ b/testing/test_bip352_vectors.py @@ -0,0 +1,349 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# test_bip352_vectors.py - BIP-352 send/receive test vector verification +# +# Verifies that crypto primitives in silentpayments.py match the BIP-352 +# reference test vectors. Pubkey extraction from raw scriptSig/witness is +# done CPython-side; all EC math is delegated to the simulator +# +import hashlib +import json +import struct +import pytest +from sp_helpers import ( + _sim_compute_ecdh_share, + _sim_compute_input_hash, + _sim_compute_shared_secret, + _sim_compute_spending_privkey, + _sim_compute_output_script, + _sim_pubkey_from_privkey, + _sim_combine_pubkeys, + _sim_negate_if_odd_y, + _sim_sum_privkeys, +) + +NUMS_H = bytes.fromhex( + "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" +) + + +def load_bip352_vectors(): + with open("bip352_test_vectors.json") as f: + return json.load(f) + + +# --------------------------------------------------------------------------- +# CPython-side helpers +# --------------------------------------------------------------------------- + + +def _hash160(data): + return hashlib.new("ripemd160", hashlib.sha256(data).digest()).digest() + + +def _parse_witness_stack(txinwitness_hex): + """Parse serialized witness into a list of stack items (bytes).""" + if not txinwitness_hex: + return [] + raw = bytes.fromhex(txinwitness_hex) + pos = 0 + n_items = raw[pos] + pos += 1 + items = [] + for _ in range(n_items): + # CompactSize length + length = raw[pos] + pos += 1 + if length == 0xFD: + length = int.from_bytes(raw[pos : pos + 2], "little") + pos += 2 + elif length == 0xFE: + length = int.from_bytes(raw[pos : pos + 4], "little") + pos += 4 + items.append(raw[pos : pos + length]) + pos += length + return items + + +def _is_p2pkh(spk): + return ( + len(spk) == 25 + and spk[0] == 0x76 + and spk[1] == 0xA9 + and spk[-2] == 0x88 + and spk[-1] == 0xAC + ) + + +def _is_p2wpkh(spk): + return len(spk) == 22 and spk[0] == 0x00 and spk[1] == 0x14 + + +def _is_p2tr(spk): + return len(spk) == 34 and spk[0] == 0x51 and spk[1] == 0x20 + + +def _is_p2sh(spk): + return len(spk) == 23 and spk[0] == 0xA9 and spk[1] == 0x14 and spk[-1] == 0x87 + + +def _extract_pubkey_from_vin(vin): + """Extract compressed pubkey from a BIP-352 test vector vin entry. + + Returns (pubkey_bytes, is_taproot) or (None, None) if ineligible. + Follows BIP-352 reference get_pubkey_from_input() logic. + """ + spk = bytes.fromhex(vin["prevout"]["scriptPubKey"]["hex"]) + script_sig = bytes.fromhex(vin.get("scriptSig", "")) + witness = _parse_witness_stack(vin.get("txinwitness", "")) + + if _is_p2pkh(spk): + spk_hash = spk[3:23] + for i in range(len(script_sig), 32, -1): + candidate = script_sig[i - 33 : i] + if candidate[0] in (0x02, 0x03) and _hash160(candidate) == spk_hash: + return candidate, False + return None, None + + if _is_p2sh(spk): + redeem_script = script_sig[1:] + if _is_p2wpkh(redeem_script) and witness: + pk = witness[-1] + if len(pk) == 33 and pk[0] in (0x02, 0x03): + return pk, False + return None, None + + if _is_p2wpkh(spk): + if witness: + pk = witness[-1] + if len(pk) == 33 and pk[0] in (0x02, 0x03): + return pk, False + return None, None + + if _is_p2tr(spk): + stack = list(witness) + if stack: + if len(stack) > 1 and stack[-1][0] == 0x50: + stack.pop() + if len(stack) > 1: + control_block = stack[-1] + internal_key = control_block[1:33] + if internal_key == NUMS_H: + return None, None + x_only = spk[2:34] + return b"\x02" + x_only, True + + return None, None + + +def _build_outpoints(vin_list): + """Build (txid_bytes, vout_bytes) list from vin entries. + + txid is reversed to internal byte order, vout is 4-byte LE. + """ + outpoints = [] + for vin in vin_list: + txid = bytes.fromhex(vin["txid"])[::-1] + vout = struct.pack(" 2323 for g in groups.values()): + for eo in expected["outputs"]: + assert eo == [] + return + + # 3. Shared secrets and 4. Output derivation + actual_outputs = set() + for scan_key_hex, group in groups.items(): + scan_pub_key = bytes.fromhex(scan_key_hex) + privkey_bytes = bytes.fromhex("%064x" % privkey_sum) + ecdh_share = _sim_compute_ecdh_share( + sim_exec, sim_execfile, privkey_bytes, scan_pub_key + ) + input_hash = _sim_compute_input_hash( + sim_exec, sim_execfile, outpoints, A_sum + ) + shared_secret = _sim_compute_shared_secret( + sim_exec, sim_execfile, ecdh_share, input_hash + ) + + # Check shared_secret against expected (find first recipient index for this scan key) + for ri, _ in group: + es = expected["shared_secrets"][ri] + if es is not None: + assert shared_secret.hex() == es, ( + "shared_secret mismatch for recipient %d" % ri + ) + break + + for k, (ri, recipient) in enumerate(group): + spend_pub_key = bytes.fromhex(recipient["spend_pub_key"]) + script = _sim_compute_output_script( + sim_exec, + sim_execfile, + outpoints, + A_sum, + ecdh_share, + spend_pub_key, + k, + ) + actual_outputs.add(script[2:].hex()) + + assert any(set(eo) == actual_outputs for eo in expected["outputs"]), ( + "output mismatch: got %s" % actual_outputs + ) + + +class TestBIP352Receiving: + @pytest.mark.parametrize("vec_idx", range(len(_VECTORS)), ids=_VEC_IDS) + def test_receiving(self, sim_exec, sim_execfile, vec_idx): + vec = _VECTORS[vec_idx] + receiving = vec["receiving"][0] + expected = receiving["expected"] + vin_list = receiving["given"]["vin"] + scan_priv_key = bytes.fromhex( + receiving["given"]["key_material"]["scan_priv_key"] + ) + spend_priv_key = bytes.fromhex( + receiving["given"]["key_material"]["spend_priv_key"] + ) + outpoints = _build_outpoints(vin_list) + + # 1. Extract key material and compute A_sum + extracted = _extract_key_material(vin_list) + + if not extracted: + assert expected.get("outputs", []) == [] + return + + expected_pk_sum = expected.get("input_pub_key_sum") + if expected_pk_sum is None: + # Point at infinity or no valid inputs + assert expected.get("outputs", []) == [] + return + + pubkeys = [pk for pk, _, _ in extracted] + A_sum = _sim_combine_pubkeys(sim_exec, sim_execfile, pubkeys) + assert A_sum.hex() == expected_pk_sum, "input_pub_key_sum mismatch" + + # 2. Shared secret + input_hash = _sim_compute_input_hash(sim_exec, sim_execfile, outpoints, A_sum) + ecdh_share = _sim_compute_ecdh_share( + sim_exec, sim_execfile, scan_priv_key, A_sum + ) + shared_secret = _sim_compute_shared_secret( + sim_exec, sim_execfile, ecdh_share, input_hash + ) + assert shared_secret.hex() == expected.get("shared_secret"), ( + "shared_secret mismatch" + ) + + # 3. Spending privkey tweaks + expected_outputs = expected.get("outputs", []) + if not expected_outputs: + return + + for expected_out in expected_outputs: + expected_tweak = bytes.fromhex(expected_out["priv_key_tweak"]) + expected_pubkey = expected_out["pub_key"] + d = _sim_compute_spending_privkey( + sim_exec, sim_execfile, spend_priv_key, expected_tweak + ) + computed_pk = _sim_pubkey_from_privkey(sim_exec, sim_execfile, d) + assert computed_pk[1:].hex() == expected_pubkey, ( + "spending privkey mismatch for output %s" % expected_pubkey + ) From 41d5a7701b5b6547efc813091db284d4eb26625c 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 10/32] BIP-375: implement Silent Payments PSBT signing workflows Integrate preview silent payment address as output in auth:interact Integrate silent payments in signing workflow Add sp_hrp property to chains.py Add silent payments fields to psbt.py --- shared/auth.py | 12 ++++++ shared/chains.py | 3 ++ shared/psbt.py | 90 ++++++++++++++++++++++++++++++++++++---- shared/silentpayments.py | 58 +++++++++++++++++++++++--- 4 files changed, 150 insertions(+), 13 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index f28259bd..67c466de 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -430,6 +430,12 @@ 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 Payments outputs to make preview useful + if self.psbt.has_silent_payment_outputs(): + with stash.SensitiveValues() as sv: + self.psbt.process_silent_payments(sv) + 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 +706,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 +722,8 @@ def output_summary_text(self, msg): else: if len(largest_outs) < MAX_VISIBLE_OUTPUTS: rendered, _ = self.render_output(tx_out) + 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,8 @@ 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) + 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..f5cc3654 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 # used to identify silent payment outputs - 66-byte (scan-pub || spend-pub) + #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,10 @@ 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: + self.sp_v0_info = val + elif kt == PSBT_OUT_SP_V0_LABEL: + self.sp_v0_label = val else: self.unknown = self.unknown or [] pos, length = key @@ -511,6 +524,12 @@ def serialize(self, out_fd, is_v2): wr(PSBT_OUT_SCRIPT, self.script) wr(PSBT_OUT_AMOUNT, self.amount) + 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) @@ -644,7 +663,9 @@ def fraud(idx, af, err=""): class psbtInputProxy(psbtProxy): # just need to store a simple number for these - short_values = { PSBT_IN_SIGHASH_TYPE } + # Silent Payments (shares/dleq): bytes (not proxied) so scan-pub can be used as dict key + # and dict can be mutated in place + 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 +681,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): @@ -708,6 +730,10 @@ def __init__(self, fd, idx): #self.musig_pubnonces = None #self.musig_part_sigs = None + # === silent payments === + #self.sp_ecdh_shares = None # dict[scan-pub] = ecdh_share + #self.sp_dleq_proofs = None # dict[scan-pub] = dleq_proof + self.parse(fd) @property @@ -1165,6 +1191,12 @@ 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: + self.sp_ecdh_shares = self.sp_ecdh_shares or {} + self.sp_ecdh_shares[key] = val + elif kt == PSBT_IN_SP_DLEQ: + self.sp_dleq_proofs = self.sp_dleq_proofs or {} + self.sp_dleq_proofs[key] = val else: # including: PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS self.unknown = self.unknown or [] @@ -1261,17 +1293,27 @@ 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 11/32] BIP-376: handle spending silent payments outputs Add sp_spend support to _derive_input_privkey Validate bip376 spend key derivation --- shared/psbt.py | 18 ++++++- shared/silentpayments.py | 68 +++++++++++++++++++++----- testing/devtest/unit_silentpayments.py | 17 +++++++ testing/devtest/verify_sp_outputs.py | 2 + testing/psbt.py | 16 ++++++ 5 files changed, 106 insertions(+), 15 deletions(-) diff --git a/shared/psbt.py b/shared/psbt.py index f5cc3654..a176533d 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -44,6 +44,7 @@ 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 ) @@ -670,7 +671,7 @@ class psbtInputProxy(psbtProxy): # only part-sigs have a key to be stored. no_keys = {PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT, PSBT_IN_FINAL_SCRIPTSIG, - PSBT_IN_FINAL_SCRIPTWITNESS,PSBT_IN_TAP_KEY_SIG, + PSBT_IN_FINAL_SCRIPTWITNESS, PSBT_IN_SP_TWEAK, PSBT_IN_TAP_KEY_SIG, PSBT_IN_TAP_INTERNAL_KEY, PSBT_IN_TAP_MERKLE_ROOT} blank_flds = ( @@ -682,7 +683,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): @@ -733,6 +734,8 @@ def __init__(self, fd, idx): # === silent payments === #self.sp_ecdh_shares = None # dict[scan-pub] = ecdh_share #self.sp_dleq_proofs = None # dict[scan-pub] = dleq_proof + #self.sp_tweak = None + #self.sp_spend_bip32_derivation = None # (spend-pub, xfp || path) self.parse(fd) @@ -1197,6 +1200,10 @@ def store(self, kt, key, val): elif kt == PSBT_IN_SP_DLEQ: self.sp_dleq_proofs = self.sp_dleq_proofs or {} 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 [] @@ -1301,6 +1308,13 @@ def serialize(self, out_fd, is_v2): 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) diff --git a/shared/silentpayments.py b/shared/silentpayments.py index d0c8dfe9..3cf671fe 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -1,6 +1,6 @@ # (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. # -# silentpayments.py - BIP-352/BIP-375 +# silentpayments.py - BIP-352/BIP-375/BIP-376 # # Consolidates cryptographic primitives and PSBT handling logic for Silent Payments # @@ -41,6 +41,37 @@ def encode_silent_payment_address(scan_key, spend_key, version=0): return ngu.codecs.bip352_encode(hrp, scan_key, spend_key, version) +def validate_bip376_spend(input, output_xonly, my_xfp=None, parent=None): + """Validate silent payment spend using PSBT input data + + TODO: replace with test vectors once available + + Raises FatalPSBTIssue if any SP spend validation checks fail + """ + B_spend_coords, val_coords = input.sp_spend_bip32_derivation + xfp_path = input.parse_xfp_path(val_coords) + if my_xfp is not None: + xfp_path = input.handle_zero_xfp(xfp_path, my_xfp, parent) + + sp_path = xfp_path[1:] + if len(sp_path) != 5: + raise FatalPSBTIssue("SP spend path must have 5 components") + if sp_path[0] != (352 | 0x80000000): + raise FatalPSBTIssue("SP spend key purpose must use 352h path") + + expected_coin = chains.current_chain().b44_cointype | 0x80000000 + if sp_path[1] != expected_coin: + raise FatalPSBTIssue("SP spend path coin type does not match network") + if not (sp_path[2] & 0x80000000): + raise FatalPSBTIssue("SP spend path account must be hardened") + if sp_path[3] != 0x80000000: + raise FatalPSBTIssue("SP spend path key type must be 0h") + + B_spend = input.get(B_spend_coords) + if _compute_silent_payment_spending_xonly(B_spend, input.sp_tweak) != output_xonly: + raise FatalPSBTIssue("SP_TWEAK does not match UTXO output key") + + # ----------------------------------------------------------------------------- # Silent Payments Cryptographic Primitives # ----------------------------------------------------------------------------- @@ -737,25 +768,36 @@ def _derive_input_privkey(self, input, sv): Derive the contributing private key for an eligible input Note: + For silent payment inputs, derives from sp_spend_bip32_derivation and sp_tweak 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: bytes: Derived private key as (32-byte scalar) or None if not eligible """ - # TODO: BIP-376 sp_spend_derivation should go here - spk = input.utxo_spk - if spk and _is_p2tr(spk): - if input.ik_idx is not None and input.taproot_subpaths: - _, path_coords = input.taproot_subpaths[input.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 = None + + if input.sp_tweak and input.sp_spend_bip32_derivation: + _, val_coords = input.sp_spend_bip32_derivation + xfp_path = self.parse_xfp_path(val_coords) + if xfp_path[0] == self.my_xfp: + privkey = _compute_silent_payment_spending_privkey(self._path_to_privkey(xfp_path, sv), input.sp_tweak) else: - for xfp_path in self._iter_input_xfp_paths(input): - if xfp_path[0] == self.my_xfp: - return self._path_to_privkey(xfp_path, sv) - return None + spk = input.utxo_spk + if spk and _is_p2tr(spk): + if input.ik_idx is not None and input.taproot_subpaths: + _, path_coords = input.taproot_subpaths[input.ik_idx[0]] + xfp_path = self.parse_xfp_path(path_coords[2]) + if xfp_path[0] == self.my_xfp: + privkey = self._path_to_privkey(xfp_path, sv) + if input.taproot_internal_key: + privkey = self._normalize_p2tr_privkey(privkey, self.get(input.taproot_internal_key)) + else: + for xfp_path in self._iter_input_xfp_paths(input): + if xfp_path[0] == self.my_xfp: + privkey = self._path_to_privkey(xfp_path, sv) + break + return privkey def _iter_input_xfp_paths(self, input): """ diff --git a/testing/devtest/unit_silentpayments.py b/testing/devtest/unit_silentpayments.py index 71a1c8f7..a2b61a2f 100644 --- a/testing/devtest/unit_silentpayments.py +++ b/testing/devtest/unit_silentpayments.py @@ -16,6 +16,7 @@ _combine_pubkeys, _compute_shared_secret_tweak, _compute_silent_payment_output_script, + _compute_silent_payment_spending_privkey, _is_p2pkh, _is_p2wpkh, _is_p2tr, @@ -195,6 +196,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, bytes) and len(result) == 32 +assert result != (1).to_bytes(32, "big") +assert 0 < int.from_bytes(result, "big") < ngu.secp256k1.curve_order_int() + +try: + _compute_silent_payment_spending_privkey(b"\x00" * 32, b"\x00" * 32) + assert False, "Should raise" +except ValueError as e: + assert "not in valid scalar range" in str(e) # --------------------------------------------------------------------------- # Mock Infrastructure @@ -212,6 +227,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 0e9ac8bf..95d191d7 100644 --- a/testing/devtest/verify_sp_outputs.py +++ b/testing/devtest/verify_sp_outputs.py @@ -53,6 +53,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 fb07d2fe3b30bff8569938f36e739374edc95c3f 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 12/32] SP: add Silent Payments integration tests Create end-to-end tests for SP in testing/test_silentpayments.py - Expose foreign_mk for partial ownership coverage test Integrate SP spend into psbt functions: consider_inputs, determine_my_signing_key, sign_it - Synthesize parsed_subpaths from PSBT_IN_SPEND_BIP32_DERIVATION - Set input.sp_idxs to placeholder to synthesize input is_owned - Implement SP input signing Refactor _compute_and_store_ecdh_shares and _derive_input_privkey to leverage input.sp_idxs --- shared/psbt.py | 50 ++- shared/silentpayments.py | 128 ++++---- testing/devtest/unit_silentpayments.py | 7 +- testing/devtest/verify_sp_outputs.py | 8 +- testing/test_silentpayments.py | 407 +++++++++++++++++++++++++ testing/txn.py | 1 + 6 files changed, 525 insertions(+), 76 deletions(-) create mode 100644 testing/test_silentpayments.py diff --git a/shared/psbt.py b/shared/psbt.py index a176533d..1c5ad1d3 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -48,7 +48,7 @@ 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, validate_bip376_spend psbt_tmp256 = bytearray(256) @@ -747,6 +747,10 @@ def is_segwit(self): def is_musig(self): return bool(self.musig_pubkeys or self.musig_pubnonces or self.musig_part_sigs) + @property + def is_sp_spend(self): + return bool(self.sp_tweak and self.sp_spend_bip32_derivation) + def get_taproot_script_sigs(self): # returns set of (xonly, script) provided via PSBT_IN_TAP_SCRIPT_SIG # we do not parse control blocks (k) not needed @@ -1025,7 +1029,9 @@ def determine_my_signing_key(self, my_idx, addr_or_pubkey, my_xfp, psbt, parsed_ raise FatalPSBTIssue('Need witness script for input #%d' % my_idx) elif self.af == AF_P2TR: - if len(parsed_subpaths) == 1: + if self.is_sp_spend: + validate_bip376_spend(self, addr_or_pubkey, my_xfp, psbt) + elif len(parsed_subpaths) == 1: # keyspend without a script path assert self.taproot_merkle_root is None, "merkle_root should not be defined for simple keyspend" assert self.ik_idx == [0] @@ -1831,7 +1837,7 @@ async def validate(self): inp_have_subpath = False for i in self.inputs: - if i.subpaths or i.taproot_subpaths: + if i.subpaths or i.taproot_subpaths or i.is_sp_spend: inp_have_subpath = True if self.is_v2: @@ -2205,6 +2211,14 @@ def consider_inputs(self, cosign_xfp=None): smallest_nsequence = txi.nSequence parsed_subpaths = inp.parse_subpaths(self.my_xfp, self, cosign_xfp) + # Silent Payments: synthesize parsed_subpaths from PSBT_IN_SPEND_BIP32_DERIVATION + if inp.is_sp_spend and not parsed_subpaths: + k, v = inp.sp_spend_bip32_derivation + sp_path = inp.parse_xfp_path(v) + sp_path = inp.handle_zero_xfp(sp_path, self.my_xfp, self) + parsed_subpaths = {inp.get(k): sp_path} + if sp_path[0] in (self.my_xfp, cosign_xfp): + inp.sp_idxs = [0] # mark as owned so downstream checks treat it like any other signable input if not inp.has_utxo(): if inp.sp_idxs and not inp.fully_signed: @@ -2868,7 +2882,7 @@ def sign_it(self, alternate_secret=None, my_xfp=None): assert inp.sighash in [SIGHASH_ALL, SIGHASH_DEFAULT], "POR sighash not ALL/DEFAULT" # decide if it is appropriate to drop sighash from PSBT - if inp.taproot_subpaths: + if inp.taproot_subpaths or inp.is_sp_spend: drop_sighash = (inp.sighash == SIGHASH_DEFAULT) else: drop_sighash = (inp.sighash == SIGHASH_ALL) @@ -2950,7 +2964,11 @@ def sign_it(self, alternate_secret=None, my_xfp=None): assert not inp.added_sigs, "already done??" assert not inp.taproot_key_sig, "already done taproot??" - if inp.taproot_subpaths: + if inp.is_sp_spend: + # Silent Payments: Use spend-pub from sp_spend_bip32_derivation + schnorrsig = True + pubk, sp = inp.sp_spend_bip32_derivation + elif inp.taproot_subpaths: schnorrsig = True pubk = inp.taproot_subpaths[sp_idx][0] sp = inp.taproot_subpaths[sp_idx][1][2] @@ -2971,7 +2989,7 @@ def sign_it(self, alternate_secret=None, my_xfp=None): # expensive test, but works... and important pu = node.pubkey() - if schnorrsig: + if schnorrsig and not inp.is_sp_spend: pu = pu[1:] assert pu == pk, "Path (%s) led to wrong pubkey for input#%d" % (skp, in_idx) @@ -2990,7 +3008,7 @@ def sign_it(self, alternate_secret=None, my_xfp=None): digest = self.make_txn_sighash(in_idx, txi, inp.sighash) else: # Hash the inputs and such in totally new ways, based on BIP-143 - if not inp.taproot_subpaths: + if not inp.taproot_subpaths and not inp.is_sp_spend: digest = self.make_txn_segwit_sighash(in_idx, txi, inp.amount, inp.segwit_v0_scriptCode(), inp.sighash) @@ -3016,6 +3034,24 @@ def sign_it(self, alternate_secret=None, my_xfp=None): sk = node.privkey() # Do the ACTUAL signature ... finally!!! if schnorrsig: + # Silent Payments: handle signing SP inputs + if inp.is_sp_spend: + digest = self.make_txn_taproot_sighash(in_idx, hash_type=inp.sighash) + if sv.deltamode: + digest = ngu.hash.sha256d(digest) + tweaked_sk = compute_silent_payment_spending_privkey(sk, inp.sp_tweak) + sig = ngu.secp256k1.sign_schnorr(tweaked_sk, digest, ngu.random.bytes(32)) + stash.blank_object(tweaked_sk) + + if inp.sighash != SIGHASH_DEFAULT: + sig += bytes([inp.sighash]) + inp.taproot_key_sig = sig + self.sig_added = True + self.set_modifiable_flag(inp) + stash.blank_object(sk) + stash.blank_object(node) + del sk, node + continue kp = ngu.secp256k1.keypair(sk) xonly_pk = kp.xonly_pubkey().to_bytes() diff --git a/shared/silentpayments.py b/shared/silentpayments.py index 3cf671fe..9e32e36c 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -6,9 +6,14 @@ # import chains import ngu +import stash 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, + TAP_TWEAK_H, +) from serializations import SIGHASH_ALL, SIGHASH_DEFAULT from ubinascii import unhexlify as a2b_hex from utils import keypath_to_str @@ -174,7 +179,7 @@ def _compute_silent_payment_output_script(outpoints, A_sum_bytes, ecdh_share_byt return b"\x51\x20" + x_only -def _compute_silent_payment_spending_privkey(b_spend_bytes, sp_tweak_bytes): +def compute_silent_payment_spending_privkey(b_spend_bytes, sp_tweak_bytes): """ Compute private key for spending a silent payment output @@ -618,30 +623,24 @@ def _compute_and_store_ecdh_shares(self, sv): 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_material = [] # list of (inp, privkey_int) + input_material = [] # list of [(inp, privkey_bytes), ...] for inp in self.inputs: - if not inp.sp_idxs or not self._is_input_eligible(inp): + if not self._is_input_eligible(inp): continue - input_sk = self._derive_input_privkey(inp, sv) - if input_sk: - input_material.append((inp, input_sk)) - elif inp.taproot_subpaths or inp.subpaths: - has_foreign = True - - # FIXME: is this the right approach? cosign_xfp? - # 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): + # Classify each eligible input: + # ours: sp_idxs set AND _derive_input_privkey yields a key + # foreign: sp_idxs set but derivation returns None (cosigner or WIF-store path) + # foreign: no sp_idxs but a contributing pubkey is identifiable (other signer's input) + if inp.sp_idxs: + input_sk = self._derive_input_privkey(inp, sv) + if input_sk is not None: + input_material.append((inp, input_sk)) + else: has_foreign = True - break + elif self._pubkey_from_input(inp): + has_foreign = True if not input_material: return False @@ -765,57 +764,43 @@ def _get_silent_payment_scan_keys(self): def _derive_input_privkey(self, input, sv): """ - Derive the contributing private key for an eligible input + Derive the contributing private key for our inputs - Note: - For silent payment inputs, derives from sp_spend_bip32_derivation and sp_tweak - 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 + Derivation path source depends on input type: + - SP-spend: sp_spend_bip32_derivation + - P2TR: taproot_subpaths[ik_idx[0]] + - other: subpaths[sp_idxs[0]] Returns: - bytes: Derived private key as (32-byte scalar) or None if not eligible + bytes: Derived private key (32-byte scalar) or None """ - privkey = None + if not input.sp_idxs: + return None - if input.sp_tweak and input.sp_spend_bip32_derivation: + if input.is_sp_spend: _, val_coords = input.sp_spend_bip32_derivation xfp_path = self.parse_xfp_path(val_coords) - if xfp_path[0] == self.my_xfp: - privkey = _compute_silent_payment_spending_privkey(self._path_to_privkey(xfp_path, sv), input.sp_tweak) - else: - spk = input.utxo_spk - if spk and _is_p2tr(spk): - if input.ik_idx is not None and input.taproot_subpaths: - _, path_coords = input.taproot_subpaths[input.ik_idx[0]] - xfp_path = self.parse_xfp_path(path_coords[2]) - if xfp_path[0] == self.my_xfp: - privkey = self._path_to_privkey(xfp_path, sv) - if input.taproot_internal_key: - privkey = self._normalize_p2tr_privkey(privkey, self.get(input.taproot_internal_key)) - else: - for xfp_path in self._iter_input_xfp_paths(input): - if xfp_path[0] == self.my_xfp: - privkey = self._path_to_privkey(xfp_path, sv) - break - return privkey - - def _iter_input_xfp_paths(self, input): - """ - 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) + if xfp_path[0] != self.my_xfp: + return None + return compute_silent_payment_spending_privkey(self._path_to_privkey(xfp_path, sv), input.sp_tweak) - Returns: - generator: Parsed xfp_path tuples for all derivation paths in the input - """ - if input.taproot_subpaths: - for _, path_coords in input.taproot_subpaths: - yield self.parse_xfp_path(path_coords[2]) - elif input.subpaths: - for _, path_coords in input.subpaths: - yield self.parse_xfp_path(path_coords) + spk = input.utxo_spk + if spk and _is_p2tr(spk): + if not input.taproot_subpaths or not input.ik_idx: + return None + _, path_coords = input.taproot_subpaths[input.ik_idx[0]] + xfp_path = self.parse_xfp_path(path_coords[2]) + if xfp_path[0] != self.my_xfp: + return None + return self._tweak_p2tr_privkey(self._path_to_privkey(xfp_path, sv), input) + + if not input.subpaths: + return None + _, path_coords = input.subpaths[input.sp_idxs[0]] + xfp_path = self.parse_xfp_path(path_coords) + if xfp_path[0] != self.my_xfp: + return None + return self._path_to_privkey(xfp_path, sv) def _path_to_privkey(self, xfp_path, sv): """ @@ -826,3 +811,18 @@ def _path_to_privkey(self, xfp_path, sv): """ node = sv.derive_path(keypath_to_str(xfp_path, skip=1), register=False) return node.privkey() + + def _tweak_p2tr_privkey(self, privkey_bytes, input): + """ + Tweak a P2TR private key per BIP-341, returning the BIP-340-normalized scalar + + Returns: + bytes: Tweaked private key (32-byte scalar) + """ + kp = ngu.secp256k1.keypair(privkey_bytes) + tweak_data = kp.xonly_pubkey().to_bytes() + if input.taproot_merkle_root: + tweak_data += self.get(input.taproot_merkle_root) + t = ngu.hash.sha256t(TAP_TWEAK_H, tweak_data, True) + kpt = kp.xonly_tweak_add(t) + return _negate_if_odd_y(kpt.privkey()) diff --git a/testing/devtest/unit_silentpayments.py b/testing/devtest/unit_silentpayments.py index a2b61a2f..770bc677 100644 --- a/testing/devtest/unit_silentpayments.py +++ b/testing/devtest/unit_silentpayments.py @@ -16,7 +16,7 @@ _combine_pubkeys, _compute_shared_secret_tweak, _compute_silent_payment_output_script, - _compute_silent_payment_spending_privkey, + compute_silent_payment_spending_privkey, _is_p2pkh, _is_p2wpkh, _is_p2tr, @@ -200,17 +200,18 @@ # Spending Privkey # --------------------------------------------------------------------------- -result = _compute_silent_payment_spending_privkey((1).to_bytes(32, "big"), (42).to_bytes(32, "big")) +result = compute_silent_payment_spending_privkey((1).to_bytes(32, "big"), (42).to_bytes(32, "big")) assert isinstance(result, bytes) and len(result) == 32 assert result != (1).to_bytes(32, "big") assert 0 < int.from_bytes(result, "big") < ngu.secp256k1.curve_order_int() try: - _compute_silent_payment_spending_privkey(b"\x00" * 32, b"\x00" * 32) + compute_silent_payment_spending_privkey(b"\x00" * 32, b"\x00" * 32) assert False, "Should raise" except ValueError as e: assert "not in valid scalar range" in str(e) + # --------------------------------------------------------------------------- # Mock Infrastructure # --------------------------------------------------------------------------- diff --git a/testing/devtest/verify_sp_outputs.py b/testing/devtest/verify_sp_outputs.py index 95d191d7..5fada052 100644 --- a/testing/devtest/verify_sp_outputs.py +++ b/testing/devtest/verify_sp_outputs.py @@ -21,7 +21,7 @@ _compute_ecdh_share, _compute_input_hash, _compute_silent_payment_output_script, - _compute_silent_payment_spending_privkey, + compute_silent_payment_spending_privkey, _negate_if_odd_y, _sum_privkeys, SilentPaymentsMixin, @@ -58,6 +58,10 @@ def __init__(self): self.sp_idxs = None self.ik_idx = None + @property + def is_sp_spend(self): + return bool(self.sp_tweak and self.sp_spend_bip32_derivation) + class MockOutput: def __init__(self): @@ -241,7 +245,7 @@ def _build_mock_psbt(params): elif mode == "compute_spending_privkey": b_spend = a2b_hex(params["b_spend"]) tweak = a2b_hex(params["tweak"]) - result = _compute_silent_payment_spending_privkey(b_spend, tweak) + result = compute_silent_payment_spending_privkey(b_spend, tweak) RV.write(b2a_hex(result).decode()) elif mode == "negate_if_odd_y": diff --git a/testing/test_silentpayments.py b/testing/test_silentpayments.py new file mode 100644 index 00000000..80cdcb45 --- /dev/null +++ b/testing/test_silentpayments.py @@ -0,0 +1,407 @@ +# (c) Copyright 2024 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# test_silentpayments.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 set output scripts -> verify +# +# Requires simulator running. Run with: pytest test_silentpayments.py +# + +import pytest +import struct +import time +from binascii import unhexlify +from binascii import hexlify +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": "%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(0.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 + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + foreign_sub = psbt._foreign_mk.subkey_for_path("0/1") + foreign_privkey_int = int.from_bytes(foreign_sub.privkey(), "big") + 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, + ) + 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. + + Uses the canonical BIP-376 PSBT shape: the input has PSBT_IN_SP_TWEAK and + PSBT_IN_SP_SPEND_BIP32_DERIVATION (BIP-352-style path), with no + PSBT_IN_TAP_BIP32_DERIVATION or PSBT_IN_TAP_INTERNAL_KEY. + """ + xp = dev.master_xpub + + SP_TWEAK = bytes.fromhex("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2") + # BIP-352 spend-key path: m/352h/1h/0h/0h/0 (testnet coin=1, account=0, spend=0, index=0) + SP_SPEND_PATH = "352h/1h/0h/0h/0" + SP_SPEND_PATH_INTS = [ + 352 | 0x80000000, + 1 | 0x80000000, + 0 | 0x80000000, + 0 | 0x80000000, + 0, + ] + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + + inp = psbt.inputs[0] + + master = BIP32Node.from_wallet_key(simulator_fixed_tprv) + mfp = master.fingerprint() + B_spend = master.subkey_for_path(SP_SPEND_PATH).sec() + xfp_and_path = mfp + struct.pack("<" + "I" * len(SP_SPEND_PATH_INTS), *SP_SPEND_PATH_INTS) + + # Compute SP output key: P_k = B_spend + t_k * G + parsed = ec_pubkey_parse(B_spend) + tweaked = ec_pubkey_tweak_add(parsed, SP_TWEAK) + tweaked_xonly_obj, _ = xonly_pubkey_from_pubkey(tweaked) + output_xonly = xonly_pubkey_serialize(tweaked_xonly_obj) + + # Replace witness UTXO scriptPubKey with the SP output key + amount_bytes = inp.witness_utxo[:8] + inp.witness_utxo = amount_bytes + b"\x22\x51\x20" + output_xonly + print(f"output_xonly: {output_xonly.hex()}") + + # BIP-376 inputs do not carry standard taproot derivation + inp.taproot_bip32_paths = {} + inp.taproot_internal_key = None + + inp.sp_tweak = SP_TWEAK + # sp_spend_bip32_derivation: key=33-byte compressed B_spend, value=mfp+path + inp.sp_spend_bip32_derivation = {B_spend: xfp_and_path} + + 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) + + 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 + + _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" + + +# --------------------------------------------------------------------------- +# Handle failure test +# --------------------------------------------------------------------------- + + +def test_exit_gracefully_on_sp_validation_failure(dev, fake_txn, start_sign, end_sign): + """Invalid SP output info should cause signing to fail with a clear error message.""" + xp = dev.master_xpub + + def sp_hacker(psbt): + # Set sp_v0_info with incorrect length (should be 64 bytes) + psbt.outputs[0].sp_v0_info = b"\x00" * 10 + psbt.outputs[0].script = None + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + with pytest.raises(CCProtoError, match="SP_V0_INFO wrong size"): + end_sign(accept=False, finalize=False) diff --git a/testing/txn.py b/testing/txn.py index b8e510d6..b7ee3f90 100644 --- a/testing/txn.py +++ b/testing/txn.py @@ -264,6 +264,7 @@ def doit(inputs, outputs, master_xpub=None, psbt_hacker=None, add_xpub=None, psb # last minute chance to mod PSBT object if psbt_hacker: + psbt._foreign_mk = foreign_mk psbt_hacker(psbt) rv = BytesIO() From 56a595b041b2be5d93fd79a72f897f4c4b3bb6b6 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:58:22 -0400 Subject: [PATCH 13/32] SP: introduce partial share contribution workflow Add test to validate 'Contribute Shares?' UX Simplify SP preview and signing code paths --- shared/auth.py | 25 ++++++++++++++++++++++++- shared/psbt.py | 11 ++++------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 67c466de..2a71064f 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -434,7 +434,30 @@ async def interact(self): # Silent Payments: Validate and pre-process Silent Payments outputs to make preview useful if self.psbt.has_silent_payment_outputs(): with stash.SensitiveValues() as sv: - self.psbt.process_silent_payments(sv) + if not self.psbt.process_silent_payments(sv): + # 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 diff --git a/shared/psbt.py b/shared/psbt.py index 1c5ad1d3..480f3461 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -2846,13 +2846,10 @@ def sign_it(self, alternate_secret=None, my_xfp=None): # Silent Payment Processing if self.has_silent_payment_outputs(): - sp_ready = self.process_silent_payments(sv) - if not sp_ready: - # Silent Payments: must not sign if output scripts not set - # TODO: is this still necessary? if so needs test - self.ux_notes.append(("Silent Payments", - "ECDH shares added. Signatures withheld until all signers contribute.")) - return + if not self.process_silent_payments(sv): + # Silent Payments: must not sign if output scripts not set for all signers + # Defensive re-check - ApproveTransaction::interact should handle this case before reaching signing + raise FatalPSBTIssue("Silent Payments: Signing cannot proceed until all signers contribute their shares") # progress dis.fullscreen('Signing...') From c63e5784f34c68c4fe70bc67a2813e4a5d49210c 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 14/32] 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 --- shared/auth.py | 12 ++-- shared/psbt.py | 7 +- shared/silentpayments.py | 80 +++++++++++++++------- testing/test_silentpayments.py | 121 +++++++++++++++++++++++++++++++-- 4 files changed, 178 insertions(+), 42 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 2a71064f..72388b44 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -729,15 +729,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/psbt.py b/shared/psbt.py index 480f3461..4a0ad7e2 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -1477,12 +1477,7 @@ def output_iter(self, start=0, stop=None): for idx in range(start, stop): out = self.outputs[idx] amount = unpack(" 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(0.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(0.1) + title, _ = cap_story() + assert title == "CONTRIBUTE SHARES?" + + end_sign(accept=False) + + # --------------------------------------------------------------------------- # BIP-376 Silent Payments spend sp output test # --------------------------------------------------------------------------- @@ -405,3 +477,38 @@ 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 per BIP-352 convention).""" + 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.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) From 6e6d18c20555887f174d903a51676b04942791a0 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:49:46 -0400 Subject: [PATCH 15/32] SP: add sp spend derivation path tests Verify SP spend path was derived from m/352h Add signature verification for taproot inputs and SP outputs --- testing/devtest/verify_sp_outputs.py | 45 ++++++ testing/sp_helpers.py | 51 ++++-- testing/test_silentpayments.py | 223 ++++++++++++++++++++------- 3 files changed, 250 insertions(+), 69 deletions(-) diff --git a/testing/devtest/verify_sp_outputs.py b/testing/devtest/verify_sp_outputs.py index 5fada052..bd1effc5 100644 --- a/testing/devtest/verify_sp_outputs.py +++ b/testing/devtest/verify_sp_outputs.py @@ -12,16 +12,20 @@ import ngu import struct import ujson +from uio import BytesIO 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 psbt import psbtObject +from serializations import SIGHASH_DEFAULT from silentpayments import ( _combine_pubkeys, _compute_ecdh_share, _compute_input_hash, _compute_silent_payment_output_script, compute_silent_payment_spending_privkey, + _compute_silent_payment_spending_xonly, _negate_if_odd_y, _sum_privkeys, SilentPaymentsMixin, @@ -248,7 +252,48 @@ def _build_mock_psbt(params): result = compute_silent_payment_spending_privkey(b_spend, tweak) RV.write(b2a_hex(result).decode()) +elif mode == "compute_spend_output_xonly": + B_spend = a2b_hex(params["B_spend"]) + sp_tweak = a2b_hex(params["sp_tweak"]) + result = _compute_silent_payment_spending_xonly(B_spend, sp_tweak) + RV.write(b2a_hex(result).decode()) + elif mode == "negate_if_odd_y": privkey = a2b_hex(params["privkey"]) result = _negate_if_odd_y(privkey) RV.write(b2a_hex(result).decode()) + +elif mode == "verify_taproot_key_spend_signature": + psbt = psbtObject.read_psbt(BytesIO(a2b_hex(params["psbt"]))) + input_index = params["input_index"] + expected = params.get("expected", True) + + if psbt.version is None: + psbt.version = 2 if psbt.txn is None else 0 + psbt.is_v2 = psbt.version >= 2 + + for idx, inp in enumerate(psbt.inputs): + utxo = inp.get_utxo(idx) + inp.amount = utxo.nValue + inp.utxo_spk = utxo.scriptPubKey + + inp = psbt.inputs[input_index] + # P2TR scriptPubKey: OP_1 (0x51) OP_PUSHBYTES_32 (0x20) <32-byte x-only> + output_xonly = inp.utxo_spk[2:34] + + sig_coords = inp.taproot_key_sig + assert sig_coords, "Missing taproot key signature" + sig = psbt.get(sig_coords) + + hash_type = SIGHASH_DEFAULT if len(sig) == 64 else sig[-1] + digest = psbt.make_txn_taproot_sighash(input_index, hash_type=hash_type) + + try: + result = ngu.secp256k1.verify_schnorr(sig[:64], digest, ngu.secp256k1.xonly_pubkey(output_xonly)) + except Exception: + result = False + + assert result == expected, "Taproot spend signature verify: expected %s got %s" % ( + expected, + result, + ) diff --git a/testing/sp_helpers.py b/testing/sp_helpers.py index 147fb089..0d1bd0a2 100644 --- a/testing/sp_helpers.py +++ b/testing/sp_helpers.py @@ -107,12 +107,8 @@ def _serialize_psbt(psbt): return { "txn_modifiable": psbt.txn_modifiable, - "global_ecdh": { - k.hex(): v.hex() for k, v in psbt.sp_global_ecdh_shares.items() - }, - "global_dleq": { - k.hex(): v.hex() for k, v in psbt.sp_global_dleq_proofs.items() - }, + "global_ecdh": {k.hex(): v.hex() for k, v in psbt.sp_global_ecdh_shares.items()}, + "global_dleq": {k.hex(): v.hex() for k, v in psbt.sp_global_dleq_proofs.items()}, "inputs": inputs, "outputs": outputs, } @@ -154,9 +150,7 @@ def _sim_pubkey_from_input(sim_exec, sim_execfile, psbt, input_index): return bytes.fromhex(rv) if rv else None -def _sim_verify_dleq( - sim_exec, sim_execfile, pubkey, scan_key, ecdh_share, proof, expected=True -): +def _sim_verify_dleq(sim_exec, sim_execfile, pubkey, scan_key, ecdh_share, proof, expected=True): _sim_sp( sim_exec, sim_execfile, @@ -184,9 +178,20 @@ def _sim_compute_ecdh_share(sim_exec, sim_execfile, privkey, scan_key): return bytes.fromhex(rv) -def _sim_compute_output_script( - sim_exec, sim_execfile, outpoints, summed_pubkey, ecdh_share, spend_key, k -): +def _sim_compute_spend_output_xonly(sim_exec, sim_execfile, B_spend, sp_tweak): + rv = _sim_sp( + sim_exec, + sim_execfile, + "compute_spend_output_xonly", + { + "B_spend": B_spend.hex(), + "sp_tweak": sp_tweak.hex(), + }, + ) + return bytes.fromhex(rv) + + +def _sim_compute_output_script(sim_exec, sim_execfile, outpoints, summed_pubkey, ecdh_share, spend_key, k): rv = _sim_sp( sim_exec, sim_execfile, @@ -202,6 +207,28 @@ def _sim_compute_output_script( return bytes.fromhex(rv) +def _sim_verify_taproot_key_spend_signature(sim_exec, sim_execfile, signed_psbt, input_index, expected=True): + _sim_sp( + sim_exec, + sim_execfile, + "verify_taproot_key_spend_signature", + { + "psbt": signed_psbt.hex(), + "input_index": input_index, + "expected": expected, + }, + ) + + +def _verify_signatures(sim_exec, sim_execfile, tp, signed_psbt_bytes): + """Verify Schnorr signatures for taproot key-spend inputs""" + for i, inp in enumerate(tp.inputs): + if inp.witness_utxo and len(inp.witness_utxo) >= 11: + if inp.witness_utxo[8:11] == b"\x22\x51\x20": + _sim_verify_taproot_key_spend_signature(sim_exec, sim_execfile, signed_psbt_bytes, i) + # TODO: add ECDSA verification for P2WPKH inputs + + def _sim_combine_pubkeys(sim_exec, sim_execfile, pubkeys): rv = _sim_sp( sim_exec, diff --git a/testing/test_silentpayments.py b/testing/test_silentpayments.py index e232e32c..ae361f5f 100644 --- a/testing/test_silentpayments.py +++ b/testing/test_silentpayments.py @@ -16,14 +16,14 @@ from bip32 import BIP32Node from ckcc_protocol.protocol import CCProtoError from psbt import BasicPSBT -from pysecp256k1 import ec_pubkey_parse, ec_pubkey_tweak_add -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, + _sim_compute_spend_output_xonly, + _verify_signatures, ) from constants import simulator_fixed_tprv @@ -42,6 +42,17 @@ SPEND_KEY_B = unhexlify("02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5") SPEND_KEY_C = unhexlify("02fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556") +# BIP-376 SP spend constants (BIP-352 path: m/352h/1h/0h/0h/0, testnet) +SP_TWEAK = bytes.fromhex("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2") +SP_SPEND_PATH = "352h/1h/0h/0h/0" +SP_SPEND_PATH_INTS = [ + 352 | 0x80000000, + 1 | 0x80000000, + 0 | 0x80000000, + 0 | 0x80000000, + 0, +] + # --------------------------------------------------------------------------- # Helpers @@ -95,6 +106,49 @@ def _compute_foreign_share(sim_exec, sim_execfile, privkey_int, scan_key): return bytes.fromhex(parts[0]), bytes.fromhex(parts[1]) +def _setup_sp_spend_input( + sim_exec, + sim_execfile, + inp, + sp_tweak, + path, + path_ints, + *, + wrong_tweak=None, + clear_taproot=True, +): + """Configure a PSBT input for a BIP-376 SP spend. + + Rewrites inp.witness_utxo to lock to B_spend + utxo_tweak*G, sets + inp.sp_tweak (to psbt_tweak if given, else utxo_tweak), and sets + inp.sp_spend_bip32_derivation. Optionally clears taproot derivation. + """ + master = BIP32Node.from_wallet_key(simulator_fixed_tprv) + mfp = master.fingerprint() + B_spend = master.subkey_for_path(path).sec() + xfp_and_path = mfp + struct.pack("<" + "I" * len(path_ints), *path_ints) + + xonly = _sim_compute_spend_output_xonly(sim_exec, sim_execfile, B_spend, sp_tweak) + amount_bytes = inp.witness_utxo[:8] + inp.witness_utxo = amount_bytes + b"\x22\x51\x20" + xonly + + if clear_taproot: + inp.taproot_bip32_paths = {} + inp.taproot_internal_key = None + + inp.sp_tweak = wrong_tweak if wrong_tweak is not None else sp_tweak + inp.sp_spend_bip32_derivation = {B_spend: xfp_and_path} + + +def _assert_sp_spend_success(sim_exec, sim_execfile, signed): + """Assert a signed SP spend PSBT has correct global shares and output scripts.""" + 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 + _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + _verify_signatures(sim_exec, sim_execfile, tp, signed) + + # --------------------------------------------------------------------------- # Basic Tests # --------------------------------------------------------------------------- @@ -147,6 +201,7 @@ def sp_hacker(psbt): tp = BasicPSBT().parse(signed) _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + _verify_signatures(sim_exec, sim_execfile, tp, signed) def test_sp_mixed_inputs(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): @@ -162,6 +217,9 @@ def sp_hacker(psbt): tp = BasicPSBT().parse(signed) _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + # Schnorr verification skipped: multi-input taproot sighash covers all inputs' + # sequences, and fake_txn may not write PSBT_IN_SEQUENCE — devtest would + # reconstruct a wrong hashSequence. # --------------------------------------------------------------------------- @@ -318,9 +376,7 @@ def sp_hacker(psbt): # --------------------------------------------------------------------------- -def test_sp_partial_owned_coverage_incomplete( - dev, fake_txn, start_sign, end_sign, cap_story -): +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 @@ -346,9 +402,7 @@ def sp_hacker(psbt): tp = BasicPSBT().parse(signed) - assert not tp.sp_global_ecdh_shares, ( - "Global ECDH shares must not exist in multi-signer path" - ) + 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 @@ -356,18 +410,14 @@ def sp_hacker(psbt): 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" - ) + 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 -): +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 @@ -404,59 +454,122 @@ def test_sp_spend_silent_payment_output(dev, fake_txn, start_sign, end_sign, sim """ xp = dev.master_xpub - SP_TWEAK = bytes.fromhex("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2") - # BIP-352 spend-key path: m/352h/1h/0h/0h/0 (testnet coin=1, account=0, spend=0, index=0) - SP_SPEND_PATH = "352h/1h/0h/0h/0" - SP_SPEND_PATH_INTS = [ - 352 | 0x80000000, - 1 | 0x80000000, - 0 | 0x80000000, - 0 | 0x80000000, - 0, - ] + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + SP_SPEND_PATH, + SP_SPEND_PATH_INTS, + clear_taproot=True, + ) + + 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) + _assert_sp_spend_success(sim_exec, sim_execfile, signed) + + +def test_sp_spend_silent_payment_output_with_taproot(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Spend from SP output with correct proof -> sign (taproot derivation retained).""" + xp = dev.master_xpub def sp_hacker(psbt): _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + SP_SPEND_PATH, + SP_SPEND_PATH_INTS, + clear_taproot=False, + ) + + 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) + _assert_sp_spend_success(sim_exec, sim_execfile, signed) - inp = psbt.inputs[0] - master = BIP32Node.from_wallet_key(simulator_fixed_tprv) - mfp = master.fingerprint() - B_spend = master.subkey_for_path(SP_SPEND_PATH).sec() - xfp_and_path = mfp + struct.pack("<" + "I" * len(SP_SPEND_PATH_INTS), *SP_SPEND_PATH_INTS) +def test_sp_spend_wrong_tweak_rejected(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """SP_TWEAK that does not match the witness UTXO output key must be rejected.""" + xp = dev.master_xpub + WRONG_TWEAK = bytes.fromhex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") - # Compute SP output key: P_k = B_spend + t_k * G - parsed = ec_pubkey_parse(B_spend) - tweaked = ec_pubkey_tweak_add(parsed, SP_TWEAK) - tweaked_xonly_obj, _ = xonly_pubkey_from_pubkey(tweaked) - output_xonly = xonly_pubkey_serialize(tweaked_xonly_obj) + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + # Witness UTXO uses SP_TWEAK; PSBT carries WRONG_TWEAK — firmware must reject this + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + SP_SPEND_PATH, + SP_SPEND_PATH_INTS, + wrong_tweak=WRONG_TWEAK, + ) - # Replace witness UTXO scriptPubKey with the SP output key - amount_bytes = inp.witness_utxo[:8] - inp.witness_utxo = amount_bytes + b"\x22\x51\x20" + output_xonly - print(f"output_xonly: {output_xonly.hex()}") + psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + with pytest.raises(CCProtoError, match="SP_TWEAK does not match UTXO output key"): + end_sign(accept=True, finalize=False) - # BIP-376 inputs do not carry standard taproot derivation - inp.taproot_bip32_paths = {} - inp.taproot_internal_key = None - inp.sp_tweak = SP_TWEAK - # sp_spend_bip32_derivation: key=33-byte compressed B_spend, value=mfp+path - inp.sp_spend_bip32_derivation = {B_spend: xfp_and_path} +def test_sp_spend_wrong_purpose_rejected(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """SP_SPEND_BIP32_DERIVATION with a non-352h purpose must be rejected.""" + xp = dev.master_xpub + # BIP-84 path instead of BIP-352 + WRONG_PATH = "84h/1h/0h/0/0" + WRONG_PATH_INTS = [84 | 0x80000000, 1 | 0x80000000, 0 | 0x80000000, 0, 0] + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + WRONG_PATH, + WRONG_PATH_INTS, + ) 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) + with pytest.raises(CCProtoError, match="SP spend key must use 352h path"): + 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 +def test_sp_spend_wrong_coin_type_rejected(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """SP_SPEND_BIP32_DERIVATION with coin type mismatching the network must be rejected.""" + xp = dev.master_xpub + # Simulator is testnet (coin_type=1); use mainnet coin_type=0 to trigger mismatch + WRONG_COIN_PATH = "352h/0h/0h/0h/0" + WRONG_COIN_PATH_INTS = [ + 352 | 0x80000000, + 0 | 0x80000000, + 0 | 0x80000000, + 0 | 0x80000000, + 0, + ] - _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + WRONG_COIN_PATH, + WRONG_COIN_PATH_INTS, + ) - for i, inp in enumerate(tp.inputs): - assert inp.part_sigs or inp.taproot_key_sig, f"Input {i} missing signature" + psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + with pytest.raises(CCProtoError, match="SP spend path coin type does not match network"): + end_sign(accept=True, finalize=False) # --------------------------------------------------------------------------- @@ -484,9 +597,7 @@ def sp_hacker(psbt): # --------------------------------------------------------------------------- -def test_sp_spend_to_labeled_change_address( - dev, fake_txn, start_sign, end_sign, cap_story -): +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 per BIP-352 convention).""" xp = dev.master_xpub @@ -500,9 +611,7 @@ def sp_hacker(psbt): ) 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 - ) + 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() From 22847bea178843745b85db1e0cdb27cf02905c73 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:16:31 -0400 Subject: [PATCH 16/32] SP: add docs/silentpayments.md --- docs/silentpayments.md | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/silentpayments.md diff --git a/docs/silentpayments.md b/docs/silentpayments.md new file mode 100644 index 00000000..6f029715 --- /dev/null +++ b/docs/silentpayments.md @@ -0,0 +1,82 @@ +# Silent Payments + +**COLDCARD®** `EDGE` versions support [Silent Payments](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) from version `TBD`. + +COLDCARD implements the following BIPs: + +* Silent Payments [BIP-352](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki) +* Sending Silent Payments with PSBTs [BIP-375](https://github.com/bitcoin/bips/blob/master/bip-0375.mediawiki) +* Spending Silent Payment outputs with PSBTs [BIP-376](https://github.com/bitcoin/bips/blob/master/bip-0376.mediawiki) + +## Why Silent Payments? + +**Automatic privacy by default** — share one static address publicly; each payment produces a unique on-chain output + +## How It Works + +### Silent Payments Address Generation + +* the sender collects the input public key(s) from the transaction being built +* a shared secret is derived via ECDH between the combined input key and the receiver's scan public key (part of the SP Address) +* the final output is a Taproot address derived from tweaking the receiver's spend public key (part of the SP Address) and shared secret +* only the holder of the scan private key can detect incoming payments; only the holder of the spend private key can sign for them + +### ECDH Shares & DLEQ Proofs + +The shared secret can be computed from either side: + +* **sender side**: `shared_secret = (a_1 + a_2 + ...) * B_scan` — sum of input private keys × receiver's scan public key +* **receiver side**: `shared_secret = (A_1 + A_2 + ...) * b_scan` — sum of input public keys x receiver's scan private key + +In single-signer flows, COLDCARD performs the full sender-side ECDH internally. + +In multi-signer flows (multiple input owners), each signer computes a *partial ECDH share*: + +* `share_i = a_i * B_scan` — the signer's input private key × the receiver's scan public key +* the coordinator sums all shares: `sum(share_i) = (a_1 + a_2 + ...) * B_scan => shared_secret` +* each share is accompanied by a **DLEQ proof** (Discrete Log Equality) so the coordinator or signers can verify the shares were computed from the correct input secret key + +### SP Output Computation Before Signing + +* before any signing round, the coordinator must derive the correct output script from the SP address and the transaction inputs +* this requires the full set of input public keys and the complete shared secret (assembled from partial ECDH shares) +* the computed output is inserted into the PSBT; signers verify that the output in the PSBT matches the expected tweak before signing +* a signer that cannot recompute the expected output MUST refuse to sign + +## Limitations + +* one of the transaction inputs must be eligible for ["Shared Secret Derivation"](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#user-content-Inputs_For_Shared_Secret_Derivation) +* MuSig2 and FROST are not supported yet + +## Example + +SP Address encodes two public keys: a *scan key* and a *spend key* +``` +sp1qqw5jexmu4358tr090qld3egjxkvwftgnwzg7g2v86wad3gywxkln6qcc0kmh5k03cheul53fd7r7h4lg9y3xkrmz3k00ujulyg2pfcaevu9nurf3 +``` + +### Partial Signing (Collaborative Inputs - Multiple Signers) +Round 1 — ECDH share collection +- Coordinator builds PSBT with inputs from each participant and partial outputs (output script not yet finalized) +- Each input owner contributes their partial ECDH share a_i * B_scan and DLEQ proof into the PSBT +- Last contributor verifies all DLEQ proofs, combines partial shares → computes shared secret → computes final output script, updates PSBT + +Round 2 — Sign +- Each signer verifies the output scripts in the PSBT then signs their inputs normally +- Coordinator finalizes and broadcasts + +## Development + +### start simulator +``` +cd unix +% ./simulator.py --q1 +``` + +### execute tests +``` +cd testing +pytest test_bip352_vectors.py +pytest test_bip375_vectors.py +pytest test_silentpayments.py +``` From d48f5a5429c4c2db3f294aebfa1c8488454a3881 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:50:42 -0400 Subject: [PATCH 17/32] SP: implement bip352 generic format export Include key-value pairs: spscan, deriv, name, xfp, key_exp in bip352 dictionary Add bip352 test block to test_export_coldcard Support 127 character limit for silent payments v0 --- shared/export.py | 18 ++++++++++++++++++ testing/bech32.py | 2 +- testing/sp_helpers.py | 32 ++++++++++++++++++++++++++++++++ testing/test_export.py | 20 ++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/shared/export.py b/shared/export.py index 59c417db..966dc0cd 100644 --- a/shared/export.py +++ b/shared/export.py @@ -13,6 +13,7 @@ from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR from ownership import OWNERSHIP from exceptions import QRTooBigError +from silentpayments import encode_silent_payment_address async def export_by_qr(body, label, type_code, force_bbqr=False): # render as QR and show on-screen @@ -444,6 +445,23 @@ def generate_generic_export(account_num=0): node.derive(0, False).derive(0, False) rv[name]['first'] = chain.address(node, fmt) + # Silent Payments: scan-priv + spend-pub packed as a single bech32m "spscan" string + sp_deriv = "m/352h/%dh/%dh" % (chain.b44_cointype, account_num) + sp_node = sv.derive_path(sp_deriv) + scan_node = sv.derive_path(sp_deriv + "/1h/0") + spend_node = sv.derive_path(sp_deriv + "/0h/0") + + spscan = encode_silent_payment_address(scan_node.privkey(), spend_node.pubkey()) + rv["bip352"] = OrderedDict( + spscan=spscan, + deriv=sp_deriv, + name="p2tr", + xfp=xfp2str(swab32(sp_node.my_fp())), + key_exp="[%s/%s]%s" % (master_xfp_str.lower(), + sp_deriv.replace("m/", ""), + spscan), + ) + sig_deriv = "m/44h/{ct}h/{acc}h".format(ct=chain.b44_cointype, acc=account_num) + "/0/0" return ujson.dumps(rv), sig_deriv, AF_CLASSIC diff --git a/testing/bech32.py b/testing/bech32.py index b33db871..77ffe95f 100644 --- a/testing/bech32.py +++ b/testing/bech32.py @@ -79,7 +79,7 @@ def bech32_decode(bech): return (None, None, None) bech = bech.lower() pos = bech.rfind('1') - if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + if pos < 1 or pos + 7 > len(bech) or len(bech) > 127: return (None, None, None) if not all(x in CHARSET for x in bech[pos+1:]): return (None, None, None) diff --git a/testing/sp_helpers.py b/testing/sp_helpers.py index 0d1bd0a2..bf7c4f98 100644 --- a/testing/sp_helpers.py +++ b/testing/sp_helpers.py @@ -314,3 +314,35 @@ def _sim_negate_if_odd_y(sim_exec, sim_execfile, privkey): }, ) return bytes.fromhex(rv) + + +# --------------------------------------------------------------------------- +# Helpers for test_export.py bip352 assertions +# --------------------------------------------------------------------------- + + +def decode_spscan(s, expected_hrp): + """Decode an spscan/tspscan bech32m string into (scan_priv: bytes32, spend_pub: bytes33).""" + from bech32 import bech32_decode, convertbits, Encoding + + hrp, data, spec = bech32_decode(s) + assert hrp == expected_hrp, f"bad HRP: {hrp!r} != {expected_hrp!r}" + assert spec == Encoding.BECH32M, "must be bech32m" + assert data[0] == 0, "expected version 0" + payload = bytes(convertbits(data[1:], 5, 8, False)) + assert len(payload) == 65, f"expected 65 bytes, got {len(payload)}" + return payload[:32], payload[32:] + + +def derive_sp_keys_for_account(account_num, coin_type): + """Return (scan_priv: bytes32, spend_pub: bytes33) for the given account/coin_type. + + Derives from the simulator's fixed test seed, matching the firmware's SP derivation paths. + """ + from bip32 import BIP32Node + from constants import simulator_fixed_tprv + + root = BIP32Node.from_wallet_key(simulator_fixed_tprv) + scan_node = root.subkey_for_path("352h/%dh/%dh/1h/0" % (coin_type, account_num)) + spend_node = root.subkey_for_path("352h/%dh/%dh/0h/0" % (coin_type, account_num)) + return scan_node.privkey(), spend_node.sec() diff --git a/testing/test_export.py b/testing/test_export.py index 6e37b3ba..2ad3923c 100644 --- a/testing/test_export.py +++ b/testing/test_export.py @@ -425,6 +425,26 @@ def test_export_coldcard(way, dev, acct_num, app, pick_menu_item, goto_home, cap else: assert False + # BIP-352 Silent Payments block + from sp_helpers import decode_spscan, derive_sp_keys_for_account + bip352 = obj["bip352"] + ct = 0 if netcode == "BTC" else 1 + acct = int(acct_num or 0) + assert set(bip352.keys()) == {"spscan", "deriv", "name", "xfp", "key_exp"} + assert bip352["deriv"] == "m/352h/%dh/%dh" % (ct, acct) + assert bip352["name"] == "p2tr" + assert len(bip352["xfp"]) == 8 + + hrp_expected = "spscan" if netcode == "BTC" else "tspscan" + scan_priv_enc, spend_pub_enc = decode_spscan(bip352["spscan"], hrp_expected) + exp_scan_priv, exp_spend_pub = derive_sp_keys_for_account(acct, ct) + assert scan_priv_enc == exp_scan_priv + assert spend_pub_enc == exp_spend_pub + + expected_origin = "[%s/352h/%dh/%dh]" % (xfp2str(simulator_fixed_xfp).lower(), ct, acct) + assert bip352["key_exp"].startswith(expected_origin + hrp_expected + "1") + assert bip352["key_exp"].split("]", 1)[1] == bip352["spscan"] + @pytest.mark.parametrize('way', ["sd", "vdisk", "nfc", "qr"]) @pytest.mark.parametrize('testnet', [True, False]) @pytest.mark.parametrize('acct_num', [None, '0', '99', '123']) From 1d7daa34a682eebabab9b14d226c67c7bf88b7e2 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:57:12 -0400 Subject: [PATCH 18/32] SP: add more integration tests Mixed inputs SP spend and P2TR Reject non-SIGHASH_ALL Reject psbt v0 send to SP output --- testing/test_silentpayments.py | 56 +++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/testing/test_silentpayments.py b/testing/test_silentpayments.py index ae361f5f..142b9f97 100644 --- a/testing/test_silentpayments.py +++ b/testing/test_silentpayments.py @@ -222,6 +222,28 @@ def sp_hacker(psbt): # reconstruct a wrong hashSequence. +def test_sp_mixed_sp_spend_and_regular_input(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Regular P2TR input + SP-spend input: both private keys summed for ECDH, both signed""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[1], + SP_TWEAK, + SP_SPEND_PATH, + SP_SPEND_PATH_INTS, + clear_taproot=True, + ) + + psbt_bytes = fake_txn([["p2tr"], ["p2tr"]], 1, xp, psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + signed = end_sign(accept=True, finalize=False) + _assert_sp_spend_success(sim_exec, sim_execfile, signed) + + # --------------------------------------------------------------------------- # Mixed output tests # --------------------------------------------------------------------------- @@ -538,7 +560,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) - with pytest.raises(CCProtoError, match="SP spend key must use 352h path"): + with pytest.raises(CCProtoError, match="SP spend key purpose must use 352h path"): end_sign(accept=True, finalize=False) @@ -592,6 +614,38 @@ def sp_hacker(psbt): end_sign(accept=False, finalize=False) +def test_sp_non_sighash_all_rejected(dev, fake_txn, start_sign, end_sign): + """Non-SIGHASH_ALL on an input with SP outputs must be rejected (BIP-375).""" + xp = dev.master_xpub + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + psbt.inputs[0].sighash = 3 # SIGHASH_SINGLE + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + with pytest.raises(CCProtoError, match="Silent payments require SIGHASH_ALL"): + end_sign(accept=True, finalize=False) + + +def test_sp_psbt_v0_rejected(dev, fake_txn, start_sign, end_sign): + """PSBTv0 with SP outputs is rejected: outpoint fields are PSBTv2-only. + + BIP-352 SP computation requires PSBT_IN_PREVIOUS_TXID and PSBT_IN_OUTPUT_INDEX + (PSBTv2 fields) to derive the input hash. PSBTv0 embeds outpoints only in the + unsigned transaction, so firmware raises when trying to collect them. + """ + 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=False, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + with pytest.raises(CCProtoError, match="Missing outpoint for silent payment input"): + end_sign(accept=True, finalize=False) + + # --------------------------------------------------------------------------- # Silent Payments Change Detection Tests # --------------------------------------------------------------------------- From fc6e5d3e1e526a828dbcce3532a1a080e977e7b7 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 1 May 2026 08:31:13 -0400 Subject: [PATCH 19/32] SP (refactor): test hygiene Remove MockPSBT from verify_sp_outputs Test test_sp_spend_to_labeled_change_address should sign transaction Replace _serialize_psbt with read_psbt Replace single use compute_foreign_share with generate_dleq_proof Remove unnecessary int-byte conversions in test_bip352_vectors.py --- testing/devtest/verify_sp_outputs.py | 144 ++++++--------------------- testing/sp_helpers.py | 93 ++--------------- testing/test_bip352_vectors.py | 19 ++-- testing/test_bip375_vectors.py | 9 +- testing/test_silentpayments.py | 138 +++++++++++++------------ 5 files changed, 123 insertions(+), 280 deletions(-) diff --git a/testing/devtest/verify_sp_outputs.py b/testing/devtest/verify_sp_outputs.py index bd1effc5..32d23d70 100644 --- a/testing/devtest/verify_sp_outputs.py +++ b/testing/devtest/verify_sp_outputs.py @@ -10,11 +10,7 @@ # import main import ngu -import struct import ujson -from uio import BytesIO -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 psbt import psbtObject @@ -28,8 +24,10 @@ _compute_silent_payment_spending_xonly, _negate_if_odd_y, _sum_privkeys, - SilentPaymentsMixin, ) +from ubinascii import hexlify as b2a_hex +from ubinascii import unhexlify as a2b_hex +from uio import BytesIO if isinstance(main.SP_VERIFY, str): with open(main.SP_VERIFY) as f: @@ -40,99 +38,25 @@ 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_tweak = None - self.sp_spend_bip32_derivation = {} - self.sp_idxs = None - self.ik_idx = None - - @property - def is_sp_spend(self): - return bool(self.sp_tweak and self.sp_spend_bip32_derivation) - - -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("utxo_spk"): - inp.utxo_spk = a2b_hex(d["utxo_spk"]) - elif 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(" Date: Fri, 1 May 2026 17:47:51 -0400 Subject: [PATCH 20/32] SP: improve sp spend without sp outputs and change detection Convert validate_bip376_spend to validate_silent_payment_inputs Use accounts to validate sp spend material and mark output.is_change Introduce reusable sp_derive_path function Fix change detection issue raised by previous test hygiene commit - Signing was restored for test_sp_spend_labeled_change_detected Fix psbt exception in consider_outputs with sp change - Add apply_label_to_spend_key primitive - Match spend public key to labeled spend public key for change New tests: sp spend to non sp output sp spend rejection tests - unknown account, incorrect sp spend derivation path sp change output with foreign keys is allowed but displays warning Changed test: sp spend to label change - fix label spend key derivation, use sp input --- shared/auth.py | 9 +- shared/export.py | 4 +- shared/psbt.py | 17 +- shared/silentpayments.py | 187 ++++++++++++++++------ testing/devtest/unit_silentpayments.py | 4 - testing/devtest/verify_sp_outputs.py | 8 + testing/sp_helpers.py | 27 ++++ testing/test_silentpayments.py | 212 +++++++++++++++++++++++-- 8 files changed, 392 insertions(+), 76 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index 72388b44..cfaf64ab 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -432,9 +432,12 @@ async def interact(self): args = self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp) # Silent Payments: Validate and pre-process Silent Payments outputs to make preview useful - if self.psbt.has_silent_payment_outputs(): - with stash.SensitiveValues() as sv: - if not self.psbt.process_silent_payments(sv): + with stash.SensitiveValues() as sv: + if self.psbt.has_silent_payment_inputs(): + self.psbt.validate_silent_payment_inputs(sv) + + if self.psbt.has_silent_payment_outputs(): + if not self.psbt.process_silent_payment_outputs(sv): # Coverage incomplete: shares computed but waiting on other signers # Skip normal approval flow — prompt user to contribute shares, then save del args diff --git a/shared/export.py b/shared/export.py index 966dc0cd..acbc92f2 100644 --- a/shared/export.py +++ b/shared/export.py @@ -13,7 +13,7 @@ from charcodes import KEY_NFC, KEY_CANCEL, KEY_QR from ownership import OWNERSHIP from exceptions import QRTooBigError -from silentpayments import encode_silent_payment_address +from silentpayments import encode_silent_payment_address, sp_derive_path async def export_by_qr(body, label, type_code, force_bbqr=False): # render as QR and show on-screen @@ -446,7 +446,7 @@ def generate_generic_export(account_num=0): rv[name]['first'] = chain.address(node, fmt) # Silent Payments: scan-priv + spend-pub packed as a single bech32m "spscan" string - sp_deriv = "m/352h/%dh/%dh" % (chain.b44_cointype, account_num) + sp_deriv = sp_derive_path(chain.b44_cointype, account_num) sp_node = sv.derive_path(sp_deriv) scan_node = sv.derive_path(sp_deriv + "/1h/0") spend_node = sv.derive_path(sp_deriv + "/0h/0") diff --git a/shared/psbt.py b/shared/psbt.py index 4a0ad7e2..835db959 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -48,7 +48,7 @@ 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, compute_silent_payment_spending_privkey, validate_bip376_spend +from silentpayments import SilentPaymentsMixin, compute_silent_payment_spending_privkey psbt_tmp256 = bytearray(256) @@ -1030,7 +1030,7 @@ def determine_my_signing_key(self, my_idx, addr_or_pubkey, my_xfp, psbt, parsed_ elif self.af == AF_P2TR: if self.is_sp_spend: - validate_bip376_spend(self, addr_or_pubkey, my_xfp, psbt) + pass # Silent Payments inputs are handled in validate_silent_payment_inputs elif len(parsed_subpaths) == 1: # keyspend without a script path assert self.taproot_merkle_root is None, "merkle_root should not be defined for simple keyspend" @@ -2006,7 +2006,8 @@ def consider_outputs(self, len_pths, hard_p, prefix_pths, idx_max, cosign_xfp=No self.num_change_outputs += 1 total_change += txo.nValue - if validate_inp_pths: + # Silent Payments: SP change is validated by _detect_sp_change_outputs, skip additional checks here + if validate_inp_pths and not output.sp_v0_info: # Enforce some policy on change outputs: # - need to "look like" they are going to same wallet as inputs came from # - range limit last two path components (numerically) @@ -2797,7 +2798,11 @@ def sign_it(self, alternate_secret=None, my_xfp=None): # Double-check the change outputs are right. This is slow, but critical because # it detects bad actors, not bugs or mistakes. # - equivalent check already done for p2sh outputs when we re-built the redeem script - change_outs = [n for n,o in enumerate(self.outputs) if o.is_change] + + # SP change is verified at preview time by _detect_sp_change_outputs; + # the subpath/check_pubkey_at_path machinery below doesn't apply to it. + change_outs = [n for n, o in enumerate(self.outputs) + if o.is_change and not o.sp_v0_info] if change_outs: dis.fullscreen('Change Check...') @@ -2840,8 +2845,10 @@ def sign_it(self, alternate_secret=None, my_xfp=None): "BIP-32 path doesn't match actual address.") # Silent Payment Processing + if self.has_silent_payment_inputs(): + self.validate_silent_payment_inputs(sv) if self.has_silent_payment_outputs(): - if not self.process_silent_payments(sv): + if not self.process_silent_payment_outputs(sv): # Silent Payments: must not sign if output scripts not set for all signers # Defensive re-check - ApproveTransaction::interact should handle this case before reaching signing raise FatalPSBTIssue("Silent Payments: Signing cannot proceed until all signers contribute their shares") diff --git a/shared/silentpayments.py b/shared/silentpayments.py index 8edb87ad..4bfee85d 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -7,9 +7,11 @@ import chains import ngu import stash +from glob import settings from dleq import generate_dleq_proof, verify_dleq_proof -from exceptions import FatalPSBTIssue +from exceptions import FatalPSBTIssue, FraudulentChangeOutput from precomp_tag_hash import ( + BIP352_LABEL_TAG_H, BIP352_SHARED_SECRET_TAG_H, BIP352_INPUTS_TAG_H, TAP_TWEAK_H, @@ -46,41 +48,34 @@ def encode_silent_payment_address(scan_key, spend_key, version=0): return ngu.codecs.bip352_encode(hrp, scan_key, spend_key, version) -def validate_bip376_spend(input, output_xonly, my_xfp=None, parent=None): - """Validate silent payment spend using PSBT input data - - TODO: replace with test vectors once available - - Raises FatalPSBTIssue if any SP spend validation checks fail - """ - B_spend_coords, val_coords = input.sp_spend_bip32_derivation - xfp_path = input.parse_xfp_path(val_coords) - if my_xfp is not None: - xfp_path = input.handle_zero_xfp(xfp_path, my_xfp, parent) - - sp_path = xfp_path[1:] - if len(sp_path) != 5: - raise FatalPSBTIssue("SP spend path must have 5 components") - if sp_path[0] != (352 | 0x80000000): - raise FatalPSBTIssue("SP spend key purpose must use 352h path") - - expected_coin = chains.current_chain().b44_cointype | 0x80000000 - if sp_path[1] != expected_coin: - raise FatalPSBTIssue("SP spend path coin type does not match network") - if not (sp_path[2] & 0x80000000): - raise FatalPSBTIssue("SP spend path account must be hardened") - if sp_path[3] != 0x80000000: - raise FatalPSBTIssue("SP spend path key type must be 0h") - - B_spend = input.get(B_spend_coords) - if _compute_silent_payment_spending_xonly(B_spend, input.sp_tweak) != output_xonly: - raise FatalPSBTIssue("SP_TWEAK does not match UTXO output key") +def sp_derive_path(coin_type, account): + """BIP-352 base derivation path for a given coin type and account number""" + return "m/352h/%dh/%dh" % (coin_type, account) # ----------------------------------------------------------------------------- # Silent Payments Cryptographic Primitives # ----------------------------------------------------------------------------- +def _apply_label_to_spend_key(B_spend_bytes, b_scan_bytes, label): + """ + Apply BIP-352 label tweak to spend key + + Formula: B_m = B_spend + hash_BIP0352/Label(b_scan || m)*G + + Args: + B_spend_bytes: Base spend public key (33-byte compressed) + b_scan_bytes: Scan private key (32-byte scalar) + label: Label integer (0 for change) + + Returns: + bytes: Labeled spend public key (33-byte compressed) + """ + msg = b_scan_bytes + label.to_bytes(4, 'big') + label_tweak_bytes = ngu.hash.sha256t(BIP352_LABEL_TAG_H, msg, True) + label_tweak_point = ngu.secp256k1.ec_pubkey_tweak_mul(G, label_tweak_bytes) + return ngu.secp256k1.ec_pubkey_combine([B_spend_bytes, label_tweak_point]) + def _combine_pubkeys(pubkeys): """ @@ -294,21 +289,19 @@ class SilentPaymentsMixin: This class assumes it is mixed into psbtObject and has access to psbt as self """ - def process_silent_payments(self, sv): + def process_silent_payment_outputs(self, sv): """ Core SP workflow: validate, compute shares, compute scripts Notes: - - This function is intended to be called during the preview phase, before signing + - This function is intended to be called during the preview phase and again during 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/signing - False if we generated shares but are waiting on others, or if we don't have necessary info to proceed + False if we generated shares but are waiting on others """ - if not self.has_silent_payment_outputs(): - return False self._validate_psbt_structure() self._validate_input_eligibility() self._validate_ecdh_coverage() @@ -319,7 +312,7 @@ def process_silent_payments(self, sv): if self._is_ecdh_coverage_complete(): # Computes scripts, or validates existing ones against recomputed values self._compute_silent_payment_output_scripts() - self._detect_sp_change_outputs() + self._detect_sp_change_outputs(sv) return self._all_sp_outputs_have_scripts() return False @@ -341,6 +334,63 @@ def render_silent_payment_output_string(self, output): return " - silent payments address -\n%s\n" % encode_silent_payment_address(scan_key, spend_key) + def validate_silent_payment_inputs(self, sv): + """ + Validate inputs spending silent payments (BIP-376) + + Raises FatalPSBTIssue on any failure. + """ + coin_type = chains.current_chain().b44_cointype + expected_coin = coin_type | 0x80000000 + + candidate_accounts = self._get_candidate_sp_accounts() + + for i, inp in enumerate(self.inputs): + if not inp.is_sp_spend: + continue + + # Validate path shape and extract B_spend for both our and foreign inputs + B_spend_coords, val_coords = inp.sp_spend_bip32_derivation + xfp_path = inp.parse_xfp_path(val_coords) + xfp_path = inp.handle_zero_xfp(xfp_path, self.my_xfp, self) + + sp_path = xfp_path[1:] + if len(sp_path) != 5: + raise FatalPSBTIssue("Input #%d: SP spend path must have 5 components" % i) + if sp_path[0] != (352 | 0x80000000): + raise FatalPSBTIssue("Input #%d: SP spend key purpose must use 352h path" % i) + if sp_path[1] != expected_coin: + raise FatalPSBTIssue("Input #%d: SP spend path coin type does not match network" % i) + if not (sp_path[2] & 0x80000000): + raise FatalPSBTIssue("Input #%d: SP spend path account must be hardened" % i) + if sp_path[3] != 0x80000000: + raise FatalPSBTIssue("Input #%d: SP spend path key type must be 0h" % i) + if sp_path[4] != 0: + raise FatalPSBTIssue("Input #%d: SP spend path index must be 0" % i) + + B_spend = inp.get(B_spend_coords) + + # For our inputs, validate account is known and B_spend matches derivation from this device + if xfp_path[0] == self.my_xfp: + account = sp_path[2] & ~0x80000000 + if account not in candidate_accounts: + raise FatalPSBTIssue( + "Input #%d: SP spend account not recognized on this device" % i) + + spend_node = sv.derive_path( + sp_derive_path(coin_type, account) + "/0h/0", register=False) + derived = spend_node.pubkey() + del spend_node + if derived != B_spend: + raise FatalPSBTIssue("Input #%d: SP spend pubkey mismatch" % i) + + spk = inp.utxo_spk + if not spk or not _is_p2tr(spk): + raise FatalPSBTIssue("Input #%d: SP spend UTXO is not P2TR" % i) + utxo_xonly = spk[2:34] + if _compute_silent_payment_spending_xonly(B_spend, inp.sp_tweak) != utxo_xonly: + raise FatalPSBTIssue("Input #%d: SP_TWEAK does not match UTXO output key" % i) + # ----------------------------------------------------------------------------- # Input Helper Functions # ----------------------------------------------------------------------------- @@ -613,9 +663,6 @@ def _compute_and_store_ecdh_shares(self, sv): """ Compute ECDH shares and DLEQ proofs for our inputs, store in PSBT fields - Notes: - Sets self.sp_all_inputs_ours for callers - Returns: bool: True if shares were computed False if we have no signable inputs @@ -644,14 +691,12 @@ def _compute_and_store_ecdh_shares(self, sv): raise FatalPSBTIssue("No eligible inputs for ECDH computation") return False - self.sp_all_inputs_ours = not has_foreign - scan_keys = self._get_silent_payment_scan_keys() if not scan_keys: return False for scan_key in scan_keys: - if self.sp_all_inputs_ours: + if not has_foreign: # Single-signer: combine all input private keys, one global ECDH share and DLEQ proofs combined_sk = _sum_privkeys(sk for _, sk in input_material) ecdh_share = _compute_ecdh_share(combined_sk, scan_key) @@ -714,6 +759,10 @@ def _compute_silent_payment_output_scripts(self): # Utility Functions # ----------------------------------------------------------------------------- + def has_silent_payment_inputs(self): + """Check if PSBT contains any silent payment inputs""" + return any(inp.is_sp_spend for inp in self.inputs) + def has_silent_payment_outputs(self): """ Check if PSBT contains any silent payment outputs @@ -733,17 +782,59 @@ def _all_sp_outputs_have_scripts(self): return False return True - def _detect_sp_change_outputs(self): + def _detect_sp_change_outputs(self, sv): """ - Mark SP outputs as change if sp_v0_label is present and equals 0 (BIP-352 change convention) + Mark SP outputs as change if they match our accounts - No return value; modifies self.outputs 'is_change' in place + Raises FraudulentChangeOutput if all SP-eligible inputs are ours (single-signer); + multi-signer, a non-matching label=0 output is skipped (may belong to a co-signer). """ - for outp in self.outputs: + has_foreign = False + for inp in self.inputs: + if not self._is_input_eligible(inp): + continue + if inp.sp_idxs: + if self._derive_input_privkey(inp, sv) is None: + has_foreign = True + break + elif self._pubkey_from_input(inp): + has_foreign = True + break + + coin_type = chains.current_chain().b44_cointype + candidate_accounts = self._get_candidate_sp_accounts() + + for out_idx, outp in enumerate(self.outputs): if not outp.sp_v0_info or not outp.sp_v0_label: continue - if int.from_bytes(outp.sp_v0_label, "little") == 0: - outp.is_change = True + if int.from_bytes(outp.sp_v0_label, "little") != 0: + continue + + scan_pub = outp.sp_v0_info[:33] + spend_pub = outp.sp_v0_info[33:66] + + matched = False + for account in candidate_accounts: + base = sp_derive_path(coin_type, account) + scan_node = sv.derive_path(base + "/1h/0", register=False) + spend_node = sv.derive_path(base + "/0h/0", register=False) + label_spend_pub = _apply_label_to_spend_key(spend_node.pubkey(), scan_node.privkey(), 0) + matched = (scan_node.pubkey() == scan_pub) and (label_spend_pub == spend_pub) + del scan_node, spend_node + if matched: + outp.is_change = True + break + + if not matched and not has_foreign: + raise FraudulentChangeOutput(out_idx, "SP change output keys do not match this device") + + def _get_candidate_sp_accounts(self): + """Get all known account numbers from device settings, always include account 0""" + seen = {0} + for _af, acct_num in settings.get('accts', []): + if acct_num: + seen.add(acct_num) + return sorted(seen) def _get_outpoints(self): """ diff --git a/testing/devtest/unit_silentpayments.py b/testing/devtest/unit_silentpayments.py index 770bc677..58130bdf 100644 --- a/testing/devtest/unit_silentpayments.py +++ b/testing/devtest/unit_silentpayments.py @@ -458,8 +458,6 @@ def _make_sp_output(scan_key): (MY_XFP, 44, 0, 0, 1): TEST_PRIVKEY2, } ) -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) @@ -469,8 +467,6 @@ def _make_sp_output(scan_key): outp.sp_v0_info = TEST_SCAN_KEY + TEST_SPEND_KEY psbt.outputs = [outp] psbt._compute_and_store_ecdh_shares({TEST_DERIV_OURS: TEST_PRIVKEY}) -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) diff --git a/testing/devtest/verify_sp_outputs.py b/testing/devtest/verify_sp_outputs.py index 32d23d70..2c17e325 100644 --- a/testing/devtest/verify_sp_outputs.py +++ b/testing/devtest/verify_sp_outputs.py @@ -16,6 +16,7 @@ from psbt import psbtObject from serializations import SIGHASH_DEFAULT from silentpayments import ( + _apply_label_to_spend_key, _combine_pubkeys, _compute_ecdh_share, _compute_input_hash, @@ -180,6 +181,13 @@ def _load_psbt(params): result = _compute_silent_payment_spending_xonly(B_spend, sp_tweak) RV.write(b2a_hex(result).decode()) +elif mode == "apply_label_to_spend_key": + spend_pub = a2b_hex(params["spend_pub"]) + scan_priv = a2b_hex(params["scan_priv"]) + label = int(params["label"]) + result = _apply_label_to_spend_key(spend_pub, scan_priv, label) + RV.write(b2a_hex(result).decode()) + elif mode == "negate_if_odd_y": privkey = a2b_hex(params["privkey"]) result = _negate_if_odd_y(privkey) diff --git a/testing/sp_helpers.py b/testing/sp_helpers.py index 70c7c1ae..e0b078c9 100644 --- a/testing/sp_helpers.py +++ b/testing/sp_helpers.py @@ -98,6 +98,19 @@ def _sim_compute_ecdh_share(sim_exec, sim_execfile, privkey, scan_key): return bytes.fromhex(rv) +def _sim_generate_dleq_proof(sim_exec, sim_execfile, privkey, scan_key): + rv = _sim_sp( + sim_exec, + sim_execfile, + "generate_dleq_proof", + { + "privkey": privkey.hex(), + "scan_key": scan_key.hex(), + }, + ) + return bytes.fromhex(rv) + + def _sim_compute_spend_output_xonly(sim_exec, sim_execfile, B_spend, sp_tweak): rv = _sim_sp( sim_exec, @@ -223,6 +236,20 @@ def _sim_sum_privkeys(sim_exec, sim_execfile, privkeys): return bytes.fromhex(rv) +def _sim_apply_label_to_spend_key(sim_exec, sim_execfile, spend_pub, scan_priv, label): + rv = _sim_sp( + sim_exec, + sim_execfile, + "apply_label_to_spend_key", + { + "spend_pub": spend_pub.hex(), + "scan_priv": scan_priv.hex(), + "label": label, + }, + ) + return bytes.fromhex(rv) + + def _sim_negate_if_odd_y(sim_exec, sim_execfile, privkey): rv = _sim_sp( sim_exec, diff --git a/testing/test_silentpayments.py b/testing/test_silentpayments.py index 4310d1d0..fc95db60 100644 --- a/testing/test_silentpayments.py +++ b/testing/test_silentpayments.py @@ -17,9 +17,11 @@ from constants import simulator_fixed_tprv from psbt import BasicPSBT from sp_helpers import ( + _sim_apply_label_to_spend_key, _sim_compute_ecdh_share, _sim_compute_output_script, _sim_compute_spend_output_xonly, + _sim_generate_dleq_proof, _sim_get_ecdh_and_pubkey, _sim_get_outpoints, _sim_verify_dleq, @@ -38,6 +40,7 @@ # BIP-376 SP spend constants (BIP-352 path: m/352h/1h/0h/0h/0, testnet) SP_TWEAK = bytes.fromhex("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2") +SP_SCAN_PATH = "352h/1h/0h/1h/0" SP_SPEND_PATH = "352h/1h/0h/0h/0" SP_SPEND_PATH_INTS = [ 352 | 0x80000000, @@ -47,6 +50,7 @@ 0, ] +SIMULATOR_DELAY = 0 # seconds to wait for the simulator to process the PSBT and update the story # --------------------------------------------------------------------------- # Helpers @@ -141,8 +145,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) - - time.sleep(0.1) + time.sleep(SIMULATOR_DELAY) title, story = cap_story() assert title == "OK TO SEND?" assert "not well understood script" not in story @@ -160,6 +163,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -175,6 +179,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -191,6 +196,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn([["p2wpkh"], ["p2tr"]], 1, xp, psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -218,6 +224,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn([["p2tr"], ["p2tr"]], 1, xp, psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) _assert_sp_spend_success(sim_exec, sim_execfile, signed) @@ -243,6 +250,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 3, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -268,6 +276,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 2, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -291,6 +300,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 2, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -313,6 +323,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn([["p2wpkh"], ["p2wpkh"]], 1, xp, psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -351,6 +362,7 @@ def sp_hacker(psbt): psbt_hacker=sp_hacker, ) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) @@ -394,8 +406,7 @@ def sp_hacker(psbt): psbt_hacker=sp_hacker, ) start_sign(psbt_bytes) - - time.sleep(0.1) + time.sleep(SIMULATOR_DELAY) title, story = cap_story() assert title == "CONTRIBUTE SHARES?" assert "ECDH shares" in story @@ -435,8 +446,7 @@ def sp_hacker(psbt): psbt_hacker=sp_hacker, ) start_sign(psbt_bytes) - - time.sleep(0.1) + time.sleep(SIMULATOR_DELAY) title, _ = cap_story() assert title == "CONTRIBUTE SHARES?" @@ -448,6 +458,36 @@ def sp_hacker(psbt): # --------------------------------------------------------------------------- +def test_sp_spend_input_to_non_sp_output(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Spend from SP input to a non SP output. + + Verifies that SP input validation (tweak, path) passes and the tx is signed + without computing ECDH shares (which are only needed for SP outputs). + """ + xp = dev.master_xpub + + def sp_hacker(psbt): + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + SP_SPEND_PATH, + SP_SPEND_PATH_INTS, + clear_taproot=True, + ) + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) + signed = end_sign(accept=True, finalize=False) + + tp = BasicPSBT().parse(signed) + assert not tp.sp_global_ecdh_shares, "Global ECDH shares must not exist when spending to non-SP outputs" + assert not tp.inputs[0].sp_ecdh_shares, "Per-input ECDH shares must not exist when spending to non-SP outputs" + _verify_signatures(sim_exec, sim_execfile, tp, signed) + + 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. @@ -471,6 +511,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) _assert_sp_spend_success(sim_exec, sim_execfile, signed) @@ -493,6 +534,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) signed = end_sign(accept=True, finalize=False) _assert_sp_spend_success(sim_exec, sim_execfile, signed) _verify_sp_outputs(sim_exec, sim_execfile, BasicPSBT().parse(signed), SCAN_KEY, [SPEND_KEY_B]) @@ -518,6 +560,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) with pytest.raises(CCProtoError, match="SP_TWEAK does not match UTXO output key"): end_sign(accept=True, finalize=False) @@ -542,6 +585,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) with pytest.raises(CCProtoError, match="SP spend key purpose must use 352h path"): end_sign(accept=True, finalize=False) @@ -572,10 +616,71 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) with pytest.raises(CCProtoError, match="SP spend path coin type does not match network"): end_sign(accept=True, finalize=False) +def test_sp_spend_unknown_account_rejected(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """Path with an account not in settings.get('accts') (and not 0) must be rejected.""" + xp = dev.master_xpub + UNKNOWN_ACCT_PATH = "352h/1h/7h/0h/0" + UNKNOWN_ACCT_PATH_INTS = [ + 352 | 0x80000000, + 1 | 0x80000000, + 7 | 0x80000000, + 0 | 0x80000000, + 0, + ] + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + UNKNOWN_ACCT_PATH, + UNKNOWN_ACCT_PATH_INTS, + ) + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) + with pytest.raises(CCProtoError, match="SP spend account not recognized on this device"): + end_sign(accept=True, finalize=False) + + +def test_sp_spend_tampered_pubkey_rejected(dev, fake_txn, start_sign, end_sign, sim_exec, sim_execfile): + """PSBT-stated B_spend must match device-derived spend pubkey for the stated path.""" + xp = dev.master_xpub + FAKE_PUBKEY_PATH = "84h/1h/0h/0/0" + + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + master = BIP32Node.from_wallet_key(simulator_fixed_tprv) + mfp = master.fingerprint() + # Tampered: B_spend derived from a non-SP path; xfp+path advertises the legitimate SP path + fake_B_spend = master.subkey_for_path(FAKE_PUBKEY_PATH).sec() + xfp_and_path = mfp + struct.pack("<" + "I" * len(SP_SPEND_PATH_INTS), *SP_SPEND_PATH_INTS) + + # Witness UTXO locked to fake_B_spend + SP_TWEAK*G so tweak math itself passes + xonly = _sim_compute_spend_output_xonly(sim_exec, sim_execfile, fake_B_spend, SP_TWEAK) + inp = psbt.inputs[0] + amount_bytes = inp.witness_utxo[:8] + inp.witness_utxo = amount_bytes + b"\x22\x51\x20" + xonly + inp.taproot_bip32_paths = {} + inp.taproot_internal_key = None + inp.sp_tweak = SP_TWEAK + inp.sp_spend_bip32_derivation = {fake_B_spend: xfp_and_path} + + psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) + start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) + with pytest.raises(CCProtoError, match="SP spend pubkey mismatch"): + end_sign(accept=True, finalize=False) + + # --------------------------------------------------------------------------- # Handle failure test # --------------------------------------------------------------------------- @@ -592,6 +697,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) with pytest.raises(CCProtoError, match="SP_V0_INFO wrong size"): end_sign(accept=True, finalize=False) @@ -606,6 +712,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) with pytest.raises(CCProtoError, match="Silent payments require SIGHASH_ALL"): end_sign(accept=True, finalize=False) @@ -624,6 +731,7 @@ def sp_hacker(psbt): psbt_bytes = fake_txn(1, 1, xp, addr_fmt="p2wpkh", psbt_v2=False, psbt_hacker=sp_hacker) start_sign(psbt_bytes) + time.sleep(SIMULATOR_DELAY) with pytest.raises(CCProtoError, match="Missing outpoint for silent payment input"): end_sign(accept=True, finalize=False) @@ -633,27 +741,37 @@ def sp_hacker(psbt): # --------------------------------------------------------------------------- -def test_sp_spend_to_labeled_change_address(dev, fake_txn, start_sign, end_sign, cap_story, sim_exec, sim_execfile): +def test_sp_spend_labeled_change_detected(dev, fake_txn, start_sign, end_sign, cap_story, sim_exec, sim_execfile): """Two SP outputs: output 0 is a send (no label), output 1 is change (label=0 per BIP-352 convention).""" xp = dev.master_xpub root = BIP32Node.from_wallet_key(simulator_fixed_tprv) - sim_scan_pub = root.subkey_for_path("352h/1h/0h/1h/0").sec() - sim_spend_pub = root.subkey_for_path("352h/1h/0h/0h/0").sec() + scan_node = root.subkey_for_path(SP_SCAN_PATH) + scan_pub = scan_node.sec() + spend_pub = root.subkey_for_path(SP_SPEND_PATH).sec() + labeled_spend_pub = _sim_apply_label_to_spend_key(sim_exec, sim_execfile, spend_pub, scan_node.privkey(), 0) def sp_hacker(psbt): + _setup_sp_spend_input( + sim_exec, + sim_execfile, + psbt.inputs[0], + SP_TWEAK, + SP_SPEND_PATH, + SP_SPEND_PATH_INTS, + ) _add_sp_outputs( psbt, [ (0, SCAN_KEY, SPEND_KEY_B), - (1, sim_scan_pub, sim_spend_pub), + (1, scan_pub, labeled_spend_pub), ], ) 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) + psbt_bytes = fake_txn(1, 2, xp, addr_fmt="p2tr", psbt_v2=True, psbt_hacker=sp_hacker) start_sign(psbt_bytes) - + time.sleep(SIMULATOR_DELAY) title, story = cap_story() assert title == "OK TO SEND?" assert "not well understood script" not in story @@ -663,9 +781,74 @@ def sp_hacker(psbt): signed = end_sign(accept=True, finalize=False) tp = BasicPSBT().parse(signed) assert SCAN_KEY in tp.sp_global_ecdh_shares and SCAN_KEY in tp.sp_global_dleq_proofs - assert sim_scan_pub in tp.sp_global_ecdh_shares and sim_scan_pub in tp.sp_global_dleq_proofs + assert scan_pub in tp.sp_global_ecdh_shares and scan_pub in tp.sp_global_dleq_proofs _verify_sp_outputs(sim_exec, sim_execfile, tp, SCAN_KEY, [SPEND_KEY_B]) - _verify_sp_outputs(sim_exec, sim_execfile, tp, sim_scan_pub, [sim_spend_pub]) + _verify_sp_outputs(sim_exec, sim_execfile, tp, scan_pub, [labeled_spend_pub]) + _verify_signatures(sim_exec, sim_execfile, tp, signed) + + +def test_cosigner_change_output_is_allowed_with_warning(dev, fake_txn, start_sign, end_sign, cap_story, sim_exec, sim_execfile): + """label=0 with foreign keys must NOT raise fraud in a multi-signer flow. + + This tests bypasses the normal "coverage incomplete" workflow to simulate this scenario: + When foreign inputs are present, a label=0 SP output + whose scan/spend keys don't belong to this device is treated as a cosigner's change + address and rendered as a normal SP recipient — not marked as change, displays a warning. + """ + xp = dev.master_xpub + + # Fixed deterministic foreign key so the test is reproducible + foreign_mk = BIP32Node.from_master_secret(bytes(range(32))) + foreign_key = foreign_mk.subkey_for_path("0/0") + foreign_pub = foreign_key.sec() + foreign_xfp = foreign_mk.fingerprint() + foreign_priv = foreign_key.privkey() + + # Pre-compute real ECDH shares and DLEQ proofs for the foreign input. + # These must be valid: after preview sets output scripts, _validate_ecdh_coverage + # re-runs and verifies per-input DLEQ proofs. + foreign_shares = { + scan_key: _sim_compute_ecdh_share(sim_exec, sim_execfile, foreign_priv, scan_key) + for scan_key in (SCAN_KEY, SCAN_KEY_2) + } + foreign_proofs = { + scan_key: _sim_generate_dleq_proof(sim_exec, sim_execfile, foreign_priv, scan_key) + for scan_key in (SCAN_KEY, SCAN_KEY_2) + } + + def sp_hacker(psbt): + # Output 0: normal SP send (no label, third-party recipient) + # Output 1: cosigner's SP change (label=0, keys are NOT this device's) + _add_sp_outputs(psbt, [ + (0, SCAN_KEY, SPEND_KEY_B), + (1, SCAN_KEY_2, SPEND_KEY_A), + ]) + psbt.outputs[1].sp_v0_label = b"\x00\x00\x00\x00" + + # Make input 0 foreign: replace the random bip32 entry with the fixed unknown key. + # The firmware will fail to derive a privkey for this XFP → all_sp_inputs_ours=False. + psbt.inputs[0].bip32_paths = {} + psbt.inputs[0].bip32_paths[foreign_pub] = foreign_xfp + struct.pack(' Date: Wed, 3 Jun 2026 18:53:12 -0400 Subject: [PATCH 21/32] fix (SP): apply sp output sort correctly when scan key is the same for multiple outputs Per BIP-375: sort by spend key then output index in ascending order for each scan key group to determine ordering of the k value - Fix compute_silent_payment_output_scripts to match spec - Update bip375_test_vectors.json to version 1.1.1 --- shared/silentpayments.py | 32 ++++++++------- testing/bip375_test_vectors.json | 41 +++++++++++-------- testing/devtest/unit_silentpayments.py | 19 +++------ testing/test_bip375_vectors.py | 56 ++++++++------------------ 4 files changed, 64 insertions(+), 84 deletions(-) diff --git a/shared/silentpayments.py b/shared/silentpayments.py index 4bfee85d..e1b50960 100644 --- a/shared/silentpayments.py +++ b/shared/silentpayments.py @@ -4,16 +4,17 @@ # # Consolidates cryptographic primitives and PSBT handling logic for Silent Payments # + import chains import ngu import stash -from glob import settings from dleq import generate_dleq_proof, verify_dleq_proof from exceptions import FatalPSBTIssue, FraudulentChangeOutput +from glob import settings from precomp_tag_hash import ( + BIP352_INPUTS_TAG_H, BIP352_LABEL_TAG_H, BIP352_SHARED_SECRET_TAG_H, - BIP352_INPUTS_TAG_H, TAP_TWEAK_H, ) from serializations import SIGHASH_ALL, SIGHASH_DEFAULT @@ -57,6 +58,7 @@ def sp_derive_path(coin_type, account): # Silent Payments Cryptographic Primitives # ----------------------------------------------------------------------------- + def _apply_label_to_spend_key(B_spend_bytes, b_scan_bytes, label): """ Apply BIP-352 label tweak to spend key @@ -71,7 +73,7 @@ def _apply_label_to_spend_key(B_spend_bytes, b_scan_bytes, label): Returns: bytes: Labeled spend public key (33-byte compressed) """ - msg = b_scan_bytes + label.to_bytes(4, 'big') + msg = b_scan_bytes + label.to_bytes(4, "big") label_tweak_bytes = ngu.hash.sha256t(BIP352_LABEL_TAG_H, msg, True) label_tweak_point = ngu.secp256k1.ec_pubkey_tweak_mul(G, label_tweak_bytes) return ngu.secp256k1.ec_pubkey_combine([B_spend_bytes, label_tweak_point]) @@ -374,11 +376,9 @@ def validate_silent_payment_inputs(self, sv): if xfp_path[0] == self.my_xfp: account = sp_path[2] & ~0x80000000 if account not in candidate_accounts: - raise FatalPSBTIssue( - "Input #%d: SP spend account not recognized on this device" % i) + raise FatalPSBTIssue("Input #%d: SP spend account not recognized on this device" % i) - spend_node = sv.derive_path( - sp_derive_path(coin_type, account) + "/0h/0", register=False) + spend_node = sv.derive_path(sp_derive_path(coin_type, account) + "/0h/0", register=False) derived = spend_node.pubkey() del spend_node if derived != B_spend: @@ -731,15 +731,17 @@ def _compute_silent_payment_output_scripts(self): """ outpoints = self._get_outpoints() + # BIP-375: sort by spend key then output index in ascending order for each scan key group to determine + # ordering of the k value + sp_outputs = [(outp.sp_v0_info, out_idx, outp) for out_idx, outp in enumerate(self.outputs) if outp.sp_v0_info] + sp_outputs.sort(key=lambda t: (t[0], t[1])) + # 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] + for sp_info, out_idx, outp in sp_outputs: + scan_key = sp_info[:33] + B_spend = sp_info[33:66] k = scan_key_k.get(scan_key, 0) ecdh_share, summed_pubkey = self._get_ecdh_and_pubkey(scan_key) @@ -810,7 +812,7 @@ def _detect_sp_change_outputs(self, sv): if int.from_bytes(outp.sp_v0_label, "little") != 0: continue - scan_pub = outp.sp_v0_info[:33] + scan_pub = outp.sp_v0_info[:33] spend_pub = outp.sp_v0_info[33:66] matched = False @@ -831,7 +833,7 @@ def _detect_sp_change_outputs(self, sv): def _get_candidate_sp_accounts(self): """Get all known account numbers from device settings, always include account 0""" seen = {0} - for _af, acct_num in settings.get('accts', []): + for _af, acct_num in settings.get("accts", []): if acct_num: seen.add(acct_num) return sorted(seen) diff --git a/testing/bip375_test_vectors.json b/testing/bip375_test_vectors.json index 3f16f51b..922eeebe 100644 --- a/testing/bip375_test_vectors.json +++ b/testing/bip375_test_vectors.json @@ -1,6 +1,6 @@ { "description": "BIP-375 Test Vectors", - "version": "1.1", + "version": "1.1.1", "notes": [ "Generated by https://github.com/macgyver13/bip375-test-generator/", "Each vector includes: base64-encoded psbt and description", @@ -904,7 +904,7 @@ { "output_index": 0, "amount": 100000, - "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe0680361e1b1e9de5e42cb2007f7ca54b9e0d57ed13938fad56d3f19e57513a8fce039", + "sp_v0_info": "027a487fc19fb769877b8742d6ea18118f3c4e72b1ea8c6de602a7ad4a41dbe06803ea8f228be2b716dacdefcb2810072874f00a476f5c07bd051917126cc64b3def", "sp_v0_label": 1, "script": "51202268cf529959f30ac8f7a5471ca4c67536a51f0e5b168af03eed4715edcc72e0" }, @@ -918,7 +918,7 @@ }, { "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", + "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/zecNmih9U74ybAAEDCDDmAgAAAAAAAQQiUSDkKhgUiNj/9ucWH3FfXvD0fcbISAhIqtaERxvz6ljzUgEJQgJfDsGzjZmEp+ti8VvkAwIEEPFC7fTWnjY/VhWv0qa2zQLOCzIZpapd+PIc45PpYs3afVFUbaTTXDjtAYilludg8wEKBAAAAAAA", "supplementary": { "inputs": [ { @@ -958,16 +958,16 @@ { "output_index": 1, "amount": 190000, - "sp_v0_info": "025f0ec1b38d9984a7eb62f15be403020410f142edf4d69e363f5615afd2a6b6cd037cd462358c69c40525247a8793944d9e67484b248fd84a1e7ee4f920b4da41b5", + "sp_v0_info": "025f0ec1b38d9984a7eb62f15be403020410f142edf4d69e363f5615afd2a6b6cd02ce0b3219a5aa5df8f21ce393e962cdda7d51546da4d35c38ed0188a596e760f3", "sp_v0_label": 0, - "script": "5120761dbd34929884a66729ebdcdbacf167482df62c14f098e4ef18d70fe06aefd0" + "script": "5120e42a181488d8fff6e7161f715f5ef0f47dc6c8480848aad684471bf3ea58f352" } ] } }, { - "description": "can finalize: two sp outputs - output 0 uses label=3 / output 1 uses label=1", - "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQECAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR/gkwQAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCICJa45kPVF365uoUKpN0arSqgSfjyBNY63wha08anrtiAiBl42BSGRArJQdeOVXz80zjvPKjXR4QFd1dw7/tPs81qQEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWghAlWBv6pzMVb2uY5xom9w/XazZ4ZJ9isfxed7jpq45hBQIh4Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWhAeUCBnrbp8ljBWDh8CmTMFvUXx+9ECRIszqqU9OL/U3bEJHv8Fw2bJjsOPSBrZoyH6Us03ur6AKF5KwR614h1uwABAwighgEAAAAAAAEEIlEgyvcy9B51UZCclKM10VeOp7dmAnuIbN0cFwHZv8a3hIgBCUICi3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWgDliRhtuNd91TQDUhtw53swgJxhFB2YU5cDtLJDyPT6b4BCgQDAAAAAAEDCDDmAgAAAAAAAQQiUSBNB6DTsPYafF7+NsLDxYfItphBhUYAjPgZu9pM4xgSIQEJQgKLeQ4Jh/3p8NEjmOtr4wxzdnk8ZHhloqAvLvyWDZZlaANaVzfUxngmF8xrDElrsrxiVVoyslQkT7ycnANd9GnlqwEKBAEAAAAA", + "description": "can finalize: three sp outputs (same scan key) - output 0 uses label=1, outputs 1 and 2 uses label=2 (same spend key)", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEAQEBBQEDAQYBAAABDiAYpxdmOwurFLEqGncTI/8eQHndUy5d0T4o6hCBxwCYSgEPBAAAAAABAR+AGgYAAAAAABYAFCKactNKZFvTSWu79Qu7gckGP0+UIgICyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL9HMEQCIFaplAuaU3aICoTcvxrXSZnHH6i4SXpbYSuzopSyB0k4AiA98iUFQysKS0I3s2sHC3R8ZpycvK7JZiE0PcjMfiNnYgEBAwQBAAAAIgYCyBe7dSGvw16pbzv7Jw5utQ3f+lVgYnuWH+wA8pllCL8IAAAAgAAAAAABEAT+////Ih0Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWghAlWBv6pzMVb2uY5xom9w/XazZ4ZJ9isfxed7jpq45hBQIh4Ci3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWhAeUCBnrbp8ljBWDh8CmTMFvUXx+9ECRIszqqU9OL/U3bEJHv8Fw2bJjsOPSBrZoyH6Us03ur6AKF5KwR614h1uwABAwihhgEAAAAAAAEEIlEgG/+hD9qiUCwvDJKF/grxmlD4UV47bfv1behQ2ZkBRAABCUICi3kOCYf96fDRI5jra+MMc3Z5PGR4ZaKgLy78lg2WZWgD1DFYje5hqzo5clspnDdC3Gaghh3s9ZG0I7DlJwcPgjQBCgQBAAAAAAEDCDLmAgAAAAAAAQQiUSCITaZwjIK5tWyYvTvm5fU4lLsb/ZC/rPiUAlVw9r1TBQEJQgKLeQ4Jh/3p8NEjmOtr4wxzdnk8ZHhloqAvLvyWDZZlaAMhk60Mh1+sUkNzgCKQCnckxhdd2smUQWWqtFeq35xPPAEKBAIAAAAAAQMIooYBAAAAAAABBCJRINKR0zkoDgyG4scIpZH46OL4ktEqfP6FELZneZtxExyGAQlCAot5DgmH/enw0SOY62vjDHN2eTxkeGWioC8u/JYNlmVoAyGTrQyHX6xSQ3OAIpAKdyTGF13ayZRBZaq0V6rfnE88AQoEAgAAAAA=", "supplementary": { "inputs": [ { @@ -977,8 +977,8 @@ "prevout_txid": "18a717663b0bab14b12a1a771323ff1e4079dd532e5dd13e28ea1081c700984a", "prevout_index": 0, "prevout_scriptpubkey": "0014229a72d34a645bd3496bbbf50bbb81c9063f4f94", - "amount": 300000, - "witness_utxo": "e093040000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", + "amount": 400000, + "witness_utxo": "801a060000000000160014229a72d34a645bd3496bbbf50bbb81c9063f4f94", "sequence": 4294967294, "signed": true } @@ -994,17 +994,24 @@ "outputs": [ { "output_index": 0, - "amount": 100000, - "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", - "sp_v0_label": 3, - "script": "5120caf732f41e7551909c94a335d1578ea7b766027b886cdd1c1701d9bfc6b78488" + "amount": 100001, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803d431588dee61ab3a39725b299c3742dc66a0861decf591b423b0e527070f8234", + "sp_v0_label": 1, + "script": "51201bffa10fdaa2502c2f0c9285fe0af19a50f8515e3b6dfbf56de850d999014400" }, { "output_index": 1, - "amount": 190000, - "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d96656803b44347335c63f505e55e268ab280f318b4a6aa14492ff379c3668a1f54ef8c9b", - "sp_v0_label": 1, - "script": "51204d07a0d3b0f61a7c5efe36c2c3c587c8b698418546008cf819bbda4ce3181221" + "amount": 190002, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d966568032193ad0c875fac5243738022900a7724c6175ddac9944165aab457aadf9c4f3c", + "sp_v0_label": 2, + "script": "5120884da6708c82b9b56c98bd3be6e5f53894bb1bfd90bfacf894025570f6bd5305" + }, + { + "output_index": 2, + "amount": 100002, + "sp_v0_info": "028b790e0987fde9f0d12398eb6be30c7376793c647865a2a02f2efc960d966568032193ad0c875fac5243738022900a7724c6175ddac9944165aab457aadf9c4f3c", + "sp_v0_label": 2, + "script": "5120d291d339280e0c86e2c708a591f8e8e2f892d12a7cfe8510b667799b71131c86" } ] } diff --git a/testing/devtest/unit_silentpayments.py b/testing/devtest/unit_silentpayments.py index 58130bdf..1a2078c7 100644 --- a/testing/devtest/unit_silentpayments.py +++ b/testing/devtest/unit_silentpayments.py @@ -61,8 +61,6 @@ # ECDH share computation ecdh_share = _compute_ecdh_share(TEST_PRIVKEY, TEST_SCAN_KEY) -assert len(ecdh_share) == 33 -assert ecdh_share[0] in (0x02, 0x03) # Input hash computation (order-independent) outpoints = [ @@ -77,8 +75,6 @@ ] 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) @@ -118,8 +114,6 @@ pubkey = ngu.secp256k1.ec_pubkey_tweak_mul(G, TEST_PRIVKEY) proof = generate_dleq_proof(TEST_PRIVKEY, TEST_SCAN_KEY) -assert len(proof) == 64 -assert verify_dleq_proof(pubkey, TEST_SCAN_KEY, ecdh_share, proof) # Tampered proof rejected tampered = bytearray(proof) @@ -200,11 +194,8 @@ # Spending Privkey # --------------------------------------------------------------------------- -result = compute_silent_payment_spending_privkey((1).to_bytes(32, "big"), (42).to_bytes(32, "big")) -assert isinstance(result, bytes) and len(result) == 32 -assert result != (1).to_bytes(32, "big") -assert 0 < int.from_bytes(result, "big") < ngu.secp256k1.curve_order_int() - +# Valid computation is value-tested by BIP-352 receive vectors; only the +# zero-input error path is unique to this unit test. try: compute_silent_payment_spending_privkey(b"\x00" * 32, b"\x00" * 32) assert False, "Should raise" @@ -522,6 +513,7 @@ def _make_sp_output(scan_key): expected = _compute_silent_payment_output_script(outpoints, summed_pk, combined_share, TEST_SPEND_KEY, 0) assert outp.script == expected +# BIP-375: order by spend key when to determine k order when scan key is the same # 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") @@ -541,9 +533,10 @@ def _make_sp_output(scan_key): ops = [(b"\x01" * 32, b"\x00\x00\x00\x00")] spk_sum = _combine_pubkeys([pk]) +# spend keys sorted ascending: a (022f..) < c (0279..) < b (02c6..) 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 sp4.script == _compute_silent_payment_output_script(ops, spk_sum, es, spend_key_c, k=1) +assert sp2.script == _compute_silent_payment_output_script(ops, spk_sum, es, spend_key_b, k=2) assert reg1.script is None assert reg3.script is None diff --git a/testing/test_bip375_vectors.py b/testing/test_bip375_vectors.py index 0f6f9d58..b1944d97 100644 --- a/testing/test_bip375_vectors.py +++ b/testing/test_bip375_vectors.py @@ -90,12 +90,8 @@ def _get(self, substr): ("k values assigned to wrong output indices", "output script mismatch"), ] - @pytest.mark.parametrize( - "desc,expected_err", _SIM_INVALID, ids=[d for d, _ in _SIM_INVALID] - ) - def test_invalid_vectors_sim_validate_sp( - self, sim_exec, sim_execfile, desc, expected_err - ): + @pytest.mark.parametrize("desc,expected_err", _SIM_INVALID, ids=[d for d, _ in _SIM_INVALID]) + def test_invalid_vectors_sim_validate_sp(self, sim_exec, sim_execfile, desc, expected_err): p = BasicPSBT().parse(self._get(desc)["psbt"].encode()) err = _sim_validate_sp(sim_exec, sim_execfile, p) assert err, "Expected validation failure for: %s" % desc @@ -121,24 +117,15 @@ def test_valid_ecdh_computation(self, sim_exec, sim_execfile): eligible_privkeys = [ unhexlify(inp["private_key"]) for inp in vec["supplementary"]["inputs"] - if inp["private_key"] - and _sim_pubkey_from_input( - sim_exec, sim_execfile, p, inp["input_index"] - ) + if inp["private_key"] and _sim_pubkey_from_input(sim_exec, sim_execfile, p, inp["input_index"]) ] for expected in vec["supplementary"]["sp_proofs"]: scan_key = unhexlify(expected["scan_key"]) expected_ecdh = unhexlify(expected["ecdh_share"]) if "input_index" in expected: - ik = next( - k - for k in vec["supplementary"]["inputs"] - if k["input_index"] == expected["input_index"] - ) + ik = next(k for k in vec["supplementary"]["inputs"] if k["input_index"] == expected["input_index"]) privkey = unhexlify(ik["private_key"]) - actual = _sim_compute_ecdh_share( - sim_exec, sim_execfile, privkey, scan_key - ) + actual = _sim_compute_ecdh_share(sim_exec, sim_execfile, privkey, scan_key) else: shares = [ _sim_compute_ecdh_share( @@ -150,9 +137,9 @@ def test_valid_ecdh_computation(self, sim_exec, sim_execfile): for privkey in eligible_privkeys ] actual = _sim_combine_pubkeys(sim_exec, sim_execfile, shares) - assert actual == expected_ecdh, ( - "ECDH mismatch in valid[%d] for scan_key %s" - % (vi, expected["scan_key"][:16]) + assert actual == expected_ecdh, "ECDH mismatch in valid[%d] for scan_key %s" % ( + vi, + expected["scan_key"][:16], ) def test_valid_dleq_verification(self, sim_exec, sim_execfile): @@ -162,12 +149,8 @@ def test_valid_dleq_verification(self, sim_exec, sim_execfile): 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 - ) + 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, @@ -185,9 +168,7 @@ def test_valid_dleq_verification(self, sim_exec, sim_execfile): 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) - ) + assert ecdh_share is not None, "valid[%d] inp[%d]: ECDH share missing" % (vi, inp_idx) _sim_verify_dleq( sim_exec, sim_execfile, @@ -202,18 +183,17 @@ def test_valid_output_script_derivation(self, sim_exec, sim_execfile): vec = self.VALID_VECTORS[vi] p = BasicPSBT().parse(vec["psbt"].encode()) sp_outputs = [ - (o.sp_v0_info[:33], o.sp_v0_info[33:66], o.script) - for o in p.outputs + (o.sp_v0_info[:33], o.sp_v0_info[33:66], out_index, o.script) + for out_index, o in enumerate(p.outputs) if o.sp_v0_info and len(o.sp_v0_info) == 66 ] if not sp_outputs: continue + sp_outputs.sort(key=lambda t: (t[0], t[1], t[2])) outpoints = _sim_get_outpoints(sim_exec, sim_execfile, p) scan_key_k = {} - for scan_key, spend_key, script in sp_outputs: - ecdh_share, summed_pubkey = _sim_get_ecdh_and_pubkey( - sim_exec, sim_execfile, p, scan_key - ) + for scan_key, spend_key, _oi, script in sp_outputs: + ecdh_share, summed_pubkey = _sim_get_ecdh_and_pubkey(sim_exec, sim_execfile, p, scan_key) if not ecdh_share: continue if not script: # If script is missing, skip vectors test missing script @@ -230,6 +210,4 @@ def test_valid_output_script_derivation(self, sim_exec, sim_execfile): spend_key, k, ) - assert script == expected, ( - "valid[%d]: output script mismatch at k=%d" % (vi, k) - ) + assert script == expected, "valid[%d]: output script mismatch at k=%d" % (vi, k) From 812d7c6bf6fb1f54c407afd37c206c54b090637d Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:58:47 -0400 Subject: [PATCH 22/32] feat (SP): hide export finalize txn option for silent payment outputs Discourage broadcasting silent payments directly from the device. The wallet coordinator should be responsible for verifying the PSBT and finalizing for broadcast. - Users can restore the default ColdCard behavior by using the Danger Zone -> SP Final Export setting. - USB API was not modified. --- shared/auth.py | 27 +++++++++-- shared/flow.py | 8 +++ shared/nvstore.py | 1 + testing/test_silentpayments.py | 89 ++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 5 deletions(-) diff --git a/shared/auth.py b/shared/auth.py index cfaf64ab..62080652 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -30,6 +30,10 @@ TXN_INPUT_OFFSET = 0 TXN_OUTPUT_OFFSET = MAX_TXN_LEN +SP_EXPORT_NOTE = '''\ +Silent Payment output detected - finalize signed PSBT on wallet coordinator to broadcast. +Direct broadcast requires opt-in via Danger Zone > SP Final Export.''' + class UserAuthorizedAction: active_request = None @@ -855,6 +859,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None, # User authorized PSBT for signing, and we added signatures. # - allow PushTX if enabled (first thing) # - can save final TXN out to SD card/VirtDisk, share by NFC, QR. + # - silent payments should discourage save final txn by default from glob import PSRAM, hsm_active from sffile import SFFile @@ -866,9 +871,15 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None, base_title = "PSBT " + ("Signed" if psbt.sig_added else "Updated") is_complete = psbt.is_complete() + sp_export_hint = None if finalize is not None: # USB case - user can choose whether to attempt finalization is_complete = finalize + elif is_complete and psbt.has_silent_payment_outputs(): + from glob import settings + if not settings.get('spfin', False): + is_complete = False + sp_export_hint = SP_EXPORT_NOTE with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as psram: if is_complete: @@ -876,7 +887,9 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None, noun = "Finalized TX ready for broadcast" else: psbt.serialize(psram) - noun = "Partly Signed PSBT" + # not finalizing: either genuinely partial (needs more sigs) or an SP tx we + # kept as PSBT on purpose - only the latter (sp_export_hint) is fully signed. + noun = "Signed PSBT" if sp_export_hint else "Partly Signed PSBT" txid = None data_len = psram.tell() @@ -991,13 +1004,14 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None, # typical case: save to SD card, show filenames we used assert isinstance(ch, dict) msg = await _save_to_disk(psbt, txid, ch, is_complete, data_len, - output_encoder, filename) + output_encoder, filename, sp_export_hint) input_method = None first_time = False title = base_title -async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, filename=None): +async def _save_to_disk(psbt, txid, save_options, is_complete, data_len, output_encoder, + filename=None, sp_note=None): # Saving a PSBT from PSRAM to something disk-like. # - handle save-to-SD/VirtDisk cases. With re-attempt when no card, etc. assert isinstance(save_options, dict) # from import_export_prompt @@ -1033,7 +1047,7 @@ def _chunk_write(file_d, ofs, chunk=4096): while 1: # try to put back into same spot, but also do top-of-card - if not is_complete: + if not is_complete and not sp_note: # keep the filename under control during multiple passes target_fname = base.replace('-part', '') + '-part.psbt' else: @@ -1113,7 +1127,7 @@ def _chunk_write(file_d, ofs, chunk=4096): # Done, show the filenames we used. if out_fn: msg = "Updated PSBT is:\n\n%s" % out_fn - if out2_fn: + if out2_fn or sp_note: msg += '\n\n' else: # del_after is probably set @@ -1121,6 +1135,9 @@ def _chunk_write(file_d, ofs, chunk=4096): if out2_fn: msg += 'Finalized transaction (ready for broadcast):\n\n%s' % out2_fn + elif sp_note: + # in place of the finalized-txn line: explain why it was not produced + msg += sp_note return msg diff --git a/shared/flow.py b/shared/flow.py index 0e50f32e..af93b90c 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -357,6 +357,14 @@ async def goto_home(*a): warnings, funds can be stolen by specially crafted PSBT or MitM. Keep blocked unless you intend to sign special transactions.'''), + ToggleMenuItem("SP Final Export", "spfin", ["Default: PSBT", "Allow Final"], + story='''\ +Silent Payment outputs are computed on-device. A coordinator requires a signed \ +PSBT to verify the computation. By default, Silent Payment transactions \ +export as PSBT for the coordinator to finalize. + +Enable only if you intend to broadcast Silent Payment \ +transactions directly from this device.'''), ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet4', 'Regtest'], value_map=['BTC', 'XTN', 'XRT'], on_change=change_which_chain, diff --git a/shared/nvstore.py b/shared/nvstore.py index 2c543bcd..a2dcc7fb 100644 --- a/shared/nvstore.py +++ b/shared/nvstore.py @@ -70,6 +70,7 @@ # sssp = (complex) If present, a (single signer) spending-policy is defined (maybe disabled) # lfr = (string) If present, the reason why Spending Policy blocked last transaction # wifs = (list) List of tuples (public/private key) +# spfin = (bool) allow finalized/broadcast export of Silent Payment txns (default: PSBT only) # Stored w/ key=00 for access before login # _skip_pin = hard code a PIN value (dangerous, only for debug) diff --git a/testing/test_silentpayments.py b/testing/test_silentpayments.py index fc95db60..5d830495 100644 --- a/testing/test_silentpayments.py +++ b/testing/test_silentpayments.py @@ -871,3 +871,92 @@ def sp_hacker(psbt): time.sleep(SIMULATOR_DELAY) with pytest.raises(CCProtoError, match="SP change output keys do not match this device"): end_sign(accept=True, finalize=False) + + +# --------------------------------------------------------------------------- +# Finalized/broadcast export policy for SP-output transactions +# --------------------------------------------------------------------------- + + +def _one_sp_output_psbt(dev, fake_txn): + def sp_hacker(psbt): + _add_sp_outputs(psbt, [(0, SCAN_KEY, SPEND_KEY_B)]) + return fake_txn(1, 1, dev.master_xpub, addr_fmt="p2wpkh", psbt_v2=True, psbt_hacker=sp_hacker) + + +def _sp_sign_microsd(open_microsd, microsd_path, goto_home, pick_menu_item, + cap_story, need_keypress, psbt_bytes): + # Sign an SP-output PSBT via the air-gapped microSD flow (finalize=None path), + # approve, and let the first-pass auto-echo save to SD. Returns the save-summary + # story and the files that ended up on the card. + import os + from glob import glob as _glob + + name = 'sptest' + for f in _glob(microsd_path(name + '*')): + os.remove(f) + with open_microsd(name + '.psbt', 'wb') as sd: + sd.write(psbt_bytes) + + goto_home() + pick_menu_item('Ready To Sign') + time.sleep(0.1) + title, story = cap_story() + if 'OK TO SEND' not in title: + pick_menu_item(name + '.psbt') + time.sleep(0.1) + title, story = cap_story() + assert title == 'OK TO SEND?' + need_keypress('y') + + time.sleep(0.1) + title, story = cap_story() + assert 'Updated PSBT' in story + + txn = _glob(microsd_path(name + '*-final.txn')) + signed = _glob(microsd_path(name + '*-signed.psbt')) + part = _glob(microsd_path(name + '*-part.psbt')) + return story, txn, (signed or part) + + +def test_sp_export_defaults_to_psbt(dev, fake_txn, settings_remove, open_microsd, + microsd_path, goto_home, pick_menu_item, cap_story, need_keypress): + # Default (spfin unset): SP-output tx exports the signed PSBT, no broadcast tx, + # and the policy note replaces the finalized-transaction line. + settings_remove('spfin') + psbt_bytes = _one_sp_output_psbt(dev, fake_txn) + + story, txn, psbt_out = _sp_sign_microsd(open_microsd, microsd_path, goto_home, + pick_menu_item, cap_story, need_keypress, psbt_bytes) + + assert not txn, "no finalized broadcast tx by default" + assert psbt_out and 'signed.psbt' in psbt_out[0] + assert 'Finalized transaction' not in story + assert 'SP Final Export' in story, "policy note should be shown" + + tp = BasicPSBT().parse(open(psbt_out[0], 'rb').read()) + assert any(o.sp_v0_info for o in tp.outputs), "PSBT retains SP metadata for verification" + + +def test_sp_export_final_allowed(dev, fake_txn, settings_set, settings_remove, open_microsd, + microsd_path, goto_home, pick_menu_item, cap_story, need_keypress): + # With the opt-in setting, the current finalized-export behavior is restored. + settings_set('spfin', 1) + try: + psbt_bytes = _one_sp_output_psbt(dev, fake_txn) + story, txn, _ = _sp_sign_microsd(open_microsd, microsd_path, goto_home, + pick_menu_item, cap_story, need_keypress, psbt_bytes) + assert txn, "finalized broadcast tx written when allowed" + assert 'Finalized transaction' in story + assert 'SP Final Export' not in story + finally: + settings_remove('spfin') + + +def test_sp_export_usb_finalize_unchanged(dev, fake_txn, start_sign, end_sign, settings_remove): + # USB STXN_FINALIZE is honored despite SP outputs and spfin unset (USB path untouched). + settings_remove('spfin') + psbt_bytes = _one_sp_output_psbt(dev, fake_txn) + start_sign(psbt_bytes, finalize=True) + time.sleep(SIMULATOR_DELAY) + end_sign(accept=True, finalize=True) # asserts a non-PSBT finalized tx is returned From fad381dd18fb609a573dbe7389e2696668042bc6 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:02:03 -0400 Subject: [PATCH 23/32] bump ckcc submodule sp partial fields --- external/ckcc-protocol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/ckcc-protocol b/external/ckcc-protocol index da36586b..59c7a961 160000 --- a/external/ckcc-protocol +++ b/external/ckcc-protocol @@ -1 +1 @@ -Subproject commit da36586bdb55ae7c98d7b98bc0f8b50ce996259c +Subproject commit 59c7a961c70b0dc7d0d279973339fb5cb7cb879a From e8d1fafd6659761372cd90faf494b532661ea415 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:32:54 -0400 Subject: [PATCH 24/32] feat (MuSig2 + SP): partial field parse & serialize plumbing Adds provisional SP_PARTIAL_* fields. --- shared/psbt.py | 27 ++++++++++++++++--- testing/data/desc-musig-sp-demo.txt | 1 + testing/data/musig2-sp-cosigner-contrib.psbt | Bin 0 -> 3884 bytes testing/devtest/unit_silentpayments.py | 26 ++++++++++++++---- testing/psbt.py | 26 ++++++++++++++++++ 5 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 testing/data/desc-musig-sp-demo.txt create mode 100644 testing/data/musig2-sp-cosigner-contrib.psbt diff --git a/shared/psbt.py b/shared/psbt.py index 835db959..c0a7e449 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -45,6 +45,7 @@ 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, + PSBT_IN_SP_PARTIAL_ECDH_SHARE, PSBT_IN_SP_PARTIAL_DLEQ, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR, AF_P2WPKH, AF_CLASSIC, AF_P2WPKH_P2SH, AFC_SEGWIT, AF_BARE_PK ) @@ -666,7 +667,8 @@ class psbtInputProxy(psbtProxy): # just need to store a simple number for these # Silent Payments (shares/dleq): bytes (not proxied) so scan-pub can be used as dict key # and dict can be mutated in place - short_values = { PSBT_IN_SIGHASH_TYPE, PSBT_IN_SP_ECDH_SHARE, PSBT_IN_SP_DLEQ } + short_values = { PSBT_IN_SIGHASH_TYPE, PSBT_IN_SP_ECDH_SHARE, PSBT_IN_SP_DLEQ, + PSBT_IN_SP_PARTIAL_ECDH_SHARE, PSBT_IN_SP_PARTIAL_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, @@ -683,6 +685,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_partial_ecdh_shares', 'sp_partial_dleq_proofs', 'sp_ecdh_shares', 'sp_dleq_proofs', 'sp_tweak', 'sp_spend_bip32_derivation', ) @@ -732,10 +735,14 @@ def __init__(self, fd, idx): #self.musig_part_sigs = None # === silent payments === - #self.sp_ecdh_shares = None # dict[scan-pub] = ecdh_share - #self.sp_dleq_proofs = None # dict[scan-pub] = dleq_proof + #self.sp_ecdh_shares = None # dict[scan-key] = ecdh_share + #self.sp_dleq_proofs = None # dict[scan-key] = dleq_proof #self.sp_tweak = None #self.sp_spend_bip32_derivation = None # (spend-pub, xfp || path) + # key = scan_key_33 || participant_pk_33 + #self.sp_partial_ecdh_shares = None # dict[scan-key||part-pk] = ecdh_share + # key = scan_key_33 || participant_pk_33 + #self.sp_partial_dleq_proofs = None # dict[scan-key||part-pk] = dleq_proof self.parse(fd) @@ -1210,6 +1217,12 @@ def store(self, kt, key, val): self.sp_tweak = self.get(val) elif kt == PSBT_IN_SP_SPEND_BIP32_DERIVATION: self.sp_spend_bip32_derivation = (key, val) + elif kt == PSBT_IN_SP_PARTIAL_ECDH_SHARE: + self.sp_partial_ecdh_shares = self.sp_partial_ecdh_shares or {} + self.sp_partial_ecdh_shares[(key[:33], key[33:66])] = val + elif kt == PSBT_IN_SP_PARTIAL_DLEQ: + self.sp_partial_dleq_proofs = self.sp_partial_dleq_proofs or {} + self.sp_partial_dleq_proofs[(key[:33], key[33:66])] = val else: # including: PSBT_IN_FINAL_SCRIPTSIG, PSBT_IN_FINAL_SCRIPTWITNESS self.unknown = self.unknown or [] @@ -1321,6 +1334,14 @@ def serialize(self, out_fd, is_v2): k, v = self.sp_spend_bip32_derivation wr(PSBT_IN_SP_SPEND_BIP32_DERIVATION, v, k) + if self.sp_partial_ecdh_shares: + for (scan_key, participant_pk), share in self.sp_partial_ecdh_shares.items(): + wr(PSBT_IN_SP_PARTIAL_ECDH_SHARE, share, scan_key + participant_pk) + + if self.sp_partial_dleq_proofs: + for (scan_key, participant_pk), proof in self.sp_partial_dleq_proofs.items(): + wr(PSBT_IN_SP_PARTIAL_DLEQ, proof, scan_key + participant_pk) + if self.unknown: for k, v in self.unknown: wr(None, v, k) diff --git a/testing/data/desc-musig-sp-demo.txt b/testing/data/desc-musig-sp-demo.txt new file mode 100644 index 00000000..6ab47f53 --- /dev/null +++ b/testing/data/desc-musig-sp-demo.txt @@ -0,0 +1 @@ +tr(musig([0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez,[6ba6cfd0/48h/1h/0h/3h]tpubDFcrvj5n7gyazzxdg9k6uvzQsoQWow1xbksr7EvKPRBgUbwCdqu2qxyTJjYFNJ7MQLfdXSJV4n8xPZGtrvwQtEbktinC4EP3k8JN2hcBtz4,[747b698e/48h/1h/0h/3h]tpubDExj5FnaUnPAnoAge8edwn7qr8VdquTossNwPPPZVdGH3AZ3et8vVNnJXagqq18d4QWYoja9GeHfpVGZQjQAHozrSHK1HZtnF4XCE1RwE24)/0/*) \ No newline at end of file diff --git a/testing/data/musig2-sp-cosigner-contrib.psbt b/testing/data/musig2-sp-cosigner-contrib.psbt new file mode 100644 index 0000000000000000000000000000000000000000..717fe933d8f999e2a2240cb9526f19e53bbbb268 GIT binary patch literal 3884 zcmd56`M})?xAjB8?>-vK$koL~+LLx%ZEI@44?hPygKKeSXjT{`38Qmf!FFKEF?3uv^Fucnbyr zfk0r$w-*cy2E)Nz><0=0??tknR8Ib{1r~&{i$P$pQs4k`_X~tRgIu)GF>xH_O5-wK z^Lt0NewW?Kk3;o^&RCg?w$d$Nzn~C2)Xc6DL%Xk}oXJa#2@+3>?rQpe7GOQDRfrAm zIoowj03^8h%5k?Hl)X?0)jVkG(N~xc$EgAO(H2)J&B#6&(vp8h@j@P4$-%--4CGf# zXQ+WdH^A)0F1f+3yMaRJEpo~AhA)fKydODf@g3u#svP9vE&U*w7-1l`euFCvar7Q-Pjbyfy%o?3r)#6N#+BnA?M`{4e)4lIrY5{BLn zjf@DaoU2G_Qp#0Vw$#~5nlw$&Hz>y5{&Z=bOjnxabB6$-LxJpo0_ny{0rr3Z?Vw~A-D7RIM-We4TTH5r0jP$A#A{)DuZnp~!Eo7-L){7+5 zYqybM=-(N6RSL7)Y#KmJIiSHbm9NbBN|5q0@T=3<=h`{eb}U~0dBrI06fev5koE?| z&FSjd?1y7pzO8fOBGKvRhbu28K%U5lmZ!f*2RmFdz@Z@illt_PGVR@Vr z6d*THkaKO#x-l2#MGE!^6u}f;rM)}4S?q+WNbck-(CHAiX0lN(=R_Sp?a!5|Zd>JZ zupOrL7K0n@lNxwYQ<(cvdfxQ~uoh@uRXw**by)7$>YTXVYREEN8va>7FdT36_PxZY z8a09w6ksif#W07vz6w=jsc!4%tn4%9MpLe5E+{Bv?I%%w>8?bwQEH9s>V4XIBXnMU ztx;ILT=$dSgF(u|2cMiJb+XGB8EX!-$g7?z@ws4>29_6$;2+K^x_sp`S|oM8?{iCy~93x|T97@1O&Q?h&w zrcP>^A9!6@gTE54vk>9ga|Io*-md2ctOas}YE*?-c#$uhATeLBb&1@wRab+Wi1qXw z$d(Jl!8JHR0oF2|p6Dfgp_SU7+?eeA8iq+ZRqI1gA^L0HJVe^C>^EkktO^@FJN$rW zB*UcTjIeYO*7WK@T`5F2LdfRYf$bUtIcxh*@Ni6V_WpA2q}I)#R5;-pvKT_i8V@yzdy$Nrh#d;DiLA z6DwL4DfD(8^ey~M?4dV)8t%i|L!pS&^4ry;&YFG0Drx~X zCri+5HF$h#@uSF*LmTnW!Gn9v|jNB}x9iuC14>MU+&4n&u_2Hk7(x-rKr)OWhlzNE|5oh4?Tj;n;63{OP0u_G*m2cLIThU zSGA$cG(3xqjmv zyMMce7JYwZPtnI385LcwsXa@)giey zLK{62&QoD(O}h<|E&pl=c;X<-;>XMj@u=(L{qZ^aT&^Zmrwz? Date: Wed, 20 May 2026 19:18:58 -0400 Subject: [PATCH 25/32] feat (MuSig2 + SP): calculate stable nonce_msg using sp_v0_info Generate nonce message for SP outputs - required since scriptPubKey is not defined on musig round 1. taproot sighash computation remains unaffected for musig round 2. --- shared/psbt.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/shared/psbt.py b/shared/psbt.py index c0a7e449..3bc2ac07 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -2008,7 +2008,13 @@ def consider_outputs(self, len_pths, hard_p, prefix_pths, idx_max, cosign_xfp=No if self.session: if idx == 0: self.session.update(ser_compact_size(self.num_outputs)) - self.session.update(txo.serialize()) + sp_out = self.outputs[idx] + if sp_out.sp_v0_info: + # Use the stable value||sp_v0_info bytes so the session cache is identical + # in both rounds. See "MuSig2 Silent Payments: stable nonce_msg" for more details. + self.session.update(pack(' Date: Fri, 26 Jun 2026 13:09:41 -0400 Subject: [PATCH 26/32] feat (MuSig2 + SP): build musig reusable helpers --- shared/psbt.py | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/shared/psbt.py b/shared/psbt.py index 3bc2ac07..7ae5569f 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -2646,6 +2646,27 @@ def musig_derive_keyagg_cache(to_derive, agg_key, keyagg_cache): return agg + @staticmethod + def musig_build_cache(agg_k, participant_pks): + # Aggregate the participant keys and verify the result matches the enrolled + # aggregate key. Raises FatalPSBTIssue on mismatch + keyagg_cache = ngu.secp256k1.MusigKeyAggCache() + ngu.secp256k1.musig_pubkey_agg( + [ngu.secp256k1.pubkey(pk) for pk in participant_pks], keyagg_cache) + if keyagg_cache.agg_pubkey().to_bytes() != agg_k: + raise FatalPSBTIssue("musig keyagg mismatch") + return keyagg_cache + + def musig_taproot_tweak(self, inp, keyagg_cache, der_xonly): + # BIP-341 keyspend tweak applied to the keyagg cache. Caller decides whether a + # tweak applies (keyspend only, not tapscript leaf). Returns (output_key_33, tweak_32). + tweak_data = der_xonly + if inp.taproot_merkle_root: + tweak_data += self.get(inp.taproot_merkle_root) + tap_tweak = ngu.hash.sha256t(TAP_TWEAK_H, tweak_data, True) + output_key = ngu.secp256k1.musig_pubkey_xonly_tweak_add(keyagg_cache, tap_tweak) + return output_key.to_bytes(), tap_tweak + def musig_process_input(self, session, inp_idx, inp, keypair, agg_k, der_agg_k, digest, leaf_hash=b""): @@ -2661,15 +2682,7 @@ def musig_process_input(self, session, inp_idx, inp, keypair, agg_k, der_agg_k, musig_partial_sigs = inp.get_musig_part_sigs() musig_pubnonces = inp.get_musig_pubnonces() - keyagg_cache = ngu.secp256k1.MusigKeyAggCache() - - # below will sort, but should be already sorted in PSBT - ngu.secp256k1.musig_pubkey_agg( - [ngu.secp256k1.pubkey(pk) for pk in cosigners], - keyagg_cache - ) - # verify aggregate key is correct - assert keyagg_cache.agg_pubkey().to_bytes() == agg_k + keyagg_cache = self.musig_build_cache(agg_k, cosigners) musig_index = None # index of musig expression in key list for i, k in enumerate(self.active_miniscript.to_descriptor().keys): @@ -2689,17 +2702,14 @@ def musig_process_input(self, session, inp_idx, inp, keypair, agg_k, der_agg_k, # is derived aggregate key xonly ? dak_xo = int(len(der_agg_k) == 32) # key is derived inside the key_agg cache - assert self.musig_derive_keyagg_cache(to_derive, agg_k, keyagg_cache)[dak_xo:] == der_agg_k + derived_k = self.musig_derive_keyagg_cache(to_derive, agg_k, keyagg_cache) + assert derived_k[dak_xo:] == der_agg_k if not leaf_hash: # now finally get the output key - only for musig in taproot internal key - tweak_data = der_agg_k - if inp.taproot_merkle_root: - tweak_data += self.get(inp.taproot_merkle_root) - tweak32 = ngu.hash.sha256t(TAP_TWEAK_H, tweak_data, True) - output_key = ngu.secp256k1.musig_pubkey_xonly_tweak_add(keyagg_cache, tweak32) + output_key, _ = self.musig_taproot_tweak(inp, keyagg_cache, derived_k[1:]) # tweaked derived aggregate key - der_agg_k = output_key.to_bytes() + der_agg_k = output_key my_musig_pubnonces_key = (my_participant_key, der_agg_k, leaf_hash) From 62b0579e0c83e8d6495bcccb96c11eb1b586a5aa Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:02:42 -0400 Subject: [PATCH 27/32] feat (MuSig2 + SP): compute sum(IL) with musig_derive_keyagg_cache --- shared/psbt.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shared/psbt.py b/shared/psbt.py index 7ae5569f..38ebaeb1 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -2636,15 +2636,20 @@ def ecdsa_grind_sign(sk, digest, sighash): @staticmethod def musig_derive_keyagg_cache(to_derive, agg_key, keyagg_cache): + # Returns (agg_pubkey_33, sum_IL_32) where sum_IL is used to reconstruct the + # BIP-327 tweak accumulator for MuSig2+SP partial ECDH share combining. ck = MUSIG_CHAIN_CODE agg = agg_key + sum_IL = 0 + SECP256K1_ORDER = ngu.secp256k1.curve_order_int() for idx in to_derive: I = ngu.hmac.hmac_sha512(ck, agg + pack(">I", idx)) IL, ck = I[:32], I[32:] ngu.secp256k1.musig_pubkey_ec_tweak_add(keyagg_cache, IL) agg = keyagg_cache.agg_pubkey().to_bytes() + sum_IL = (sum_IL + int.from_bytes(IL, 'big')) % SECP256K1_ORDER - return agg + return agg, sum_IL.to_bytes(32, 'big') @staticmethod def musig_build_cache(agg_k, participant_pks): @@ -2702,7 +2707,7 @@ def musig_process_input(self, session, inp_idx, inp, keypair, agg_k, der_agg_k, # is derived aggregate key xonly ? dak_xo = int(len(der_agg_k) == 32) # key is derived inside the key_agg cache - derived_k = self.musig_derive_keyagg_cache(to_derive, agg_k, keyagg_cache) + derived_k, _ = self.musig_derive_keyagg_cache(to_derive, agg_k, keyagg_cache) assert derived_k[dak_xo:] == der_agg_k if not leaf_hash: From d8bf144110104b372dae95f4200eca31c0be0c66 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:08:03 -0400 Subject: [PATCH 28/32] fix (psbt): handle unpacking taproot_subpaths with cached coordinates Gracefully handle the 3-element value parse_taproot_subpaths caches when the same input is parsed more than once (MuSig2 + SP). --- shared/psbt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/shared/psbt.py b/shared/psbt.py index 38ebaeb1..7b706d15 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -335,7 +335,11 @@ def parse_taproot_subpaths(self, my_xfp, parent, cosign_xfp=None): key, val = self.taproot_subpaths[i] assert key[1] == 32 # "PSBT_IN_TAP_BIP32_DERIVATION xonly-pubkey length != 32" xonly_pk = self.get(key) - pos, length = val + # 3rd element is cached coordinates written by earlier parse, drop and it will be re-computed below + if len(val) == 2: + pos, length = val + else: + pos, length, _ = val end_pos = pos + length self.fd.seek(pos) leaf_hash_len = deser_compact_size(self.fd) From a22f47a722daf28b76665a2a2e47551c25fcd97f Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 20 May 2026 19:19:07 -0400 Subject: [PATCH 29/32] feat (MuSig2 + SP): partial ECDH share contribution and signing UX auth: - Signing UX: prompt the user to contribute shares, run round 1 nonce generation. psbt: - Add musig_keyagg_context: assemble the keyagg cache and derive the BIP-327 combine factors (MusigEcdhFactors). - Allow musig sp inputs round 1 sign_it despite incomplete SP coverage. serializations: - determine_my_change - get_address returns (None, None) when scriptPubKey is None, no longer errors on musig sp inputs before output scripts are computed. silentpayments: - Contribute partial shares and DLEQ proof bound to the account-level participant key (_contribute_musig_ecdh_shares). - Aggregate shares via KeyAgg coefficients and the input's combine factors (_musig_aggregate_shares, _musig_keyagg_coefficient, _musig_sp_ecdh_factors, _musig_input_ecdh_share). Add bip327 tagged hashes Add musig2 silent payments round 1 test and fixtures. --- shared/auth.py | 24 +- shared/precomp_tag_hash.py | 6 + shared/psbt.py | 50 +++- shared/serializations.py | 5 + shared/silentpayments.py | 333 ++++++++++++++++++++++++-- testing/data/musig2-sp-round1-in.psbt | Bin 0 -> 1264 bytes testing/test_musig2_silentpayments.py | 136 +++++++++++ 7 files changed, 518 insertions(+), 36 deletions(-) create mode 100644 testing/data/musig2-sp-round1-in.psbt create mode 100644 testing/test_musig2_silentpayments.py diff --git a/shared/auth.py b/shared/auth.py index 62080652..ff24c918 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -444,7 +444,6 @@ async def interact(self): if not self.psbt.process_silent_payment_outputs(sv): # 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" @@ -454,16 +453,23 @@ async def interact(self): if ch != 'y': self.refused = True await ux_dramatic_pause("Refused.", 1) - del self.psbt + del args, 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) + + # Handle nonce generation for MuSig2 + SP inputs + if self.psbt.has_musig_sp_inputs(): + self.psbt.consider_outputs(*args, cosign_xfp=ccc_c_xfp) + del args + if self.psbt.session: + self.psbt.session.update(pack('b\xd4\x13\xba\x8c\xcdQ\x98\x91\x90\x17\xa1\xe9\x9c\xbei\x1fZ4\xa9\x93w\xe0\x95' +# BIP-327 MuSig2 KeyAgg tag hashes +# SHA256(KeyAgg list) +KEYAGG_LIST_TAG_H = b'H\x1c\x97\x1c<\x0bF\xd7\xf0\xb2u\xaeY\x8dN,~\xd71\x9cYJ\\n\xc7\x9e\xa0\xd4\x99\x02\x94\xf0' +# SHA256(KeyAgg coefficient) +KEYAGG_COEFF_TAG_H = b'\xbf\xc9\x04\x03M\x1c\x88\xe8\xc8\x0e"\xe5=$Vmd\x82N\xd6Br\x81\xc0\x91\x00\xf9M\xcdR\xc9\x81' + # BIP-374 DLEQ proof tag hashes # SHA256(BIP0374/aux) DLEQ_TAG_AUX_H = b'\xfc\xe2u partial-ECDH-share path""" + return inp.is_musig and self._is_input_eligible(inp) + + def _is_regular_sp_input(self, inp): + """SP-eligible non-MuSig2 input -> per-input ECDH-share path""" + return self._is_input_eligible(inp) and not inp.is_musig + + def _contribute_musig_ecdh_shares(self, sv, input): + """ + Contribute partial ECDH share + DLEQ proof for our participant key in a MuSig2 SP input, + store contributions in PSBT input fields + + Returns: + bool: True if we contributed at least one share + False if we are not a participant + """ + # Select participants from the spending aggregate + participants = self._musig_spending_participants(input) + if not participants: + return False + key = self.active_miniscript.to_descriptor().key + + # Locate our participant in the enrolled descriptor and derive it via its + # origin path as specified in the descriptor + my_participant_sk = None + my_participant_pk = None + for k in key.keys: + if k.origin.cc_fp != self.my_xfp: + continue + node = sv.derive_path(k.origin.str_derivation(), register=False) + my_participant_sk = node.privkey() + my_participant_pk = k.node.pubkey() + del node + break + + if my_participant_sk is None: + return False + + # Confirm our account-level key is a participant in this input's musig aggregate + my_xonly = my_participant_pk[1:] + is_participant = any((p[1:] if len(p) == 33 else p) == my_xonly for p in participants) + if not is_participant: + stash.blank_object(my_participant_sk) + return False + + contributed = False + scan_keys = self._get_silent_payment_scan_keys() + for scan_key in scan_keys: + if input.sp_partial_ecdh_shares and (scan_key, my_participant_pk) in input.sp_partial_ecdh_shares: + contributed = True + continue + + my_share = _compute_ecdh_share(my_participant_sk, scan_key) + my_proof = generate_dleq_proof(my_participant_sk, scan_key) + + input.sp_partial_ecdh_shares = input.sp_partial_ecdh_shares or {} + input.sp_partial_dleq_proofs = input.sp_partial_dleq_proofs or {} + input.sp_partial_ecdh_shares[(scan_key, my_participant_pk)] = my_share + input.sp_partial_dleq_proofs[(scan_key, my_participant_pk)] = my_proof + contributed = True + + stash.blank_object(my_participant_sk) + return contributed + + def _musig_input_ecdh_share(self, input, scan_key): + """ + Combine this input's MuSig2 partial ECDH shares for scan_key into the aggregate + share a_Q * B_scan + + Note: + This is the MuSig2 equivalent of inp.sp_ecdh_shares[scan_key] for a non-musig input. + + Returns: + bytes: aggregate share, or None if no shares are present for scan_key or no + enrolled MuSig2 descriptor is available + """ + if not input.sp_partial_ecdh_shares: + return None + + participant_pks, factors = self._musig_sp_ecdh_factors(input) + if participant_pks is None or factors is None: + return None + + # Only combine shares from the spending aggregate's participants + participant_set = set(participant_pks) + partial_shares = [ + (participant_pk, share) + for (entry_scan_key, participant_pk), share in input.sp_partial_ecdh_shares.items() + if entry_scan_key == scan_key and participant_pk in participant_set + ] + if not partial_shares: + return None + + return _musig_aggregate_shares(participant_pks, partial_shares, factors, scan_key) + + def _musig_sp_ecdh_factors(self, input): + """ + Derive BIP-327 factors to combine partial ECDH shares into a_Q * B_scan for a + tr(musig(...)) keyspend SP input + + Rebuilds the KeyAgg cache from the participant set, applies the descriptor's + BIP-328 synthetic derivation and the BIP-341 taproot tweak, then reads the parity + accumulators (gacc before taproot, g_v after) and the accumulated tweak (tacc). + + Returns: + tuple: (participant_pks, MusigEcdhFactors) + (None, None) if there is no enrolled MuSig2 descriptor for this input + """ + participant_pks = self._musig_spending_participants(input) + if not participant_pks: + return None, None + agg_k = self.active_miniscript.to_descriptor().key.node.pubkey() + + # BIP-328 synthetic derivation indices (e.g. /0/*) from the enrolled descriptor + parsed = input.parse_subpaths(self.my_xfp, self) + xfp_paths = [v[1:] for v in parsed.values()] + branch, idx = self.active_miniscript.subderivation_indexes(xfp_paths) + to_derive = [branch, idx] + + # aggregate + verify, BIP-328 synthetic derivation, BIP-341 keyspend taproot tweak + _, _, output_key, factors = self.musig_keyagg_context( + input, agg_k, participant_pks, to_derive, compute_factors=True + ) + + # derived output key must match the input's actual scriptPubKey + if output_key[1:] != input.utxo_spk[2:34]: + raise FatalPSBTIssue("MuSig2+Silent Payments output key mismatch") + + return participant_pks, factors + + def _musig_spending_participants(self, input): + """ + Participant pubkeys of the aggregate that actually spends this input, per the + enrolled descriptor; None if unavailable. + """ + if self.active_miniscript is None: + return None + key = self.active_miniscript.to_descriptor().key + if not isinstance(key, MusigKey): + return None + return input.get_musig_pubkeys().get(key.node.pubkey()) diff --git a/testing/data/musig2-sp-round1-in.psbt b/testing/data/musig2-sp-round1-in.psbt new file mode 100644 index 0000000000000000000000000000000000000000..b6449adb3a3565e4cd5169b84a78dbc315819c2c GIT binary patch literal 1264 zcmXRYPAd7&_?v}^fq{XM2|}?jGBUCA810$cp`+aNg;lqslEI`c+jEvd^=M+FJ z2BkoSSH4yr5!cwJu(^NA{;(+E!<+-$&Wfi?ANu=<960aGGFefKW6_Lb%UwhMw$8nm zv#U$u(9&sUSwDH-Z{DiWQgt%wST!#L|EoI@Amf;USW!%WKJ!GKJBD8@7v(uN`Iar% zt8kh{!H@f)X54&M?J(aEQHJbg=PwvAFf=d%F;GhbP+xDGb zb;5b=$6U!wgluOf6dcTi{LR3~%%QL!6z{;e<8)%SzIxw4MceNY9OYm3}`NQm{X7ct4_OloGq^=4E8R)`D32Yd2n=6arAkddsr zc@gXN{?-T2sxnqN{Pqg^VOYJlKnY}E!ecSGfm_`|c4Qvrth2iFcZn|>N z`6YAiYvT#=f8+z0ZaCo%*Bps5g7_?` + + +import json +import os +import pytest +import time +from psbt import BasicPSBT + +DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +ROUND1_PSBT = os.path.join(DATA_DIR, 'musig2-sp-round1-in.psbt') +# The tr(musig(A,B,C)) treasury descriptor matching the fixtures. Alice (xfp 0f056943) is the +# simulator's default seed; the device enrolls this wallet before signing. +TREASURY_DESC = os.path.join(DATA_DIR, 'desc-musig-sp-demo.txt') + +NUM_RECIPIENTS = 5 + +SIMULATOR_DELAY = .1 # seconds to wait for the simulator to process the PSBT and update the story + + +def _load_psbt(path): + with open(path, 'rb') as f: + return f.read() + +def _get_scan_keys(tp): + return {bytes(out.sp_v0_info[:33]) for out in tp.outputs if out.sp_v0_info} + +def _enroll_treasury(clear_miniscript, offer_minsc_import, press_select): + """Register the tr(musig(A,B,C)) treasury so signing runs the enrolled path.""" + clear_miniscript() + with open(TREASURY_DESC) as f: + desc = f.read().strip() + _, story = offer_minsc_import(json.dumps(dict(name='sp_treasury', desc=desc))) + assert 'Create new miniscript wallet?' in story + press_select() + +def _our_participant_key(tp): + """ + Return the participant pubkey (33 bytes) that the simulator contributed shares for. + We identify it by finding any entry in sp_partial_ecdh_shares on input[0]. + Raises if none found (round 1 contribution not present). + """ + inp = tp.inputs[0] + if not inp.sp_partial_ecdh_shares: + raise AssertionError("No MuSig2 partial ECDH shares found on input[0] after round 1") + # Composite key is (scan_key, participant_pk); the simulator only contributes its own + # participant key, so all entries share the same participant_pk in the second slot. + _, participant_key = next(iter(inp.sp_partial_ecdh_shares)) + return participant_key + +# --------------------------------------------------------------------------- +# Round 1 test +# --------------------------------------------------------------------------- + +def test_musig2_sp_round1(dev, start_sign, end_sign, cap_story, + clear_miniscript, offer_minsc_import, press_select): + """ + Round 1: load the pre-contribution PSBT, approve the 'CONTRIBUTE SHARES?' story, + and confirm the simulator contributes ECDH shares (0x21), DLEQ proofs (0x22), and a + MuSig2 pubnonce (0x1b) for every SP scan key — without adding a signature. + """ + if not os.path.exists(ROUND1_PSBT): + pytest.skip(f"Fixture not found: {ROUND1_PSBT}") + + _enroll_treasury(clear_miniscript, offer_minsc_import, press_select) + + start_sign(_load_psbt(ROUND1_PSBT)) + + time.sleep(SIMULATOR_DELAY) + title, story = cap_story() + if title != 'CONTRIBUTE SHARES?': + print("\nSTORY BODY:\n", story) + assert title == 'CONTRIBUTE SHARES?', f"Unexpected story title: {title!r}" + assert 'Silent payment ECDH shares will be added to this transaction' in story, "SP addresses should appear in round 1 story" + + signed = end_sign(accept=True, finalize=False, accept_ms_import=True) + tp = BasicPSBT().parse(signed) + + scan_keys = _get_scan_keys(tp) + assert len(scan_keys) == NUM_RECIPIENTS, \ + f"Expected {NUM_RECIPIENTS} SP recipients, got {len(scan_keys)}" + + _assert_round1_output(tp, scan_keys) + +def _assert_round1_output(tp, scan_keys): + """ + After Round 1 the PSBT must have, on input[0]: + - One sp_partial_ecdh_shares entry per scan key, keyed by (scan_key, our_pk) + - One sp_partial_dleq_proofs entry per scan key, keyed by (scan_key, our_pk) + - One musig_pubnonces entry (our 0x1b contribution) + - No taproot_key_sig (coordinator hasn't aggregated yet) + """ + inp = tp.inputs[0] + + assert inp.sp_partial_ecdh_shares, "Round 1: 0x21 ECDH shares missing on input[0]" + assert inp.sp_partial_dleq_proofs, "Round 1: 0x22 DLEQ proofs missing on input[0]" + assert inp.musig_pubnonces, "Round 1: 0x1b pubnonce missing on input[0]" + assert not inp.taproot_key_sig, "Round 1: tap_key_sig must not be present" + + participant_key = _our_participant_key(tp) + + # One entry per scan key in the ECDH share map + contributed_scan_keys_ecdh = {sk for (sk, pk) in inp.sp_partial_ecdh_shares if pk == participant_key} + assert contributed_scan_keys_ecdh == scan_keys, ( + f"Round 1 ECDH share scan keys mismatch: " + f"got {len(contributed_scan_keys_ecdh)}, expected {len(scan_keys)}" + ) + + # One entry per scan key in the DLEQ proof map + contributed_scan_keys_dleq = {sk for (sk, pk) in inp.sp_partial_dleq_proofs if pk == participant_key} + assert contributed_scan_keys_dleq == scan_keys, ( + f"Round 1 DLEQ proof scan keys mismatch: " + f"got {len(contributed_scan_keys_dleq)}, expected {len(scan_keys)}" + ) + + # ECDH share format: compressed point + for (sk, pk), share in inp.sp_partial_ecdh_shares.items(): + assert len(share) == 33 and share[0] in (0x02, 0x03), \ + f"Invalid ECDH share format for scan_key={sk.hex()}: {share.hex()}" + + # DLEQ proof format: 64 bytes + for (sk, pk), proof in inp.sp_partial_dleq_proofs.items(): + assert len(proof) == 64, \ + f"DLEQ proof must be 64 bytes for scan_key={sk.hex()}, got {len(proof)}" + + # Exactly one pubnonce from our participant key + our_pubnonces = [(pk, ak, lh) for (pk, ak, lh) in inp.musig_pubnonces if pk == participant_key] + assert len(our_pubnonces) == 1, \ + f"Expected 1 pubnonce from our participant key, got {len(our_pubnonces)}" + pubnonce = inp.musig_pubnonces[our_pubnonces[0]] + assert len(pubnonce) == 66, f"Pubnonce must be 66 bytes, got {len(pubnonce)}" From fa5a9f4d92207a176957fa321d194253b15aa30d Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Wed, 20 May 2026 19:20:13 -0400 Subject: [PATCH 30/32] test (MuSig2 + SP): two round integration tests and fixtures Add cosigner contrib test fixture for simulating round 2 partial signature - 1 simulator/signer in test_musig2_sp_full_flow, the other 2 participants in 3 of 3 provide contributions from coordinator. --- testing/data/musig2-sp-cosigner-contrib.psbt | Bin 3884 -> 3884 bytes testing/data/musig2-sp-round1-in.psbt | Bin 1264 -> 1264 bytes testing/test_musig2_silentpayments.py | 212 ++++++++++++++++++- 3 files changed, 209 insertions(+), 3 deletions(-) diff --git a/testing/data/musig2-sp-cosigner-contrib.psbt b/testing/data/musig2-sp-cosigner-contrib.psbt index 717fe933d8f999e2a2240cb9526f19e53bbbb268..17b74e0c2599db472da804890aa394b6d19254b4 100644 GIT binary patch delta 28 kcmZ1@w?=M*70={*e14P7c|s=l@&#_z0`UT-z|Bs~35=8fF$PXv#Nr14<~I!Y delta 32 ocmeys`GIr8KgP*{EP<2%F$PXvz!W&ypDA$jN2UbE$%|P00O;8b2LJ#7 diff --git a/testing/test_musig2_silentpayments.py b/testing/test_musig2_silentpayments.py index c48402a5..ddf4e324 100644 --- a/testing/test_musig2_silentpayments.py +++ b/testing/test_musig2_silentpayments.py @@ -1,15 +1,19 @@ # (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC. # -# test_musig2_silentpayments.py - Coldcard simulator tests for MuSig2 + Silent Payments (BIP-327 + BIP-352). +# test_musig2_silentpayments.py - Coldcard simulator tests for MuSig2 + Silent Payments (BIP-327 + BIP-352) # # Fixture PSBTs are generated by the Rust binary in bip375-examples: # cargo run -p musig2-signer --bin payroll +# +# Use CC_SP_OUT env var to capture debug artifacts (PSBTs, final transaction) from the test run. +# ex. export CC_SP_OUT=/tmp/cc-sp-out && pytest test_musig2_silentpayments.py import json import os import pytest import time + from psbt import BasicPSBT DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') @@ -17,6 +21,9 @@ # The tr(musig(A,B,C)) treasury descriptor matching the fixtures. Alice (xfp 0f056943) is the # simulator's default seed; the device enrolls this wallet before signing. TREASURY_DESC = os.path.join(DATA_DIR, 'desc-musig-sp-demo.txt') +# Bob's + Charlie's static Round-1 contributions (pubnonce 0x1b, ECDH shares 0x21, DLEQ 0x22 +# on input[0]). Generated once by the Rust tool from the same keys as ROUND1_PSBT. +COSIGNER_CONTRIB_PSBT = os.path.join(DATA_DIR, 'musig2-sp-cosigner-contrib.psbt') NUM_RECIPIENTS = 5 @@ -76,9 +83,10 @@ def test_musig2_sp_round1(dev, start_sign, end_sign, cap_story, if title != 'CONTRIBUTE SHARES?': print("\nSTORY BODY:\n", story) assert title == 'CONTRIBUTE SHARES?', f"Unexpected story title: {title!r}" - assert 'Silent payment ECDH shares will be added to this transaction' in story, "SP addresses should appear in round 1 story" + assert 'ECDH shares' in story + assert 'Other signers' in story - signed = end_sign(accept=True, finalize=False, accept_ms_import=True) + signed = end_sign(accept=True, finalize=False, accept_ms_import=False) tp = BasicPSBT().parse(signed) scan_keys = _get_scan_keys(tp) @@ -134,3 +142,201 @@ def _assert_round1_output(tp, scan_keys): f"Expected 1 pubnonce from our participant key, got {len(our_pubnonces)}" pubnonce = inp.musig_pubnonces[our_pubnonces[0]] assert len(pubnonce) == 66, f"Pubnonce must be 66 bytes, got {len(pubnonce)}" + +# --------------------------------------------------------------------------- +# Round 1 and partial Round 2 test +# --------------------------------------------------------------------------- + +def test_musig2_sp_full_flow(dev, start_sign, end_sign, cap_story, sim_exec, + clear_miniscript, offer_minsc_import, press_select): + """ + Full two-round flow with the simulator as Alice: + + Round 1 (live) -> device emits Alice's pubnonce (0x1b), ECDH share (0x21), DLEQ (0x22) + Merge -> add Bob's + Charlie's static Round-1 contributions + Round 2 (live) -> device verifies the cosigners' DLEQ proofs, sums all three ECDH shares, + derives the SP output scripts, aggregates all three nonces, and emits + Alice's partial sig (0x1c) + + Only Alice signs, so the device produces one partial sig and does NOT finalize (it cannot + aggregate until all three partials are present). + """ + merged, alice_pk, scan_keys = _alice_round1_and_merge( + start_sign, end_sign, cap_story, sim_exec, + clear_miniscript, offer_minsc_import, press_select) + + # ---- After the merge, all three participants' Round-1 material must be present ---- + inp = merged.inputs[0] + assert len(inp.musig_pubnonces) == 3, \ + f"Expected 3 pubnonces after merge, got {len(inp.musig_pubnonces)}" + assert len(inp.sp_partial_ecdh_shares) == 3 * NUM_RECIPIENTS, ( + f"Expected {3 * NUM_RECIPIENTS} ECDH share entries " + f"(3 participants × {NUM_RECIPIENTS} scan keys), got {len(inp.sp_partial_ecdh_shares)}" + ) + assert len(inp.sp_partial_dleq_proofs) == 3 * NUM_RECIPIENTS, ( + f"Expected {3 * NUM_RECIPIENTS} DLEQ proof entries, got {len(inp.sp_partial_dleq_proofs)}" + ) + for sk in scan_keys: + ecdh_pks = {pk for (s, pk) in inp.sp_partial_ecdh_shares if s == sk} + dleq_pks = {pk for (s, pk) in inp.sp_partial_dleq_proofs if s == sk} + assert len(ecdh_pks) == 3, \ + f"scan_key {sk.hex()}: expected 3 ECDH participants, got {len(ecdh_pks)}" + assert ecdh_pks == dleq_pks, \ + f"scan_key {sk.hex()}: ECDH and DLEQ participant sets differ" + + # ---- Round 2: device signs the merged PSBT ---- + start_sign(merged.as_bytes()) + time.sleep(SIMULATOR_DELAY) + title, story = cap_story() + assert title == 'OK TO SEND?', f"R2 unexpected story title: {title!r}" + assert 'sp1' in story, "SP addresses should appear in round 2 confirmation story" + + signed = end_sign(accept=True, finalize=False, accept_ms_import=False) + tp = BasicPSBT().parse(signed) + + scan_keys = _get_scan_keys(tp) + assert len(scan_keys) == NUM_RECIPIENTS, \ + f"Expected {NUM_RECIPIENTS} SP recipients, got {len(scan_keys)}" + + _assert_round2_output(tp, scan_keys, alice_pk) + + # Hand off Alice's R2-signed PSBT to the Rust coordinator for finalization. + _maybe_write_artifact(signed, "r2-alice.psbt") + +def test_musig2_sp_round2_tampered_recipient(dev, start_sign, end_sign, cap_story, sim_exec, + clear_miniscript, offer_minsc_import, press_select, press_cancel): + """ + Negative test: a coordinator writes a PSBT_OUT_SCRIPT for an SP output that differs from the + one derivable from the contributed ECDH shares (swapped recipient). The device must re-derive + the script, detect the mismatch, refuse to sign, and emit no partial sig. + + We build the same merged Round-2 PSBT as the happy path, then overwrite one SP output's + script with a well-formed but wrong P2TR before submitting for signing. A wrong (not empty) + script is required: an empty script is silently filled by the device, so only a present-and- + wrong script reaches the mismatch branch in silentpayments.py. + """ + merged, _alice_pk, _scan_keys = _alice_round1_and_merge( + start_sign, end_sign, cap_story, sim_exec, + clear_miniscript, offer_minsc_import, press_select) + + # Tamper: overwrite one SP output's script with a well-formed but wrong P2TR (OP_1 <32 bytes>). + tampered = False + for out in merged.outputs: + if out.sp_v0_info: + out.script = b'\x51\x20' + (b'\xab' * 32) + tampered = True + break + assert tampered, "No SP output found to tamper" + + # Round 2: device must detect the script mismatch and refuse to produce a partial sig. + start_sign(merged.as_bytes()) + time.sleep(SIMULATOR_DELAY) + title, story = cap_story() + + if title == 'OK TO SEND?': + # Approval shown: the mismatch should fire during signing, so end_sign must raise. + with pytest.raises(Exception): + end_sign(accept=True, finalize=False, accept_ms_import=False) + else: + # Mismatch detected during PSBT validation; an error story is shown. + assert 'recipient' in story.lower() or 'mismatch' in story.lower() or \ + 'invalid' in story.lower() or 'fail' in story.lower(), \ + f"Expected tamper-detection error, got title={title!r} story={story!r}" + + press_cancel() + +def _alice_round1_and_merge(start_sign, end_sign, cap_story, sim_exec, + clear_miniscript, offer_minsc_import, press_select): + """ + Run Alice's live Round 1 against ROUND1_PSBT, then merge Bob's + Charlie's static Round-1 + contributions. Returns (merged_psbt, alice_pk, scan_keys). + """ + if not (os.path.exists(ROUND1_PSBT) and os.path.exists(COSIGNER_CONTRIB_PSBT)): + pytest.skip(f"Test PSBT fixtures not found at {DATA_DIR}") + + # Clears MUSIG_SESSION_CACHE first: the session-scoped simulator + module-global cache would + # otherwise let a prior test would interfere with Alice's R1->R2 nonce regeneration. + sim_exec("from psbt import MUSIG_SESSION_CACHE; MUSIG_SESSION_CACHE.clear()") + + _enroll_treasury(clear_miniscript, offer_minsc_import, press_select) + + # ---- Round 1: Alice (the simulator) contributes nonce + ECDH share + DLEQ ---- + start_sign(_load_psbt(ROUND1_PSBT)) + time.sleep(SIMULATOR_DELAY) + title, _ = cap_story() + assert title == 'CONTRIBUTE SHARES?', f"R1 unexpected story title: {title!r}" + r1_out = BasicPSBT().parse(end_sign(accept=True, finalize=False, accept_ms_import=False)) + + alice_pk = _our_participant_key(r1_out) + + # ---- Merge Bob's + Charlie's static Round-1 contributions ---- + contrib = BasicPSBT().parse(_load_psbt(COSIGNER_CONTRIB_PSBT)) + _merge_cosigner_contrib(r1_out, contrib) + + scan_keys = _get_scan_keys(r1_out) + assert len(scan_keys) == NUM_RECIPIENTS, \ + f"Expected {NUM_RECIPIENTS} SP recipients, got {len(scan_keys)}" + + return r1_out, alice_pk, scan_keys + +def _assert_round2_output(tp, scan_keys, participant_key): + """ + After Round 2 the PSBT must have, on input[0]: + - Exactly one musig_part_sigs entry, from our participant key (0x1c). Only Alice signs here; + the device cannot finalize until all three partials are present (done later in Rust). + - No tap_key_sig yet. + + And every SP output must have a derived P2TR script. + """ + inp = tp.inputs[0] + + assert inp.musig_part_sigs, "Round 2: 0x1c partial sig missing on input[0]" + + our_part_sigs = [(pk, ak, lh) for (pk, ak, lh) in inp.musig_part_sigs if pk == participant_key] + assert len(our_part_sigs) == 1, \ + f"Round 2: expected exactly 1 partial sig from our participant, got {len(our_part_sigs)}" + assert len(inp.musig_part_sigs) == 1, \ + f"Round 2: only Alice should have signed, got {len(inp.musig_part_sigs)} partial sigs" + + part_sig = inp.musig_part_sigs[our_part_sigs[0]] + assert len(part_sig) == 32, \ + f"MuSig2 partial sig must be 32 bytes, got {len(part_sig)}" + + # Every SP output must have a finalized P2TR script + sp_scripts = [] + for out in tp.outputs: + if out.sp_v0_info: + assert out.script is not None, "SP output script not set after round 2" + assert len(out.script) == 34, \ + f"SP P2TR script must be 34 bytes (OP_1 <32-byte xonly>), got {len(out.script)}" + assert out.script[0] == 0x51, \ + f"SP output must start with OP_1 (0x51), got 0x{out.script[0]:02x}" + sp_scripts.append(bytes(out.script)) + + assert len(sp_scripts) == NUM_RECIPIENTS, \ + f"Expected {NUM_RECIPIENTS} SP output scripts, got {len(sp_scripts)}" + + return sp_scripts + +def _maybe_write_artifact(data, filename): + """If CC_SP_OUT is set, write a demo artifact into that directory.""" + out_dir = os.environ.get('CC_SP_OUT') + if not out_dir: + return + os.makedirs(out_dir, exist_ok=True) + path = os.path.join(out_dir, filename) + with open(path, 'wb') as f: + f.write(data) + print(f"Wrote {filename} to {path}") + +def _merge_cosigner_contrib(target_tp, contrib_tp): + """ + Merge Bob's + Charlie's static Round-1 contributions into input[0] of a PSBT that already + holds Alice's live Round-1 output. Copies the pubnonce (0x1b), partial ECDH share (0x21), and + DLEQ proof (0x22) maps. + """ + src = contrib_tp.inputs[0] + dst = target_tp.inputs[0] + dst.musig_pubnonces.update(src.musig_pubnonces) + dst.sp_partial_ecdh_shares.update(src.sp_partial_ecdh_shares) + dst.sp_partial_dleq_proofs.update(src.sp_partial_dleq_proofs) From f8f607afa753b1bccd611c37e696963063c1d900 Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:13:36 -0400 Subject: [PATCH 31/32] test (MuSig2 + SP): full two round - three simulators/signers test_musig2_sp_three_hardware_signers coordinates three separate simulator processes to compute silent payments outputs and sign a psbt paying multiple recipients through two rounds. multi signer refactor --- testing/test_musig2_sp_signers.py | 469 ++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 testing/test_musig2_sp_signers.py diff --git a/testing/test_musig2_sp_signers.py b/testing/test_musig2_sp_signers.py new file mode 100644 index 00000000..762221d7 --- /dev/null +++ b/testing/test_musig2_sp_signers.py @@ -0,0 +1,469 @@ +# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# test_musig2_sp_signers.py - Coldcard simulator tests for MuSig2 + Silent Payments. +# +# Test keeps three simulator processes alive so Alice, Bob, Charlie each perform both +# rounds and the last signer finalizes the transaction. +# +# Expected musig2-sp-final-tx.hex is generated by the Rust binary in bip375-examples: +# cargo run -p musig2-signer --bin finalize +# +# Use CC_SP_OUT env var to capture debug artifacts (PSBTs, final transaction) from the test run. +# ex. export CC_SP_OUT=/tmp/cc-sp-out && pytest test_musig2_sp_signers.py + + +import io +import json +import os +import pytest +import shutil +import sys +import time +from binascii import b2a_hex +from ckcc_protocol.client import ColdcardDevice +from ckcc_protocol.protocol import CCProtocolPacker +from core_fixtures import _cap_story, _dev_hw_label, _need_keypress, _press_cancel, _press_select +from ctransaction import CTransaction, CTxOut +from helpers import xfp2str +from psbt import BasicPSBT +from run_sim_tests import ColdcardSimulator +from test_musig2_silentpayments import ( + NUM_RECIPIENTS, + ROUND1_PSBT, + TREASURY_DESC, + _load_psbt, + _get_scan_keys, + _maybe_write_artifact, +) + +DATA_DIR = os.path.join(os.path.dirname(__file__), "data") + +SIGNER_XFPS = { + "Alice": "0F056943", + "Bob": "6BA6CFD0", + "Charlie": "747B698E", +} +SIM_START_TIMEOUT = 15 +SIM_START_POLL_INTERVAL = 0.5 + + +def _exec_sim_file(device, path): + full_path = os.path.abspath(path) + hook = "execfile(%r)" % full_path + rv = device.send_recv(b"EXEC" + hook.encode("utf-8"), timeout=60000, encrypt=False) + rv = rv.decode("utf-8") + assert "Traceback" not in rv, rv + return rv + + +def _clear_sim_cache(device): + cmd = "from psbt import MUSIG_SESSION_CACHE; MUSIG_SESSION_CACHE.clear()" + rv = device.send_recv(b"EXEC" + cmd.encode("utf-8"), timeout=60000, encrypt=False) + assert b"Traceback" not in rv, rv + + +def _activate_bip39_passphrase(device, is_q1, passphrase): + """Activate a simulator BIP39 passphrase and refresh the encrypted USB session.""" + device.send_recv(CCProtocolPacker.bip39_passphrase(passphrase), timeout=None) + time.sleep(0.1) + + _, story = _cap_story(device) + assert "to master seed" in story + _press_select(device, is_q1) + + time.sleep(0.3) + _, story = _cap_story(device) + if "Press (1) to store temporary seed into Seed Vault" in story: + _press_select(device, is_q1) + time.sleep(0.2) + _, story = _cap_story(device) + + assert "Above is the master key fingerprint" in story + _press_select(device, is_q1) + + done = None + while done is None: + time.sleep(0.05) + done = device.send_recv(CCProtocolPacker.get_passphrase_done(), timeout=None) + assert done[1:4] == "pub" + + device.start_encryption() + device.check_mitm() + + +class HardwareSigner: + """Device-bound operations needed by the two-round signing test.""" + + def __init__(self, name, device, is_q1): + self.name = name + self.device = device + self.is_q1 = is_q1 + + def assert_fingerprint(self): + got = xfp2str(self.device.master_fingerprint) + assert got == SIGNER_XFPS[self.name], f"{self.name}: expected XFP {SIGNER_XFPS[self.name]}, got {got}" + + def clear_session_cache(self): + _clear_sim_cache(self.device) + + def enroll_treasury(self): + _exec_sim_file(self.device, "devtest/wipe_miniscript.py") + with open(TREASURY_DESC) as f: + desc = f.read().strip() + config = json.dumps(dict(name="sp_treasury", desc=desc)).encode("ascii") + file_len, sha = self.device.upload_file(config) + self.device.send_recv(CCProtocolPacker.miniscript_enroll(file_len, sha)) + + time.sleep(0.2) + _, story = _cap_story(self.device) + assert "Create new miniscript wallet?" in story + _press_select(self.device, self.is_q1) + time.sleep(0.1) + + def sign(self, psbt_bytes, expected_title, finalize=False): + file_len, sha = self.device.upload_file(psbt_bytes) + self.device.send_recv(CCProtocolPacker.sign_transaction(file_len, sha, finalize)) + + title, story = _cap_story(self.device) + assert title == expected_title, f"{self.name}: expected story {expected_title!r}, got {title!r}: {story}" + + _need_keypress(self.device, "y", timeout=None) + done = None + while done is None: + done = self.device.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) + result_len, checksum = done + result = self.device.download_file(result_len, checksum) + _press_cancel(self.device, self.is_q1) + + if finalize: + assert not result.startswith(b"psbt\xff") + else: + assert result.startswith(b"psbt\xff") + return result, story + + +def _start_child_simulator(simulator): + """ColdcardSimulator uses bare `python`; make it resolve to pytest's interpreter.""" + old_path = os.environ.get("PATH", "") + os.environ["PATH"] = os.path.dirname(sys.executable) + os.pathsep + old_path + try: + simulator.start(start_wait=SIM_START_POLL_INTERVAL) + finally: + os.environ["PATH"] = old_path + + try: + deadline = time.monotonic() + SIM_START_TIMEOUT + last_error = None + while time.monotonic() < deadline: + exit_code = simulator.proc.poll() + if exit_code is not None: + raise RuntimeError(f"Simulator exited before socket was ready: {simulator.socket} (exit={exit_code})") + + device = None + try: + device = ColdcardDevice(sn=simulator.socket, is_simulator=True) + return + except Exception as exc: + last_error = exc + time.sleep(SIM_START_POLL_INTERVAL) + finally: + if device is not None: + device.close() + + exit_code = simulator.proc.poll() + status = "still running" if exit_code is None else f"exited with {exit_code}" + raise RuntimeError(f"Simulator socket was not ready after {SIM_START_TIMEOUT}s: {simulator.socket} ({status})") from last_error + except Exception: + socket_path = simulator.socket + work_path = f"/tmp/cc-simulators/{simulator.proc.pid}" + simulator.stop() + if os.path.exists(socket_path): + os.unlink(socket_path) + shutil.rmtree(work_path, ignore_errors=True) + raise + + +@pytest.fixture +def three_musig_signers(request, is_headless): + """ + Alice, Bob and Charlie each run in their own segregated simulator process so the test + starts every simulator it needs (no pre-running simulator required) and each signer + retains an independent MUSIG_SESSION_CACHE between Round 1 and Round 2. + + The hardware build matches the simulator run_sim_tests pre-starts (read via --sim-socket); + a bare pytest run with no simulator defaults to Q1. Alice uses the fixed simulator seed + (no passphrase); Bob and Charlie layer BIP39 passphrases to get distinct fingerprints. + """ + sim_socket = request.config.getoption("--sim-socket") + if sim_socket: + probe = ColdcardDevice(sn=sim_socket, is_simulator=True) + try: + hw_label = _dev_hw_label(probe) + finally: + probe.close() + else: + hw_label = "q1" if request.config.getoption("--Q") else "mk%d" % int(request.config.getoption("--mk")) + + if hw_label == "q1": + args = ["--q1"] + elif hw_label == "mk4": + args = ["--mk4"] + elif hw_label == "mk5": + args = [] + else: + pytest.skip(f"Three-simulator MuSig2 test does not support {hw_label}") + is_q1 = hw_label == "q1" + + signers = [] + simulators = [] + + try: + for name, passphrase in [("Alice", None), ("Bob", "Me"), ("Charlie", "Myself")]: + simulator = ColdcardSimulator( + args=list(args), + headless=is_headless, + segregate=True, + ) + _start_child_simulator(simulator) + simulators.append(simulator) + device = ColdcardDevice(sn=simulator.socket, is_simulator=True) + signer = HardwareSigner(name, device, is_q1) + signers.append(signer) + if passphrase is not None: + _activate_bip39_passphrase(device, is_q1, passphrase) + + for signer in signers: + assert _dev_hw_label(signer.device) == hw_label + signer.assert_fingerprint() + signer.clear_session_cache() + signer.enroll_treasury() + + yield signers + finally: + for signer in reversed(signers): + signer.device.close() + for simulator in reversed(simulators): + socket_path = simulator.socket + work_path = f"/tmp/cc-simulators/{simulator.proc.pid}" + simulator.stop() + if os.path.exists(socket_path): + os.unlink(socket_path) + shutil.rmtree(work_path, ignore_errors=True) + + +def _assert_round1_contribution(before, after, signer_name, scan_keys): + """Assert and return the participant key added by one hardware signer.""" + before_inp = before.inputs[0] + after_inp = after.inputs[0] + + new_pubnonces = set(after_inp.musig_pubnonces) - set(before_inp.musig_pubnonces) + new_ecdh = set(after_inp.sp_partial_ecdh_shares) - set(before_inp.sp_partial_ecdh_shares) + new_dleq = set(after_inp.sp_partial_dleq_proofs) - set(before_inp.sp_partial_dleq_proofs) + + assert len(new_pubnonces) == 1, f"{signer_name}: expected one new pubnonce, got {len(new_pubnonces)}" + participant_keys = {pk for _, pk in new_ecdh} + assert len(participant_keys) == 1, f"{signer_name}: expected one ECDH participant key, got {len(participant_keys)}" + (participant_key,) = participant_keys + + (nonce_key,) = new_pubnonces + assert nonce_key[0] == participant_key + assert {sk for sk, pk in new_ecdh if pk == participant_key} == scan_keys + assert {sk for sk, pk in new_dleq if pk == participant_key} == scan_keys + assert len(new_ecdh) == len(scan_keys) + assert len(new_dleq) == len(scan_keys) + + assert len(after_inp.musig_pubnonces[nonce_key]) == 66 + for key in new_ecdh: + share = after_inp.sp_partial_ecdh_shares[key] + assert len(share) == 33 and share[0] in (0x02, 0x03) + for key in new_dleq: + assert len(after_inp.sp_partial_dleq_proofs[key]) == 64 + + assert not after_inp.musig_part_sigs + assert not after_inp.taproot_key_sig + return participant_key + + +def _sp_scripts(tp): + return [bytes(out.script) for out in tp.outputs if out.sp_v0_info] + + +def _assert_partial_sigs(tp, participant_keys, expected_names): + inp = tp.inputs[0] + assert len(inp.musig_part_sigs) == len(expected_names) + expected_keys = {participant_keys[name] for name in expected_names} + actual_keys = {pk for pk, _ak, _lh in inp.musig_part_sigs} + assert actual_keys == expected_keys + assert all(len(sig) == 32 for sig in inp.musig_part_sigs.values()) + assert not inp.taproot_key_sig + + +def _assert_complete_psbt(tp, participant_keys): + """The final signer's PSBT: all three partial sigs plus the aggregated taproot key sig.""" + inp = tp.inputs[0] + assert len(inp.musig_part_sigs) == len(participant_keys) + actual_keys = {pk for pk, _ak, _lh in inp.musig_part_sigs} + assert actual_keys == set(participant_keys.values()) + assert all(len(sig) == 32 for sig in inp.musig_part_sigs.values()) + + assert inp.taproot_key_sig, "final signer must aggregate the taproot key sig" + # 64-byte Schnorr sig, or 65 bytes when a sighash flag is appended + assert len(inp.taproot_key_sig) in (64, 65) + + +def _parse_transaction(raw): + tx = CTransaction() + tx.deserialize(io.BytesIO(raw)) + return tx + + +def _parse_txout(raw): + txout = CTxOut() + txout.deserialize(io.BytesIO(raw)) + return txout + + +def _assert_final_transaction(raw_tx, expected_psbt): + actual = _parse_transaction(raw_tx) + + expected_version = 2 if expected_psbt.txn_version is None else expected_psbt.txn_version + expected_locktime = 0 if expected_psbt.fallback_locktime is None else expected_psbt.fallback_locktime + assert actual.nVersion == expected_version + assert actual.nLockTime == expected_locktime + assert len(actual.vin) == 1 + assert len(actual.vout) == len(expected_psbt.outputs) + + expected_input = expected_psbt.inputs[0] + actual_input = actual.vin[0] + assert actual_input.prevout.hash == int.from_bytes(expected_input.previous_txid, "little") + assert actual_input.prevout.n == expected_input.prevout_idx + assert actual_input.scriptSig == b"" + expected_sequence = 0xffffffff if expected_input.sequence is None else expected_input.sequence + assert actual_input.nSequence == expected_sequence + + expected_outputs = [(out.amount, out.script) for out in expected_psbt.outputs] + actual_outputs = [(out.nValue, out.scriptPubKey) for out in actual.vout] + assert actual_outputs == expected_outputs + + witness_utxo = _parse_txout(expected_input.witness_utxo) + assert sum(out.nValue for out in actual.vout) < witness_utxo.nValue + + sp_outputs = [out for out in expected_psbt.outputs if out.sp_v0_info] + change_outputs = [out for out in expected_psbt.outputs if not out.sp_v0_info] + assert len(sp_outputs) == NUM_RECIPIENTS + assert len(change_outputs) == 1 + assert all(out.script and len(out.script) == 34 and out.script[0] == 0x51 for out in sp_outputs) + + assert len(actual.wit.vtxinwit) == 1 + witness = actual.wit.vtxinwit[0].scriptWitness.stack + assert len(witness) == 1 + assert len(witness[0]) == 64 + + +# --------------------------------------------------------------------------- +# Test +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("finalize_on_device", [True, False], ids=["final-txn", "signed-psbt"]) +def test_musig2_sp_three_hardware_signers(three_musig_signers, finalize_on_device): + """ + End-to-end hardware workflow: + + Round 1: Alice -> Bob -> Charlie each add a pubnonce and SP ECDH/DLEQ shares. + Round 2: Alice -> Bob add partial signatures; Charlie adds the third partial and + aggregates the MuSig2 signature. + + Charlie's last step runs as one of two cases (CC finalizes only as part of the final + signature, and his nonce is one-shot, so each output needs its own fresh run): + - finalize_on_device=True : Charlie signs with finalize=True -> broadcast-ready txn. + - finalize_on_device=False: Charlie signs with finalize=False -> fully-signed PSBT. + """ + if not all(os.path.exists(path) for path in (ROUND1_PSBT, TREASURY_DESC)): + pytest.skip(f"Three-signer fixtures not found at {DATA_DIR}") + + signers = three_musig_signers + current = BasicPSBT().parse(_load_psbt(ROUND1_PSBT)) + scan_keys = _get_scan_keys(current) + assert len(scan_keys) == NUM_RECIPIENTS + + participant_keys = {} + + # Round 1: pass the growing PSBT through three independent hardware devices. + for expected_count, signer in enumerate(signers, start=1): + before = current + expected_title = "OK TO SEND?" if expected_count == len(signers) else "CONTRIBUTE SHARES?" + signed, story = signer.sign( + before.as_bytes(), + expected_title=expected_title, + finalize=False, + ) + if expected_title == "CONTRIBUTE SHARES?": + assert "ECDH shares" in story + assert "Other signers" in story + else: + # Charlie's shares complete ECDH coverage during preview, but his empty device + # session cache still makes this approval cryptographic Round 1. + assert "sp1" in story + + current = BasicPSBT().parse(signed) + _maybe_write_artifact(signed, f"r1-{signer.name.lower()}.psbt") + participant_keys[signer.name] = _assert_round1_contribution(before, current, signer.name, scan_keys) + assert len(current.inputs[0].musig_pubnonces) == expected_count + assert len(current.inputs[0].sp_partial_ecdh_shares) == expected_count * NUM_RECIPIENTS + assert len(current.inputs[0].sp_partial_dleq_proofs) == expected_count * NUM_RECIPIENTS + + assert len(set(participant_keys.values())) == 3 + assert len(current.inputs[0].musig_pubnonces) == 3 + assert len(current.inputs[0].sp_partial_ecdh_shares) == 3 * NUM_RECIPIENTS + assert len(current.inputs[0].sp_partial_dleq_proofs) == 3 * NUM_RECIPIENTS + + # Round 2: Alice derives the SP scripts and signs; Bob verifies and signs the same PSBT. + round2_psbt = current + expected_scripts = None + for expected_count, signer in enumerate(signers[:2], start=1): + signed, story = signer.sign( + round2_psbt.as_bytes(), + expected_title="OK TO SEND?", + finalize=False, + ) + assert "sp1" in story + + round2_psbt = BasicPSBT().parse(signed) + _maybe_write_artifact(signed, f"r2-{signer.name.lower()}.psbt") + _assert_partial_sigs( + round2_psbt, + participant_keys, + [s.name for s in signers[:expected_count]], + ) + + scripts = _sp_scripts(round2_psbt) + assert len(scripts) == NUM_RECIPIENTS + assert all(len(script) == 34 and script[0] == 0x51 for script in scripts) + if expected_scripts is None: + expected_scripts = scripts + else: + assert scripts == expected_scripts + + # Charlie consumes the last secret nonce, adds the final partial, and aggregates the + # MuSig2 signature. + charlie = signers[2] + result, story = charlie.sign( + round2_psbt.as_bytes(), + expected_title="OK TO SEND?", + finalize=finalize_on_device, + ) + assert "sp1" in story + + if finalize_on_device: + # Charlie finalizes in the same pass: broadcast-ready transaction. + _maybe_write_artifact(b2a_hex(result) + b"\n", "musig2-sp-final.hex") + _assert_final_transaction(result, round2_psbt) + else: + # Charlie exports the fully-signed PSBT (all three partials + aggregated key sig). + complete_psbt = BasicPSBT().parse(result) + _maybe_write_artifact(result, "musig2-sp-final.psbt") + _assert_complete_psbt(complete_psbt, participant_keys) + scripts = _sp_scripts(complete_psbt) + assert len(scripts) == NUM_RECIPIENTS + assert scripts == expected_scripts From 24c49c7ac96131a577d0d9a2bf4cf77c9d33097a Mon Sep 17 00:00:00 2001 From: macgyver13 <4712150+macgyver13@users.noreply.github.com> Date: Tue, 26 May 2026 15:14:18 -0400 Subject: [PATCH 32/32] docs (MuSig2 + SP): Add MuSig2 to SP documentation --- docs/silentpayments.md | 76 +++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/docs/silentpayments.md b/docs/silentpayments.md index 6f029715..94e78b7c 100644 --- a/docs/silentpayments.md +++ b/docs/silentpayments.md @@ -25,14 +25,14 @@ COLDCARD implements the following BIPs: The shared secret can be computed from either side: -* **sender side**: `shared_secret = (a_1 + a_2 + ...) * B_scan` — sum of input private keys × receiver's scan public key -* **receiver side**: `shared_secret = (A_1 + A_2 + ...) * b_scan` — sum of input public keys x receiver's scan private key +* **sender side**: `shared_secret = (a_1 + a_2 + ...) * B_scan` — sum of input private keys * receiver's scan public key +* **receiver side**: `shared_secret = (A_1 + A_2 + ...) * b_scan` — sum of input public keys * receiver's scan private key In single-signer flows, COLDCARD performs the full sender-side ECDH internally. In multi-signer flows (multiple input owners), each signer computes a *partial ECDH share*: -* `share_i = a_i * B_scan` — the signer's input private key × the receiver's scan public key +* `share_i = a_i * B_scan` — the signer's input private key * the receiver's scan public key * the coordinator sums all shares: `sum(share_i) = (a_1 + a_2 + ...) * B_scan => shared_secret` * each share is accompanied by a **DLEQ proof** (Discrete Log Equality) so the coordinator or signers can verify the shares were computed from the correct input secret key @@ -46,37 +46,89 @@ In multi-signer flows (multiple input owners), each signer computes a *partial E ## Limitations * one of the transaction inputs must be eligible for ["Shared Secret Derivation"](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#user-content-Inputs_For_Shared_Secret_Derivation) -* MuSig2 and FROST are not supported yet +* MuSig2 spends are supported for taproot key-spend inputs (`tr(musig(...))`) - see [MuSig2 + Silent Payments](#musig2-silent-payments). FROST is not supported yet. ## Example SP Address encodes two public keys: a *scan key* and a *spend key* -``` + +```plain sp1qqw5jexmu4358tr090qld3egjxkvwftgnwzg7g2v86wad3gywxkln6qcc0kmh5k03cheul53fd7r7h4lg9y3xkrmz3k00ujulyg2pfcaevu9nurf3 ``` ### Partial Signing (Collaborative Inputs - Multiple Signers) + Round 1 — ECDH share collection -- Coordinator builds PSBT with inputs from each participant and partial outputs (output script not yet finalized) -- Each input owner contributes their partial ECDH share a_i * B_scan and DLEQ proof into the PSBT -- Last contributor verifies all DLEQ proofs, combines partial shares → computes shared secret → computes final output script, updates PSBT + +* Coordinator builds PSBT with inputs from each participant and partial outputs (output script not yet finalized) +* Each input owner contributes their partial ECDH share a_i * B_scan and DLEQ proof into the PSBT +* Last contributor verifies all DLEQ proofs, combines partial shares → computes shared secret → computes final output script, updates PSBT Round 2 — Sign -- Each signer verifies the output scripts in the PSBT then signs their inputs normally -- Coordinator finalizes and broadcasts + +* Each signer verifies the output scripts in the PSBT then signs their inputs normally +* Coordinator finalizes and broadcasts + +## MuSig2 + Silent Payments + +A MuSig2 wallet (e.g. 3-of-3 `tr(musig(A,B,C)/0/*)`) can pay Silent Payments recipients. The aggregate secret key to unlock the on-chain taproot output key Q is split across all signers, so no single party can compute the ECDH shared secret alone - this requires an extension to MuSig2 PSBT Fields (`PSBT_IN_SP_PARTIAL_ECDH_SHARE` and `PSBT_IN_SP_PARTIAL_DLEQ`) to support Silent Payments ouput script computation. + +To keep the total number of PSBT rounds to a minimum the MuSig2 nonce and ECDH share collection are combined in the first round: + +* Round 1 - Contribute: Partial ECDH share, DLEQ proof, and fresh MuSig2 pubnonce from every signer (last signer computes Silent Payment output scripts from the aggregated ECDH share). +* Round 2 - Sign: Each signer independently re-verifies Silent Payment output scripts then contributes partial MuSig2 signature completing the MuSig2 aggregation and PSBT finalization. + +### Call Tree + +Signing entry is `psbtObject.sign_it()` (`shared/psbt.py`), `process_silent_payment_outputs()` runs on every round: it validates ECDH coverage, contributes this signer's share, and computes the output scripts via `_compute_silent_payment_output_scripts()`. The last contributor sets the scripts at the end of Round 1. Round 2 every signer re-runs that same path to re-compute and validate the scripts already in the PSBT (mismatch aborts) before contributing a partial signature in `musig_process_input()`. + +```text +sign_it() psbt.py:2861,2944 +│ +├─ process_silent_payment_outputs() silentpayments.py:372 (runs every round) +│ │ +│ ├─ _validate_ecdh_coverage() :690 (required shares + DLEQ present) +│ │ +│ ├─ _compute_and_store_ecdh_shares() :776 +│ │ └─ _contribute_musig_ecdh_shares() :1091 +│ │ ├─ _compute_ecdh_share() :112 +│ │ └─ generate_dleq_proof() dleq.py:41 +│ │ +│ └─ _compute_silent_payment_output_scripts() :846 +│ │ Round 1 (last contributor): compute + set output scripts +│ │ Round 2 (every signer): re-compute + validate vs PSBT (mismatch → abort) +│ └─ _get_ecdh_and_pubkey() :476 +│ └─ _musig_input_ecdh_share() :1199 +│ ├─ _musig_sp_ecdh_factors() :1160 +│ │ └─ psbt.musig_keyagg_context() psbt.py:2678 +│ │ ├─ musig_build_cache() :2658 +│ │ ├─ musig_derive_keyagg_cache() :2641 +│ │ └─ musig_taproot_tweak() :2668 +│ │ → MusigEcdhFactors(negation_factor, total_tweak) +│ └─ _musig_aggregate_shares() :242 +│ └─ _musig_keyagg_coefficient() :273 +│ +└─ musig_process_input() psbt.py:2713 + Round 1: generate this signer's pubnonce + Round 2: contribute partial MuSig2 signature +``` ## Development ### start simulator -``` + +```sh cd unix % ./simulator.py --q1 ``` ### execute tests -``` + +```sh cd testing pytest test_bip352_vectors.py pytest test_bip375_vectors.py pytest test_silentpayments.py +pytest test_musig2_silentpayments.py +pytest test_musig2_sp_signers.py ```