6 · Generalized Bulletproofs (GBP)

We finally arrived in our last part in which the prover will fold the secrets in a compact and zero-knowledge proof, while the verifier will get the folded proof and be convinced that the proof is legit. Let's do it from both perspectives. First we will go through all the equations once from the prover side and learn why it becomes zero-knowledge and logarithmic in size.

From the prover side

Let's start seeing the steps that the prover would do (obviously he will not do each step because they are fully developed here for educational purposes only and the prover will jump directly to the last equations in order to create the proof itself).

The full GBP equation again reads as:

WLaL+WRaR+WOaO+WCaC+c=0

We will go through all the steps here to achieve the compact final equations that the prover uses to create the final proof which will be stored in the blockchain and which contains most of the size of the membership proofs. So let's get started with the steps that we need to perform to achieve that:

1 - Squeeze the circuits equation into one number by dotting with (1,y,y2,)

y,aLaRaO=0

2 - Squeeze the GBP in the same way, each row weighted by a power of z. Then fold both into one line. Notice that the equation holds and everything sums to zero.

y,aLaRaO+zz,WLaL+WRaR+WOaO+WCaC+c=0

3 - Push zz through each matrix product and name the results. The verifier could compute all of these himself.

wL=zzWL,wR=zzWR,wO=zzWO,wC=zzWC,wc=zz,c

4 - The second bracket becomes a sum of separate inner products

y,aLaRy,aO+wL,aL+wR,aR+wO,aO+wC,aC+wc=0

5 - Move the constant term to the right, which flips its sign.

y,aLaRy,aO+wL,aL+wR,aR+wO,aO+wC,aC=wc

Now let's collapse the six inner products into three

6 - The two aO terms share a right vector, so combine them: wO,aOy,aO=wOy,aO

y,aLaR+wL,aL+wR,aR+wOy,aO+wC,aC=wc

7 - The first term is a product of two secrets. y is public, so shift it across: y,aLaR=aL,yaR

aL,yaR+wL,aL+wR,aR+wOy,aO+wC,aC=wc

8 - Rewrite the wR term in the same yaR basis: wR,aR=wRy1,yaR

aL,yaR+wL,aL+wRy1,yaR+wOy,aO+wC,aC=wc

9 - Terms one and three now share the right vector yaR, so collect them:

aL+wRy1,yaR+wL,aL+wOy,aO+wC,aC=wc

10 - Add δ(y,z)=y1wR,wL to both sides. It has no witness in it, so the verifier can add it too.

aL+wRy1,yaR+wL,aL+y1wR,wL+wOy,aO+wC,aC=wc+δ

11 - Terms two and three now share wL: wL,aL+y1wR,wL=aL+y1wR,wL

aL+wRy1,yaR+aL+y1wR,wL+wOy,aO+wC,aC=wc+δ

12 - The first two share the left vector aL+wRy1, so collect once more and then only three inner products remain:

aL+wRy1,yaR+wL+wOy,aO+wC,aC=wc+δ

Now let's make the three inner products into one via vector polynomials.

13 - We have a sum of inner products Δ=iLi,Ri that we want to see as a single inner product. Put the left vectors into one vector polynomial and the right vectors into another and this will be the whole trick. Now we manipulate a bit the powers so partners add up to the same total n: give left term i the power pi, and its right partner the power npi:

l(X)=iLiXpi,r(X)=iRiXnpi

14 - Now multiply them out. The inner product of two vector polynomials is the ordinary polynomial you get by taking the inner product of each pair of vector-coefficients and adding the powers

l(X),r(X)=iLiXpi,jRjXnpj=ijLi,RjXpi+npj

15 - Split that double sum into the i=j part and the ij part

=iLi,RiXni=j:pi+npi=n+ijLi,RjXpi+npjij:powern when the p are distinct

16 - Now you can see what happened. Every diagonal pair (i,i) lands on Xn and adds itself to that one coefficient. Every off-diagonal pair lands somewhere else. So reading out the Xn coefficient recovers exactly Δ

[Xn]l(X),r(X)=iLi,Ri=Δ

Just a piece of notation here. [Xm]P(X) means "the coefficient sitting in front of Xm in the polynomial P". For example [X2](7+4X+5X2+9X3)=5. It just reads one coefficient out of a polynomial and throws the rest away.

So finally three inner products became the single number Δ, sitting as one coefficient of one polynomial, and evaluating that polynomial at a random point (step 19) turns it into a single inner product of two ordinary vectors.

