99import struct
1010from typing import Tuple
1111
12+ from deps .bitcoin_test .messages import COutPoint
1213from deps .bitcoin_test .psbt import (
1314 PSBT ,
1415 PSBT_GLOBAL_TX_MODIFIABLE ,
16+ PSBT_IN_OUTPUT_INDEX ,
17+ PSBT_IN_PREVIOUS_TXID ,
1518 PSBT_IN_SIGHASH_TYPE ,
1619 PSBT_IN_WITNESS_UTXO ,
1720 PSBT_OUT_SCRIPT ,
1821)
1922from deps .dleq import dleq_verify_proof
2023from secp256k1lab .secp256k1 import GE
2124
22- from .inputs import is_input_eligible , parse_witness_utxo , pubkey_from_eligible_input
25+ from .bip352_crypto import compute_silent_payment_output_script
26+ from .inputs import (
27+ collect_input_ecdh_and_pubkey ,
28+ is_input_eligible ,
29+ parse_witness_utxo ,
30+ pubkey_from_eligible_input ,
31+ )
2332from .psbt_bip375 import (
2433 PSBT_GLOBAL_SP_ECDH_SHARE ,
2534 PSBT_GLOBAL_SP_DLEQ ,
@@ -165,13 +174,9 @@ def validate_ecdh_coverage(psbt: PSBT) -> Tuple[bool, str]:
165174 if not dleq_proof :
166175 return False , "Global ECDH share missing DLEQ proof"
167176
168- # Compute A_sum "input public keys" for global verification
169- A_sum = None
170- for input_map in psbt .i :
171- pubkey = pubkey_from_eligible_input (input_map )
172- if pubkey is not None :
173- A_sum = pubkey if A_sum is None else A_sum + pubkey
174- assert A_sum is not None , "No public keys found for inputs"
177+ _ , summed_pubkey_bytes = collect_input_ecdh_and_pubkey (psbt , scan_key )
178+ assert summed_pubkey_bytes is not None , "No public keys found for inputs"
179+ A_sum = GE .from_bytes (summed_pubkey_bytes )
175180
176181 valid , msg = validate_dleq_proof (A_sum , scan_key , ecdh_share , dleq_proof )
177182 if not valid :
@@ -182,11 +187,14 @@ def validate_ecdh_coverage(psbt: PSBT) -> Tuple[bool, str]:
182187 for i , input_map in enumerate (psbt .i ):
183188 is_eligible , _ = is_input_eligible (input_map )
184189 ecdh_share = input_map .get_by_key (PSBT_IN_SP_ECDH_SHARE , scan_key )
185- if not is_eligible and ecdh_share :
186- return (
187- False ,
188- f"Input { i } has ECDH share but is ineligible for silent payments" ,
189- )
190+ # Disabled this check for now since it is not strictly forbidden by BIP-375
191+ if not is_eligible :
192+ continue
193+ # if not is_eligible and ecdh_share:
194+ # return (
195+ # False,
196+ # f"Input {i} has ECDH share but is ineligible for silent payments",
197+ # )
190198 if is_eligible and not ecdh_share :
191199 return (
192200 False ,
@@ -285,4 +293,60 @@ def validate_input_eligibility(psbt: PSBT) -> Tuple[bool, str]:
285293
286294
287295def validate_output_scripts (psbt : PSBT ) -> Tuple [bool , str ]:
288- return False , "Output scripts check not implemented yet"
296+ """
297+ Validate computed output scripts match silent payment derivation
298+
299+ Checks:
300+ - For each SP output with PSBT_OUT_SCRIPT set, recomputes the expected P2TR
301+ script from the ECDH share and input public keys and verifies it matches
302+ - k values are tracked per scan key and incremented for each SP output sharing
303+ the same scan key (outputs with different scan keys use independent k counters)
304+ """
305+ # Build outpoints list
306+ outpoints = []
307+ for input_map in psbt .i :
308+ if PSBT_IN_PREVIOUS_TXID in input_map and PSBT_IN_OUTPUT_INDEX in input_map :
309+ output_index_bytes = input_map .get (PSBT_IN_OUTPUT_INDEX )
310+ txid_int = int .from_bytes (input_map [PSBT_IN_PREVIOUS_TXID ], "little" )
311+ output_index = struct .unpack ("<I" , output_index_bytes )[0 ]
312+ outpoints .append (COutPoint (txid_int , output_index ))
313+
314+ # Track k values per scan key
315+ scan_key_k_values = {}
316+
317+ # Validate each SP output
318+ for output_idx , output_map in enumerate (psbt .o ):
319+ if PSBT_OUT_SP_V0_INFO not in output_map :
320+ continue # Skip non-SP outputs
321+
322+ sp_info = output_map [PSBT_OUT_SP_V0_INFO ]
323+ scan_pubkey_bytes = sp_info [:33 ]
324+ spend_pubkey_bytes = sp_info [33 :]
325+
326+ k = scan_key_k_values .get (scan_pubkey_bytes , 0 )
327+
328+ # Get ECDH share and summed pubkey
329+ ecdh_share_bytes , summed_pubkey_bytes = collect_input_ecdh_and_pubkey (
330+ psbt , scan_pubkey_bytes
331+ )
332+
333+ if ecdh_share_bytes and summed_pubkey_bytes and outpoints :
334+ computed_script = compute_silent_payment_output_script (
335+ outpoints , summed_pubkey_bytes , ecdh_share_bytes , spend_pubkey_bytes , k
336+ )
337+
338+ if PSBT_OUT_SCRIPT in output_map :
339+ actual_script = output_map [PSBT_OUT_SCRIPT ]
340+ if actual_script != computed_script :
341+ return (
342+ False ,
343+ f"Output { output_idx } script doesn't match silent payments derivation" ,
344+ )
345+
346+ scan_key_k_values [scan_pubkey_bytes ] = k + 1
347+ elif PSBT_OUT_SCRIPT in output_map :
348+ return (
349+ False ,
350+ f"Output { output_idx } has PSBT_OUT_SCRIPT but missing ECDH share or input pubkeys" ,
351+ )
352+ return True , None
0 commit comments