Skip to content

Commit a669218

Browse files
committed
BIP-0376: refactor reference
1 parent d8db6b3 commit a669218

3 files changed

Lines changed: 25 additions & 132 deletions

File tree

bip-0376.mediawiki

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ These are new fields added to the existing PSBT format. Because PSBT is designed
147147
== Reference implementation ==
148148

149149
A Python reference implementation is provided in [[bip-0376/reference.py|<code>bip-0376/reference.py</code>]].
150+
It uses the vendored <code>secp256k1lab</code> test-only secp256k1 implementation from BIP 374 for elliptic curve arithmetic and BIP 340 signing.
150151

151152
It demonstrates the Signer behavior specified in this BIP:
152153

@@ -161,7 +162,7 @@ Machine-readable test vectors are provided in [[bip-0376/test-vectors.json|<code
161162

162163
The vector set includes:
163164

164-
* Valid cases with and without key negation.
165+
* Valid signing cases with and without key negation.
165166
* Invalid cases for output-key mismatch, zero tweaked key, and out-of-range spend key.
166167
167168
The reference implementation can be run against the vectors with:

bip-0376/reference.py

Lines changed: 23 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -7,116 +7,13 @@
77

88
import json
99
import sys
10-
import hashlib
1110
from pathlib import Path
12-
from typing import Optional, Tuple
1311

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
12+
# Use the vendored copy shared with BIP 374 instead of duplicating secp256k1
13+
# arithmetic and BIP 340 signing logic here.
14+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "bip-0374/secp256k1lab/src"))
15+
from secp256k1lab.bip340 import schnorr_sign
16+
from secp256k1lab.secp256k1 import G, Scalar
12017

12118

12219
def parse_hex(data: str, expected_len: int, field_name: str) -> bytes:
@@ -126,24 +23,28 @@ def parse_hex(data: str, expected_len: int, field_name: str) -> bytes:
12623
return raw
12724

12825

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):
26+
def derive_signing_key(
27+
spend_seckey: bytes, tweak: bytes, output_pubkey: bytes
28+
) -> tuple[Scalar, Scalar, bool]:
29+
try:
30+
b_spend = Scalar.from_bytes_checked(spend_seckey)
31+
except ValueError as exc:
32+
raise ValueError("spend key out of range") from exc
33+
if b_spend == 0:
13234
raise ValueError("spend key out of range")
13335

134-
tweak_int = int_from_bytes(tweak)
135-
d_raw = (b_spend + tweak_int) % n
36+
d_raw = b_spend + Scalar.from_bytes_wrapping(tweak)
13637
if d_raw == 0:
13738
raise ValueError("tweaked private key is zero")
13839

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
40+
Q = d_raw * G
41+
assert not Q.infinity
42+
negated = not Q.has_even_y()
43+
d = d_raw if not negated else -d_raw
14344

144-
Q_even = point_mul(G, d)
145-
assert Q_even is not None
146-
if bytes_from_point(Q_even) != output_pubkey:
45+
Q_even = d * G
46+
assert not Q_even.infinity
47+
if Q_even.to_bytes_xonly() != output_pubkey:
14748
raise ValueError("tweaked key does not match output key")
14849

14950
return d_raw, d, negated
@@ -169,12 +70,9 @@ def run_test_vectors(path: Path) -> bool:
16970
message = parse_hex(given["message"], 32, "message")
17071
aux_rand = parse_hex(given["aux_rand"], 32, "aux_rand")
17172

172-
d_raw, d, negated = derive_signing_key(spend_seckey, tweak, output_pubkey)
173-
signature = schnorr_sign(message, bytes_from_int(d), aux_rand)
73+
_, d, _ = derive_signing_key(spend_seckey, tweak, output_pubkey)
74+
signature = schnorr_sign(message, d.to_bytes(), aux_rand)
17475

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"]
17876
assert signature.hex() == expected["signature"]
17977
except Exception as exc:
18078
all_passed = False

bip-0376/test-vectors.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010
"aux_rand": "a617dfb275f834e26a6f0c94052dd88982c86297dba990fd96645026e7c69e10"
1111
},
1212
"expected": {
13-
"raw_tweaked_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b",
14-
"negated": false,
15-
"final_seckey": "d341d791762f93fb9ba47ec1492040b7c67bd63ede8d7fadde90ca1c1e217a7b",
1613
"signature": "d0c4f5ee3768c03a8d8e1204b8e52c6a4ded1f456d0f1707e7841928945c5a45bc1c0bc671d79612ef1c67a54bd50d653ce3d33c1fd966ce9a91e053f9417778"
1714
}
1815
},
@@ -26,9 +23,6 @@
2623
"aux_rand": "6b31977a8ac73ede3f3653ea0d96bc3656242461e31d771985a0b17084d3cf91"
2724
},
2825
"expected": {
29-
"raw_tweaked_seckey": "7fa902ec0eabdbe69f8853746e9f5e664c94eef0e36a688d1499cab7f5d6e516",
30-
"negated": true,
31-
"final_seckey": "8056fd13f15424196077ac8b9160a1986e19edf5cbde37aeab3893d4da5f5c2b",
3226
"signature": "3df67213afd895a833bc046e9455c77a7e40165638ad489669a5c498d4d71ab565a9c54abda2e23e931f7a0f78a9f151bba07b8400b7b96d24f857c8ba65c022"
3327
}
3428
}

0 commit comments

Comments
 (0)