☰

Bulletproofs

In Layman's terms

Before we start, let's recall our goal with rangeproofs: prove that the amount that we are sending lies between 0 and 264−1 atomic units (piconeros) (and consequently it is not negative as well).

Explaining Bulletproofs (BP) in layman's terms is not an easy task so let's do justice to the saying "a picture is worth a thousand words" to get the motivations to do it. Below you will find an illustration of a standard one input and two outputs transaction using confidential transactions versus bulletproofs.

Fig. 1 - Comparison between Borromean and Bulletproof rangeproofs.

As you remember from Borromean signatures, we had to store one commitment Ci for each bit i of the amount, but what if we could prove that we know all the Ci values without actually storing all of them? This is the main idea of Bulletproofs. We will do some math tricks to prove that we know a very long vector without actually storing it.

Math behind

Before I start, I would like to emphasize that it is only my understanding of how Bulletproofs work and it does not cover all its aspects and it may not have the depth that you are looking for. There are actually much better materials available, from which I built my understanding of BP. Please look at the Resources page to explore them. With that said, let's start just another text from someone trying to introduce BP.

If we look at the "ecdhInfo" of a transaction, we will see that the amount information is public stored at the blockchain (not explicitly but as a commitment). Let's start from there. Suppose we have a transaction with commitment C=xG+vH where x is the blinding factor and v is the amount. Our objective with Bulletproofs (a form of rangeproofs) is to prove, in a very compact way, without revealing v, that it lies between 0 and 2n−1 where n=64 in Monero.

Let's start structuring the problem in math terms. We want to prove that 0<v<2n−1 and in order to do so, it suffices to prove that the following three statements are true:

  1. v=⟨𝐚L,2n⟩ , where 𝐚L is the binary vector representing v and 2n is the power of two vector where n=64
  2. 𝐚R=𝐚L−1n
  3. 𝐚L∘𝐚R=0n
For example, if v=5 and n=3, we have the range of possible values going from 0 to 23−1=7 and the above equations can be easily checked for this case:
  • v=5
  • 2n=[20,21,22]=[1,2,4]
  • 𝐚L=[1,0,1]
  • 𝐚R=[0,−1,0]

We observe that if the vector 𝐚L is not made of 0s and 1s, then the product 𝐚L∘𝐚R won't be 0. Now we just need to find a way to combine these three statements into one so we can simply verify whether that new equation created holds. Let's start by slightly modifying 2) and 3) with the goal of letting it as a dot product that is equal to 0.

We will make use of the Schwartz-Zippel lemma which roughly states that the probability of finding a root for a polynome of order n between all the possible numbers N is roughly n/N. As we are playing with modular arithmetics, N is pretty big and finding a root is very unlikely unless all the coefficients are zero. (For a random y different from 0 and given the polynome P(y)=a0+a1y1+a2y2+...+anyn, P(y)=0 if a0=a1=a2=...=an=0 with a negligible probability otherwise.)

  1. v=⟨𝐚L,2n⟩ , where 𝐚L is the binary vector representing v and 2n is the power of two vector where n=64
  2. 𝐚R=𝐚L−1n⟶⟨𝐚L−1n−𝐚R,𝐲n⟩=0
  3. 𝐚L∘𝐚R=0n⟶⟨𝐚L∘𝐚R,𝐲n⟩=0
We did this manipulation to have all the terms equal to zero and as dot products. Now we can manipulate a bit this terms to find an equation that would only be valid if all these equations are true. Let's use again the polynome trick to represent this single equation as it is a valid assumption to make as we want to have linearly indepent (LI) terms. We can multiply each term by another random scalar, called z, and group them into one equation. As we want to have LI terms again and we have only three terms, we need a polynome of order 2. Let's do it:
  1. vz2=⟨𝐚L,2n⟩z2
  2. ⟨𝐚L−1n−𝐚R,𝐲n⟩z=0z=0
  3. ⟨𝐚L∘𝐚R,𝐲n⟩=0

Now let's rewrite the terms as a polynomial equation: vz2+0+0=⟨𝐚L,2n⟩z2+⟨𝐚L−1n−𝐚R,𝐲n⟩z+⟨𝐚L∘𝐚R,𝐲n⟩

How to expand the equation and find delta

