<div class="controls">
<label>Prime <em>p</em>:
<select id="selP">
<option value="23">23</option>
<option value="47">47</option>
<option value="89" selected>89</option>
<option value="127">127</option>
</select>
</label>
<label>Generator <em>g</em>: <span id="lblG">โ</span></label>
<label>Target <em>h = g^x mod p</em>:
<select id="selH"></select>
</label>
<button id="btnReset" type="button">Reset</button>
<button id="btnStep" type="button">Step โ</button>
<button id="btnRun" type="button">Run all</button>
</div>
<div class="info-row">
<span id="lblTortoise">Tortoise: step 0</span>
<span id="lblHare">Hare: step 0</span>
<span id="lblCollision"></span>
</div>
<canvas id="cv" width="480" height="280"></canvas>
<div id="result" class="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .5rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .3rem; }
select { font-size: .85rem; padding: .2rem .4rem; border: 1px solid #adb1b8; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button#btnReset { background: #fff; color: #1d3557; }
.info-row { display: flex; flex-wrap: wrap; gap: 1rem; font-size: .82rem; margin-bottom: .4rem; color: #444; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px; background: #f8fafc;
max-width: 100%; }
.result { font-size: 1rem; font-weight: 700; min-height: 1.5em; margin-top: .5rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pollard's rho for discrete logarithm in Z_p*.
// Find x such that g^x โก h (mod p).
// Uses the Brent / Floyd tortoise-and-hare cycle detection.
//
// Partition Z_p* into 3 sets by remainder mod 3:
// S1 (rem 0): new = h * cur mod p โ b_inc += 1
// S2 (rem 1): new = cur^2 mod p โ a_inc *= 2, b_inc *= 2
// S3 (rem 2): new = g * cur mod p โ a_inc += 1
// We track (x_val, a, b) with x_val = g^a * h^b mod p.
// When tortoise == hare: g^(a1-a2) โก h^(b2-b1) โ x = (a1-a2)/(b2-b1) mod ord
function modpow(base, exp, mod) {
let result = 1n; base %= mod;
while (exp > 0n) {
if (exp & 1n) result = result * base % mod;
base = base * base % mod; exp >>= 1n;
}
return result;
}
// Extended GCD โ returns [g, x, y] with a*x + b*y = g
function extgcd(a, b) {
if (b === 0n) return [a, 1n, 0n];
const [g, x, y] = extgcd(b, a % b);
return [g, y, x - (a / b) * y];
}
function modinv(a, m) {
const [g, x] = extgcd(((a % m) + m) % m, m);
if (g !== 1n) return null; // no inverse
return ((x % m) + m) % m;
}
// Check if g is a primitive root mod p (generator of Z_p*)
function isPrimitiveRoot(g, p) {
const ord = p - 1n;
// Factorize ord simply for small primes
let factors = [];
let n = ord;
for (let f = 2n; f * f <= n; f++) {
if (n % f === 0n) { factors.push(f); while (n % f === 0n) n /= f; }
}
if (n > 1n) factors.push(n);
return factors.every(f => modpow(BigInt(g), ord / f, p) !== 1n);
}
function findGenerator(p) {
for (let g = 2n; g < p; g++) {
if (isPrimitiveRoot(g, p)) return g;
}
return 2n;
}
// One step of the rho walk: returns [new_val, new_a, new_b]
function rhoStep(v, a, b, g, h, p, ord) {
const r = Number(v % 3n);
if (r === 0) {
return [v * h % p, a, (b + 1n) % ord];
} else if (r === 1) {
return [v * v % p, 2n * a % ord, 2n * b % ord];
} else {
return [v * g % p, (a + 1n) % ord, b];
}
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let P, G, H, ORD, TARGET_X;
let tortoise, hare; // {val, a, b}
let stepCount = 0;
let collisionFound = false;
let answerX = null;
let trail = []; // [{t: BigInt, h: BigInt}] for drawing
const MAX_TRAIL = 60;
function init() {
P = BigInt(document.getElementById('selP').value);
ORD = P - 1n;
G = findGenerator(P);
document.getElementById('lblG').textContent = G.toString();
// Populate target selector
const selH = document.getElementById('selH');
selH.innerHTML = '';
// Build a few interesting h values (small x values)
const xs = [3,5,7,11,13,17,19];
xs.filter(x => x < Number(ORD)).forEach(x => {
const h = modpow(G, BigInt(x), P);
const opt = document.createElement('option');
opt.value = x;
opt.textContent = `${h} (x=${x})`;
selH.appendChild(opt);
});
selH.selectedIndex = 2; // default x=7
reset();
}
function reset() {
P = BigInt(document.getElementById('selP').value);
ORD = P - 1n;
G = findGenerator(P);
const x0 = parseInt(document.getElementById('selH').value, 10);
TARGET_X = BigInt(x0);
H = modpow(G, TARGET_X, P);
// Start: val = g*h mod p, a=1, b=1
const sv = G * H % P;
tortoise = { val: sv, a: 1n, b: 1n };
hare = { val: sv, a: 1n, b: 1n };
stepCount = 0;
collisionFound = false;
answerX = null;
trail = [{ t: sv, h: sv }];
document.getElementById('lblCollision').textContent = '';
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
updateLabels();
draw();
}
function step() {
if (collisionFound) return;
// Tortoise: 1 step
const [tv, ta, tb] = rhoStep(tortoise.val, tortoise.a, tortoise.b, G, H, P, ORD);
tortoise = { val: tv, a: ta, b: tb };
// Hare: 2 steps
const [hv1, ha1, hb1] = rhoStep(hare.val, hare.a, hare.b, G, H, P, ORD);
const [hv2, ha2, hb2] = rhoStep(hv1, ha1, hb1, G, H, P, ORD);
hare = { val: hv2, a: ha2, b: hb2 };
stepCount++;
trail.push({ t: tv, h: hv2 });
if (trail.length > MAX_TRAIL) trail.shift();
if (tortoise.val === hare.val) {
collisionFound = true;
const bDiff = ((hare.b - tortoise.b) % ORD + ORD) % ORD;
const aDiff = ((tortoise.a - hare.a) % ORD + ORD) % ORD;
const inv = modinv(bDiff, ORD);
if (inv !== null) {
answerX = aDiff * inv % ORD;
} else {
// gcd > 1: try dividing โ simplified for small groups
const g = extgcd(bDiff, ORD)[0];
if (aDiff % g === 0n) {
const newOrd = ORD / g;
const inv2 = modinv(bDiff / g, newOrd);
if (inv2 !== null) {
const candidate = aDiff / g * inv2 % newOrd;
// Find the right multiple
for (let k = 0n; k < g; k++) {
const x = candidate + k * newOrd;
if (modpow(G, x, P) === H) { answerX = x; break; }
}
}
}
}
document.getElementById('lblCollision').textContent =
fmt('Collision at step %stepCount%! tortoise=hare=%val%', { stepCount: stepCount, val: tortoise.val });
const ok = answerX !== null && modpow(G, answerX, P) === H;
const res = document.getElementById('result');
if (ok) {
res.textContent = fmt('โ Found x = %answerX% โ %G%^%answerX% mod %P% = %H% (secret was %TARGET_X%)', { answerX: answerX, G: G, P: P, H: H, TARGET_X: TARGET_X });
res.className = 'result ok';
} else {
res.textContent = `Collision found but gcd split needed โ try Run all for brute-force verify.`;
res.className = 'result bad';
}
}
updateLabels();
draw();
}
function runAll() {
let limit = 0;
while (!collisionFound && limit++ < 2000) step();
}
function updateLabels() {
document.getElementById('lblTortoise').textContent =
fmt('Tortoise: step %stepCount%, val=%val%', { stepCount: stepCount, val: tortoise.val });
document.getElementById('lblHare').textContent =
fmt('Hare: step %v%, val=%val%', { v: stepCount*2, val: hare.val });
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const COLORS = { tortoise: '#e63946', hare: '#457b9d', trail: '#a8c0d6' };
function draw() {
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H_px = cv.height;
ctx.clearRect(0, 0, W, H_px);
// Draw a number line of group elements 1..P-1 as circles
const n = Number(P) - 1;
const cols = Math.min(n, 22);
const rows = Math.ceil(n / cols);
const cw = Math.floor((W - 20) / cols);
const ch = Math.floor((H_px - 40) / rows);
const r = Math.min(cw, ch) / 2 - 2;
function pos(v) {
const idx = Number(v) - 1; // 1-indexed group element -> 0-indexed
const col = idx % cols;
const row = Math.floor(idx / cols);
return { x: 10 + col * cw + cw / 2, y: 20 + row * ch + ch / 2 };
}
// Draw all elements
for (let v = 1; v <= n; v++) {
const p = pos(v);
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, 2 * Math.PI);
const isTort = BigInt(v) === tortoise.val;
const isHare = BigInt(v) === hare.val;
if (isTort && isHare) {
ctx.fillStyle = '#6a0dad'; // collision: purple
} else if (isTort) {
ctx.fillStyle = COLORS.tortoise;
} else if (isHare) {
ctx.fillStyle = COLORS.hare;
} else {
ctx.fillStyle = '#e8eef3';
}
ctx.fill();
ctx.strokeStyle = '#adb1b8'; ctx.lineWidth = 1;
ctx.stroke();
if (r > 8) {
ctx.fillStyle = (isTort || isHare) ? '#fff' : '#555';
ctx.font = `${Math.max(9, Math.min(11, r))}px ui-monospace, monospace`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(v.toString(), p.x, p.y);
}
}
// Highlight H and G
const pH = pos(Number(H));
ctx.beginPath(); ctx.arc(pH.x, pH.y, r + 3, 0, 2 * Math.PI);
ctx.strokeStyle = '#f4a261'; ctx.lineWidth = 2; ctx.stroke();
// Legend
ctx.font = '11px system-ui'; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
const items = [
{ c: COLORS.tortoise, label: 'Tortoise' },
{ c: COLORS.hare, label: 'Hare' },
{ c: '#6a0dad', label: 'Collision' },
{ c: '#f4a261', label: fmt('h=%H% (target)', { H: H }) },
];
let lx = 8;
items.forEach(it => {
ctx.fillStyle = it.c;
ctx.fillRect(lx, H_px - 14, 10, 10);
ctx.fillStyle = '#333';
ctx.fillText(it.label, lx + 13, H_px - 5);
lx += ctx.measureText(it.label).width + 28;
});
}
// โโ Bootstrap โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.getElementById('selP').addEventListener('change', init);
document.getElementById('selH').addEventListener('change', reset);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('btnStep').addEventListener('click', step);
document.getElementById('btnRun').addEventListener('click', runAll);
init();
<div class="controls">
<label>Prime <em>p</em>:
<select id="selP">
<option value="23">23</option>
<option value="47">47</option>
<option value="89" selected>89</option>
<option value="127">127</option>
</select>
</label>
<label>Generator <em>g</em>: <span id="lblG">โ</span></label>
<label>Target <em>h = g^x mod p</em>:
<select id="selH"></select>
</label>
<button id="btnReset" type="button">Reset</button>
<button id="btnStep" type="button">Step โ</button>
<button id="btnRun" type="button">Run all</button>
</div>
<div class="info-row">
<span id="lblTortoise">Tortoise: step 0</span>
<span id="lblHare">Hare: step 0</span>
<span id="lblCollision"></span>
</div>
<canvas id="cv" width="480" height="280"></canvas>
<div id="result" class="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .5rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .3rem; }
select { font-size: .85rem; padding: .2rem .4rem; border: 1px solid #adb1b8; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button#btnReset { background: #fff; color: #1d3557; }
.info-row { display: flex; flex-wrap: wrap; gap: 1rem; font-size: .82rem; margin-bottom: .4rem; color: #444; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px; background: #f8fafc;
max-width: 100%; }
.result { font-size: 1rem; font-weight: 700; min-height: 1.5em; margin-top: .5rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pollard's rho for discrete logarithm in Z_p*.
// Find x such that g^x โก h (mod p).
// Uses the Brent / Floyd tortoise-and-hare cycle detection.
//
// Partition Z_p* into 3 sets by remainder mod 3:
// S1 (rem 0): new = h * cur mod p โ b_inc += 1
// S2 (rem 1): new = cur^2 mod p โ a_inc *= 2, b_inc *= 2
// S3 (rem 2): new = g * cur mod p โ a_inc += 1
// We track (x_val, a, b) with x_val = g^a * h^b mod p.
// When tortoise == hare: g^(a1-a2) โก h^(b2-b1) โ x = (a1-a2)/(b2-b1) mod ord
function modpow(base, exp, mod) {
let result = 1n; base %= mod;
while (exp > 0n) {
if (exp & 1n) result = result * base % mod;
base = base * base % mod; exp >>= 1n;
}
return result;
}
// Extended GCD โ returns [g, x, y] with a*x + b*y = g
function extgcd(a, b) {
if (b === 0n) return [a, 1n, 0n];
const [g, x, y] = extgcd(b, a % b);
return [g, y, x - (a / b) * y];
}
function modinv(a, m) {
const [g, x] = extgcd(((a % m) + m) % m, m);
if (g !== 1n) return null; // no inverse
return ((x % m) + m) % m;
}
// Check if g is a primitive root mod p (generator of Z_p*)
function isPrimitiveRoot(g, p) {
const ord = p - 1n;
// Factorize ord simply for small primes
let factors = [];
let n = ord;
for (let f = 2n; f * f <= n; f++) {
if (n % f === 0n) { factors.push(f); while (n % f === 0n) n /= f; }
}
if (n > 1n) factors.push(n);
return factors.every(f => modpow(BigInt(g), ord / f, p) !== 1n);
}
function findGenerator(p) {
for (let g = 2n; g < p; g++) {
if (isPrimitiveRoot(g, p)) return g;
}
return 2n;
}
// One step of the rho walk: returns [new_val, new_a, new_b]
function rhoStep(v, a, b, g, h, p, ord) {
const r = Number(v % 3n);
if (r === 0) {
return [v * h % p, a, (b + 1n) % ord];
} else if (r === 1) {
return [v * v % p, 2n * a % ord, 2n * b % ord];
} else {
return [v * g % p, (a + 1n) % ord, b];
}
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let P, G, H, ORD, TARGET_X;
let tortoise, hare; // {val, a, b}
let stepCount = 0;
let collisionFound = false;
let answerX = null;
let trail = []; // [{t: BigInt, h: BigInt}] for drawing
const MAX_TRAIL = 60;
function init() {
P = BigInt(document.getElementById('selP').value);
ORD = P - 1n;
G = findGenerator(P);
document.getElementById('lblG').textContent = G.toString();
// Populate target selector
const selH = document.getElementById('selH');
selH.innerHTML = '';
// Build a few interesting h values (small x values)
const xs = [3,5,7,11,13,17,19];
xs.filter(x => x < Number(ORD)).forEach(x => {
const h = modpow(G, BigInt(x), P);
const opt = document.createElement('option');
opt.value = x;
opt.textContent = `${h} (x=${x})`;
selH.appendChild(opt);
});
selH.selectedIndex = 2; // default x=7
reset();
}
function reset() {
P = BigInt(document.getElementById('selP').value);
ORD = P - 1n;
G = findGenerator(P);
const x0 = parseInt(document.getElementById('selH').value, 10);
TARGET_X = BigInt(x0);
H = modpow(G, TARGET_X, P);
// Start: val = g*h mod p, a=1, b=1
const sv = G * H % P;
tortoise = { val: sv, a: 1n, b: 1n };
hare = { val: sv, a: 1n, b: 1n };
stepCount = 0;
collisionFound = false;
answerX = null;
trail = [{ t: sv, h: sv }];
document.getElementById('lblCollision').textContent = '';
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
updateLabels();
draw();
}
function step() {
if (collisionFound) return;
// Tortoise: 1 step
const [tv, ta, tb] = rhoStep(tortoise.val, tortoise.a, tortoise.b, G, H, P, ORD);
tortoise = { val: tv, a: ta, b: tb };
// Hare: 2 steps
const [hv1, ha1, hb1] = rhoStep(hare.val, hare.a, hare.b, G, H, P, ORD);
const [hv2, ha2, hb2] = rhoStep(hv1, ha1, hb1, G, H, P, ORD);
hare = { val: hv2, a: ha2, b: hb2 };
stepCount++;
trail.push({ t: tv, h: hv2 });
if (trail.length > MAX_TRAIL) trail.shift();
if (tortoise.val === hare.val) {
collisionFound = true;
const bDiff = ((hare.b - tortoise.b) % ORD + ORD) % ORD;
const aDiff = ((tortoise.a - hare.a) % ORD + ORD) % ORD;
const inv = modinv(bDiff, ORD);
if (inv !== null) {
answerX = aDiff * inv % ORD;
} else {
// gcd > 1: try dividing โ simplified for small groups
const g = extgcd(bDiff, ORD)[0];
if (aDiff % g === 0n) {
const newOrd = ORD / g;
const inv2 = modinv(bDiff / g, newOrd);
if (inv2 !== null) {
const candidate = aDiff / g * inv2 % newOrd;
// Find the right multiple
for (let k = 0n; k < g; k++) {
const x = candidate + k * newOrd;
if (modpow(G, x, P) === H) { answerX = x; break; }
}
}
}
}
document.getElementById('lblCollision').textContent =
fmt('Collision at step %stepCount%! tortoise=hare=%val%', { stepCount: stepCount, val: tortoise.val });
const ok = answerX !== null && modpow(G, answerX, P) === H;
const res = document.getElementById('result');
if (ok) {
res.textContent = fmt('โ Found x = %answerX% โ %G%^%answerX% mod %P% = %H% (secret was %TARGET_X%)', { answerX: answerX, G: G, P: P, H: H, TARGET_X: TARGET_X });
res.className = 'result ok';
} else {
res.textContent = `Collision found but gcd split needed โ try Run all for brute-force verify.`;
res.className = 'result bad';
}
}
updateLabels();
draw();
}
function runAll() {
let limit = 0;
while (!collisionFound && limit++ < 2000) step();
}
function updateLabels() {
document.getElementById('lblTortoise').textContent =
fmt('Tortoise: step %stepCount%, val=%val%', { stepCount: stepCount, val: tortoise.val });
document.getElementById('lblHare').textContent =
fmt('Hare: step %v%, val=%val%', { v: stepCount*2, val: hare.val });
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const COLORS = { tortoise: '#e63946', hare: '#457b9d', trail: '#a8c0d6' };
function draw() {
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H_px = cv.height;
ctx.clearRect(0, 0, W, H_px);
// Draw a number line of group elements 1..P-1 as circles
const n = Number(P) - 1;
const cols = Math.min(n, 22);
const rows = Math.ceil(n / cols);
const cw = Math.floor((W - 20) / cols);
const ch = Math.floor((H_px - 40) / rows);
const r = Math.min(cw, ch) / 2 - 2;
function pos(v) {
const idx = Number(v) - 1; // 1-indexed group element -> 0-indexed
const col = idx % cols;
const row = Math.floor(idx / cols);
return { x: 10 + col * cw + cw / 2, y: 20 + row * ch + ch / 2 };
}
// Draw all elements
for (let v = 1; v <= n; v++) {
const p = pos(v);
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, 2 * Math.PI);
const isTort = BigInt(v) === tortoise.val;
const isHare = BigInt(v) === hare.val;
if (isTort && isHare) {
ctx.fillStyle = '#6a0dad'; // collision: purple
} else if (isTort) {
ctx.fillStyle = COLORS.tortoise;
} else if (isHare) {
ctx.fillStyle = COLORS.hare;
} else {
ctx.fillStyle = '#e8eef3';
}
ctx.fill();
ctx.strokeStyle = '#adb1b8'; ctx.lineWidth = 1;
ctx.stroke();
if (r > 8) {
ctx.fillStyle = (isTort || isHare) ? '#fff' : '#555';
ctx.font = `${Math.max(9, Math.min(11, r))}px ui-monospace, monospace`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(v.toString(), p.x, p.y);
}
}
// Highlight H and G
const pH = pos(Number(H));
ctx.beginPath(); ctx.arc(pH.x, pH.y, r + 3, 0, 2 * Math.PI);
ctx.strokeStyle = '#f4a261'; ctx.lineWidth = 2; ctx.stroke();
// Legend
ctx.font = '11px system-ui'; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
const items = [
{ c: COLORS.tortoise, label: 'Tortoise' },
{ c: COLORS.hare, label: 'Hare' },
{ c: '#6a0dad', label: 'Collision' },
{ c: '#f4a261', label: fmt('h=%H% (target)', { H: H }) },
];
let lx = 8;
items.forEach(it => {
ctx.fillStyle = it.c;
ctx.fillRect(lx, H_px - 14, 10, 10);
ctx.fillStyle = '#333';
ctx.fillText(it.label, lx + 13, H_px - 5);
lx += ctx.measureText(it.label).width + 28;
});
}
// โโ Bootstrap โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.getElementById('selP').addEventListener('change', init);
document.getElementById('selH').addEventListener('change', reset);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('btnStep').addEventListener('click', step);
document.getElementById('btnRun').addEventListener('click', runAll);
init();
Comments
Loading comments...