Ragu & Tachyon
Home
  • Scaling Zcash
  • An Introduction to Proof-Carrying Data
  • Statements and Challenges
  • Arithmetization
  • QAP and Polynomial Encoding
  • Polynomial Commitments
  • The Inner Product Argument
  • Batched Openings
  • The Complete Proof
  • Recursive Proofs
  • Accumulation
  • Split Accumulation
  • Curve Cycles
  • Revdot Folding
  • Wiring and the Registry
  • Endoscalars
  • Staging
  • Recursion in Ragu
  • Notes, Nullifiers, and Tachygrams
  • Pool State and Anchors
  • Oblivious Sync
  • The Proof Tree
  • Aggregation and Bundles
Final Recap
  • Notation
  • Glossary
  • Curves
Home
  • Scaling Zcash
  • An Introduction to Proof-Carrying Data
  • Statements and Challenges
  • Arithmetization
  • QAP and Polynomial Encoding
  • Polynomial Commitments
  • The Inner Product Argument
  • Batched Openings
  • The Complete Proof
  • Recursive Proofs
  • Accumulation
  • Split Accumulation
  • Curve Cycles
  • Revdot Folding
  • Wiring and the Registry
  • Endoscalars
  • Staging
  • Recursion in Ragu
  • Notes, Nullifiers, and Tachygrams
  • Pool State and Anchors
  • Oblivious Sync
  • The Proof Tree
  • Aggregation and Bundles
Final Recap
  • Notation
  • Glossary
  • Curves
  • Scaling Zcash
  • An Introduction to Proof-Carrying Data
  • Statements and Challenges
  • Arithmetization
  • QAP and Polynomial Encoding
  • Polynomial Commitments
  • The Inner Product Argument
  • Batched Openings
  • The Complete Proof
  • Recursive Proofs
  • Accumulation
  • Split Accumulation
  • Curve Cycles
  • Revdot Folding
  • Wiring and the Registry
  • Endoscalars
  • Staging
  • Recursion in Ragu
  • Notes, Nullifiers, and Tachygrams
  • Pool State and Anchors
  • Oblivious Sync
  • The Proof Tree
  • Aggregation and Bundles
  • Final Recap
  • Appendix

    • Notation
    • Glossary
    • Curves

The Inner Product Argument

The previous chapter reduced a polynomial evaluation to one statement about a secret vector, and it finished the exchange by revealing that vector. This chapter replaces the reveal with a proof whose size grows with the logarithm of the vector's length. The idea is to turn a claim about a vector of length n into a claim of the same shape about a vector of length n/2, and to repeat this until one scalar is left, which the prover can send.

We derive one halving round in full, run the argument by hand on the toy curve, and then look at what cheating would take and what the verifier pays. The last sections compare this textbook form with the version in Ragu's code, which differs in several visible ways while proving the same claim.

1. The claim to prove

Section 9 of the previous chapter ended with this statement, where U is an extra generator and xn is the vector of powers of x:

given G, U, P, x, the prover knows a such that P=⟨a,G⟩+⟨a,xn⟩⋅U.

The halving step never uses the fact that the second vector consists of powers, so we write b for it until Section 8 needs the powers again:

P=⟨a,G⟩+⟨a,b⟩⋅U.

The verifier knows G, b, U and P. Only the prover knows a. We keep the running example from the previous chapter, the polynomial p(X)=7+2X+5X2+3X3 at x=9:

QuantityValue
Coefficient vectora=(7,2,5,3)
Evaluation pointx=9
Power vectorb=(1,9,38,41)
Claimed valuey=⟨a,b⟩=37
CommitmentC=⟨a,G⟩=(27,30)
Statement pointP=C+37⋅U=(5,29)

Notation

The general Bulletproofs relation has a secret b with its own generators H. The Ragu Book states that form and then sets H to zero for polynomial openings, because the powers of x are public. We use only the reduced form. The book also writes f and Q where this site writes a and U.

2. Halving produces cross terms

To shorten a, the prover has to combine its entries. A natural way to do this is to split each vector into a lower and an upper half, written alo​,ahi​ and Glo​,Ghi​, and add the halves. Each half has length n/2, so the sums do too.

Now compare the commitment before and after. The original commitment pairs each half with its own half of the generators:

⟨a,G⟩=⟨alo​,Glo​⟩+⟨ahi​,Ghi​⟩.

The shortened vectors pair every half with every other half, because the inner product distributes over the sums:

