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

@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 ready for review June 5, 2025 23:22
@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.

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.
func Register(f gkr.GateFunction, nbIn int, options ...registerOption) (registered bool, err error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be nice to make registerOption public. Otherwise it is a bit strange that you have a public method which returns instance of private type. It works but not imo idiomatic Go.

Copy link
Collaborator

Choose a reason for hiding this comment

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

And it usually is better to make registerOption as func(*registerSettings) error to return errors. It seems that some options are incompatible with each other and it would be good to detect during option parsing.

for _, option := range options {
option(&s)
}

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.

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.

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)


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
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: Empty Slice Padding Causes Out-of-Bounds Access

The ExtendRepeatLast function, WireAssignment.RepeatUntilEnd method, and the inline padding logic in NewSolvingData all share a bug. When the slice or segment being padded is empty, their loops attempt to access an element at index j-1 where j is 0, leading to an out-of-bounds panic.

Additional Locations (2)

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
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