Introduction

Imagine you want to prove your bank balance is above zero โ€” but you refuse to show anyone the actual number. Can you do it? In everyday life, you'd rely on a trusted bank to vouch for you. In cryptography, you can do it alone, with mathematics.

Bulletproofs are a family of zero-knowledge range proofs introduced by Bรผnz, Bootle, Boneh, Poelstra, Wuille, and Maxwell in 2017. They answer a precise question: given a commitment (a cryptographic lock that hides a number), can you convince a verifier that the hidden value lies in the range [0, 2n2^{n}) โ€” without revealing the value itself, and without needing any trusted party to set up the system?

The answer is yes, and the proof is remarkably short: only O(log n) elements, compared to the O(n)O(n) you'd need naively. That brevity is what puts the "bullet" in Bulletproofs โ€” small enough to fit inside a blockchain transaction.

The core building block is the Pedersen commitment: C = vยทG + rยทH, where G and H are public elliptic-curve points, v is the secret value, and r is a random blinding factor. C hides v perfectly (you can't learn v from C alone), yet it is binding โ€” once you publish C, you cannot later claim it commits to a different value. The range proof then shows, without opening C, that 0 โ‰ค v < 2n2^{n}.

Try the Range Proof

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) &nbsp;|&nbsp; n = <b>%n%</b> bits &nbsp;|&nbsp; 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> &nbsp;(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> &nbsp;โ†’&nbsp;
      <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();
Result
Console

Notice the two roles. The prover knows the secret; they produce bit commitments and prove each one is binary. The verifier only sees the commitments โ€” they can check every claim without ever learning the hidden number. This asymmetry โ€” easy to verify, hard to fake โ€” is the engine of zero-knowledge proofs and connects directly to the hardness studied in factoring and discrete logarithms.

The Real Complexity

Bulletproofs are not magic โ€” their security and efficiency come from a precise chain of ideas.

Status: proven secure under the discrete-logarithm assumption (Bรผnz et al., 2017). There is no trusted setup, no trapdoor that a ceremony participant could exploit.

  • Naive range proof: to show v < 2n2^{n}, commit to each of the n bits of v separately and prove each commitment is 0 or 1. That works, but produces n proof elements โ€” too large for a blockchain.
  • Inner product argument: the key insight is that the n bit-checks can be folded recursively into a single inner-product relation. Each folding step halves the problem, turning O(n)O(n) proof elements into O(logโกn)O(\log n). The prover and verifier run a O(log n)-round interactive protocol, which is then made non-interactive via the Fiatโ€“Shamir heuristic (replacing the verifier's random challenges with a hash).
  • Proof size: for n = 64 bits, a Bulletproof is about 600 bytes โ€” roughly 16ร— smaller than earlier range proofs. Aggregating m proofs costs only an extra O(logโกm)O(\log m) rather than O(mโ‹…n)O(m \cdot n).
  • Verification cost: O(n)O(n) scalar multiplications on an elliptic curve. Slower than checking a simple signature, but fast enough for real transaction validation.
  • Soundness: a cheating prover who commits to v โ‰ฅ 2n2^{n} cannot pass the verification without solving the discrete-logarithm problem on the chosen curve โ€” believed to require exponential time with today's algorithms.

The construction is a beautiful example of recursive proof compression: a hard problem (prove a range) is transformed into a linear algebra relation, then squashed logarithmically with no loss of soundness.

Where It Matters

A proof that a hidden number is non-negative turns out to be the missing piece in many privacy-preserving systems:

  • Confidential transactions: Monero adopted Bulletproofs in 2018 (replacing earlier ring-CT range proofs), cutting average transaction size by ~80% and fees dramatically. The Mimblewimble protocol uses them in Grin and Beam for fully private payment channels.
  • Verifiable computation: the inner-product argument at the heart of Bulletproofs generalizes to proving arbitrary arithmetic circuits, not just range checks. This makes Bulletproofs a building block for more expressive zero-knowledge virtual machines.
  • Anonymous credentials: showing "my age is โ‰ฅ 18" without revealing your birthdate is a range proof. Bulletproofs make such credentials compact enough for real identity systems.
  • Threshold signatures and MPC: range proofs prevent participants in a multi-party computation from contributing out-of-range inputs that could bias the result.
  • Regulatory compliance: a company can prove a financial quantity (e.g., a reserve ratio) exceeds a threshold without publishing its exact books โ€” auditability without full disclosure.

Bulletproofs sit in the broader family of succinct non-interactive proofs. Related schemes like Groth16 and PLONK offer constant-size proofs but require a trusted setup; SNARKs based on pairings trade setup cost for even shorter proofs. Bulletproofs occupy the sweet spot of no setup, logarithmic size โ€” which is why they dominate in open, permissionless blockchains.

Conclusion

Bulletproofs crystallize a remarkable idea: you can convince a stranger that a hidden number satisfies a constraint, while revealing nothing else. The proof is short enough to live on a blockchain, secure under a standard cryptographic assumption, and requires no ceremony or trusted third party.

The technique โ€” fold a big problem into a smaller one, repeat until trivial, then collapse the interaction with a hash โ€” is a template that keeps reappearing in modern cryptography. Understanding Bulletproofs is a gateway to the richer world of zero-knowledge proofs and the computational hardness results that make privacy possible.

Privacy and verifiability, once thought to be opposites, turn out to coexist โ€” as long as the discrete-logarithm problem stays hard.

Share this article

Pick a channel โ€” or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/bulletproofs/Content licensed under CC BY-NC 4.0.