Choose a composite number and watch ECM hunt for a factor. The algorithm picks a random elliptic curve, does arithmetic modulo your number, and waits for a greatest common divisor to escape from 1. When the GCD hits a non-trivial value, it is a factor.
<p class="hint">Pick a composite number, then press <b>Run ECM</b>. The algorithm tries random elliptic curves over ℤ/nℤ until a point addition fails — and that failure leaks a factor via gcd. Each row is one curve attempt.</p>
<div class="controls">
<label>Number to factor:
<select id="nsel">
<option value="455839">455839 = 599 × 761</option>
<option value="1234567897">1234567897 = 17 × 73 × 994817</option>
<option value="15">15 = 3 × 5</option>
<option value="77">77 = 7 × 11</option>
<option value="3233">3233 = 53 × 61 (toy RSA)</option>
<option value="9073">9073 = 43 × 211</option>
</select>
</label>
<button id="run" type="button">Run ECM</button>
<button id="clr" type="button" class="ghost">Clear</button>
</div>
<div id="result" class="result"></div>
<table id="log">
<thead><tr><th>#</th><th>Curve (a)</th><th>Point</th><th>GCD found</th><th>Outcome</th></tr></thead>
<tbody></tbody>
</table>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .93rem; }
.hint { color: #444; margin: 0 0 .6rem; line-height: 1.45; font-size: .88rem; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; display: flex; gap: .35rem; align-items: center; }
select { font: inherit; padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; background: #fff; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-weight: 700; font-size: 1.05rem; min-height: 1.5em; margin-bottom: .4rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
table { width: 100%; border-collapse: collapse; font-size: .82rem; }
th { background: #e8eef3; padding: .3rem .45rem; text-align: left; border-bottom: 2px solid #cdd9e3; }
td { padding: .25rem .45rem; border-bottom: 1px solid #e2e7ec; }
tr.hit td { background: #d4f0dc; font-weight: 600; }
tr.miss td { color: #888; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lenstra ECM demo — pure vanilla JS, BigInt arithmetic.
// We work with BigInt throughout. The algorithm:
// 1. Pick random curve y^2 = x^3 + a*x + b (mod n) via random point (x0,y0,a).
// b is determined: b = y0^2 - x0^3 - a*x0 (mod n).
// 2. Compute k * P using the group law, where k = B1-smooth number (product of
// small prime powers fitting in a "stage 1" bound). Here we use k = lcm(1..B).
// 3. During each doubling/addition, the modular inverse may fail (gcd(denom, n) > 1).
// That gcd is a non-trivial factor of n — success!
// 4. If we finish stage 1 without failure, the curve is "unlucky" — try another.
const B = 30; // stage-1 bound (small for demo speed; real ECM uses B1~1e6)
function bigRand(max) {
// Random BigInt in [0, max)
const bits = max.toString(2).length;
let r;
do {
let v = 0n;
for (let i = 0; i < bits; i += 32)
v = (v << 32n) | BigInt(Math.floor(Math.random() * 0x100000000));
r = v & ((1n << BigInt(bits)) - 1n);
} while (r >= max);
return r;
}
function mod(a, n) { return ((a % n) + n) % n; }
// Extended GCD: returns [g, x, y] s.t. a*x + b*y = g
function egcd(a, b) {
if (b === 0n) return [a, 1n, 0n];
const [g, x1, y1] = egcd(b, mod(a, b));
return [g, y1, x1 - (a / b) * y1];
}
function gcd(a, b) { a = a < 0n ? -a : a; b = b < 0n ? -b : b; while (b) { [a, b] = [b, a % b]; } return a; }
// Modular inverse of a mod n. Returns null if gcd(a,n) > 1 (signals factor found).
function modinv(a, n) {
a = mod(a, n);
const [g, x] = egcd(a, n);
if (g !== 1n) return { factor: g }; // gcd leaked a factor!
return mod(x, n);
}
// Point on curve y^2 = x^3 + a*x + b (mod n). null = point at infinity.
function pointAdd(P, Q, a, n) {
if (P === null) return Q;
if (Q === null) return P;
let lam;
if (P[0] === Q[0]) {
if (P[1] !== Q[1]) return null; // P = -Q
// doubling: lam = (3x^2 + a) / (2y)
const num = mod(3n * P[0] * P[0] + a, n);
const den = mod(2n * P[1], n);
const inv = modinv(den, n);
if (inv && inv.factor) return inv; // factor found!
lam = mod(num * inv, n);
} else {
const num = mod(Q[1] - P[1], n);
const den = mod(Q[0] - P[0], n);
const inv = modinv(den, n);
if (inv && inv.factor) return inv;
lam = mod(num * inv, n);
}
const x = mod(lam * lam - P[0] - Q[0], n);
const y = mod(lam * (P[0] - x) - P[1], n);
return [x, y];
}
// Scalar multiplication k*P using double-and-add.
function scalarMul(k, P, a, n) {
let R = null, Q = P;
while (k > 0n) {
if (k & 1n) {
const res = pointAdd(R, Q, a, n);
if (res && res.factor) return res;
R = res;
}
const res2 = pointAdd(Q, Q, a, n);
if (res2 && res2.factor) return res2;
Q = res2;
k >>= 1n;
}
return R;
}
// Compute k = product of prime powers <= B (lcm(1..B) approximation via primes)
function stageK(B) {
// Sieve of Eratosthenes for primes <= B
const sieve = Array(B + 1).fill(true);
sieve[0] = sieve[1] = false;
for (let i = 2; i <= B; i++) if (sieve[i]) for (let j = 2*i; j <= B; j += i) sieve[j] = false;
let k = 1n;
for (let p = 2; p <= B; p++) {
if (!sieve[p]) continue;
let q = p;
while (q <= B) q *= p;
q /= p;
k *= BigInt(q);
}
return k;
}
const K = stageK(B);
// Run one ECM attempt on n. Returns {factor} or null.
function ecmOnce(n) {
const x0 = bigRand(n - 2n) + 1n;
const y0 = bigRand(n - 2n) + 1n;
const a = bigRand(n - 2n) + 1n;
// b = y0^2 - x0^3 - a*x0 (mod n) — we don't need b explicitly in computation
const P = [x0, y0];
const res = scalarMul(K, P, a, n);
if (res && res.factor) {
const f = res.factor;
if (f > 1n && f < n) return { factor: f, a, x0, y0 };
}
return { factor: null, a, x0, y0 };
}
const MAX_CURVES = 40;
let running = false;
document.getElementById('run').addEventListener('click', async () => {
if (running) return;
running = true;
const n = BigInt(document.getElementById('nsel').value);
const tbody = document.querySelector('#log tbody');
const resultEl = document.getElementById('result');
tbody.innerHTML = '';
resultEl.textContent = 'Trying curves…';
resultEl.className = 'result';
// Quick check: if n is prime, bail out
if (n < 4n) { resultEl.textContent = 'n must be composite.'; running = false; return; }
const trivial = gcd(n, 6n);
let earlyFactor = null;
if (trivial > 1n && trivial < n) earlyFactor = trivial;
if (earlyFactor) {
resultEl.textContent = fmt('Trivial factor: %earlyFactor% (gcd with 6)', { earlyFactor: earlyFactor });
resultEl.className = 'result ok';
running = false;
return;
}
let found = null;
for (let i = 1; i <= MAX_CURVES && !found; i++) {
// Yield to browser between iterations
await new Promise(r => setTimeout(r, 0));
const res = ecmOnce(n);
const tr = document.createElement('tr');
const g = res.factor;
if (g && g > 1n && g < n) {
found = g;
tr.className = 'hit';
tr.innerHTML = fmt('<td>%i%</td><td>%a%</td><td>(%x0%, %y0%)</td><td>%g%</td><td>✓ Factor found!</td>', { i: i, a: res.a, x0: res.x0, y0: res.y0, g: g });
} else {
tr.className = 'miss';
tr.innerHTML = fmt('<td>%i%</td><td>%a%</td><td>(%x0%, %y0%)</td><td>1 or n</td><td>No luck</td>', { i: i, a: res.a, x0: res.x0, y0: res.y0 });
}
tbody.appendChild(tr);
}
if (found) {
const other = n / found;
resultEl.textContent = `✓ ${n} = ${found} × ${other}`;
resultEl.className = 'result ok';
console.log(fmt('ECM factored %n% = %found% × %other%', { n: n, found: found, other: other }));
} else {
resultEl.textContent = fmt('No factor found in %MAX_CURVES% curves (try again).', { MAX_CURVES: MAX_CURVES });
resultEl.className = 'result bad';
}
running = false;
});
document.getElementById('clr').addEventListener('click', () => {
document.querySelector('#log tbody').innerHTML = '';
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
<p class="hint">Pick a composite number, then press <b>Run ECM</b>. The algorithm tries random elliptic curves over ℤ/nℤ until a point addition fails — and that failure leaks a factor via gcd. Each row is one curve attempt.</p>
<div class="controls">
<label>Number to factor:
<select id="nsel">
<option value="455839">455839 = 599 × 761</option>
<option value="1234567897">1234567897 = 17 × 73 × 994817</option>
<option value="15">15 = 3 × 5</option>
<option value="77">77 = 7 × 11</option>
<option value="3233">3233 = 53 × 61 (toy RSA)</option>
<option value="9073">9073 = 43 × 211</option>
</select>
</label>
<button id="run" type="button">Run ECM</button>
<button id="clr" type="button" class="ghost">Clear</button>
</div>
<div id="result" class="result"></div>
<table id="log">
<thead><tr><th>#</th><th>Curve (a)</th><th>Point</th><th>GCD found</th><th>Outcome</th></tr></thead>
<tbody></tbody>
</table>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: .93rem; }
.hint { color: #444; margin: 0 0 .6rem; line-height: 1.45; font-size: .88rem; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; display: flex; gap: .35rem; align-items: center; }
select { font: inherit; padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; background: #fff; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.result { font-weight: 700; font-size: 1.05rem; min-height: 1.5em; margin-bottom: .4rem; }
.result.ok { color: #0a7d33; }
.result.bad { color: #c92f3c; }
table { width: 100%; border-collapse: collapse; font-size: .82rem; }
th { background: #e8eef3; padding: .3rem .45rem; text-align: left; border-bottom: 2px solid #cdd9e3; }
td { padding: .25rem .45rem; border-bottom: 1px solid #e2e7ec; }
tr.hit td { background: #d4f0dc; font-weight: 600; }
tr.miss td { color: #888; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lenstra ECM demo — pure vanilla JS, BigInt arithmetic.
// We work with BigInt throughout. The algorithm:
// 1. Pick random curve y^2 = x^3 + a*x + b (mod n) via random point (x0,y0,a).
// b is determined: b = y0^2 - x0^3 - a*x0 (mod n).
// 2. Compute k * P using the group law, where k = B1-smooth number (product of
// small prime powers fitting in a "stage 1" bound). Here we use k = lcm(1..B).
// 3. During each doubling/addition, the modular inverse may fail (gcd(denom, n) > 1).
// That gcd is a non-trivial factor of n — success!
// 4. If we finish stage 1 without failure, the curve is "unlucky" — try another.
const B = 30; // stage-1 bound (small for demo speed; real ECM uses B1~1e6)
function bigRand(max) {
// Random BigInt in [0, max)
const bits = max.toString(2).length;
let r;
do {
let v = 0n;
for (let i = 0; i < bits; i += 32)
v = (v << 32n) | BigInt(Math.floor(Math.random() * 0x100000000));
r = v & ((1n << BigInt(bits)) - 1n);
} while (r >= max);
return r;
}
function mod(a, n) { return ((a % n) + n) % n; }
// Extended GCD: returns [g, x, y] s.t. a*x + b*y = g
function egcd(a, b) {
if (b === 0n) return [a, 1n, 0n];
const [g, x1, y1] = egcd(b, mod(a, b));
return [g, y1, x1 - (a / b) * y1];
}
function gcd(a, b) { a = a < 0n ? -a : a; b = b < 0n ? -b : b; while (b) { [a, b] = [b, a % b]; } return a; }
// Modular inverse of a mod n. Returns null if gcd(a,n) > 1 (signals factor found).
function modinv(a, n) {
a = mod(a, n);
const [g, x] = egcd(a, n);
if (g !== 1n) return { factor: g }; // gcd leaked a factor!
return mod(x, n);
}
// Point on curve y^2 = x^3 + a*x + b (mod n). null = point at infinity.
function pointAdd(P, Q, a, n) {
if (P === null) return Q;
if (Q === null) return P;
let lam;
if (P[0] === Q[0]) {
if (P[1] !== Q[1]) return null; // P = -Q
// doubling: lam = (3x^2 + a) / (2y)
const num = mod(3n * P[0] * P[0] + a, n);
const den = mod(2n * P[1], n);
const inv = modinv(den, n);
if (inv && inv.factor) return inv; // factor found!
lam = mod(num * inv, n);
} else {
const num = mod(Q[1] - P[1], n);
const den = mod(Q[0] - P[0], n);
const inv = modinv(den, n);
if (inv && inv.factor) return inv;
lam = mod(num * inv, n);
}
const x = mod(lam * lam - P[0] - Q[0], n);
const y = mod(lam * (P[0] - x) - P[1], n);
return [x, y];
}
// Scalar multiplication k*P using double-and-add.
function scalarMul(k, P, a, n) {
let R = null, Q = P;
while (k > 0n) {
if (k & 1n) {
const res = pointAdd(R, Q, a, n);
if (res && res.factor) return res;
R = res;
}
const res2 = pointAdd(Q, Q, a, n);
if (res2 && res2.factor) return res2;
Q = res2;
k >>= 1n;
}
return R;
}
// Compute k = product of prime powers <= B (lcm(1..B) approximation via primes)
function stageK(B) {
// Sieve of Eratosthenes for primes <= B
const sieve = Array(B + 1).fill(true);
sieve[0] = sieve[1] = false;
for (let i = 2; i <= B; i++) if (sieve[i]) for (let j = 2*i; j <= B; j += i) sieve[j] = false;
let k = 1n;
for (let p = 2; p <= B; p++) {
if (!sieve[p]) continue;
let q = p;
while (q <= B) q *= p;
q /= p;
k *= BigInt(q);
}
return k;
}
const K = stageK(B);
// Run one ECM attempt on n. Returns {factor} or null.
function ecmOnce(n) {
const x0 = bigRand(n - 2n) + 1n;
const y0 = bigRand(n - 2n) + 1n;
const a = bigRand(n - 2n) + 1n;
// b = y0^2 - x0^3 - a*x0 (mod n) — we don't need b explicitly in computation
const P = [x0, y0];
const res = scalarMul(K, P, a, n);
if (res && res.factor) {
const f = res.factor;
if (f > 1n && f < n) return { factor: f, a, x0, y0 };
}
return { factor: null, a, x0, y0 };
}
const MAX_CURVES = 40;
let running = false;
document.getElementById('run').addEventListener('click', async () => {
if (running) return;
running = true;
const n = BigInt(document.getElementById('nsel').value);
const tbody = document.querySelector('#log tbody');
const resultEl = document.getElementById('result');
tbody.innerHTML = '';
resultEl.textContent = 'Trying curves…';
resultEl.className = 'result';
// Quick check: if n is prime, bail out
if (n < 4n) { resultEl.textContent = 'n must be composite.'; running = false; return; }
const trivial = gcd(n, 6n);
let earlyFactor = null;
if (trivial > 1n && trivial < n) earlyFactor = trivial;
if (earlyFactor) {
resultEl.textContent = fmt('Trivial factor: %earlyFactor% (gcd with 6)', { earlyFactor: earlyFactor });
resultEl.className = 'result ok';
running = false;
return;
}
let found = null;
for (let i = 1; i <= MAX_CURVES && !found; i++) {
// Yield to browser between iterations
await new Promise(r => setTimeout(r, 0));
const res = ecmOnce(n);
const tr = document.createElement('tr');
const g = res.factor;
if (g && g > 1n && g < n) {
found = g;
tr.className = 'hit';
tr.innerHTML = fmt('<td>%i%</td><td>%a%</td><td>(%x0%, %y0%)</td><td>%g%</td><td>✓ Factor found!</td>', { i: i, a: res.a, x0: res.x0, y0: res.y0, g: g });
} else {
tr.className = 'miss';
tr.innerHTML = fmt('<td>%i%</td><td>%a%</td><td>(%x0%, %y0%)</td><td>1 or n</td><td>No luck</td>', { i: i, a: res.a, x0: res.x0, y0: res.y0 });
}
tbody.appendChild(tr);
}
if (found) {
const other = n / found;
resultEl.textContent = `✓ ${n} = ${found} × ${other}`;
resultEl.className = 'result ok';
console.log(fmt('ECM factored %n% = %found% × %other%', { n: n, found: found, other: other }));
} else {
resultEl.textContent = fmt('No factor found in %MAX_CURVES% curves (try again).', { MAX_CURVES: MAX_CURVES });
resultEl.className = 'result bad';
}
running = false;
});
document.getElementById('clr').addEventListener('click', () => {
document.querySelector('#log tbody').innerHTML = '';
document.getElementById('result').textContent = '';
document.getElementById('result').className = 'result';
});
Notice: the same number may require many curve attempts before one leaks a factor. Each new curve is an independent lottery ticket. The expected number of curves depends only on the size of the smallest prime factor — not on the size of the number itself. That is ECM's magic: small factors are found quickly no matter how large the surrounding number is.
Comments
Loading comments...