⟨alo​+ahi​,Glo​+Ghi​⟩=⟨alo​,Glo​⟩+⟨ahi​,Ghi​⟩+⟨ahi​,Glo​⟩+⟨alo​,Ghi​⟩.

The last two terms are cross terms, and they are what stands between the short vectors and the original commitment. In this section and the next we use the second vector from the previous chapter, because the running vector has ⟨ahi​,Ghi​⟩=O by coincidence, and an identity point would hide the effect we want to see.

Take d=(3,1,8,4), whose commitment is Cd​=(6,23). It splits into dlo​=(3,1) and dhi​=(8,4), and the four inner products between the halves are:

PiecePoint
⟨dlo​,Glo​⟩(4,25)
⟨dhi​,Ghi​⟩(8,22)
⟨dhi​,Glo​⟩(1,29)
⟨dlo​,Ghi​⟩(8,22)

The first two pieces add up to Cd​. The plain sums are dlo​+dhi​=(11,5) and Glo​+Ghi​=((28,10), (18,10)), and their inner product is (30,23). That point is the sum of all four pieces, so it differs from Cd​.

The cross terms depend on a, so the verifier cannot compute them. The prover has to send them. That creates a new problem, because a prover who sends two points and then folds in a fixed, known way could pick those points to make any claim look consistent. Section 6 turns this into an explicit attack. The fix is to weight the halves with a random challenge that the verifier picks after the prover has sent the cross terms, so that the prover cannot tailor them to the weights.

3. Choosing the weights

The weights must not disturb the two terms that make up the original commitment. Suppose we scale the upper half of a by a scalar α and the upper half of G by a scalar β. Expanding as before gives

⟨alo​+αahi​,Glo​+βGhi​⟩=⟨alo​,Glo​⟩+αβ⟨ahi​,Ghi​⟩+α⟨ahi​,Glo​⟩+β⟨alo​,Ghi​⟩.

For the second term to keep weight 1 we need αβ=1, so α and β must be inverses of each other. This is why inverses of the challenge appear throughout the argument. One half of the pair goes on the scalars and the other goes on the generators, and they cancel exactly on the terms we want to keep.

Take the weight 3, whose inverse modulo 43 is 29. Scaling both high halves by 3 gives (26,8). Removing the cross pieces, each of which carries weight 3, leaves (17,7), which is ⟨dlo​,Glo​⟩+9⟨dhi​,Ghi​⟩ and not Cd​. Scaling dhi​ by 29 and Ghi​ by 3 gives (4,6). Removing the cross pieces, which now carry weights 29 and 3, leaves (6,23)=Cd​.

The scale of each half is also free, as long as the product over a matching pair of halves stays 1. Bulletproofs and the Ragu Book spread the challenge u over both halves, which gives weights u,u−1 on the scalars and u−1,u on the generators. This symmetric form puts u±2 on the cross terms, and it leads to the tidy formula in Section 8. Ragu's code uses the one-sided form from the display above, with the lower halves unweighted, and Section 9 shows that the two forms differ only by a rescaling.

4. One round of folding

The second vector b pairs with a in the same way that G does, so it needs the same weights as G to keep ⟨a,b⟩ intact. One round therefore defines, for a challenge u,

a′b′G′​=ualo​+u−1ahi​,=u−1blo​+ubhi​,=u−1Glo​+uGhi​.​

We now find the point that these three vectors commit to. Expanding the first inner product term by term gives

⟨a′,G′⟩​=⟨ualo​+u−1ahi​,u−1Glo​+uGhi​⟩=uu−1⟨alo​,Glo​⟩+uu⟨alo​,Ghi​⟩+u−1u−1⟨ahi​,Glo​⟩+u−1u⟨ahi​,Ghi​⟩=⟨a,G⟩+u2⟨alo​,Ghi​⟩+u−2⟨ahi​,Glo​⟩.​

The scalar inner product expands in exactly the same way, because b′ has the same weights as G′:

⟨a′,b′⟩=⟨a,b⟩+u2⟨alo​,bhi​⟩+u−2⟨ahi​,blo​⟩.

A two by two grid showing how the four inner products of a folding round sort into the diagonal, which keeps weight one, and two off-diagonal cross terms.

Next we form the point P′=⟨a′,G′⟩+⟨a′,b′⟩⋅U by adding the first expansion to U times the second, and we collect terms by their power of u:

P′​=⟨a,G⟩+⟨a,b⟩⋅U+u−2(⟨ahi​,Glo​⟩+⟨ahi​,blo​⟩⋅U)+u2(⟨alo​,Ghi​⟩+⟨alo​,bhi​⟩⋅U).​