Applying some math manipulations, it is possible to achieve the independent term as we want (see Dalek's equations):

Finally, we want to combine these terms into a single inner product. Our goal is to rearrange the inner product above so that terms involving 𝐚L appear only on the left-hand side, terms involving 𝐚R appear only on the right-hand side, and non-secret terms (which the verifier can compute on its own) are factored out into a new term δ(y,z).

First, break the statement into simpler terms, then rearrange: z2v=z2⟨𝐚L,2n⟩+z⟨𝐚L,𝐲n⟩−z⟨𝐚R,𝐲n⟩−z⟨1,𝐲n⟩+⟨𝐚L,𝐚R∘𝐲n⟩(1)z2v+z⟨1,𝐲n⟩=z2⟨𝐚L,2n⟩+z⟨𝐚L,𝐲n⟩−z⟨1,𝐚R∘𝐲n⟩+⟨𝐚L,𝐚R∘𝐲n⟩(2)z2v+z⟨1,𝐲n⟩=⟨𝐚L,z22n⟩+⟨𝐚L,z𝐲n⟩+⟨−z1,𝐚R∘𝐲n⟩+⟨𝐚L,𝐚R∘𝐲n⟩(3)z2v+z⟨1,𝐲n⟩=⟨𝐚L,z22n+z𝐲n+𝐚R∘𝐲n⟩+⟨−z1,𝐚R∘𝐲n⟩(4) To combine the terms on the right-hand side, add ⟨−z1,z22n+z𝐲n⟩ to each side, then simplify: z2v+z⟨1,𝐲n⟩−⟨z1,z22n+z𝐲n⟩=⟨𝐚L,z22n+z𝐲n+𝐚R∘𝐲n⟩(1)+⟨−z1,z22n+z𝐲n+𝐚R∘𝐲n⟩(2)z2v+(z−z2)⟨1,𝐲n⟩−z3⟨1,2n⟩=⟨𝐚L−z1,z22n+z𝐲n+𝐚R∘𝐲n⟩(3) Combining all non-secret terms outside the inner product δ(y,z)=(z−z2)⟨1,𝐲n⟩−z3⟨1,2n⟩, we finally obtain z2v+δ(y,z)=⟨𝐚L−z1,𝐲n∘(𝐚R+z1)+z22n⟩. This is equivalent to the original inner-product equation, but has a single inner product with 𝐚L on the left, 𝐚R on the right, and non-secret terms factored out. Let's call the left-hand side of the single inner product equation "unblinded" 𝐥(x) and the right-hand side "unblinded" 𝐫(x), such that unblinded 𝐥(x)=𝐚L−z1(1)unblinded 𝐫(x)=𝐲n∘(𝐚R+z1)+z22n(2)z2v+δ(y,z)=⟨unblinded 𝐥(x),unblinded 𝐫(x)⟩(3)

After some math and rearranging the terms in order to have a dot product between terms only dependent on 𝐚L and 𝐚R, we achieve the following equation: z2v+δ(y,z)=⟨𝐚L−z1,𝐲n∘(𝐚R+z1)+z22n⟩

Let's now name the left side of the equation unblinded 𝐥(𝐚L,z) and observe that it is only a function of 𝐚L while the right side is now named unblinded 𝐫(𝐚R,y,z) and is a function of 𝐚R (the result of the inner product is a scalar).

z2v+δ(y,z)=⟨unblinded 𝐥,unblinded 𝐫⟩

We name these terms unblinded because we cannot send them to the verifier without exposing 𝐚L and 𝐚R. If we did so, we would reveal the value v that we are trying to hide. So, in order to make it zero-knowledge, we need to blind these terms and make commitments to them. Let's start by rewritting the left and right side of our dot product with a blinding vector defined as 𝐥(x) and 𝐫(x) as:

𝐥(x)=𝐥0+𝐥1x=(𝐚L+𝐬Lx)−z1 𝐫(x)=𝐫0+𝐫1x=𝐲n∘((𝐚R+𝐬Rx)+z1)+z22n

Now we have our inner product as one equation and if we can prove that it is true than we would achieve our goal of proving that v lies between 0 and 264−1. Now the tricky and ingenious part comes to play. Our inner product is a function of aL and aR which are pretty big vectors (128 scalars each) and we don't want to store them in the blockchain, otherwise we would not get any advantage in relation to the previous Sigma protocol or Confidential Transactions that we had before. The idea here is to find a way to store the proofs in a manner that it scales under O(n) in space (which is the case of Confidential Transactions). It turns out that it is possible to do it in O(2log2n), which is a great improvement! For instance, for a vector of n=128 scalars, we could store only 14 scalars.

Let's see how Monero specifically does it by first considering the general ideas and equations and then a small example to understand it.

We will use the method 'Divide and Conquer' so at each step we will reduce our problem into a similar problem but with a smaller size until we reach a trivial solution and we can propagate back the final result to solve the big problem.

So, in the beginning, we have our vectors 𝐥 and 𝐫 and its inner product t=⟨𝐥,𝐫⟩.

Let's define P as a function of 𝐥 , 𝐫 and t=⟨𝐥,𝐫⟩. Where U is a point known by the verifier and prover.

P=𝐚𝐆+𝐛𝐇+⟨𝐚,𝐛⟩U

Notice that this is the main equation that we want to prove its validity. If this equation holds, then we know that the creator knows 𝐚 and 𝐛 and its inner product. (Moreover, we also know that 𝐚L is made of bits and v lies on the desired range). Our goal will be to store less data than 𝐚 and 𝐛 themselves while letting the verifier know that this equation is true. Only the value P will be stored in the blockchain.

Now let's define the variables that will be in the recursion, where x is a scalar that depends on 𝐋 and 𝐑 and the sub-indices l and h represent the lower and higher part of the vector when split into two parts, respectively:

cL=⟨𝐚l,𝐛h⟩ cR=⟨𝐚h,𝐛l⟩ L=𝐚l𝐆h+𝐛h𝐇l+cLU R=𝐚h𝐆l+𝐛l𝐇h+cRU 𝐆′=𝐆lx−1+𝐆hx 𝐇′=𝐇lx+𝐇hx−1 𝐚′=𝐚lx+𝐚hx−1 𝐛′=𝐛lx−1+𝐛hx Notice again that we are trying to find a way to store as less information as possible in the blockchain while maintaining a solid mathematical proof of the inner product c=⟨𝐚,𝐛⟩, where 𝐚=𝐥, 𝐛=𝐫 and c=t initially. According to the above definitions, let's see how we can express c′ and P′: c′=⟨𝐚′,𝐛′⟩=⟨𝐚lx+𝐚hx−1,𝐛lx−1+𝐛hx⟩=⟨𝐚l,𝐛l⟩+⟨𝐚h,𝐛h⟩⏟⟨𝐚,𝐛⟩+⟨𝐚l,𝐛h⟩⏟cLx2+⟨𝐚h,𝐛l⟩⏟cRx−2=c+cLx2+cRx−2 P′=𝐚′𝐆′+𝐛′𝐇′+⟨𝐚′,𝐛′⟩U Let's start by expressing 𝐚′𝐆′ , 𝐛′𝐇′ and ⟨𝐚′,𝐛′⟩ separately. 𝐚′𝐆′=(𝐚lx+𝐚hx−1)(𝐆lx−1+𝐆hx)=𝐚l𝐆l+𝐚h𝐆h+𝐚l𝐆hx2+𝐚h𝐆lx−2 𝐛′𝐇′=(𝐛lx−1+𝐛hx)(𝐇lx+𝐇hx−1)=𝐛l𝐇l+𝐛h𝐇h+𝐛l𝐇hx−2+𝐛h𝐇lx2 ⟨𝐚′,𝐛′⟩U=(c+cLx2+cRx−2)U Now let's try to express everything together to see how P′ looks. P′=𝐚l𝐆l+𝐚h𝐆h⏟𝐚𝐆+𝐛l𝐇l+𝐛h𝐇h⏟𝐛𝐇+𝐚l𝐆hx2+𝐛h𝐇lx2+cLx2U⏟Lx2+𝐚h𝐆lx−2+𝐛l𝐇hx−2+cRx−2U⏟Rx−2+cU⏟⟨𝐚,𝐛⟩ P′=𝐚𝐆+𝐛𝐇+⟨𝐚,𝐛⟩U⏟P+Lx2+Rx−2 P′=P+Lx2+Rx−2

Let's take a step back and look what we have accomplished.

  1. We started storing P in the blockchain (which is the commitment to 𝐚,𝐛 and our inner product).
  2. At each round we stored the points L and R (typically for a vector containing 128 scalars, we will make 7 rounds and store 7 values for L and R each) and we cut by half the dimension of 𝐚,𝐛,𝐆,𝐇 (without storing them in the blockchain).
  3. By the end of each round, we can compute P′ which depends on the previous P, L and R and known values of x.
  4. In the last round, P′ is trivially calculated as the last dot product c′=⟨𝐚′,𝐛′⟩ will be a number and we will therefore know its value.
  5. We store the last a and b scalars so the verifier can reconstruct the tests (as he knows how to construct 𝐆 and 𝐇) without knowing the whole scalar vectors 𝐚 and 𝐛.
  6. By knowing the last P′, we can reconstruct all previous P and verify that our initial equation holds.

Finally, we can rewrite the above procedure into one equation in order to test if P is true as informed.

L1x12+⋯+Lkxk2+P′+Rkxk−2+⋯+R1x1−2=?aG+bH+abU

Where the scalars a and b and the point vectors L and R are stored in the blockchain. The values of x1⋯xk can be retrieved as a function of L and R for each step as part of the non-interactive Fiat-Shamir protocol.

Finally, we store in the blockchain for the bulletproof range-proofs:

  • V: a vector of points with commitments to v[i] and hiding values γ[i].
  • A: a point which represents the commitment to 𝐚L and 𝐚R and hiding value α.
  • S: a point which represents the commitment to 𝐬L and 𝐬R and hiding value ρ.
  • T1: a point which represents the commitment to t1 and hiding value τ1.
  • T2: a point which represents the commitment to t2 and hiding value τ2.
  • taux: a scalar, which represents the hiding value related to T1, T2, V and t.
  • mu: a scalar, which represents the hiding value related to A and S.
  • 𝐋: a vector of points of size log2(64*outputs) containing the proving steps for the inner product.
  • 𝐑: a vector of points of size log2(64*outputs) containing the proving steps for the inner product.
  • a: a scalar which represents the value of a in the last step of the inner product.
  • b: a scalar which represents the value of b in the last step of the inner product.
  • t: a scalar, which represents the inner product value to be tested.