Skip to content

Commit 2f1c4d8

Browse files
authored
Merge pull request bitcoin#124 from sipa/square_terminology
Settle on notation: is_square(y), has_square_y(P)
2 parents eacf0c6 + 0c6a9cf commit 2f1c4d8

File tree

4 files changed

+33
-31
lines changed

4 files changed

+33
-31
lines changed

bip-schnorr.mediawiki

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ The following conventions are used, with constants as defined for [https://www.s
106106
** The function ''bytes(P)'', where ''P'' is a point, returns ''bytes(x(P))'.
107107
** The function ''int(x)'', where ''x'' is a 32-byte array, returns the 256-bit unsigned integer whose most significant byte first encoding is ''x''.
108108
** The function ''is_square(x)'', where ''x'' is an integer, returns whether or not ''x'' is a quadratic residue modulo ''p''. Since ''p'' is prime, it is equivalent to the Legendre symbol ''(x / p) = x<sup>(p-1)/2</sup> mod p'' being equal to ''1'' (see [https://en.wikipedia.org/wiki/Euler%27s_criterion Euler's criterion])<ref>For points ''P'' on the secp256k1 curve it holds that ''x<sup>(p-1)/2</sup> &ne; 0 mod p''.</ref>.
109-
** The function ''is_positive(P)'', where ''P'' is a point, is defined as ''not is_infinite(P) and is_square(y(P))''<ref>For points ''P'' on the secp256k1 curve it holds that ''is_positive(P) = not is_positive(-P)''.</ref>.
110-
** The function ''lift_x(x)'', where ''x'' is an integer in range ''0..p-1'', returns the point ''P'' for which ''x(P) = x'' and ''is_positive(P)'', or fails if no such point exists<ref>Given an candidate X coordinate ''x'' in the range ''0..p-1'', there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then ''x'' is not a valid X coordinate either, i.e., no point ''P'' exists for which ''x(P) = x''. Given a candidate ''x'', the valid Y coordinates are the square roots of ''c = x<sup>3</sup> + 7 mod p'' and they can be computed as ''y = &plusmn;c<sup>(p+1)/4</sup> mod p'' (see [https://en.wikipedia.org/wiki/Quadratic_residue#Prime_or_prime_power_modulus Quadratic residue]) if they exist, which can be checked by squaring and comparing with ''c''. Due to [https://en.wikipedia.org/wiki/Euler%27s_criterion Euler's criterion] it then holds that ''c<sup>(p-1)/2</sup> = 1 mod p''. The same criterion applied to ''y'' results in ''y<sup>(p-1)/2</sup> mod p = &plusmn;c<sup>((p+1)/4)((p-1)/2)</sup> mod p = &plusmn;1 mod p''. Therefore ''y = +c<sup>(p+1)/4</sup> mod p'' is a quadratic residue and ''-y mod p'' is not.</ref>. The function ''lift_x(x)'' is equivalent to the following pseudocode:
109+
** The function ''has_square_y(P)'', where ''P'' is a point, is defined as ''not is_infinite(P) and is_square(y(P))''<ref>For points ''P'' on the secp256k1 curve it holds that ''has_square_y(P) = not has_square_y(-P)''.</ref>.
110+
** The function ''lift_x(x)'', where ''x'' is an integer in range ''0..p-1'', returns the point ''P'' for which ''x(P) = x'' and ''has_square_y(P)'', or fails if no such point exists<ref>Given an candidate X coordinate ''x'' in the range ''0..p-1'', there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then ''x'' is not a valid X coordinate either, i.e., no point ''P'' exists for which ''x(P) = x''. Given a candidate ''x'', the valid Y coordinates are the square roots of ''c = x<sup>3</sup> + 7 mod p'' and they can be computed as ''y = &plusmn;c<sup>(p+1)/4</sup> mod p'' (see [https://en.wikipedia.org/wiki/Quadratic_residue#Prime_or_prime_power_modulus Quadratic residue]) if they exist, which can be checked by squaring and comparing with ''c''. Due to [https://en.wikipedia.org/wiki/Euler%27s_criterion Euler's criterion] it then holds that ''c<sup>(p-1)/2</sup> = 1 mod p''. The same criterion applied to ''y'' results in ''y<sup>(p-1)/2</sup> mod p = &plusmn;c<sup>((p+1)/4)((p-1)/2)</sup> mod p = &plusmn;1 mod p''. Therefore ''y = +c<sup>(p+1)/4</sup> mod p'' is a quadratic residue and ''-y mod p'' is not.</ref>. The function ''lift_x(x)'' is equivalent to the following pseudocode:
111111
*** Let ''c = x<sup>3</sup> + 7 mod p''.
112112
*** Let ''y = c<sup>(p+1)/4</sup> mod p''.
113113
*** Fail if ''c &ne; y<sup>2</sup> mod p''.
@@ -139,11 +139,11 @@ The algorithm ''Sign(sk, m)'' is defined as:
139139
* Let ''d' = int(sk)''
140140
* Fail if ''d' = 0'' or ''d' &ge; n''
141141
* Let ''P = d'⋅G''
142-
* Let ''d = d' '' if ''is_positive(P)'', otherwise let ''d = n - d' ''.
142+
* Let ''d = d' '' if ''has_square_y(P)'', otherwise let ''d = n - d' ''.
143143
* Let ''k' = int(hash<sub>BIPSchnorrDerive</sub>(bytes(d) || m)) mod n''<ref>Note that in general, taking the output of a hash function modulo the curve order will produce an unacceptably biased result. However, for the secp256k1 curve, the order is sufficiently close to ''2<sup>256</sup>'' that this bias is not observable (''1 - n / 2<sup>256</sup>'' is around ''1.27 * 2<sup>-128</sup>'').</ref>.
144144
* Fail if ''k' = 0''.
145145
* Let ''R = k'G''.
146-
* Let ''k = k' '' if ''is_positive(R)'', otherwise let ''k = n - k' ''.
146+
* Let ''k = k' '' if ''has_square_y(R)'', otherwise let ''k = n - k' ''.
147147
* Let ''e = int(hash<sub>BIPSchnorr</sub>(bytes(R) || bytes(P) || m)) mod n''.
148148
* Return the signature ''bytes(R) || bytes((k + ed) mod n)''.
149149
@@ -171,12 +171,12 @@ The algorithm ''Verify(pk, m, sig)'' is defined as:
171171
* Let ''s = int(sig[32:64])''; fail if ''s &ge; n''.
172172
* Let ''e = int(hash<sub>BIPSchnorr</sub>(bytes(r) || bytes(P) || m)) mod n''.
173173
* Let ''R = s⋅G - e⋅P''.
174-
* Fail if ''not is_positive(R)'' or ''x(R) &ne; r''.
174+
* Fail if ''not has_square_y(R)'' or ''x(R) &ne; r''.
175175
* Return success iff no failure occurred before reaching this point.
176176
177177
For every valid secret key ''sk'' and message ''m'', ''Verify(PubKey(sk),m,Sign(sk,m))'' will succeed.
178178
179-
Note that the correctness of verification relies on the fact that ''point(pk)'' always returns a positive point (i.e., with a square Y coordinate). A hypothetical verification algorithm that treats points as public keys, and takes the point ''P'' directly as input would fail any time a negative point is used. While it is possible to correct for this by negating negative points before further processing, this would result in a scheme where every (message, signature) pair is valid for two public keys (a type of malleability that exists for ECDSA as well, but we don't wish to retain). We avoid these problems by treating just the X coordinate as public key.
179+
Note that the correctness of verification relies on the fact that ''point(pk)'' always returns a point with a square Y coordinate. A hypothetical verification algorithm that treats points as public keys, and takes the point ''P'' directly as input would fail any time a point with non-square Y is used. While it is possible to correct for this by negating points with non-square Y coordinate before further processing, this would result in a scheme where every (message, signature) pair is valid for two public keys (a type of malleability that exists for ECDSA as well, but we don't wish to retain). We avoid these problems by treating just the X coordinate as public key.
180180
181181
==== Batch Verification ====
182182
@@ -206,7 +206,7 @@ Many techniques are known for optimizing elliptic curve implementations. Several
206206
'''Squareness testing''' The function ''is_square(x)'' is defined as above, but can be computed more efficiently using an [https://en.wikipedia.org/wiki/Jacobi_symbol#Calculating_the_Jacobi_symbol extended GCD algorithm].
207207
208208
'''Jacobian coordinates''' Elliptic Curve operations can be implemented more efficiently by using [https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates Jacobian coordinates]. Elliptic Curve operations implemented this way avoid many intermediate modular inverses (which are computationally expensive), and the scheme proposed in this document is in fact designed to not need any inversions at all for verification. When operating on a point ''P'' with Jacobian coordinates ''(x,y,z)'' which is not the point at infinity and for which ''x(P)'' is defined as ''x / z<sup>2</sup>'' and ''y(P)'' is defined as ''y / z<sup>3</sup>'':
209-
* ''is_positive(P)'' can be implemented as ''is_square(yz mod p)''.
209+
* ''has_square_y(P)'' can be implemented as ''is_square(yz mod p)''.
210210
* ''x(P) &ne; r'' can be implemented as ''(0 &le; r < p) and (x &ne; z<sup>2</sup>r mod p)''.
211211
212212
== Applications ==

bip-schnorr/reference.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ def tagged_hash(tag, msg):
1111
tag_hash = hashlib.sha256(tag.encode()).digest()
1212
return hashlib.sha256(tag_hash + tag_hash + msg).digest()
1313

14+
def is_infinity(P):
15+
return P is None
16+
1417
def x(P):
1518
return P[0]
1619

@@ -59,11 +62,11 @@ def int_from_bytes(b):
5962
def hash_sha256(b):
6063
return hashlib.sha256(b).digest()
6164

62-
def jacobi(x):
63-
return pow(x, (p - 1) // 2, p)
65+
def is_square(x):
66+
return pow(x, (p - 1) // 2, p) == 1
6467

65-
def is_quad(x):
66-
return jacobi(x) == 1
68+
def has_square_y(P):
69+
return not is_infinity(P) and is_square(y(P))
6770

6871
def pubkey_gen(seckey):
6972
x = int_from_bytes(seckey)
@@ -79,12 +82,12 @@ def schnorr_sign(msg, seckey0):
7982
if not (1 <= seckey0 <= n - 1):
8083
raise ValueError('The secret key must be an integer in the range 1..n-1.')
8184
P = point_mul(G, seckey0)
82-
seckey = seckey0 if is_quad(y(P)) else n - seckey0
85+
seckey = seckey0 if has_square_y(P) else n - seckey0
8386
k0 = int_from_bytes(tagged_hash("BIPSchnorrDerive", bytes_from_int(seckey) + msg)) % n
8487
if k0 == 0:
8588
raise RuntimeError('Failure. This happens only with negligible probability.')
8689
R = point_mul(G, k0)
87-
k = n - k0 if not is_quad(y(R)) else k0
90+
k = n - k0 if not has_square_y(R) else k0
8891
e = int_from_bytes(tagged_hash("BIPSchnorr", bytes_from_point(R) + bytes_from_point(P) + msg)) % n
8992
return bytes_from_point(R) + bytes_from_int((k + e * seckey) % n)
9093

@@ -104,7 +107,7 @@ def schnorr_verify(msg, pubkey, sig):
104107
return False
105108
e = int_from_bytes(tagged_hash("BIPSchnorr", sig[0:32] + pubkey + msg)) % n
106109
R = point_add(point_mul(G, s), point_mul(P, n - e))
107-
if R is None or not is_quad(y(R)) or x(R) != r:
110+
if R is None or not has_square_y(R) or x(R) != r:
108111
return False
109112
return True
110113

bip-schnorr/test-vectors.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ def vector2():
3030
msg = bytes_from_int(0x5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C)
3131
sig = schnorr_sign(msg, seckey)
3232

33-
# This singature vector would not verify if the implementer checked the
34-
# jacobi symbol of the X coordinate of R instead of the Y coordinate.
33+
# This signature vector would not verify if the implementer checked the
34+
# squareness of the X coordinate of R instead of the Y coordinate.
3535
R = point_from_bytes(sig[0:32])
36-
assert(jacobi(R[0]) != 1)
36+
assert(not is_square(R[0]))
3737

3838
return (bytes_from_int(seckey), pubkey_gen(seckey), msg, sig, "TRUE", None)
3939

@@ -43,15 +43,14 @@ def vector3():
4343
sig = schnorr_sign(msg, seckey)
4444
return (bytes_from_int(seckey), pubkey_gen(seckey), msg, sig, "TRUE", "test fails if msg is reduced modulo p or n")
4545

46-
# Signs with a given nonce. Results in an invalid signature if y(kG) is not a
47-
# quadratic residue.
46+
# Signs with a given nonce. Results in an invalid signature if y(kG) is not a square
4847
def schnorr_sign_fixed_nonce(msg, seckey0, k):
4948
if len(msg) != 32:
5049
raise ValueError('The message must be a 32-byte array.')
5150
if not (1 <= seckey0 <= n - 1):
5251
raise ValueError('The secret key must be an integer in the range 1..n-1.')
5352
P = point_mul(G, seckey0)
54-
seckey = seckey0 if (jacobi(P[1]) == 1) else n - seckey0
53+
seckey = seckey0 if has_square_y(P) else n - seckey0
5554
R = point_mul(G, k)
5655
e = int_from_bytes(tagged_hash("BIPSchnorr", bytes_from_point(R) + bytes_from_point(P) + msg)) % n
5756
return bytes_from_point(R) + bytes_from_int((k + e * seckey) % n)
@@ -84,9 +83,9 @@ def vector6():
8483
k = 3
8584
sig = schnorr_sign_fixed_nonce(msg, seckey, k)
8685

87-
# Y coordinate of R is not a quadratic residue
86+
# Y coordinate of R is not a square
8887
R = point_mul(G, k)
89-
assert(jacobi(R[1]) != 1)
88+
assert(not has_square_y(R))
9089

9190
return (None, pubkey_gen(seckey), msg, sig, "FALSE", "incorrect R residuosity")
9291

@@ -121,7 +120,7 @@ def vector9():
121120
sig = schnorr_sign_fixed_nonce(msg, seckey, k)
122121
bytes_from_point.__code__ = bytes_from_point_tmp
123122

124-
return (None, pubkey_gen(seckey), msg, sig, "FALSE", "sG - eP is infinite. Test fails in single verification if jacobi(y(inf)) is defined as 1 and x(inf) as 0")
123+
return (None, pubkey_gen(seckey), msg, sig, "FALSE", "sG - eP is infinite. Test fails in single verification if has_square_y(inf) is defined as true and x(inf) as 0")
125124

126125
def bytes_from_point_inf1(P):
127126
if P == None:
@@ -140,7 +139,7 @@ def vector10():
140139
sig = schnorr_sign_fixed_nonce(msg, seckey, k)
141140
bytes_from_point.__code__ = bytes_from_point_tmp
142141

143-
return (None, pubkey_gen(seckey), msg, sig, "FALSE", "sG - eP is infinite. Test fails in single verification if jacobi(y(inf)) is defined as 1 and x(inf) as 1")
142+
return (None, pubkey_gen(seckey), msg, sig, "FALSE", "sG - eP is infinite. Test fails in single verification if has_square_y(inf) is defined as true and x(inf) as 1")
144143

145144
# It's cryptographically impossible to create a test vector that fails if run
146145
# in an implementation which merely misses the check that sig[0:32] is an X

0 commit comments

Comments
 (0)