17 - Now let's run the recipe of steps 13 to 16 on the exact three inner products of step 12. Each pair becomes one term of l(X) and its partner one term of r(X), at powers chosen to add up to n=2c+2. Two fresh random blinding vectors sL,sR are dropped in at power n+1, so they mask the secrets without ever landing on Xn

l(X)=(aL+y1wR)Xc+1+aOXn+sLXn+1+iaC,iXn1i(1)r(X)=(yaR+wL)Xc+1+(wOy)X0+(ysR)Xn+1+iwC,iX1+i(2)

Match the terms against step 12:

Every partner power sums to n.
[Xn]l(X),r(X)=aL+wRy1,yaR+wL+wOy,aO+wC,aC,

which is exactly the left side of step 12, and step 12 set it equal to wc+δ. That value is what step 19 names tn. The blinds do not disturb it: sL and sR sit at Xn+1, and no term pairs with them to reach Xn (their own diagonal lands at X2n+2), so they hide l and r while leaving the Xn coefficient untouched.

18 - Again, let's check the powers add to n for each wanted pair

(c+1)+(c+1)wires=n,n+0outputs=n,(n1i)+(1+i)commitment i=n

19 - The product l(X),r(X) is a single polynomial. Call it t(X). Its coefficients are the numbers tj, and steps 13 to 18 were arranged so that one of them, the coefficient of Xn, is exactly the value of the whole Generalized Bulletproof equation.

t(X)=l(X),r(X)=jtjXj,tn=δwc

So the entire proof has come down to one claim: the coefficient tn. Every other tj is a byproduct of the multiplication and doesn't mean much (but we need to store them to prove that the whole thing is correct). Now the prover must convince the verifier of that one coefficient without ever handing over l and r.

Before we continue let's look what some symbols represent:

The way to prove a coefficient of a hidden polynomial is to commit everything first, then open at a random x. Steps 20 to 25 do exactly that.

20 - The prover's first message is a batch of Pedersen commitments: everything it must pin down before the opening challenge x, namely the secret values of the circuit, the tree nodes, and the "useless" coefficients of t. In each equation the scalars multiply a generator vector 𝐆k,𝐇k (or a single G for a scalar), plus a blind times H, so the prover is bound to them while revealing nothing:

AI=kaL,k𝐆k+kaR,k𝐇k+αH,AO=kaO,k𝐆k+βH(1)S=ksL,k𝐆k+ksR,k𝐇k+ρH,Ci=kaC,i,k𝐆k+μiH(2)Tj=tjG+τjH(jn)(3)

Term by term:

Two reasons everything is committed rather than sent in the clear. The blinds α,β,ρ,μi,τj (each times H) make every point reveal nothing about its contents, which is the zero-knowledge. And fixing them all before x is drawn is what makes the Fiat-Shamir challenge sound: the prover is pinned to this witness and cannot adapt it to the challenge.

Once those commitments are done, the verifier's challenges are fixed by hashing them, so the prover cannot go back and adjust anything. The table below shows how each challenge is composed. The one we use next is x, drawn right after the Tj.

challengeequation
yy=𝔽((AIAOSch))
zz=𝔽((ch))
xx=𝔽((Tjnch))
xipxip=𝔽((τxut^ch))
round xkxk=𝔽((LkRkch))

21 - Open the polynomial at x. Evaluating l(X),r(X),t(X) at X=x turns l and r back into plain vectors and collapses the polynomial to one number, t^=t(x)=l,r. The prover sends t^ together with the matching blind τx. This τx is the blind of the whole opened value, which means, τx is the blind of all Tj blinds τj recombined at x, namely τx=jτjxj. Because x is settled only after the Tj are fixed, the prover cannot shape the polynomial to fit it.

l=l(x),r=r(x),t^=l,r=t(x),τx=jτjxj

In the proof: t^ is w104 C1 · w178 C2. τx is w102 C1 · w176 C2.

22 - Gather the blinds of the point commitments AI,AO,S,Ci into one scalar u, each weighted by the power its commitment occupies in l(X)

u=αxc+1+βxn+ρxn+1+iμixn1i

In the proof: u is w103 C1 · w177 C2.

The powers of l(X) come from step 17. Its Xc+1 coefficient came from AI (blind α), its Xn from AO (blind β), its Xn+1 from S (blind ρ), and each Xn1i from Ci (blind μi). So u collects the blinds of exactly those four point commitments and nothing else. It leaves out the Tj blinds, which already sit inside τx for check one. So the prover sends u so that check two can subtract uH and cancel all four masks in one shot. That cancellation is the zero-knowledge, since the blinds hide the committed values inside each point and then vanish together in the final equation.

