Skip to content

Conversation

@Tabaie
Copy link
Contributor

@Tabaie Tabaie commented Dec 10, 2025

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.

  • Constraint system / wiring:
    • Switch System.GkrInfo to []*gkrinfo.StoringInfo; replace AddGkr with NewGkr() returning (infoPtr, index) across all curves.
  • Solvers & hints:
    • Load multiple GKR infos via gkrtypes.NewSolvingInfo; wire new hints using gkrhints.TestEngineHints (GetAssignment, Solve, Prove).
    • Adjust hint IDs via csolver.GetHintID and use new per-curve NewSolvingData (padded instances), removing worker-pool deps; ensure challenges use SetBytesCanonical.
  • GKR types/registry:
    • Introduce gkrtypes.NewCircuit/NewSolvingInfo; augment Gate with allowed curves and SupportsCurve; add EqualGateFunction checks.
    • Rework gkrgates.Register API (options now validate/return errors), curve selection, duplicate registration reconciliation, and add lightweight verifier helpers.
  • std/gkrapi (public API overhaul):
    • New usage: gkrapi.New(api) → build circuit (e.g., NewInput, Gate, NamedGate) → Compile(hash)AddInstance (per-instance I/O); add Circuit.GetValue for debugging.
    • Remove old Solve/Verify and test-engine-only paths; integrate commitment-based initial challenge via multicommit.
  • Poseidon2 GKR integration:
    • Update to new API (GkrCompressor) and gate registration; restrict to BLS12-377 via WithCurves.
  • Utilities & misc:
    • Add utils.ExtendRepeatLast and utils.SliceOfRefs; cleanup unused helpers.
    • Minor fixes: correct MulAcc (mul instead of add), typos, and small-rational SetBytesCanonical.
    • Expose gkr.ComputeLogNbInstances; tweak tests accordingly.

Written by Cursor Bugbot for commit 72d954c. This will update automatically on new commits. Configure here.

Copy link
Collaborator

@ivokub ivokub left a 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.

gkrData := gkr.NewSolvingData(solvingInfo)
var gkrHints *gkrhints.TestEngineHints

newOpts := make([]csolver.Option, len(opts), len(opts)+3)
Copy link
Collaborator

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.

Copy link
Contributor Author

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)),
Copy link
Collaborator

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.

Copy link
Contributor Author

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
Copy link
Collaborator

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?

Copy link
Contributor Author

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 {
Copy link
Collaborator

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.

Copy link
Contributor Author

@Tabaie Tabaie Dec 19, 2025

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 {
Copy link
Collaborator

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.

Copy link
Contributor Author

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 {
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

return -1
// New creates a new GKR API
func New(api frontend.API) *API {
toStore, index := api.(gkrinfo.ConstraintSystem).NewGkr()
Copy link
Collaborator

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)

Copy link
Contributor Author

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)
Copy link
Collaborator

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?

Copy link
Contributor Author

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?

@ivokub ivokub added dep: linea Issues affecting Linea downstream feat: gkr PRs related to GKR labels Dec 17, 2025
@Tabaie Tabaie marked this pull request as ready for review December 19, 2025 04:28
@Tabaie Tabaie requested a review from ivokub December 19, 2025 04:28
for j := n; j < len(a[i]); j++ {
a[i][j] = a[i][j-1]
}
}
Copy link

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.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dep: linea Issues affecting Linea downstream feat: gkr PRs related to GKR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants