-
Notifications
You must be signed in to change notification settings - Fork 471
feat: GKR Add Instance #1504
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?
feat: GKR Add Instance #1504
Conversation
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 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) { |
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 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.
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.
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 |
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.
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. |
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 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") |
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 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 ¶metricError{name: "aaaa"}
}
err := functionThatReturnsErrors()
var wantedError *parametricError
if errors.As(err, ¶metricError) {
// 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) |
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.
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) { |
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 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) | ||
} | ||
} |
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.
Check if the cursor comment makes sense. I do not follow why you are comparing against len(c.ins)
std/gkrapi/compile.go
Outdated
|
||
forSnarkSorted := utils.MapRange(0, len(s.toStore.Circuit), slicePtrAt(forSnark.circuit)) | ||
forSnarkSorted := utils.MapRange(0, len(c.toStore.Circuit), slicePtrAt(forSnark.circuit)) |
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.
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.
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.
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.
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.
Changed to one (documented) utility function rather than two.
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: 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.
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 ofGetValue
.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 returnsfalse, 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
toGkrPermutations
to correctly reflect its function. (Note: a future PR should implement compressions instead and revert the name change)A benchmark for gkr-poseidon2