Skip to content

Commit 33c75c4

Browse files
Add Tonelli-Shanks modular square root algorithm.
Implements Legendre symbol checks and both the p≡3 (mod 4) fast path and the full Tonelli-Shanks procedure, with doctests for success and error cases.
1 parent f5988cc commit 33c75c4

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

maths/tonelli_shanks.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
Tonelli-Shanks algorithm for modular square roots.
3+
4+
Given an odd prime modulus ``prime`` and an integer ``residue``, find an
5+
integer ``root`` such that ``root ** 2 ≡ residue (mod prime)``, or report that
6+
no square root exists.
7+
8+
The algorithm is efficient when ``prime ≡ 3 (mod 4)`` (a single
9+
exponentiation) and uses the full Tonelli-Shanks procedure for
10+
``prime ≡ 1 (mod 4)``.
11+
12+
https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm
13+
"""
14+
15+
from __future__ import annotations
16+
17+
18+
def legendre_symbol(residue: int, prime: int) -> int:
19+
"""
20+
Compute the Legendre symbol (residue / prime).
21+
22+
Returns 1 if residue is a quadratic residue modulo prime (and residue
23+
is not divisible by prime), -1 if it is a non-residue, and 0 if
24+
residue ≡ 0 (mod prime).
25+
26+
>>> legendre_symbol(2, 7)
27+
1
28+
>>> legendre_symbol(3, 7)
29+
-1
30+
>>> legendre_symbol(14, 7)
31+
0
32+
>>> legendre_symbol(5, 11)
33+
1
34+
"""
35+
if prime <= 2 or prime % 2 == 0:
36+
raise ValueError("prime must be an odd prime")
37+
symbol = pow(residue % prime, (prime - 1) // 2, prime)
38+
return -1 if symbol == prime - 1 else symbol
39+
40+
41+
def tonelli_shanks(residue: int, prime: int) -> int:
42+
"""
43+
Return a modular square root of ``residue`` modulo odd prime ``prime``.
44+
45+
If both roots exist, the smaller non-negative representative is returned.
46+
Raises ValueError when ``residue`` is not a quadratic residue, or when
47+
``prime`` is not a valid odd prime modulus for this routine.
48+
49+
>>> tonelli_shanks(5, 41)
50+
13
51+
>>> pow(13, 2, 41)
52+
5
53+
>>> tonelli_shanks(2, 7)
54+
3
55+
>>> pow(3, 2, 7)
56+
2
57+
>>> tonelli_shanks(10, 13)
58+
6
59+
>>> tonelli_shanks(0, 11)
60+
0
61+
>>> tonelli_shanks(8, 17)
62+
5
63+
>>> pow(5, 2, 17)
64+
8
65+
>>> tonelli_shanks(3, 7)
66+
Traceback (most recent call last):
67+
...
68+
ValueError: 3 is not a quadratic residue modulo 7
69+
>>> tonelli_shanks(5, 4)
70+
Traceback (most recent call last):
71+
...
72+
ValueError: prime must be an odd prime
73+
>>> tonelli_shanks(5, 1)
74+
Traceback (most recent call last):
75+
...
76+
ValueError: prime must be an odd prime
77+
"""
78+
if prime <= 2 or prime % 2 == 0:
79+
raise ValueError("prime must be an odd prime")
80+
81+
residue %= prime
82+
if residue == 0:
83+
return 0
84+
85+
symbol = legendre_symbol(residue, prime)
86+
if symbol != 1:
87+
raise ValueError(f"{residue} is not a quadratic residue modulo {prime}")
88+
89+
# Fast path: prime ≡ 3 (mod 4)
90+
if prime % 4 == 3:
91+
root = pow(residue, (prime + 1) // 4, prime)
92+
return min(root, prime - root)
93+
94+
# Write prime - 1 = q * 2^s with q odd
95+
exponent_q = prime - 1
96+
power_of_two_s = 0
97+
while exponent_q % 2 == 0:
98+
exponent_q //= 2
99+
power_of_two_s += 1
100+
101+
# Find a quadratic non-residue z
102+
non_residue = 2
103+
while legendre_symbol(non_residue, prime) != -1:
104+
non_residue += 1
105+
106+
modular_c = pow(non_residue, exponent_q, prime)
107+
modular_r = pow(residue, (exponent_q + 1) // 2, prime)
108+
modular_t = pow(residue, exponent_q, prime)
109+
remaining_s = power_of_two_s
110+
111+
while modular_t != 1:
112+
# Find the least i such that t^(2^i) ≡ 1 (mod prime)
113+
test_power = modular_t
114+
least_i = 0
115+
for candidate_i in range(1, remaining_s):
116+
test_power = pow(test_power, 2, prime)
117+
if test_power == 1:
118+
least_i = candidate_i
119+
break
120+
else:
121+
raise ValueError(f"{residue} is not a quadratic residue modulo {prime}")
122+
123+
modular_b = pow(modular_c, 1 << (remaining_s - least_i - 1), prime)
124+
modular_r = (modular_r * modular_b) % prime
125+
modular_c = pow(modular_b, 2, prime)
126+
modular_t = (modular_t * modular_c) % prime
127+
remaining_s = least_i
128+
129+
return min(modular_r, prime - modular_r)
130+
131+
132+
if __name__ == "__main__":
133+
import doctest
134+
135+
doctest.testmod()

0 commit comments

Comments
 (0)