23 - Check one. The goal here is to make sure that t^ is the genuine t(x) whose Xn coefficient equals δwc. t^ is a public value published by the prover in the proof. The verifier rebuilds t(x) as a commitment out of the Tj and one public term on G

t^G+τxH=?xn(δwc)G+jnxjTj

The wc here is the constant from step 3: wc=k=1qzkck. Each ck is the constant term of constraint k and q is the number of constraints. They come from the gadgets that build the circuit, for example the point-on-curve check contributes the curve constant b, the equality and offset gadgets contribute their y0 shifts, the divisor gates their intercept, and so on. A constraint that sets no constant contributes as ck=0. The verifier reweights them by the same constraint powers zk it used everywhere else and sums, which is why wc is a single public number the verifier recomputes on its own.

So this equation checks that the opened scalar t^ is the honest evaluation t(x) whose Xn coefficient is the public value δwc. With that, the prover has locked the coefficient tn and can no longer change it.

24 - Check two. The idea here is to construct a point commitment P that opens as l,r=t^ for the very l,r hidden inside the committed points. First, let's start constructing P, as the verifier would do, with only public information. The idea is to weight each published point by the power of x that its coefficients sit and regroup them by generator.

P=xc+1AI+xnAO+xn+1S+ixn1iCi(1)+kykxc+1wR,k𝐆kk𝐇k+kyk(xc+1wL,k+wO,k+ix1+iwC,i,k)𝐇k(2)uH+xipt^G(3)

The top line weights each committed point AI,AO,S,Ci by the power of x it occupies in l(X). The middle line folds the public weight vectors wL,wR,wO,wC onto the generators 𝐆k,𝐇k. The last line subtracts uH, which cancels the blinds of those four point commitments and glues t^ to G through xip.

Substitute the commitment definitions and every blind cancels, leaving

P=klk𝐆k+kykrk𝐇k+t^U
Step by step: how equation 24 becomes equation 25 (the equation just above)

0. What lk and rk are. In step 17, l(X) and r(X) are vector polynomials: each coefficient of a power Xp is itself a length-n vector, one entry per generator. Opening at the challenge x (step 21) collapses each polynomial to a single vector, l=l(x) and r=r(x). Then lk and rk are just the k-th entries of those two vectors, the parts that ride on 𝐆k and 𝐇k. Reading the coefficients of step 17 component by component:

lk=(aL,k+ykwR,k)xc+1+aO,kxn+sL,kxn+1+iaC,i,kxn1i(1)rk=(ykaR,k+wL,k)xc+1+(wO,kyk)x0+yksR,kxn+1+iwC,i,kx1+i(2)

The index k runs over the n generator slots, not over the powers of X. Everything below rebuilds exactly these two components, lk on 𝐆k and ykrk on 𝐇k.

1. Write out the commitments. Like we did in step 20, just a more compact notation.

AI=aL,𝐆+aR,𝐇+αH,AO=aO,𝐆+βH,S=sL,𝐆+sR,𝐇+ρH,Ci=gi,𝐆+μiH

2. Substitute them into the top line of step 24. Insert those definitions into xc+1AI+xnAO+xn+1S+ixn1iCi and split each term by the generator it lands on

on 𝐆k:xc+1aL,k+xnaO,k+xn+1sL,k+ixn1igi,k(1)on 𝐇k:xc+1aR,k+xn+1sR,k(2)on H:αxc+1+βxn+ρxn+1+iμixn1i(3)

3. Finish the 𝐆k side. The 𝐆k row above is lk missing only the public piece y1wR of l's Xc+1 coefficient. The middle line of step 24 supplies exactly that piece, so the two add up to the full coefficient

(xc+1aL,k+)from the commitments+ykxc+1wR,kfrom the middle line=lkklk𝐆k

4. Finish the 𝐇k side. Everything on 𝐇k is measured against the reweighted generator 𝐇k=yk𝐇k. The commitment parts aR,k,sR,k are for the moment alone but when added to the public parts wL,wO,wC from the middle line, each already carrying its yk, then we can see the rk forming:

xc+1aR,k+xn+1sR,kcommitments+yk(xc+1wL,k+wO,k+ix1+iwC,i,k)middle line=ykrk(the y piece, next step)

