Here you are the verifier. A prover claims to know a secret degree-low polynomial and announces its total sum. You don't recompute the sum — you can't, it's too big. Instead you fold the claim into a single random point and force the prover to stay consistent. An honest prover always survives; a lying prover gets caught with high probability.
<p class="hint">You are the <b>verifier</b>. A prover claims the secret polynomial g(x) = 3 + 5x + 2x² (mod 97) sums to a certain value over x = 0..4. You can’t recompute the sum — instead you pick a <b>random point</b> and check one equation. An honest prover always passes; a liar gets caught most of the time.</p>
<div class="rows">
<div class="row"><span class="lbl">Prover’s claimed total S</span><b id="claim">—</b></div>
<div class="row"><span class="lbl">Your random challenge r</span><b id="rval">—</b></div>
<div class="row"><span class="lbl">One equation you check</span><b id="eq">—</b></div>
</div>
<div class="status" id="status">Choose a prover to run a proof.</div>
<div class="btns">
<button id="honest" type="button">Run honest prover</button>
<button id="liar" type="button">Run lying prover</button>
<button id="batch" type="button" class="ghost">Run liar ×500</button>
</div>
<div class="tally" id="tally"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.rows { display: flex; flex-direction: column; gap: .35rem; margin: .4rem 0 .8rem; }
.row { display: flex; justify-content: space-between; align-items: center;
background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px; padding: .45rem .7rem; }
.lbl { font-size: .85rem; color: #1d3557; }
.row b { font: 700 15px ui-monospace, monospace; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.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; }
.tally { font-size: .88rem; color: #444; margin-top: .6rem; line-height: 1.5; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A miniature interactive proof in the spirit of IP = PSPACE (Shamir, 1990).
// The "true" object is a low-degree polynomial g over the field Z_p.
// The prover claims a SUM S of g over a domain. The verifier never recomputes
// the sum directly; it picks a RANDOM point r and checks one polynomial identity.
// Honesty => always consistent. A lie in S forces an inconsistent polynomial,
// and a random r catches it unless r happens to be a shared root: at most
// (degree)/p chance of slipping through. This is the soundness of the protocol.
const P = 97; // prime field size
const COEF = [3, 5, 2]; // secret g(x) = 3 + 5x + 2x^2
const DOMAIN = [0,1,2,3,4]; // sum g over these x
const mod = n => ((n % P) + P) % P;
const g = x => mod(COEF[0] + COEF[1]*x + COEF[2]*x*x);
const trueSum = DOMAIN.reduce((a,x) => mod(a + g(x)), 0);
function setStatus(t, cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function show(claim, r, lhs, rhs){
document.getElementById('claim').textContent = claim;
document.getElementById('rval').textContent = (r===null?'—':r);
document.getElementById('eq').textContent = (r===null?'—':(lhs+' =? '+rhs));
}
// One round of the proof. The prover commits to a low-degree polynomial and a
// claimed sum. The verifier picks a random point r and checks that the prover’s
// polynomial agrees with the real g at r (the folded identity).
// Honest prover: sends g itself; the check always holds.
// Lying prover: must send a DIFFERENT low-degree polynomial h != g to back its
// false sum. Two distinct degree-d polynomials agree on at most d points, so a
// random r exposes the lie unless r happens to be one of those <= d collisions:
// soundness error <= d/P. This is exactly how IP protocols stay sound.
function evalPoly(c, x){ let v=0, pw=1; for(const k of c){ v=mod(v+k*pw); pw=mod(pw*x); } return v; }
function lyingPoly(){
// pick a random low-degree h != g by perturbing some coefficients
let h;
do { h = COEF.map(k => mod(k + Math.floor(Math.random()*P))); }
while (h.every((k,i)=>k===COEF[i]));
return h;
}
function runProof(lying){
const r = Math.floor(Math.random()*P);
const proverPoly = lying ? lyingPoly() : COEF.slice();
const claim = lying
? DOMAIN.reduce((a,x)=>mod(a+evalPoly(proverPoly,x)),0) // sum of the FAKE poly
: trueSum;
const proverVal = evalPoly(proverPoly, r);
const verifierVal = g(r); // verifier checks real g at r
const pass = proverVal === verifierVal;
return { claim, r, proverVal, verifierVal, pass, lying };
}
function play(lying){
const o = runProof(lying);
show(o.claim, o.r, o.proverVal, o.verifierVal);
if(!lying){ setStatus(fmt('Honest prover: claim %claim% is the true sum, and the check holds. ACCEPT.', { claim: o.claim }),'ok'); }
else if(o.pass){ setStatus('Lying prover slipped through this time (r hit a shared root). ACCEPT — a rare miss.','bad'); }
else { setStatus(fmt('Lying prover: the folded equation fails at r=%r%. REJECTED!', { r: o.r }),'ok'); }
document.getElementById('tally').textContent='';
}
function batch(){
const trials=500; let caught=0;
for(let i=0;i<trials;i++){ if(!runProof(true).pass) caught++; }
const rate=(100*caught/trials).toFixed(1);
show('(many)', null, null, null);
setStatus(fmt('Liar caught in %caught% of %trials% runs (%rate%%).', { caught: caught, trials: trials, rate: rate }), caught>trials*0.9?'ok':'bad');
document.getElementById('tally').textContent =
fmt('A degree-%v% lie can only escape when the random r lands on a shared root — at most %v%/%P% ≈ %v2%% of the time. Repeat the proof a few times and the cheat’s odds collapse toward zero.', { v: COEF.length-1, P: P, v2: (100*(COEF.length-1)/P).toFixed(1) });
}
document.getElementById('honest').addEventListener('click',()=>play(false));
document.getElementById('liar').addEventListener('click',()=>play(true));
document.getElementById('batch').addEventListener('click',batch);
show('—', null, null, null);
<p class="hint">You are the <b>verifier</b>. A prover claims the secret polynomial g(x) = 3 + 5x + 2x² (mod 97) sums to a certain value over x = 0..4. You can’t recompute the sum — instead you pick a <b>random point</b> and check one equation. An honest prover always passes; a liar gets caught most of the time.</p>
<div class="rows">
<div class="row"><span class="lbl">Prover’s claimed total S</span><b id="claim">—</b></div>
<div class="row"><span class="lbl">Your random challenge r</span><b id="rval">—</b></div>
<div class="row"><span class="lbl">One equation you check</span><b id="eq">—</b></div>
</div>
<div class="status" id="status">Choose a prover to run a proof.</div>
<div class="btns">
<button id="honest" type="button">Run honest prover</button>
<button id="liar" type="button">Run lying prover</button>
<button id="batch" type="button" class="ghost">Run liar ×500</button>
</div>
<div class="tally" id="tally"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.rows { display: flex; flex-direction: column; gap: .35rem; margin: .4rem 0 .8rem; }
.row { display: flex; justify-content: space-between; align-items: center;
background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px; padding: .45rem .7rem; }
.lbl { font-size: .85rem; color: #1d3557; }
.row b { font: 700 15px ui-monospace, monospace; color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.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; }
.tally { font-size: .88rem; color: #444; margin-top: .6rem; line-height: 1.5; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A miniature interactive proof in the spirit of IP = PSPACE (Shamir, 1990).
// The "true" object is a low-degree polynomial g over the field Z_p.
// The prover claims a SUM S of g over a domain. The verifier never recomputes
// the sum directly; it picks a RANDOM point r and checks one polynomial identity.
// Honesty => always consistent. A lie in S forces an inconsistent polynomial,
// and a random r catches it unless r happens to be a shared root: at most
// (degree)/p chance of slipping through. This is the soundness of the protocol.
const P = 97; // prime field size
const COEF = [3, 5, 2]; // secret g(x) = 3 + 5x + 2x^2
const DOMAIN = [0,1,2,3,4]; // sum g over these x
const mod = n => ((n % P) + P) % P;
const g = x => mod(COEF[0] + COEF[1]*x + COEF[2]*x*x);
const trueSum = DOMAIN.reduce((a,x) => mod(a + g(x)), 0);
function setStatus(t, cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function show(claim, r, lhs, rhs){
document.getElementById('claim').textContent = claim;
document.getElementById('rval').textContent = (r===null?'—':r);
document.getElementById('eq').textContent = (r===null?'—':(lhs+' =? '+rhs));
}
// One round of the proof. The prover commits to a low-degree polynomial and a
// claimed sum. The verifier picks a random point r and checks that the prover’s
// polynomial agrees with the real g at r (the folded identity).
// Honest prover: sends g itself; the check always holds.
// Lying prover: must send a DIFFERENT low-degree polynomial h != g to back its
// false sum. Two distinct degree-d polynomials agree on at most d points, so a
// random r exposes the lie unless r happens to be one of those <= d collisions:
// soundness error <= d/P. This is exactly how IP protocols stay sound.
function evalPoly(c, x){ let v=0, pw=1; for(const k of c){ v=mod(v+k*pw); pw=mod(pw*x); } return v; }
function lyingPoly(){
// pick a random low-degree h != g by perturbing some coefficients
let h;
do { h = COEF.map(k => mod(k + Math.floor(Math.random()*P))); }
while (h.every((k,i)=>k===COEF[i]));
return h;
}
function runProof(lying){
const r = Math.floor(Math.random()*P);
const proverPoly = lying ? lyingPoly() : COEF.slice();
const claim = lying
? DOMAIN.reduce((a,x)=>mod(a+evalPoly(proverPoly,x)),0) // sum of the FAKE poly
: trueSum;
const proverVal = evalPoly(proverPoly, r);
const verifierVal = g(r); // verifier checks real g at r
const pass = proverVal === verifierVal;
return { claim, r, proverVal, verifierVal, pass, lying };
}
function play(lying){
const o = runProof(lying);
show(o.claim, o.r, o.proverVal, o.verifierVal);
if(!lying){ setStatus(fmt('Honest prover: claim %claim% is the true sum, and the check holds. ACCEPT.', { claim: o.claim }),'ok'); }
else if(o.pass){ setStatus('Lying prover slipped through this time (r hit a shared root). ACCEPT — a rare miss.','bad'); }
else { setStatus(fmt('Lying prover: the folded equation fails at r=%r%. REJECTED!', { r: o.r }),'ok'); }
document.getElementById('tally').textContent='';
}
function batch(){
const trials=500; let caught=0;
for(let i=0;i<trials;i++){ if(!runProof(true).pass) caught++; }
const rate=(100*caught/trials).toFixed(1);
show('(many)', null, null, null);
setStatus(fmt('Liar caught in %caught% of %trials% runs (%rate%%).', { caught: caught, trials: trials, rate: rate }), caught>trials*0.9?'ok':'bad');
document.getElementById('tally').textContent =
fmt('A degree-%v% lie can only escape when the random r lands on a shared root — at most %v%/%P% ≈ %v2%% of the time. Repeat the proof a few times and the cheat’s odds collapse toward zero.', { v: COEF.length-1, P: P, v2: (100*(COEF.length-1)/P).toFixed(1) });
}
document.getElementById('honest').addEventListener('click',()=>play(false));
document.getElementById('liar').addEventListener('click',()=>play(true));
document.getElementById('batch').addEventListener('click',batch);
show('—', null, null, null);
Notice the asymmetry. The prover does the heavy lifting; you only flip a coin and check one equation. Press Run honest prover and it always passes. Press Run lying prover — its first lie forces every later answer to be inconsistent, and your random point exposes it almost every time. Run it again and again: the cheat's success rate hugs the tiny theoretical bound, never your trust.
Comments
Loading comments...