<div class="hint">A 16-node Kademlia network with <b>k-bucket routing tables</b> (k=2). Each node only knows O(log N) peers — one bucket per bit position. XOR is the distance metric. Pick a target and watch the lookup converge in ≤ 4 hops.</div>
<div class="controls">
<label>Target key: <select id="target"></select></label>
<label>Start: <select id="startNode"></select></label>
<button id="findBtn" type="button">Find</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div class="network-wrap">
<svg id="network" viewBox="0 0 340 280" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div id="log" class="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .85rem; }
select { font-size: .85rem; padding: .2rem .35rem; border: 1px solid #aaa; border-radius: 5px; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.network-wrap { width: 100%; overflow: hidden; }
svg { width: 100%; height: auto; display: block; }
.log { font-size: .82rem; line-height: 1.6; color: #333; margin-top: .5rem;
background: #f5f7fa; border-radius: 7px; padding: .5rem .7rem;
min-height: 2rem; max-height: 130px; overflow-y: auto; }
.log .hop { color: #1d3557; }
.log .found { color: #0a7d33; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kademlia XOR routing — 16 nodes (4-bit IDs), k-bucket routing tables (k=2).
// Each node stores only O(log N) peers: one bucket per bit position.
// Lookup: greedily move to the known node closest to the target under XOR.
const N = 16, K = 2;
const ids = Array.from({length: N}, (_, i) => i);
// Highest set bit of xor (bucket index)
function highestBit(xor) {
if (xor === 0) return -1;
let b = 0;
while ((1 << (b + 1)) <= xor) b++;
return b;
}
// Build routing table for one node: buckets[bitPos] = [up to K node IDs]
function buildRT(nodeId) {
const buckets = {};
ids.forEach(id => {
if (id === nodeId) return;
const bit = highestBit(id ^ nodeId);
if (!buckets[bit]) buckets[bit] = [];
if (buckets[bit].length < K) buckets[bit].push(id);
});
return buckets;
}
const routingTables = {};
ids.forEach(id => { routingTables[id] = buildRT(id); });
// Greedy XOR lookup using routing tables
function kademliaLookup(start, target) {
const path = [start];
const visited = new Set([start]);
let current = start;
while (current !== target) {
const known = Object.values(routingTables[current]).flat();
let best = -1, bestDist = current ^ target; // must strictly improve
known.forEach(id => {
if (!visited.has(id)) {
const d = id ^ target;
if (d < bestDist) { bestDist = d; best = id; }
}
});
if (best === -1) break; // stuck — target not reachable with strict improvement
visited.add(best);
path.push(best);
current = best;
}
return path;
}
// SVG layout — nodes on a circle
const CX = 170, CY = 140, R = 112;
function nodePos(id) {
const angle = (id / N) * 2 * Math.PI - Math.PI / 2;
return { x: CX + R * Math.cos(angle), y: CY + R * Math.sin(angle) };
}
const svgEl = document.getElementById('network');
const ns = 'http://www.w3.org/2000/svg';
function el(tag, attrs) {
const e = document.createElementNS(ns, tag);
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
return e;
}
function drawNetwork(path) {
while (svgEl.firstChild) svgEl.removeChild(svgEl.firstChild);
const target = parseInt(document.getElementById('target').value);
// Draw path edges
for (let i = 0; i + 1 < path.length; i++) {
const a = nodePos(path[i]), b = nodePos(path[i + 1]);
svgEl.appendChild(el('line', {
x1: a.x, y1: a.y, x2: b.x, y2: b.y,
stroke: '#e63946', 'stroke-width': '2', 'stroke-dasharray': '5 3'
}));
// Arrow tip
const dx = b.x - a.x, dy = b.y - a.y;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
const ux = dx / len, uy = dy / len;
const tx = b.x - ux * 15, ty = b.y - uy * 15;
const px = -uy * 5, py = ux * 5;
const pts = (tx + px) + ',' + (ty + py) + ' ' + b.x + ',' + b.y + ' ' + (tx - px) + ',' + (ty - py);
svgEl.appendChild(el('polygon', { points: pts, fill: '#e63946' }));
}
// Draw nodes
ids.forEach(id => {
const pos = nodePos(id);
const bin = id.toString(2).padStart(4, '0');
const inPath = path.includes(id);
const isStart = path.length > 0 && path[0] === id;
const isEnd = path.length > 0 && path[path.length - 1] === id;
const isTarget = id === target;
let fill = '#dce8f5', stroke = '#a8c4d8', sw = '1.3', textFill = '#1d3557', sd = '';
if (isEnd && isTarget) { fill = '#0a7d33'; stroke = '#065c25'; textFill = '#fff'; }
else if (isStart) { fill = '#1d3557'; stroke = '#0d2035'; textFill = '#fff'; }
else if (inPath) { fill = '#457b9d'; stroke = '#1d3557'; textFill = '#fff'; }
else if (isTarget) { fill = '#fff'; stroke = '#e63946'; sw = '2'; sd = '3 2'; }
const g = document.createElementNS(ns, 'g');
const c = el('circle', { cx: pos.x, cy: pos.y, r: 14, fill, stroke, 'stroke-width': sw });
if (sd) c.setAttribute('stroke-dasharray', sd);
g.appendChild(c);
g.appendChild(el('text', {
x: pos.x, y: pos.y, 'text-anchor': 'middle', 'dominant-baseline': 'central',
'font-size': '9', 'font-family': 'ui-monospace,monospace', 'font-weight': '700', fill: textFill
})).textContent = bin;
svgEl.appendChild(g);
});
}
let animTimer = null;
function runLookup() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
const target = parseInt(document.getElementById('target').value);
const start = parseInt(document.getElementById('startNode').value);
const path = kademliaLookup(start, target);
// Build log
const logEl = document.getElementById('log');
logEl.innerHTML = '';
path.forEach((id, i) => {
const bin = id.toString(2).padStart(4, '0');
const xorDist = id ^ target;
const xorBin = xorDist.toString(2).padStart(4, '0');
if (i === 0) {
logEl.innerHTML += fmt('<span class="hop">Start node %id% (%bin%): XOR to target = %xorBin% (%xorDist%)</span><br>', { id: id, bin: bin, xorBin: xorBin, xorDist: xorDist });
} else if (id === target) {
logEl.innerHTML += fmt('<span class="found">Hop %i%: arrived at target %id% (%bin%) — done in %i% hop%v%!</span>', { i: i, id: id, bin: bin, v: i !== 1 ? 's' : '' });
} else {
logEl.innerHTML += fmt('<span class="hop">Hop %i%: node %id% (%bin%): XOR to target = %xorBin% (%xorDist%)</span><br>', { i: i, id: id, bin: bin, xorBin: xorBin, xorDist: xorDist });
}
});
if (path[path.length - 1] !== target) {
logEl.innerHTML += '<span style="color:#c92f3c">Could not reach target from this start node with strict XOR improvement.</span>';
}
// Animate step by step
let step = 0;
function animate() {
drawNetwork(path.slice(0, step + 1));
if (step < path.length - 1) { step++; animTimer = setTimeout(animate, 650); }
}
animate();
}
// Populate selects
const targetSel = document.getElementById('target');
const startSel = document.getElementById('startNode');
ids.forEach(id => {
const bin = id.toString(2).padStart(4, '0');
[targetSel, startSel].forEach((sel, idx) => {
const o = document.createElement('option');
o.value = id;
o.textContent = id + ' (' + bin + ')';
sel.appendChild(o);
});
});
targetSel.value = 11;
startSel.value = 0;
document.getElementById('findBtn').addEventListener('click', runLookup);
document.getElementById('resetBtn').addEventListener('click', () => {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
drawNetwork([]);
document.getElementById('log').innerHTML = '';
});
drawNetwork([]);
<div class="hint">A 16-node Kademlia network with <b>k-bucket routing tables</b> (k=2). Each node only knows O(log N) peers — one bucket per bit position. XOR is the distance metric. Pick a target and watch the lookup converge in ≤ 4 hops.</div>
<div class="controls">
<label>Target key: <select id="target"></select></label>
<label>Start: <select id="startNode"></select></label>
<button id="findBtn" type="button">Find</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div class="network-wrap">
<svg id="network" viewBox="0 0 340 280" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div id="log" class="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .85rem; }
select { font-size: .85rem; padding: .2rem .35rem; border: 1px solid #aaa; border-radius: 5px; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.network-wrap { width: 100%; overflow: hidden; }
svg { width: 100%; height: auto; display: block; }
.log { font-size: .82rem; line-height: 1.6; color: #333; margin-top: .5rem;
background: #f5f7fa; border-radius: 7px; padding: .5rem .7rem;
min-height: 2rem; max-height: 130px; overflow-y: auto; }
.log .hop { color: #1d3557; }
.log .found { color: #0a7d33; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kademlia XOR routing — 16 nodes (4-bit IDs), k-bucket routing tables (k=2).
// Each node stores only O(log N) peers: one bucket per bit position.
// Lookup: greedily move to the known node closest to the target under XOR.
const N = 16, K = 2;
const ids = Array.from({length: N}, (_, i) => i);
// Highest set bit of xor (bucket index)
function highestBit(xor) {
if (xor === 0) return -1;
let b = 0;
while ((1 << (b + 1)) <= xor) b++;
return b;
}
// Build routing table for one node: buckets[bitPos] = [up to K node IDs]
function buildRT(nodeId) {
const buckets = {};
ids.forEach(id => {
if (id === nodeId) return;
const bit = highestBit(id ^ nodeId);
if (!buckets[bit]) buckets[bit] = [];
if (buckets[bit].length < K) buckets[bit].push(id);
});
return buckets;
}
const routingTables = {};
ids.forEach(id => { routingTables[id] = buildRT(id); });
// Greedy XOR lookup using routing tables
function kademliaLookup(start, target) {
const path = [start];
const visited = new Set([start]);
let current = start;
while (current !== target) {
const known = Object.values(routingTables[current]).flat();
let best = -1, bestDist = current ^ target; // must strictly improve
known.forEach(id => {
if (!visited.has(id)) {
const d = id ^ target;
if (d < bestDist) { bestDist = d; best = id; }
}
});
if (best === -1) break; // stuck — target not reachable with strict improvement
visited.add(best);
path.push(best);
current = best;
}
return path;
}
// SVG layout — nodes on a circle
const CX = 170, CY = 140, R = 112;
function nodePos(id) {
const angle = (id / N) * 2 * Math.PI - Math.PI / 2;
return { x: CX + R * Math.cos(angle), y: CY + R * Math.sin(angle) };
}
const svgEl = document.getElementById('network');
const ns = 'http://www.w3.org/2000/svg';
function el(tag, attrs) {
const e = document.createElementNS(ns, tag);
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
return e;
}
function drawNetwork(path) {
while (svgEl.firstChild) svgEl.removeChild(svgEl.firstChild);
const target = parseInt(document.getElementById('target').value);
// Draw path edges
for (let i = 0; i + 1 < path.length; i++) {
const a = nodePos(path[i]), b = nodePos(path[i + 1]);
svgEl.appendChild(el('line', {
x1: a.x, y1: a.y, x2: b.x, y2: b.y,
stroke: '#e63946', 'stroke-width': '2', 'stroke-dasharray': '5 3'
}));
// Arrow tip
const dx = b.x - a.x, dy = b.y - a.y;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
const ux = dx / len, uy = dy / len;
const tx = b.x - ux * 15, ty = b.y - uy * 15;
const px = -uy * 5, py = ux * 5;
const pts = (tx + px) + ',' + (ty + py) + ' ' + b.x + ',' + b.y + ' ' + (tx - px) + ',' + (ty - py);
svgEl.appendChild(el('polygon', { points: pts, fill: '#e63946' }));
}
// Draw nodes
ids.forEach(id => {
const pos = nodePos(id);
const bin = id.toString(2).padStart(4, '0');
const inPath = path.includes(id);
const isStart = path.length > 0 && path[0] === id;
const isEnd = path.length > 0 && path[path.length - 1] === id;
const isTarget = id === target;
let fill = '#dce8f5', stroke = '#a8c4d8', sw = '1.3', textFill = '#1d3557', sd = '';
if (isEnd && isTarget) { fill = '#0a7d33'; stroke = '#065c25'; textFill = '#fff'; }
else if (isStart) { fill = '#1d3557'; stroke = '#0d2035'; textFill = '#fff'; }
else if (inPath) { fill = '#457b9d'; stroke = '#1d3557'; textFill = '#fff'; }
else if (isTarget) { fill = '#fff'; stroke = '#e63946'; sw = '2'; sd = '3 2'; }
const g = document.createElementNS(ns, 'g');
const c = el('circle', { cx: pos.x, cy: pos.y, r: 14, fill, stroke, 'stroke-width': sw });
if (sd) c.setAttribute('stroke-dasharray', sd);
g.appendChild(c);
g.appendChild(el('text', {
x: pos.x, y: pos.y, 'text-anchor': 'middle', 'dominant-baseline': 'central',
'font-size': '9', 'font-family': 'ui-monospace,monospace', 'font-weight': '700', fill: textFill
})).textContent = bin;
svgEl.appendChild(g);
});
}
let animTimer = null;
function runLookup() {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
const target = parseInt(document.getElementById('target').value);
const start = parseInt(document.getElementById('startNode').value);
const path = kademliaLookup(start, target);
// Build log
const logEl = document.getElementById('log');
logEl.innerHTML = '';
path.forEach((id, i) => {
const bin = id.toString(2).padStart(4, '0');
const xorDist = id ^ target;
const xorBin = xorDist.toString(2).padStart(4, '0');
if (i === 0) {
logEl.innerHTML += fmt('<span class="hop">Start node %id% (%bin%): XOR to target = %xorBin% (%xorDist%)</span><br>', { id: id, bin: bin, xorBin: xorBin, xorDist: xorDist });
} else if (id === target) {
logEl.innerHTML += fmt('<span class="found">Hop %i%: arrived at target %id% (%bin%) — done in %i% hop%v%!</span>', { i: i, id: id, bin: bin, v: i !== 1 ? 's' : '' });
} else {
logEl.innerHTML += fmt('<span class="hop">Hop %i%: node %id% (%bin%): XOR to target = %xorBin% (%xorDist%)</span><br>', { i: i, id: id, bin: bin, xorBin: xorBin, xorDist: xorDist });
}
});
if (path[path.length - 1] !== target) {
logEl.innerHTML += '<span style="color:#c92f3c">Could not reach target from this start node with strict XOR improvement.</span>';
}
// Animate step by step
let step = 0;
function animate() {
drawNetwork(path.slice(0, step + 1));
if (step < path.length - 1) { step++; animTimer = setTimeout(animate, 650); }
}
animate();
}
// Populate selects
const targetSel = document.getElementById('target');
const startSel = document.getElementById('startNode');
ids.forEach(id => {
const bin = id.toString(2).padStart(4, '0');
[targetSel, startSel].forEach((sel, idx) => {
const o = document.createElement('option');
o.value = id;
o.textContent = id + ' (' + bin + ')';
sel.appendChild(o);
});
});
targetSel.value = 11;
startSel.value = 0;
document.getElementById('findBtn').addEventListener('click', runLookup);
document.getElementById('resetBtn').addEventListener('click', () => {
if (animTimer) { clearTimeout(animTimer); animTimer = null; }
drawNetwork([]);
document.getElementById('log').innerHTML = '';
});
drawNetwork([]);
Notice the pattern: the first hop fixes the most-significant bit of the XOR difference, the second fixes the next bit, and so on. Each step peels off one binary digit of uncertainty. With N = 24 = 16 nodes, 4 hops always suffice — and with a billion nodes (230), only 30 hops would be needed.
Comments
Loading comments...