skip to content
Skill Issue Dev | Dax the Dev
search

Range proofs in 80 lines: Pedersen commitments and a tiny Bulletproof

Print view

Sections

A confidential transaction has to prove one annoying little thing: that the hidden amount is non-negative and bounded. Without that, an attacker can mint coins out of thin air by committing to a “negative balance” that wraps around the field. The cryptographic primitive that does the proving is the range proof, and the question of which range proof to ship in 2026 is — surprisingly — still live.

This post does three things:

  1. Derives the inner-product argument that makes Bulletproofs short.
  2. Walks an 80-line, runnable toy Bulletproof prover/verifier in the browser.
  3. Maps the trade-offs between Bulletproofs, classical range proofs, and SNARK-based range proofs onto the deployment surface I keep hitting in zera-sdk.

It’s a sibling piece to Pedersen commitments, in production. Read that one first if “Pedersen” still feels like a textbook word to you. This one assumes you know what C = a·G + b·H is and want to know what to do with it.

What a range proof has to do

The setup. A prover holds a value vv and a blinding factor γ\gamma, and publishes a Pedersen commitment

V=vG+γHV = v \cdot G + \gamma \cdot H

with G,HG, H independent generators of an elliptic-curve group of prime order qq. The hiding property of VV comes from γ\gamma being uniformly random; the binding property comes from GG and HH being independent (no known β\beta with H=βGH = \beta G).

The prover then wants to convince a verifier that vv lies in some range, typically [0,2n)[0, 2^n) for n=32n = 32 or n=64n = 64. Crucially, vv stays hidden. The verifier learns only the fact that the committed value is in range.

The naive proof of "v[0,2n)v \in [0, 2^n)" is to commit bit-by-bit: write v=i=0n1ai2iv = \sum_{i=0}^{n-1} a_i \cdot 2^i with ai{0,1}a_i \in \{0,1\}, commit to each aia_i, and prove each ai(ai1)=0a_i (a_i - 1) = 0. That works. It takes O(n)O(n) commitments and O(n)O(n) proof size, which is what Confidential Transactions in Bitcoin shipped in 2015 and which is roughly 2.5 KB per transaction at n=64n = 64.

Bünz, Bootle, Boneh, Poelstra, Wuille, and Maxwell (2018) — the Bulletproofs paper — got that down to about 672 bytes, with no trusted setup, by replacing the linear blob with a logarithmic-size inner-product argument. The compression ratio is roughly 4× over the naive bit-commitment scheme, and it gets better as the range grows.

The inner-product argument, derived

The whole game in Bulletproofs is the inner-product argument (IPA). Forget range proofs for a paragraph. The IPA proves the following:

Statement. Given commitments PGP \in \mathbb{G} and G,HGn\mathbf{G}, \mathbf{H} \in \mathbb{G}^n, plus a scalar cFqc \in \mathbb{F}_q, the prover knows vectors a,bFqn\mathbf{a}, \mathbf{b} \in \mathbb{F}_q^n such that

P=a,G+b,Handa,b=c.P = \langle \mathbf{a}, \mathbf{G} \rangle + \langle \mathbf{b}, \mathbf{H} \rangle \quad \text{and} \quad \langle \mathbf{a}, \mathbf{b} \rangle = c.

The naive proof is to send a\mathbf{a} and b\mathbf{b} — that’s 2n2n scalars. The IPA gets it to 2log2n2 \log_2 n group elements plus two scalars.

The trick is recursion. Split each vector in half: a=(aLaR)\mathbf{a} = (\mathbf{a}_L \,|\, \mathbf{a}_R), same for b,G,H\mathbf{b}, \mathbf{G}, \mathbf{H}. The prover sends two cross-terms:

L=aL,GR+bR,HL,R=aR,GL+bL,HR.L = \langle \mathbf{a}_L, \mathbf{G}_R \rangle + \langle \mathbf{b}_R, \mathbf{H}_L \rangle, \quad R = \langle \mathbf{a}_R, \mathbf{G}_L \rangle + \langle \mathbf{b}_L, \mathbf{H}_R \rangle.

The verifier responds with a random challenge xFqx \in \mathbb{F}_q^*. Both parties then compute folded vectors of half the length:

