<div class="controls">
<label>Prime <i>p</i>: <input id="inp-p" type="number" value="97" min="3" step="2"></label>
<label><i>a</i> (mod p): <input id="inp-a" type="number" value="75" min="1"></label>
<button id="btn-run" type="button">Solve x²≡a (mod p)</button>
</div>
<div id="result" class="result"></div>
<div id="steps" class="steps"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; margin-bottom: .8rem; }
.controls label { font-size: .88rem; color: #444; display: flex; flex-direction: column; gap: .2rem; }
.controls input { width: 90px; padding: .3rem .5rem; border: 1px solid #cdd9e3; border-radius: 6px;
font-size: 1rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; align-self: flex-end; }
.result { font: 700 1rem system-ui, sans-serif; min-height: 1.5em; margin-bottom: .5rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.steps { font-size: .85rem; line-height: 1.6; }
.step { background: #f4f7fa; border-left: 3px solid #1d3557; padding: .3rem .6rem;
margin: .3rem 0; border-radius: 0 6px 6px 0; font-family: ui-monospace, monospace; }
.step.hi { border-color: #0a7d33; background: #e6f4ec; }
.step.label { font-family: system-ui, sans-serif; border-color: #8899aa;
background: transparent; font-style: italic; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Tonelli-Shanks algorithm: find x such that x^2 ≡ a (mod p)
// Reference: Shanks 1973; see Wikipedia "Tonelli–Shanks algorithm"
function modpow(base, exp, mod) {
// Fast modular exponentiation using BigInt
base = ((base % mod) + mod) % mod;
let result = 1n;
while (exp > 0n) {
if (exp & 1n) result = result * base % mod;
base = base * base % mod;
exp >>= 1n;
}
return result;
}
function isPrime(n) {
if (n < 2n) return false;
if (n === 2n) return true;
if (n % 2n === 0n) return false;
for (let i = 3n; i * i <= n; i += 2n) if (n % i === 0n) return false;
return true;
}
function tonelliShanks(n, p) {
// Returns { root, steps } or { root: null, steps }
const steps = [];
const log = (msg, hi) => steps.push({ msg, hi: !!hi });
n = ((n % p) + p) % p;
// Step 1: Euler's criterion
const euler = modpow(n, (p - 1n) / 2n, p);
log(fmt('Euler criterion: %n%^((%p%-1)/2) ≡ %euler% (mod %p%)', { n: n, p: p, euler: euler }));
if (euler !== 1n) {
log(fmt('%n% is NOT a quadratic residue mod %p% — no solution exists.', { n: n, p: p }));
return { root: null, steps };
}
log(fmt('%n% is a quadratic residue mod %p% — a root exists.', { n: n, p: p }), true);
// Trivial case: p ≡ 3 (mod 4)
if (p % 4n === 3n) {
const r = modpow(n, (p + 1n) / 4n, p);
log(`p ≡ 3 (mod 4): direct formula x = a^((p+1)/4) mod p`);
log(`x = ${n}^((${p}+1)/4) = ${n}^${(p+1n)/4n} ≡ ${r} (mod ${p})`, true);
log(fmt('Verify: %r%² = %v% ≡ %v2% (mod %p%) ✓', { r: r, v: r*r, v2: r*r % p, p: p }), true);
return { root: r, steps };
}
// Factor out 2s from p-1: p-1 = Q * 2^S
let Q = p - 1n, S = 0n;
while (Q % 2n === 0n) { Q /= 2n; S++; }
log(fmt('Factor p-1=%v%: Q=%Q%, S=%S% (p-1 = Q·2^S)', { v: p-1n, Q: Q, S: S }));
// Find a non-residue z
let z = 2n;
while (modpow(z, (p - 1n) / 2n, p) !== p - 1n) z++;
log(fmt('Non-residue found: z=%z% (z^((p-1)/2) ≡ -1 mod p)', { z: z }));
// Initialize
let M = S;
let c = modpow(z, Q, p);
let t = modpow(n, Q, p);
let R = modpow(n, (Q + 1n) / 2n, p);
log(fmt('Init: M=%M%, c=%c%, t=%t%, R=%R%', { M: M, c: c, t: t, R: R }));
log(`Descent loop: reduce 2-adic valuation of t until t≡1 (mod p)`);
// Main loop
for (let iter = 0; iter < 100; iter++) {
if (t === 0n) { log(`t=0 → x=0`); return { root: 0n, steps }; }
if (t === 1n) { log(fmt('t≡1 (mod p): descent complete → x=%R%', { R: R }), true); break; }
// Find least i such that t^(2^i) ≡ 1
let i = 1n, tmp = t * t % p;
while (tmp !== 1n) { tmp = tmp * tmp % p; i++; }
log(`i=${i}: t^(2^${i}) ≡ 1 (mod p); b = c^(2^(M-i-1))`);
const b = modpow(c, modpow(2n, M - i - 1n, p - 1n), p);
M = i;
c = b * b % p;
t = t * c % p;
R = R * b % p;
log(` → M=${M}, c=${c}, t=${t}, R=${R}`);
}
log(fmt('Result: x=%R%, x²≡%v% (mod %p%) ✓', { R: R, v: R*R%p, p: p }), true);
return { root: R, steps };
}
function run() {
const pVal = BigInt(document.getElementById('inp-p').value || '97');
const aVal = BigInt(document.getElementById('inp-a').value || '75');
const stepsEl = document.getElementById('steps');
const resultEl = document.getElementById('result');
stepsEl.innerHTML = '';
resultEl.textContent = '';
resultEl.className = 'result';
if (!isPrime(pVal) || pVal < 3n) {
resultEl.textContent = fmt('p=%pVal% is not an odd prime.', { pVal: pVal });
resultEl.className = 'result bad';
return;
}
if (aVal <= 0n || aVal >= pVal) {
resultEl.textContent = `a must be between 1 and p-1.`;
resultEl.className = 'result bad';
return;
}
const { root, steps } = tonelliShanks(aVal, pVal);
steps.forEach(({ msg, hi }) => {
const d = document.createElement('div');
d.className = 'step' + (hi ? ' hi' : '');
d.textContent = msg;
stepsEl.appendChild(d);
});
if (root === null) {
resultEl.textContent = fmt('No solution: %aVal% is not a quadratic residue mod %pVal%.', { aVal: aVal, pVal: pVal });
resultEl.className = 'result bad';
} else {
const other = (pVal - root) % pVal;
resultEl.textContent = fmt('x = %root% (also x = %other%); %root%² ≡ %v% (mod %pVal%) ✓', { root: root, other: other, v: root*root%pVal, pVal: pVal });
resultEl.className = 'result ok';
}
}
document.getElementById('btn-run').addEventListener('click', run);
run();
<div class="controls">
<label>Prime <i>p</i>: <input id="inp-p" type="number" value="97" min="3" step="2"></label>
<label><i>a</i> (mod p): <input id="inp-a" type="number" value="75" min="1"></label>
<button id="btn-run" type="button">Solve x²≡a (mod p)</button>
</div>
<div id="result" class="result"></div>
<div id="steps" class="steps"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; margin-bottom: .8rem; }
.controls label { font-size: .88rem; color: #444; display: flex; flex-direction: column; gap: .2rem; }
.controls input { width: 90px; padding: .3rem .5rem; border: 1px solid #cdd9e3; border-radius: 6px;
font-size: 1rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; align-self: flex-end; }
.result { font: 700 1rem system-ui, sans-serif; min-height: 1.5em; margin-bottom: .5rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.steps { font-size: .85rem; line-height: 1.6; }
.step { background: #f4f7fa; border-left: 3px solid #1d3557; padding: .3rem .6rem;
margin: .3rem 0; border-radius: 0 6px 6px 0; font-family: ui-monospace, monospace; }
.step.hi { border-color: #0a7d33; background: #e6f4ec; }
.step.label { font-family: system-ui, sans-serif; border-color: #8899aa;
background: transparent; font-style: italic; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Tonelli-Shanks algorithm: find x such that x^2 ≡ a (mod p)
// Reference: Shanks 1973; see Wikipedia "Tonelli–Shanks algorithm"
function modpow(base, exp, mod) {
// Fast modular exponentiation using BigInt
base = ((base % mod) + mod) % mod;
let result = 1n;
while (exp > 0n) {
if (exp & 1n) result = result * base % mod;
base = base * base % mod;
exp >>= 1n;
}
return result;
}
function isPrime(n) {
if (n < 2n) return false;
if (n === 2n) return true;
if (n % 2n === 0n) return false;
for (let i = 3n; i * i <= n; i += 2n) if (n % i === 0n) return false;
return true;
}
function tonelliShanks(n, p) {
// Returns { root, steps } or { root: null, steps }
const steps = [];
const log = (msg, hi) => steps.push({ msg, hi: !!hi });
n = ((n % p) + p) % p;
// Step 1: Euler's criterion
const euler = modpow(n, (p - 1n) / 2n, p);
log(fmt('Euler criterion: %n%^((%p%-1)/2) ≡ %euler% (mod %p%)', { n: n, p: p, euler: euler }));
if (euler !== 1n) {
log(fmt('%n% is NOT a quadratic residue mod %p% — no solution exists.', { n: n, p: p }));
return { root: null, steps };
}
log(fmt('%n% is a quadratic residue mod %p% — a root exists.', { n: n, p: p }), true);
// Trivial case: p ≡ 3 (mod 4)
if (p % 4n === 3n) {
const r = modpow(n, (p + 1n) / 4n, p);
log(`p ≡ 3 (mod 4): direct formula x = a^((p+1)/4) mod p`);
log(`x = ${n}^((${p}+1)/4) = ${n}^${(p+1n)/4n} ≡ ${r} (mod ${p})`, true);
log(fmt('Verify: %r%² = %v% ≡ %v2% (mod %p%) ✓', { r: r, v: r*r, v2: r*r % p, p: p }), true);
return { root: r, steps };
}
// Factor out 2s from p-1: p-1 = Q * 2^S
let Q = p - 1n, S = 0n;
while (Q % 2n === 0n) { Q /= 2n; S++; }
log(fmt('Factor p-1=%v%: Q=%Q%, S=%S% (p-1 = Q·2^S)', { v: p-1n, Q: Q, S: S }));
// Find a non-residue z
let z = 2n;
while (modpow(z, (p - 1n) / 2n, p) !== p - 1n) z++;
log(fmt('Non-residue found: z=%z% (z^((p-1)/2) ≡ -1 mod p)', { z: z }));
// Initialize
let M = S;
let c = modpow(z, Q, p);
let t = modpow(n, Q, p);
let R = modpow(n, (Q + 1n) / 2n, p);
log(fmt('Init: M=%M%, c=%c%, t=%t%, R=%R%', { M: M, c: c, t: t, R: R }));
log(`Descent loop: reduce 2-adic valuation of t until t≡1 (mod p)`);
// Main loop
for (let iter = 0; iter < 100; iter++) {
if (t === 0n) { log(`t=0 → x=0`); return { root: 0n, steps }; }
if (t === 1n) { log(fmt('t≡1 (mod p): descent complete → x=%R%', { R: R }), true); break; }
// Find least i such that t^(2^i) ≡ 1
let i = 1n, tmp = t * t % p;
while (tmp !== 1n) { tmp = tmp * tmp % p; i++; }
log(`i=${i}: t^(2^${i}) ≡ 1 (mod p); b = c^(2^(M-i-1))`);
const b = modpow(c, modpow(2n, M - i - 1n, p - 1n), p);
M = i;
c = b * b % p;
t = t * c % p;
R = R * b % p;
log(` → M=${M}, c=${c}, t=${t}, R=${R}`);
}
log(fmt('Result: x=%R%, x²≡%v% (mod %p%) ✓', { R: R, v: R*R%p, p: p }), true);
return { root: R, steps };
}
function run() {
const pVal = BigInt(document.getElementById('inp-p').value || '97');
const aVal = BigInt(document.getElementById('inp-a').value || '75');
const stepsEl = document.getElementById('steps');
const resultEl = document.getElementById('result');
stepsEl.innerHTML = '';
resultEl.textContent = '';
resultEl.className = 'result';
if (!isPrime(pVal) || pVal < 3n) {
resultEl.textContent = fmt('p=%pVal% is not an odd prime.', { pVal: pVal });
resultEl.className = 'result bad';
return;
}
if (aVal <= 0n || aVal >= pVal) {
resultEl.textContent = `a must be between 1 and p-1.`;
resultEl.className = 'result bad';
return;
}
const { root, steps } = tonelliShanks(aVal, pVal);
steps.forEach(({ msg, hi }) => {
const d = document.createElement('div');
d.className = 'step' + (hi ? ' hi' : '');
d.textContent = msg;
stepsEl.appendChild(d);
});
if (root === null) {
resultEl.textContent = fmt('No solution: %aVal% is not a quadratic residue mod %pVal%.', { aVal: aVal, pVal: pVal });
resultEl.className = 'result bad';
} else {
const other = (pVal - root) % pVal;
resultEl.textContent = fmt('x = %root% (also x = %other%); %root%² ≡ %v% (mod %pVal%) ✓', { root: root, other: other, v: root*root%pVal, pVal: pVal });
resultEl.className = 'result ok';
}
}
document.getElementById('btn-run').addEventListener('click', run);
run();
Comments
Loading comments...