The first line is P. The other two brackets are the two points the prover must send, and we name them

LR​=⟨ahi​,Glo​⟩+⟨ahi​,blo​⟩⋅U,=⟨alo​,Ghi​⟩+⟨alo​,bhi​⟩⋅U.​

This gives the update rule for the statement point:

P′=P+u−2⋅L+u2⋅R.

The verifier can compute everything on the right once it has L and R, and it can fold G and b itself because they are public. Only a′ remains in the prover's hands. The result is a new claim with the same shape as the old one, at half the length:

P′=⟨a′,G′⟩+⟨a′,b′⟩⋅U.

The order of the messages matters, because the prover must commit to L and R before it learns u:

5. A complete run on the toy curve

For n=4 the argument takes k=log2​n=2 rounds. We number rounds in the order they happen, write a(j) for the vector after round j, and start from a(0)=a, b(0)=b, G(0)=G and P(0)=P. The challenges below are picked by hand so that the numbers avoid accidental coincidences. Fiat–Shamir, covered in Statements and Challenges, would derive them from a transcript in a real proof.

Round 1

The prover computes the cross terms from the secret vector:

Prover computesValue
⟨ahi​,blo​⟩ with ahi​=(5,3), blo​=(1,9)32
⟨alo​,bhi​⟩ with alo​=(7,2), bhi​=(38,41)4
L1​=⟨ahi​,Glo​⟩+32⋅U(22,7)
R1​=⟨alo​,Ghi​⟩+4⋅U(30,8)

After it receives L1​ and R1​, the verifier picks the challenge:

The verifier sends u1​=15. Its inverse is u1−1​=23, and the weights on the cross terms are u1−2​=13 and u12​=10.

Both parties fold what they hold. The last two rows of the next table compute the same point in two ways, once by the verifier from public values and once by the prover from the folded secret vector:

PartyComputationResult
Provera(1)=15alo​+23ahi​(5,13)
Bothb(1)=23blo​+15bhi​(34,5)
BothG(1)=23Glo​+15Ghi​((6,23), (4,25))
VerifierP(1)=P(0)+13⋅L1​+10⋅R1​(30,23)
Prover⟨a(1),G(1)⟩+20⋅U(30,23)

The inner product changes the way the algebra predicts: 37+13⋅32+10⋅4=20 modulo 43, and direct computation of ⟨a(1),b(1)⟩ gives 20 as well.

Round 2

The second round applies the same steps to vectors of length 2, so each half now has one entry:

Prover computesValue
⟨ahi​,blo​⟩ with ahi​=(13), blo​=(34)12
⟨alo​,bhi​⟩ with alo​=(5), bhi​=(5)25
L2​=⟨ahi​,Glo​⟩+12⋅U(28,21)
R2​=⟨alo​,Ghi​⟩+25⋅U(14,9)

The verifier sends u2​=10, with u2−1​=13, u2−2​=40 and u22​=14.

PartyComputationResult
Provera(2)=10alo​+13ahi​(4)
Bothb(2)=13blo​+10bhi​(19)
BothG(2)=13Glo​+10Ghi​((20,25))
VerifierP(2)=P(1)+40⋅L2​+14⋅R2​(14,22)
Prover⟨a(2),G(2)⟩+33⋅U(14,22)

The final check

All three vectors now have one entry, so the prover can send its scalar without exposing a long vector. We write a^, b^ and G^ for the single entries left at the end. The claim P(k)=⟨a(k),G(k)⟩+⟨a(k),b(k)⟩⋅U becomes

P(k)=a^⋅G^+a^b^⋅U=a^⋅(G^+b^⋅U).

The prover sends a^=4. The verifier already holds b^=19 and G^=(20,25) from folding, and it checks one equation:

P(2)=(14,22),a^⋅(G^+b^⋅U)=4⋅((20,25)+19⋅U)=(14,22).

The two points match, so the verifier accepts.

Unrolling the update rule gives the whole check in one line, which is how an implementation evaluates it:

P+j=1∑k​(uj−2​⋅Lj​+uj2​⋅Rj​)=a^⋅(G^+b^⋅U).

The whole proof is L1​=(22,7), R1​=(30,8), L2​=(28,21), R2​=(14,9) and a^=4. For n=4 that is four points and one scalar, against the four scalars of revealing a. The saving only appears at larger n, because the proof grows with log2​n.

The book and the code label these values differently

