Play both roles. Pick a value and a random secret, then Commit — the demo hashes them together into a single fingerprint and shows only that. The value stays hidden, but you can no longer change it.
<p class="hint">A commitment is a sealed envelope. Pick a value and a random secret, then <b>Commit</b> — you publish only the hash. It reveals nothing about the value (<b>hiding</b>), yet you can open it only one way (<b>binding</b>). Then <b>Reveal</b>, or <b>Try to cheat</b> by changing the value afterward.</p>
<div class="row">
<label>Value <input id="value" value="HEADS"></label>
<label>Secret <input id="secret" value="r4nd0m"></label>
<button id="gen" type="button" class="ghost">New secret</button>
</div>
<div class="btns">
<button id="commit" type="button">Commit</button>
<button id="reveal" type="button" disabled>Reveal</button>
<button id="cheat" type="button" class="ghost" disabled>Try to cheat</button>
</div>
<div class="env" id="env">No commitment yet.</div>
<div class="status" id="status"></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 .7rem; line-height: 1.45; }
.row { display: flex; gap: .6rem; flex-wrap: wrap; align-items: end; margin-bottom: .6rem; }
label { font: 600 .8rem system-ui, sans-serif; color: #1d3557; display: flex; flex-direction: column; gap: .2rem; }
input { font: 500 14px ui-monospace, monospace; padding: .4rem .5rem; border: 1px solid #cdd9e3; border-radius: 8px; width: 9rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .3rem 0 .6rem; }
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; }
button:disabled { opacity: .4; cursor: not-allowed; }
.env { background: #e8eef3; border: 1px dashed #adb1b8; border-radius: 10px; padding: .7rem .9rem;
font: 600 13px ui-monospace, monospace; color: #1d3557; word-break: break-all; line-height: 1.5; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A commitment scheme: commit(value, secret) -> H(value || secret), a single
// "fingerprint". HIDING: the hash leaks nothing about value (the secret blinds it). BINDING: to open you must reveal (value, secret) and anyone re-hashes to verify;
// finding a second (value', secret') with the same hash is a collision — believed intractable for a real hash. We use a small deterministic hash (FNV-1a 32-bit)
// just to make the fingerprint visible and reproducible in-browser.
function hash(value, secret){
const s = value + '|' + secret;
let h = 0x811c9dc5; // FNV offset basis
for(let i=0;i<s.length;i++){
h ^= s.charCodeAt(i);
h = Math.imul(h, 0x01000193) >>> 0; // FNV prime, keep 32-bit unsigned
}
return ('00000000' + h.toString(16)).slice(-8);
}
let committed = null; // { value, secret, digest } — what the committer locked in
const $ = id => document.getElementById(id);
function setStatus(t, cls){ const s = $('status'); s.textContent = t; s.className = 'status ' + (cls||''); }
function doCommit(){
const value = $('value').value, secret = $('secret').value;
const digest = hash(value, secret);
committed = { value, secret, digest };
$('env').textContent = fmt('Published commitment (sealed envelope): %digest%', { digest: digest });
$('reveal').disabled = false; $('cheat').disabled = false;
setStatus('Committed. The value is hidden — only the hash is public. You are now bound to it.', 'ok');
}
// Honest reveal: open with the SAME value+secret. Verifier re-hashes and compares.
function doReveal(){
if(!committed){ return; }
const claimed = $('value').value, secret = $('secret').value;
const recomputed = hash(claimed, secret);
if(recomputed === committed.digest)
setStatus(fmt('✓ Reveal accepted: hash("%claimed%" | secret) = %recomputed% matches the commitment.', { claimed: claimed, recomputed: recomputed }), 'ok');
else
setStatus(fmt('✗ Reveal rejected: %recomputed% ≠ %digest%. Inputs do not match the envelope.', { recomputed: recomputed, digest: committed.digest }), 'bad');
}
// Cheating: after committing, change the value and try to pass it off as the original. With no matching secret, the recomputed hash won't equal the digest.
function doCheat(){
if(!committed){ return; }
const fake = (committed.value === 'TAILS') ? 'HEADS' : 'TAILS';
$('value').value = fake;
const recomputed = hash(fake, $('secret').value);
setStatus(fmt('✗ Cheat detected: you committed to "%value%" but now claim "%fake%". hash = %recomputed% ≠ %digest%, so binding holds.', { value: committed.value, fake: fake, recomputed: recomputed, digest: committed.digest }), 'bad');
}
$('commit').addEventListener('click', doCommit);
$('reveal').addEventListener('click', doReveal);
$('cheat').addEventListener('click', doCheat);
$('gen').addEventListener('click', () => { $('secret').value = Math.random().toString(36).slice(2, 10); });
<p class="hint">A commitment is a sealed envelope. Pick a value and a random secret, then <b>Commit</b> — you publish only the hash. It reveals nothing about the value (<b>hiding</b>), yet you can open it only one way (<b>binding</b>). Then <b>Reveal</b>, or <b>Try to cheat</b> by changing the value afterward.</p>
<div class="row">
<label>Value <input id="value" value="HEADS"></label>
<label>Secret <input id="secret" value="r4nd0m"></label>
<button id="gen" type="button" class="ghost">New secret</button>
</div>
<div class="btns">
<button id="commit" type="button">Commit</button>
<button id="reveal" type="button" disabled>Reveal</button>
<button id="cheat" type="button" class="ghost" disabled>Try to cheat</button>
</div>
<div class="env" id="env">No commitment yet.</div>
<div class="status" id="status"></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 .7rem; line-height: 1.45; }
.row { display: flex; gap: .6rem; flex-wrap: wrap; align-items: end; margin-bottom: .6rem; }
label { font: 600 .8rem system-ui, sans-serif; color: #1d3557; display: flex; flex-direction: column; gap: .2rem; }
input { font: 500 14px ui-monospace, monospace; padding: .4rem .5rem; border: 1px solid #cdd9e3; border-radius: 8px; width: 9rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .3rem 0 .6rem; }
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; }
button:disabled { opacity: .4; cursor: not-allowed; }
.env { background: #e8eef3; border: 1px dashed #adb1b8; border-radius: 10px; padding: .7rem .9rem;
font: 600 13px ui-monospace, monospace; color: #1d3557; word-break: break-all; line-height: 1.5; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A commitment scheme: commit(value, secret) -> H(value || secret), a single
// "fingerprint". HIDING: the hash leaks nothing about value (the secret blinds it). BINDING: to open you must reveal (value, secret) and anyone re-hashes to verify;
// finding a second (value', secret') with the same hash is a collision — believed intractable for a real hash. We use a small deterministic hash (FNV-1a 32-bit)
// just to make the fingerprint visible and reproducible in-browser.
function hash(value, secret){
const s = value + '|' + secret;
let h = 0x811c9dc5; // FNV offset basis
for(let i=0;i<s.length;i++){
h ^= s.charCodeAt(i);
h = Math.imul(h, 0x01000193) >>> 0; // FNV prime, keep 32-bit unsigned
}
return ('00000000' + h.toString(16)).slice(-8);
}
let committed = null; // { value, secret, digest } — what the committer locked in
const $ = id => document.getElementById(id);
function setStatus(t, cls){ const s = $('status'); s.textContent = t; s.className = 'status ' + (cls||''); }
function doCommit(){
const value = $('value').value, secret = $('secret').value;
const digest = hash(value, secret);
committed = { value, secret, digest };
$('env').textContent = fmt('Published commitment (sealed envelope): %digest%', { digest: digest });
$('reveal').disabled = false; $('cheat').disabled = false;
setStatus('Committed. The value is hidden — only the hash is public. You are now bound to it.', 'ok');
}
// Honest reveal: open with the SAME value+secret. Verifier re-hashes and compares.
function doReveal(){
if(!committed){ return; }
const claimed = $('value').value, secret = $('secret').value;
const recomputed = hash(claimed, secret);
if(recomputed === committed.digest)
setStatus(fmt('✓ Reveal accepted: hash("%claimed%" | secret) = %recomputed% matches the commitment.', { claimed: claimed, recomputed: recomputed }), 'ok');
else
setStatus(fmt('✗ Reveal rejected: %recomputed% ≠ %digest%. Inputs do not match the envelope.', { recomputed: recomputed, digest: committed.digest }), 'bad');
}
// Cheating: after committing, change the value and try to pass it off as the original. With no matching secret, the recomputed hash won't equal the digest.
function doCheat(){
if(!committed){ return; }
const fake = (committed.value === 'TAILS') ? 'HEADS' : 'TAILS';
$('value').value = fake;
const recomputed = hash(fake, $('secret').value);
setStatus(fmt('✗ Cheat detected: you committed to "%value%" but now claim "%fake%". hash = %recomputed% ≠ %digest%, so binding holds.', { value: committed.value, fake: fake, recomputed: recomputed, digest: committed.digest }), 'bad');
}
$('commit').addEventListener('click', doCommit);
$('reveal').addEventListener('click', doReveal);
$('cheat').addEventListener('click', doCheat);
$('gen').addEventListener('click', () => { $('secret').value = Math.random().toString(36).slice(2, 10); });
Now Reveal: send the original value and secret. Anyone can re-hash them and check they match the published commitment. Try the Try to cheat button — change the value after committing, and the verifier instantly rejects it, because no other input produces the same fingerprint. That is hiding and binding working together, exactly as in the cross-linked zero-knowledge proofs.
Comments
Loading comments...