Here is a small curve over a finite field. There is a base point G, and a ruler-and-flip rule for "adding" points: draw the line through two points, find where it meets the curve a third time, and reflect that over the horizontal axis. To add a point to itself, use the tangent line.
<p class="hint">On an elliptic curve you can <b>"add"</b> two points with a ruler:
draw the line through them, find the third place it hits the curve, and flip it
over the x-axis. Press <b>Add G</b> to keep adding the same base point G to itself
and watch the chord-and-tangent rule jump around. Going forward is easy — but given
only the final point, recovering <i>how many times</i> you added is the hard part.</p>
<canvas id="cv" width="360" height="300"></canvas>
<div class="readout" id="readout">P = G</div>
<div class="btns">
<button id="add" type="button">Add G (P → P + G)</button>
<button id="reset" type="button" class="ghost">Reset to G</button>
</div>
<p class="dlp">Secret multiplier so far: <b><span id="k">1</span></b>. A real curve uses a 256-bit secret — reversing it is the
\ <b>elliptic-curve discrete-log problem</b>, believed to need astronomically many steps.</p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { background: #f7f9fb; border: 1px solid #cdd9e3; border-radius: 8px; display: block; max-width: 100%; }
.readout { font: 600 15px ui-monospace, monospace; color: #1d3557; margin: .6rem 0; min-height: 1.3em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.dlp { font-size: .88rem; color: #555; line-height: 1.45; margin: .7rem 0 0; }
.dlp b span { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// We work over a small finite field GF(17) so the arithmetic is exact, but we
// DRAW the points on the smooth real curve y^2 = x^3 + 2x + 2 for intuition.
// Group law (chord-and-tangent):
// - line through P and Q meets the curve in a third point; reflect it over
// - to add P to itself, use the tangent line (slope from calculus).
// Forward (k -> kG) is a handful of additions. Backward (kG -> k) is the
// elliptic-curve discrete-log problem, the basis of ECC (Koblitz & Miller, 1985).
const P = 17, A = 2, B = 2; // curve over GF(17)
const G = { x: 5, y: 1 }; // base point, generates 19 points
const mod = (n, m) => ((n % m) + m) % m;
function inv(a, p){ a = mod(a, p); for (let i = 1; i < p; i++) if (mod(a*i, p) === 1) return i; return 0; }
function add(p1, p2){
if (!p1) return p2; if (!p2) return p1;
if (p1.x === p2.x && mod(p1.y + p2.y, P) === 0) return null; // P + (-P) = O
let s;
if (p1.x === p2.x && p1.y === p2.y) s = mod((3*p1.x*p1.x + A) * inv(2*p1.y, P), P);
else s = mod((p2.y - p1.y) * inv(p2.x - p1.x, P), P);
const x = mod(s*s - p1.x - p2.x, P);
const y = mod(s*(p1.x - x) - p1.y, P);
return { x, y };
}
let cur = { ...G }, k = 1, prev = null;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
// Map field coords (0..16) onto the canvas with a margin.
const M = 28, W = cv.width - 2*M, H = cv.height - 2*M;
const sx = v => M + (v / 16) * W;
const sy = v => cv.height - M - (v / 16) * H;
function drawCurve(){
ctx.clearRect(0, 0, cv.width, cv.height);
// axes
ctx.strokeStyle = '#d3dce4'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(M, sy(8)); ctx.lineTo(cv.width - M, sy(8)); ctx.stroke();
// every point of the group, faint
const all = [];
let t = { ...G };
for (let i = 0; i < 25 && t; i++){ all.push(t); t = add(t, G); }
ctx.fillStyle = '#9fb2c8';
for (const p of all){ ctx.beginPath(); ctx.arc(sx(p.x), sy(p.y), 3, 0, 7); ctx.fill(); }
// base point G
ctx.fillStyle = '#1d3557';
ctx.beginPath(); ctx.arc(sx(G.x), sy(G.y), 5, 0, 7); ctx.fill();
ctx.fillText('G', sx(G.x) + 7, sy(G.y) - 6);
// current point kG
ctx.fillStyle = '#e63946';
ctx.beginPath(); ctx.arc(sx(cur.x), sy(cur.y), 6, 0, 7); ctx.fill();
ctx.font = '600 12px ui-monospace, monospace'; ctx.fillStyle = '#c92f3c';
ctx.fillText(k + 'G', sx(cur.x) + 8, sy(cur.y) - 6);
// connecting hop from previous point (the "ruler" idea)
if (prev){
ctx.strokeStyle = '#e63946'; ctx.setLineDash([4, 3]); ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(sx(prev.x), sy(prev.y)); ctx.lineTo(sx(cur.x), sy(cur.y)); ctx.stroke();
ctx.setLineDash([]);
}
}
function readout(){
document.getElementById('readout').textContent =
cur ? (fmt('%k%G = (%x%, %y%) over GF(17)', { k: k, x: cur.x, y: cur.y })) : (fmt('%k%G = O (point at infinity)', { k: k }));
document.getElementById('k').textContent = k;
}
function step(){
prev = cur;
cur = add(cur, G); k++;
if (!cur){ // wrapped past the whole group — restart the cycle
cur = { ...G }; k = 1; prev = null;
}
drawCurve(); readout();
console.log(k + 'G = ' + (cur ? '(' + cur.x + ',' + cur.y + ')' : 'O'));
}
function reset(){ cur = { ...G }; k = 1; prev = null; drawCurve(); readout(); }
document.getElementById('add').addEventListener('click', step);
document.getElementById('reset').addEventListener('click', reset);
ctx.font = '600 12px ui-monospace, monospace';
reset();
<p class="hint">On an elliptic curve you can <b>"add"</b> two points with a ruler:
draw the line through them, find the third place it hits the curve, and flip it
over the x-axis. Press <b>Add G</b> to keep adding the same base point G to itself
and watch the chord-and-tangent rule jump around. Going forward is easy — but given
only the final point, recovering <i>how many times</i> you added is the hard part.</p>
<canvas id="cv" width="360" height="300"></canvas>
<div class="readout" id="readout">P = G</div>
<div class="btns">
<button id="add" type="button">Add G (P → P + G)</button>
<button id="reset" type="button" class="ghost">Reset to G</button>
</div>
<p class="dlp">Secret multiplier so far: <b><span id="k">1</span></b>. A real curve uses a 256-bit secret — reversing it is the
\ <b>elliptic-curve discrete-log problem</b>, believed to need astronomically many steps.</p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { background: #f7f9fb; border: 1px solid #cdd9e3; border-radius: 8px; display: block; max-width: 100%; }
.readout { font: 600 15px ui-monospace, monospace; color: #1d3557; margin: .6rem 0; min-height: 1.3em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.dlp { font-size: .88rem; color: #555; line-height: 1.45; margin: .7rem 0 0; }
.dlp b span { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// We work over a small finite field GF(17) so the arithmetic is exact, but we
// DRAW the points on the smooth real curve y^2 = x^3 + 2x + 2 for intuition.
// Group law (chord-and-tangent):
// - line through P and Q meets the curve in a third point; reflect it over
// - to add P to itself, use the tangent line (slope from calculus).
// Forward (k -> kG) is a handful of additions. Backward (kG -> k) is the
// elliptic-curve discrete-log problem, the basis of ECC (Koblitz & Miller, 1985).
const P = 17, A = 2, B = 2; // curve over GF(17)
const G = { x: 5, y: 1 }; // base point, generates 19 points
const mod = (n, m) => ((n % m) + m) % m;
function inv(a, p){ a = mod(a, p); for (let i = 1; i < p; i++) if (mod(a*i, p) === 1) return i; return 0; }
function add(p1, p2){
if (!p1) return p2; if (!p2) return p1;
if (p1.x === p2.x && mod(p1.y + p2.y, P) === 0) return null; // P + (-P) = O
let s;
if (p1.x === p2.x && p1.y === p2.y) s = mod((3*p1.x*p1.x + A) * inv(2*p1.y, P), P);
else s = mod((p2.y - p1.y) * inv(p2.x - p1.x, P), P);
const x = mod(s*s - p1.x - p2.x, P);
const y = mod(s*(p1.x - x) - p1.y, P);
return { x, y };
}
let cur = { ...G }, k = 1, prev = null;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
// Map field coords (0..16) onto the canvas with a margin.
const M = 28, W = cv.width - 2*M, H = cv.height - 2*M;
const sx = v => M + (v / 16) * W;
const sy = v => cv.height - M - (v / 16) * H;
function drawCurve(){
ctx.clearRect(0, 0, cv.width, cv.height);
// axes
ctx.strokeStyle = '#d3dce4'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(M, sy(8)); ctx.lineTo(cv.width - M, sy(8)); ctx.stroke();
// every point of the group, faint
const all = [];
let t = { ...G };
for (let i = 0; i < 25 && t; i++){ all.push(t); t = add(t, G); }
ctx.fillStyle = '#9fb2c8';
for (const p of all){ ctx.beginPath(); ctx.arc(sx(p.x), sy(p.y), 3, 0, 7); ctx.fill(); }
// base point G
ctx.fillStyle = '#1d3557';
ctx.beginPath(); ctx.arc(sx(G.x), sy(G.y), 5, 0, 7); ctx.fill();
ctx.fillText('G', sx(G.x) + 7, sy(G.y) - 6);
// current point kG
ctx.fillStyle = '#e63946';
ctx.beginPath(); ctx.arc(sx(cur.x), sy(cur.y), 6, 0, 7); ctx.fill();
ctx.font = '600 12px ui-monospace, monospace'; ctx.fillStyle = '#c92f3c';
ctx.fillText(k + 'G', sx(cur.x) + 8, sy(cur.y) - 6);
// connecting hop from previous point (the "ruler" idea)
if (prev){
ctx.strokeStyle = '#e63946'; ctx.setLineDash([4, 3]); ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(sx(prev.x), sy(prev.y)); ctx.lineTo(sx(cur.x), sy(cur.y)); ctx.stroke();
ctx.setLineDash([]);
}
}
function readout(){
document.getElementById('readout').textContent =
cur ? (fmt('%k%G = (%x%, %y%) over GF(17)', { k: k, x: cur.x, y: cur.y })) : (fmt('%k%G = O (point at infinity)', { k: k }));
document.getElementById('k').textContent = k;
}
function step(){
prev = cur;
cur = add(cur, G); k++;
if (!cur){ // wrapped past the whole group — restart the cycle
cur = { ...G }; k = 1; prev = null;
}
drawCurve(); readout();
console.log(k + 'G = ' + (cur ? '(' + cur.x + ',' + cur.y + ')' : 'O'));
}
function reset(){ cur = { ...G }; k = 1; prev = null; drawCurve(); readout(); }
document.getElementById('add').addEventListener('click', step);
document.getElementById('reset').addEventListener('click', reset);
ctx.font = '600 12px ui-monospace, monospace';
reset();
Press Add G to compute G, 2G, 3G, … one hop at a time. Going forward is effortless — a short chain of additions lands you on kG. Now imagine I hand you only the red point and ask: how many times did I add G? With a tiny curve you could count. With a real 256-bit curve there are more points than atoms in the visible universe, and counting is hopeless. That asymmetry — easy forward, brutal backward — is the whole game, just like the discrete logarithm.
Comments
Loading comments...