The Ragu Book indexes rounds downward, from j=k to j=1, so its u1​ is the last challenge and its G(0) is the final vector. It calls the final generator G0​. The code counts rounds upward from u0​, uses G0​ for the first public generator, and calls the final one G0′​. This site counts rounds upward from j=1 and writes G^ for the final generator, because G0​ already names the point (1,2) in every toy example.

6. Why cheating fails

The verifier never sees a, so we need a reason to believe that a false claim cannot pass. We start with a prover who lies about the value and follows the protocol otherwise:

Claim y=38 instead of 37, so the statement point is C+38⋅U=(1,29), and fold honestly from a. Running the argument for all 1764 pairs of nonzero challenges gives 1764 rejections and no acceptances, because the extra 1⋅U stays in the folded point in every round.

A cheater can do more than this, because Lj​ and Rj​ can be any points it likes. To see the limits of that freedom, we track one number through the protocol.

The gap

Assume for now that the prover builds every point from the public generators and knows the coefficients it used, and that nobody knows a relation among the generators. Then each point X the prover handles has one expression X=⟨c,G⟩+t⋅U, so the following number is well defined:

gap(X)=t−⟨c,b⟩.

The statement is true exactly when gap(P)=0, since that says the coefficient of U equals the inner product of the coefficients of G with b. The gap is linear in the point, which means gap(X+r⋅Y)=gap(X)+rgap(Y). It also has simple values on the generators, namely gap(Gi​)=−bi​ and gap(U)=1.

Now look at the right side of the final check. For a folded generator, linearity gives

gap(u−1⋅Gi​+u⋅Gi+n/2​)=−(u−1bi​+ubi+n/2​)=−bi′​,

so each folded generator has the folded entry of b as its negative gap, and this repeats in every round. At the end gap(G^)=−b^, and therefore

gap(a^⋅(G^+b^⋅U))=a^(−b^+b^)=0

for every possible a^. Without any relation among the generators, two equal points have equal coefficients and so equal gaps. A passing proof therefore needs the left side of the check to have gap zero as well:

gap(P)+j=1∑k​(uj−2​gap(Lj​)+uj2​gap(Rj​))=0.

Counting the challenges a cheater can survive

Let e be the gap of the current statement point at the start of some round, and suppose e=0. The prover fixes L and R, and then the verifier picks u. The new gap is zero only if, with w=u2,

e+w−1gap(L)+wgap(R)=0.

Multiplying by w gives the polynomial equation gap(R)w2+ew+gap(L)=0. Its middle coefficient is e=0, so the polynomial is not zero, whatever the prover chose. By the root count from Statements and Challenges it has at most 2 roots w, and each root has at most 2 square roots u. At most 4 challenges in a round can therefore clear a nonzero gap, and the same bound holds for every vector length.

We can test this on a two-entry vector. Comparing coefficients of each generator gives three equations in the final scalar, and we can check them directly for every strategy in a family:

Take a=(7,2), b=(1,9), and the false value y=26, so the true inner product is 25 and the statement has gap 1. Keep the honest G coefficients in L and R, and try all 1849 choices of their U coefficients. For each choice, test all 42 nonzero challenges against the equations for G0​, G1​ and U with the final scalar free. The largest number of passing challenges is 4, and the honest U coefficients pass none. A further 5,625 strategies with altered G coefficients, taken from a grid of values, reach at most 2, and in every passing case the gap equation holds.

A false statement starts with a nonzero gap and must reach gap zero by the end, so the gap has to be cleared in one of the k rounds. The chance of this is at most 4k divided by the number of nonzero challenges. On the toy curve this is a large fraction, which is one more way the toy curve is insecure. On a real curve the fraction is negligible:

For n=213 over a Pasta scalar field, which has about 2254 elements, this bound is 52 divided by about 2254, which is below 2−248.

Why the challenge must come after the cross terms

The count above relies on L and R being fixed before u. If the prover sees the challenge first, it can solve the equation for the point it still has to send:

Suppose the prover knows u1​=15 before it sends R1​, and it wants to pass with the false statement y=38, whose point carries an extra 1⋅U. It replaces the honest R1​=(30,8) by R1​−u1−2​⋅1⋅U=(28,10). The weight u12​ then turns the change into −1⋅U, and P(1) comes out as (30,23), exactly the honest folded point. The rest of the run is honest, and the verifier accepts.

The same altered point does poorly when the challenge arrives afterwards:

Now fix that altered R1​ first and let the challenge vary. Running the protocol for all 42 nonzero values of u1​ gives acceptance only at u1​∈{15,28}, the guess and its negative, which share the same square. The prover wins with probability 2/42.

