Skip to content

Conversation

Tabaie
Copy link
Contributor

@Tabaie Tabaie commented Jun 3, 2025

  • Change the GKR API so that defining the circuit and assigning instances are separated. It removes the necessity of defining hints for the output and deferred "finalize" functions.

  • Remove the Println function in favor of GetValue.

  • gkrgates.Register now checks if a gate of the same name has already been registered. If so, it compares the gates and returns an error if they are not equal. If they are equal, it returns false, nil.

  • Remove all logic pertaining to reordering of wires and instances.

  • Unless the user explicitly provides their own initial challenge, automatically use all input and output of the circuit as initial Fiat-Shamir challenge.

  • Rename GkrCompressions to GkrPermutations to correctly reflect its function. (Note: a future PR should implement compressions instead and revert the name change)

  • A benchmark for gkr-poseidon2


Note

Introduce a compile-then-add-instance GKR API with new hints, stricter gate registration/equality checks, Poseidon2 integration update, and utility/type cleanups.

  • GKR API:
    • Add compile-time circuit (Compile) and per-instance addition (AddInstance) with GetValue; auto Fiat–Shamir challenge from I/O; remove Println/test-engine paths; pad instances to power of 2.
  • Hints/Solver:
    • New NewSolvingData, GetAssignmentHint; rework SolveHint/ProveHint to per-instance flow and padding; update solver overrides across all curves.
  • Gate Registry (constraint/solver/gkrgates):
    • Register now returns (registered bool, error), prevents re-registration, verifies gate equality (added EqualGateFunction), and supports curve allowances; tests updated.
  • Types/Info:
    • Simplify gkrinfo/gkrtypes (remove reorder/permutations, compute unique outputs internally) and add helpers for assignments.
  • Poseidon2:
    • Replace compression helper with GkrCompressor using the new API; register gates per-curve; adapt tests/benchmarks.
  • Utils/Docs:
    • Add ExtendRepeatLast; prune unused algos; minor doc/typo fixes.

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

@Tabaie Tabaie requested a review from Copilot June 3, 2025 20:27
Copilot

This comment was marked as outdated.

@Tabaie Tabaie marked this pull request as ready for review June 3, 2025 21:05
@Tabaie Tabaie requested review from ivokub and gbotrel June 3, 2025 21:05
@Tabaie Tabaie requested a review from Copilot June 3, 2025 21:15
@Tabaie Tabaie marked this pull request as draft June 10, 2025 22:03
@Tabaie Tabaie marked this pull request as ready for review June 10, 2025 22:55
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

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 think the idea is good, makes it a bit simpler to use in some cases. But I would leave the current way to import variables for backwards compatibility to avoid breaking all downstream GKR usage. We could implement the changes in Linea ourselves, but gnark is also used elsewhere and breaking API changes essentially prevent our users to upgrade gnark easily. It also creates some issues when we want to backport some hotfixes for Linea as we would need to start maintaining separate branch preceeding the API breakage.

In general, I would recommend adding a lot more method documentation what some methods do, what are the inputs, what are the outputs and what are the assumptions which may cause the methods to return errors. Right now many methods lack documentation so it is trial and error to figure out how to use them correctly.

Also keep in mind that try to keep type visibility consistent. It goes particularly for options -- when you have an option type and methods which return options then both the option type and these methods should be public. A la

type cfg struct {} // dont make it public unless you need to use configuration in other package

type Option func(*cfg) error // make Option type public, as it then also shows it in package documentation and groups all methods returning options together. NB! Don't forget to add documentation what the options are for, where you can use it etc.

func WithDoThis() Option { // function should be public so that can see in the pkg documentation. NB! Don't forget to add documentation what the method does, what is the behaviour if it is not set and what are conflicting options
    return func(conf *cfg) error {
        // now check here if this option is not conflicting with some existing configuration. A la when cfg has fields `doThis, doThat bool` then you should check that `doThat` is not already set and if it is then return an error
       if conf.doThat {
           return errors.New("WithDoThat option already set")
       }
   }
}

I reviewed mostly the coding part - it has become quite difficult to understand how the full GKR implementation works end to end and if it is sound, as there are already quite a lot of changes compared to the audited version. I'd add soundness tests i.e. what happens if you try to explicitly modify GKR outputs so that they are not consistent with the expected output. Or what happens if you overwrite GKR hints to modify the results.

}