a=xaL+x1aR,b=x1bL+xbR,\mathbf{a}' = x \cdot \mathbf{a}_L + x^{-1} \cdot \mathbf{a}_R, \quad \mathbf{b}' = x^{-1} \cdot \mathbf{b}_L + x \cdot \mathbf{b}_R,

and the verifier folds the generators in the dual direction:

G=x1GL+xGR,H=xHL+x1HR.\mathbf{G}' = x^{-1} \cdot \mathbf{G}_L + x \cdot \mathbf{G}_R, \quad \mathbf{H}' = x \cdot \mathbf{H}_L + x^{-1} \cdot \mathbf{H}_R.

The new commitment is

P=x2L+P+x2R,P' = x^2 \cdot L + P + x^{-2} \cdot R,

and you can check by direct expansion that P=a,G+b,HP' = \langle \mathbf{a}', \mathbf{G}' \rangle + \langle \mathbf{b}', \mathbf{H}' \rangle exactly when the original PP relation held. Recurse on (a,b,G,H,P)(\mathbf{a}', \mathbf{b}', \mathbf{G}', \mathbf{H}', P'). After log2n\log_2 n rounds, the vectors are length 1 and the prover just sends the two remaining scalars.

That’s the entire IPA in seven lines of math. Total proof size: 2log2n2 \log_2 n group elements (the LiL_i and RiR_i from each round) + 2 final scalars. At n=64n = 64, that’s 12 group elements + 2 scalars ≈ 416 bytes.

From IPA to range proof in two reductions

The range proof reduces to the IPA in two steps.

Step 1: bit decomposition as a vector identity. Write v=aL,2nv = \langle \mathbf{a}_L, 2^{\mathbf{n}} \rangle where aL{0,1}n\mathbf{a}_L \in \{0,1\}^n is the bit decomposition and 2n=(1,2,4,,2n1)2^{\mathbf{n}} = (1, 2, 4, \dots, 2^{n-1}). Define aR=aL1n\mathbf{a}_R = \mathbf{a}_L - \mathbf{1}^n (so each aR,i{0,1}a_{R,i} \in \{0, -1\}). The conjunction "v[0,2n)v \in [0, 2^n)" becomes the vector identities

aLaR=0n,aLaR=1n,aL,2n=v.\mathbf{a}_L \circ \mathbf{a}_R = \mathbf{0}^n, \quad \mathbf{a}_L - \mathbf{a}_R = \mathbf{1}^n, \quad \langle \mathbf{a}_L, 2^{\mathbf{n}} \rangle = v.

The first identity (Hadamard product is zero) is exactly the bit constraint ai(ai1)=0a_i (a_i - 1) = 0 rewritten.

Step 2: collapse three vector identities to one inner product. The verifier samples challenges y,zy, z. The prover constructs polynomials

l(X)=(aLz1n)+sLX,\mathbf{l}(X) = (\mathbf{a}_L - z \cdot \mathbf{1}^n) + \mathbf{s}_L \cdot X, r(X)=yn(aR+z1n+sRX)+z22n,\mathbf{r}(X) = \mathbf{y}^n \circ (\mathbf{a}_R + z \cdot \mathbf{1}^n + \mathbf{s}_R \cdot X) + z^2 \cdot 2^{\mathbf{n}},

with sL,sR\mathbf{s}_L, \mathbf{s}_R random blinding vectors. The inner product t(X)=l(X),r(X)t(X) = \langle \mathbf{l}(X), \mathbf{r}(X) \rangle is a quadratic in XX, and the constant term t0t_0 collapses to

t0=z2v+δ(y,z),δ(y,z)=(zz2)1n,ynz31n,2n.t_0 = z^2 \cdot v + \delta(y, z), \quad \delta(y, z) = (z - z^2) \langle \mathbf{1}^n, \mathbf{y}^n \rangle - z^3 \langle \mathbf{1}^n, 2^{\mathbf{n}} \rangle.

The verifier knows δ(y,z)\delta(y, z) (it’s all public scalars) and knows VV (the commitment to vv), so it can check the t0t_0 equation against VV. The prover then runs the IPA on l(x)\mathbf{l}(x) and r(x)\mathbf{r}(x) for a fresh challenge xx, and that is what gets compressed to log2n\log_2 n.