In this attack gap(P)=1, gap(L1​)=0 and gap(R1​)=30. The equation 1+30⋅u12​=0 has the solutions u12​=10, which are the two challenges just found.

In a non-interactive proof this ordering becomes a requirement on the transcript. Every Lj​ and Rj​ must go into the hash before uj​ comes out, for the reason given in the section on omitted transcript data.

Why the generators must have no known relation

The gap only makes sense if each point has one expression in the generators. On the toy curve every point is a known multiple of every other point, so the gap has no fixed meaning there, and the final scalar can absorb any error:

Keep the false statement y=38 and the honest Lj​,Rj​. With challenges (15,10) the folded point is (7,6), and the right side of the check is a multiple of G^+b^⋅U=(30,23). Every point on the toy curve is a known multiple of G0​, so the prover can divide logs: a^=16/35=41. Then 41⋅(30,23)=(7,6) and the verifier accepts the false value. This is the attack the discrete log relation assumption rules out on a real curve.

The argument above therefore rests on the following assumptions. The discrete log relation assumption from the previous chapter holds for G together with U. The prover builds its points from the generators with coefficients it knows. The challenges are uniform and come after the points they weight, which in a non-interactive proof means treating the transcript hash as a random oracle. This section gives the reasoning informally and is not a security proof.

7. The verifier's linear cost

The proof is short, but the verifier still has to compute G^ and b^. Folding G round by round costs about n scalar multiplications in the first round, n/2 in the second, and so on. The verifier can do better by computing the final point in one step, which requires knowing the final weight of each original generator.

For n=4 we can substitute one round into the other. The first round gives

G(1)=(u1−1​⋅G0​+u1​⋅G2​,u1−1​⋅G1​+u1​⋅G3​),

and the second round combines those two entries with weights u2−1​ and u2​:

G^​=u2−1​⋅(u1−1​⋅G0​+u1​⋅G2​)+u2​⋅(u1−1​⋅G1​+u1​⋅G3​)=u1−1​u2−1​⋅G0​+u1−1​u2​⋅G1​+u1​u2−1​⋅G2​+u1​u2​⋅G3​.​

Each generator's weight is a product with one factor per round. Round j contributes uj​ if the generator sat in the upper half at that round and uj−1​ if it sat in the lower half. Reading the index i in binary, round 1 looks at the highest bit and the last round at the lowest. This defines the vector s with

si​=j=1∏k​uj±1​,with exponent +1 when bit k−j of i is 1, and −1 otherwise,

and it gives G^=⟨s,G⟩. The vector b folds with the same weights, so b^=⟨s,b⟩ for the same s. Here is the table for our run:

iBits of isi​Value
000u1−1​u2−1​41
101u1−1​u2​15
210u1​u2−1​23
311u1​u2​21

With (u1​,u2​)=(15,10), the multi-scalar multiplication ⟨s,G⟩ gives (20,25), the same point as G^ from the round-by-round folding. The same weights applied to b give ⟨s,b⟩=19=b^.

Computing G^ this way is one multi-scalar multiplication (MSM) with n terms. An MSM of that size is cheaper than folding, but its cost still grows linearly with n, and no shortcut is available, because the generators are independent points with no structure to exploit.

8. The polynomial g(X)

The scalar b^ is cheaper than G^, because b has structure that G lacks. Its entries are powers of x, so bi+n/2​=xn/2bi​. Substituting this into the fold gives

bi′​=u−1bi​+ubi+n/2​=(u−1+uxn/2)bi​.

One round therefore multiplies the first half of b by a single factor, and the result is still a power vector up to that factor.

In the run of Section 5, b(1)=(34,5). The predicted factor is u1−1​+u1​x2=23+15⋅38=34, and 34⋅(1,9)=(34,5) modulo 43.

Round j acts on a vector of length 2k−j+1, so its factor uses the exponent 2k−j. Multiplying the factors of all k rounds gives the final entry. Replacing x with a variable defines the polynomial

g(X)=j=1∏k​(uj−1​+uj​X2k−j),

and then b^=g(x). The coefficients of this polynomial are the entries of s. To see this, expand the product by choosing one term from each factor. Choosing uj​X2k−j adds 2k−j to the power of X and contributes uj​, while choosing uj−1​ adds nothing and contributes uj−1​. The coefficient of Xi then comes from the single choice that matches the binary digits of i, which is the rule for si​ from Section 7.

With (u1​,u2​)=(15,10), the polynomial is g(X)=(23+15X2)(13+10X). Multiplying it out gives coefficients (41,15,23,21), which is the s of the table. At x=9 the first factor is 23+15⋅38=34, the second is 13+10⋅9=17, and their product is 19=b^.

