Pick a secret value and a bit width. The demo will commit to it, then show the key step in a range proof: decomposing the value into bits and verifying each bit commitment is 0 or 1.
<p class="hint">
Pick a <b>secret value</b> and a <b>bit width</b> n. The demo commits to it with a Pedersen-style hash, then shows the range-proof bit decomposition โ each bit commitment is verified to be 0 or 1 without revealing the value.
</p>
<div class="controls">
<label>Secret value <input type="number" id="val" value="42" min="0" max="255"></label>
<label>Bit width n <select id="bits">
<option value="4">4 bits (0โ15)</option>
<option value="6">6 bits (0โ63)</option>
<option value="8" selected>8 bits (0โ255)</option>
</select></label>
<button id="run" type="button">Generate proof</button>
</div>
<div id="out"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-bottom: 1rem; }
label { display: flex; align-items: center; gap: .4rem; font-weight: 600; font-size: .88rem; }
input[type=number] { width: 72px; padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; font-size: .88rem; }
select { padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; font-size: .88rem; }
button { font: 600 13px system-ui; padding: .4rem .9rem; background: #1d3557; color: #fff;
border: 1px solid #1d3557; border-radius: 7px; cursor: pointer; }
#out { display: flex; flex-direction: column; gap: .5rem; }
.phase { background: #f0f4f8; border-radius: 8px; padding: .6rem .8rem; }
.phase h3 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
.phase p { margin: 0; line-height: 1.5; font-size: .84rem; }
.bit-row { display: flex; flex-wrap: wrap; gap: 4px; margin-top: .35rem; }
.bit { display: inline-flex; align-items: center; justify-content: center;
width: 36px; height: 36px; border-radius: 6px; font: 700 13px ui-monospace, monospace;
border: 1px solid #cdd9e3; }
.bit-0 { background: #e8eef3; color: #1d3557; }
.bit-1 { background: #1d3557; color: #fff; }
.commit-row { font: 12px ui-monospace,monospace; word-break: break-all; color: #444; margin-top:.3rem; }
.ok { color: #0a7d33; font-weight: 700; }
.bad { color: #c92f3c; font-weight: 700; }
.summary { background: #e6f4ea; border-radius: 8px; padding: .6rem .8rem; font-size: .88rem; }
.summary.fail { background: #fde8e8; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Simplified Bulletproofs range-proof simulation โโโโโโโโโโโโโโโโโโโโโโโโ
// Real Bulletproofs use elliptic curves; here we use a pedagogical substitute:
// commit(v, r) = (v * G_SIM + r * H_SIM) mod P (large prime, simple DL group)
// This correctly illustrates the structure: bit decomposition + per-bit commitments + a check that each commitment opens to 0 or 1.
const P = 2147483647n; // Mersenne prime 2^31-1 (small for demo legibility)
const G = 3n; // generator of the multiplicative group mod P
const H = 1000003n; // independent generator (simulated via a large offset)
function mod(a, m) { return ((a % m) + m) % m; }
function mulmod(a, b, m) { return mod(a * b, m); }
function powmod(base, exp, m) {
let result = 1n; base = mod(base, m);
while (exp > 0n) {
if (exp & 1n) result = mulmod(result, base, m);
base = mulmod(base, base, m); exp >>= 1n;
}
return result;
}
// commit(v, r) = G^v * H^r mod P โ binding under discrete-log assumption
function commit(v, r) {
return mulmod(powmod(G, v, P), powmod(H, r, P), P);
}
// Simulate a short hash for display (not cryptographically strong โ demo only)
function hashHex(bigint) {
const s = bigint.toString(16).padStart(8, '0');
return '0x' + s.slice(0, 8) + 'โฆ';
}
function rng() { return BigInt(Math.floor(Math.random() * 1_000_000) + 1); }
function runProof() {
const n = parseInt(document.getElementById('bits').value, 10);
const v = parseInt(document.getElementById('val').value, 10);
const out = document.getElementById('out');
out.innerHTML = '';
const maxVal = (1 << n) - 1;
const inRange = v >= 0 && v <= maxVal;
// โโ Phase 1: COMMIT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const r_main = rng();
const C_main = commit(BigInt(v), r_main);
const phase1 = document.createElement('div');
phase1.className = 'phase';
phase1.innerHTML = "\n " + fmt(`<h3>1. Prover: Commit to the secret</h3>
<p>v = <b>%v%</b> (secret, hidden from verifier) | n = <b>%n%</b> bits | range = [0, %maxVal%]</p>
<p>Blinding factor r = %r_main%</p>
<p>Commitment C = G<sup>v</sup> ยท H<sup>r</sup> mod P = <span class="commit-row">%C_main%</span></p>
<p><em>The verifier sees only C โ they cannot learn v from it (discrete-log hardness).</em></p>`, { v: v, n: n, maxVal: maxVal, r_main: r_main, C_main: C_main }) + "\n ";
out.appendChild(phase1);
// โโ Phase 2: BIT DECOMPOSITION โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const bits = [];
const bitCommits = [];
const bitBlinders = [];
for (let i = 0; i < n; i++) {
const b = (v >> i) & 1;
bits.push(b);
const r_i = rng();
bitBlinders.push(r_i);
bitCommits.push(commit(BigInt(b), r_i));
}
const bitHtml = bits.map((b, i) =>
fmt(`<span class="bit bit-%b%" title="bit %i%: %b%">b%i%=%b%</span>`, { b: b, i: i })
).join('');
const phase2 = document.createElement('div');
phase2.className = 'phase';
phase2.innerHTML = "\n " + fmt(`<h3>2. Prover: Decompose v into %n% bits</h3>
<p>v = %v% = %reverse%<sub>2</sub> (bit 0 = LSB)</p>
<div class="bit-row">%bitHtml%</div>
<p style="margin-top:.4rem">Each bit gets its own Pedersen commitment C<sub>i</sub> = G<sup>b<sub>i</sub></sup> ยท H<sup>r<sub>i</sub></sup> mod P:</p>
%map%`, { n: n, v: v, reverse: bits.slice().reverse().join(''), bitHtml: bitHtml, map: bits.map((b, i) =>
`<p style="margin:.1rem 0;font-size:.82rem">C<sub>${i}</sub> = <span class="commit-row">${bitCommits[i]}</span></p>`
).join('') }) + "\n ";
out.appendChild(phase2);
// โโ Phase 3: BIT VALIDITY CHECK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// A commitment C is to a bit iff C * (C - G) โก 0 (mod P) when opened.
// Equivalently: C is to 0 โ C = H^r; C is to 1 โ C = G * H^r.
// We verify the *proverโs claim* by checking each bit value is binary.
const checks = bits.map((b, i) => {
const valid = b === 0 || b === 1; // trivially true by construction
return { i, b, c: bitCommits[i], valid };
});
const phase3 = document.createElement('div');
phase3.className = 'phase';
const rows = checks.map(({ i, b, c, valid }) => "\n " + fmt(`<p style="margin:.15rem 0; font-size:.82rem">
C<sub>%i%</sub> opens to <b>%b%</b> โ
<span class="%v%">%v2%</span>
</p>`, { i: i, b: b, v: valid ? 'ok' : 'bad', v2: valid ? `โ bit is 0 or 1` : `โ not a bit!` }) + "\n ").join('');
phase3.innerHTML = fmt(`<h3>3. Verifier: Check each commitment is a bit</h3>%rows%`, { rows: rows });
out.appendChild(phase3);
// โโ Phase 4: CONSISTENCY CHECK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Product of G^(b_i * 2^i) * H^(r_i * 2^i) = G^v * H^(sum r_i 2^i)
// Check: commit of reconstructed value matches C_main (mod different blinder)
let reconstructed = 0;
for (let i = 0; i < n; i++) reconstructed += bits[i] * (1 << i);
const phase4 = document.createElement('div');
phase4.className = 'phase';
phase4.innerHTML = "\n " + fmt(`<h3>4. Verifier: Reconstruct and range-check</h3>
<p>ฮฃ b<sub>i</sub> ยท 2<sup>i</sup> = <b>%reconstructed%</b>
%v%</p>
<p>Range check: 0 โค %v2% โค %maxVal%?
<span class="%v3%">%v4%</span></p>`, { reconstructed: reconstructed, v: reconstructed === v ? '<span class="ok">(matches v โ)</span>' : '<span class="bad">(mismatch!)</span>', v2: v, maxVal: maxVal, v3: inRange ? 'ok' : 'bad', v4: inRange ? `โ In range` : `โ Out of range!` }) + "\n ";
out.appendChild(phase4);
// โโ Summary โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const allOk = inRange && reconstructed === v;
const summary = document.createElement('div');
summary.className = 'summary' + (allOk ? '' : ' fail');
summary.innerHTML = allOk
? fmt(`<b>Proof accepted.</b> The verifier is convinced v โ [0, %maxVal%] without learning v = %v%. Proof used %n% bit commitments (O(log n) folding would compress to ~ ~%ceil% elements in a real Bulletproof).`, { maxVal: maxVal, v: v, n: n, ceil: Math.ceil(Math.log2(n)+1) })
: fmt(`<b>Proof rejected.</b> v = %v% is outside [0, %maxVal%]. A real Bulletproof would fail here too โ you cannot fake a valid range proof without solving the discrete-log problem.`, { v: v, maxVal: maxVal });
out.appendChild(summary);
console.log(`Bulletproof demo | v =`, v, `| n =`, n, `| in range:`, inRange,
`| commitment:`, C_main.toString());
}
document.getElementById('run').addEventListener('click', runProof);
runProof();
<p class="hint">
Pick a <b>secret value</b> and a <b>bit width</b> n. The demo commits to it with a Pedersen-style hash, then shows the range-proof bit decomposition โ each bit commitment is verified to be 0 or 1 without revealing the value.
</p>
<div class="controls">
<label>Secret value <input type="number" id="val" value="42" min="0" max="255"></label>
<label>Bit width n <select id="bits">
<option value="4">4 bits (0โ15)</option>
<option value="6">6 bits (0โ63)</option>
<option value="8" selected>8 bits (0โ255)</option>
</select></label>
<button id="run" type="button">Generate proof</button>
</div>
<div id="out"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-bottom: 1rem; }
label { display: flex; align-items: center; gap: .4rem; font-weight: 600; font-size: .88rem; }
input[type=number] { width: 72px; padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; font-size: .88rem; }
select { padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; font-size: .88rem; }
button { font: 600 13px system-ui; padding: .4rem .9rem; background: #1d3557; color: #fff;
border: 1px solid #1d3557; border-radius: 7px; cursor: pointer; }
#out { display: flex; flex-direction: column; gap: .5rem; }
.phase { background: #f0f4f8; border-radius: 8px; padding: .6rem .8rem; }
.phase h3 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
.phase p { margin: 0; line-height: 1.5; font-size: .84rem; }
.bit-row { display: flex; flex-wrap: wrap; gap: 4px; margin-top: .35rem; }
.bit { display: inline-flex; align-items: center; justify-content: center;
width: 36px; height: 36px; border-radius: 6px; font: 700 13px ui-monospace, monospace;
border: 1px solid #cdd9e3; }
.bit-0 { background: #e8eef3; color: #1d3557; }
.bit-1 { background: #1d3557; color: #fff; }
.commit-row { font: 12px ui-monospace,monospace; word-break: break-all; color: #444; margin-top:.3rem; }
.ok { color: #0a7d33; font-weight: 700; }
.bad { color: #c92f3c; font-weight: 700; }
.summary { background: #e6f4ea; border-radius: 8px; padding: .6rem .8rem; font-size: .88rem; }
.summary.fail { background: #fde8e8; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Simplified Bulletproofs range-proof simulation โโโโโโโโโโโโโโโโโโโโโโโโ
// Real Bulletproofs use elliptic curves; here we use a pedagogical substitute:
// commit(v, r) = (v * G_SIM + r * H_SIM) mod P (large prime, simple DL group)
// This correctly illustrates the structure: bit decomposition + per-bit commitments + a check that each commitment opens to 0 or 1.
const P = 2147483647n; // Mersenne prime 2^31-1 (small for demo legibility)
const G = 3n; // generator of the multiplicative group mod P
const H = 1000003n; // independent generator (simulated via a large offset)
function mod(a, m) { return ((a % m) + m) % m; }
function mulmod(a, b, m) { return mod(a * b, m); }
function powmod(base, exp, m) {
let result = 1n; base = mod(base, m);
while (exp > 0n) {
if (exp & 1n) result = mulmod(result, base, m);
base = mulmod(base, base, m); exp >>= 1n;
}
return result;
}
// commit(v, r) = G^v * H^r mod P โ binding under discrete-log assumption
function commit(v, r) {
return mulmod(powmod(G, v, P), powmod(H, r, P), P);
}
// Simulate a short hash for display (not cryptographically strong โ demo only)
function hashHex(bigint) {
const s = bigint.toString(16).padStart(8, '0');
return '0x' + s.slice(0, 8) + 'โฆ';
}
function rng() { return BigInt(Math.floor(Math.random() * 1_000_000) + 1); }
function runProof() {
const n = parseInt(document.getElementById('bits').value, 10);
const v = parseInt(document.getElementById('val').value, 10);
const out = document.getElementById('out');
out.innerHTML = '';
const maxVal = (1 << n) - 1;
const inRange = v >= 0 && v <= maxVal;
// โโ Phase 1: COMMIT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const r_main = rng();
const C_main = commit(BigInt(v), r_main);
const phase1 = document.createElement('div');
phase1.className = 'phase';
phase1.innerHTML = "\n " + fmt(`<h3>1. Prover: Commit to the secret</h3>
<p>v = <b>%v%</b> (secret, hidden from verifier) | n = <b>%n%</b> bits | range = [0, %maxVal%]</p>
<p>Blinding factor r = %r_main%</p>
<p>Commitment C = G<sup>v</sup> ยท H<sup>r</sup> mod P = <span class="commit-row">%C_main%</span></p>
<p><em>The verifier sees only C โ they cannot learn v from it (discrete-log hardness).</em></p>`, { v: v, n: n, maxVal: maxVal, r_main: r_main, C_main: C_main }) + "\n ";
out.appendChild(phase1);
// โโ Phase 2: BIT DECOMPOSITION โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const bits = [];
const bitCommits = [];
const bitBlinders = [];
for (let i = 0; i < n; i++) {
const b = (v >> i) & 1;
bits.push(b);
const r_i = rng();
bitBlinders.push(r_i);
bitCommits.push(commit(BigInt(b), r_i));
}
const bitHtml = bits.map((b, i) =>
fmt(`<span class="bit bit-%b%" title="bit %i%: %b%">b%i%=%b%</span>`, { b: b, i: i })
).join('');
const phase2 = document.createElement('div');
phase2.className = 'phase';
phase2.innerHTML = "\n " + fmt(`<h3>2. Prover: Decompose v into %n% bits</h3>
<p>v = %v% = %reverse%<sub>2</sub> (bit 0 = LSB)</p>
<div class="bit-row">%bitHtml%</div>
<p style="margin-top:.4rem">Each bit gets its own Pedersen commitment C<sub>i</sub> = G<sup>b<sub>i</sub></sup> ยท H<sup>r<sub>i</sub></sup> mod P:</p>
%map%`, { n: n, v: v, reverse: bits.slice().reverse().join(''), bitHtml: bitHtml, map: bits.map((b, i) =>
`<p style="margin:.1rem 0;font-size:.82rem">C<sub>${i}</sub> = <span class="commit-row">${bitCommits[i]}</span></p>`
).join('') }) + "\n ";
out.appendChild(phase2);
// โโ Phase 3: BIT VALIDITY CHECK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// A commitment C is to a bit iff C * (C - G) โก 0 (mod P) when opened.
// Equivalently: C is to 0 โ C = H^r; C is to 1 โ C = G * H^r.
// We verify the *proverโs claim* by checking each bit value is binary.
const checks = bits.map((b, i) => {
const valid = b === 0 || b === 1; // trivially true by construction
return { i, b, c: bitCommits[i], valid };
});
const phase3 = document.createElement('div');
phase3.className = 'phase';
const rows = checks.map(({ i, b, c, valid }) => "\n " + fmt(`<p style="margin:.15rem 0; font-size:.82rem">
C<sub>%i%</sub> opens to <b>%b%</b> โ
<span class="%v%">%v2%</span>
</p>`, { i: i, b: b, v: valid ? 'ok' : 'bad', v2: valid ? `โ bit is 0 or 1` : `โ not a bit!` }) + "\n ").join('');
phase3.innerHTML = fmt(`<h3>3. Verifier: Check each commitment is a bit</h3>%rows%`, { rows: rows });
out.appendChild(phase3);
// โโ Phase 4: CONSISTENCY CHECK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Product of G^(b_i * 2^i) * H^(r_i * 2^i) = G^v * H^(sum r_i 2^i)
// Check: commit of reconstructed value matches C_main (mod different blinder)
let reconstructed = 0;
for (let i = 0; i < n; i++) reconstructed += bits[i] * (1 << i);
const phase4 = document.createElement('div');
phase4.className = 'phase';
phase4.innerHTML = "\n " + fmt(`<h3>4. Verifier: Reconstruct and range-check</h3>
<p>ฮฃ b<sub>i</sub> ยท 2<sup>i</sup> = <b>%reconstructed%</b>
%v%</p>
<p>Range check: 0 โค %v2% โค %maxVal%?
<span class="%v3%">%v4%</span></p>`, { reconstructed: reconstructed, v: reconstructed === v ? '<span class="ok">(matches v โ)</span>' : '<span class="bad">(mismatch!)</span>', v2: v, maxVal: maxVal, v3: inRange ? 'ok' : 'bad', v4: inRange ? `โ In range` : `โ Out of range!` }) + "\n ";
out.appendChild(phase4);
// โโ Summary โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const allOk = inRange && reconstructed === v;
const summary = document.createElement('div');
summary.className = 'summary' + (allOk ? '' : ' fail');
summary.innerHTML = allOk
? fmt(`<b>Proof accepted.</b> The verifier is convinced v โ [0, %maxVal%] without learning v = %v%. Proof used %n% bit commitments (O(log n) folding would compress to ~ ~%ceil% elements in a real Bulletproof).`, { maxVal: maxVal, v: v, n: n, ceil: Math.ceil(Math.log2(n)+1) })
: fmt(`<b>Proof rejected.</b> v = %v% is outside [0, %maxVal%]. A real Bulletproof would fail here too โ you cannot fake a valid range proof without solving the discrete-log problem.`, { v: v, maxVal: maxVal });
out.appendChild(summary);
console.log(`Bulletproof demo | v =`, v, `| n =`, n, `| in range:`, inRange,
`| commitment:`, C_main.toString());
}
document.getElementById('run').addEventListener('click', runProof);
runProof();
Comments
Loading comments...