Pick a secret value and a valid range, then commit it. The demo simulates a Prover and a Verifier: the prover generates a proof that the committed value lies in the range, and the verifier checks the proof — without ever learning what the value is.
<!-- Zero-knowledge range proof demo: commit a secret value, then prove it is in range -->
<div class="panel">
<div class="section-label">Prover</div>
<label class="field-label">Secret value (x):
<input id="secret" type="number" min="0" max="255" value="42" title="The secret number you want to prove is in range">
</label>
<label class="field-label">Claimed range:
<select id="rangeMax" title="Choose the upper bound of the range">
<option value="7">0 – 7 (3 bits)</option>
<option value="15">0 – 15 (4 bits)</option>
<option value="31">0 – 31 (5 bits)</option>
<option value="63">0 – 63 (6 bits)</option>
<option value="127">0 – 127 (7 bits)</option>
<option value="255" selected>0 – 255 (8 bits)</option>
</select>
</label>
<button id="btnCommit" type="button">Commit</button>
</div>
<div class="panel commitment-panel" id="commitPanel" style="display:none">
<div class="section-label">Commitment (public)</div>
<div class="commit-row">
<span class="mono" id="commitVal"></span>
<span class="tag">C</span>
</div>
<div class="hint-small">The verifier sees this hash — it binds you to x without revealing x.</div>
<button id="btnProve" type="button">Generate Proof</button>
</div>
<div class="panel proof-panel" id="proofPanel" style="display:none">
<div class="section-label">Bit Decomposition Proof</div>
<div id="bitRows" class="bit-rows"></div>
<div class="hint-small">Each bit of x gets its own commitment. The verifier checks they are all 0 or 1 and reconstruct x.</div>
</div>
<div class="panel verifier-panel" id="verifierPanel" style="display:none">
<div class="section-label">Verifier</div>
<div class="status" id="verifyStatus"></div>
<div class="hint-small" id="verifyDetail"></div>
<button id="btnReset" type="button" class="ghost">Start Over</button>
</div>
/* Layout for the ZK range proof interactive demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.panel { background: #f4f7fa; border: 1px solid #d0dae4; border-radius: 10px;
padding: .8rem 1rem; margin-bottom: .6rem; }
.section-label { font-size: .7rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #5a7088; margin-bottom: .45rem; }
.field-label { display: flex; align-items: center; gap: .5rem; font-size: .9rem;
font-weight: 600; margin-bottom: .45rem; }
input[type=number] { width: 72px; padding: .3rem .4rem; border: 1px solid #b0bec5;
border-radius: 6px; font-size: .95rem; text-align: center; }
select { padding: .3rem .4rem; border: 1px solid #b0bec5; border-radius: 6px;
font-size: .9rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer;
margin-top: .3rem; }
button.ghost { background: #fff; color: #1d3557; }
.commit-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .35rem; }
.mono { font-family: ui-monospace, monospace; font-size: .82rem; color: #1d3557;
background: #dce8f3; padding: .2rem .45rem; border-radius: 5px;
word-break: break-all; }
.tag { font-size: .7rem; font-weight: 700; text-transform: uppercase; background: #e2ecf5;
color: #3a6080; padding: .15rem .4rem; border-radius: 4px; }
.hint-small { font-size: .8rem; color: #555; line-height: 1.4; margin-top: .3rem; }
/* Bit decomposition cells — each bit gets its own Pedersen commitment */
.bit-rows { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .4rem; }
.bit-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.bit-box { width: 34px; height: 34px; border-radius: 6px; display: flex;
align-items: center; justify-content: center; font: 700 14px ui-monospace;
border: 1.5px solid #8da7be; }
.bit-box.b0 { background: #e8eef3; color: #444; }
.bit-box.b1 { background: #1d3557; color: #fff; border-color: #1d3557; }
.bit-idx { font-size: .6rem; color: #7a8fa0; }
.commit-mini { font-family: ui-monospace; font-size: .7rem; color: #3a6080;
background: #dce8f3; border-radius: 4px; padding: .1rem .3rem;
margin-top: 2px; }
.status { font-size: 1rem; font-weight: 700; padding: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.verifier-panel { background: #f0f9f4; border-color: #b8d8c6; }
.verifier-panel .status.bad { background: #fdf0f0; border-radius: 6px;
padding: .3rem .5rem; }
// Zero-Knowledge Range Proof demo
// We simulate Pedersen commitments and bit-decomposition proofs with toy arithmetic.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pedersen commitment: C = G^x * H^r mod p
// C = x*G + r*H (mod p), simulated with integers mod a large prime
const P = 2147483647; // Mersenne prime used as group order (2^31 - 1, Mersenne prime)
const G = 3; // generator G
const H = 7; // independent generator H (chosen so nobody knows log_G(H))
function modpow(base, exp, mod) {
// Fast modular exponentiation (BigInt for overflow safety)
let result = 1n;
base = BigInt(base) % BigInt(mod);
exp = BigInt(exp);
mod = BigInt(mod);
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp = exp / 2n;
base = base * base % mod;
}
return Number(result);
}
function pedersen(x, r) {
// Compute Pedersen commitment C = G^x * H^r mod P
return (modpow(G, x, P) * modpow(H, r, P)) % P;
}
function randBlind() {
// Random blinding factor (in a real system this would be cryptographically random)
return Math.floor(Math.random() * 1000000) + 1;
}
// Shared state between Prover and Verifier steps
let state = {};
function hexShort(n) {
// Display a number as a short hex string
return '0x' + n.toString(16).toUpperCase().padStart(8, '0');
}
document.getElementById('btnCommit').addEventListener('click', () => {
// Prover: pick secret x, choose random blinding r, publish C = G^x * H^r mod P
const x = parseInt(document.getElementById('secret').value, 10);
const maxVal = parseInt(document.getElementById('rangeMax').value, 10);
if (isNaN(x) || x < 0) {
alert('Please enter a non-negative integer.');
return;
}
const r = randBlind();
const C = pedersen(x, r);
state = { x, r, C, maxVal };
document.getElementById('commitVal').textContent = hexShort(C);
document.getElementById('commitPanel').style.display = '';
document.getElementById('proofPanel').style.display = 'none';
document.getElementById('verifierPanel').style.display = 'none';
document.getElementById('bitRows').innerHTML = '';
});
document.getElementById('btnProve').addEventListener('click', () => {
// Prover: decompose x into bits, commit each bit separately
const { x, r, C, maxVal } = state;
const bits = Math.round(Math.log2(maxVal + 1));
// Decompose x in binary and create one commitment per bit
const bitValues = [];
const bitBlinds = [];
const bitCommits = [];
for (let i = 0; i < bits; i++) {
const b = (x >> i) & 1;
const rb = randBlind();
const cb = pedersen(b, rb);
bitValues.push(b);
bitBlinds.push(rb);
bitCommits.push(cb);
}
state.bitValues = bitValues;
state.bitBlinds = bitBlinds;
state.bitCommits = bitCommits;
state.bits = bits;
// Render the bit commitments as colored boxes (dark = 1, light = 0)
const container = document.getElementById('bitRows');
container.innerHTML = '';
for (let i = bits - 1; i >= 0; i--) {
const cell = document.createElement('div');
cell.className = 'bit-cell';
const box = document.createElement('div');
box.className = 'bit-box b' + bitValues[i];
box.textContent = bitValues[i];
const idx = document.createElement('div');
idx.className = 'bit-idx';
idx.textContent = '2⁻' + i + ' ' + i18nFmt('(bit %i%)', { i });
const cm = document.createElement('div');
cm.className = 'commit-mini';
cm.textContent = hexShort(bitCommits[i]);
cell.appendChild(box);
cell.appendChild(idx);
cell.appendChild(cm);
container.appendChild(cell);
}
document.getElementById('proofPanel').style.display = '';
// Hand proof to the Verifier after a short pause
setTimeout(() => verify(x, maxVal, bits, bitValues, bitCommits), 300);
});
function verify(x, maxVal, bits, bitValues, bitCommits) {
// Verifier checks: all committed values are bits AND x is in [0, maxVal]
const inRange = x >= 0 && x <= maxVal;
// In a real proof the verifier checks the commitment equations; here we simulate the outcome
const allBits = bitValues.every(b => b === 0 || b === 1);
document.getElementById('verifierPanel').style.display = '';
const statusEl = document.getElementById('verifyStatus');
const detailEl = document.getElementById('verifyDetail');
if (inRange && allBits) {
statusEl.className = 'status ok';
statusEl.textContent = '✓ Proof accepted — value is in range';
detailEl.textContent = i18nFmt('Verified %bits% bit-commitments. Confirmed: 0 ≤ x ≤ %maxVal%. The verifier learned nothing about x itself.', { bits, maxVal });
} else {
statusEl.className = 'status bad';
statusEl.textContent = '✗ Proof rejected — value is out of range';
detailEl.textContent = i18nFmt('The value exceeds the claimed bound of %maxVal%. A real prover cannot fake this without breaking discrete log.', { maxVal });
}
}
document.getElementById('btnReset').addEventListener('click', () => {
// Reset demo to initial state
state = {};
document.getElementById('commitPanel').style.display = 'none';
document.getElementById('proofPanel').style.display = 'none';
document.getElementById('verifierPanel').style.display = 'none';
document.getElementById('secret').value = '42';
document.getElementById('rangeMax').value = '255';
document.getElementById('bitRows').innerHTML = '';
});
<!-- Zero-knowledge range proof demo: commit a secret value, then prove it is in range -->
<div class="panel">
<div class="section-label">Prover</div>
<label class="field-label">Secret value (x):
<input id="secret" type="number" min="0" max="255" value="42" title="The secret number you want to prove is in range">
</label>
<label class="field-label">Claimed range:
<select id="rangeMax" title="Choose the upper bound of the range">
<option value="7">0 – 7 (3 bits)</option>
<option value="15">0 – 15 (4 bits)</option>
<option value="31">0 – 31 (5 bits)</option>
<option value="63">0 – 63 (6 bits)</option>
<option value="127">0 – 127 (7 bits)</option>
<option value="255" selected>0 – 255 (8 bits)</option>
</select>
</label>
<button id="btnCommit" type="button">Commit</button>
</div>
<div class="panel commitment-panel" id="commitPanel" style="display:none">
<div class="section-label">Commitment (public)</div>
<div class="commit-row">
<span class="mono" id="commitVal"></span>
<span class="tag">C</span>
</div>
<div class="hint-small">The verifier sees this hash — it binds you to x without revealing x.</div>
<button id="btnProve" type="button">Generate Proof</button>
</div>
<div class="panel proof-panel" id="proofPanel" style="display:none">
<div class="section-label">Bit Decomposition Proof</div>
<div id="bitRows" class="bit-rows"></div>
<div class="hint-small">Each bit of x gets its own commitment. The verifier checks they are all 0 or 1 and reconstruct x.</div>
</div>
<div class="panel verifier-panel" id="verifierPanel" style="display:none">
<div class="section-label">Verifier</div>
<div class="status" id="verifyStatus"></div>
<div class="hint-small" id="verifyDetail"></div>
<button id="btnReset" type="button" class="ghost">Start Over</button>
</div>
/* Layout for the ZK range proof interactive demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.panel { background: #f4f7fa; border: 1px solid #d0dae4; border-radius: 10px;
padding: .8rem 1rem; margin-bottom: .6rem; }
.section-label { font-size: .7rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #5a7088; margin-bottom: .45rem; }
.field-label { display: flex; align-items: center; gap: .5rem; font-size: .9rem;
font-weight: 600; margin-bottom: .45rem; }
input[type=number] { width: 72px; padding: .3rem .4rem; border: 1px solid #b0bec5;
border-radius: 6px; font-size: .95rem; text-align: center; }
select { padding: .3rem .4rem; border: 1px solid #b0bec5; border-radius: 6px;
font-size: .9rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer;
margin-top: .3rem; }
button.ghost { background: #fff; color: #1d3557; }
.commit-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .35rem; }
.mono { font-family: ui-monospace, monospace; font-size: .82rem; color: #1d3557;
background: #dce8f3; padding: .2rem .45rem; border-radius: 5px;
word-break: break-all; }
.tag { font-size: .7rem; font-weight: 700; text-transform: uppercase; background: #e2ecf5;
color: #3a6080; padding: .15rem .4rem; border-radius: 4px; }
.hint-small { font-size: .8rem; color: #555; line-height: 1.4; margin-top: .3rem; }
/* Bit decomposition cells — each bit gets its own Pedersen commitment */
.bit-rows { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .4rem; }
.bit-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.bit-box { width: 34px; height: 34px; border-radius: 6px; display: flex;
align-items: center; justify-content: center; font: 700 14px ui-monospace;
border: 1.5px solid #8da7be; }
.bit-box.b0 { background: #e8eef3; color: #444; }
.bit-box.b1 { background: #1d3557; color: #fff; border-color: #1d3557; }
.bit-idx { font-size: .6rem; color: #7a8fa0; }
.commit-mini { font-family: ui-monospace; font-size: .7rem; color: #3a6080;
background: #dce8f3; border-radius: 4px; padding: .1rem .3rem;
margin-top: 2px; }
.status { font-size: 1rem; font-weight: 700; padding: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.verifier-panel { background: #f0f9f4; border-color: #b8d8c6; }
.verifier-panel .status.bad { background: #fdf0f0; border-radius: 6px;
padding: .3rem .5rem; }
// Zero-Knowledge Range Proof demo
// We simulate Pedersen commitments and bit-decomposition proofs with toy arithmetic.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pedersen commitment: C = G^x * H^r mod p
// C = x*G + r*H (mod p), simulated with integers mod a large prime
const P = 2147483647; // Mersenne prime used as group order (2^31 - 1, Mersenne prime)
const G = 3; // generator G
const H = 7; // independent generator H (chosen so nobody knows log_G(H))
function modpow(base, exp, mod) {
// Fast modular exponentiation (BigInt for overflow safety)
let result = 1n;
base = BigInt(base) % BigInt(mod);
exp = BigInt(exp);
mod = BigInt(mod);
while (exp > 0n) {
if (exp % 2n === 1n) result = result * base % mod;
exp = exp / 2n;
base = base * base % mod;
}
return Number(result);
}
function pedersen(x, r) {
// Compute Pedersen commitment C = G^x * H^r mod P
return (modpow(G, x, P) * modpow(H, r, P)) % P;
}
function randBlind() {
// Random blinding factor (in a real system this would be cryptographically random)
return Math.floor(Math.random() * 1000000) + 1;
}
// Shared state between Prover and Verifier steps
let state = {};
function hexShort(n) {
// Display a number as a short hex string
return '0x' + n.toString(16).toUpperCase().padStart(8, '0');
}
document.getElementById('btnCommit').addEventListener('click', () => {
// Prover: pick secret x, choose random blinding r, publish C = G^x * H^r mod P
const x = parseInt(document.getElementById('secret').value, 10);
const maxVal = parseInt(document.getElementById('rangeMax').value, 10);
if (isNaN(x) || x < 0) {
alert('Please enter a non-negative integer.');
return;
}
const r = randBlind();
const C = pedersen(x, r);
state = { x, r, C, maxVal };
document.getElementById('commitVal').textContent = hexShort(C);
document.getElementById('commitPanel').style.display = '';
document.getElementById('proofPanel').style.display = 'none';
document.getElementById('verifierPanel').style.display = 'none';
document.getElementById('bitRows').innerHTML = '';
});
document.getElementById('btnProve').addEventListener('click', () => {
// Prover: decompose x into bits, commit each bit separately
const { x, r, C, maxVal } = state;
const bits = Math.round(Math.log2(maxVal + 1));
// Decompose x in binary and create one commitment per bit
const bitValues = [];
const bitBlinds = [];
const bitCommits = [];
for (let i = 0; i < bits; i++) {
const b = (x >> i) & 1;
const rb = randBlind();
const cb = pedersen(b, rb);
bitValues.push(b);
bitBlinds.push(rb);
bitCommits.push(cb);
}
state.bitValues = bitValues;
state.bitBlinds = bitBlinds;
state.bitCommits = bitCommits;
state.bits = bits;
// Render the bit commitments as colored boxes (dark = 1, light = 0)
const container = document.getElementById('bitRows');
container.innerHTML = '';
for (let i = bits - 1; i >= 0; i--) {
const cell = document.createElement('div');
cell.className = 'bit-cell';
const box = document.createElement('div');
box.className = 'bit-box b' + bitValues[i];
box.textContent = bitValues[i];
const idx = document.createElement('div');
idx.className = 'bit-idx';
idx.textContent = '2⁻' + i + ' ' + i18nFmt('(bit %i%)', { i });
const cm = document.createElement('div');
cm.className = 'commit-mini';
cm.textContent = hexShort(bitCommits[i]);
cell.appendChild(box);
cell.appendChild(idx);
cell.appendChild(cm);
container.appendChild(cell);
}
document.getElementById('proofPanel').style.display = '';
// Hand proof to the Verifier after a short pause
setTimeout(() => verify(x, maxVal, bits, bitValues, bitCommits), 300);
});
function verify(x, maxVal, bits, bitValues, bitCommits) {
// Verifier checks: all committed values are bits AND x is in [0, maxVal]
const inRange = x >= 0 && x <= maxVal;
// In a real proof the verifier checks the commitment equations; here we simulate the outcome
const allBits = bitValues.every(b => b === 0 || b === 1);
document.getElementById('verifierPanel').style.display = '';
const statusEl = document.getElementById('verifyStatus');
const detailEl = document.getElementById('verifyDetail');
if (inRange && allBits) {
statusEl.className = 'status ok';
statusEl.textContent = '✓ Proof accepted — value is in range';
detailEl.textContent = i18nFmt('Verified %bits% bit-commitments. Confirmed: 0 ≤ x ≤ %maxVal%. The verifier learned nothing about x itself.', { bits, maxVal });
} else {
statusEl.className = 'status bad';
statusEl.textContent = '✗ Proof rejected — value is out of range';
detailEl.textContent = i18nFmt('The value exceeds the claimed bound of %maxVal%. A real prover cannot fake this without breaking discrete log.', { maxVal });
}
}
document.getElementById('btnReset').addEventListener('click', () => {
// Reset demo to initial state
state = {};
document.getElementById('commitPanel').style.display = 'none';
document.getElementById('proofPanel').style.display = 'none';
document.getElementById('verifierPanel').style.display = 'none';
document.getElementById('secret').value = '42';
document.getElementById('rangeMax').value = '255';
document.getElementById('bitRows').innerHTML = '';
});
Notice the key property: the verifier sees only the commitment and the proof, never the secret. Try entering a value outside the range — the proof fails, and the commitment gives nothing away about the actual number. The proof is zero-knowledge (the verifier learns nothing beyond "it's in range") and succinct (the certificate is tiny compared to checking all possibilities).
Comments
Loading comments...