Skip to content

Commit 8c38c9e

Browse files
committed
This fills in the remaining TODO sections in BIP-376
1 parent eb49796 commit 8c38c9e

3 files changed

Lines changed: 308 additions & 2 deletions

File tree

bip-0376.mediawiki

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,29 @@ These are new fields added to the existing PSBT format. Because PSBT is designed
146146

147147
== Reference implementation ==
148148

149-
'''''TODO'''''
149+
A Python reference implementation is provided in [[bip-0376/reference.py|<code>bip-0376/reference.py</code>]].
150+
151+
It demonstrates the Signer behavior specified in this BIP:
152+
153+
* Key derivation using ''d = (b<sub>spend</sub> + tweak) mod n''.
154+
* Key negation when ''d·G'' has odd y-coordinate.
155+
* Verification that the resulting x-only public key matches the output key ''P''.
156+
* BIP 340 signing with the derived key.
150157
151158
=== Test vectors ===
152159

153-
'''''TODO'''''
160+
Machine-readable test vectors are provided in [[bip-0376/test-vectors.json|<code>bip-0376/test-vectors.json</code>]].
161+
162+
The vector set includes:
163+
164+
* Valid cases with and without key negation.
165+
* Invalid cases for output-key mismatch, zero tweaked key, and out-of-range spend key.
166+
167+
The reference implementation can be run against the vectors with:
168+
169+
<pre>
170+
./bip-0376/reference.py bip-0376/test-vectors.json
171+
</pre>
154172

155173
== Appendix ==
156174

