Skip to content

Commit 7023f18

Browse files
committed
BIP draft: Add full-aggregation for Schnorr signatures
1 parent 6815a0d commit 7023f18

9 files changed

Lines changed: 1381 additions & 0 deletions

bip-XXXX.mediawiki

Lines changed: 431 additions & 0 deletions
Large diffs are not rendered by default.

bip-XXXX/gen_test_vectors.py

Lines changed: 466 additions & 0 deletions
Large diffs are not rendered by default.

bip-XXXX/reference.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Reference implementation of Full Aggregation of BIP 340 Signatures
4+
per the DahLIAS interactive signing protocol.
5+
6+
WARNING: This implementation is for demonstration purposes only and is not
7+
optimized for production use.
8+
"""
9+
10+
from pathlib import Path
11+
from typing import List, Tuple, Optional
12+
import secrets
13+
import sys
14+
15+
sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src"))
16+
from secp256k1lab.secp256k1 import G, GE, Scalar
17+
from secp256k1lab.util import tagged_hash, xor_bytes
18+
19+
n = GE.ORDER
20+
21+
FULLAGG_TAG_AUX = "FullAgg/aux"
22+
FULLAGG_TAG_NONCE = "FullAgg/nonce"
23+
FULLAGG_TAG_NONCECOEF = "FullAgg/noncecoef"
24+
FULLAGG_TAG_SIG = "FullAgg/sig"
25+
26+
27+
#
28+
# Helpers
29+
#
30+
31+
def has_even_y(P: GE) -> bool:
32+
return P.has_even_y()
33+
34+
35+
def cbytes(P: GE) -> bytes:
36+
return P.to_bytes_compressed()
37+
38+
39+
def xbytes(P: GE) -> bytes:
40+
return P.to_bytes_xonly()
41+
42+
43+
#
44+
# Key Tweaking
45+
#
46+
47+
def TweakSK(sk: Scalar, t: Scalar, is_xonly: bool) -> Scalar:
48+
d = sk if (not is_xonly or has_even_y(sk * G)) else -sk
49+
return d + t
50+
51+
52+
def TweakPK(pk: GE, t: Scalar, is_xonly: bool) -> GE:
53+
P = pk if (not is_xonly or has_even_y(pk)) else -pk
54+
Q = P + t * G
55+
assert not Q.infinity
56+
return Q
57+
58+
59+
#
60+
# Nonce Generation and Aggregation
61+
#
62+
63+
def NonceGen(sk: Optional[Scalar] = None,
64+
extra_in: bytes = b'') -> Tuple[Tuple[Scalar, Scalar], Tuple[GE, GE]]:
65+
rand_prime = secrets.token_bytes(32)
66+
if sk is not None:
67+
rand = xor_bytes(sk.to_bytes(), tagged_hash(FULLAGG_TAG_AUX, rand_prime))
68+
else:
69+
rand = rand_prime
70+
r1 = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCE, rand + extra_in + b'\x00'))
71+
r2 = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCE, rand + extra_in + b'\x01'))
72+
assert r1 != 0 and r2 != 0
73+
R1, R2 = r1 * G, r2 * G
74+
return (r1, r2), (R1, R2)
75+
76+
77+
def NonceAgg(pubnonces: List[Tuple[GE, GE]]) -> Tuple[GE, GE]:
78+
u = len(pubnonces)
79+
R1, R2 = pubnonces[0]
80+
for i in range(1, u):
81+
R1 = R1 + pubnonces[i][0]
82+
R2 = R2 + pubnonces[i][1]
83+
if R1.infinity or R2.infinity:
84+
raise ValueError("aggregate nonce is the point at infinity")
85+
return R1, R2
86+
87+
88+
#
89+
# Session Values
90+
#
91+
92+
def GetSessionValues(aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes],
93+
pubnonces: List[Tuple[GE, GE]]) -> Tuple[GE, Scalar]:
94+
R1, R2 = aggnonce
95+
u = len(pks)
96+
nonce_data = cbytes(R1) + cbytes(R2)
97+
for i in range(u):
98+
nonce_data += xbytes(pks[i]) + msgs[i] + cbytes(pubnonces[i][1])
99+
b = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_NONCECOEF, nonce_data))
100+
R = R1 + b * R2
101+
assert not R.infinity
102+
return R, b
103+
104+
105+
#
106+
# Signing
107+
#
108+
109+
def Sign(secnonce: Tuple[Scalar, Scalar], sk: Scalar, m: bytes,
110+
aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes],
111+
pubnonces: List[Tuple[GE, GE]]) -> Scalar:
112+
assert len(pks) == len(msgs) == len(pubnonces) >= 1
113+
assert len(m) == 32 and all(len(mi) == 32 for mi in msgs)
114+
r1, r2 = secnonce
115+
assert r1 != 0 and r2 != 0
116+
assert sk != 0
117+
P = sk * G
118+
R2_local = r2 * G
119+
120+
# Index lookup and uniqueness check
121+
matches = [j for j in range(len(pubnonces)) if pubnonces[j][1] == R2_local]
122+
assert len(matches) == 1
123+
j = matches[0]
124+
assert xbytes(pks[j]) == xbytes(P) and msgs[j] == m
125+
126+
R, b = GetSessionValues(aggnonce, pks, msgs, pubnonces)
127+
e = Scalar(1) if has_even_y(R) else -Scalar(1)
128+
d_prime = sk if has_even_y(P) else -sk
129+
130+
L = b''
131+
for i in range(len(pks)):
132+
L += xbytes(pks[i]) + msgs[i]
133+
c_j = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[j]) + msgs[j]))
134+
s_j = e * (r1 + b * r2) + c_j * d_prime
135+
return s_j
136+
137+
138+
#
139+
# Aggregation
140+
#
141+
142+
def SigAgg(aggnonce: Tuple[GE, GE], pks: List[GE], msgs: List[bytes],
143+
pubnonces: List[Tuple[GE, GE]], psigs: List[Scalar]) -> Tuple[GE, Scalar]:
144+
assert len(pks) == len(msgs) == len(pubnonces) == len(psigs) >= 1
145+
assert all(len(mi) == 32 for mi in msgs)
146+
R, _ = GetSessionValues(aggnonce, pks, msgs, pubnonces)
147+
s = Scalar.sum(*psigs)
148+
return R, s
149+
150+
151+
#
152+
# Partial Signature Verification
153+
#
154+
155+
def PartialSigVerify(psig: Scalar, pks: List[GE], msgs: List[bytes],
156+
pubnonces: List[Tuple[GE, GE]], signer_index: int) -> bool:
157+
if not (len(pks) == len(msgs) == len(pubnonces) >= 1) or any(len(mi) != 32 for mi in msgs):
158+
return False
159+
if not (0 <= signer_index < len(pks)):
160+
return False
161+
i = signer_index
162+
R1_i, R2_i = pubnonces[i]
163+
P_i = GE.from_bytes_xonly(xbytes(pks[i]))
164+
R, b = GetSessionValues(NonceAgg(pubnonces), pks, msgs, pubnonces)
165+
e = Scalar(1) if has_even_y(R) else -Scalar(1)
166+
167+
L = b''
168+
for k in range(len(pks)):
169+
L += xbytes(pks[k]) + msgs[k]
170+
c_i = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[i]) + msgs[i]))
171+
R_eff_i = R1_i + b * R2_i
172+
return psig * G == e * R_eff_i + c_i * P_i
173+
174+
175+
#
176+
# Aggregate Signature Verification
177+
#
178+
179+
def Verify(pks: List[GE], msgs: List[bytes], sig: Tuple[GE, Scalar]) -> bool:
180+
R_point, s = sig
181+
if not (len(pks) == len(msgs) >= 1) or any(len(mi) != 32 for mi in msgs):
182+
return False
183+
u = len(pks)
184+
R = GE.from_bytes_xonly(xbytes(R_point))
185+
Ps = [GE.from_bytes_xonly(xbytes(pks[i])) for i in range(u)]
186+
187+
L = b''
188+
for i in range(u):
189+
L += xbytes(pks[i]) + msgs[i]
190+
rhs = R
191+
for i in range(u):
192+
c_i = Scalar.from_bytes_wrapping(tagged_hash(FULLAGG_TAG_SIG, L + xbytes(R) + xbytes(pks[i]) + msgs[i]))
193+
rhs = rhs + c_i * Ps[i]
194+
return s * G == rhs

0 commit comments

Comments
 (0)