for _, curve := range s.curves {
curvesForTesting := s.curves
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For gate degree testing - do we need an option at all? Cannot we just take some "good" modulus and test over that field. For me it makes a bit confusing that the user can choose the curve, but then needs to know the relation between the gate degree and modulus.

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 it may be best to pick a random 144 bit or so modulus and just test over that.

func Register(f gkr.GateFunction, nbIn int, options ...registerOption) error {
s := registerSettings{degree: -1, solvableVar: -1, name: GetDefaultGateName(f), curves: []ecc.ID{ecc.BN254}}
//
// If the gate is already registered, it will return false and no error.
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 need to return boolean to indicate if the gate was registered or not? When you return an error, then it is always false. And when the gate is already registered with same properties, then it is a no-op.

It seems you only use it for testing, I think it doesn't make sense to break APIs and add extra return values you don't use outside of tests.

// Attempt to register the zero gate without specifying a degree
registered, err := Register(zeroGate, 1, WithName(gateName))
assert.Error(t, err, "error must be returned for zero polynomial")
assert.EqualError(t, err, expectedError, "error message must match expected error")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is not idiomatic Go to compare errors by messages. If you really want to ensure that some error is what you expect, then define a new error type

var errSomeKnownError = errors.New("error msg")
///

err := functionThatReturnsErrors()
errors.Is(err, errSomeKnownError)

or if you want to add context to error then implement error interface

type parametricError struct { name string }
func (pe parametricError) Error() string {
    return fmt.Sprintf("error with parameter: %s", pe.name)
}

func functionThatReturnsErrors() error {
    return &parametricError{name: "aaaa"}
}

err := functionThatReturnsErrors()
var wantedError *parametricError
if errors.As(err, &parametricError) {
    // error is *parametricError
}

d.assignment = make(WireAssignment, len(d.circuit))
for i := range d.assignment {
d.assignment[i] = make([]fr.Element, info.NbInstances)
type newSolvingDataOption func(*newSolvingDataSettings)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be public type if With... options are public. Also function should return errors and you should check that the options are not incompatible with each other.

}

// AddInstance adds a new instance to the GKR circuit, returning the values of output variables for the instance.
func (c *Circuit) AddInstance(input map[gkr.Variable]frontend.Variable) (map[gkr.Variable]frontend.Variable, error) {
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 keep Import method for backwards compatibility. Otherwise you really break all downstream usage of GKR and do not give good migration path for upgrading gnark as the only option is to rewrite all GKR circuits. And I think keeping Import is trivial as you could just do loop of AddInstance inside.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #1589

if !c.toStore.Circuit[k].IsInput() {
return nil, fmt.Errorf("value provided for non-input variable %d", k)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if the cursor comment makes sense. I do not follow why you are comparing against len(c.ins)

c.toStore.ProveHintID = solver.GetHintID(c.hints.Prove)

forSnarkSorted := utils.MapRange(0, len(s.toStore.Circuit), slicePtrAt(forSnark.circuit))
forSnarkSorted := utils.MapRange(0, len(c.toStore.Circuit), slicePtrAt(forSnark.circuit))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo it is difficult to follow what is happening here. I need to know what MapRange and slicePtrAt does, but neither have documentation and slicePtrAt returns a function. I really recommend just doing small loops inline, otherwise I need to jump several times to see differeunt function definitions to understand what is going on and why.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, for example, here what you do I think is:

forSnarkSorted := make([]*gkrtypes.Wire, len(c.ToStore.Circuit))
for i := range forSnarkSorted {
    forSnarkSorted[i] = &forSnark.circuit[i]
}

and for me it is a lot more understandable and explicit. Now, you only use utils.MapRange and slicePtrAt only once, exactly here. So instead of four lines of explicit implementation there are several function definitions and more difficult reviewing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to one (documented) utility function rather than two.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@ivokub ivokub added the feat: gkr PRs related to GKR label Sep 11, 2025
cursor[bot]

This comment was marked as outdated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: gkr PRs related to GKR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enable the GKR API to add instances
2 participants