-
Notifications
You must be signed in to change notification settings - Fork 501
feat: Enable multiple GKR sub-circuits #1661
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
base: master
Are you sure you want to change the base?
Conversation
Co-authored-by: Copilot <[email protected]>
ivokub
left a comment
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 still think the automated gate degree testing during registration makes the implementation a bit too complex. Seems ok implementation wise, but I'm not fully confident.
And imo restricting the gates being only used when compiled on particular curves adds also unnnecessary complexity (and most of the times we provide gnark.Curves() everywhere). For example in Poseidon2 package we correctly error out if someone wants to call the circuit on mismatching curve, so the assertion is already done.
For me gate registration is still way too verbose. Instead of
gkrgates.Register(extKeyGate(&p.RoundKeys[round][varIndex]), 2, gkrgates.WithUnverifiedDegree(1), gkrgates.WithUnverifiedSolvableVar(0), gkrgates.WithName(gateNames.linear(varIndex, round)), gkrgates.WithCurves(ecc.BLS12_377))
imo it makes so much more sense if I could do
gkrgates.Register(xtKeyGate(&p.RoundKeys[round][varIndex]), 2 /*inputs*/, 1 /*degree*/, "NAME-R1-V2")
And the one-by-one instance addition. Makes sense, but have you checked the profiles how does it affect circuit compile/solve time? I can imagine when doing one by one there is overhead?
I can try working on the PR tomorrow to wrap it finally up.
constraint/bls12-377/solver.go
Outdated
| gkrData := gkr.NewSolvingData(solvingInfo) | ||
| var gkrHints *gkrhints.TestEngineHints | ||
|
|
||
| newOpts := make([]csolver.Option, len(opts), len(opts)+3) |
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.
What is the point of allocating manually and then copying? Do you want to keep the initial opts argument intact?
If you just remove the manual allocation and copy, and only keep opts = append(opts, ...) then it achieves the same thing as append will allocate sufficient space and copy to fit everything.
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.
Yes for an extra safety guarantee. For example if the caller gives a sub-slice and we append to it, it could overwrite the super-slice if I'm not mistaken. It's unlikely for anyone to do that but it costs so little to prevent it.
| newOpts := make([]csolver.Option, len(opts), len(opts)+3) | ||
| copy(newOpts, opts) | ||
| opts = append(newOpts, | ||
| csolver.OverrideHint(csolver.GetHintID(gkrHints.GetAssignment), gkr.GetAssignmentHint(gkrData)), |
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 don't understhand, why do we call csolver.GetHintID(gkrHints.Solve)? You have just initialized gkrHints as a nil pointer -- so you use a method (*TestEngineHints)(nil).GetAssignment.
Instead we could just have a placeholder hint somewhere PlaceHolderGetAssignment at package level and use this. This is also how we do for API.Commit.
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.
That hint is not just a placeholder. It is the actual hint for the test engine. Because it computes (at the solving stage) and uses (at the proving stage) the full circuit assignments in the background, so it needs to be tied to an object for those to persist.
But the empty object is only used to get references to those functions.
| csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) | ||
|
|
||
| gkrData := gkr.NewSolvingData(solvingInfo) | ||
| var gkrHints *gkrhints.TestEngineHints |
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.
Why do we use TestEnghineHints? We run the actual solver here already?
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.
We're not actually using the test engine hints. This object is used to get references to the test engine hints for them to be replaced.
| // the curve's order, in which case the degree will be computed incorrectly. | ||
| func WithCurves(curves ...ecc.ID) registerOption { | ||
| return func(settings *registerSettings) { | ||
| func WithCurves(curves ...ecc.ID) RegisterOption { |
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.
The documentation does not match the implementation. Now in the registration you set allowedCurves based on the option.
For me this is complex - I think the gate should know itself where it can work (if it doesn't work over all fields) and then panic.
And I think we can still use the approach where we test the degree of the gate on a random modulus instead of letting the users define it themselves. Otherwise the user has to know what is the relation between gate degree and the provided modulus, which doesn't make sense as it is used for automatic degree testing.
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.
The documentation does not match the implementation
Fixed!
I think the gate should know itself where it can work (if it doesn't work over all fields) and then panic.
I prefer a compile time error to a solving/proving time panic.
And I think we can still use the approach where we test the degree of the gate on a random modulus instead of letting the users define it themselves.
That idea is appealing, but for example in gates for hash functions we have the permutation constants cached as fr.Elements, and if we run that on a random modulus the gate will have to panic, making degree testing fail.
| return fmt.Errorf("gate \"%s\" already registered with a different number of inputs (%d != %d)", s.name, g.NbIn(), nbIn) | ||
| } | ||
|
|
||
| for _, curve := range curvesForTesting { |
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 would remove the automatic gate degree testing. For me the implementation becomes more complex and actually more fragile to use (the user could mess up options and have unverfied wrong degree etc). I'd keep this PR only for supporting multiple GKR instances.
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'm open to removing the unverified degrees. We can mostly rely on automatic testing and verified user hints (useful for debugging.)
| "github.com/consensys/gnark/internal/utils" | ||
| ) | ||
|
|
||
| type TestEngineHints struct { |
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.
It is called TestEngineHints, but it is referenced during actual SNARK circuit solving?
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.
The test engine doesn't compile. It directly solves the circuit as it goes through the circuit Define function. So the default hints have to be compatible with the test engine, because unlike the R1CS / Plonkish case, we won't have the opportunity to replace them with the right ones later.
std/gkrapi/compile.go
Outdated
| return -1 | ||
| // New creates a new GKR API | ||
| func New(api frontend.API) *API { | ||
| toStore, index := api.(gkrinfo.ConstraintSystem).NewGkr() |
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 would handle the error better - right now if the API doesn't implement it we just get "doesn't implement the interface" panic, but it doesn't allow user to fix the issue (perhaps they wrapped their builder for adding functionality etc)
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.
The test engine, r1cs and scs APIs all implement this. Do we have a lot of use cases with user implemented APIs? If that's the case I agree we can return proper errors.
| return nil, fmt.Errorf("missing entry for input variable %d", wI) | ||
| } else { | ||
| hintIn[hintInI+2] = inV | ||
| c.assignments[wI] = append(c.assignments[wI], inV) |
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.
Could we potentially have a perf regression here? If we add instances one-by-one then we append there the slices also one by one. If we have 50k instances, then it just adds up time- and allocation wise.
Thats why it would be nice to keep the previous approach as well -- then we know beforehand how big are the assignemnts and allocate only once. And we could also do solving only once?
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.
Could we potentially have a perf regression here?
I think so but since this is only run during compile time (or with the test engine) I figured there's no need to worry much about perf. If we care about it I think a Circuit.Grow function similar to bytes.Buffer would be a good idea. Thoughts?
Thats why it would be nice to keep the previous approach as well
There's another PR that brings back v1. Would you like me to merge it into this?
| for j := n; j < len(a[i]); j++ { | ||
| a[i][j] = a[i][j-1] | ||
| } | ||
| } |
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.
Bug: Panic on zero instances in repeatUntilEnd
The repeatUntilEnd function's comment states it requires n > 0, but there's no validation. If a GKR circuit is registered with NbInstances = 0, then ecc.NextPowerOfTwo(0) returns 1, creating assignment arrays of length 1. When ProveHint calls repeatUntilEnd(0), the inner loop starts at j = 0 and attempts a[i][0] = a[i][-1], causing an index out of bounds panic instead of returning a meaningful error. The same issue exists in the inline padding loop within NewSolvingData.
Note: deserialization tests are failing due to a change in the structure of constraint systems. Rather than a single "gkr info" sub-object, we now have a slice of them.
Note
Adds multi-GKR support (per-circuit), replaces AddGkr with NewGkr, updates solvers/hints and GKR API, and refactors GKR types/registry with minor fixes.
System.GkrInfoto[]*gkrinfo.StoringInfo; replaceAddGkrwithNewGkr()returning(infoPtr, index)across all curves.gkrtypes.NewSolvingInfo; wire new hints usinggkrhints.TestEngineHints(GetAssignment,Solve,Prove).csolver.GetHintIDand use new per-curveNewSolvingData(padded instances), removing worker-pool deps; ensure challenges useSetBytesCanonical.gkrtypes.NewCircuit/NewSolvingInfo; augmentGatewith allowedcurvesandSupportsCurve; addEqualGateFunctionchecks.gkrgates.RegisterAPI (options now validate/return errors), curve selection, duplicate registration reconciliation, and add lightweight verifier helpers.gkrapi.New(api)→ build circuit (e.g.,NewInput,Gate,NamedGate) →Compile(hash)→AddInstance(per-instance I/O); addCircuit.GetValuefor debugging.Solve/Verifyand test-engine-only paths; integrate commitment-based initial challenge viamulticommit.GkrCompressor) and gate registration; restrict toBLS12-377viaWithCurves.utils.ExtendRepeatLastandutils.SliceOfRefs; cleanup unused helpers.MulAcc(mul instead of add), typos, and small-rationalSetBytesCanonical.gkr.ComputeLogNbInstances; tweak tests accordingly.Written by Cursor Bugbot for commit 72d954c. This will update automatically on new commits. Configure here.