mirror of
https://github.com/bitcoin/bips.git
synced 2026-04-20 16:28:39 +00:00
BIP-375: add test_runner and validate PSBT structure
Implement psbt structure checks Add test_runner.py for processing test vectors
This commit is contained in:
155
bip-0375/test_runner.py
Normal file
155
bip-0375/test_runner.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Process test vectors JSON file and run validation checks"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
deps_dir = project_root / "deps"
|
||||||
|
secp256k1lab_dir = deps_dir / "secp256k1lab" / "src"
|
||||||
|
for path in [str(deps_dir), str(secp256k1lab_dir)]:
|
||||||
|
if path not in sys.path:
|
||||||
|
sys.path.insert(0, path)
|
||||||
|
|
||||||
|
from validator.psbt_bip375 import BIP375PSBT
|
||||||
|
from validator.validate_psbt import (
|
||||||
|
validate_psbt_structure,
|
||||||
|
validate_ecdh_coverage,
|
||||||
|
validate_input_eligibility,
|
||||||
|
validate_output_scripts,
|
||||||
|
)
|
||||||
|
|
||||||
|
CHECK_FUNCTIONS = {
|
||||||
|
"psbt_structure": validate_psbt_structure,
|
||||||
|
"ecdh_coverage": validate_ecdh_coverage,
|
||||||
|
"input_eligibility": validate_input_eligibility,
|
||||||
|
"output_scripts": validate_output_scripts,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bip375_psbt(
|
||||||
|
psbt_data: str, checks: list[str], debug: bool = False
|
||||||
|
) -> Tuple[bool, str]:
|
||||||
|
"""Performs sequential validation of a PSBT against BIP-375 rules"""
|
||||||
|
psbt = BIP375PSBT.from_base64(psbt_data)
|
||||||
|
|
||||||
|
if checks is None:
|
||||||
|
checks = [
|
||||||
|
"psbt_structure",
|
||||||
|
"ecdh_coverage",
|
||||||
|
"input_eligibility",
|
||||||
|
"output_scripts",
|
||||||
|
]
|
||||||
|
|
||||||
|
for check_name in checks:
|
||||||
|
if check_name not in CHECK_FUNCTIONS:
|
||||||
|
return False, f"Unknown check: {check_name}"
|
||||||
|
|
||||||
|
check_fn = CHECK_FUNCTIONS[check_name]
|
||||||
|
|
||||||
|
is_valid, msg = check_fn(psbt)
|
||||||
|
if debug:
|
||||||
|
msg = f"{check_name.upper()}: {msg}" if msg else msg
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
|
return False, msg
|
||||||
|
|
||||||
|
return True, "All checks passed"
|
||||||
|
|
||||||
|
|
||||||
|
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_tests(test_data: dict, verbosity: int = 0) -> tuple[int, int]:
|
||||||
|
"""Run validation checks for each test vector"""
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
# Process invalid PSBTs (should fail validation)
|
||||||
|
invalid_tests = test_data.get("invalid", [])
|
||||||
|
print(f"Invalid PSBTs: {len(invalid_tests)}")
|
||||||
|
for test_vector in invalid_tests:
|
||||||
|
is_valid, result = validate_bip375_psbt(
|
||||||
|
test_vector["psbt"], test_vector.get("checks"), debug=verbosity >= 2
|
||||||
|
)
|
||||||
|
print(f"{test_vector['description']}")
|
||||||
|
if not is_valid:
|
||||||
|
passed += 1
|
||||||
|
if verbosity >= 1:
|
||||||
|
print(f" {result}")
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
if result:
|
||||||
|
print(f" ERROR: {result}")
|
||||||
|
|
||||||
|
# Process valid PSBTs (should pass validation)
|
||||||
|
valid_tests = test_data.get("valid", [])
|
||||||
|
print("")
|
||||||
|
print(f"Valid PSBTs: {len(valid_tests)}")
|
||||||
|
for test_vector in valid_tests:
|
||||||
|
is_valid, result = validate_bip375_psbt(
|
||||||
|
test_vector["psbt"], test_vector.get("checks"), debug=verbosity >= 2
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"{test_vector['description']}")
|
||||||
|
if is_valid:
|
||||||
|
passed += 1
|
||||||
|
if verbosity >= 1:
|
||||||
|
print(f" {result}")
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
if result:
|
||||||
|
print(f" ERROR: {result}")
|
||||||
|
|
||||||
|
return passed, failed
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Silent Payments PSBT Validator",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--test-file",
|
||||||
|
"-f",
|
||||||
|
default=str(project_root / "bip375_test_vectors.json"),
|
||||||
|
help="Test vector file to run (default: bip375_test_vectors.json)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v",
|
||||||
|
dest="verbosity",
|
||||||
|
action="count",
|
||||||
|
default=0,
|
||||||
|
help="Verbosity level: -v shows pass/fail details, -vv enables debug output",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
test_data = load_test_vectors(args.test_file)
|
||||||
|
|
||||||
|
print(f"Description: {test_data.get('description', 'N/A')}")
|
||||||
|
print(f"Version: {test_data.get('version', 'N/A')}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
passed, failed = run_validation_tests(test_data, args.verbosity)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Summary: {passed} passed, {failed} failed")
|
||||||
|
|
||||||
|
sys.exit(0 if failed == 0 else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
127
bip-0375/validator/validate_psbt.py
Normal file
127
bip-0375/validator/validate_psbt.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Validates PSBTs according to BIP-375 rules
|
||||||
|
|
||||||
|
Provides independent checks for PSBT structure, ECDH share coverage,
|
||||||
|
input eligibility, and output script correctness.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from deps.bitcoin_test.psbt import (
|
||||||
|
PSBT,
|
||||||
|
PSBT_GLOBAL_TX_MODIFIABLE,
|
||||||
|
PSBT_OUT_SCRIPT,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .psbt_bip375 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_psbt_structure(psbt: PSBT) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Validate PSBT structure requirements
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
- Each output must have PSBT_OUT_SCRIPT or PSBT_OUT_SP_V0_INFO
|
||||||
|
- PSBT_OUT_SP_V0_LABEL requires PSBT_OUT_SP_V0_INFO
|
||||||
|
- SP_V0_INFO must be 66 bytes (33-byte scan key + 33-byte spend key)
|
||||||
|
- ECDH shares must be 33 bytes
|
||||||
|
- DLEQ proofs must be 64 bytes
|
||||||
|
- TX_MODIFIABLE is zero when PSBT_OUT_SCRIPT set for SP output
|
||||||
|
"""
|
||||||
|
# Check output requirements
|
||||||
|
for i, output_map in enumerate(psbt.o):
|
||||||
|
has_script = (
|
||||||
|
PSBT_OUT_SCRIPT in output_map and len(output_map[PSBT_OUT_SCRIPT]) > 0
|
||||||
|
)
|
||||||
|
has_sp_info = PSBT_OUT_SP_V0_INFO in output_map
|
||||||
|
has_sp_label = PSBT_OUT_SP_V0_LABEL in output_map
|
||||||
|
|
||||||
|
# Output must have script or SP info
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
# SP label requires SP 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",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate SP_V0_INFO field length
|
||||||
|
if has_sp_info:
|
||||||
|
sp_info = output_map[PSBT_OUT_SP_V0_INFO]
|
||||||
|
if len(sp_info) != 66:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Output {i} SP_V0_INFO has wrong length ({len(sp_info)} bytes, expected 66)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate ECDH share lengths (global and per-input)
|
||||||
|
global_ecdh_shares = psbt.g.get_all_by_type(PSBT_GLOBAL_SP_ECDH_SHARE)
|
||||||
|
for _, ecdh_share in global_ecdh_shares:
|
||||||
|
if len(ecdh_share) != 33:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Global ECDH share has wrong length ({len(ecdh_share)} bytes, expected 33)",
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, input_map in enumerate(psbt.i):
|
||||||
|
input_ecdh_shares = input_map.get_all_by_type(PSBT_IN_SP_ECDH_SHARE)
|
||||||
|
for _, ecdh_share in input_ecdh_shares:
|
||||||
|
if len(ecdh_share) != 33:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Input {i} ECDH share has wrong length ({len(ecdh_share)} bytes, expected 33)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate DLEQ proof lengths (global and per-input)
|
||||||
|
global_dleq_proofs = psbt.g.get_all_by_type(PSBT_GLOBAL_SP_DLEQ)
|
||||||
|
for _, dleq_proof in global_dleq_proofs:
|
||||||
|
if len(dleq_proof) != 64:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Global DLEQ proof has wrong length ({len(dleq_proof)} bytes, expected 64)",
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, input_map in enumerate(psbt.i):
|
||||||
|
input_dleq_proofs = input_map.get_all_by_type(PSBT_IN_SP_DLEQ)
|
||||||
|
for _, dleq_proof in input_dleq_proofs:
|
||||||
|
if len(dleq_proof) != 64:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Input {i} DLEQ proof has wrong length ({len(dleq_proof)} bytes, expected 64)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check TX_MODIFIABLE flag when PSBT_OUT_SCRIPT is set
|
||||||
|
for output_map in psbt.o:
|
||||||
|
if PSBT_OUT_SP_V0_INFO in output_map and PSBT_OUT_SCRIPT in output_map:
|
||||||
|
if len(output_map.get(PSBT_OUT_SCRIPT, b"")) > 0:
|
||||||
|
if psbt.g.get(PSBT_GLOBAL_TX_MODIFIABLE) != b"\x00":
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
"PSBT_OUT_SCRIPT set for silent payments output but PSBT_GLOBAL_TX_MODIFIABLE not zeroed",
|
||||||
|
)
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ecdh_coverage(psbt: PSBT) -> Tuple[bool, str]:
|
||||||
|
return False, "ECDH coverage check not implemented yet"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_input_eligibility(psbt: PSBT) -> Tuple[bool, str]:
|
||||||
|
return False, "Input eligibility check not implemented yet"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_output_scripts(psbt: PSBT) -> Tuple[bool, str]:
|
||||||
|
return False, "Output scripts check not implemented yet"
|
||||||
Reference in New Issue
Block a user