Skip to content

Commit e3c7677

Browse files
committed
Implement test runner to evaluate bip 375 test vectors
1 parent 9a30c28 commit e3c7677

File tree

6 files changed

+2392
-0
lines changed

6 files changed

+2392
-0
lines changed

bip-0375/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# BIP 375 Reference Implementation
2+
3+
This directory contains the complete reference implementation for BIP 375: Sending Silent Payments with PSBTs.
4+
5+
## **Core Files**
6+
7+
### Reference Implementation
8+
9+
- **`reference.py`** - Minimal standalone BIP 375 validator with integrated test runner (executable)
10+
11+
### Dependencies (from BIP 374)
12+
13+
- **`dleq_374.py`** - BIP 374 DLEQ proof implementation
14+
- **`secp256k1_374.py`** - Secp256k1 implementation
15+
16+
## **Usage**
17+
18+
### Run Reference Implementation Tests
19+
20+
```bash
21+
python reference.py # Run all tests using test_vectors.json
22+
python reference.py -f custom.json # Use custom test file
23+
python reference.py -v # Verbose mode with detailed errors
24+
```

bip-0375/bip352_utils.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import hashlib
2+
import struct
3+
from io import BytesIO
4+
from secp256k1_374 import GE, G
5+
from typing import Union, List, Tuple
6+
7+
8+
def from_hex(hex_string):
9+
"""Deserialize from a hex string representation (e.g. from RPC)"""
10+
return BytesIO(bytes.fromhex(hex_string))
11+
12+
13+
def ser_uint32(u: int) -> bytes:
14+
return u.to_bytes(4, "big")
15+
16+
17+
def ser_uint256(u):
18+
return u.to_bytes(32, "little")
19+
20+
21+
def deser_uint256(f):
22+
return int.from_bytes(f.read(32), "little")
23+
24+
25+
def deser_txid(txid: str):
26+
# recall that txids are serialized little-endian, but displayed big-endian
27+
# this means when converting from a human readable hex txid, we need to first
28+
# reverse it before deserializing it
29+
dixt = "".join(map(str.__add__, txid[-2::-2], txid[-1::-2]))
30+
return bytes.fromhex(dixt)
31+
32+
33+
def deser_compact_size(f: BytesIO):
34+
view = f.getbuffer()
35+
nbytes = view.nbytes
36+
view.release()
37+
if nbytes == 0:
38+
return 0 # end of stream
39+
40+
nit = struct.unpack("<B", f.read(1))[0]
41+
if nit == 253:
42+
nit = struct.unpack("<H", f.read(2))[0]
43+
elif nit == 254:
44+
nit = struct.unpack("<I", f.read(4))[0]
45+
elif nit == 255:
46+
nit = struct.unpack("<Q", f.read(8))[0]
47+
return nit
48+
49+
50+
def deser_string(f: BytesIO):
51+
nit = deser_compact_size(f)
52+
return f.read(nit)
53+
54+
55+
def deser_string_vector(f: BytesIO):
56+
nit = deser_compact_size(f)
57+
r = []
58+
for _ in range(nit):
59+
t = deser_string(f)
60+
r.append(t)
61+
return r
62+
63+
64+
def is_p2tr(spk: bytes) -> bool:
65+
if len(spk) != 34:
66+
return False
67+
# OP_1 OP_PUSHBYTES_32 <32 bytes>
68+
return (spk[0] == 0x51) & (spk[1] == 0x20)
69+
70+
71+
def is_p2wpkh(spk: bytes) -> bool:
72+
if len(spk) != 22:
73+
return False
74+
# OP_0 OP_PUSHBYTES_20 <20 bytes>
75+
return (spk[0] == 0x00) & (spk[1] == 0x14)
76+
77+
78+
def is_p2sh(spk: bytes) -> bool:
79+
if len(spk) != 23:
80+
return False
81+
# OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL
82+
return (spk[0] == 0xA9) & (spk[1] == 0x14) & (spk[-1] == 0x87)
83+
84+
85+
def is_p2pkh(spk: bytes) -> bool:
86+
if len(spk) != 25:
87+
return False
88+
# OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
89+
return (
90+
(spk[0] == 0x76)
91+
& (spk[1] == 0xA9)
92+
& (spk[2] == 0x14)
93+
& (spk[-2] == 0x88)
94+
& (spk[-1] == 0xAC)
95+
)
96+
97+
98+
def is_eligible_input_type(script_pubkey: bytes) -> bool:
99+
"""Check if scriptPubKey is an eligible input type for silent payments per BIP-352"""
100+
return (
101+
is_p2pkh(script_pubkey)
102+
or is_p2wpkh(script_pubkey)
103+
or is_p2tr(script_pubkey)
104+
or is_p2sh(script_pubkey)
105+
)
106+
107+
108+
def parse_non_witness_utxo(non_witness_utxo: bytes, output_index: int) -> bytes:
109+
"""Extract scriptPubKey from NON_WITNESS_UTXO field"""
110+
try:
111+
offset = 0
112+
113+
# Skip version (4 bytes)
114+
if len(non_witness_utxo) < 4:
115+
return None
116+
offset += 4
117+
118+
# Parse input count (compact size)
119+
if offset >= len(non_witness_utxo):
120+
return None
121+
122+
input_count = non_witness_utxo[offset]
123+
offset += 1
124+
if input_count >= 0xFD:
125+
# Handle larger compact size (simplified - just skip)
126+
if input_count == 0xFD:
127+
offset += 2
128+
elif input_count == 0xFE:
129+
offset += 4
130+
else:
131+
offset += 8
132+
input_count = (
133+
struct.unpack("<H", non_witness_utxo[offset - 2 : offset])[0]
134+
if input_count == 0xFD
135+
else 0
136+
)
137+
138+
# Skip all inputs
139+
for _ in range(input_count):
140+
# Skip txid (32) + vout (4)
141+
offset += 36
142+
if offset >= len(non_witness_utxo):
143+
return None
144+
145+
# Skip scriptSig
146+
script_len = non_witness_utxo[offset]
147+
offset += 1
148+
if script_len >= 0xFD:
149+
return None # Simplified - don't handle large scripts
150+
offset += script_len
151+
152+
# Skip sequence (4)
153+
offset += 4
154+
if offset > len(non_witness_utxo):
155+
return None
156+
157+
# Parse output count
158+
if offset >= len(non_witness_utxo):
159+
return None
160+
output_count = non_witness_utxo[offset]
161+
offset += 1
162+
if output_count >= 0xFD:
163+
if output_count == 0xFD:
164+
output_count = struct.unpack(
165+
"<H", non_witness_utxo[offset : offset + 2]
166+
)[0]
167+
offset += 2
168+
else:
169+
return None # Simplified
170+
171+
# Find the output at output_index
172+
for i in range(output_count):
173+
# Skip amount (8 bytes)
174+
if offset + 8 >= len(non_witness_utxo):
175+
return None
176+
offset += 8
177+
178+
# Parse scriptPubKey length
179+
if offset >= len(non_witness_utxo):
180+
return None
181+
script_len = non_witness_utxo[offset]
182+
offset += 1
183+
if script_len >= 0xFD:
184+
if script_len == 0xFD:
185+
script_len = struct.unpack(
186+
"<H", non_witness_utxo[offset : offset + 2]
187+
)[0]
188+
offset += 2
189+
else:
190+
return None
191+
192+
# Extract scriptPubKey if this is our output
193+
if i == output_index:
194+
if offset + script_len > len(non_witness_utxo):
195+
return None
196+
return non_witness_utxo[offset : offset + script_len]
197+
198+
# Otherwise skip to next output
199+
offset += script_len
200+
if offset > len(non_witness_utxo):
201+
return None
202+
203+
return None
204+
except Exception:
205+
return None
206+
207+
208+
def compute_bip352_output_script(
209+
outpoints: List[Tuple[bytes, int]],
210+
summed_pubkey_bytes: bytes,
211+
ecdh_share_bytes: bytes,
212+
spend_pubkey_bytes: bytes,
213+
k: int = 0,
214+
) -> bytes:
215+
"""Compute BIP-352 silent payment output script"""
216+
# Find smallest outpoint lexicographically
217+
serialized_outpoints = [txid + struct.pack("<I", idx) for txid, idx in outpoints]
218+
smallest_outpoint = min(serialized_outpoints)
219+
220+
# Compute input_hash = hash_BIP0352/Inputs(smallest_outpoint || A)
221+
tag_data = b"BIP0352/Inputs"
222+
tag_hash = hashlib.sha256(tag_data).digest()
223+
input_hash_preimage = tag_hash + tag_hash + smallest_outpoint + summed_pubkey_bytes
224+
input_hash_bytes = hashlib.sha256(input_hash_preimage).digest()
225+
input_hash = int.from_bytes(input_hash_bytes, "big")
226+
227+
# Compute shared_secret = input_hash * ecdh_share
228+
ecdh_point = GE.from_bytes(ecdh_share_bytes)
229+
shared_secret_point = input_hash * ecdh_point
230+
shared_secret_bytes = shared_secret_point.to_bytes_compressed()
231+
232+
# Compute t_k = hash_BIP0352/SharedSecret(shared_secret || k)
233+
tag_data = b"BIP0352/SharedSecret"
234+
tag_hash = hashlib.sha256(tag_data).digest()
235+
t_preimage = tag_hash + tag_hash + shared_secret_bytes + k.to_bytes(4, "big")
236+
t_k_bytes = hashlib.sha256(t_preimage).digest()
237+
t_k = int.from_bytes(t_k_bytes, "big")
238+
239+
# Compute P_k = B_spend + t_k * G
240+
B_spend = GE.from_bytes(spend_pubkey_bytes)
241+
P_k = B_spend + (t_k * G)
242+
243+
# Create P2TR script (x-only pubkey)
244+
x_only = P_k.to_bytes_compressed()[1:] # Remove parity byte
245+
return bytes([0x51, 0x20]) + x_only

0 commit comments

Comments
 (0)