-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Foreign fields 5: Optimized multiplication of sums #1262
Conversation
Before giving an actual code review, I will start by writing comments on the above explanation.
This is true for FFMul, where the remainder is indeed given in compact form in the first 2 columns of the next row. But in the FFAdd gate, the result was given in 3-limb form, in the first 3 columns of the next row so it could chain easily with FFAdd and FFMul. Thus, what exactly are you referring to with the above sentence?
You mean one would naively range check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving some comments for now regarding collateral changes introduced in the PR, which are not just the FFMul optimization. Leaving the rest of it (the most remarkable part) for later (but not too much).
@querolita the equation that ffadd adds is |
I mean that you'd check |
This is the full constraint system for the
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first pass, looks great so far! Will continue later
// we assume that all summands si are bounded with si[2] <= f[2] checks, which implies si < 2^k where k := ceil(log(f)) | ||
// our assertion below gives us | ||
// |x|*|y| + q*f + |r| < (x.length * y.length) 2^2k + 2^2k + 2^2k < 3 * 2^(2*258) < 2^264 * (native modulus) | ||
assert( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the use of assert
in our code base, it makes the code flow much better and easier to read than constantly interrupting the flow with a bunch of verbose if (!/** **/) throw Error("..")
statements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spotted one possible thing (below); other than that looks amazing and makes sense.
This PR introduces a heavily optimized way to constrain foreign field expressions involving the product of two values or sums of values on the left side and a sum on the right side. Example:
A close analysis of the ffmul gate shows that for expressions like these, the ffmul gate is valid even without doing range checks or bounds checks on the sums
x - y
anda + b
(assuming that the original values x, y, a, b are range-checked as usual).Note that this is violating the assumptions that went into the original soundness proof of the ffmul gate, in multiple ways. At the bottom, we sketch an argument for why these assumptions can be weakened.
The API we introduce for adding these multiplication constraints uses a new "lazy sum" type, which allows optimal chaining and constraint-savings without leaking any of the involved complexity to the developer:
What happens under the hood
a + b
that go into the remainder are wired to the multiplication result directly, without any additional constraints. This has to do with the fact that the ffadd gate constrains its resulta + b
in compact limb form[r01, r2]
, and ffmul only uses the remainder in its compact form, so ffadd tells us enough about its output to be safely used in ffmul.assertMul(x, y, z)
) rather than a computation (mul(x, y) -> z
).x - y
which become a multiplication input, ffmul uses the two lower limbs individually, which aren't constrained by ffadd. Therefore, we add additional generic gates which perform roughly the ffadd logic, just on the lowest limb. This is enough to guarantee that ffadd-equivalent equations hold on all 3 limbs. This is cheaper than ensuring limbwise constraints using a multi-range check onx - y
.Results
Empirically, using
assertMul()
for the example equation(x - y)(x + y) === x^2 - y^2
lowers the constraint count from 32 to 20 compared to a naive version withmul()
,add()
andsub()
. Note that the naive version also suffers because it computes the left and right side separately, and so applies range-checks to the same result twice. This is another motivation for why an "assertion" API is better than a "computation" API.These savings are crucial for EC addition, which essentially only consists of three calls to
assertMul()
with different types of sum.Summary of changes
ForeignField.assertMul()
,ForeignField.Sum()
basic.ts
Unconstrained
API. to facilitate the generic gate computation inSum
, I had to do computations on an auxiliary bigint that are interleaved with provable code. I added theUnconstrained
class as a safe wrapper for auxiliary values like this, trying to realize some of @nholland94's ideas about boundaries between proof/auxiliary execution contextsSketch: Soundness of
assertMul()
r01
, and for the high limbr2
r0 = x0 + sign*y0 - overflow*f0 - carry*2^l
, we can use that in the eq. for both lower limbs and conclude the same thing forr1
separately|LHS - RHS| < n
which guarantee non-overflow are still valid. We can even allow "small negative" values (close to the native modulus) which might be caused by an adversarial carry in ffadd, nothing breaks.|x - y| <= |x| + |y|
. Ifx
andy
are small enough, the bound we need on|x - y|
is still guaranteed. TheassertMul()
gadget enforces a rigorous version of this argument which takes into account the number of summands, see here. We maintain the same assumption used in other gadgets, that inputs are bounds-checked relative to f, i.e.x[2] <= f[2]
, but depending on the number of summands we enforce a stronger bound on f than elsewhere (other gadgets enforce f < 2^259,assertMul()
enforces at leastf < 2^258
because we can no longer assume thatr
is "positive". It doesn't matter because in reality, we're only interested in f < 2^256 and that's supported even for pretty long sums.