mirror of
https://github.com/bitcoin/bips.git
synced 2026-04-27 16:38:39 +00:00
BIP-375: add output scripts validation
Add support for computing bip352 output scripts Extract ECDH shares and public key from PSBT and aggregate both if necessary Refactor validate_ecdh_coverage to use collect_input_ecdh_and_pubkey
This commit is contained in:
@@ -9,9 +9,12 @@ input eligibility, and output script correctness.
|
||||
import struct
|
||||
from typing import Tuple
|
||||
|
||||
from deps.bitcoin_test.messages import COutPoint
|
||||
from deps.bitcoin_test.psbt import (
|
||||
PSBT,
|
||||
PSBT_GLOBAL_TX_MODIFIABLE,
|
||||
PSBT_IN_OUTPUT_INDEX,
|
||||
PSBT_IN_PREVIOUS_TXID,
|
||||
PSBT_IN_SIGHASH_TYPE,
|
||||
PSBT_IN_WITNESS_UTXO,
|
||||
PSBT_OUT_SCRIPT,
|
||||
@@ -19,7 +22,13 @@ from deps.bitcoin_test.psbt import (
|
||||
from deps.dleq import dleq_verify_proof
|
||||
from secp256k1lab.secp256k1 import GE
|
||||
|
||||
from .inputs import is_input_eligible, parse_witness_utxo, pubkey_from_eligible_input
|
||||
from .bip352_crypto import compute_silent_payment_output_script
|
||||
from .inputs import (
|
||||
collect_input_ecdh_and_pubkey,
|
||||
is_input_eligible,
|
||||
parse_witness_utxo,
|
||||
pubkey_from_eligible_input,
|
||||
)
|
||||
from .psbt_bip375 import (
|
||||
PSBT_GLOBAL_SP_ECDH_SHARE,
|
||||
PSBT_GLOBAL_SP_DLEQ,
|
||||
@@ -165,13 +174,9 @@ def validate_ecdh_coverage(psbt: PSBT) -> Tuple[bool, str]:
|
||||
if not dleq_proof:
|
||||
return False, "Global ECDH share missing DLEQ proof"
|
||||
|
||||
# Compute A_sum "input public keys" for global verification
|
||||
A_sum = None
|
||||
for input_map in psbt.i:
|
||||
pubkey = pubkey_from_eligible_input(input_map)
|
||||
if pubkey is not None:
|
||||
A_sum = pubkey if A_sum is None else A_sum + pubkey
|
||||
assert A_sum is not None, "No public keys found for inputs"
|
||||
_, summed_pubkey_bytes = collect_input_ecdh_and_pubkey(psbt, scan_key)
|
||||
assert summed_pubkey_bytes is not None, "No public keys found for inputs"
|
||||
A_sum = GE.from_bytes(summed_pubkey_bytes)
|
||||
|
||||
valid, msg = validate_dleq_proof(A_sum, scan_key, ecdh_share, dleq_proof)
|
||||
if not valid:
|
||||
@@ -182,11 +187,14 @@ def validate_ecdh_coverage(psbt: PSBT) -> Tuple[bool, str]:
|
||||
for i, input_map in enumerate(psbt.i):
|
||||
is_eligible, _ = is_input_eligible(input_map)
|
||||
ecdh_share = input_map.get_by_key(PSBT_IN_SP_ECDH_SHARE, scan_key)
|
||||
if not is_eligible and ecdh_share:
|
||||
return (
|
||||
False,
|
||||
f"Input {i} has ECDH share but is ineligible for silent payments",
|
||||
)
|
||||
# Disabled this check for now since it is not strictly forbidden by BIP-375
|
||||
if not is_eligible:
|
||||
continue
|
||||
# if not is_eligible and ecdh_share:
|
||||
# return (
|
||||
# False,
|
||||
# f"Input {i} has ECDH share but is ineligible for silent payments",
|
||||
# )
|
||||
if is_eligible and not ecdh_share:
|
||||
return (
|
||||
False,
|
||||
@@ -285,4 +293,60 @@ def validate_input_eligibility(psbt: PSBT) -> Tuple[bool, str]:
|
||||
|
||||
|
||||
def validate_output_scripts(psbt: PSBT) -> Tuple[bool, str]:
|
||||
return False, "Output scripts check not implemented yet"
|
||||
"""
|
||||
Validate computed output scripts match silent payment derivation
|
||||
|
||||
Checks:
|
||||
- For each SP output with PSBT_OUT_SCRIPT set, recomputes the expected P2TR
|
||||
script from the ECDH share and input public keys and verifies it matches
|
||||
- k values are tracked per scan key and incremented for each SP output sharing
|
||||
the same scan key (outputs with different scan keys use independent k counters)
|
||||
"""
|
||||
# Build outpoints list
|
||||
outpoints = []
|
||||
for input_map in psbt.i:
|
||||
if PSBT_IN_PREVIOUS_TXID in input_map and PSBT_IN_OUTPUT_INDEX in input_map:
|
||||
output_index_bytes = input_map.get(PSBT_IN_OUTPUT_INDEX)
|
||||
txid_int = int.from_bytes(input_map[PSBT_IN_PREVIOUS_TXID], "little")
|
||||
output_index = struct.unpack("<I", output_index_bytes)[0]
|
||||
outpoints.append(COutPoint(txid_int, output_index))
|
||||
|
||||
# Track k values per scan key
|
||||
scan_key_k_values = {}
|
||||
|
||||
# Validate each SP output
|
||||
for output_idx, output_map in enumerate(psbt.o):
|
||||
if PSBT_OUT_SP_V0_INFO not in output_map:
|
||||
continue # Skip non-SP outputs
|
||||
|
||||
sp_info = output_map[PSBT_OUT_SP_V0_INFO]
|
||||
scan_pubkey_bytes = sp_info[:33]
|
||||
spend_pubkey_bytes = sp_info[33:]
|
||||
|
||||
k = scan_key_k_values.get(scan_pubkey_bytes, 0)
|
||||
|
||||
# Get ECDH share and summed pubkey
|
||||
ecdh_share_bytes, summed_pubkey_bytes = collect_input_ecdh_and_pubkey(
|
||||
psbt, scan_pubkey_bytes
|
||||
)
|
||||
|
||||
if ecdh_share_bytes and summed_pubkey_bytes and outpoints:
|
||||
computed_script = compute_silent_payment_output_script(
|
||||
outpoints, summed_pubkey_bytes, ecdh_share_bytes, spend_pubkey_bytes, k
|
||||
)
|
||||
|
||||
if PSBT_OUT_SCRIPT in output_map:
|
||||
actual_script = output_map[PSBT_OUT_SCRIPT]
|
||||
if actual_script != computed_script:
|
||||
return (
|
||||
False,
|
||||
f"Output {output_idx} script doesn't match silent payments derivation",
|
||||
)
|
||||
|
||||
scan_key_k_values[scan_pubkey_bytes] = k + 1
|
||||
elif PSBT_OUT_SCRIPT in output_map:
|
||||
return (
|
||||
False,
|
||||
f"Output {output_idx} has PSBT_OUT_SCRIPT but missing ECDH share or input pubkeys",
|
||||
)
|
||||
return True, None
|
||||
|
||||
Reference in New Issue
Block a user