Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions bip-0375/README.md
Original file line number Diff line number Diff line change
@@ -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/)
37 changes: 37 additions & 0 deletions bip-0375/constants.py
Original file line number Diff line number Diff line change
@@ -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
142 changes: 142 additions & 0 deletions bip-0375/dleq.py
Original file line number Diff line number Diff line change
@@ -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)
Loading