bip-0376/reference.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env python3
2+
"""BIP-0376 reference implementation and test vector runner.
3+
4+
Run:
5+
./bip-0376/reference.py bip-0376/test-vectors.json
6+
"""
7+
8+
import json
9+
import sys
10+
import hashlib
11+
from pathlib import Path
12+
from typing import Optional, Tuple
13+
14+
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
15+
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
16+
G = (
17+
0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
18+
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
19+
)
20+
21+
Point = Tuple[int, int]
22+
23+
24+
def tagged_hash(tag: str, msg: bytes) -> bytes:
25+
tag_hash = hashlib.sha256(tag.encode("utf-8")).digest()
26+
return hashlib.sha256(tag_hash + tag_hash + msg).digest()
27+
28+
29+
def int_from_bytes(data: bytes) -> int:
30+
return int.from_bytes(data, byteorder="big")
31+
32+
33+
def bytes_from_int(x: int) -> bytes:
34+
return x.to_bytes(32, byteorder="big")
35+
36+
37+
def has_even_y(P: Point) -> bool:
38+
return (P[1] % 2) == 0
39+
40+
41+
def bytes_from_point(P: Point) -> bytes:
42+
return bytes_from_int(P[0])
43+
44+
45+
def xor_bytes(a: bytes, b: bytes) -> bytes:
46+
return bytes(x ^ y for (x, y) in zip(a, b))
47+
48+
49+
def lift_x(x_coord: int) -> Optional[Point]:
50+
if x_coord >= p:
51+
return None
52+
y_sq = (pow(x_coord, 3, p) + 7) % p
53+
y_coord = pow(y_sq, (p + 1) // 4, p)
54+
if pow(y_coord, 2, p) != y_sq:
55+
return None
56+
return (x_coord, y_coord if (y_coord % 2) == 0 else p - y_coord)
57+
58+
59+
def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]:
60+
if P1 is None:
61+
return P2
62+
if P2 is None:
63+
return P1
64+
if (P1[0] == P2[0]) and (P1[1] != P2[1]):
65+
return None
66+
if P1 == P2:
67+
lam = (3 * P1[0] * P1[0] * pow(2 * P1[1], p - 2, p)) % p
68+
else:
69+
lam = ((P2[1] - P1[1]) * pow(P2[0] - P1[0], p - 2, p)) % p
70+
x3 = (lam * lam - P1[0] - P2[0]) % p
71+
y3 = (lam * (P1[0] - x3) - P1[1]) % p
72+
return (x3, y3)
73+
74+
75+
def point_mul(P: Optional[Point], scalar: int) -> Optional[Point]:
76+
R = None
77+
for i in range(256):
78+
if (scalar >> i) & 1:
79+
R = point_add(R, P)
80+
P = point_add(P, P)
81+
return R
82+
83+
84+
def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool:
85+
if len(pubkey) != 32 or len(sig) != 64:
86+
return False
87+
P = lift_x(int_from_bytes(pubkey))
88+
r = int_from_bytes(sig[0:32])
89+
s = int_from_bytes(sig[32:64])
90+
if P is None or r >= p or s >= n:
91+
return False
92+
e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n
93+
R = point_add(point_mul(G, s), point_mul(P, n - e))
94+
if R is None:
95+
return False
96+
return has_even_y(R) and (R[0] == r)
97+
98+
99+
def schnorr_sign(msg: bytes, seckey: bytes, aux_rand: bytes) -> bytes:
100+
d0 = int_from_bytes(seckey)
101+
if not (1 <= d0 <= n - 1):
102+
raise ValueError("The secret key must be in the range 1..n-1.")
103+
if len(aux_rand) != 32:
104+
raise ValueError("aux_rand must be 32 bytes.")
105+
P = point_mul(G, d0)
106+
assert P is not None
107+
d = d0 if has_even_y(P) else n - d0
108+
t = xor_bytes(bytes_from_int(d), tagged_hash("BIP0340/aux", aux_rand))
109+
k0 = int_from_bytes(tagged_hash("BIP0340/nonce", t + bytes_from_point(P) + msg)) % n
110+
if k0 == 0:
111+
raise RuntimeError("Failure. This happens only with negligible probability.")
112+
R = point_mul(G, k0)
113+
assert R is not None
114+
k = k0 if has_even_y(R) else n - k0
115+
e = int_from_bytes(tagged_hash("BIP0340/challenge", bytes_from_point(R) + bytes_from_point(P) + msg)) % n
116+
sig = bytes_from_point(R) + bytes_from_int((k + e * d) % n)
117+
if not schnorr_verify(msg, bytes_from_point(P), sig):
118+
raise RuntimeError("The created signature does not pass verification.")
119+
return sig
120+
121+
122+
def parse_hex(data: str, expected_len: int, field_name: str) -> bytes:
123+
raw = bytes.fromhex(data)
124+
if len(raw) != expected_len:
125+
raise ValueError(f"{field_name} must be {expected_len} bytes.")
126+
return raw
127+
128+
129+
def derive_signing_key(spend_seckey: bytes, tweak: bytes, output_pubkey: bytes) -> Tuple[int, int, bool]:
130+
b_spend = int_from_bytes(spend_seckey)
131+
if not (1 <= b_spend <= n - 1):
132+
raise ValueError("spend key out of range")
133+
134+
tweak_int = int_from_bytes(tweak)
135+
d_raw = (b_spend + tweak_int) % n
136+
if d_raw == 0:
137+
raise ValueError("tweaked private key is zero")
138+
139+
Q = point_mul(G, d_raw)
140+
assert Q is not None
141+
negated = not has_even_y(Q)
142+
d = d_raw if not negated else n - d_raw
143+
144+
Q_even = point_mul(G, d)
145+
assert Q_even is not None
146+
if bytes_from_point(Q_even) != output_pubkey:
147+
raise ValueError("tweaked key does not match output key")
148+
149+
return d_raw, d, negated
150+
151+
152+
def run_test_vectors(path: Path) -> bool:
153+
vectors = json.loads(path.read_text(encoding="utf-8"))
154+
all_passed = True
155+
156+
valid_vectors = vectors.get("valid", [])
157+
invalid_vectors = vectors.get("invalid", [])
158+
159+
print(f"Running {len(valid_vectors)} valid vectors")
160+
for index, vector in enumerate(valid_vectors):
161+
description = vector["description"]
162+
given = vector["given"]
163+
expected = vector["expected"]
164+
print(f"- valid[{index}] {description}")
165+
try:
166+
spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey")
167+
tweak = parse_hex(given["tweak"], 32, "tweak")
168+
output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey")
169+
message = parse_hex(given["message"], 32, "message")
170+
aux_rand = parse_hex(given["aux_rand"], 32, "aux_rand")
171+
172+
d_raw, d, negated = derive_signing_key(spend_seckey, tweak, output_pubkey)
173+
signature = schnorr_sign(message, bytes_from_int(d), aux_rand)
174+
175+
assert bytes_from_int(d_raw).hex() == expected["raw_tweaked_seckey"]
176+
assert negated == expected["negated"]
177+
assert bytes_from_int(d).hex() == expected["final_seckey"]
178+
assert signature.hex() == expected["signature"]
179+
except Exception as exc:
180+
all_passed = False
181+
print(f" FAILED: {exc}")
182+
183+
print(f"Running {len(invalid_vectors)} invalid vectors")
184+
for index, vector in enumerate(invalid_vectors):
185+
description = vector["description"]
186+
given = vector["given"]
187+
error_substr = vector["error_substr"]
188+
print(f"- invalid[{index}] {description}")
189+
try:
190+
spend_seckey = parse_hex(given["spend_seckey"], 32, "spend_seckey")
191+
tweak = parse_hex(given["tweak"], 32, "tweak")
192+
output_pubkey = parse_hex(given["output_pubkey"], 32, "output_pubkey")
193+
derive_signing_key(spend_seckey, tweak, output_pubkey)
194+
all_passed = False
195+
print(" FAILED: expected an exception")
196+
except Exception as exc:
197+
if error_substr not in str(exc):
198+
all_passed = False
199+
print(f" FAILED: wrong error, got: {exc}")
200+
201+
print("All test vectors passed." if all_passed else "Some test vectors failed.")
202+
return all_passed
203+
204+
205+
def main() -> int:
206+
if len(sys.argv) > 2:
207+
print(f"Usage: {sys.argv[0]} [test-vectors.json]")
208+
return 1
209+
210+
if len(sys.argv) == 2:
211+
vector_path = Path(sys.argv[1])
212+
else:
213+
vector_path = Path(__file__).with_name("test-vectors.json")
214+
215+
if not vector_path.is_file():
216+
print(f"Vector file not found: {vector_path}")
217+
return 1
218+
219+
return 0 if run_test_vectors(vector_path) else 1
220+
221+
222+
if __name__ == "__main__":
223+
raise SystemExit(main())