The whole construction is one Pedersen commitment, two challenges, two polynomial-coefficient commitments, and an IPA. It fits in a paragraph and runs in a browser.

The 80-line toy

This is a runnable Bulletproof-style range proof for n=4n = 4 (so v[0,16)v \in [0, 16)). It is intentionally small. It uses scalar arithmetic in a tiny prime field instead of an elliptic-curve group, which means it demonstrates the protocol shape but provides zero cryptographic security. Read it for the algebra, not the hardness.

sandbox [ vanilla-ts ]
run

The shape is the thing. A real Bulletproof replaces my BigInt scalars with elliptic-curve points (typically Ristretto or BN254 G1), runs the IPA recursively to length 1 instead of just one fold, and uses Fiat-Shamir over a transcript that includes every public group element. The protocol stays under 700 bytes for n=64n = 64, and the verifier cost stays at O(n)O(n) multiplications (the prover dominates at O(nlogn)O(n \log n)).

Choosing a range proof in 2026

The trade-off space has settled enough to write down honestly.

OptionCostLatencyBlast radiusNotes
Naive bit-commitment range proof ~2.5 KB at n=64 Prover ~100ms, verifier ~10ms Low risk; well understood since 2015 Confidential Transactions shipped this. Big proofs, but trivial to audit.
Bulletproofs (Bunz et al. 2018) ~672 bytes at n=64; logarithmic in n Prover ~500ms, verifier ~20ms (linear) Battle-tested in Monero, dalek-cryptography No trusted setup. Best choice when you have one or a few range checks per tx.
Bulletproofs+ (Chung-Han-Hwang-Kim-Lee 2020) ~576 bytes at n=64 Prover ~10% faster than BP; verifier ~25% faster Less deployment than original BP Drop-in if you control both ends. Worth it for a fresh deployment.
SNARK-embedded range proof (Groth16 / PLONK) ~200-300 bytes; constant Verifier ~3-5ms (constant); prover dominates Inherits the SNARK's trusted setup story Right answer when you're already paying for a SNARK. zera-sdk does this.
STARK-embedded range proof ~50-200 KB; logarithmic Prover slow, verifier fast Post-quantum, transparent setup Big proofs are the cost. Worth it for batched provers (rollups, not transfers).

The pattern: if you’re already running a SNARK for the privacy proof, embed the range check inside it and pay nothing extra. If you don’t have a SNARK and you want short proofs without a trusted setup, Bulletproofs are the right answer. The naive bit-commitment scheme is what you ship when you don’t trust the cryptanalysis of either and you’re willing to pay 2.5 KB per transaction. STARKs are aspirational for transfers and the right tool for rollups.

In zera-sdk, the range check on amount is a 64-bit decomposition inside the Groth16 transfer circuit. Cost: 64 R1CS constraints (one per bit), zero additional bytes on chain. The Bulletproof would have been 672 bytes per spend, which on Solana at 5,000 lamports per byte adds up faster than the constraint cost in the prover.

What I’d reach for, and when

The framing I keep coming back to: range proofs are a feature of a privacy system, not a product. The product is the privacy pool. The range proof exists because, without it, the pool is exploitable. Pick the one that disappears most quietly into the rest of your system.

For the unified shielded pool on Solana, the SNARK-embedded approach wins for compute units and bytes. For a chain that doesn’t already have a SNARK, Bulletproofs are the line where the cryptography costs roughly the same per-byte on chain as a multisig and you stop arguing about it. For anything post-quantum, STARKs are the only answer — the discrete-log assumption everything else here leans on collapses to a quantum adversary, and the bullet has to be biting.

Bulletproofs greatly improve on the linear (in the bitlength of the range) proof size of confidential transactions. They are also a drop-in replacement for the range proofs used in Monero and other confidential-transaction systems, requiring no trusted setup and relying only on the discrete-logarithm assumption.

Bunz, Bootle, Boneh, Poelstra, Wuille, Maxwell ↗ source

The 80-line toy at the top of this post is the entire algebraic core of that paper, with the elliptic curve removed. Once you see that the inner-product argument is just fold the vector in half, prove a smaller statement, the rest of the construction is bookkeeping.

Further reading

Hire me — book a 30-min call $ book →