Skip to content

Commit 2068ec8

Browse files
feat(Maths): add Tonelli-Shanks modular square root
Implements Tonelli–Shanks for odd prime moduli with Legendre check, p≡3 (mod 4) fast path, and Vitest coverage. Signed-off-by: Felipe Fernandes <felipe.of.dev@gmail.com>
1 parent 5c39e87 commit 2068ec8

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

Maths/TonelliShanks.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Tonelli–Shanks algorithm for modular square roots modulo an odd prime.
3+
* https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm
4+
*
5+
* Returns the smaller non-negative root r such that r^2 ≡ n (mod p).
6+
* Throws RangeError when n is not a quadratic residue or p is invalid.
7+
*/
8+
9+
/**
10+
* @param {number} a
11+
* @param {number} p odd prime
12+
* @returns {number} Legendre symbol (a/p) in {-1, 0, 1}
13+
*/
14+
function legendreSymbol(a, p) {
15+
const exp = (p - 1) / 2
16+
let result = 1
17+
a = ((a % p) + p) % p
18+
let base = a
19+
let e = exp
20+
while (e > 0) {
21+
if (e % 2 === 1) result = (result * base) % p
22+
base = (base * base) % p
23+
e = Math.floor(e / 2)
24+
}
25+
if (result === p - 1) return -1
26+
return result
27+
}
28+
29+
/**
30+
* @param {number} n integer
31+
* @param {number} p odd prime modulus
32+
* @returns {number} smaller non-negative modular square root
33+
*/
34+
export function tonelliShanks(n, p) {
35+
if (
36+
typeof n !== 'number' ||
37+
typeof p !== 'number' ||
38+
!Number.isInteger(n) ||
39+
!Number.isInteger(p)
40+
) {
41+
throw new TypeError('Arguments must be integers')
42+
}
43+
if (p <= 2 || p % 2 === 0) {
44+
throw new RangeError('p must be an odd prime')
45+
}
46+
47+
n = ((n % p) + p) % p
48+
if (n === 0) return 0
49+
50+
const ls = legendreSymbol(n, p)
51+
if (ls !== 1) {
52+
throw new RangeError('n is not a quadratic residue modulo p')
53+
}
54+
55+
const modPow = (base, exp, mod) => {
56+
let result = 1
57+
base = ((base % mod) + mod) % mod
58+
while (exp > 0) {
59+
if (exp % 2 === 1) result = (result * base) % mod
60+
base = (base * base) % mod
61+
exp = Math.floor(exp / 2)
62+
}
63+
return result
64+
}
65+
66+
// Fast path: p ≡ 3 (mod 4)
67+
if (p % 4 === 3) {
68+
const r = modPow(n, (p + 1) / 4, p)
69+
return Math.min(r, p - r)
70+
}
71+
72+
// Write p - 1 = q * 2^s with q odd
73+
let q = p - 1
74+
let s = 0
75+
while (q % 2 === 0) {
76+
q /= 2
77+
s += 1
78+
}
79+
80+
// Find a quadratic non-residue z
81+
let z = 2
82+
while (legendreSymbol(z, p) !== -1) {
83+
z += 1
84+
if (z >= p) throw new RangeError('failed to find quadratic non-residue')
85+
}
86+
87+
let m = s
88+
let c = modPow(z, q, p)
89+
let r = modPow(n, (q + 1) / 2, p)
90+
let t = modPow(n, q, p)
91+
92+
while (t !== 1) {
93+
let i = 1
94+
let t2i = (t * t) % p
95+
while (t2i !== 1) {
96+
t2i = (t2i * t2i) % p
97+
i += 1
98+
if (i === m) throw new RangeError('tonelli-shanks failed')
99+
}
100+
const b = modPow(c, 2 ** (m - i - 1), p)
101+
r = (r * b) % p
102+
c = (b * b) % p
103+
t = (t * c) % p
104+
m = i
105+
}
106+
107+
return Math.min(r, p - r)
108+
}

Maths/test/TonelliShanks.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { tonelliShanks } from '../TonelliShanks'
2+
3+
describe('tonelliShanks', () => {
4+
it.each([
5+
[2, 7, 3], // 3^2 = 9 ≡ 2 (mod 7)
6+
[10, 13, 6], // 6^2 = 36 ≡ 10 (mod 13)
7+
[0, 11, 0],
8+
[5, 11, 4] // 4^2 = 16 ≡ 5 (mod 11); p ≡ 3 (mod 4)
9+
])('returns smaller root for %i mod %i', (n, p, expected) => {
10+
expect(tonelliShanks(n, p)).toBe(expected)
11+
})
12+
13+
it('throws for non-residues', () => {
14+
expect(() => tonelliShanks(3, 7)).toThrow(RangeError)
15+
})
16+
17+
it('throws for invalid modulus', () => {
18+
expect(() => tonelliShanks(2, 2)).toThrow(RangeError)
19+
expect(() => tonelliShanks(2, 4)).toThrow(RangeError)
20+
})
21+
22+
it('throws for non-integer inputs', () => {
23+
expect(() => tonelliShanks('2', 7)).toThrow(TypeError)
24+
})
25+
})

0 commit comments

Comments
 (0)