<p class="hint">
Each node is an elliptic curve labeled by its <em>j</em>-invariant (mod a small prime). Edges are degree-2 isogenies. Click <b>Random Walk</b> to simulate Alice choosing a secret path. Then click <b>Reveal Torsion Info</b> to see the auxiliary data SIKE would have published — and watch how it pins down the secret path.
</p>
<div id="graph-wrap">
<canvas id="c" width="480" height="300"></canvas>
</div>
<div class="controls">
<button id="btn-walk" type="button">Random Walk (Alice’s secret)</button>
<button id="btn-torsion" type="button" disabled>Reveal Torsion Info</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="info" class="info-box"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
#graph-wrap { background: #f4f7fa; border-radius: 10px; overflow: hidden; margin-bottom: .7rem; }
canvas { display: block; max-width: 100%; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 8px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.info-box { font-size: .88rem; line-height: 1.55; min-height: 2.4em;
background: #eef3f8; border-radius: 8px; padding: .5rem .8rem; color: #1d3557; }
.info-box.warn { background: #fff3cd; color: #7a5200; }
.info-box.danger { background: #fde8ea; color: #8b1e26; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy isogeny graph: 13 supersingular j-invariants mod 431 (a small prime where
// the supersingular locus has ~floor((431-1)/12)+1 = 36+ curves, but we use 13 hand-chosen nodes that form a connected 3-regular degree-2 isogeny graph).
// This is illustrative — real SIKE uses p~434 bits, ~2^130 nodes.
const NODES = [
{id:0, j:0, x:240, y:150},
{id:1, j:8, x:340, y:80},
{id:2, j:27, x:380, y:190},
{id:3, j:64, x:300, y:260},
{id:4, j:125, x:180, y:270},
{id:5, j:216, x:100, y:200},
{id:6, j:343, x:90, y:90},
{id:7, j:512, x:170, y:50},
{id:8, j:1, x:420, y:140},
{id:9, j:9, x:430, y:260},
{id:10, j:16, x:240, y:30},
{id:11, j:25, x:50, y:150},
{id:12, j:36, x:140, y:300},
];
// Edges: each node has exactly 3 degree-2 isogeny neighbours (3-regular graph)
const EDGES = [
[0,1],[0,6],[0,10],
[1,2],[1,7],
[2,3],[2,8],
[3,4],[3,9],
[4,5],[4,12],
[5,6],[5,11],
[6,7],
[7,10],
[8,9],[8,10],
[9,12],
[10,11],
[11,12],
];
// Build adjacency list
const ADJ = NODES.map(() => []);
EDGES.forEach(([a,b]) => { ADJ[a].push(b); ADJ[b].push(a); });
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// State
let secretPath = []; // Alice’s walk (list of node ids)
let torRevealed = false;
let attackStep = 0;
let animFrame = null;
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw edges
EDGES.forEach(([a, b]) => {
const A = NODES[a], B = NODES[b];
const onPath = isOnPath(a, b);
ctx.beginPath();
ctx.moveTo(A.x, A.y);
ctx.lineTo(B.x, B.y);
ctx.strokeStyle = onPath ? '#e63946' : '#b0bec5';
ctx.lineWidth = onPath ? 3 : 1.5;
ctx.stroke();
});
// Draw nodes
NODES.forEach(node => {
const inPath = secretPath.includes(node.id);
const isStart = secretPath[0] === node.id;
const isEnd = secretPath.length > 0 && secretPath[secretPath.length - 1] === node.id;
const pinned = torRevealed && isPinned(node.id);
// Circle
ctx.beginPath();
ctx.arc(node.x, node.y, 18, 0, 2 * Math.PI);
if (isStart) ctx.fillStyle = '#2a9d8f';
else if (isEnd) ctx.fillStyle = '#e9c46a';
else if (pinned) ctx.fillStyle = '#e63946';
else if (inPath) ctx.fillStyle = '#457b9d';
else ctx.fillStyle = '#dce8f0';
ctx.fill();
ctx.strokeStyle = inPath ? '#1d3557' : '#90a4ae';
ctx.lineWidth = inPath ? 2 : 1;
ctx.stroke();
// Label
ctx.fillStyle = inPath || pinned ? '#fff' : '#455a64';
ctx.font = '700 10px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('j=' + node.j, node.x, node.y);
});
// Legend
if (secretPath.length > 0) {
ctx.font = '11px system-ui';
ctx.fillStyle = '#555';
ctx.textAlign = 'left';
ctx.fillText('● Start ● End — Secret walk', 8, H - 10);
}
}
function isOnPath(a, b) {
for (let i = 0; i < secretPath.length - 1; i++) {
if ((secretPath[i] === a && secretPath[i+1] === b) ||
(secretPath[i] === b && secretPath[i+1] === a)) return true;
}
return false;
}
// Simulate torsion-point constraint: the auxiliary data narrows candidates
// to those nodes that "agree" with the published image. Here we fake this by flagging any node within 1 hop of the secret end that is NOT on the path.
function isPinned(id) {
if (!torRevealed || secretPath.length < 2) return false;
const endNode = secretPath[secretPath.length - 1];
return ADJ[endNode].includes(id) && !secretPath.includes(id);
}
function randomWalk(startId, steps) {
const path = [startId];
for (let i = 0; i < steps; i++) {
const cur = path[path.length - 1];
const prev = path.length > 1 ? path[path.length - 2] : -1;
const choices = ADJ[cur].filter(n => n !== prev);
path.push(choices[Math.floor(Math.random() * choices.length)]);
}
return path;
}
function setInfo(msg, cls = '') {
const el = document.getElementById('info');
el.innerHTML = msg;
el.className = 'info-box ' + cls;
}
document.getElementById('btn-walk').addEventListener('click', () => {
const start = Math.floor(Math.random() * NODES.length);
secretPath = randomWalk(start, 4 + Math.floor(Math.random() * 3));
torRevealed = false;
document.getElementById('btn-torsion').disabled = false;
draw();
setInfo(
fmt('<b>Alice’s secret walk:</b> %map%<br>The red path is her private key. She publishes only the <b>endpoint</b> (yellow) + torsion images.', { map: secretPath.map(i => 'j=' + NODES[i].j).join(' → ') })
);
});
document.getElementById('btn-torsion').addEventListener('click', () => {
if (secretPath.length < 2) return;
torRevealed = true;
draw();
const end = NODES[secretPath[secretPath.length - 1]];
const pinned = NODES.filter(n => isPinned(n.id));
setInfo(
fmt('<b>Torsion images published.</b> The endpoint is j=%j%. The auxiliary torsion data constrains the possible secret walks: the red-highlighted neighbours of the endpoint are now <em>ruled in</em> as structural witnesses. In real SIKE, Castryck & Decru used this to reconstruct Alice’s entire secret isogeny in polynomial time — <b>the graph became transparent.</b>', { j: end.j }),
'danger'
);
document.getElementById('btn-torsion').disabled = true;
});
document.getElementById('btn-reset').addEventListener('click', () => {
secretPath = [];
torRevealed = false;
document.getElementById('btn-torsion').disabled = true;
draw();
setInfo('Click <b>Random Walk</b> to simulate Alice choosing a secret path through the isogeny graph.');
});
draw();
setInfo('Click <b>Random Walk</b> to simulate Alice choosing a secret path through the isogeny graph.');
// Verify: check that the graph is connected and every node has degree >= 2
(function verify() {
const visited = new Set();
const q = [0];
while (q.length) {
const n = q.shift();
if (visited.has(n)) continue;
visited.add(n);
ADJ[n].forEach(m => q.push(m));
}
const allConnected = visited.size === NODES.length;
const minDeg = Math.min(...ADJ.map(a => a.length));
console.assert(allConnected, 'Graph must be connected');
console.assert(minDeg >= 2, 'Every node must have degree >= 2');
console.log(fmt('Graph OK — connected: %allConnected% | min degree: %minDeg%', { allConnected: allConnected, minDeg: minDeg }));
})();
<p class="hint">
Each node is an elliptic curve labeled by its <em>j</em>-invariant (mod a small prime). Edges are degree-2 isogenies. Click <b>Random Walk</b> to simulate Alice choosing a secret path. Then click <b>Reveal Torsion Info</b> to see the auxiliary data SIKE would have published — and watch how it pins down the secret path.
</p>
<div id="graph-wrap">
<canvas id="c" width="480" height="300"></canvas>
</div>
<div class="controls">
<button id="btn-walk" type="button">Random Walk (Alice’s secret)</button>
<button id="btn-torsion" type="button" disabled>Reveal Torsion Info</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="info" class="info-box"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
#graph-wrap { background: #f4f7fa; border-radius: 10px; overflow: hidden; margin-bottom: .7rem; }
canvas { display: block; max-width: 100%; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 8px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.info-box { font-size: .88rem; line-height: 1.55; min-height: 2.4em;
background: #eef3f8; border-radius: 8px; padding: .5rem .8rem; color: #1d3557; }
.info-box.warn { background: #fff3cd; color: #7a5200; }
.info-box.danger { background: #fde8ea; color: #8b1e26; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy isogeny graph: 13 supersingular j-invariants mod 431 (a small prime where
// the supersingular locus has ~floor((431-1)/12)+1 = 36+ curves, but we use 13 hand-chosen nodes that form a connected 3-regular degree-2 isogeny graph).
// This is illustrative — real SIKE uses p~434 bits, ~2^130 nodes.
const NODES = [
{id:0, j:0, x:240, y:150},
{id:1, j:8, x:340, y:80},
{id:2, j:27, x:380, y:190},
{id:3, j:64, x:300, y:260},
{id:4, j:125, x:180, y:270},
{id:5, j:216, x:100, y:200},
{id:6, j:343, x:90, y:90},
{id:7, j:512, x:170, y:50},
{id:8, j:1, x:420, y:140},
{id:9, j:9, x:430, y:260},
{id:10, j:16, x:240, y:30},
{id:11, j:25, x:50, y:150},
{id:12, j:36, x:140, y:300},
];
// Edges: each node has exactly 3 degree-2 isogeny neighbours (3-regular graph)
const EDGES = [
[0,1],[0,6],[0,10],
[1,2],[1,7],
[2,3],[2,8],
[3,4],[3,9],
[4,5],[4,12],
[5,6],[5,11],
[6,7],
[7,10],
[8,9],[8,10],
[9,12],
[10,11],
[11,12],
];
// Build adjacency list
const ADJ = NODES.map(() => []);
EDGES.forEach(([a,b]) => { ADJ[a].push(b); ADJ[b].push(a); });
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// State
let secretPath = []; // Alice’s walk (list of node ids)
let torRevealed = false;
let attackStep = 0;
let animFrame = null;
function draw() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw edges
EDGES.forEach(([a, b]) => {
const A = NODES[a], B = NODES[b];
const onPath = isOnPath(a, b);
ctx.beginPath();
ctx.moveTo(A.x, A.y);
ctx.lineTo(B.x, B.y);
ctx.strokeStyle = onPath ? '#e63946' : '#b0bec5';
ctx.lineWidth = onPath ? 3 : 1.5;
ctx.stroke();
});
// Draw nodes
NODES.forEach(node => {
const inPath = secretPath.includes(node.id);
const isStart = secretPath[0] === node.id;
const isEnd = secretPath.length > 0 && secretPath[secretPath.length - 1] === node.id;
const pinned = torRevealed && isPinned(node.id);
// Circle
ctx.beginPath();
ctx.arc(node.x, node.y, 18, 0, 2 * Math.PI);
if (isStart) ctx.fillStyle = '#2a9d8f';
else if (isEnd) ctx.fillStyle = '#e9c46a';
else if (pinned) ctx.fillStyle = '#e63946';
else if (inPath) ctx.fillStyle = '#457b9d';
else ctx.fillStyle = '#dce8f0';
ctx.fill();
ctx.strokeStyle = inPath ? '#1d3557' : '#90a4ae';
ctx.lineWidth = inPath ? 2 : 1;
ctx.stroke();
// Label
ctx.fillStyle = inPath || pinned ? '#fff' : '#455a64';
ctx.font = '700 10px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('j=' + node.j, node.x, node.y);
});
// Legend
if (secretPath.length > 0) {
ctx.font = '11px system-ui';
ctx.fillStyle = '#555';
ctx.textAlign = 'left';
ctx.fillText('● Start ● End — Secret walk', 8, H - 10);
}
}
function isOnPath(a, b) {
for (let i = 0; i < secretPath.length - 1; i++) {
if ((secretPath[i] === a && secretPath[i+1] === b) ||
(secretPath[i] === b && secretPath[i+1] === a)) return true;
}
return false;
}
// Simulate torsion-point constraint: the auxiliary data narrows candidates
// to those nodes that "agree" with the published image. Here we fake this by flagging any node within 1 hop of the secret end that is NOT on the path.
function isPinned(id) {
if (!torRevealed || secretPath.length < 2) return false;
const endNode = secretPath[secretPath.length - 1];
return ADJ[endNode].includes(id) && !secretPath.includes(id);
}
function randomWalk(startId, steps) {
const path = [startId];
for (let i = 0; i < steps; i++) {
const cur = path[path.length - 1];
const prev = path.length > 1 ? path[path.length - 2] : -1;
const choices = ADJ[cur].filter(n => n !== prev);
path.push(choices[Math.floor(Math.random() * choices.length)]);
}
return path;
}
function setInfo(msg, cls = '') {
const el = document.getElementById('info');
el.innerHTML = msg;
el.className = 'info-box ' + cls;
}
document.getElementById('btn-walk').addEventListener('click', () => {
const start = Math.floor(Math.random() * NODES.length);
secretPath = randomWalk(start, 4 + Math.floor(Math.random() * 3));
torRevealed = false;
document.getElementById('btn-torsion').disabled = false;
draw();
setInfo(
fmt('<b>Alice’s secret walk:</b> %map%<br>The red path is her private key. She publishes only the <b>endpoint</b> (yellow) + torsion images.', { map: secretPath.map(i => 'j=' + NODES[i].j).join(' → ') })
);
});
document.getElementById('btn-torsion').addEventListener('click', () => {
if (secretPath.length < 2) return;
torRevealed = true;
draw();
const end = NODES[secretPath[secretPath.length - 1]];
const pinned = NODES.filter(n => isPinned(n.id));
setInfo(
fmt('<b>Torsion images published.</b> The endpoint is j=%j%. The auxiliary torsion data constrains the possible secret walks: the red-highlighted neighbours of the endpoint are now <em>ruled in</em> as structural witnesses. In real SIKE, Castryck & Decru used this to reconstruct Alice’s entire secret isogeny in polynomial time — <b>the graph became transparent.</b>', { j: end.j }),
'danger'
);
document.getElementById('btn-torsion').disabled = true;
});
document.getElementById('btn-reset').addEventListener('click', () => {
secretPath = [];
torRevealed = false;
document.getElementById('btn-torsion').disabled = true;
draw();
setInfo('Click <b>Random Walk</b> to simulate Alice choosing a secret path through the isogeny graph.');
});
draw();
setInfo('Click <b>Random Walk</b> to simulate Alice choosing a secret path through the isogeny graph.');
// Verify: check that the graph is connected and every node has degree >= 2
(function verify() {
const visited = new Set();
const q = [0];
while (q.length) {
const n = q.shift();
if (visited.has(n)) continue;
visited.add(n);
ADJ[n].forEach(m => q.push(m));
}
const allConnected = visited.size === NODES.length;
const minDeg = Math.min(...ADJ.map(a => a.length));
console.assert(allConnected, 'Graph must be connected');
console.assert(minDeg >= 2, 'Every node must have degree >= 2');
console.log(fmt('Graph OK — connected: %allConnected% | min degree: %minDeg%', { allConnected: allConnected, minDeg: minDeg }));
})();
Comments
Loading comments...