Ordering mistakes are easy to make here, so we also check a longer case:

The script behind this chapter repeats the comparison for k=3 with eight generators and challenges (3,5,7): round-by-round folding gives G^=(25,2), and the coefficients of g give the same point through one multi-scalar multiplication. The folded power vector and g(9) also agree, at 37.

The book's listing of s matches this after relabelling rounds. With rounds numbered downward, its uj​ is our uk+1−j​, and its second entry has a positive exponent on u1​, its last challenge, which is our s1​.

This also gives a compact statement of the verifier's costs. The expression G^=⟨s,G⟩ says that G^ is the Pedersen commitment to g, whose coefficient vector is s:

Task for n=213Operations
Computing b^=g(x) with repeated squaring12 squarings and 13 factor multiplications in Fq​
Computing s from the challengesabout 8,192 field multiplications
Computing G^=⟨s,G⟩8,192 scalar multiplications, one per generator
Combining the Lj​ and Rj​26 scalar multiplications

Everything in this table except the two rows involving all of s is logarithmic in n. A recursive verifier cannot afford those rows, because it would have to perform them inside a circuit. The accumulation chapter works from this exact point. It notices that G^ is a claim about the commitment to a polynomial with a short description, namely the k challenges, and it postpones checking that claim.

9. The code's one-sided fold

The remaining sections compare the form above with Ragu's implementation, which the code describes as adapted from halo2. The differences fall into two groups. This section covers how the code folds, and the next covers what statement it folds.

Reconstructed from code at commit 3219e08, not confirmed by the authors

The Ragu Book and the code differ here, and everything we say about the code in Sections 9 to 11 is our reading of it. The book uses the symmetric weights u,u−1 of Section 4. The prover code uses the one-sided weights below. This site teaches the symmetric form because the book uses it and because the g(X) of later chapters comes from it. Where the question is what Ragu actually runs, the site follows the code.

The code leaves the lower halves unweighted. To avoid mixing up the two conventions, we write w for the challenge in this form, although the code calls it uj​:

a′b′G′​=alo​+w−1ahi​,=blo​+wbhi​,=Glo​+wGhi​.​

These are the lines in prover.rs that fold the two scalar vectors:

for i in 0..half {
    p_prime[i] = p_prime[i] + &(p_prime[i + half] * &u_j_inv);
    b[i] = b[i] + &(b[i + half] * &u_j);
}

Repeating the expansion from Section 4 with these weights gives the terms w−1L and wR, with the same L and R:

⟨a′,G′⟩P′​=⟨alo​,Glo​⟩+w⟨alo​,Ghi​⟩+w−1⟨ahi​,Glo​⟩+⟨ahi​,Ghi​⟩,=P+w−1⋅L+w⋅R.​

Setting w=u2 makes this update identical to the symmetric one. The folded vectors then differ only by a scale factor. Writing asym′​ for the symmetric fold,

asym′​=ualo​+u−1ahi​=u(alo​+u−2ahi​)=ua′,

and likewise Gsym′​=u−1G′ and bsym′​=u−1b′. The factors cancel in both inner products, which explains why the statement point does not change.

Run the same statement P=(5,29) with the code's weights and the squared challenges (w1​,w2​)=(10,14), which are the squares of (15,10) modulo 43.

ValueSymmetric formOne-sided form
First-round cross termsL1​=(22,7), R1​=(30,8)L1​=(22,7), R1​=(30,8)
Point after round 1(30,23)(30,23)
Point after round 2(14,22)(14,22)
Vector after round 1(5,13)(29,41)
Final scalara^=435
Final b entryb^=1912
Final generatorG^=(20,25)(6,8)

The product of the challenges is u1​u2​=21, and multiplying the one-sided scalar by it gives 35⋅21=4=a^. Dividing the one-sided b entry and generator by it gives b^ and G^ as well. Both forms pass their final checks.

The one-sided form also changes the polynomial. In the verifier code, compute_b and compute_s work with ∏j=1k​(1+wj​X2k−j), which equals the symmetric g(X) times ∏j​uj​ when wj​=uj2​:

For (w1​,w2​)=(10,14), the code's polynomial is (1+10X2)(1+14X), with coefficients (1,14,10,11). Dividing them by u1​u2​=21 gives (41,15,23,21), the coefficients of the symmetric g. At x=9 the code's polynomial takes the value 12, which is the one-sided b entry from the table above.