5. Account for the lone k𝐇k. One piece of r's X0 coefficient wOy is still missing: the public y. On the reweighted generator it collapses to a constant 1 with no y left

yk·(yk)𝐇k=𝐇kk𝐇k

That is the bare term in step 24 which coefficient is exactly 1 per generator, not a missing scalar. Adding it completes the 𝐇k side to kykrk𝐇k.

6. Cancel the blinds. The H row of step 2 is precisely the scalar u of step 22. The last line of step 24 carries uH, so the two annihilate

(αxc+1+βxn+ρxn+1+iμixn1i)HuH=(uu)H=𝒪

This is the zero-knowledge step: the masks vanish together and leave nothing on H.

7. Glue t^ to U. The remaining term of the last line is xipt^G, which is t^U once we name U:=xipG.

Result. Collecting steps 3, 4 to 5, 6, and 7

P=klk𝐆k+kykrk𝐇k+t^U

which is exactly equation 25.

So the same t^ is genuinely t(x) of the prover's real witness, so it cannot be a number made up to satisfy check one. Together the two checks force tn=δwc on the true witness.

Now the final part, the logarithmic fold.

25 - Regrouped, that point is exactly l on the 𝐆's, r on the reweighted 𝐇's and t^ glued to U

P=klk𝐆k+kykrk𝐇k+t^U,𝐇k:=yk𝐇k,U:=xipG

26 - Rename a:=l, b:=r. The entire statement is now one inner product

P=a,𝐆+b,𝐇+a,bU

27 - Split each vector in half. The prover sends the two points that cross the split

L=a1,𝐆2+b2,𝐇1+a1,b2U,R=a2,𝐆1+b1,𝐇2+a2,b1U

In the proof: the round Lk,Rk are w105–w120 C1 · w179–w192 C2, interleaved L0,R0,L1,R1,.

28 - A fresh challenge x for this round is drawn in the same way by (hashing the round's L,R). Fold vectors and generators with opposite powers.

a=xa1+x1a2,b=x1b1+xb2,𝐆=x1𝐆1+x𝐆2,𝐇=x𝐇1+x1𝐇2

29 - Why it works: expand one folded inner product and watch the diagonal survive while the off-diagonal splits off

a,b=xa1+x1a2,x1b1+xb2(1)=xx1a1,b1+x1xa2,b2=a1,b1+a2,b2=a,b+x2a1,b2+x2a2,b1(2)

30 - The same expansion on the 𝐆 and 𝐇 sides gives the identical pattern:

a,𝐆=a,𝐆+x2a1,𝐆2+x2a2,𝐆1,b,𝐇=b,𝐇+x2b2,𝐇1+x2b1,𝐇2

31 - Add the three lines: the x2 parts are exactly L, the x2 parts are exactly R. So the folded statement is the same statement but half the length.

a,𝐆+b,𝐇+a,bU=Pold+x2L+x2R=:P

32 - After log2n rounds each vector is a single scalar a,b, sent in the clear. Each original generator survives times a product of the round challenges, sk=jxj±1 chosen by the bits of k

P+jxj2Lj+jxj2Rj=?aksk𝐆k+bksn1kyk𝐇k+abxipG

the reversed index sn1k on the 𝐇 side is because b folded with the opposite powers to a, so its surviving products come out mirrored.

In the proof: a is w121 C1 · w193 C2. b is w122 C1 · w194 C2.

Notice that the checks 1 and 2 are each multiplied by an independent random weight and dropped into one accumulator, so the whole proof, is settled by a single multiexponentiation, for each curve, collapsing to the identity, if it holds.

From the verifier point of view

Before we run all of that backwards, let's stop for a second and see clearly what the Generalized Bulletproof equation actually is for each side, because the prover and the verifier are reading the very same line written in two different domains. The prover reads it as arithmetic between secret numbers. The verifier reads it in the exponent, as operations between public points.

And why are these two the same thing? Because a Pedersen commitment is a group homomorphism, φ(ax+by)=aφ(x)+bφ(y). Any linear relation that the prover knows to be 0 in the field becomes a relation between points equal to 𝒪, and since 𝐆,𝐇,𝐆k,𝐇k have no known discrete-log relation between them, the point equation can only collapse to 𝒪 if the scalar coefficients were already zero.

Now an important question: how can we be sure that the prover ran the honest circuit to arrive at that polynomial equation? Well, the matrices WL,WR,WO,WC and the constants c are not in the proof at all. They are the circuit, they are public and fixed, the same for every transaction, and we build them ourselves before even start looking at the proof that the prover gave us. If you look at the code, the verifier reconstructs its weights by himself and never from the proof. The prover chooses the witness and nothing else. So when the folded equation closes to 𝒪, it closes against the verifier matrices, and the binding of the commitments together with Schwartz-Zippel force his hidden witness to satisfy exactly the constraints that the verifier wrote.

Alright, to get started let's try to follow one real output O all the way through and see where it ends up. O is born as an ordinary one-time output on Ed25519, sitting on chain. When someone spends it, the transaction never names it, the transaction only publishes the re-randomized tuple O~,I~,C~ and SAL proves the ownership of O~. Meanwhile (O,I,C) is one leaf among hundreds of millions in the tree and we use the different circuits to prove that we know a path (in a hidden way) from that leaf up to our root and that O~ is the honest disguise of it, and doing that it turns O's coordinates and every node of the path into the wires aL,aR,aO and the tree openings aC. Those wires fold into l(X) and r(X), which get sealed into the commitments AI,AO,S and the layer commitments C𝐆. Finally the two checks and the logarithmic fold crush everything into the few bytes that are inside the fcmp_pp field: AI,AO,S, the layer commitments, the coefficient commitments Tj, the opened scalars τx,u,t^, and then the round points Lk,Rk with the final a,b. And nowhere in all of that does O ever appear itself! It survives only as blinded points and a handful of folded scalars that look uniformly random to us, and yet, by everything we did above, they were proved to be a genuine leaf of our own tree.

Now backwards

We previously built the proof forwards (from the prover perspective), now let's do it backwards (from the verifier's perspective) taking what we have on chain as starting point.

