diff --git a/bip-0375/README.md b/bip-0375/README.md new file mode 100644 index 0000000000..13fa2c8566 --- /dev/null +++ b/bip-0375/README.md @@ -0,0 +1,52 @@ +# BIP 375 Reference Implementation + +This directory contains reference implementation for BIP 375: Sending Silent Payments with PSBTs. + +## Core Files +- **`constants.py`** - PSBT field type definitions +- **`parser.py`** - PSBT structure parsing +- **`inputs.py`** - Input validation helpers +- **`dleq.py`** - DLEQ proof validation +- **`validator.py`** - Main BIP 375 validator +- **`test_runner.py`** - Test infrastructure (executable) + +## Dependencies +- **`../bip-0374/reference.py`** - BIP 374 DLEQ proof reference +- **`../bip-0374/secp256k1.py`** - secp256k1 implementation + +## Testing + +### Test Vectors +- **`test_vectors.json`** - 17 test vectors (13 invalid + 4 valid) covering: + - Invalid input types (P2MS, non-standard scripts) + - Missing/invalid DLEQ proofs + - ECDH share validation + - Output script verification + - SIGHASH requirements + - BIP-352 output address matching + +### Generating Test Vectors + +Test vectors were generated using [test_generator.py](https://github.com/macgyver13/bip375-examples/blob/main/python/tests/test_generator.py) + +### Run Tests + +```bash +python test_runner.py # Run all tests +python test_runner.py -v # Verbose mode with detailed errors +``` + +**Expected output:** All 17 tests should pass validation (4 valid accepted, 13 invalid rejected). + +## Validation Layers + +The validator implements progressive validation: +1. **PSBT Structure** - Parse PSBT v2 format +2. **Input Eligibility** - Validate eligible input types (P2PKH, P2WPKH, P2TR, P2SH-P2WPKH) +3. **DLEQ Proofs** - Verify ECDH share correctness using BIP-374 +4. **Output Fields** - Check PSBT_OUT_SCRIPT or PSBT_OUT_SP_V0_INFO requirements +5. **BIP-352 Outputs** - Validate output scripts match expected silent payment addresses + +## Examples + +Demo implementations using this reference can be found in [bip375-examples](https://github.com/macgyver13/bip375-examples/) diff --git a/bip-0375/constants.py b/bip-0375/constants.py new file mode 100644 index 0000000000..027568f8c8 --- /dev/null +++ b/bip-0375/constants.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +""" +BIP 375: PSBT Field Type Constants + +Minimal BIP 375 field types needed for PSBT v2 validation with silent payments. +""" + + +class PSBTFieldType: + """Minimal BIP 375 field types needed for reference validator""" + + # Global fields (required for validation) + PSBT_GLOBAL_TX_VERSION = 0x02 + PSBT_GLOBAL_INPUT_COUNT = 0x04 + PSBT_GLOBAL_OUTPUT_COUNT = 0x05 + PSBT_GLOBAL_VERSION = 0xFB + PSBT_GLOBAL_SP_ECDH_SHARE = 0x07 + PSBT_GLOBAL_SP_DLEQ = 0x08 + + # Input fields (required for validation) + PSBT_IN_NON_WITNESS_UTXO = 0x00 + PSBT_IN_WITNESS_UTXO = 0x01 + PSBT_IN_PARTIAL_SIG = 0x02 + PSBT_IN_SIGHASH_TYPE = 0x03 + PSBT_IN_REDEEM_SCRIPT = 0x04 + PSBT_IN_BIP32_DERIVATION = 0x06 + PSBT_IN_PREVIOUS_TXID = 0x0E + PSBT_IN_OUTPUT_INDEX = 0x0F + PSBT_IN_TAP_INTERNAL_KEY = 0x17 + PSBT_IN_SP_ECDH_SHARE = 0x1D + PSBT_IN_SP_DLEQ = 0x1E + + # Output fields (required for validation) + PSBT_OUT_AMOUNT = 0x03 + PSBT_OUT_SCRIPT = 0x04 + PSBT_OUT_SP_V0_INFO = 0x09 + PSBT_OUT_SP_V0_LABEL = 0x0A diff --git a/bip-0375/dleq.py b/bip-0375/dleq.py new file mode 100644 index 0000000000..6d7aa8ebd0 --- /dev/null +++ b/bip-0375/dleq.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +BIP 375: DLEQ Proof Validation + +Functions for validating DLEQ proofs on ECDH shares in PSBTs. +""" + +from typing import Dict, List, Optional, Tuple + +from constants import PSBTFieldType +# External references bip-0374 +from reference import dleq_verify_proof +from secp256k1 import GE + + +def extract_dleq_components( + dleq_field: Dict, ecdh_field: Dict +) -> Tuple[bytes, bytes, bytes]: + """Extract and validate DLEQ proof components from PSBT fields""" + + # Extract key and value components + proof = dleq_field["value"] + dleq_scan_key_bytes = dleq_field["key"] + ecdh_share_bytes = ecdh_field["value"] + ecdh_scan_key_bytes = ecdh_field["key"] + + # Validate proof length + if len(proof) != 64: + raise ValueError(f"Invalid DLEQ proof length: {len(proof)} bytes (expected 64)") + + # Validate BIP 375 key-value structure + if len(ecdh_scan_key_bytes) != 33: + raise ValueError( + f"Invalid ECDH scan key length: {len(ecdh_scan_key_bytes)} bytes (expected 33)" + ) + if len(ecdh_share_bytes) != 33: + raise ValueError( + f"Invalid ECDH share length: {len(ecdh_share_bytes)} bytes (expected 33)" + ) + if len(dleq_scan_key_bytes) != 33: + raise ValueError( + f"Invalid DLEQ scan key length: {len(dleq_scan_key_bytes)} bytes (expected 33)" + ) + + # Verify scan keys match between ECDH and DLEQ fields + if ecdh_scan_key_bytes != dleq_scan_key_bytes: + raise ValueError("Scan key mismatch between ECDH and DLEQ fields") + + return proof, ecdh_scan_key_bytes, ecdh_share_bytes + + +def get_pubkey_from_input(input_fields: Dict[int, bytes]) -> Optional[GE]: + """Extract public key from PSBT input fields""" + # Try BIP32 derivation field (highest priority, BIP-174 standard) + if PSBTFieldType.PSBT_IN_BIP32_DERIVATION in input_fields: + derivation_data = input_fields[PSBTFieldType.PSBT_IN_BIP32_DERIVATION] + if isinstance(derivation_data, dict): + pubkey_candidate = derivation_data.get("key", b"") + if len(pubkey_candidate) == 33: + return GE.from_bytes(pubkey_candidate) + + return None + + +def validate_global_dleq_proof( + global_fields: Dict[int, bytes], + input_maps: List[Dict[int, bytes]] = None, + input_keys: List[Dict] = None, +) -> bool: + """Validate global DLEQ proof using BIP 374 implementation""" + + if PSBTFieldType.PSBT_GLOBAL_SP_DLEQ not in global_fields: + return False + if PSBTFieldType.PSBT_GLOBAL_SP_ECDH_SHARE not in global_fields: + return False + + # Extract and validate components + try: + proof, scan_key_bytes, ecdh_share_bytes = extract_dleq_components( + global_fields[PSBTFieldType.PSBT_GLOBAL_SP_DLEQ], + global_fields[PSBTFieldType.PSBT_GLOBAL_SP_ECDH_SHARE], + ) + except ValueError: + return False + + # Convert to GE points + B = GE.from_bytes(scan_key_bytes) # scan key + C = GE.from_bytes(ecdh_share_bytes) # ECDH result + + # For global ECDH shares, we need to combine all input public keys + # According to BIP 375: "Let A_n be the sum of the public keys A of all eligible inputs" + A_combined = None + + # Extract and combine public keys from PSBT fields (preferred, BIP-174 standard) + for input_fields in input_maps: + input_pubkey = get_pubkey_from_input(input_fields) + + if input_pubkey is not None: + if A_combined is None: + A_combined = input_pubkey + else: + A_combined = A_combined + input_pubkey + + if A_combined is None: + return False + + return dleq_verify_proof(A_combined, B, C, proof) + + +def validate_input_dleq_proof( + input_fields: Dict[int, bytes], + input_keys: List[Dict] = None, + input_index: int = None, +) -> bool: + """Validate input DLEQ proof using BIP 374 implementation""" + + if PSBTFieldType.PSBT_IN_SP_DLEQ not in input_fields: + return False + if PSBTFieldType.PSBT_IN_SP_ECDH_SHARE not in input_fields: + return False + + # Extract and validate components + try: + proof, scan_key_bytes, ecdh_share_bytes = extract_dleq_components( + input_fields[PSBTFieldType.PSBT_IN_SP_DLEQ], + input_fields[PSBTFieldType.PSBT_IN_SP_ECDH_SHARE], + ) + except ValueError: + return False + + # Convert to GE points + B = GE.from_bytes(scan_key_bytes) # scan key + C = GE.from_bytes(ecdh_share_bytes) # ECDH result + + # Extract input public key A from available sources + A = get_pubkey_from_input(input_fields) + + if A is None: + return False + + # Perform DLEQ verification + return dleq_verify_proof(A, B, C, proof) diff --git a/bip-0375/inputs.py b/bip-0375/inputs.py new file mode 100644 index 0000000000..7e20da952f --- /dev/null +++ b/bip-0375/inputs.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +BIP 375: Input Validation Helpers + +Functions for validating PSBT input types and checking segwit versions. +""" + +import struct +from typing import Optional + +from constants import PSBTFieldType + + +def check_invalid_segwit_version(witness_utxo: bytes) -> bool: + """Check if witness UTXO uses invalid segwit version for silent payments""" + + # Skip amount (8 bytes) and script length + if len(witness_utxo) < 9: + return False + + offset = 8 # Skip amount + script_len = witness_utxo[offset] + offset += 1 + + if offset + script_len > len(witness_utxo): + return False + + script = witness_utxo[offset : offset + script_len] + + # Check if it's segwit v2 or higher + if len(script) >= 2 and script[0] >= 0x52: # OP_2 or higher + return True + + return False + + +def get_input_script_pubkey(input_fields: dict) -> Optional[bytes]: + """Extract scriptPubKey from PSBT input fields""" + script_pubkey = None + + # Try WITNESS_UTXO first (segwit inputs) + if PSBTFieldType.PSBT_IN_WITNESS_UTXO in input_fields: + witness_utxo = input_fields[PSBTFieldType.PSBT_IN_WITNESS_UTXO] + # Extract scriptPubKey from witness_utxo (skip 8-byte amount + 1-byte length) + if len(witness_utxo) >= 9: + script_len = witness_utxo[8] + script_pubkey = witness_utxo[9 : 9 + script_len] + + # Try NON_WITNESS_UTXO for legacy inputs (P2PKH, P2SH) + elif PSBTFieldType.PSBT_IN_NON_WITNESS_UTXO in input_fields: + non_witness_utxo = input_fields[PSBTFieldType.PSBT_IN_NON_WITNESS_UTXO] + # Get the output index from PSBT_IN_OUTPUT_INDEX field + if PSBTFieldType.PSBT_IN_OUTPUT_INDEX in input_fields: + output_index_bytes = input_fields[PSBTFieldType.PSBT_IN_OUTPUT_INDEX] + if len(output_index_bytes) == 4: + output_index = struct.unpack(" tuple[bool, str]: + """Validate that input is an eligible type for silent payments""" + + script_pubkey = get_input_script_pubkey(input_fields) + + if script_pubkey is None: + return False, f"Input {input_index} missing UTXO information" + + if not is_eligible_input_type(script_pubkey): + return False, f"Input {input_index} uses ineligible input type" + + # For P2SH, verify it's P2SH-P2WPKH + if is_p2sh(script_pubkey): + if PSBTFieldType.PSBT_IN_REDEEM_SCRIPT in input_fields: + redeem_script = input_fields[PSBTFieldType.PSBT_IN_REDEEM_SCRIPT] + # Verify redeemScript is P2WPKH + if not is_p2wpkh(redeem_script): + return False, f"Input {input_index} P2SH is not P2SH-P2WPKH" + else: + return False, f"Input {input_index} P2SH missing PSBT_IN_REDEEM_SCRIPT" + + return True, "Input is eligible" + + +# ===================================================== +# Silent Payments Utilities +# ===================================================== + +def is_p2tr(spk: bytes) -> bool: + if len(spk) != 34: + return False + # OP_1 OP_PUSHBYTES_32 <32 bytes> + return (spk[0] == 0x51) & (spk[1] == 0x20) + + +def is_p2wpkh(spk: bytes) -> bool: + if len(spk) != 22: + return False + # OP_0 OP_PUSHBYTES_20 <20 bytes> + return (spk[0] == 0x00) & (spk[1] == 0x14) + + +def is_p2sh(spk: bytes) -> bool: + if len(spk) != 23: + return False + # OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL + return (spk[0] == 0xA9) & (spk[1] == 0x14) & (spk[-1] == 0x87) + + +def is_p2pkh(spk: bytes) -> bool: + if len(spk) != 25: + return False + # OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG + return ( + (spk[0] == 0x76) + & (spk[1] == 0xA9) + & (spk[2] == 0x14) + & (spk[-2] == 0x88) + & (spk[-1] == 0xAC) + ) + + +def is_eligible_input_type(script_pubkey: bytes) -> bool: + """Check if scriptPubKey is an eligible input type for silent payments per BIP-352""" + return ( + is_p2pkh(script_pubkey) + or is_p2wpkh(script_pubkey) + or is_p2tr(script_pubkey) + or is_p2sh(script_pubkey) + ) + + +def parse_non_witness_utxo(non_witness_utxo: bytes, output_index: int) -> bytes: + """Extract scriptPubKey from NON_WITNESS_UTXO field""" + try: + offset = 0 + + # Skip version (4 bytes) + if len(non_witness_utxo) < 4: + return None + offset += 4 + + # Parse input count (compact size) + if offset >= len(non_witness_utxo): + return None + + input_count = non_witness_utxo[offset] + offset += 1 + if input_count >= 0xFD: + # Handle larger compact size (simplified - just skip) + if input_count == 0xFD: + offset += 2 + elif input_count == 0xFE: + offset += 4 + else: + offset += 8 + input_count = ( + struct.unpack("= len(non_witness_utxo): + return None + + # Skip scriptSig + script_len = non_witness_utxo[offset] + offset += 1 + if script_len >= 0xFD: + return None # Simplified - don't handle large scripts + offset += script_len + + # Skip sequence (4) + offset += 4 + if offset > len(non_witness_utxo): + return None + + # Parse output count + if offset >= len(non_witness_utxo): + return None + output_count = non_witness_utxo[offset] + offset += 1 + if output_count >= 0xFD: + if output_count == 0xFD: + output_count = struct.unpack( + "= len(non_witness_utxo): + return None + offset += 8 + + # Parse scriptPubKey length + if offset >= len(non_witness_utxo): + return None + script_len = non_witness_utxo[offset] + offset += 1 + if script_len >= 0xFD: + if script_len == 0xFD: + script_len = struct.unpack( + " len(non_witness_utxo): + return None + return non_witness_utxo[offset : offset + script_len] + + # Otherwise skip to next output + offset += script_len + if offset > len(non_witness_utxo): + return None + + return None + except Exception: + return None diff --git a/bip-0375/parser.py b/bip-0375/parser.py new file mode 100644 index 0000000000..a21e1a1472 --- /dev/null +++ b/bip-0375/parser.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +BIP 375: PSBT Structure Parser + +Functions for parsing PSBT v2 structure into global, input, and output field maps. +""" + +import struct +from typing import Dict, List, Tuple + +from constants import PSBTFieldType + + +def parse_psbt_structure( + psbt_data: bytes, +) -> Tuple[Dict[int, bytes], List[Dict[int, bytes]], List[Dict[int, bytes]]]: + """Parse PSBT structure into global, input, and output field maps""" + + def parse_compact_size_uint(data: bytes, offset: int) -> Tuple[int, int]: + """Parse compact size uint and return (value, new_offset)""" + if offset >= len(data): + raise ValueError("Not enough data") + + first_byte = data[offset] + if first_byte < 0xFD: + return first_byte, offset + 1 + elif first_byte == 0xFD: + return struct.unpack(" Tuple[Dict[int, bytes], int]: + """Parse a PSBT section and return (field_map, new_offset)""" + fields = {} + + while offset < len(data): + # Read key length + key_len, offset = parse_compact_size_uint(data, offset) + if key_len == 0: # End of section + break + + # Read key data + if offset + key_len > len(data): + raise ValueError("Truncated key data") + key_data = data[offset : offset + key_len] + offset += key_len + + # Read value length + value_len, offset = parse_compact_size_uint(data, offset) + + # Read value data + if offset + value_len > len(data): + raise ValueError("Truncated value data") + value_data = data[offset : offset + value_len] + offset += value_len + + # Extract field type and handle key-value pairs + if key_data: + field_type = key_data[0] + key_content = key_data[1:] if len(key_data) > 1 else b"" + + # For BIP 375 and BIP-174 key-value fields, store both key and value + if field_type in [ + PSBTFieldType.PSBT_GLOBAL_SP_ECDH_SHARE, + PSBTFieldType.PSBT_GLOBAL_SP_DLEQ, + PSBTFieldType.PSBT_IN_SP_ECDH_SHARE, + PSBTFieldType.PSBT_IN_SP_DLEQ, + PSBTFieldType.PSBT_IN_BIP32_DERIVATION, + PSBTFieldType.PSBT_IN_PARTIAL_SIG, + ]: + fields[field_type] = {"key": key_content, "value": value_data} + else: + # For standard PSBT fields, just store value + fields[field_type] = value_data + + return fields, offset + + if len(psbt_data) < 5 or psbt_data[:5] != b"psbt\xff": + raise ValueError("Invalid PSBT magic") + + offset = 5 + + # Parse global section + global_fields, offset = parse_section(psbt_data, offset) + + # Determine number of inputs and outputs (standard PSBT fields) + num_inputs = ( + global_fields.get(PSBTFieldType.PSBT_GLOBAL_INPUT_COUNT, b"\x00")[0] + if PSBTFieldType.PSBT_GLOBAL_INPUT_COUNT in global_fields + else 1 + ) + num_outputs = ( + global_fields.get(PSBTFieldType.PSBT_GLOBAL_OUTPUT_COUNT, b"\x00")[0] + if PSBTFieldType.PSBT_GLOBAL_OUTPUT_COUNT in global_fields + else 1 + ) + + # Parse input sections + input_maps = [] + for _ in range(num_inputs): + input_fields, offset = parse_section(psbt_data, offset) + input_maps.append(input_fields) + + # Parse output sections + output_maps = [] + for _ in range(num_outputs): + output_fields, offset = parse_section(psbt_data, offset) + output_maps.append(output_fields) + + return global_fields, input_maps, output_maps diff --git a/bip-0375/test_runner.py b/bip-0375/test_runner.py new file mode 100644 index 0000000000..29c37f1fea --- /dev/null +++ b/bip-0375/test_runner.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +BIP 375: Test Runner + +Validates BIP 375 PSBT test vectors. +""" + +import argparse +import base64 +import json +import os +import sys + +# Add sibling directory bip-374 to path before to make secp256k1 and dleq reference available +current_dir = os.path.dirname(os.path.abspath(__file__)) +sibling_dir_path = os.path.join(current_dir, '..', 'bip-0374') +if sibling_dir_path not in sys.path: + sys.path.append(sibling_dir_path) +from validator import validate_bip375_psbt + + +def load_test_vectors(filename: str) -> dict: + """Load test vectors from JSON file""" + try: + with open(filename, "r") as f: + return json.load(f) + except FileNotFoundError: + print(f"Error: Test vector file '{filename}' not found") + sys.exit(1) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON in test vector file: {e}") + sys.exit(1) + + +def run_validation(psbt_b64: str, test_case: dict = None) -> tuple[bool, str]: + """Run BIP 375 validation on a PSBT""" + try: + # Decode PSBT + psbt_data = base64.b64decode(psbt_b64) + + # Extract optional test material for BIP-352 validation + input_keys = test_case.get("input_keys") if test_case else None + + # Run complete BIP 375 validation + return validate_bip375_psbt(psbt_data, input_keys) + + except Exception as e: + return False, f"Validation error: {str(e)}" + + +def run_tests(test_data: dict, verbose: bool = False) -> None: + """Run complete BIP 375 validation on all test cases""" + + print("BIP 375 Reference Implementation - Test Runner (Complete Validation)") + print("=" * 78) + print(f"Description: {test_data['description']}") + print(f"Version: {test_data['version']}") + print(f"Invalid test cases: {len(test_data['invalid'])}") + print(f"Valid test cases: {len(test_data['valid'])}") + + test_num = 1 + passed = 0 + failed = 0 + + # Run invalid test cases + print("\n=== Running Invalid Test Cases ===") + for test_case in test_data["invalid"]: + description = test_case["description"] + psbt_b64 = test_case["psbt"] + + print(f"Test {test_num}: {description}") + + is_valid, msg = run_validation(psbt_b64, test_case) + + if verbose: + print(f" Result: {msg}") + + # For invalid cases, we expect validation to reject them + if not is_valid: + passed += 1 + else: + failed += 1 + print(f" FAILED: Expected invalid but got: {msg}") + + test_num += 1 + + # Run valid test cases + print() + print("=== Running Valid Test Cases ===") + for test_case in test_data["valid"]: + description = test_case["description"] + psbt_b64 = test_case["psbt"] + + print(f"Test {test_num}: {description}") + + is_valid, msg = run_validation(psbt_b64, test_case) + + if verbose: + print(f" Result: {msg}") + + if is_valid: + passed += 1 + else: + failed += 1 + print(f" FAILED: {msg}") + + test_num += 1 + + print(f"\n✓ Validation complete: {passed} passed, {failed} failed") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="BIP 375 Reference Implementation - Test Runner", + ) + parser.add_argument( + "--test-file", + "-f", + default="test_vectors.json", + help="Test vector file to run (default: test_vectors.json)", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Show detailed output for each test", + ) + + args = parser.parse_args() + + # Load test vectors + test_data = load_test_vectors(args.test_file) + + # Run tests + run_tests(test_data, args.verbose) diff --git a/bip-0375/test_vectors.json b/bip-0375/test_vectors.json new file mode 100644 index 0000000000..53a13e7092 --- /dev/null +++ b/bip-0375/test_vectors.json @@ -0,0 +1,769 @@ +{ + "description": "BIP 375 Test Vectors - All Scenarios", + "version": "1.0", + "format_notes": [ + "All keys are hex-encoded", + "PSBTs have all necessary fields", + "Test vectors are organized into 'invalid' and 'valid' arrays", + "Comment provides additional context for all test cases" + ], + "invalid": [ + { + "description": "Missing DLEQ proof for ECDH share", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiCrtYht20vGCALx8ZiisSkDZZzJ7nPgIx1FVehBiNyWQAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8BAwQBAAAAAAEDCBhzAQAAAAAAAQQiUSAXu7qlEuAw+8o+5RT3sjgH4RGDGkdFRHCRNMn5v0a2xAEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "abb5886ddb4bc60802f1f198a2b12903659cc9ee73e0231d4555e84188dc9640", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": null, + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "512017bbbaa512e030fbca3ee514f7b23807e111831a474544709134c9f9bf46b6c4", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "PSBT_IN_SP_ECDH_SHARE without corresponding PSBT_IN_SP_DLEQ" + }, + { + "description": "Invalid DLEQ proof", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiBJpgB4JAmRsNt6srOq9JqFCEYGmw7mPo8/4lJ2+/nPoAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+ECB1dab73m7/xP6o4gDeMISP8SUJ7Lb0cTBayb1WDeYZi1u5ekG4K3g7oO2Cor11Y5A758GvXy2nF0tx1E7u/5hAQMEAQAAAAABAwgYcwEAAAAAAAEEIlEgsuRy0TOUWM9fKDzk0MnUwzY/UdSp9SkkH4SdztRbddABCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "49a60078240991b0db7ab2b3aaf49a850846069b0ee63e8f3fe25276fbf9cfa0", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "81d5d69bef79bbff13faa3880378c2123fc49427b2dbd1c4c16b26f5583798662d6ee5e906e0ade0ee83b60a8af5d58e40ef9f06bd7cb69c5d2dc7513bbbfe61", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120b2e472d1339458cf5f283ce4d0c9d4c3363f51d4a9f529241f849dced45b75d0", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "DLEQ proof verification failed" + }, + { + "description": "Non-SIGHASH_ALL signature with silent payments", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiCfF8iU72tyS4+7lJj0TTc8KFCARRz/QDHgOTLIuPm2twEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQCAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAfNn2bwSJU+NoXtyj8XaZFSWYvOykbyFO0rjN/aSdtqAHj4lLZt/ZimcG5oc68kn132TsyYRE6RNyl32W+tgrQwABAwgYcwEAAAAAAAEEIlEgdE4JlR8XtYJIPHoMCMBib+rDZmk8hPrzngNLYS3o3HEBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "9f17c894ef6b724b8fbb9498f44d373c285080451cff4031e03932c8b8f9b6b7", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "7cd9f66f048953e3685edca3f17699152598bceca46f214ed2b8cdfda49db6a0078f894b66dfd98a6706e6873af249f5df64ecc98444e91372977d96fad82b43", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120744e09951f17b582483c7a0c08c0626feac366693c84faf39e034b612de8dc71", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "Silent payment outputs require SIGHASH_ALL signatures only" + }, + { + "description": "Mixed segwit versions with silent payments", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiCuv6p7LnQnsTi6F7UesBJ3TJOXBoHuPhb2Y3hjUklAJAEPBAAAAAABASughgEAAAAAACJSIIYiz2yIzTx5hSOFG+T1WYNgEYY+sDBYObtzXueh9KPpARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAwdZ/OHiCv4F5FeoVgh1k1bODXB/AXFWM49gkkqF9kIwQSH27m93BCAyMLZwZcbJmCDxpBU4iGT8kQSisZnN+YAABAwgYcwEAAAAAAAEEIlEgIZ6/eW18O5peg4tLa+KXfHTrjWr+b09YgAaz1sNh63wBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "aebfaa7b2e7427b138ba17b51eb012774c93970681ee3e16f663786352494024", + "prevout_index": 0, + "prevout_scriptpubkey": "52208622cf6c88cd3c798523851be4f559836011863eb0305839bb735ee7a1f4a3e9", + "amount": 100000, + "witness_utxo": "a0860100000000002252208622cf6c88cd3c798523851be4f559836011863eb0305839bb735ee7a1f4a3e9", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120219ebf796d7c3b9a5e838b4b6be2977c74eb8d6afe6f4f588006b3d6c361eb7c", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "mixed_segwit" + }, + { + "description": "Silent payment outputs but no ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiAkKwMCMdEgYFJ2eRmpLxfpzUNydadt47rD+2VnyyekwwEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAAAEDCBhzAQAAAAAAAQQiUSAE49PFUgTAYIPf3lo2ltGczidvHq5eXgsw3uhama8pVgEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "242b030231d1206052767919a92f17e9cd437275a76de3bac3fb6567cb27a4c3", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "512004e3d3c55204c06083dfde5a3696d19cce276f1eae5e5e0b30dee85a99af2956", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "no_ecdh_shares" + }, + { + "description": "Global ECDH share without DLEQ proof", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/wABDiD2W3/BmfoPsrzNsfoGEte1D2K0+PqV1WXEoB9MWC6SpAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAAAEDCBhzAQAAAAAAAQQiUSCBAAI55HC7UjfZ+r6wGeJ5j8RXLnnJOQtdHV42G7fgIAEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "f65b7fc199fa0fb2bccdb1fa0612d7b50f62b4f8fa95d565c4a01f4c582e92a4", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": null, + "is_global": true, + "input_index": null + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "512081000239e470bb5237d9fabeb019e2798fc4572e79c9390b5d1d5e361bb7e020", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "missing_global_dleq" + }, + { + "description": "Wrong SP_V0_INFO field size", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiBhb07a0EqylXgp4bK5tBHX2y2Sc2xQAy4NalHiyZy13gEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAwdZ/OHiCv4F5FeoVgh1k1bODXB/AXFWM49gkkqF9kIwQSH27m93BCAyMLZwZcbJmCDxpBU4iGT8kQSisZnN+YAABAwgYcwEAAAAAAAEEIlEgqx9NHhF5eGF2DneK22zBZvlqth3Rr4eZ5lH0FTIh7JMBCUEC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRAA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "616f4edad04ab2957829e1b2b9b411d7db2d92736c50032e0d6a51e2c99cb5de", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120ab1f4d1e11797861760e778adb6cc166f96ab61dd1af8799e651f4153221ec93", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde4921144", + "sp_label": null + } + ], + "comment": "wrong_sp_info_size" + }, + { + "description": "Mixed eligible and ineligible input types", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAIAAAABBQQBAAAAAQYBAwABDiApw6Peut1f6dS0XD295x0CWioK+UM7tO+6i0Pv5za0oQEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAAiHQLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+CECVRZOeSbVDVKgn/mQZHpelcHbG/xophb7wtohOSf5i/8iHgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+EDB1n84eIK/gXkV6hWCHWTVs4NcH8BcVYzj2CSSoX2QjBBIfbub3cEIDIwtnBlxsmYIPGkFTiIZPyRBKKxmc35gAAEOILbM9qpt6XebBi77G8FTHGWl57KukFWmYqsFQG8N9EfUAQ8EAAAAAAEQBP7///8BAFMCAAAAAeQ/TH2g2e+DCDNBekstaV4FoHWp1MBWN7X86t8ZX5WyAAAAAAD/////AfBJAgAAAAAAF6kUY3evt1S5inYSGc8WdvySwjeEULaHAAAAAAEER1IhAo8dCC1gAfpLqJmkA9r5sdsBvpJcIlM69jDuZJOwvp95IQM1iX7FrnBP5V2OpdRnsK69s3mb4Qw2BHHZt4nIEa/eO1KuAAEDCJBfAQAAAAAAAQQiUSDsAiz0NrWnm7cXwbLngQA8S0LMDyzAhBLXhwYsyqNHgwEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "29c3a3debadd5fe9d4b45c3dbde71d025a2a0af9433bb4efba8b43efe736b4a1", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + }, + { + "input_index": 1, + "private_key": "b0d095ddc9d046be37f1f083e2993b2c7ace820a0696c730d930a4eba61fc056", + "public_key": "028f1d082d6001fa4ba899a403daf9b1db01be925c22533af630ee6493b0be9f79", + "prevout_txid": "b6ccf6aa6de9779b062efb1bc1531c65a5e7b2ae9055a662ab05406f0df447d4", + "prevout_index": 0, + "prevout_scriptpubkey": "a9146377afb754b98a761219cf1676fc92c2378450b687", + "amount": 150000, + "witness_utxo": "0200000001e43f4c7da0d9ef830833417a4b2d695e05a075a9d4c05637b5fceadf195f95b20000000000ffffffff01f04902000000000017a9146377afb754b98a761219cf1676fc92c2378450b68700000000", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 90000, + "script": "5120ec022cf436b5a79bb717c1b2e781003c4b42cc0f2cc08412d787062ccaa34783", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "P2WPKH and P2SH multisig mixed - only P2WPKH is eligible" + }, + { + "description": "Wrong ECDH share size", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiC1ovhHRrNOgzq0ftE7S5hwtBwhRvo0a3NKnEftiiROwwEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPggAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+YsAAQMIGHMBAAAAAAABBCJRIIAwkkgvJinSnZhZELCCxoyiB8DtQD2Nh7Ac6dO/cfgBAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "b5a2f84746b34e833ab47ed13b4b9870b41c2146fa346b734a9c47ed8a244ec3", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120803092482f2629d29d985910b082c68ca207c0ed403d8d87b01ce9d3bf71f801", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "ECDH share must be 33 bytes" + }, + { + "description": "Wrong DLEQ proof size", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiA78BpakVw0yvjzRht09tYBDcMoUSAE43p653aHic5dEwEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEDCBhzAQAAAAAAAQQiUSDtdP0G8Hjd7PFuoDosE3hSYzYMLSEIV++xEaqAMWmt+wEJQgLQKf+W3iy894K+Q1nEhiDqkrzda+8DK5UVi5GhaT+0+AJNUYNT9L0Y12nPaP9i7xBmm3CGJGsKZAP+V73kkhFEiwA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "3bf01a5a915c34caf8f3461b74f6d6010dc328512004e37a7ae7768789ce5d13", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120ed74fd06f078ddecf16ea03a2c13785263360c2d210857efb111aa803169adfb", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "DLEQ proof must be 64 bytes" + }, + { + "description": "Label without SP_V0_INFO", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiCYIQIrPQiMfZ8vxvALHE0JCEOOoHYl2kTD+2DBdIFEqgEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAAAAQMIGHMBAAAAAAABBCJRICPE97d2KoVw4KmW77zDea7n/IowriFuMhY4RHSeKDQ7AQoEAQAAAAA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "9821022b3d088c7d9f2fc6f00b1c4d0908438ea07625da44c3fb60c1748144aa", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [], + "expected_ecdh_shares": [], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "512023c4f7b7762a8570e0a996efbcc379aee7fc8a30ae216e32163844749e28343b", + "is_silent_payment": false, + "sp_info": null, + "sp_label": null + } + ], + "comment": "PSBT_OUT_SP_V0_LABEL requires PSBT_OUT_SP_V0_INFO" + }, + { + "description": "Address mismatch", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAwABDiBumss8ldbK7k1KciUCaeuPPQVy7U+E9Lf8M6mavj46BQEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAwdZ/OHiCv4F5FeoVgh1k1bODXB/AXFWM49gkkqF9kIwQSH27m93BCAyMLZwZcbJmCDxpBU4iGT8kQSisZnN+YAABAwgYcwEAAAAAAAEEIlEgzeOdiwW0lvjxjyCyfQr5oyMzCeBHIhnggT/ztdN4fikBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "6e9acb3c95d6caee4d4a72250269eb8f3d0572ed4f84f4b7fc33a99abe3e3a05", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120cde39d8b05b496f8f18f20b27d0af9a3233309e0472219e0813ff3b5d3787e29", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "Output script doesn't match BIP-352 computed address" + }, + { + "description": "Both global and per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIIAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQ4gXPiMRKRM1SJjYb7RB1GUb5+HCVIpEeB3gV9ffweFr2kBDwQAAAAAAQEfoIYBAAAAAAAWABT42vdq2AOw76ldbP+K7giR068cjAEQBP7///8iBgPTV/fAcY8keOP9j4zMJynd2MDMrrHwKxgab0TUO5+NjQQAAAAAAQMEAQAAACIdAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIeAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQMIGHMBAAAAAAABBCJRIEun4ulLywKN+7koGWi1ftwp9nBXsfyyF920UMMaUccMAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "5cf88c44a44cd5226361bed10751946f9f8709522911e077815f5f7f0785af69", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": true, + "input_index": null + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "51204ba7e2e94bcb028dfbb9281968b57edc29f67057b1fcb217ddb450c31a51c70c", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "Cannot have both global and per-input ECDH shares for same scan key" + } + ], + "valid": [ + { + "description": "Single signer with global ECDH share", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQBAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIIAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQ4gbprLPJXWyu5NSnIlAmnrjz0Fcu1PhPS3/DOpmr4+OgUBDwQAAAAAAQEfoIYBAAAAAAAWABT42vdq2AOw76ldbP+K7giR068cjAEQBP7///8iBgPTV/fAcY8keOP9j4zMJynd2MDMrrHwKxgab0TUO5+NjQQAAAAAAQMEAQAAAAABAwgYcwEAAAAAAAEEIlEgrhn77icwoalS19JZjMcD/d87lyslFIse0aea6HOdXgcBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "6e9acb3c95d6caee4d4a72250269eb8f3d0572ed4f84f4b7fc33a99abe3e3a05", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": true, + "input_index": null + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "5120ae19fbee2730a1a952d7d2598cc703fddf3b972b25148b1ed1a79ae8739d5e07", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "One entity controls all inputs, uses global approach for efficiency" + }, + { + "description": "Multi-party with per-input ECDH shares", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAIAAAABBQQBAAAAAQYBAwABDiBc+IxEpEzVImNhvtEHUZRvn4cJUikR4HeBX19/B4WvaQEPBAAAAAABAR9QwwAAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAwdZ/OHiCv4F5FeoVgh1k1bODXB/AXFWM49gkkqF9kIwQSH27m93BCAyMLZwZcbJmCDxpBU4iGT8kQSisZnN+YAABDiAT0u662aEJkUdBDIam/gsg7p23WdpBz9Tul5LNUBI3FQEPBAAAAAABAR9QwwAAAAAAABYAFEIccVrt+YOvDjtnb/fElNEFT826ARAE/v///yIGAo8dCC1gAfpLqJmkA9r5sdsBvpJcIlM69jDuZJOwvp95BAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghA1voNBApxyR/ork41dS4Wzpp+9kxe62Fr2V5knHctCgBIh4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAwP4szFLfu5Jb0/9lS3qW2OwOAxpn0EG0+Zyw6BFJ4oeoK9PVG8D/czrOfTKrY9YSGGVFd4CPssf7BtK+Bv86tgABAwgYcwEAAAAAAAEEIlEgC9xqHau4dRdnjCtffc3/KXbcXRE1xN2T9mcj6++5jiUBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsA", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "5cf88c44a44cd5226361bed10751946f9f8709522911e077815f5f7f0785af69", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 50000, + "witness_utxo": "50c3000000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + }, + { + "input_index": 1, + "private_key": "b0d095ddc9d046be37f1f083e2993b2c7ace820a0696c730d930a4eba61fc056", + "public_key": "028f1d082d6001fa4ba899a403daf9b1db01be925c22533af630ee6493b0be9f79", + "prevout_txid": "13d2eebad9a1099147410c86a6fe0b20ee9db759da41cfd4ee9792cd50123715", + "prevout_index": 0, + "prevout_scriptpubkey": "0014421c715aedf983af0e3b676ff7c494d1054fcdba", + "amount": 50000, + "witness_utxo": "50c3000000000000160014421c715aedf983af0e3b676ff7c494d1054fcdba", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": false, + "input_index": 0 + }, + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "035be8341029c7247fa2b938d5d4b85b3a69fbd9317bad85af65799271dcb42801", + "dleq_proof": "c0fe2ccc52dfbb925bd3ff654b7a96d8ec0e031a67d041b4f99cb0e81149e287a82bd3d51bc0ff733ace7d32ab63d61218654577808fb2c7fb06d2be06ff3ab6", + "is_global": false, + "input_index": 1 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 95000, + "script": "51200bdc6a1dabb87517678c2b5f7dcdff2976dc5d1135c4dd93f66723ebefb98e25", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "Two signers each contribute ECDH shares for their respective inputs" + }, + { + "description": "Silent payment with change detection", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQCAAAAAQYBAwABDiAlbK6m2hWAb7hW7a50mI1EDHqxtcCGHsgR0ZCSdHudHAEPBAAAAAABAR+ghgEAAAAAABYAFPja92rYA7DvqV1s/4ruCJHTrxyMARAE/v///yIGA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NBAAAAAABAwQBAAAAIh0C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPghAlUWTnkm1Q1SoJ/5kGR6XpXB2xv8aKYW+8LaITkn+Yv/Ih4C0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPhAwdZ/OHiCv4F5FeoVgh1k1bODXB/AXFWM49gkkqF9kIwQSH27m93BCAyMLZwZcbJmCDxpBU4iGT8kQSisZnN+YAABAwhQwwAAAAAAAAEEIlEgVbkWS8N9yG9biTYWgqowiLWz+lPa20PpXxvRE5uxwDUBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgD9SRDSFIBZWa8RdH6alxaGGLN2TPxXIQ7QnI4IvUlMjcBCgQBAAAAAAEDCMivAAAAAAAAAQQWABTjwxDMKvOsbmLK5L0j4+5SueHJWSICA9NX98BxjyR44/2PjMwnKd3YwMyusfArGBpvRNQ7n42NDAAAAAAAAAAAAAAAAQA=", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "256caea6da15806fb856edae74988d440c7ab1b5c0861ec811d19092747b9d1c", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": 1 + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 50000, + "script": "512055b9164bc37dc86f5b89361682aa3088b5b3fa53dadb43e95f1bd1139bb1c035", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f803f524434852016566bc45d1fa6a5c5a1862cdd933f15c843b42723822f5253237", + "sp_label": 1 + }, + { + "output_index": 1, + "amount": 45000, + "script": "0014e3c310cc2af3ac6e62cae4bd23e3ee52b9e1c959", + "is_silent_payment": false, + "sp_info": null, + "sp_label": null + } + ], + "comment": "Uses PSBT_OUT_SP_V0_LABEL and BIP32 derivation for change identification" + }, + { + "description": "Multiple silent payment outputs to same scan key", + "psbt": "cHNidP8B+wQCAAAAAQIEAgAAAAEEBAEAAAABBQQCAAAAAQYBAyIHAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4IQJVFk55JtUNUqCf+ZBkel6Vwdsb/GimFvvC2iE5J/mL/yIIAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4QMHWfzh4gr+BeRXqFYIdZNWzg1wfwFxVjOPYJJKhfZCMEEh9u5vdwQgMjC2cGXGyZgg8aQVOIhk/JEEorGZzfmAAAQ4gLHvTL/FQccCuAyc4ZKFDbIpWITVp4RMtz46nPsjDMiIBDwQAAAAAAQEfoIYBAAAAAAAWABT42vdq2AOw76ldbP+K7giR068cjAEQBP7///8iBgPTV/fAcY8keOP9j4zMJynd2MDMrrHwKxgab0TUO5+NjQQAAAAAAQMEAQAAAAABAwhAnAAAAAAAAAEEIlEg+ytxOv1SuiRxgbmYQapPLIhVut99rOLrLjBV2hPuK4wBCUIC0Cn/lt4svPeCvkNZxIYg6pK83WvvAyuVFYuRoWk/tPgCTVGDU/S9GNdpz2j/Yu8QZptwhiRrCmQD/le95JIRRIsAAQMI2NYAAAAAAAABBCJRIFmgqeG9mJh0IBNVOGDtC0S3JvvFIOnFcbxGSV8kefYYAQlCAtAp/5beLLz3gr5DWcSGIOqSvN1r7wMrlRWLkaFpP7T4Ak1Rg1P0vRjXac9o/2LvEGabcIYkawpkA/5XveSSEUSLAA==", + "input_keys": [ + { + "input_index": 0, + "private_key": "e75f1990643539150761b0dc9eceb2ec0cf8636883eea2112d991dec3567a7b9", + "public_key": "03d357f7c0718f2478e3fd8f8ccc2729ddd8c0ccaeb1f02b181a6f44d43b9f8d8d", + "prevout_txid": "2c7bd32ff15071c0ae03273864a1436c8a56213569e1132dcf8ea73ec8c33222", + "prevout_index": 0, + "prevout_scriptpubkey": "0014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "amount": 100000, + "witness_utxo": "a086010000000000160014f8daf76ad803b0efa95d6cff8aee0891d3af1c8c", + "sequence": 4294967295 + } + ], + "scan_keys": [ + { + "scan_pubkey": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "spend_pubkey": "024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "label": null + } + ], + "expected_ecdh_shares": [ + { + "scan_key": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8", + "ecdh_result": "0255164e7926d50d52a09ff990647a5e95c1db1bfc68a616fbc2da213927f98bff", + "dleq_proof": "c1d67f387882bf817915ea15821d64d5b3835c1fc05c558ce3d82492a17d908c10487dbb9bddc1080c8c2d9c1971b266083c69054e22193f244128ac66737e60", + "is_global": false, + "input_index": 0 + } + ], + "expected_outputs": [ + { + "output_index": 0, + "amount": 40000, + "script": "5120fb2b713afd52ba247181b99841aa4f2c8855badf7dace2eb2e3055da13ee2b8c", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + }, + { + "output_index": 1, + "amount": 55000, + "script": "512059a0a9e1bd9898742013553860ed0b44b726fbc520e9c571bc46495f2479f618", + "is_silent_payment": true, + "sp_info": "02d029ff96de2cbcf782be4359c48620ea92bcdd6bef032b95158b91a1693fb4f8024d518353f4bd18d769cf68ff62ef10669b7086246b0a6403fe57bde49211448b", + "sp_label": null + } + ], + "comment": "Two outputs to same silent payment address, different k values" + } + ] +} \ No newline at end of file diff --git a/bip-0375/validator.py b/bip-0375/validator.py new file mode 100644 index 0000000000..1098a73e6f --- /dev/null +++ b/bip-0375/validator.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +""" +BIP 375: PSBT Validator + +Complete BIP 375 validation for PSBTs with silent payment outputs. +""" + +import hashlib +import struct +from typing import Tuple, List, Dict, Optional + +from constants import PSBTFieldType +from dleq import validate_global_dleq_proof, validate_input_dleq_proof +from inputs import validate_input_eligibility, check_invalid_segwit_version +from parser import parse_psbt_structure +# External references bip-0374 +from secp256k1 import GE, G + + +def validate_bip375_psbt( + psbt_data: bytes, + input_keys: Optional[List[Dict]] = None, +) -> Tuple[bool, str]: + """Validate a PSBT according to BIP 375 rules + + Args: + psbt_data: Raw PSBT bytes + input_keys: Optional list of input key material for BIP-352 validation + """ + + # Basic PSBT structure validation + if len(psbt_data) < 5 or psbt_data[:5] != b"psbt\xff": + return False, "Invalid PSBT magic" + + # Parse PSBT fields + global_fields, input_maps, output_maps = parse_psbt_structure(psbt_data) + + # Check if silent payment outputs exist + # Either SP_V0_INFO or SP_V0_LABEL indicates silent payment intent + has_silent_outputs = any( + PSBTFieldType.PSBT_OUT_SP_V0_INFO in output_fields + or PSBTFieldType.PSBT_OUT_SP_V0_LABEL in output_fields + for output_fields in output_maps + ) + + if not has_silent_outputs: + # If no silent payment outputs, this is just a regular PSBT v2 + return True, "Valid PSBT v2 (no silent payments)" + + # Critical structural validation - SP_V0_INFO field sizes and PSBT_OUT_SCRIPT requirements + for i, output_fields in enumerate(output_maps): + # BIP375: Each output must have either PSBT_OUT_SCRIPT or PSBT_OUT_SP_V0_INFO (or both) + has_script = PSBTFieldType.PSBT_OUT_SCRIPT in output_fields + has_sp_info = PSBTFieldType.PSBT_OUT_SP_V0_INFO in output_fields + has_sp_label = PSBTFieldType.PSBT_OUT_SP_V0_LABEL in output_fields + + if not has_script and not has_sp_info: + return ( + False, + f"Output {i} must have either PSBT_OUT_SCRIPT or PSBT_OUT_SP_V0_INFO", + ) + + # PSBT_OUT_SP_V0_LABEL requires PSBT_OUT_SP_V0_INFO + if has_sp_label and not has_sp_info: + return ( + False, + f"Output {i} has PSBT_OUT_SP_V0_LABEL but missing PSBT_OUT_SP_V0_INFO", + ) + + if has_sp_info: + sp_info = output_fields[PSBTFieldType.PSBT_OUT_SP_V0_INFO] + if len(sp_info) != 66: # 33 + 33 bytes for scan_key + spend_key + return ( + False, + f"Output {i} SP_V0_INFO has wrong size ({len(sp_info)} bytes, expected 66)", + ) + + # ECDH shares must exist + has_global_ecdh = PSBTFieldType.PSBT_GLOBAL_SP_ECDH_SHARE in global_fields + has_input_ecdh = any( + PSBTFieldType.PSBT_IN_SP_ECDH_SHARE in input_fields + for input_fields in input_maps + ) + + if not has_global_ecdh and not has_input_ecdh: + return False, "Silent payment outputs present but no ECDH shares found" + + # Cannot have both global and per-input ECDH shares for same scan key + if has_global_ecdh and has_input_ecdh: + # Extract scan key from global ECDH share + global_ecdh_field = global_fields[PSBTFieldType.PSBT_GLOBAL_SP_ECDH_SHARE] + global_scan_key = global_ecdh_field["key"] + + # Check if any input has ECDH share for the same scan key + for i, input_fields in enumerate(input_maps): + if PSBTFieldType.PSBT_IN_SP_ECDH_SHARE in input_fields: + input_ecdh_field = input_fields[PSBTFieldType.PSBT_IN_SP_ECDH_SHARE] + input_scan_key = input_ecdh_field["key"] + if input_scan_key == global_scan_key: + return ( + False, + "Cannot have both global and per-input ECDH shares for same scan key", + ) + + # DLEQ proofs must exist for ECDH shares + if has_global_ecdh: + has_global_dleq = PSBTFieldType.PSBT_GLOBAL_SP_DLEQ in global_fields + if not has_global_dleq: + return False, "Global ECDH share present but missing DLEQ proof" + + if has_input_ecdh: + for i, input_fields in enumerate(input_maps): + if PSBTFieldType.PSBT_IN_SP_ECDH_SHARE in input_fields: + if PSBTFieldType.PSBT_IN_SP_DLEQ not in input_fields: + return False, f"Input {i} has ECDH share but missing DLEQ proof" + + # Verify DLEQ proofs + if has_global_ecdh: + if not validate_global_dleq_proof(global_fields, input_maps): + return False, "Global DLEQ proof verification failed" + + if has_input_ecdh: + for i, input_fields in enumerate(input_maps): + if PSBTFieldType.PSBT_IN_SP_ECDH_SHARE in input_fields: + if not validate_input_dleq_proof(input_fields, None, i): + return False, f"Input {i} DLEQ proof verification failed" + + # Segwit version restrictions + for i, input_fields in enumerate(input_maps): + if PSBTFieldType.PSBT_IN_WITNESS_UTXO in input_fields: + witness_utxo = input_fields[PSBTFieldType.PSBT_IN_WITNESS_UTXO] + if check_invalid_segwit_version(witness_utxo): + return False, f"Input {i} uses segwit version > 1 with silent payments" + + # Eligible input type requirement + # When silent payment outputs exist, ALL inputs must be eligible types + for i, input_fields in enumerate(input_maps): + is_valid, error_msg = validate_input_eligibility(input_fields, i) + if not is_valid: + return False, error_msg + + # SIGHASH_ALL requirement + for i, input_fields in enumerate(input_maps): + if PSBTFieldType.PSBT_IN_SIGHASH_TYPE in input_fields: + sighash = input_fields[PSBTFieldType.PSBT_IN_SIGHASH_TYPE] + if len(sighash) >= 4: + sighash_type = struct.unpack(" Tuple[bool, str]: + """Validate BIP-352 output script derivation (requires input_keys test material)""" + + # Build outpoints list from PSBT inputs + outpoints = [] + for input_fields in input_maps: + if PSBTFieldType.PSBT_IN_PREVIOUS_TXID in input_fields: + txid = input_fields[PSBTFieldType.PSBT_IN_PREVIOUS_TXID] + output_index_bytes = input_fields.get(PSBTFieldType.PSBT_IN_OUTPUT_INDEX) + if output_index_bytes and len(output_index_bytes) == 4: + output_index = struct.unpack(" bytes: + """Compute BIP-352 silent payment output script""" + # Find smallest outpoint lexicographically + serialized_outpoints = [txid + struct.pack("