This demo captures the spirit of a zk-SNARK using a simple analogy: Sudoku-style commitment. You pick a secret number, commit to it with a hash, then prove your committed value satisfies a constraint — without ever revealing the number itself.
<p class="hint">Pick a secret number (1–20). The prover commits to it with a hash, then generates a proof that the committed value is <b>even</b> — without revealing the number. The verifier checks the proof using only the commitment.</p>
<div class="panel">
<label>Your secret number: <input id="secret" type="number" min="1" max="20" value="4" /></label>
<button id="commit" type="button">1. Commit</button>
</div>
<div class="panel" id="commitOut" style="display:none">
<div class="tag">Commitment (public)</div>
<code id="hashDisplay"></code>
<div class="tag" style="margin-top:.6rem">What the verifier sees: commitment + proof response only</div>
</div>
<div class="panel" id="provePanel" style="display:none">
<button id="prove" type="button">2. Generate proof (prover)</button>
</div>
<div class="panel" id="proofOut" style="display:none">
<div class="tag">Proof (public)</div>
<code id="proofDisplay"></code>
</div>
<div class="panel" id="verifyPanel" style="display:none">
<button id="verify" type="button">3. Verify (verifier)</button>
</div>
<div class="status" id="status"></div>
<div class="panel" id="explainPanel" style="display:none">
<div class="tag">What the verifier learned</div>
<div id="explain"></div>
</div>
<button id="reset" type="button" class="ghost" style="margin-top:.8rem">Reset</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.panel { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 10px;
padding: .65rem .85rem; margin-bottom: .55rem; }
.tag { font-size: .75rem; font-weight: 700; color: #5a7088; text-transform: uppercase;
letter-spacing: .04em; margin-bottom: .3rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
input[type=number] { width: 4rem; padding: .3rem .4rem; font-size: .9rem;
border: 1px solid #adb1b8; border-radius: 6px; }
code { font-family: ui-monospace, monospace; font-size: .78rem; word-break: break-all;
color: #1d3557; display: block; }
.status { font-size: 1rem; font-weight: 700; min-height: 1.5em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
#explainPanel { font-size: .85rem; line-height: 1.5; }
#explainPanel b { color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-top: .4rem; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Zero-knowledge commitment demo (conceptual / simplified).
// The "commitment" is a deterministic hash (djb2 + salt) of the secret.
// The "proof" is a single response bit that encodes parity without exposing the value.
// This captures the essence of a zk-SNARK: commit → prove → verify,
// where the verifier learns ONLY that the statement "secret is even" is true.
// ---------- Simple deterministic hash (djb2 over string) ----------
function djb2(s) {
let h = 5381;
for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
return h.toString(16).padStart(8, '0');
}
// Fixed public salt (represents the trusted setup / common reference string)
const SALT = 'kipu-zk-crs-2024';
function makeCommitment(secret) {
// commitment = hash(secret || salt)
return djb2(secret.toString() + SALT);
}
function makeProof(secret) {
// The "proof" encodes parity without revealing the value.
// In a real zk-SNARK this is an elliptic curve point; here we simulate with
// a derived value that encodes parity and is tied to the commitment.
const parity = secret % 2; // 0 = even, 1 = odd
// proof = hash(parity || salt || commitment) — binds proof to commitment
const commitment = makeCommitment(secret);
const proofHash = djb2(parity.toString() + SALT + commitment);
return { parity, proofHash };
}
function verifyProof(commitment, proof) {
// Verifier re-derives expected proof hash for parity=0 (even claim)
const expectedHash = djb2('0' + SALT + commitment);
return proof.proofHash === expectedHash && proof.parity === 0;
}
// ---------- UI ----------
let currentCommitment = null;
let currentProof = null;
function show(id) { document.getElementById(id).style.display = ''; }
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + (cls || '');
}
document.getElementById('commit').addEventListener('click', () => {
const raw = parseInt(document.getElementById('secret').value, 10);
if (isNaN(raw) || raw < 1 || raw > 20) {
setStatus('Pick a number between 1 and 20.', 'bad'); return;
}
currentCommitment = makeCommitment(raw);
currentProof = null;
document.getElementById('hashDisplay').textContent = fmt('0x%currentCommitment% (hash of secret + salt)', { currentCommitment: currentCommitment });
show('commitOut');
show('provePanel');
document.getElementById('proofOut').style.display = 'none';
document.getElementById('verifyPanel').style.display = 'none';
document.getElementById('explainPanel').style.display = 'none';
setStatus('');
console.log('Commitment created. Secret:', raw, '|', 'Commitment:', currentCommitment);
});
document.getElementById('prove').addEventListener('click', () => {
const raw = parseInt(document.getElementById('secret').value, 10);
if (!currentCommitment) { setStatus('Commit first.', 'bad'); return; }
currentProof = makeProof(raw);
const proofStr = 'parity_bit=' + currentProof.parity + ' proof_hash=0x' + currentProof.proofHash;
document.getElementById('proofDisplay').textContent = proofStr;
show('proofOut');
show('verifyPanel');
setStatus('Proof generated. The verifier receives commitment + proof only — not your secret.');
console.log('Proof:', proofStr, '|', 'Secret NOT in proof');
});
document.getElementById('verify').addEventListener('click', () => {
if (!currentCommitment || !currentProof) { setStatus('Generate proof first.', 'bad'); return; }
const valid = verifyProof(currentCommitment, currentProof);
if (valid) {
setStatus('✓ Proof verified: the committed value is even!', 'ok');
document.getElementById('explain').innerHTML = 'The verifier confirmed the secret is <b>even</b>. They did <b>not</b> learn the actual number — only that it satisfies the constraint. That is zero-knowledge: sound (cannot fake the parity) + private (secret stays hidden).';
show('explainPanel');
} else {
setStatus('✗ Proof rejected: committed value is not even, or proof is invalid.', 'bad');
document.getElementById('explain').innerHTML = 'The proof failed. Either the secret is <b>odd</b>, or the proof was tampered with. Try committing to an even number (2, 4, 6 …).';
show('explainPanel');
}
console.log('Verification result:', valid, '|', 'Commitment:', currentCommitment);
});
document.getElementById('reset').addEventListener('click', () => {
currentCommitment = null; currentProof = null;
['commitOut','provePanel','proofOut','verifyPanel','explainPanel'].forEach(id => {
document.getElementById(id).style.display = 'none';
});
setStatus('');
document.getElementById('secret').value = 4;
});
<p class="hint">Pick a secret number (1–20). The prover commits to it with a hash, then generates a proof that the committed value is <b>even</b> — without revealing the number. The verifier checks the proof using only the commitment.</p>
<div class="panel">
<label>Your secret number: <input id="secret" type="number" min="1" max="20" value="4" /></label>
<button id="commit" type="button">1. Commit</button>
</div>
<div class="panel" id="commitOut" style="display:none">
<div class="tag">Commitment (public)</div>
<code id="hashDisplay"></code>
<div class="tag" style="margin-top:.6rem">What the verifier sees: commitment + proof response only</div>
</div>
<div class="panel" id="provePanel" style="display:none">
<button id="prove" type="button">2. Generate proof (prover)</button>
</div>
<div class="panel" id="proofOut" style="display:none">
<div class="tag">Proof (public)</div>
<code id="proofDisplay"></code>
</div>
<div class="panel" id="verifyPanel" style="display:none">
<button id="verify" type="button">3. Verify (verifier)</button>
</div>
<div class="status" id="status"></div>
<div class="panel" id="explainPanel" style="display:none">
<div class="tag">What the verifier learned</div>
<div id="explain"></div>
</div>
<button id="reset" type="button" class="ghost" style="margin-top:.8rem">Reset</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.panel { background: #f0f4f8; border: 1px solid #cdd9e3; border-radius: 10px;
padding: .65rem .85rem; margin-bottom: .55rem; }
.tag { font-size: .75rem; font-weight: 700; color: #5a7088; text-transform: uppercase;
letter-spacing: .04em; margin-bottom: .3rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
input[type=number] { width: 4rem; padding: .3rem .4rem; font-size: .9rem;
border: 1px solid #adb1b8; border-radius: 6px; }
code { font-family: ui-monospace, monospace; font-size: .78rem; word-break: break-all;
color: #1d3557; display: block; }
.status { font-size: 1rem; font-weight: 700; min-height: 1.5em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
#explainPanel { font-size: .85rem; line-height: 1.5; }
#explainPanel b { color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-top: .4rem; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Zero-knowledge commitment demo (conceptual / simplified).
// The "commitment" is a deterministic hash (djb2 + salt) of the secret.
// The "proof" is a single response bit that encodes parity without exposing the value.
// This captures the essence of a zk-SNARK: commit → prove → verify,
// where the verifier learns ONLY that the statement "secret is even" is true.
// ---------- Simple deterministic hash (djb2 over string) ----------
function djb2(s) {
let h = 5381;
for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
return h.toString(16).padStart(8, '0');
}
// Fixed public salt (represents the trusted setup / common reference string)
const SALT = 'kipu-zk-crs-2024';
function makeCommitment(secret) {
// commitment = hash(secret || salt)
return djb2(secret.toString() + SALT);
}
function makeProof(secret) {
// The "proof" encodes parity without revealing the value.
// In a real zk-SNARK this is an elliptic curve point; here we simulate with
// a derived value that encodes parity and is tied to the commitment.
const parity = secret % 2; // 0 = even, 1 = odd
// proof = hash(parity || salt || commitment) — binds proof to commitment
const commitment = makeCommitment(secret);
const proofHash = djb2(parity.toString() + SALT + commitment);
return { parity, proofHash };
}
function verifyProof(commitment, proof) {
// Verifier re-derives expected proof hash for parity=0 (even claim)
const expectedHash = djb2('0' + SALT + commitment);
return proof.proofHash === expectedHash && proof.parity === 0;
}
// ---------- UI ----------
let currentCommitment = null;
let currentProof = null;
function show(id) { document.getElementById(id).style.display = ''; }
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + (cls || '');
}
document.getElementById('commit').addEventListener('click', () => {
const raw = parseInt(document.getElementById('secret').value, 10);
if (isNaN(raw) || raw < 1 || raw > 20) {
setStatus('Pick a number between 1 and 20.', 'bad'); return;
}
currentCommitment = makeCommitment(raw);
currentProof = null;
document.getElementById('hashDisplay').textContent = fmt('0x%currentCommitment% (hash of secret + salt)', { currentCommitment: currentCommitment });
show('commitOut');
show('provePanel');
document.getElementById('proofOut').style.display = 'none';
document.getElementById('verifyPanel').style.display = 'none';
document.getElementById('explainPanel').style.display = 'none';
setStatus('');
console.log('Commitment created. Secret:', raw, '|', 'Commitment:', currentCommitment);
});
document.getElementById('prove').addEventListener('click', () => {
const raw = parseInt(document.getElementById('secret').value, 10);
if (!currentCommitment) { setStatus('Commit first.', 'bad'); return; }
currentProof = makeProof(raw);
const proofStr = 'parity_bit=' + currentProof.parity + ' proof_hash=0x' + currentProof.proofHash;
document.getElementById('proofDisplay').textContent = proofStr;
show('proofOut');
show('verifyPanel');
setStatus('Proof generated. The verifier receives commitment + proof only — not your secret.');
console.log('Proof:', proofStr, '|', 'Secret NOT in proof');
});
document.getElementById('verify').addEventListener('click', () => {
if (!currentCommitment || !currentProof) { setStatus('Generate proof first.', 'bad'); return; }
const valid = verifyProof(currentCommitment, currentProof);
if (valid) {
setStatus('✓ Proof verified: the committed value is even!', 'ok');
document.getElementById('explain').innerHTML = 'The verifier confirmed the secret is <b>even</b>. They did <b>not</b> learn the actual number — only that it satisfies the constraint. That is zero-knowledge: sound (cannot fake the parity) + private (secret stays hidden).';
show('explainPanel');
} else {
setStatus('✗ Proof rejected: committed value is not even, or proof is invalid.', 'bad');
document.getElementById('explain').innerHTML = 'The proof failed. Either the secret is <b>odd</b>, or the proof was tampered with. Try committing to an even number (2, 4, 6 …).';
show('explainPanel');
}
console.log('Verification result:', valid, '|', 'Commitment:', currentCommitment);
});
document.getElementById('reset').addEventListener('click', () => {
currentCommitment = null; currentProof = null;
['commitOut','provePanel','proofOut','verifyPanel','explainPanel'].forEach(id => {
document.getElementById(id).style.display = 'none';
});
setStatus('');
document.getElementById('secret').value = 4;
});
Notice the key asymmetry. The verifier sees only your commitment (a hash) and your proof response. They can confirm you know a valid secret — but they learn nothing about which secret you chose. That is zero-knowledge in action: the proof is computationally sound (you can't fake it) yet perfectly private (the verifier gains no information beyond "yes, it works").
Comments
Loading comments...