bip-0376/test-vectors.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"valid": [
3+
{
4+
"description": "No negation required; tweaked key directly matches output key",
5+
"given": {
6+
"spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a",
7+
"tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221",
8+
"output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028",
9+
"message": "289e5175e02c788c2d442cfe81d6be0533d8c13e253ef763fda45d37accfe4d4",
10+
"aux_rand": "a617dfb275f834e26a6f0c94052dd88982c86297dba990fd96645026e7c69e10"
11+
},
12+
"expected": {
13+
"raw_tweaked_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b",
14+
"negated": false,
15+
"final_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b",
16+
"signature": "d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f9417778"
17+
}
18+
},
19+
{
20+
"description": "Negation required because (b_spend + tweak)G has odd Y",
21+
"given": {
22+
"spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a",
23+
"tweak": "58e2385eb96d1c906bbd807eafd1fddb80fb2f43026a16386a400e6832644cbc",
24+
"output_pubkey": "db0edc417c73c567add118de8d138b2d0b64083f0a1bd8e876936415de7edc46",
25+
"message": "a78521e49048b6e0d368d3fba417fc20c7546272dafa78a8a173fcca6c81233b",
26+
"aux_rand": "6b31977a8ac73ede3f3653ea0d96bc3656242461e31d771985a0b17084d3cf91"
27+
},
28+
"expected": {
29+
"raw_tweaked_seckey": "7fa902ec0eabdbe69f8853746e9f5e664c94eef0e36a688d1499cab7f5d6e516",
30+
"negated": true,
31+
"final_seckey": "8056fd13f15424196077ac8b9160a1986e19edf5cbde37aeab3893d4da5f5c2b",
32+
"signature": "3df67213afd895a833bc046e9455c77a7e40165638ad489669a5c498d4d71ab565a9c54abda2e23e931f7a0f78a9f151bba07b8400b7b96d24f857c8ba65c022"
33+
}
34+
}
35+
],
36+
"invalid": [
37+
{
38+
"description": "Tweaked key does not match output key",
39+
"given": {
40+
"spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a",
41+
"tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221",
42+
"output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da029"
43+
},
44+
"error_substr": "tweaked key does not match output key"
45+
},
46+
{
47+
"description": "Tweaked private key is zero",
48+
"given": {
49+
"spend_seckey": "26c6ca8d553ebf5633cad2f5becd608acb99bfade1005254aa59bc4fc372985a",
50+
"tweak": "d9393572aac140a9cc352d0a41329f73ef151d38ce484de71578a23d0cc3a8e7",
51+
"output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028"
52+
},
53+
"error_substr": "tweaked private key is zero"
54+
},
55+
{
56+
"description": "Spend key out of range",
57+
"given": {
58+
"spend_seckey": "0000000000000000000000000000000000000000000000000000000000000000",
59+
"tweak": "ac7b0d0420f0d4a567d9abcb8a52e02cfae21690fd8d2d5934370dcc5aaee221",
60+
"output_pubkey": "528b75296fa646acecf3fcb7c7697f92f7645ea0e41e6ee8a66554739d2da028"
61+
},
62+
"error_substr": "spend key out of range"
63+
}
64+
]
65+
}

0 commit comments

Comments
 (0)