Below are four senders (Alice, Bob, Carol, Dave) each sending a message to four recipients. The messages travel through three mix nodes. At each node the batch is collected, the order shuffled, and each message re-encrypted before forwarding.
<div class="layout">
<div class="col senders">
<div class="col-label">Senders</div>
<div class="node sender" id="s0">Alice</div>
<div class="node sender" id="s1">Bob</div>
<div class="node sender" id="s2">Carol</div>
<div class="node sender" id="s3">Dave</div>
</div>
<div class="col mix-col">
<div class="col-label">Mix 1</div>
<div class="mix-node" id="mix0">
<span class="mix-label">M1</span>
<span class="mix-sub">shuffle + re-encrypt</span>
</div>
</div>
<div class="col mix-col">
<div class="col-label">Mix 2</div>
<div class="mix-node" id="mix1">
<span class="mix-label">M2</span>
<span class="mix-sub">shuffle + re-encrypt</span>
</div>
</div>
<div class="col mix-col">
<div class="col-label">Mix 3</div>
<div class="mix-node" id="mix2">
<span class="mix-label">M3</span>
<span class="mix-sub">shuffle + re-encrypt</span>
</div>
</div>
<div class="col recipients">
<div class="col-label">Recipients</div>
<div class="node recipient" id="r0">W</div>
<div class="node recipient" id="r1">X</div>
<div class="node recipient" id="r2">Y</div>
<div class="node recipient" id="r3">Z</div>
</div>
</div>
<svg id="arrows" width="100%" height="100%" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;"></svg>
<div class="info-row">
<div id="info" class="info">Press the button to route messages through the mix.</div>
</div>
<div class="btn-row">
<button id="runBtn" type="button">Send through mix</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;color:#222;padding:.5rem;position:relative}
.layout{display:flex;align-items:center;justify-content:space-between;gap:4px;position:relative;height:260px;margin-bottom:.4rem}
.col{display:flex;flex-direction:column;align-items:center;gap:10px;z-index:1}
.col-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#666;margin-bottom:2px}
.senders,.recipients{gap:12px}
.node{width:62px;height:42px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.82rem;border:2px solid #cdd9e3;transition:transform .2s,box-shadow .2s}
.sender{background:#dbeafe;border-color:#93c5fd;color:#1e40af}
.recipient{background:#dcfce7;border-color:#86efac;color:#166534}
.mix-col{justify-content:center}
.mix-node{width:74px;height:130px;border-radius:14px;background:#f0f4ff;border:2px solid #a5b4fc;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px}
.mix-label{font-weight:800;font-size:1.1rem;color:#4338ca}
.mix-sub{font-size:.6rem;color:#6366f1;text-align:center;line-height:1.2;max-width:62px}
.info-row{margin:.3rem 0 .5rem}
.info{font-size:.88rem;color:#444;min-height:1.3em;line-height:1.4}
.btn-row{display:flex;gap:.5rem;flex-wrap:wrap}
button{font:600 13px system-ui,sans-serif;padding:.4rem .85rem;border:1px solid #1d3557;background:#1d3557;color:#fff;border-radius:8px;cursor:pointer}
button.ghost{background:#fff;color:#1d3557}
.arrow-path{fill:none;stroke-width:2;opacity:.8}
@keyframes dash{to{stroke-dashoffset:0}}
.animating .mix-node{box-shadow:0 0 0 3px #818cf8}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Mix network demo: 4 senders, 3 mix nodes, 4 recipients.
// Each mix node randomly shuffles the message batch before forwarding.
// The animation reveals the full path, but to trace one sender you must
// know the shuffle at every node — an outside observer of a single link sees nothing.
const COLORS = ['#e63946','#f4a261','#2a9d8f','#6a4c93'];
const SENDER_LABELS = ['Alice','Bob','Carol','Dave'];
const RECV_LABELS = ['W','X','Y','Z'];
// A fixed "intended destination" per sender (the actual routing).
// Chaum's idea: encrypt the destination with each mix's key, peel layers at each hop.
// Here we simulate the routing as a random permutation chosen fresh each run.
let animRunning = false;
function getCenter(el, ref) {
const er = el.getBoundingClientRect();
const rr = ref.getBoundingClientRect();
return { x: er.left - rr.left + er.width / 2, y: er.top - rr.top + er.height / 2 };
}
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// Draw a curved arrow between two points.
function makeArrow(svg, x1, y1, x2, y2, color, delay) {
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2 - 18;
const d = `M${x1},${y1} Q${mx},${my} ${x2},${y2}`;
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('d', d);
path.setAttribute('class','arrow-path');
path.setAttribute('stroke', color);
const len = path.getTotalLength ? path.getTotalLength() : 120;
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
path.style.animation = `dash 0.45s ease ${delay}s forwards`;
svg.appendChild(path);
return path;
}
async function wait(ms) { return new Promise(r => setTimeout(r, ms)); }
async function run() {
if (animRunning) return;
animRunning = true;
document.getElementById('runBtn').disabled = true;
// Clear previous arrows.
const svg = document.getElementById('arrows');
const layout = document.querySelector('.layout');
svg.innerHTML = '';
document.querySelectorAll('.mix-node').forEach(m => m.classList.remove('animating'));
document.getElementById('info').textContent = 'Collecting messages at Mix 1…';
// Three independent random permutations — one per mix node.
const perm1 = shuffle([0,1,2,3]);
const perm2 = shuffle([0,1,2,3]);
const perm3 = shuffle([0,1,2,3]);
// The composed permutation: sender i → perm3[perm2[perm1[i]]] → recipient index.
const finalDest = [0,1,2,3].map(i => perm3[perm2[perm1[i]]]);
const senderEls = [0,1,2,3].map(i => document.getElementById('s'+i));
const mixEls = [0,1,2].map(i => document.getElementById('mix'+i));
const recvEls = [0,1,2,3].map(i => document.getElementById('r'+i));
// Step 1: senders → Mix 1
mixEls[0].classList.add('animating');
for (let i = 0; i < 4; i++) {
const c1 = getCenter(senderEls[i], layout);
const c2 = getCenter(mixEls[0], layout);
makeArrow(svg, c1.x, c1.y, c2.x, c2.y, COLORS[i], i * 0.08);
}
await wait(700);
// Step 2: Mix 1 → Mix 2 (shuffled by perm1)
document.getElementById('info').textContent = 'Mix 1 shuffles and re-encrypts → forwarding to Mix 2…';
mixEls[0].classList.remove('animating');
mixEls[1].classList.add('animating');
for (let slot = 0; slot < 4; slot++) {
const origSender = perm1[slot]; // the sender whose message lands in slot `slot`
const c1 = getCenter(mixEls[0], layout);
const c2 = getCenter(mixEls[1], layout);
// Stagger slightly with vertical offset to show distinct arrows
makeArrow(svg, c1.x, c1.y + (slot-1.5)*14, c2.x, c2.y + (slot-1.5)*14, COLORS[origSender], slot * 0.09);
}
await wait(700);
// Step 3: Mix 2 → Mix 3 (shuffled by perm2)
document.getElementById('info').textContent = 'Mix 2 shuffles and re-encrypts → forwarding to Mix 3…';
mixEls[1].classList.remove('animating');
mixEls[2].classList.add('animating');
// After perm1, slots hold perm1[0..3] senders. perm2 reorders those slots.
const afterPerm2 = [0,1,2,3].map(slot2 => perm1[perm2[slot2]]);
for (let slot = 0; slot < 4; slot++) {
const origSender = afterPerm2[slot];
const c1 = getCenter(mixEls[1], layout);
const c2 = getCenter(mixEls[2], layout);
makeArrow(svg, c1.x, c1.y + (slot-1.5)*14, c2.x, c2.y + (slot-1.5)*14, COLORS[origSender], slot * 0.09);
}
await wait(700);
// Step 4: Mix 3 → recipients (shuffled by perm3)
document.getElementById('info').textContent = 'Mix 3 delivers — sender-to-recipient link revealed:';
mixEls[2].classList.remove('animating');
for (let i = 0; i < 4; i++) {
const dest = finalDest[i];
const c1 = getCenter(mixEls[2], layout);
const c2 = getCenter(recvEls[dest], layout);
makeArrow(svg, c1.x, c1.y + (i-1.5)*14, c2.x, c2.y, COLORS[i], i * 0.1);
}
await wait(700);
// Show the routing table.
const lines = finalDest.map((d,i) => `${SENDER_LABELS[i]}→${RECV_LABELS[d]}`).join(' ');
document.getElementById('info').textContent =
fmt('Delivered: %lines%. An observer at Mix 1 only sees 4 messages enter and 4 exit — shuffled, re-encrypted, untrackable.', { lines: lines });
// Log a verification check: composed permutation matches.
console.assert(
[0,1,2,3].every(i => perm3[perm2[perm1[i]]] === finalDest[i]),
'Composed permutation mismatch — bug!'
);
console.log('Routing:', SENDER_LABELS.map((s,i) => s+'→'+RECV_LABELS[finalDest[i]]).join(', '));
console.log('Perm1:', perm1, 'Perm2:', perm2, 'Perm3:', perm3);
animRunning = false;
document.getElementById('runBtn').disabled = false;
}
function reset() {
if (animRunning) return;
document.getElementById('arrows').innerHTML = '';
document.querySelectorAll('.mix-node').forEach(m => m.classList.remove('animating'));
document.getElementById('info').textContent = 'Press the button to route messages through the mix.';
}
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', reset);
<div class="layout">
<div class="col senders">
<div class="col-label">Senders</div>
<div class="node sender" id="s0">Alice</div>
<div class="node sender" id="s1">Bob</div>
<div class="node sender" id="s2">Carol</div>
<div class="node sender" id="s3">Dave</div>
</div>
<div class="col mix-col">
<div class="col-label">Mix 1</div>
<div class="mix-node" id="mix0">
<span class="mix-label">M1</span>
<span class="mix-sub">shuffle + re-encrypt</span>
</div>
</div>
<div class="col mix-col">
<div class="col-label">Mix 2</div>
<div class="mix-node" id="mix1">
<span class="mix-label">M2</span>
<span class="mix-sub">shuffle + re-encrypt</span>
</div>
</div>
<div class="col mix-col">
<div class="col-label">Mix 3</div>
<div class="mix-node" id="mix2">
<span class="mix-label">M3</span>
<span class="mix-sub">shuffle + re-encrypt</span>
</div>
</div>
<div class="col recipients">
<div class="col-label">Recipients</div>
<div class="node recipient" id="r0">W</div>
<div class="node recipient" id="r1">X</div>
<div class="node recipient" id="r2">Y</div>
<div class="node recipient" id="r3">Z</div>
</div>
</div>
<svg id="arrows" width="100%" height="100%" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;"></svg>
<div class="info-row">
<div id="info" class="info">Press the button to route messages through the mix.</div>
</div>
<div class="btn-row">
<button id="runBtn" type="button">Send through mix</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;color:#222;padding:.5rem;position:relative}
.layout{display:flex;align-items:center;justify-content:space-between;gap:4px;position:relative;height:260px;margin-bottom:.4rem}
.col{display:flex;flex-direction:column;align-items:center;gap:10px;z-index:1}
.col-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#666;margin-bottom:2px}
.senders,.recipients{gap:12px}
.node{width:62px;height:42px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.82rem;border:2px solid #cdd9e3;transition:transform .2s,box-shadow .2s}
.sender{background:#dbeafe;border-color:#93c5fd;color:#1e40af}
.recipient{background:#dcfce7;border-color:#86efac;color:#166534}
.mix-col{justify-content:center}
.mix-node{width:74px;height:130px;border-radius:14px;background:#f0f4ff;border:2px solid #a5b4fc;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px}
.mix-label{font-weight:800;font-size:1.1rem;color:#4338ca}
.mix-sub{font-size:.6rem;color:#6366f1;text-align:center;line-height:1.2;max-width:62px}
.info-row{margin:.3rem 0 .5rem}
.info{font-size:.88rem;color:#444;min-height:1.3em;line-height:1.4}
.btn-row{display:flex;gap:.5rem;flex-wrap:wrap}
button{font:600 13px system-ui,sans-serif;padding:.4rem .85rem;border:1px solid #1d3557;background:#1d3557;color:#fff;border-radius:8px;cursor:pointer}
button.ghost{background:#fff;color:#1d3557}
.arrow-path{fill:none;stroke-width:2;opacity:.8}
@keyframes dash{to{stroke-dashoffset:0}}
.animating .mix-node{box-shadow:0 0 0 3px #818cf8}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Mix network demo: 4 senders, 3 mix nodes, 4 recipients.
// Each mix node randomly shuffles the message batch before forwarding.
// The animation reveals the full path, but to trace one sender you must
// know the shuffle at every node — an outside observer of a single link sees nothing.
const COLORS = ['#e63946','#f4a261','#2a9d8f','#6a4c93'];
const SENDER_LABELS = ['Alice','Bob','Carol','Dave'];
const RECV_LABELS = ['W','X','Y','Z'];
// A fixed "intended destination" per sender (the actual routing).
// Chaum's idea: encrypt the destination with each mix's key, peel layers at each hop.
// Here we simulate the routing as a random permutation chosen fresh each run.
let animRunning = false;
function getCenter(el, ref) {
const er = el.getBoundingClientRect();
const rr = ref.getBoundingClientRect();
return { x: er.left - rr.left + er.width / 2, y: er.top - rr.top + er.height / 2 };
}
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// Draw a curved arrow between two points.
function makeArrow(svg, x1, y1, x2, y2, color, delay) {
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2 - 18;
const d = `M${x1},${y1} Q${mx},${my} ${x2},${y2}`;
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('d', d);
path.setAttribute('class','arrow-path');
path.setAttribute('stroke', color);
const len = path.getTotalLength ? path.getTotalLength() : 120;
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
path.style.animation = `dash 0.45s ease ${delay}s forwards`;
svg.appendChild(path);
return path;
}
async function wait(ms) { return new Promise(r => setTimeout(r, ms)); }
async function run() {
if (animRunning) return;
animRunning = true;
document.getElementById('runBtn').disabled = true;
// Clear previous arrows.
const svg = document.getElementById('arrows');
const layout = document.querySelector('.layout');
svg.innerHTML = '';
document.querySelectorAll('.mix-node').forEach(m => m.classList.remove('animating'));
document.getElementById('info').textContent = 'Collecting messages at Mix 1…';
// Three independent random permutations — one per mix node.
const perm1 = shuffle([0,1,2,3]);
const perm2 = shuffle([0,1,2,3]);
const perm3 = shuffle([0,1,2,3]);
// The composed permutation: sender i → perm3[perm2[perm1[i]]] → recipient index.
const finalDest = [0,1,2,3].map(i => perm3[perm2[perm1[i]]]);
const senderEls = [0,1,2,3].map(i => document.getElementById('s'+i));
const mixEls = [0,1,2].map(i => document.getElementById('mix'+i));
const recvEls = [0,1,2,3].map(i => document.getElementById('r'+i));
// Step 1: senders → Mix 1
mixEls[0].classList.add('animating');
for (let i = 0; i < 4; i++) {
const c1 = getCenter(senderEls[i], layout);
const c2 = getCenter(mixEls[0], layout);
makeArrow(svg, c1.x, c1.y, c2.x, c2.y, COLORS[i], i * 0.08);
}
await wait(700);
// Step 2: Mix 1 → Mix 2 (shuffled by perm1)
document.getElementById('info').textContent = 'Mix 1 shuffles and re-encrypts → forwarding to Mix 2…';
mixEls[0].classList.remove('animating');
mixEls[1].classList.add('animating');
for (let slot = 0; slot < 4; slot++) {
const origSender = perm1[slot]; // the sender whose message lands in slot `slot`
const c1 = getCenter(mixEls[0], layout);
const c2 = getCenter(mixEls[1], layout);
// Stagger slightly with vertical offset to show distinct arrows
makeArrow(svg, c1.x, c1.y + (slot-1.5)*14, c2.x, c2.y + (slot-1.5)*14, COLORS[origSender], slot * 0.09);
}
await wait(700);
// Step 3: Mix 2 → Mix 3 (shuffled by perm2)
document.getElementById('info').textContent = 'Mix 2 shuffles and re-encrypts → forwarding to Mix 3…';
mixEls[1].classList.remove('animating');
mixEls[2].classList.add('animating');
// After perm1, slots hold perm1[0..3] senders. perm2 reorders those slots.
const afterPerm2 = [0,1,2,3].map(slot2 => perm1[perm2[slot2]]);
for (let slot = 0; slot < 4; slot++) {
const origSender = afterPerm2[slot];
const c1 = getCenter(mixEls[1], layout);
const c2 = getCenter(mixEls[2], layout);
makeArrow(svg, c1.x, c1.y + (slot-1.5)*14, c2.x, c2.y + (slot-1.5)*14, COLORS[origSender], slot * 0.09);
}
await wait(700);
// Step 4: Mix 3 → recipients (shuffled by perm3)
document.getElementById('info').textContent = 'Mix 3 delivers — sender-to-recipient link revealed:';
mixEls[2].classList.remove('animating');
for (let i = 0; i < 4; i++) {
const dest = finalDest[i];
const c1 = getCenter(mixEls[2], layout);
const c2 = getCenter(recvEls[dest], layout);
makeArrow(svg, c1.x, c1.y + (i-1.5)*14, c2.x, c2.y, COLORS[i], i * 0.1);
}
await wait(700);
// Show the routing table.
const lines = finalDest.map((d,i) => `${SENDER_LABELS[i]}→${RECV_LABELS[d]}`).join(' ');
document.getElementById('info').textContent =
fmt('Delivered: %lines%. An observer at Mix 1 only sees 4 messages enter and 4 exit — shuffled, re-encrypted, untrackable.', { lines: lines });
// Log a verification check: composed permutation matches.
console.assert(
[0,1,2,3].every(i => perm3[perm2[perm1[i]]] === finalDest[i]),
'Composed permutation mismatch — bug!'
);
console.log('Routing:', SENDER_LABELS.map((s,i) => s+'→'+RECV_LABELS[finalDest[i]]).join(', '));
console.log('Perm1:', perm1, 'Perm2:', perm2, 'Perm3:', perm3);
animRunning = false;
document.getElementById('runBtn').disabled = false;
}
function reset() {
if (animRunning) return;
document.getElementById('arrows').innerHTML = '';
document.querySelectorAll('.mix-node').forEach(m => m.classList.remove('animating'));
document.getElementById('info').textContent = 'Press the button to route messages through the mix.';
}
document.getElementById('runBtn').addEventListener('click', run);
document.getElementById('resetBtn').addEventListener('click', reset);
Comments
Loading comments...