The code's challenges are not literally squares, since a hash outputs wj​ directly. A uniform wj​ could be a non-square, so the one-sided form is not a change of variables for every challenge, though the expansion above holds for any nonzero w. The gap argument of Section 6 also carries over, with the simpler conclusion that a round has at most 2 surviving challenges.

10. The code's opening statement

The book opens P=C+y⋅U directly. The code makes three changes to the statement before it starts folding, and each change answers a specific need. Throughout, C is the polynomial commitment and v is the claimed value, which is the y of the earlier sections.

Moving the value into the constant coefficient

The polynomial p(X)−v has a root at x exactly when p(x)=v. Its coefficient vector differs from a only in the constant entry, and its commitment is C−v⋅G0​, where G0​ here is the first public generator. The verifier can compute this point itself. The claim then has the form "this commitment has inner product zero with b", which removes the need for any U term in the starting point, since 0⋅U=O.

Adding a mask

The final scalar is a fixed linear combination of the coefficients, with weights the verifier can compute, so sending it reveals one piece of information about a. To hide that combination, the prover picks a random polynomial that has a root at x, commits to it, and adds a random multiple of it:

Start from the random vector (11,4,19,6), whose polynomial takes the value 26 at x=9. Subtracting that from the constant coefficient gives smask​=(28,4,19,6), whose polynomial has a root at 9. Its commitment is S=(6,23).

The notation smask​ keeps this vector apart from the weight vector s of Section 7. The code uses the letter s for both, as s_poly and compute_s. Adding ξ⋅S, for a challenge ξ sampled after S, gives the statement that the code folds:

P′=C−v⋅G0​+ξ⋅S,a′=a+ξsmask​−(v,0,…,0).

The inner product of a′ with b is p(x)+ξ⋅0−v, which is zero exactly when the claim is true.

With ξ=6, the combined vector is a+6smask​=(3,26,33,39). It still evaluates to 37 at x=9, because the mask contributes zero there. Subtracting v=37 from the constant coefficient gives a′=(9,26,33,39), and direct evaluation gives ⟨a′,b⟩=0. The verifier computes P′=C−37⋅G0​+6⋅S=(23,24), and committing to a′ directly gives (23,24).

The order matters for the same reason as before. If the mask did not vanish at x, it could shift the evaluation, and a prover who knew ξ in advance could choose the mask to cover a false value. Fixing S before ξ removes that choice:

Suppose the prover claims the false value v=38 and commits to the unadjusted mask (11,4,19,6), which takes the value 26 at x=9. The combination evaluates to the claimed value only if 37+ξ⋅26=38, so the prover needs ξ=5. Running the full argument for all 43 values of ξ gives acceptance at ξ=5 and nowhere else, a chance of 1/43.

The mask changes the value the prover reveals at the end:

Rerunning the same challenges with no mask gives the final scalar 7, which is a fixed linear combination of the coefficients once the challenges are public. With the mask it is 4, and it also depends on the random entries of smask​.

The module documentation says the mask is retained for the folded coefficients, and also that the IPA at this commit has no Pedersen blinding, so neither the commitment nor the Lj​,Rj​ carry a blinding term. The mask is therefore not a claim that the argument is zero knowledge. This matches the status recorded in chapter 02, where the Ragu Book and code do not yet establish zero knowledge. The same documentation records a limitation of unblinded round commitments. A valid opening can produce an identity Lj​ or Rj​, for example R with two coefficients at x=0, and the transcript rejects identity points.

Scaling U by a challenge

The statement in Section 1 uses a fixed U, and that leaves a weakness the earlier sections did not need to address. A prover could send a commitment with a hidden U component, C∗=⟨a,G⟩+δ⋅U. This point is not a commitment to any polynomial under G alone, but it can still be opened, because the extra δ makes up the difference between the true evaluation and a false claim.

The code handles this by sampling a challenge z after C, v and S are in the transcript, and using z⋅U as the generator for inner products in every round. With a′ formed from the claimed v, the final check now requires δ=z⟨a′,b⟩. The prover chose δ and v before it saw z, so this can hold for only one value of z when δ=0.

The prover sends C∗=C+6⋅U=(14,22) and claims v=37−6=31. The vector behind the G part of C∗ still evaluates to 37, so the claim is false for that vector. After subtracting v, the inner product is 6, and the spare 6⋅U in C∗ supplies the matching U term as long as U carries weight 1. Running the argument for all 42 nonzero values of z gives acceptance at z=1 only, which is the case where the scaling does nothing. A fixed, unscaled U would therefore accept this claim every time, and a random z accepts it with probability 1/42.

