This demo simulates the core of a VDF based on repeated squaring: starting from an input x, compute x2TmodN by squaring T times in sequence. Each step depends on the previous — no parallelism can help.
<div class="vdf-wrap">
<div class="panel">
<div class="row-group">
<label>Input x <span class="hint-small">(1–99)</span></label>
<input id="inp-x" type="number" min="1" max="99" value="7" />
</div>
<div class="row-group">
<label>Delay T <span class="hint-small">(steps)</span></label>
<input id="inp-t" type="range" min="4" max="20" value="10" />
<span id="t-val">10</span>
</div>
<div class="row-group">
<label>Modulus N</label>
<span id="mod-n" class="mono">3233</span>
<span class="hint-small">(= 61 × 53)</span>
</div>
<div class="btns">
<button id="btn-run" type="button">▶ Compute VDF</button>
<button id="btn-verify" type="button" disabled>✓ Verify</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="chain-wrap">
<div id="chain" class="chain"></div>
</div>
<div class="result-panel" id="result-panel" style="display:none">
<div class="result-row"><span class="lbl">Output y =</span><span id="out-y" class="mono big"></span></div>
<div class="result-row"><span class="lbl">Proof π =</span><span id="out-pi" class="mono"></span></div>
<div class="timing">
<span>Compute: <b id="t-compute">—</b></span>
<span>Verify: <b id="t-verify">—</b></span>
</div>
<div id="verdict" class="verdict"></div>
</div>
<p class="footnote">
Demo uses N = 3233 (= 61 × 53) — tiny for pedagogy. Real VDFs use 2048-bit RSA groups where knowing the factorization is impossible without the trapdoor.
</p>
</div>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; color: #1a1a2e; background: #f8f9fc; padding: .6rem; }
.vdf-wrap { display: flex; flex-direction: column; gap: .8rem; }
.panel { background: #fff; border: 1px solid #dde3ec; border-radius: 10px; padding: .9rem 1rem; display: flex; flex-direction: column; gap: .55rem; }
.row-group { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
label { font-weight: 600; font-size: .85rem; min-width: 6rem; }
.hint-small { font-size: .75rem; color: #888; font-weight: 400; }
input[type=number] { width: 4rem; padding: .25rem .4rem; border: 1px solid #c5ccd8; border-radius: 6px; font: inherit; }
input[type=range] { width: 120px; accent-color: #2563eb; }
.mono { font-family: ui-monospace, monospace; font-size: .9rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 .82rem system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer; border: 1.5px solid #2563eb; background: #2563eb; color: #fff; transition: opacity .15s; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #2563eb; }
.chain-wrap { overflow-x: auto; padding-bottom: .2rem; }
.chain { display: flex; align-items: center; gap: 0; min-height: 2.6rem; }
.step { display: flex; align-items: center; }
.box { background: #e0e7ff; border: 1.5px solid #6366f1; border-radius: 6px; padding: .25rem .45rem;
font: 600 .78rem ui-monospace, monospace; color: #3730a3; white-space: nowrap;
opacity: 0; transform: scale(.7); transition: opacity .25s, transform .25s; }
.box.show { opacity: 1; transform: scale(1); }
.box.current { background: #fef08a; border-color: #ca8a04; color: #713f12; }
.box.done { background: #dcfce7; border-color: #16a34a; color: #14532d; }
.arrow { font-size: .8rem; color: #94a3b8; padding: 0 3px; }
.result-panel { background: #fff; border: 1px solid #dde3ec; border-radius: 10px; padding: .9rem 1rem; display: flex; flex-direction: column; gap: .45rem; }
.result-row { display: flex; align-items: baseline; gap: .5rem; }
.lbl { font-size: .8rem; font-weight: 600; color: #555; min-width: 6rem; }
.big { font-size: 1.2rem; color: #1e40af; }
.timing { display: flex; gap: 1.2rem; font-size: .82rem; color: #555; }
.verdict { font-size: .95rem; font-weight: 700; padding: .3rem .5rem; border-radius: 6px; }
.verdict.ok { background: #dcfce7; color: #14532d; }
.verdict.fail { background: #fee2e2; color: #991b1b; }
.footnote { font-size: .75rem; color: #888; line-height: 1.5; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VDF demo: repeated squaring mod N (pedagogy — N=3233=61×53, tiny but illustrative).
// Compute: y = x^(2^T) mod N — T sequential squarings, no parallelism possible.
// Proof: π = x^(2^floor(T/2)) mod N (the midpoint value, a simplified Pietrzak commit).
// Verify: check π^(2^ceil(T/2)) ≡ y (mod N) — half the squarings, proves correctness.
const N = 3233; // 61 × 53
let state = null; // { y, pi, T, x }
const $ = id => document.getElementById(id);
const tVal = $('t-val');
$('inp-t').addEventListener('input', e => { tVal.textContent = e.target.value; });
function modpow(base, exp, mod) {
// Iterative square-and-multiply for BigInt
let result = 1n;
base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp >>= 1n;
base = base * base % mod;
}
return result;
}
function computeVDF(x, T, N) {
// Sequential squarings: x, x^2, x^4, ..., x^(2^T)
const steps = [BigInt(x) % BigInt(N)];
for (let i = 0; i < T; i++) {
steps.push(steps[steps.length - 1] ** 2n % BigInt(N));
}
const y = steps[T];
// Simplified proof: midpoint commitment π = x^(2^floor(T/2))
const pi = steps[Math.floor(T / 2)];
return { y, pi, steps };
}
function verifyVDF(pi, y, T, N) {
// Verify: π^(2^ceil(T/2)) mod N should equal y
const halvings = BigInt(Math.ceil(T / 2));
const exp = 1n << halvings; // 2^ceil(T/2)
const check = modpow(pi, exp, BigInt(N));
return check === y;
}
async function runVDF() {
const x = parseInt($('inp-x').value) || 7;
const T = parseInt($('inp-t').value) || 10;
$('btn-run').disabled = true;
$('btn-verify').disabled = true;
$('result-panel').style.display = 'none';
$('verdict').textContent = '';
$('verdict').className = 'verdict';
const chain = $('chain');
chain.innerHTML = '';
const t0 = performance.now();
const { y, pi, steps } = computeVDF(x, T, N);
const computeMs = (performance.now() - t0).toFixed(3);
// Animate chain
for (let i = 0; i <= T; i++) {
const step = document.createElement('div');
step.className = 'step';
const box = document.createElement('div');
box.className = 'box' + (i === T ? ' done' : i === Math.floor(T/2) ? ' current' : '');
box.textContent = i === 0 ? `x=${steps[0]}` : `x²^${i}=${steps[i]}`;
box.title = i === Math.floor(T/2) ? 'π (proof midpoint)' : '';
step.appendChild(box);
if (i < T) {
const arr = document.createElement('span');
arr.className = 'arrow';
arr.textContent = '→²';
step.appendChild(arr);
}
chain.appendChild(step);
// Stagger animation
await new Promise(r => setTimeout(r, Math.max(30, 300 / T)));
box.classList.add('show');
}
state = { y, pi, T, x };
$('out-y').textContent = y.toString();
$('out-pi').textContent = `${pi} (= x^(2^${Math.floor(T/2)}) mod ${N})`;
$('t-compute').textContent = fmt('%computeMs% ms (%T% squarings)', { computeMs: computeMs, T: T });
$('t-verify').textContent = '—';
$('result-panel').style.display = 'flex';
$('btn-run').disabled = false;
$('btn-verify').disabled = false;
}
function verifyResult() {
if (!state) return;
const t0 = performance.now();
const ok = verifyVDF(state.pi, state.y, state.T, N);
const verMs = (performance.now() - t0).toFixed(3);
$('t-verify').textContent = fmt('%verMs% ms (%ceil% squarings)', { verMs: verMs, ceil: Math.ceil(state.T / 2) });
const v = $('verdict');
v.className = 'verdict ' + (ok ? 'ok' : 'fail');
v.textContent = ok
? fmt('✓ Verified! π^(2^%ceil%) ≡ %y% (mod %N%)', { ceil: Math.ceil(state.T/2), y: state.y, N: N })
: '✗ Verification failed — output is incorrect.';
}
function reset() {
state = null;
$('chain').innerHTML = '';
$('result-panel').style.display = 'none';
$('verdict').textContent = '';
$('btn-verify').disabled = true;
}
$('btn-run').addEventListener('click', runVDF);
$('btn-verify').addEventListener('click', verifyResult);
$('btn-reset').addEventListener('click', reset);
// Auto-run on load
runVDF();
<div class="vdf-wrap">
<div class="panel">
<div class="row-group">
<label>Input x <span class="hint-small">(1–99)</span></label>
<input id="inp-x" type="number" min="1" max="99" value="7" />
</div>
<div class="row-group">
<label>Delay T <span class="hint-small">(steps)</span></label>
<input id="inp-t" type="range" min="4" max="20" value="10" />
<span id="t-val">10</span>
</div>
<div class="row-group">
<label>Modulus N</label>
<span id="mod-n" class="mono">3233</span>
<span class="hint-small">(= 61 × 53)</span>
</div>
<div class="btns">
<button id="btn-run" type="button">▶ Compute VDF</button>
<button id="btn-verify" type="button" disabled>✓ Verify</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="chain-wrap">
<div id="chain" class="chain"></div>
</div>
<div class="result-panel" id="result-panel" style="display:none">
<div class="result-row"><span class="lbl">Output y =</span><span id="out-y" class="mono big"></span></div>
<div class="result-row"><span class="lbl">Proof π =</span><span id="out-pi" class="mono"></span></div>
<div class="timing">
<span>Compute: <b id="t-compute">—</b></span>
<span>Verify: <b id="t-verify">—</b></span>
</div>
<div id="verdict" class="verdict"></div>
</div>
<p class="footnote">
Demo uses N = 3233 (= 61 × 53) — tiny for pedagogy. Real VDFs use 2048-bit RSA groups where knowing the factorization is impossible without the trapdoor.
</p>
</div>
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; color: #1a1a2e; background: #f8f9fc; padding: .6rem; }
.vdf-wrap { display: flex; flex-direction: column; gap: .8rem; }
.panel { background: #fff; border: 1px solid #dde3ec; border-radius: 10px; padding: .9rem 1rem; display: flex; flex-direction: column; gap: .55rem; }
.row-group { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
label { font-weight: 600; font-size: .85rem; min-width: 6rem; }
.hint-small { font-size: .75rem; color: #888; font-weight: 400; }
input[type=number] { width: 4rem; padding: .25rem .4rem; border: 1px solid #c5ccd8; border-radius: 6px; font: inherit; }
input[type=range] { width: 120px; accent-color: #2563eb; }
.mono { font-family: ui-monospace, monospace; font-size: .9rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 .82rem system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer; border: 1.5px solid #2563eb; background: #2563eb; color: #fff; transition: opacity .15s; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #2563eb; }
.chain-wrap { overflow-x: auto; padding-bottom: .2rem; }
.chain { display: flex; align-items: center; gap: 0; min-height: 2.6rem; }
.step { display: flex; align-items: center; }
.box { background: #e0e7ff; border: 1.5px solid #6366f1; border-radius: 6px; padding: .25rem .45rem;
font: 600 .78rem ui-monospace, monospace; color: #3730a3; white-space: nowrap;
opacity: 0; transform: scale(.7); transition: opacity .25s, transform .25s; }
.box.show { opacity: 1; transform: scale(1); }
.box.current { background: #fef08a; border-color: #ca8a04; color: #713f12; }
.box.done { background: #dcfce7; border-color: #16a34a; color: #14532d; }
.arrow { font-size: .8rem; color: #94a3b8; padding: 0 3px; }
.result-panel { background: #fff; border: 1px solid #dde3ec; border-radius: 10px; padding: .9rem 1rem; display: flex; flex-direction: column; gap: .45rem; }
.result-row { display: flex; align-items: baseline; gap: .5rem; }
.lbl { font-size: .8rem; font-weight: 600; color: #555; min-width: 6rem; }
.big { font-size: 1.2rem; color: #1e40af; }
.timing { display: flex; gap: 1.2rem; font-size: .82rem; color: #555; }
.verdict { font-size: .95rem; font-weight: 700; padding: .3rem .5rem; border-radius: 6px; }
.verdict.ok { background: #dcfce7; color: #14532d; }
.verdict.fail { background: #fee2e2; color: #991b1b; }
.footnote { font-size: .75rem; color: #888; line-height: 1.5; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VDF demo: repeated squaring mod N (pedagogy — N=3233=61×53, tiny but illustrative).
// Compute: y = x^(2^T) mod N — T sequential squarings, no parallelism possible.
// Proof: π = x^(2^floor(T/2)) mod N (the midpoint value, a simplified Pietrzak commit).
// Verify: check π^(2^ceil(T/2)) ≡ y (mod N) — half the squarings, proves correctness.
const N = 3233; // 61 × 53
let state = null; // { y, pi, T, x }
const $ = id => document.getElementById(id);
const tVal = $('t-val');
$('inp-t').addEventListener('input', e => { tVal.textContent = e.target.value; });
function modpow(base, exp, mod) {
// Iterative square-and-multiply for BigInt
let result = 1n;
base = base % mod;
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp >>= 1n;
base = base * base % mod;
}
return result;
}
function computeVDF(x, T, N) {
// Sequential squarings: x, x^2, x^4, ..., x^(2^T)
const steps = [BigInt(x) % BigInt(N)];
for (let i = 0; i < T; i++) {
steps.push(steps[steps.length - 1] ** 2n % BigInt(N));
}
const y = steps[T];
// Simplified proof: midpoint commitment π = x^(2^floor(T/2))
const pi = steps[Math.floor(T / 2)];
return { y, pi, steps };
}
function verifyVDF(pi, y, T, N) {
// Verify: π^(2^ceil(T/2)) mod N should equal y
const halvings = BigInt(Math.ceil(T / 2));
const exp = 1n << halvings; // 2^ceil(T/2)
const check = modpow(pi, exp, BigInt(N));
return check === y;
}
async function runVDF() {
const x = parseInt($('inp-x').value) || 7;
const T = parseInt($('inp-t').value) || 10;
$('btn-run').disabled = true;
$('btn-verify').disabled = true;
$('result-panel').style.display = 'none';
$('verdict').textContent = '';
$('verdict').className = 'verdict';
const chain = $('chain');
chain.innerHTML = '';
const t0 = performance.now();
const { y, pi, steps } = computeVDF(x, T, N);
const computeMs = (performance.now() - t0).toFixed(3);
// Animate chain
for (let i = 0; i <= T; i++) {
const step = document.createElement('div');
step.className = 'step';
const box = document.createElement('div');
box.className = 'box' + (i === T ? ' done' : i === Math.floor(T/2) ? ' current' : '');
box.textContent = i === 0 ? `x=${steps[0]}` : `x²^${i}=${steps[i]}`;
box.title = i === Math.floor(T/2) ? 'π (proof midpoint)' : '';
step.appendChild(box);
if (i < T) {
const arr = document.createElement('span');
arr.className = 'arrow';
arr.textContent = '→²';
step.appendChild(arr);
}
chain.appendChild(step);
// Stagger animation
await new Promise(r => setTimeout(r, Math.max(30, 300 / T)));
box.classList.add('show');
}
state = { y, pi, T, x };
$('out-y').textContent = y.toString();
$('out-pi').textContent = `${pi} (= x^(2^${Math.floor(T/2)}) mod ${N})`;
$('t-compute').textContent = fmt('%computeMs% ms (%T% squarings)', { computeMs: computeMs, T: T });
$('t-verify').textContent = '—';
$('result-panel').style.display = 'flex';
$('btn-run').disabled = false;
$('btn-verify').disabled = false;
}
function verifyResult() {
if (!state) return;
const t0 = performance.now();
const ok = verifyVDF(state.pi, state.y, state.T, N);
const verMs = (performance.now() - t0).toFixed(3);
$('t-verify').textContent = fmt('%verMs% ms (%ceil% squarings)', { verMs: verMs, ceil: Math.ceil(state.T / 2) });
const v = $('verdict');
v.className = 'verdict ' + (ok ? 'ok' : 'fail');
v.textContent = ok
? fmt('✓ Verified! π^(2^%ceil%) ≡ %y% (mod %N%)', { ceil: Math.ceil(state.T/2), y: state.y, N: N })
: '✗ Verification failed — output is incorrect.';
}
function reset() {
state = null;
$('chain').innerHTML = '';
$('result-panel').style.display = 'none';
$('verdict').textContent = '';
$('btn-verify').disabled = true;
}
$('btn-run').addEventListener('click', runVDF);
$('btn-verify').addEventListener('click', verifyResult);
$('btn-reset').addEventListener('click', reset);
// Auto-run on load
runVDF();
Notice the asymmetry. Computing takes T sequential squarings — you can watch each step tick by. Verifying uses a compact proof and checks the answer in a single operation, many times faster. This gap is the whole point: the prover spent real time, but the verifier doesn't have to.
Comments
Loading comments...