Reading what the prover sent:

1 - Read the three wire commitments, then draw the challenges the transcript binds to them:

AI,AO,Sy,z,n=2c+2

2 - Rebuild the public weight vectors, folding the fixed circuit with powers of z. Note wR already absorbs the y1 here

wL=zzWL,wR=(zzWR)y1,wO=zzWO,wC=zzWC𝐆

3 - Get the one public constant of the whole equation, δ(y,z), from those weights alone:

δ=wR,wL

4 - Read the t-polynomial commitments and the challenge x that opens them, then read the three opened scalars in the clear.

Tj(jn)x,t^,u,τx

5 - Check one: the t-polynomial identity (random weight w1)

The same as previously: t^ claims to be the Xn coefficient of t(X) and the verifier rebuilds that coefficient as a commitment and demands equality.

t^G+τxH=?xn(δkzkck)G+jnxjTj

Everything on the right is public or a point the prover already sent. Passing this pins t^ to the public value δwc: the prover cannot name any other t^ without breaking the binding of the Tj commitments.

6 - Check two: Rebuild P and fold (random weight w2)

Weight each public point by the power of x its coefficients sit at, exactly the prover's step 22. The commitments C𝐆 enter here, at xn1i.

P=xc+1AI+xnAO+xn+1S+ixn1iC𝐆(i)+(generator terms below)

7- The weight vectors fold onto the generators, each at the power of x its coefficient occupies in l(X) and r(X): wR onto the 𝐆k and wL,wO together with the tree weights wC onto the 𝐇k, the whole 𝐇 side reweighted by y1.

kxc+1wR,k𝐆k+kyk(xc+1wL,k+wO,k+ix1+iwC,i,k)𝐇kk𝐇k

8 - Glue the opened t^ to the independent base and subtract the gathered blind u.

uH+xipt^G

Steps 6 to 8 rebuild exactly the point of the prover's step 24, P=klk𝐆k+kykrk𝐇k+t^U, with a:=l, b:=r. Nothing secret was used: every coefficient is a public weight or an opened scalar, every point was sent by the prover.

9 - The inner-product argument now folds P through log2n rounds. Each (Lk,Rk) is read and enters at xk2 and xk2. The final a,b collapse the generators through the challenge products sk.

P+kxk2Lk+kxk2Rk=?aksk𝐆k+bksn1kyk𝐇k+abxipG

Why the proof is valid?

So a proof passes when, and only when, the opened t^ is at once the public constant value (check one) and the inner product of the reconstructed witness vectors (check two), across both curve proofs together. That one scalar is tied above to the root by the Schnorr proof and below to the public tuple O~,I~,R,C~ by the circuit.

Next we can see how a real proof looks like in the blockchain.