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 into a claim of the same shape about a vector of length , 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 is an extra generator and is the vector of powers of :
The halving step never uses the fact that the second vector consists of powers, so we write for it until Section 8 needs the powers again:
The verifier knows , , and . Only the prover knows . We keep the running example from the previous chapter, the polynomial at :
| Quantity | Value |
|---|---|
| Coefficient vector | |
| Evaluation point | |
| Power vector | |
| Claimed value | |
| Commitment | |
| Statement point |
Notation
The general Bulletproofs relation has a secret with its own generators . The Ragu Book states that form and then sets to zero for polynomial openings, because the powers of are public. We use only the reduced form. The book also writes and where this site writes and .
2. Halving produces cross terms
To shorten , 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 and , and add the halves. Each half has length , 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:
The shortened vectors pair every half with every other half, because the inner product distributes over the sums:
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 by coincidence, and an identity point would hide the effect we want to see.
Take , whose commitment is . It splits into and , and the four inner products between the halves are:
| Piece | Point |
|---|---|
The first two pieces add up to . The plain sums are and , and their inner product is . That point is the sum of all four pieces, so it differs from .
The cross terms depend on , 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 by a scalar and the upper half of by a scalar . Expanding as before gives
For the second term to keep weight 1 we need , 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 , whose inverse modulo 43 is . Scaling both high halves by gives . Removing the cross pieces, each of which carries weight , leaves , which is and not . Scaling by and by gives . Removing the cross pieces, which now carry weights and , leaves .
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 over both halves, which gives weights on the scalars and on the generators. This symmetric form puts 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 pairs with in the same way that does, so it needs the same weights as to keep intact. One round therefore defines, for a challenge ,
We now find the point that these three vectors commit to. Expanding the first inner product term by term gives
The scalar inner product expands in exactly the same way, because has the same weights as :

