Stay in Range: Deeper Into Bulletproofs
Table of Contents
We previously saw how the Bulletproofs Inner Product Argument (IPA) works: it lets us prove that we know a secret vector without revealing it.
That’s neat, but what can we actually do with it?
→ Range proofs!
Bulletproofs are the backbone of modern range proofs: they allow us to prove that a secret value lies within a given range, without revealing the value itself.
A motivating example#
A great use case is confidential transfers.
Imagine you want to send money to a friend, but you don’t want anyone else to see how much. You still need to prove that the transfer makes sense: you’re not sending a negative amount or exceeding your balance.
For instance, let’s say:
- the maximum amount you can transfer is 100
- the maximum balance allowed is 1000
You would produce two range proofs:
- Transfer amount is valid:
0 ≤ amount ≤ 100
- Resulting balance is valid:
0 ≤ balance - amount ≤ 1000
If both hold, your transfer is correct… without revealing the actual numbers.
What are we trying to prove?#
We want to prove that a secret value lies in the range , without revealing .
You could adapt the same logic to any range , but we’ll keep powers of 2, it makes the math cleaner.
As you probably guessed, we’ll reuse our vector machinery and the Inner Product Argument we built earlier.
Notations#
Whenever I use bold, it means I’m talking about a vector.
| Symbol | Definition | Example |
|---|---|---|
vector of length n of successive powers of 2 |
||
vector of n zeros |
||
vector of n ones |
||
vector of length n of successive powers of a random value y |
||
vector n elements, all equal to z ( scaled by ) |
Breaking our secret number into bits#
The key trick in Bulletproofs range proofs is bit decomposition.
We represent the secret value as a sum of its bits.
Let be the vector of bits of .
For example, if :
Then:
That inner product expresses exactly how binary numbers work:
(note: here i’m assuming that is in little endian)
Let’s check with our example (in big-endian form):
Our range proof will revolve around convincing the verifier that:
- this equation holds, and
- each really is a bit (0 or 1)
Convincing the verifier our bits are real 0s and 1s#
We can’t just tell the verifier our bits, that would reveal .
So we need a way to prove that every component of is either 0 or 1.
For a single number , being a bit means:
Only if is 0 or 1 does the equation hold.
For vectors, we use element-wise (Hadamard) multiplication :
So we define a new vector:
and we want to prove that:
which ensures that all entries of are binary.
A clever probabilistic trick#
How can we prove that two hidden vectors multiply to zero without revealing them?
Let’s start with a simple intuition: suppose you want to prove that a secret number equals 0.
If the verifier gives you a random value , and you respond with
(we suppose you can’t cheat, and have to send the actual result of that product)
then unless you’re (really) lucky, the only way that can hold is if .
We apply the same idea to vectors.
The verifier sends a random vector of length .
The prover must show:
If the verifier’s random challenge () makes that true, it’s overwhelmingly likely that the inner product of and really is zero.
To reduce communication, the verifier doesn’t send the whole vector , but just a single random value .
The prover then constructs
is correctly formed#
We have one last issue… we also need to show that was defined honestly:
Easy! We do this by proving another inner product equals zero:
Again, because the verifier choses randomly, the prover can’t fake it.
Recap: everything we must prove#
In summary, the prover needs to prove three relations, each reduced to an inner product that can be verified using the IPA:
Notice that I slightly rearranged the third equation.
It’s equivalent to what we had before:
but the structure of the equation now aligns better with how Bulletproofs commitments are expressed later on.
That’s our full setup! From here, we’ll move on to the heavier algebra that ties everything together.
Combining inner products#
So far, we have three separate inner products to prove.
That’s a lot of work. Ideally, we’d like to bundle them into a single proof that the verifier can check at once.
To do that, we introduce another random challenge from the verifier: .
Then we take a random linear combination of the three equations using powers of :
That way, the prover can’t “tune” each equation independently to cheat, everything must be consistent across the combination.
I’ll skip the full algebraic rearrangement, but if you want the gritty details, check out the excellent Dalek documentation.
I’m not going to detail how we rearrange everything, but if you want the details, head to the great Dalek crate documentation.
After simplification, we obtain a single clean relation:
A few observations make this expression interesting:
- the left part of the inner product is only a function of
- the right part depends only on
- the result depends only on
That separation will make it easy to commit to each side later on.
Finally the term is a constant known to the verifier, so the prover doesn’t need to worry about it.
For completeness, it’s defined as:
From vectors to polynomials#
Up to now, we’ve combined our three inner products into one equation.
But we still can’t send these raw vectors to the verifier. Doing so would leak our secret values and defeat the whole purpose of zero knowledge. So we’ll hide them with blinding factors, and use polynomials to bundle everything together.
Finally we’ll see how the prover uses Pedersen commitments to commit and hide (again) values, while still allowing the verifier to make sure everything checks out.
Hiding our vectors with blinding terms#
The prover introduces two new random vectors: .
They act as noise to mask the real vectors: .
Using them, we define two polynomial vectors that depend on a variable :
When , these polynomials reveal the original expression we care about:
Now look at what happens when we take their inner product:
This is the core equation we ultimately want to prove.
By turning the vectors into polynomials, the prover can now safely reveal at one random point (chosen by the verifier) instead of revealing the full secret vectors.
Taking their inner product#
What happens when we take the inner product of two polynomial vectors?
If we have: and , then:
In other words, the result is a quadratic polynomial.
In our case, we’ll call that polynomial :
The constant term is exactly our target inner product:
To compute the remaining coefficients , we can use a simple Karatsuba trick:
This saves some redundant work: instead of expanding everything term by term, we reuse the existing parts to derive the cross-term .
So from now on, our goals are:
- Prove that is correct: it equals
- Prove that is well formed:
- and are constructed correctly
Committing to everything#
Before the verifier can check anything, the prover must commit to all the relevant values. In a way that’s binding (he can’t change them later) but still hiding (the verifier learns nothing).
These commitments are made to elliptic-curve points and follow a strict order.
Step 1: commit to the secret value #
Using a random blinding factor :
where and are fixed, independent elliptic curve generators.
Step 2: commit to the bit vectors#
Now the prover commits to vectors and , and the blinding vectors and :
where:
- and are vectors of elliptic curve points (one per bit of )
- is another random elliptic curve generator
- and are blinding scalars
Once and are committed, the verifier (or the Fiat-Shamir heuristic) can produce challenges and .
These are then used to define and .
Step 3: commit to #
Finally, the prover commits to the coefficients of : the linear and quadratic terms and , each with its own blinding factor :
These commitments make sure the prover can’t later tweak to make the equation magically work.
Putting it all together: verification#
Everything is almost ready! 🤩
The prover has committed to all the right pieces. Now the verifier just needs to check that everything lines up.
The final challenge#
The verifier sends one last random challenge .
By evaluating the polynomials and at this random point, we can check that the claimed relationships hold with overwhelming probability (thanks to the Schwartz–Zippel lemma).
The prover computes:
Here:
- is the evaluation of at , equal to the inner prod of and
- combines all the blinding factors for
- combines the blinding factors for and
Verifier check #1: the polynomial #
First, the verifier ensures that matches the claimed polynomial evaluation .
Conceptually, it checks that:
and also that the commitment to is consistent with all earlier commitments.
The actual check is:
Why does this hold?
Verifier check #2: vectors and #
Next, the verifier checks that the revealed vectors and are consistent with all previous commitments.
To simplify verification, it rescales the generator vector by the inverse powers of :
This makes the later equations line up nicely.
Then it computes:
and checks that:
If this equality holds, it means that the prover’s and were built correctly.
The Inner Product Argument: the final step#
But wait… the prover can’t just send full vectors and , that would reveal too much information.
This is exactly where the Inner Product Argument comes back in!
To prepare for it, we slightly rearrange the equation above:
and we the inner product result into it:
(where is a random elliptic curve point used for the IPA proof).
This is the input to the IPA: the prover produces a short recursive proof that .
If you need a refresher, checkout out Breaking Down Bulletproofs (part 1) and Unfolding the Bulletproofs Magic (part 2).
What the final proof looks like#
The complete range proof includes:
- and
- and
- and its blinding factor
- (blinding for and )
- the IPA proof, which consists of:
- and vectors, from each folding step
- two scalars representing the final folded vectors and
And that’s it! we’ve just built a complete range proof from the ground up! 🧠
Starting from a simple idea: proving that a hidden value lies between 0 and , we combined a series of powerful tricks:
- Bit decomposition to express the value as a vector of 0s and 1s
- Random challenges () to tie every equation together and prevent cheating
- Polynomial commitments to hide the data while keeping all relationships consistent
- And finally, the Inner Product Argument to verify everything efficiently and compactly
All of that comes together so that a verifier can be convinced that: “the prover’s secret value lies in the correct range”, without learning anything about the value itself.
This is the cryptographic foundation that enables confidential transfers, private balances, and many other privacy-preserving systems, like the one we sketched at the start of this article.
To help you a bit, I implemented the entire process (minus the IPA part) as a Sagemath script: range_proof.sage