diff --git a/ecc/bls12-377/fr/gkr/gkr.go b/ecc/bls12-377/fr/gkr/gkr.go index b12583063b..3305e0dae5 100644 --- a/ecc/bls12-377/fr/gkr/gkr.go +++ b/ecc/bls12-377/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/bls12-377/fr/gkr/gkr_test.go b/ecc/bls12-377/fr/gkr/gkr_test.go index be3d1d60b3..617c9bc39a 100644 --- a/ecc/bls12-377/fr/gkr/gkr_test.go +++ b/ecc/bls12-377/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/bls12-377/fr/gkr/registry.go b/ecc/bls12-377/fr/gkr/registry.go new file mode 100644 index 0000000000..64f13cc0a0 --- /dev/null +++ b/ecc/bls12-377/fr/gkr/registry.go @@ -0,0 +1,320 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/ecc/bls12-377/fr/poseidon2/gkrgates/gkrgates.go b/ecc/bls12-377/fr/poseidon2/gkrgates/gkrgates.go index f779f81920..83717b1220 100644 --- a/ecc/bls12-377/fr/poseidon2/gkrgates/gkrgates.go +++ b/ecc/bls12-377/fr/poseidon2/gkrgates/gkrgates.go @@ -19,55 +19,31 @@ import ( // extKeySBoxGate applies the external matrix mul, then adds the round key, then applies the sBox // because of its symmetry, we don't need to define distinct x1 and x2 versions of it -type extKeySBoxGate struct { - roundKey fr.Element -} - -func (g *extKeySBoxGate) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 2 inputs") +func extKeySBoxGate(roundKey *fr.Element) gkr.GateFunction { + return func(x ...fr.Element) fr.Element { + x[0]. + Double(&x[0]). + Add(&x[0], &x[1]). + Add(&x[0], roundKey) + return sBox2(x[0]) } - - x[0]. - Double(&x[0]). - Add(&x[0], &x[1]). - Add(&x[0], &g.roundKey) - return sBox2(x[0]) } -func (g *extKeySBoxGate) Degree() int { - return poseidon2.DegreeSBox() -} +// intKeySBoxGate2 applies the second row of internal matrix mul, then adds the round key, then applies the sBox, returning the second element +func intKeySBoxGate2(roundKey *fr.Element) gkr.GateFunction { + return func(x ...fr.Element) fr.Element { + x[0].Add(&x[0], &x[1]) + x[1]. + Double(&x[1]). + Add(&x[1], &x[0]). + Add(&x[1], roundKey) -// intKeySBoxGateFr applies the second row of internal matrix mul, then adds the round key, then applies the sBox -type intKeySBoxGate2 struct { - roundKey fr.Element -} - -func (g *intKeySBoxGate2) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 2 inputs") + return sBox2(x[1]) } - x[0].Add(&x[0], &x[1]) - x[1]. - Double(&x[1]). - Add(&x[1], &x[0]). - Add(&x[1], &g.roundKey) - - return sBox2(x[1]) -} - -func (g *intKeySBoxGate2) Degree() int { - return poseidon2.DegreeSBox() } // extAddGate (x,y,z) -> Ext . (x,y) + z -type extAddGate struct{} - -func (g extAddGate) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 3 { - panic("expected 3 inputs") - } +func extAddGate(x ...fr.Element) fr.Element { x[0]. Double(&x[0]). Add(&x[0], &x[1]). @@ -75,10 +51,6 @@ func (g extAddGate) Evaluate(x ...fr.Element) fr.Element { return x[0] } -func (g extAddGate) Degree() int { - return 1 -} - // sBox2 is Permutation.sBox for t=2 func sBox2(x fr.Element) fr.Element { var y fr.Element @@ -88,54 +60,29 @@ func sBox2(x fr.Element) fr.Element { // extKeyGate applies the external matrix mul, then adds the round key, then applies the sBox // because of its symmetry, we don't need to define distinct x1 and x2 versions of it -type extKeyGate struct { - roundKey fr.Element -} - -func (g *extKeyGate) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 2 inputs") +func extKeyGate(roundKey *fr.Element) func(...fr.Element) fr.Element { + return func(x ...fr.Element) fr.Element { + x[0]. + Double(&x[0]). + Add(&x[0], &x[1]). + Add(&x[0], roundKey) + return x[0] } - - x[0]. - Double(&x[0]). - Add(&x[0], &x[1]). - Add(&x[0], &g.roundKey) - return x[0] -} - -func (g *extKeyGate) Degree() int { - return 1 } // for x1, the partial round gates are identical to full round gates // for x2, the partial round gates are just a linear combination // extGate2 applies the external matrix mul, outputting the second element of the result -type extGate2 struct{} - -func (extGate2) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 2 inputs") - } +func extGate2(x ...fr.Element) fr.Element { x[1]. Double(&x[1]). Add(&x[1], &x[0]) return x[1] } -func (g extGate2) Degree() int { - return 1 -} - // intGate2 applies the internal matrix mul, returning the second element -type intGate2 struct { -} - -func (g intGate2) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 2 inputs") - } +func intGate2(x ...fr.Element) fr.Element { x[0].Add(&x[0], &x[1]) x[1]. Double(&x[1]). @@ -143,164 +90,159 @@ func (g intGate2) Evaluate(x ...fr.Element) fr.Element { return x[1] } -func (g intGate2) Degree() int { - return 1 -} - -// intKeySBoxGateFr applies the second row of internal matrix mul, then adds the round key, then applies the sBox -type intKeyGate2 struct { - roundKey fr.Element -} +// intKeyGate2 applies the second row of internal matrix mul, then adds the round key +func intKeyGate2(roundKey *fr.Element) gkr.GateFunction { + return func(x ...fr.Element) fr.Element { + x[0].Add(&x[0], &x[1]) + x[1]. + Double(&x[1]). + Add(&x[1], &x[0]). + Add(&x[1], roundKey) -func (g *intKeyGate2) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 2 inputs") + return x[1] } - x[0].Add(&x[0], &x[1]) - x[1]. - Double(&x[1]). - Add(&x[1], &x[0]). - Add(&x[1], &g.roundKey) - - return x[1] -} - -func (g *intKeyGate2) Degree() int { - return 1 } -type pow4Gate struct{} - -func (g pow4Gate) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 1 { - panic("expected 1 input") - } +// powGate4 x -> x⁴ +func pow4Gate(x ...fr.Element) fr.Element { x[0].Square(&x[0]).Square(&x[0]) return x[0] } -func (g pow4Gate) Degree() int { - return 4 +// pow4TimesGate x,y -> x⁴ * y +func pow4TimesGate(x ...fr.Element) fr.Element { + x[0].Square(&x[0]).Square(&x[0]).Mul(&x[0], &x[1]) + return x[0] } -type pow4TimesGate struct{} - -type pow2Gate struct{} - -func (g pow2Gate) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 1 { - panic("expected 1 input") - } +// pow2Gate x -> x² +func pow2Gate(x ...fr.Element) fr.Element { x[0].Square(&x[0]) return x[0] } -func (g pow2Gate) Degree() int { - return 2 +// pow2TimesGate x,y -> x² * y +func pow2TimesGate(x ...fr.Element) fr.Element { + x[0].Square(&x[0]).Mul(&x[0], &x[1]) + return x[0] } -type pow2TimesGate struct{} +const ( + Pow2GateName gkr.GateName = "pow2" + Pow4GateName gkr.GateName = "pow4" + Pow2TimesGateName gkr.GateName = "pow2Times" + Pow4TimesGateName gkr.GateName = "pow4Times" +) -func (g pow2TimesGate) Degree() int { - return 3 -} +type roundGateNamer string -func (g pow2TimesGate) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 2 input") - } - x[0].Square(&x[0]).Mul(&x[0], &x[1]) - return x[0] +// RoundGateNamer returns an object that returns standardized names for gates in the GKR circuit +func RoundGateNamer(p *poseidon2.Parameters) roundGateNamer { + return roundGateNamer(p.String()) } -func (g pow4TimesGate) Evaluate(x ...fr.Element) fr.Element { - if len(x) != 2 { - panic("expected 1 input") - } - x[0].Square(&x[0]).Square(&x[0]).Mul(&x[0], &x[1]) - return x[0] +// Linear is the name of a gate where a polynomial of total degree 1 is applied to the input +func (n roundGateNamer) Linear(varIndex, round int) gkr.GateName { + return gkr.GateName(fmt.Sprintf("x%d-l-op-round=%d;%s", varIndex, round, n)) } -func (g pow4TimesGate) Degree() int { - return 5 +// Integrated is the name of a gate where a polynomial of total degree 1 is applied to the input, followed by an S-box +func (n roundGateNamer) Integrated(varIndex, round int) gkr.GateName { + return gkr.GateName(fmt.Sprintf("x%d-i-op-round=%d;%s", varIndex, round, n)) } var initOnce sync.Once // RegisterGkrGates registers the Poseidon2 compression gates for GKR -func RegisterGkrGates() { +func RegisterGkrGates() error { const ( x = iota y ) - + var err error initOnce.Do( func() { p := poseidon2.GetDefaultParameters() halfRf := p.NbFullRounds / 2 - params := p.String() + gateNames := RoundGateNamer(p) - gkr.Gates["pow2"] = pow2Gate{} - gkr.Gates["pow4"] = pow4Gate{} - gkr.Gates["pow2Times"] = pow2TimesGate{} - gkr.Gates["pow4Times"] = pow4TimesGate{} - - gateNameLinear := func(varIndex, i int) string { - return fmt.Sprintf("x%d-l-op-round=%d;%s", varIndex, i, params) + if err = gkr.RegisterGate(Pow2GateName, pow2Gate, 1, gkr.WithUnverifiedDegree(2), gkr.WithNoSolvableVar()); err != nil { + return } - - gateNameIntegrated := func(varIndex, i int) string { - return fmt.Sprintf("x%d-i-op-round=%d;%s", varIndex, i, params) + if err = gkr.RegisterGate(Pow4GateName, pow4Gate, 1, gkr.WithUnverifiedDegree(4), gkr.WithNoSolvableVar()); err != nil { + return + } + if err = gkr.RegisterGate(Pow2TimesGateName, pow2TimesGate, 2, gkr.WithUnverifiedDegree(3), gkr.WithNoSolvableVar()); err != nil { + return + } + if err = gkr.RegisterGate(Pow4TimesGateName, pow4TimesGate, 2, gkr.WithUnverifiedDegree(5), gkr.WithNoSolvableVar()); err != nil { + return } - extKeySBox := func(round int, varIndex int) { - gkr.Gates[gateNameIntegrated(varIndex, round)] = &extKeySBoxGate{ // in case we use an integrated S-box - roundKey: p.RoundKeys[round][varIndex], - } - gkr.Gates[gateNameLinear(varIndex, round)] = &extKeyGate{ // in case we use a separate S-box - roundKey: p.RoundKeys[round][varIndex], + extKeySBox := func(round int, varIndex int) error { + if err := gkr.RegisterGate(gateNames.Integrated(varIndex, round), extKeySBoxGate(&p.RoundKeys[round][varIndex]), 2, gkr.WithUnverifiedDegree(poseidon2.DegreeSBox()), gkr.WithNoSolvableVar()); err != nil { + return err } + + return gkr.RegisterGate(gateNames.Linear(varIndex, round), extKeyGate(&p.RoundKeys[round][varIndex]), 2, gkr.WithUnverifiedDegree(1), gkr.WithUnverifiedSolvableVar(0)) } - intKeySBox2 := func(round int) { - gkr.Gates[gateNameLinear(y, round)] = &intKeyGate2{ - roundKey: p.RoundKeys[round][1], - } - gkr.Gates[gateNameIntegrated(y, round)] = &intKeySBoxGate2{ - roundKey: p.RoundKeys[round][1], + intKeySBox2 := func(round int) error { + if err := gkr.RegisterGate(gateNames.Linear(y, round), intKeyGate2(&p.RoundKeys[round][1]), 2, gkr.WithUnverifiedDegree(1), gkr.WithUnverifiedSolvableVar(0)); err != nil { + return err } + return gkr.RegisterGate(gateNames.Integrated(y, round), intKeySBoxGate2(&p.RoundKeys[round][1]), 2, gkr.WithUnverifiedDegree(poseidon2.DegreeSBox()), gkr.WithNoSolvableVar()) } - fullRound := func(i int) { - extKeySBox(i, x) - extKeySBox(i, y) + fullRound := func(i int) error { + if err := extKeySBox(i, x); err != nil { + return err + } + return extKeySBox(i, y) } for i := range halfRf { - fullRound(i) + if err = fullRound(i); err != nil { + return + } } { // i = halfRf: first partial round - extKeySBox(halfRf, x) - gkr.Gates[gateNameLinear(y, halfRf)] = extGate2{} + if err = extKeySBox(halfRf, x); err != nil { + return + } + if err = gkr.RegisterGate(gateNames.Linear(y, halfRf), extGate2, 2, gkr.WithUnverifiedDegree(1), gkr.WithUnverifiedSolvableVar(0)); err != nil { + return + } } for i := halfRf + 1; i < halfRf+p.NbPartialRounds; i++ { - extKeySBox(i, x) // for x1, intKeySBox is identical to extKeySBox - gkr.Gates[gateNameLinear(y, i)] = intGate2{} + if err = extKeySBox(i, x); err != nil { // for x1, intKeySBox is identical to extKeySBox + return + } + if err = gkr.RegisterGate(gateNames.Linear(y, i), intGate2, 2, gkr.WithUnverifiedDegree(1), gkr.WithUnverifiedSolvableVar(0)); err != nil { + return + } } { i := halfRf + p.NbPartialRounds - extKeySBox(i, x) - intKeySBox2(i) + if err = extKeySBox(i, x); err != nil { + return + } + if err = intKeySBox2(i); err != nil { + return + } } for i := halfRf + p.NbPartialRounds + 1; i < p.NbPartialRounds+p.NbFullRounds; i++ { - fullRound(i) + if err = fullRound(i); err != nil { + return + } } - gkr.Gates[gateNameLinear(y, p.NbPartialRounds+p.NbFullRounds)] = extAddGate{} + err = gkr.RegisterGate(gateNames.Linear(y, p.NbPartialRounds+p.NbFullRounds), extAddGate, 3, gkr.WithUnverifiedDegree(1), gkr.WithUnverifiedSolvableVar(0)) }, ) + return err } diff --git a/ecc/bls12-381/fr/gkr/gkr.go b/ecc/bls12-381/fr/gkr/gkr.go index 6020caf0c6..fd43958133 100644 --- a/ecc/bls12-381/fr/gkr/gkr.go +++ b/ecc/bls12-381/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/bls12-381/fr/gkr/gkr_test.go b/ecc/bls12-381/fr/gkr/gkr_test.go index f0b1d8cb18..5249eb561b 100644 --- a/ecc/bls12-381/fr/gkr/gkr_test.go +++ b/ecc/bls12-381/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/bls12-381/fr/gkr/registry.go b/ecc/bls12-381/fr/gkr/registry.go new file mode 100644 index 0000000000..30cc1273ff --- /dev/null +++ b/ecc/bls12-381/fr/gkr/registry.go @@ -0,0 +1,320 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/ecc/bls24-315/fr/gkr/gkr.go b/ecc/bls24-315/fr/gkr/gkr.go index 90a9d4ecc4..24ad81d32d 100644 --- a/ecc/bls24-315/fr/gkr/gkr.go +++ b/ecc/bls24-315/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/bls24-315/fr/gkr/gkr_test.go b/ecc/bls24-315/fr/gkr/gkr_test.go index faa75a38c1..12b0c0d463 100644 --- a/ecc/bls24-315/fr/gkr/gkr_test.go +++ b/ecc/bls24-315/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/bls24-315/fr/gkr/registry.go b/ecc/bls24-315/fr/gkr/registry.go new file mode 100644 index 0000000000..eaa05cedd6 --- /dev/null +++ b/ecc/bls24-315/fr/gkr/registry.go @@ -0,0 +1,320 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/ecc/bls24-317/fr/gkr/gkr.go b/ecc/bls24-317/fr/gkr/gkr.go index b777c28c0f..308eec8e39 100644 --- a/ecc/bls24-317/fr/gkr/gkr.go +++ b/ecc/bls24-317/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/bls24-317/fr/gkr/gkr_test.go b/ecc/bls24-317/fr/gkr/gkr_test.go index f3c3c0fc19..5116784d01 100644 --- a/ecc/bls24-317/fr/gkr/gkr_test.go +++ b/ecc/bls24-317/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/bls24-317/fr/gkr/registry.go b/ecc/bls24-317/fr/gkr/registry.go new file mode 100644 index 0000000000..28aad164b9 --- /dev/null +++ b/ecc/bls24-317/fr/gkr/registry.go @@ -0,0 +1,320 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/ecc/bn254/fr/gkr/gkr.go b/ecc/bn254/fr/gkr/gkr.go index 6ad757f068..cfc28364d6 100644 --- a/ecc/bn254/fr/gkr/gkr.go +++ b/ecc/bn254/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/bn254/fr/gkr/gkr_test.go b/ecc/bn254/fr/gkr/gkr_test.go index 69b90f3376..7e41436d0a 100644 --- a/ecc/bn254/fr/gkr/gkr_test.go +++ b/ecc/bn254/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/bn254/fr/gkr/registry.go b/ecc/bn254/fr/gkr/registry.go new file mode 100644 index 0000000000..3faeba1c58 --- /dev/null +++ b/ecc/bn254/fr/gkr/registry.go @@ -0,0 +1,320 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/ecc/bw6-633/fr/gkr/gkr.go b/ecc/bw6-633/fr/gkr/gkr.go index b8e527f09c..221b985c55 100644 --- a/ecc/bw6-633/fr/gkr/gkr.go +++ b/ecc/bw6-633/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/bw6-633/fr/gkr/gkr_test.go b/ecc/bw6-633/fr/gkr/gkr_test.go index 6c9eb0b2b6..ceda23276a 100644 --- a/ecc/bw6-633/fr/gkr/gkr_test.go +++ b/ecc/bw6-633/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/bw6-633/fr/gkr/registry.go b/ecc/bw6-633/fr/gkr/registry.go new file mode 100644 index 0000000000..dfe6d2f45e --- /dev/null +++ b/ecc/bw6-633/fr/gkr/registry.go @@ -0,0 +1,320 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/ecc/bw6-761/fr/gkr/gkr.go b/ecc/bw6-761/fr/gkr/gkr.go index 55e3cff858..c188d25aa7 100644 --- a/ecc/bw6-761/fr/gkr/gkr.go +++ b/ecc/bw6-761/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/bw6-761/fr/gkr/gkr_test.go b/ecc/bw6-761/fr/gkr/gkr_test.go index 7ca772041b..da68edf078 100644 --- a/ecc/bw6-761/fr/gkr/gkr_test.go +++ b/ecc/bw6-761/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/bw6-761/fr/gkr/registry.go b/ecc/bw6-761/fr/gkr/registry.go new file mode 100644 index 0000000000..fa5e2f6057 --- /dev/null +++ b/ecc/bw6-761/fr/gkr/registry.go @@ -0,0 +1,320 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/ecc/grumpkin/fr/gkr/gkr.go b/ecc/grumpkin/fr/gkr/gkr.go index 11be197780..d8868189da 100644 --- a/ecc/grumpkin/fr/gkr/gkr.go +++ b/ecc/grumpkin/fr/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...fr.Element) fr.Element - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...fr.Element) fr.Element + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []fr.Element) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...fr.Element) fr.Element { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...fr.Element) (res fr.Element) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...fr.Element) (res fr.Element) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...fr.Element) (diff fr.Element) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...fr.Element) (neg fr.Element) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x fr.Element - one.SetOne() - x := make([]fr.Element, nbIn) - y := make([]fr.Element, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/ecc/grumpkin/fr/gkr/gkr_test.go b/ecc/grumpkin/fr/gkr/gkr_test.go index 7400ac291b..d9692f2f58 100644 --- a/ecc/grumpkin/fr/gkr/gkr_test.go +++ b/ecc/grumpkin/fr/gkr/gkr_test.go @@ -99,7 +99,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]fr.Element) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, }} @@ -167,7 +167,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]fr for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]fr.Element, numInput) @@ -203,7 +203,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { func testSingleAddGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -223,7 +223,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -243,12 +243,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]fr.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -268,7 +268,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]fr.Element) { c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -291,11 +291,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -316,7 +316,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -353,7 +353,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -370,7 +370,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]fr.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []fr.Element) { +func setRandomSlice(slice []fr.Element) { for i := range slice { slice[i].MustSetRandom() } @@ -448,8 +448,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -511,8 +511,8 @@ func TestTopSortWide(t *testing.T) { } type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -545,7 +545,7 @@ func getCircuit(path string) (Circuit, error) { func (c CircuitInfo) toCircuit() (circuit Circuit) { circuit = make(Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = GetGate(c[i].Gate) circuit[i].Inputs = make([]*Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -555,22 +555,11 @@ func (c CircuitInfo) toCircuit() (circuit Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark fr.Element -} - -func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { +func mimcRound(input ...fr.Element) (res fr.Element) { var sum fr.Element sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -579,8 +568,21 @@ func (m mimcCipherGate) Evaluate(input ...fr.Element) (res fr.Element) { return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC GateName = "mimc" + SelectInput3 GateName = "select-input-3" +) + +func init() { + if err := RegisterGate(MiMC, mimcRound, 2, WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := RegisterGate(SelectInput3, func(input ...fr.Element) fr.Element { + return input[2] + }, 3, WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -728,44 +730,99 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g _select) Evaluate(in ...fr.Element) fr.Element { - return in[g] -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") -func (g _select) Degree() int { - return 1 -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } -func (g gateWrapper) Evaluate(inputs ...fr.Element) fr.Element { - return g.g.Evaluate(inputs...) + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) + + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res + } + + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } diff --git a/ecc/grumpkin/fr/gkr/registry.go b/ecc/grumpkin/fr/gkr/registry.go new file mode 100644 index 0000000000..d142823bba --- /dev/null +++ b/ecc/grumpkin/fr/gkr/registry.go @@ -0,0 +1,374 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/grumpkin/fr" + "github.com/consensys/gnark-crypto/ecc/grumpkin/fr/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + x := make(fr.Vector, degreeBound) + x.MustSetRandom() + for i := range x { + fIn[0] = x[i] + for j := range consts { + fIn[j+1].Mul(&x[i], &consts[j]) + } + p[i] = f(fIn...) + } + + // obtain p's coefficients + p, err := interpolate(x, p) + if err != nil { + panic(err) + } + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +// interpolate fits a polynomial of degree len(X) - 1 = len(Y) - 1 to the points (X[i], Y[i]) +// Note that the runtime is O(len(X)³) +func interpolate(X, Y []fr.Element) (polynomial.Polynomial, error) { + if len(X) != len(Y) { + return nil, errors.New("X and Y must have the same length") + } + + // solve the system of equations by Gaussian elimination + augmentedRows := make([][]fr.Element, len(X)) // the last column is the Y values + for i := range augmentedRows { + augmentedRows[i] = make([]fr.Element, len(X)+1) + augmentedRows[i][0].SetOne() + augmentedRows[i][1].Set(&X[i]) + for j := 2; j < len(augmentedRows[i])-1; j++ { + augmentedRows[i][j].Mul(&augmentedRows[i][j-1], &X[i]) + } + augmentedRows[i][len(augmentedRows[i])-1].Set(&Y[i]) + } + + // make the upper triangle + for i := range len(augmentedRows) - 1 { + // use row i to eliminate the ith element in all rows below + var negInv fr.Element + if augmentedRows[i][i].IsZero() { + return nil, errors.New("singular matrix") + } + negInv.Inverse(&augmentedRows[i][i]) + negInv.Neg(&negInv) + for j := i + 1; j < len(augmentedRows); j++ { + var c fr.Element + c.Mul(&augmentedRows[j][i], &negInv) + // augmentedRows[j][i].SetZero() omitted + for k := i + 1; k < len(augmentedRows[i]); k++ { + var t fr.Element + t.Mul(&augmentedRows[i][k], &c) + augmentedRows[j][k].Add(&augmentedRows[j][k], &t) + } + } + } + + // back substitution + res := make(polynomial.Polynomial, len(X)) + for i := len(augmentedRows) - 1; i >= 0; i-- { + res[i] = augmentedRows[i][len(augmentedRows[i])-1] + for j := i + 1; j < len(augmentedRows[i])-1; j++ { + var t fr.Element + t.Mul(&res[j], &augmentedRows[i][j]) + res[i].Sub(&res[i], &t) + } + res[i].Div(&res[i], &augmentedRows[i][i]) + } + + return res, nil +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...fr.Element) fr.Element { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/internal/generator/gkr/generate.go b/internal/generator/gkr/generate.go index 0d2e09e8f5..fb92112006 100644 --- a/internal/generator/gkr/generate.go +++ b/internal/generator/gkr/generate.go @@ -11,6 +11,7 @@ type Config struct { config.FieldDependency GenerateTests bool RetainTestCaseRawInfo bool + CanUseFFT bool OutsideGkrPackage bool TestVectorsRelativePath string } @@ -18,6 +19,7 @@ type Config struct { func Generate(config Config, baseDir string, bgen *bavard.BatchGenerator) error { entries := []bavard.Entry{ {File: filepath.Join(baseDir, "gkr.go"), Templates: []string{"gkr.go.tmpl"}}, + {File: filepath.Join(baseDir, "registry.go"), Templates: []string{"registry.go.tmpl"}}, } if config.GenerateTests { diff --git a/internal/generator/gkr/template/gkr.go.tmpl b/internal/generator/gkr/template/gkr.go.tmpl index 010d6ebba5..c27daa9b59 100644 --- a/internal/generator/gkr/template/gkr.go.tmpl +++ b/internal/generator/gkr/template/gkr.go.tmpl @@ -16,14 +16,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...{{.ElementType}}) {{.ElementType}} - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...{{.ElementType}}) {{.ElementType}} + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -686,12 +706,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -839,138 +860,4 @@ func frToBigInts(dst []*big.Int, src []{{.ElementType}}) { for i := range src { src[i].BigInt(dst[i]) } -} - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...{{.ElementType}}) {{.ElementType}} { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...{{.ElementType}}) (res {{.ElementType}}) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...{{.ElementType}}) (res {{.ElementType}}) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...{{.ElementType}}) (diff {{.ElementType}}) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...{{.ElementType}}) (neg {{.ElementType}}) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x {{.ElementType}} - one.SetOne() - x := make([]{{.ElementType}}, nbIn) - y := make([]{{.ElementType}}, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} +} \ No newline at end of file diff --git a/internal/generator/gkr/template/gkr.test.go.tmpl b/internal/generator/gkr/template/gkr.test.go.tmpl index cedcebe8be..378cb813e0 100644 --- a/internal/generator/gkr/template/gkr.test.go.tmpl +++ b/internal/generator/gkr/template/gkr.test.go.tmpl @@ -100,7 +100,7 @@ func generateTestMimc(numRounds int) func(*testing.T, ...[]{{.ElementType}}) { func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { circuit := Circuit{ Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{}, nbUniqueOutputs: 2, } } @@ -168,7 +168,7 @@ func testManyInstances(t *testing.T, numInput int, test func(*testing.T, ...[]{{ for i := range fullAssignments { fullAssignments[i] = make([]fr.Element, maxSize) - setRandom(fullAssignments[i]) + setRandomSlice(fullAssignments[i]) } inputAssignments := make([][]{{.ElementType}}, numInput) @@ -204,7 +204,7 @@ func testNoGate(t *testing.T, inputAssignments ...[]{{.ElementType}}) { func testSingleAddGate(t *testing.T, inputAssignments ...[]{{.ElementType}}) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["add"], + Gate: GetGate(Add2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -224,7 +224,7 @@ func testSingleMulGate(t *testing.T, inputAssignments ...[]{{.ElementType}}) { c := make(Circuit, 3) c[2] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[0], &c[1]}, } @@ -244,12 +244,12 @@ func testSingleInputTwoIdentityGates(t *testing.T, inputAssignments ...[]{{.Elem c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } @@ -269,7 +269,7 @@ func testSingleMimcCipherGate(t *testing.T, inputAssignments ...[]{{.ElementType c := make(Circuit, 3) c[2] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[0], &c[1]}, } @@ -292,11 +292,11 @@ func testSingleInputTwoIdentityGatesComposed(t *testing.T, inputAssignments ...[ c := make(Circuit, 3) c[1] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[0]}, } c[2] = Wire{ - Gate: IdentityGate{}, + Gate: GetGate(Identity), Inputs: []*Wire{&c[1]}, } @@ -317,7 +317,7 @@ func mimcCircuit(numRounds int) Circuit { for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: mimcCipherGate{}, + Gate: GetGate("mimc"), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -354,7 +354,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]{{.El for i := 2; i < len(c); i++ { c[i] = Wire{ - Gate: Gates["mul"], + Gate: GetGate(Mul2), Inputs: []*Wire{&c[i-1], &c[0]}, } } @@ -371,7 +371,7 @@ func testATimesBSquared(t *testing.T, numRounds int, inputAssignments ...[]{{.El assert.NotNil(t, err, "bad proof accepted") } -func setRandom(slice []{{.ElementType}}) { +func setRandomSlice(slice []{{.ElementType}}) { for i := range slice { slice[i].MustSetRandom() } @@ -449,8 +449,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]fr.Element, nbInstances) in1 := make([]fr.Element, nbInstances) - setRandom(in0) - setRandom(in1) + setRandomSlice(in0) + setRandomSlice(in1) fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -513,34 +513,99 @@ func TestTopSortWide(t *testing.T) { {{template "gkrTestVectors" .}} -// gateWrapper enables assigning an arbitrary degree to a gate -type gateWrapper struct { - g Gate - d int -} +func TestRegisterGateDegreeDetection(t *testing.T) { + testGate := func(name GateName, f func(...fr.Element) fr.Element, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" -func (g gateWrapper) Degree() int { - return g.d -} + assert.NoError(t, RegisterGate(name, f, nbIn, WithDegree(degree)), "given degree must be accepted") + + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree-1)), "lower degree must be rejected") + + assert.Error(t, RegisterGate(name, f, nbIn, WithDegree(degree+1)), "higher degree must be rejected") + + assert.NoError(t, RegisterGate(name, f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, GetGate(name).Degree(), "degree must be detected correctly") + }) + } + + testGate("select", func(x ...fr.Element) fr.Element { + return x[0] + }, 3, 1) + + testGate("add2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Add(&x[0], &x[1]) + res.Add(&res, &x[2]) + return res + }, 3, 1) + + testGate("mul2", func(x ...fr.Element) fr.Element { + var res fr.Element + res.Mul(&x[0], &x[1]) + return res + }, 2, 2) + + testGate("mimc", mimcRound, 2, 7) + + testGate("sub2PlusOne", func(x ...fr.Element) fr.Element { + var res fr.Element + res. + SetOne(). + Add(&res, &x[0]). + Sub(&res, &x[1]) + return res + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, errZeroFunction) + zeroGate := func(x ...fr.Element) fr.Element { + var res fr.Element + return res + } + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1)) -func (g gateWrapper) Evaluate(inputs ...{{.ElementType}}) {{.ElementType}} { - return g.g.Evaluate(inputs...) + assert.Equal(t, expectedError, RegisterGate(gateName, zeroGate, 1, WithDegree(2))) + }) } -func TestTestGateDegree(t *testing.T) { - onGate := func(g Gate, nbIn int) func(t *testing.T) { - return func(t *testing.T) { - w := gateWrapper{g: g, d: g.Degree()} - assert.NoError(t, TestGateDegree(w, nbIn), "must succeed on the gate itself") - w.d-- - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an underreported degree") - w.d += 2 - assert.Error(t, TestGateDegree(w, nbIn), "must fail on an over-reported degree") +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("bivariate input needed") } + var res fr.Element + res.Add(&x[0], &x[1]) + res.Mul(&res, &x[0]) + return res + } + + // g: x,y -> x² + 3y + g := func(x ...fr.Element) fr.Element { + var res, y3 fr.Element + res.Square(&x[0]) + y3.Mul(&x[1], &three) + res.Add(&res, &y3) + return res } - t.Run("select", onGate(_select(0), 1)) - t.Run("add", onGate(Gates["add"], 2)) - t.Run("mul", onGate(Gates["mul"], 2)) - t.Run("mimc", onGate(mimcCipherGate{}, 2)) + // h: x -> 2x + // but it edits it input + h := func(x ...fr.Element) fr.Element { + x[0].Double(&x[0]) + return x[0] + } + + assert.False(t, GateFunction(f).isAdditive(1, 2)) + assert.False(t, GateFunction(f).isAdditive(0, 2)) + + assert.False(t, GateFunction(g).isAdditive(0, 2)) + assert.True(t, GateFunction(g).isAdditive(1, 2)) + + assert.True(t, GateFunction(h).isAdditive(0, 1)) } \ No newline at end of file diff --git a/internal/generator/gkr/template/gkr.test.vectors.gen.go.tmpl b/internal/generator/gkr/template/gkr.test.vectors.gen.go.tmpl index 7bf9cea035..832188f3d3 100644 --- a/internal/generator/gkr/template/gkr.test.vectors.gen.go.tmpl +++ b/internal/generator/gkr/template/gkr.test.vectors.gen.go.tmpl @@ -120,6 +120,4 @@ func toPrintableProof(proof gkr.Proof) (PrintableProof, error) { return res, nil } -var Gates = gkr.Gates - {{template "gkrTestVectors" .}} \ No newline at end of file diff --git a/internal/generator/gkr/template/gkr.test.vectors.go.tmpl b/internal/generator/gkr/template/gkr.test.vectors.go.tmpl index 6208b25986..0025b0164a 100644 --- a/internal/generator/gkr/template/gkr.test.vectors.go.tmpl +++ b/internal/generator/gkr/template/gkr.test.vectors.go.tmpl @@ -10,8 +10,13 @@ {{$Wire := print $GkrPackagePrefix "Wire"}} {{$CircuitLayer := print $GkrPackagePrefix "CircuitLayer"}} +{{$PackagePrefix := ""}} +{{- if .OutsideGkrPackage}} + {{$PackagePrefix = "gkr."}} +{{end}} + type WireInfo struct { - Gate string `json:"gate"` + Gate {{$PackagePrefix}}GateName `json:"gate"` Inputs []int `json:"inputs"` } @@ -45,7 +50,7 @@ func getCircuit(path string) ({{$Circuit}}, error) { func (c CircuitInfo) toCircuit() (circuit {{$Circuit}}) { circuit = make({{$Circuit}}, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = {{$PackagePrefix}}GetGate(c[i].Gate) circuit[i].Inputs = make([]*{{$Wire}}, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -55,32 +60,34 @@ func (c CircuitInfo) toCircuit() (circuit {{$Circuit}}) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark {{.ElementType}} -} - -func (m mimcCipherGate) Evaluate(input ...{{.ElementType}}) (res {{.ElementType}}) { +func mimcRound(input ...{{.ElementType}}) (res {{.ElementType}}) { var sum {{.ElementType}} sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - - res.Square(&sum) // sum^2 - res.Mul(&res, &sum) // sum^3 - res.Square(&res) //sum^6 - res.Mul(&res, &sum) //sum^7 + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark + res.Square(&sum) // sum^2 + res.Mul(&res, &sum) // sum^3 + res.Square(&res) //sum^6 + res.Mul(&res, &sum) //sum^7 return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC {{$PackagePrefix}}GateName = "mimc" + SelectInput3 {{$PackagePrefix}}GateName = "select-input-3" +) + +func init() { + if err := {{$PackagePrefix}}RegisterGate(MiMC, mimcRound, 2, {{$PackagePrefix}}WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := {{$PackagePrefix}}RegisterGate(SelectInput3, func(input ...{{.ElementType}}) {{.ElementType}} { + return input[2] + }, 3, {{$PackagePrefix}}WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -236,16 +243,6 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } -type _select int - -func (g _select) Evaluate(in ...{{.ElementType}}) {{.ElementType}} { - return in[g] -} - -func (g _select) Degree() int { - return 1 -} - {{end}} {{- define "setElement element value elementType"}} diff --git a/internal/generator/gkr/template/registry.go.tmpl b/internal/generator/gkr/template/registry.go.tmpl new file mode 100644 index 0000000000..75ca8d0267 --- /dev/null +++ b/internal/generator/gkr/template/registry.go.tmpl @@ -0,0 +1,390 @@ +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "{{.FieldPackagePath}}" + {{- if .CanUseFFT }} + "{{.FieldPackagePath}}/fft"{{- else}} + "errors"{{- end }} + "{{.FieldPackagePath}}/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make({{.FieldPackageName}}.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]{{.ElementType}}, nbIn) + consts := make({{.FieldPackageName}}.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + {{- if .CanUseFFT }} + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := {{.FieldPackageName}}.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i] = f(fIn...) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + {{- else }} + x := make({{.FieldPackageName}}.Vector, degreeBound) + x.MustSetRandom() + for i := range x { + fIn[0] = x[i] + for j := range consts { + fIn[j+1].Mul(&x[i], &consts[j]) + } + p[i] = f(fIn...) + } + + // obtain p's coefficients + p, err := interpolate(x, p) + if err != nil { + panic(err) + } + {{- end }} + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max)+1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +{{- if not .CanUseFFT }} +// interpolate fits a polynomial of degree len(X) - 1 = len(Y) - 1 to the points (X[i], Y[i]) +// Note that the runtime is O(len(X)³) +func interpolate(X, Y []{{.ElementType}}) (polynomial.Polynomial, error) { + if len(X) != len(Y) { + return nil, errors.New("X and Y must have the same length") + } + + // solve the system of equations by Gaussian elimination + augmentedRows := make([][]{{.ElementType}}, len(X)) // the last column is the Y values + for i := range augmentedRows { + augmentedRows[i] = make([]{{.ElementType}}, len(X)+1) + augmentedRows[i][0].SetOne() + augmentedRows[i][1].Set(&X[i]) + for j := 2; j < len(augmentedRows[i])-1; j++ { + augmentedRows[i][j].Mul(&augmentedRows[i][j-1], &X[i]) + } + augmentedRows[i][len(augmentedRows[i])-1].Set(&Y[i]) + } + + // make the upper triangle + for i := range len(augmentedRows) - 1 { + // use row i to eliminate the ith element in all rows below + var negInv {{.ElementType}} + if augmentedRows[i][i].IsZero() { + return nil, errors.New("singular matrix") + } + negInv.Inverse(&augmentedRows[i][i]) + negInv.Neg(&negInv) + for j := i + 1; j < len(augmentedRows); j++ { + var c {{.ElementType}} + c.Mul(&augmentedRows[j][i], &negInv) + // augmentedRows[j][i].SetZero() omitted + for k := i + 1; k < len(augmentedRows[i]); k++ { + var t {{.ElementType}} + t.Mul(&augmentedRows[i][k], &c) + augmentedRows[j][k].Add(&augmentedRows[j][k], &t) + } + } + } + + // back substitution + res := make(polynomial.Polynomial, len(X)) + for i := len(augmentedRows) - 1; i >= 0; i-- { + res[i] = augmentedRows[i][len(augmentedRows[i])-1] + for j := i + 1; j < len(augmentedRows[i])-1; j++ { + var t {{.ElementType}} + t.Mul(&res[j], &augmentedRows[i][j]) + res[i].Sub(&res[i], &t) + } + res[i].Div(&res[i], &augmentedRows[i][i]) + } + + return res, nil +} +{{- end }} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...{{.ElementType}}) {{.ElementType}} { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...{{.ElementType}}) {{.ElementType}} { + var res {{.ElementType}} + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...{{.ElementType}}) {{.ElementType}} { + var res {{.ElementType}} + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...{{.ElementType}}) {{.ElementType}} { + var res {{.ElementType}} + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...{{.ElementType}}) {{.ElementType}} { + var res {{.ElementType}} + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} \ No newline at end of file diff --git a/internal/generator/gkr/test_vectors/main.go b/internal/generator/gkr/test_vectors/main.go index 96ed2b453b..0bb86739af 100644 --- a/internal/generator/gkr/test_vectors/main.go +++ b/internal/generator/gkr/test_vectors/main.go @@ -126,11 +126,9 @@ func toPrintableProof(proof gkr.Proof) (PrintableProof, error) { return res, nil } -var Gates = gkr.Gates - type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` + Gate gkr.GateName `json:"gate"` + Inputs []int `json:"inputs"` } type CircuitInfo []WireInfo @@ -163,7 +161,7 @@ func getCircuit(path string) (gkr.Circuit, error) { func (c CircuitInfo) toCircuit() (circuit gkr.Circuit) { circuit = make(gkr.Circuit, len(c)) for i := range c { - circuit[i].Gate = Gates[c[i].Gate] + circuit[i].Gate = gkr.GetGate(c[i].Gate) circuit[i].Inputs = make([]*gkr.Wire, len(c[i].Inputs)) for k, inputCoord := range c[i].Inputs { input := &circuit[inputCoord] @@ -173,22 +171,11 @@ func (c CircuitInfo) toCircuit() (circuit gkr.Circuit) { return } -func init() { - Gates["mimc"] = mimcCipherGate{} //TODO: Add ark - Gates["select-input-3"] = _select(2) -} - -type mimcCipherGate struct { - ark small_rational.SmallRational -} - -func (m mimcCipherGate) Evaluate(input ...small_rational.SmallRational) (res small_rational.SmallRational) { +func mimcRound(input ...small_rational.SmallRational) (res small_rational.SmallRational) { var sum small_rational.SmallRational sum. - Add(&input[0], &input[1]). - Add(&sum, &m.ark) - + Add(&input[0], &input[1]) //.Add(&sum, &m.ark) TODO: add ark res.Square(&sum) // sum^2 res.Mul(&res, &sum) // sum^3 res.Square(&res) //sum^6 @@ -197,8 +184,21 @@ func (m mimcCipherGate) Evaluate(input ...small_rational.SmallRational) (res sma return } -func (m mimcCipherGate) Degree() int { - return 7 +const ( + MiMC gkr.GateName = "mimc" + SelectInput3 gkr.GateName = "select-input-3" +) + +func init() { + if err := gkr.RegisterGate(MiMC, mimcRound, 2, gkr.WithUnverifiedDegree(7)); err != nil { + panic(err) + } + + if err := gkr.RegisterGate(SelectInput3, func(input ...small_rational.SmallRational) small_rational.SmallRational { + return input[2] + }, 3, gkr.WithUnverifiedDegree(1)); err != nil { + panic(err) + } } type PrintableProof []PrintableSumcheckProof @@ -347,13 +347,3 @@ func newTestCase(path string) (*TestCase, error) { return tCase, nil } - -type _select int - -func (g _select) Evaluate(in ...small_rational.SmallRational) small_rational.SmallRational { - return in[g] -} - -func (g _select) Degree() int { - return 1 -} diff --git a/internal/generator/gkr/test_vectors/resources/single_input_two_outs.json b/internal/generator/gkr/test_vectors/resources/single_input_two_outs.json index c577c1cace..3a39e5625f 100644 --- a/internal/generator/gkr/test_vectors/resources/single_input_two_outs.json +++ b/internal/generator/gkr/test_vectors/resources/single_input_two_outs.json @@ -4,7 +4,7 @@ "inputs": [] }, { - "gate": "mul", + "gate": "mul2", "inputs": [0, 0] }, { diff --git a/internal/generator/gkr/test_vectors/resources/single_mul_gate.json b/internal/generator/gkr/test_vectors/resources/single_mul_gate.json index 0f65a07edf..d009ebe03d 100644 --- a/internal/generator/gkr/test_vectors/resources/single_mul_gate.json +++ b/internal/generator/gkr/test_vectors/resources/single_mul_gate.json @@ -8,7 +8,7 @@ "inputs": [] }, { - "gate": "mul", + "gate": "mul2", "inputs": [0, 1] } ] \ No newline at end of file diff --git a/internal/generator/main.go b/internal/generator/main.go index 940f9d76f1..11e2b98dba 100644 --- a/internal/generator/main.go +++ b/internal/generator/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/consensys/gnark-crypto/internal/generator/gkr" "os" "os/exec" "path/filepath" @@ -21,7 +22,6 @@ import ( "github.com/consensys/gnark-crypto/internal/generator/edwards/eddsa" "github.com/consensys/gnark-crypto/internal/generator/fflonk" fri "github.com/consensys/gnark-crypto/internal/generator/fri/template" - "github.com/consensys/gnark-crypto/internal/generator/gkr" "github.com/consensys/gnark-crypto/internal/generator/hash_to_field" "github.com/consensys/gnark-crypto/internal/generator/iop" "github.com/consensys/gnark-crypto/internal/generator/kzg" @@ -76,9 +76,22 @@ func main() { conf.FpUnusedBits = 64 - (conf.Fp.NbBits % 64) + frInfo := config.FieldDependency{ + FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + conf.Name + "/fr", + FieldPackageName: "fr", + ElementType: "fr.Element", + } + + gkrConfig := gkr.Config{ + FieldDependency: frInfo, + GenerateTests: true, + TestVectorsRelativePath: "../../../../internal/generator/gkr/test_vectors", + } + frOpts := []generator.Option{generator.WithASM(asmConfig)} if !(conf.Equal(config.STARK_CURVE) || conf.Equal(config.SECP256K1) || conf.Equal(config.GRUMPKIN)) { frOpts = append(frOpts, generator.WithFFT(fftConfig)) + gkrConfig.CanUseFFT = true } if conf.Equal(config.BLS12_377) { frOpts = append(frOpts, generator.WithSIS()) @@ -100,31 +113,22 @@ func main() { return } + // generate gkr on fr + // GKR tests use MiMC. Once SECP256K1 has a mimc implementation, we can generate GKR for it. + assertNoError(gkr.Generate(gkrConfig, filepath.Join(curveDir, "fr", "gkr"), bgen)) + // generate mimc on fr assertNoError(mimc.Generate(conf, filepath.Join(curveDir, "fr", "mimc"), bgen)) // generate poseidon2 on fr assertNoError(poseidon2.Generate(conf, filepath.Join(curveDir, "fr", "poseidon2"), bgen)) - frInfo := config.FieldDependency{ - FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + conf.Name + "/fr", - FieldPackageName: "fr", - ElementType: "fr.Element", - } - // generate polynomial on fr assertNoError(polynomial.Generate(frInfo, filepath.Join(curveDir, "fr", "polynomial"), true, bgen)) // generate sumcheck on fr assertNoError(sumcheck.Generate(frInfo, filepath.Join(curveDir, "fr", "sumcheck"), bgen)) - // generate gkr on fr - assertNoError(gkr.Generate(gkr.Config{ - FieldDependency: frInfo, - GenerateTests: true, - TestVectorsRelativePath: "../../../../internal/generator/gkr/test_vectors", - }, filepath.Join(curveDir, "fr", "gkr"), bgen)) - // generate test vector utils on fr assertNoError(test_vector_utils.Generate(test_vector_utils.Config{ FieldDependency: frInfo, diff --git a/internal/generator/polynomial/template/polynomial.go.tmpl b/internal/generator/polynomial/template/polynomial.go.tmpl index 52e2d055ab..6e4373091d 100644 --- a/internal/generator/polynomial/template/polynomial.go.tmpl +++ b/internal/generator/polynomial/template/polynomial.go.tmpl @@ -299,4 +299,4 @@ func computeLagrangeBasis(domainSize uint8) []Polynomial { } return res -} \ No newline at end of file +} diff --git a/internal/generator/test_vector_utils/generate.go b/internal/generator/test_vector_utils/generate.go index 216e8307dd..5b8bc05e9c 100644 --- a/internal/generator/test_vector_utils/generate.go +++ b/internal/generator/test_vector_utils/generate.go @@ -24,6 +24,7 @@ func GenerateRationals(bgen *bavard.BatchGenerator) error { }, GenerateTests: false, RetainTestCaseRawInfo: true, + CanUseFFT: false, TestVectorsRelativePath: "../../../gkr/test_vectors", } diff --git a/internal/generator/test_vector_utils/small_rational/gkr/gkr.go b/internal/generator/test_vector_utils/small_rational/gkr/gkr.go index eba02db0b9..3158701d32 100644 --- a/internal/generator/test_vector_utils/small_rational/gkr/gkr.go +++ b/internal/generator/test_vector_utils/small_rational/gkr/gkr.go @@ -21,14 +21,34 @@ import ( // The goal is to prove/verify evaluations of many instances of the same circuit -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(...small_rational.SmallRational) small_rational.SmallRational - Degree() int +// GateFunction a polynomial defining a gate. It may modify its input. The changes will be ignored. +type GateFunction func(...small_rational.SmallRational) small_rational.SmallRational + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + Evaluate GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of f + solvableVar int // if there is a solvable variable, its index, -1 otherwise +} + +// Degree returns the total degree of the gate's polynomial i.e. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns I such that x_I can always be determined from {x_i} - {x_I} and f(x...). If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn } type Wire struct { - Gate Gate + Gate *Gate Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) } @@ -690,12 +710,13 @@ func Verify(c Circuit, assignment WireAssignment, proof Proof, transcriptSetting // outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. func outputsList(c Circuit, indexes map[*Wire]int) [][]int { + idGate := GetGate("identity") res := make([][]int, len(c)) for i := range c { res[i] = make([]int, 0) c[i].nbUniqueOutputs = 0 if c[i].IsInput() { - c[i].Gate = IdentityGate{} + c[i].Gate = idGate } } ins := make(map[int]struct{}, len(c)) @@ -844,137 +865,3 @@ func frToBigInts(dst []*big.Int, src []small_rational.SmallRational) { src[i].BigInt(dst[i]) } } - -// Gates defined by name -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "sub": SubGate{}, - "neg": NegGate{}, - "mul": MulGate(2), -} - -type IdentityGate struct{} -type AddGate struct{} -type MulGate int -type SubGate struct{} -type NegGate struct{} - -func (IdentityGate) Evaluate(input ...small_rational.SmallRational) small_rational.SmallRational { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -func (g AddGate) Evaluate(x ...small_rational.SmallRational) (res small_rational.SmallRational) { - switch len(x) { - case 0: - // set zero - case 1: - res.Set(&x[0]) - default: - res.Add(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Add(&res, &x[i]) - } - } - return -} - -func (g AddGate) Degree() int { - return 1 -} - -func (g MulGate) Evaluate(x ...small_rational.SmallRational) (res small_rational.SmallRational) { - if len(x) != int(g) { - panic("wrong input count") - } - switch len(x) { - case 0: - res.SetOne() - case 1: - res.Set(&x[0]) - default: - res.Mul(&x[0], &x[1]) - for i := 2; i < len(x); i++ { - res.Mul(&res, &x[i]) - } - } - return -} - -func (g MulGate) Degree() int { - return int(g) -} - -func (g SubGate) Evaluate(element ...small_rational.SmallRational) (diff small_rational.SmallRational) { - if len(element) > 2 { - panic("not implemented") //TODO - } - diff.Sub(&element[0], &element[1]) - return -} - -func (g SubGate) Degree() int { - return 1 -} - -func (g NegGate) Evaluate(element ...small_rational.SmallRational) (neg small_rational.SmallRational) { - if len(element) != 1 { - panic("univariate gate") - } - neg.Neg(&element[0]) - return -} - -func (g NegGate) Degree() int { - return 1 -} - -// TestGateDegree checks if deg(g) = g.Degree() -func TestGateDegree(g Gate, nbIn int) error { - // TODO when Gate is a struct, turn this into a method - if nbIn < 1 { - return errors.New("at least one input required") - } - - d := g.Degree() - // evaluate g at 0 .. d - var one, _x small_rational.SmallRational - one.SetOne() - x := make([]small_rational.SmallRational, nbIn) - y := make([]small_rational.SmallRational, d+1) - for i := range y { - y[i] = g.Evaluate(x...) - _x.Add(&_x, &one) - for j := range x { - x[j] = _x - } - } - - p := polynomial.InterpolateOnRange(y) - // test if p matches g - if _, err := _x.SetRandom(); err != nil { - return err - } - for j := range x { - x[j] = _x - } - - if expected, encountered := p.Eval(&x[0]), g.Evaluate(x...); !expected.Equal(&encountered) { - return fmt.Errorf("interpolation failed: not a polynomial of degree %d or lower", d) - } - - // check if the degree is LESS than d - degP := d - for degP >= 0 && p[degP].IsZero() { - degP-- - } - if degP != d { - return fmt.Errorf("expected polynomial of degree %d, interpolation yielded %d", d, degP) - } - - return nil -} diff --git a/internal/generator/test_vector_utils/small_rational/gkr/registry.go b/internal/generator/test_vector_utils/small_rational/gkr/registry.go new file mode 100644 index 0000000000..02c78e9bcf --- /dev/null +++ b/internal/generator/test_vector_utils/small_rational/gkr/registry.go @@ -0,0 +1,374 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/internal/generator/test_vector_utils/small_rational" + "github.com/consensys/gnark-crypto/internal/generator/test_vector_utils/small_rational/polynomial" + "slices" + "sync" +) + +type GateName string + +var ( + gates = make(map[GateName]*Gate) + gatesLock sync.Mutex +) + +type registerGateSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int +} + +type RegisterGateOption func(*registerGateSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() RegisterGateOption { + return func(settings *registerGateSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) RegisterGateOption { + return func(settings *registerGateSettings) { + settings.degree = degree + } +} + +// isAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func (f GateFunction) isAdditive(i, nbIn int) bool { + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(small_rational.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := f(in...) + + x[i] = x0 + copy(in, x) + y1 := f(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := f(in...) + + y2.Sub(&y2, &y1) + y1.Sub(&y1, &y0) + + if !y2.Equal(&y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = f(in...) + + x[i] = x0 + copy(in, x) + y1 = f(in...) + + y1.Sub(&y1, &y0) + + return y1.Equal(&y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f GateFunction) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]small_rational.SmallRational, nbIn) + consts := make(small_rational.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + x := make(small_rational.Vector, degreeBound) + x.MustSetRandom() + for i := range x { + fIn[0] = x[i] + for j := range consts { + fIn[j+1].Mul(&x[i], &consts[j]) + } + p[i] = f(fIn...) + } + + // obtain p's coefficients + p, err := interpolate(x, p) + if err != nil { + panic(err) + } + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(&fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +type errorString string + +func (e errorString) Error() string { + return string(e) +} + +const errZeroFunction = errorString("detected a zero function") + +// FindDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func (f GateFunction) FindDegree(max, nbIn int) (int, error) { + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := f.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, errZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func (f GateFunction) VerifyDegree(claimedDegree, nbIn int) error { + if p := f.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return errZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (f GateFunction) FindSolvableVar(nbIn int) int { + for i := range nbIn { + if f.isAdditive(i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (f GateFunction) IsVarSolvable(claimedSolvableVar, nbIn int) bool { + return f.isAdditive(claimedSolvableVar, nbIn) +} + +// RegisterGate creates a gate object and stores it in the gates registry. +// name is a human-readable name for the gate. +// f is the polynomial function defining the gate. +// nbIn is the number of inputs to the gate. +func RegisterGate(name GateName, f GateFunction, nbIn int, options ...RegisterGateOption) error { + s := registerGateSettings{degree: -1, solvableVar: -1} + for _, option := range options { + option(&s) + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = f.FindDegree(maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err := f.VerifyDegree(s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = f.FindSolvableVar(nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !f.IsVarSolvable(s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, name) + } + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[name] = &Gate{Evaluate: f, nbIn: nbIn, degree: s.degree, solvableVar: s.solvableVar} + return nil +} + +func GetGate(name GateName) *Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + return gates[name] +} + +// interpolate fits a polynomial of degree len(X) - 1 = len(Y) - 1 to the points (X[i], Y[i]) +// Note that the runtime is O(len(X)³) +func interpolate(X, Y []small_rational.SmallRational) (polynomial.Polynomial, error) { + if len(X) != len(Y) { + return nil, errors.New("X and Y must have the same length") + } + + // solve the system of equations by Gaussian elimination + augmentedRows := make([][]small_rational.SmallRational, len(X)) // the last column is the Y values + for i := range augmentedRows { + augmentedRows[i] = make([]small_rational.SmallRational, len(X)+1) + augmentedRows[i][0].SetOne() + augmentedRows[i][1].Set(&X[i]) + for j := 2; j < len(augmentedRows[i])-1; j++ { + augmentedRows[i][j].Mul(&augmentedRows[i][j-1], &X[i]) + } + augmentedRows[i][len(augmentedRows[i])-1].Set(&Y[i]) + } + + // make the upper triangle + for i := range len(augmentedRows) - 1 { + // use row i to eliminate the ith element in all rows below + var negInv small_rational.SmallRational + if augmentedRows[i][i].IsZero() { + return nil, errors.New("singular matrix") + } + negInv.Inverse(&augmentedRows[i][i]) + negInv.Neg(&negInv) + for j := i + 1; j < len(augmentedRows); j++ { + var c small_rational.SmallRational + c.Mul(&augmentedRows[j][i], &negInv) + // augmentedRows[j][i].SetZero() omitted + for k := i + 1; k < len(augmentedRows[i]); k++ { + var t small_rational.SmallRational + t.Mul(&augmentedRows[i][k], &c) + augmentedRows[j][k].Add(&augmentedRows[j][k], &t) + } + } + } + + // back substitution + res := make(polynomial.Polynomial, len(X)) + for i := len(augmentedRows) - 1; i >= 0; i-- { + res[i] = augmentedRows[i][len(augmentedRows[i])-1] + for j := i + 1; j < len(augmentedRows[i])-1; j++ { + var t small_rational.SmallRational + t.Mul(&res[j], &augmentedRows[i][j]) + res[i].Sub(&res[i], &t) + } + res[i].Div(&res[i], &augmentedRows[i][i]) + } + + return res, nil +} + +const ( + Identity GateName = "identity" // Identity gate: x -> x + Add2 GateName = "add2" // Add2 gate: (x, y) -> x + y + Sub2 GateName = "sub2" // Sub2 gate: (x, y) -> x - y + Neg GateName = "neg" // Neg gate: x -> -x + Mul2 GateName = "mul2" // Mul2 gate: (x, y) -> x * y +) + +func init() { + // register some basic gates + + if err := RegisterGate(Identity, func(x ...small_rational.SmallRational) small_rational.SmallRational { + return x[0] + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Add2, func(x ...small_rational.SmallRational) small_rational.SmallRational { + var res small_rational.SmallRational + res.Add(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Sub2, func(x ...small_rational.SmallRational) small_rational.SmallRational { + var res small_rational.SmallRational + res.Sub(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Neg, func(x ...small_rational.SmallRational) small_rational.SmallRational { + var res small_rational.SmallRational + res.Neg(&x[0]) + return res + }, 1, WithUnverifiedDegree(1), WithUnverifiedSolvableVar(0)); err != nil { + panic(err) + } + + if err := RegisterGate(Mul2, func(x ...small_rational.SmallRational) small_rational.SmallRational { + var res small_rational.SmallRational + res.Mul(&x[0], &x[1]) + return res + }, 2, WithUnverifiedDegree(2), WithNoSolvableVar()); err != nil { + panic(err) + } +} diff --git a/internal/generator/test_vector_utils/small_rational/small-rational.go b/internal/generator/test_vector_utils/small_rational/small-rational.go index aff0463492..3015ba7eb4 100644 --- a/internal/generator/test_vector_utils/small_rational/small-rational.go +++ b/internal/generator/test_vector_utils/small_rational/small-rational.go @@ -220,6 +220,32 @@ func (z *SmallRational) Mul(x, y *SmallRational) *SmallRational { return z } +func (z *SmallRational) Div(x, y *SmallRational) *SmallRational { + var num, den big.Int + + num.Mul(&x.numerator, &y.denominator) + den.Mul(&x.denominator, &y.numerator) + + z.numerator = num + z.denominator = den + + z.simplify() + z.UpdateText() + return z +} + +func (z *SmallRational) Halve() *SmallRational { + if z.numerator.Bit(0) == 0 { + z.numerator.Rsh(&z.numerator, 1) + } else { + z.denominator.Lsh(&z.denominator, 1) + } + + z.simplify() + z.UpdateText() + return z +} + func (z *SmallRational) SetOne() *SmallRational { return z.SetInt64(1) } @@ -255,6 +281,13 @@ func (z *SmallRational) SetRandom() (*SmallRational, error) { return z, nil } +func (z *SmallRational) MustSetRandom() *SmallRational { + if _, err := z.SetRandom(); err != nil { + panic(err) + } + return z +} + func (z *SmallRational) SetUint64(i uint64) { var num big.Int num.SetUint64(i) @@ -403,6 +436,15 @@ func (z *SmallRational) SetBytes(b []byte) { z.UpdateText() } +func One() SmallRational { + res := SmallRational{ + text: "1", + } + res.numerator.SetInt64(1) + res.denominator.SetInt64(1) + return res +} + func Modulus() *big.Int { res := big.NewInt(1) res.Lsh(res, 64) diff --git a/internal/generator/test_vector_utils/small_rational/vector.go b/internal/generator/test_vector_utils/small_rational/vector.go new file mode 100644 index 0000000000..07fcc3afff --- /dev/null +++ b/internal/generator/test_vector_utils/small_rational/vector.go @@ -0,0 +1,9 @@ +package small_rational + +type Vector []SmallRational + +func (v Vector) MustSetRandom() { + for i := range v { + v[i].MustSetRandom() + } +}