Next we form the point by adding the first expansion to times the second, and we collect terms by their power of :
The first line is . The other two brackets are the two points the prover must send, and we name them
This gives the update rule for the statement point:
The verifier can compute everything on the right once it has and , and it can fold and itself because they are public. Only remains in the prover's hands. The result is a new claim with the same shape as the old one, at half the length:
The order of the messages matters, because the prover must commit to and before it learns :
5. A complete run on the toy curve
For the argument takes rounds. We number rounds in the order they happen, write for the vector after round , and start from , , and . 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 computes | Value |
|---|---|
| with , | |
| with , | |
After it receives and , the verifier picks the challenge:
The verifier sends . Its inverse is , and the weights on the cross terms are and .
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:
| Party | Computation | Result |
|---|---|---|
| Prover | ||
| Both | ||
| Both | ||
| Verifier | ||
| Prover |
The inner product changes the way the algebra predicts: modulo 43, and direct computation of gives as well.
Round 2
The second round applies the same steps to vectors of length 2, so each half now has one entry:
| Prover computes | Value |
|---|---|
| with , | |
| with , | |
The verifier sends , with , and .
| Party | Computation | Result |
|---|---|---|
| Prover | ||
| Both | ||
| Both | ||
| Verifier | ||
| Prover |
The final check
All three vectors now have one entry, so the prover can send its scalar without exposing a long vector. We write , and for the single entries left at the end. The claim becomes
The prover sends . The verifier already holds and from folding, and it checks one equation:
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:
The whole proof is , , , and . For that is four points and one scalar, against the four scalars of revealing . The saving only appears at larger , because the proof grows with .
The book and the code label these values differently
The Ragu Book indexes rounds downward, from to , so its is the last challenge and its is the final vector. It calls the final generator . The code counts rounds upward from , uses for the first public generator, and calls the final one . This site counts rounds upward from and writes for the final generator, because already names the point in every toy example.
6. Why cheating fails
The verifier never sees , 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 instead of , so the statement point is , and fold honestly from . Running the argument for all pairs of nonzero challenges gives 1764 rejections and no acceptances, because the extra stays in the folded point in every round.
A cheater can do more than this, because and 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 the prover handles has one expression , so the following number is well defined:
The statement is true exactly when , since that says the coefficient of equals the inner product of the coefficients of with . The gap is linear in the point, which means . It also has simple values on the generators, namely and .
Now look at the right side of the final check. For a folded generator, linearity gives
so each folded generator has the folded entry of as its negative gap, and this repeats in every round. At the end , and therefore
for every possible . 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:
Counting the challenges a cheater can survive
Let be the gap of the current statement point at the start of some round, and suppose . The prover fixes and , and then the verifier picks . The new gap is zero only if, with ,
Multiplying by gives the polynomial equation . Its middle coefficient is , so the polynomial is not zero, whatever the prover chose. By the root count from Statements and Challenges it has at most 2 roots , and each root has at most 2 square roots . 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 , , and the false value , so the true inner product is and the statement has gap . Keep the honest coefficients in and , and try all choices of their coefficients. For each choice, test all 42 nonzero challenges against the equations for , and with the final scalar free. The largest number of passing challenges is , and the honest coefficients pass none. A further strategies with altered coefficients, taken from a grid of values, reach at most , 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 rounds. The chance of this is at most 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 over a Pasta scalar field, which has about elements, this bound is divided by about , which is below .
Why the challenge must come after the cross terms
The count above relies on and being fixed before . If the prover sees the challenge first, it can solve the equation for the point it still has to send:
Suppose the prover knows before it sends , and it wants to pass with the false statement , whose point carries an extra . It replaces the honest by . The weight then turns the change into , and comes out as , 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 first and let the challenge vary. Running the protocol for all 42 nonzero values of gives acceptance only at , the guess and its negative, which share the same square. The prover wins with probability .
In this attack , and . The equation has the solutions , which are the two challenges just found.
In a non-interactive proof this ordering becomes a requirement on the transcript. Every and must go into the hash before 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 and the honest . With challenges the folded point is , and the right side of the check is a multiple of . Every point on the toy curve is a known multiple of , so the prover can divide logs: . Then 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 together with . 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 and . Folding round by round costs about scalar multiplications in the first round, 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 we can substitute one round into the other. The first round gives
and the second round combines those two entries with weights and :
Each generator's weight is a product with one factor per round. Round contributes if the generator sat in the upper half at that round and if it sat in the lower half. Reading the index in binary, round 1 looks at the highest bit and the last round at the lowest. This defines the vector with
and it gives . The vector folds with the same weights, so for the same . Here is the table for our run:
| Bits of | Value | ||
|---|---|---|---|
| 0 | 00 | ||
| 1 | 01 | ||
| 2 | 10 | ||
| 3 | 11 |
With , the multi-scalar multiplication gives , the same point as from the round-by-round folding. The same weights applied to give .
Computing this way is one multi-scalar multiplication (MSM) with terms. An MSM of that size is cheaper than folding, but its cost still grows linearly with , and no shortcut is available, because the generators are independent points with no structure to exploit.
8. The polynomial g(X)
The scalar is cheaper than , because has structure that lacks. Its entries are powers of , so . Substituting this into the fold gives
One round therefore multiplies the first half of by a single factor, and the result is still a power vector up to that factor.
In the run of Section 5, . The predicted factor is , and modulo 43.
Round acts on a vector of length , so its factor uses the exponent . Multiplying the factors of all rounds gives the final entry. Replacing with a variable defines the polynomial
and then . The coefficients of this polynomial are the entries of . To see this, expand the product by choosing one term from each factor. Choosing adds to the power of and contributes , while choosing adds nothing and contributes . The coefficient of then comes from the single choice that matches the binary digits of , which is the rule for from Section 7.
With , the polynomial is . Multiplying it out gives coefficients , which is the of the table. At the first factor is , the second is , and their product is .
Ordering mistakes are easy to make here, so we also check a longer case:
The script behind this chapter repeats the comparison for with eight generators and challenges : round-by-round folding gives , and the coefficients of give the same point through one multi-scalar multiplication. The folded power vector and also agree, at .
The book's listing of matches this after relabelling rounds. With rounds numbered downward, its is our , and its second entry has a positive exponent on , its last challenge, which is our .
This also gives a compact statement of the verifier's costs. The expression says that is the Pedersen commitment to , whose coefficient vector is :
| Task for | Operations |
|---|---|
| Computing with repeated squaring | squarings and factor multiplications in |
| Computing from the challenges | about field multiplications |
| Computing | scalar multiplications, one per generator |
| Combining the and | scalar multiplications |
Everything in this table except the two rows involving all of is logarithmic in . 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 is a claim about the commitment to a polynomial with a short description, namely the 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 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 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 for the challenge in this form, although the code calls it :
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 and , with the same and :
Setting makes this update identical to the symmetric one. The folded vectors then differ only by a scale factor. Writing for the symmetric fold,
and likewise and . The factors cancel in both inner products, which explains why the statement point does not change.
Run the same statement with the code's weights and the squared challenges , which are the squares of modulo 43.
| Value | Symmetric form | One-sided form |
|---|---|---|
| First-round cross terms | , | , |
| Point after round 1 | ||
| Point after round 2 | ||
| Vector after round 1 | ||
| Final scalar | ||
| Final entry | ||
| Final generator |
The product of the challenges is , and multiplying the one-sided scalar by it gives . Dividing the one-sided entry and generator by it gives and 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 , which equals the symmetric times when :
For , the code's polynomial is , with coefficients . Dividing them by gives , the coefficients of the symmetric . At the code's polynomial takes the value , which is the one-sided entry from the table above.
The code's challenges are not literally squares, since a hash outputs directly. A uniform 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 . 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 directly. The code makes three changes to the statement before it starts folding, and each change answers a specific need. Throughout, is the polynomial commitment and is the claimed value, which is the of the earlier sections.
Moving the value into the constant coefficient
The polynomial has a root at exactly when . Its coefficient vector differs from only in the constant entry, and its commitment is , where 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 ", which removes the need for any term in the starting point, since .
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 . To hide that combination, the prover picks a random polynomial that has a root at , commits to it, and adds a random multiple of it:
Start from the random vector , whose polynomial takes the value at . Subtracting that from the constant coefficient gives , whose polynomial has a root at . Its commitment is .
The notation keeps this vector apart from the weight vector of Section 7. The code uses the letter for both, as s_poly and compute_s. Adding , for a challenge sampled after , gives the statement that the code folds:
The inner product of with is , which is zero exactly when the claim is true.
With , the combined vector is . It still evaluates to at , because the mask contributes zero there. Subtracting from the constant coefficient gives , and direct evaluation gives . The verifier computes , and committing to directly gives .
The order matters for the same reason as before. If the mask did not vanish at , it could shift the evaluation, and a prover who knew in advance could choose the mask to cover a false value. Fixing before removes that choice:
Suppose the prover claims the false value and commits to the unadjusted mask , which takes the value at . The combination evaluates to the claimed value only if , so the prover needs . Running the full argument for all 43 values of gives acceptance at and nowhere else, a chance of .
The mask changes the value the prover reveals at the end:
Rerunning the same challenges with no mask gives the final scalar , which is a fixed linear combination of the coefficients once the challenges are public. With the mask it is , and it also depends on the random entries of .
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 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 or , for example with two coefficients at , and the transcript rejects identity points.
Scaling U by a challenge
The statement in Section 1 uses a fixed , and that leaves a weakness the earlier sections did not need to address. A prover could send a commitment with a hidden component, . This point is not a commitment to any polynomial under 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 after , and are in the transcript, and using as the generator for inner products in every round. With formed from the claimed , the final check now requires . The prover chose and before it saw , so this can hold for only one value of when .
The prover sends and claims . The vector behind the part of still evaluates to , so the claim is false for that vector. After subtracting , the inner product is , and the spare in supplies the matching term as long as carries weight 1. Running the argument for all 42 nonzero values of gives acceptance at only, which is the case where the scaling does nothing. A fixed, unscaled would therefore accept this claim every time, and a random accepts it with probability .
The code's comment on gives this purpose in one line, saying that it ensures the prover did not interfere with the 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 before the argument begins. Ragu fixes 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 , the prover computes each with weight on , and the check the verifier comment states is
where is the code's name for the final scalar, and and are built with the one-sided weights.
| Step | Party | Value |
|---|---|---|
| Transcript already holds , , | Both | , , |
| Send mask commitment | Prover | |
| Challenges | Verifier | , |
| Compute | Both | |
| Send round 1 cross terms | Prover | , |
| Challenge | Verifier | |
| Fold and | Prover | and |
| Fold | Prover | |
| Send round 2 cross terms | Prover | , |
| Challenge | Verifier | |
| Fold and | Prover | and |
| Fold | Prover | |
| Send final scalar | Prover | |
Compute with compute_b | Verifier | |
Compute with compute_s | Verifier | |
| Compute | Verifier | |
| Left side | Verifier | |
| Right side | Verifier |
The verifier skips the round-by-round fold of and . 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 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 at .
| Item | Ragu Book | Code at 3219e08 |
|---|---|---|
| Value generator | , fixed | , with fixed and a challenge |
| Starting point | ||
| Starting inner product | ||
| Mask | Not described | with a root at |
| Weights on | ||
| Weights on and | ||
| Update | ||
| Final weights | The vector , which is the coefficients of with the book's labels reversed | The coefficients of |
| Final generator | ||
| Proof contents | , final entry | , , final scalar |
The Halo 2 background page uses a third labelling, in which pairs the lower half of with the upper half of . Its is therefore this chapter's , so a reader comparing the sources should check the pairing and not only the letter. Later chapters on this site use the symmetric 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:
- Folding a vector in half creates two cross terms, and weights that are inverses of each other keep the original commitment intact.
- One round sends and , receives , and turns into , which is a claim of the same shape at half the length.
- After rounds the prover sends one scalar, so the proof has points and one scalar.
- 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.
- The verifier's remaining cost is , an MSM of size , while takes only factors because of the product form of .
- Ragu's code uses one-sided weights, moves the value into the constant coefficient, adds a mask, and scales 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.