The code's comment on z gives this purpose in one line, saying that it ensures the prover did not interfere with the U term in its commitments. The Halo 2 background page reaches the same goal in a different way, by having the verifier select a random group element U before the argument begins. Ragu fixes U in its public parameters and randomizes its weight instead. The book's description of the relation does not mention this step.

11. A complete run of the code's form

We can now run the code's protocol on the same claim. The challenges are again chosen by hand. The verifier starts from P′, the prover computes each Lj​,Rj​ with weight z on U, and the check the verifier comment states is

P′+j=1∑k​(wj−1​⋅Lj​+wj​⋅Rj​)=c⋅G^+(cb^z)⋅U,

where c is the code's name for the final scalar, and G^ and b^ are built with the one-sided weights.

StepPartyValue
Transcript already holds C, x, vBoth(27,30), 9, 37
Send mask commitmentProverS=(6,23)
ChallengesVerifierξ=6, z=11
Compute P′Both(23,24)
Send round 1 cross termsProverL1​=(5,2), R1​=(24,1)
ChallengeVerifierw1​=19
Fold a′ and bProver(13,19) and (35,14)
Fold GProver((20,6), (14,9))
Send round 2 cross termsProverL2​=(24,30), R2​=(8,9)
ChallengeVerifierw2​=17
Fold a′ and bProver(4) and (15)
Fold GProver((16,10))
Send final scalarProverc=4
Compute b^ with compute_bVerifier15
Compute s with compute_sVerifier(1,17,19,22)
Compute G^=⟨s,G⟩Verifier(16,10)
Left side P′+∑j​(wj−1​⋅Lj​+wj​⋅Rj​)Verifier(22,7)
Right side c⋅G^+(cb^z)⋅UVerifier(22,7)

The verifier skips the round-by-round fold of G and b. It computes both at the end from the challenges, as Sections 7 and 8 describe. In the compression verifier, which is where this IPA is checked at this commit, the call chain is verify_proof, then use_challenges, then eval, so the full linear MSM runs. The Guard type also offers use_g, which accepts a claimed G^ and returns the challenges alongside it as an Accumulator. No code under crates/ragu_pcd/src outside the IPA module calls it at this commit.

12. Comparing the two forms

The next table gathers the differences in one place. Both columns prove that a committed polynomial takes the value v at x.

ItemRagu BookCode at 3219e08
Value generatorQ, fixedz⋅U, with U fixed and z a challenge
Starting pointP=C+y⋅QP′=C−v⋅G0​+ξ⋅S
Starting inner producty0
MaskNot describedsmask​ with a root at x
Weights on au,u−11,w−1
Weights on b and Gu−1,u1,w
UpdateP+u−2⋅L+u2⋅RP′+w−1⋅L+w⋅R
Final weightsThe vector s, which is the coefficients of ∏j​(uj−1​+uj​X2k−j) with the book's labels reversedThe coefficients of ∏j​(1+wj​X2k−j)
Final generatorG0​G0′​
Proof contentsLj​,Rj​, final a entryS, Lj​,Rj​, final scalar c

The Halo 2 background page uses a third labelling, in which Lj​ pairs the lower half of a with the upper half of G. Its L is therefore this chapter's R, so a reader comparing the sources should check the pairing and not only the letter. Later chapters on this site use the symmetric g(X) of Section 8, and they point out the code's form where it matters.

13. Where this chapter leaves us

This chapter took the following steps:

  1. Folding a vector in half creates two cross terms, and weights that are inverses of each other keep the original commitment intact.
  2. One round sends L and R, receives u, and turns P into P+u−2⋅L+u2⋅R, which is a claim of the same shape at half the length.
  3. After k=log2​n rounds the prover sends one scalar, so the proof has 2k points and one scalar.
  4. A false claim has a nonzero gap, and each round lets at most 4 challenges clear it, provided the generators have no known relation and each challenge follows its cross terms.
  5. The verifier's remaining cost is G^=⟨s,G⟩, an MSM of size n, while b^=g(x) takes only k factors because of the product form of g.
  6. Ragu's code uses one-sided weights, moves the value into the constant coefficient, adds a mask, and scales U by a challenge, and it proves the same evaluation claim.

The IPA opens one polynomial at one point, and a proof in Ragu needs many openings. The next chapter reduces many evaluation claims to a single one, so that one IPA can finish the job.

Last Updated: 11/4/26, 12:56 PM
Contributors: Hanh
Prev
Polynomial Commitments
Next
Batched Openings