From 40fe15a65cbb50694b13ea8635e1a1f9714866a3 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 08:36:58 -0500 Subject: [PATCH 01/37] feat: tester over extensions Signed-off-by: Arya Tabaie --- internal/gkr/gkrcore/gate.go | 243 ++++++++++++++++++++++-------- internal/gkr/gkrcore/gate_test.go | 57 +++++-- 2 files changed, 222 insertions(+), 78 deletions(-) diff --git a/internal/gkr/gkrcore/gate.go b/internal/gkr/gkrcore/gate.go index 3119366283..a65fe20cdd 100644 --- a/internal/gkr/gkrcore/gate.go +++ b/internal/gkr/gkrcore/gate.go @@ -291,7 +291,7 @@ func CompileGateFunction(f gkr.GateFunction, nbInputs int, field *big.Int) (Seri } // Compute degree and solvable variable - tester := gateTester{mod: field} + tester := gateTester{field: PrimeField(field)} tester.setGate(bytecode, nbInputs) degree := len(tester.fitPoly(bytecode.EstimateDegree(nbInputs))) - 1 @@ -315,75 +315,190 @@ func CompileGateFunction(f gkr.GateFunction, nbInputs int, field *big.Int) (Seri }, nil } +// Field describes F_p[X]/(f), with f = Xⁿ + MinPoly[n-1]·Xⁿ⁻¹ + … + MinPoly[0] monic and +// irreducible over F_p, n = len(MinPoly). Coefficients are in increasing degree: X is [0], +// X² + 1 is [1, 0]. +type Field struct { + Modulus *big.Int + MinPoly []*big.Int +} + +// PrimeField describes F_p itself, as F_p[X]/(X). +func PrimeField(p *big.Int) Field { + return Field{Modulus: p, MinPoly: []*big.Int{big.NewInt(0)}} +} + +// degree returns n, the extension degree of the field over F_p. +func (f Field) degree() int { + return len(f.MinPoly) +} + +// gateTester evaluates gate bytecode over a field F_p[X]/(f), to discover a gate's degree and +// solvable variable. An element is a []*big.Int of length field.degree(), coefficient i the +// coefficient of Xⁱ, each reduced mod p. For PrimeField, degree() is 1, and every operation below +// reduces to arithmetic mod p. type gateTester struct { - mod *big.Int - gate GateBytecode - vars []*big.Int - nbIn int + field Field + gate GateBytecode + vars [][]*big.Int + nbIn int + + invExponent *big.Int // pⁿ - 2, the exponent inverse raises to; computed once, on first use } func (t *gateTester) setGate(g GateBytecode, nbIn int) { t.gate = g - t.vars = make([]*big.Int, g.NbConstants()+nbIn+len(g.Instructions)) t.nbIn = nbIn - copy(t.vars, g.Constants) + t.vars = make([][]*big.Int, g.NbConstants()+nbIn+len(g.Instructions)) + for i, c := range g.Constants { + t.vars[i] = t.embed(c) + } } -func (t *gateTester) isZero(a *big.Int) bool { - v := new(big.Int).Mod(a, t.mod) - return v.BitLen() == 0 +// embed returns c as the field element [c, 0, …, 0]. +func (t *gateTester) embed(c *big.Int) []*big.Int { + res := make([]*big.Int, t.field.degree()) + res[0] = new(big.Int).Mod(c, t.field.Modulus) + for i := 1; i < len(res); i++ { + res[i] = new(big.Int) + } + return res } -func (t *gateTester) equal(a, b *big.Int) bool { - return a.Cmp(b) == 0 +func (t *gateTester) zero() []*big.Int { + return t.embed(new(big.Int)) } -func (t *gateTester) add(a, b *big.Int) *big.Int { - res := new(big.Int).Add(a, b) - return res.Mod(res, t.mod) +func (t *gateTester) one() []*big.Int { + return t.embed(big.NewInt(1)) } -func (t *gateTester) sub(a, b *big.Int) *big.Int { - res := new(big.Int).Sub(a, b) - return res.Mod(res, t.mod) +func (t *gateTester) isZero(a []*big.Int) bool { + for _, ai := range a { + v := new(big.Int).Mod(ai, t.field.Modulus) + if v.BitLen() != 0 { + return false + } + } + return true } -func (t *gateTester) mul(a, b *big.Int) *big.Int { - res := new(big.Int).Mul(a, b) - return res.Mod(res, t.mod) +func (t *gateTester) equal(a, b []*big.Int) bool { + for i := range a { + if a[i].Cmp(b[i]) != 0 { + return false + } + } + return true } -func (t *gateTester) neg(a *big.Int) *big.Int { - res := new(big.Int).Neg(a) - return res.Mod(res, t.mod) +func (t *gateTester) add(a, b []*big.Int) []*big.Int { + res := make([]*big.Int, len(a)) + for i := range res { + res[i] = new(big.Int).Add(a[i], b[i]) + res[i].Mod(res[i], t.field.Modulus) + } + return res } -func (t *gateTester) inverse(a *big.Int) *big.Int { - return new(big.Int).ModInverse(a, t.mod) +func (t *gateTester) sub(a, b []*big.Int) []*big.Int { + res := make([]*big.Int, len(a)) + for i := range res { + res[i] = new(big.Int).Sub(a[i], b[i]) + res[i].Mod(res[i], t.field.Modulus) + } + return res } -func (t *gateTester) div(a, b *big.Int) *big.Int { - res := new(big.Int).ModInverse(b, t.mod) - return res.Mul(a, res).Mod(res, t.mod) +func (t *gateTester) neg(a []*big.Int) []*big.Int { + res := make([]*big.Int, len(a)) + for i := range res { + res[i] = new(big.Int).Neg(a[i]) + res[i].Mod(res[i], t.field.Modulus) + } + return res } -func (t *gateTester) randomElement() *big.Int { - res, err := rand.Int(rand.Reader, t.mod) - if err != nil { - panic(err) +// mul multiplies a and b as polynomials, then reduces the product mod f, then mod p. +func (t *gateTester) mul(a, b []*big.Int) []*big.Int { + n := t.field.degree() + prod := make([]*big.Int, 2*n-1) + for i := range prod { + prod[i] = new(big.Int) + } + + var term big.Int // scratch for each product term, to avoid allocating one per multiplication + for i, ai := range a { + for j, bj := range b { + term.Mul(ai, bj) + prod[i+j].Add(prod[i+j], &term) + } + } + + // Xⁿ ≡ -(MinPoly[n-1]Xⁿ⁻¹ + … + MinPoly[0]) (mod f); fold the coefficients at or above Xⁿ + // down, highest first, exactly as long division would. + for k := len(prod) - 1; k >= n; k-- { + coeff := prod[k] + for i := range n { + term.Mul(coeff, t.field.MinPoly[i]) + prod[k-n+i].Sub(prod[k-n+i], &term) + } + } + + res := prod[:n] + for i := range res { + res[i].Mod(res[i], t.field.Modulus) + } + return res +} + +// pow raises a to the eᵗʰ power by square-and-multiply, e ≥ 0. +func (t *gateTester) pow(a []*big.Int, e *big.Int) []*big.Int { + res := t.one() + for i := e.BitLen() - 1; i >= 0; i-- { + res = t.mul(res, res) + if e.Bit(i) == 1 { + res = t.mul(res, a) + } } return res } -func (t *gateTester) randomElements(n int) []*big.Int { - res := make([]*big.Int, n) +// inverse returns a⁻¹ = a^(pⁿ⁻²), which requires f irreducible. +func (t *gateTester) inverse(a []*big.Int) []*big.Int { + if t.invExponent == nil { + n := big.NewInt(int64(t.field.degree())) + t.invExponent = new(big.Int).Exp(t.field.Modulus, n, nil) + t.invExponent.Sub(t.invExponent, big.NewInt(2)) + } + return t.pow(a, t.invExponent) +} + +func (t *gateTester) div(a, b []*big.Int) []*big.Int { + return t.mul(a, t.inverse(b)) +} + +func (t *gateTester) randomElement() []*big.Int { + res := make([]*big.Int, t.field.degree()) + for i := range res { + v, err := rand.Int(rand.Reader, t.field.Modulus) + if err != nil { + panic(err) + } + res[i] = v + } + return res +} + +func (t *gateTester) randomElements(n int) [][]*big.Int { + res := make([][]*big.Int, n) for i := range res { res[i] = t.randomElement() } return res } -func (t *gateTester) evalPoly(p []*big.Int, x *big.Int) *big.Int { +func (t *gateTester) evalPoly(p [][]*big.Int, x []*big.Int) []*big.Int { res := p[len(p)-1] for i := len(p) - 2; i >= 0; i-- { res = t.mul(res, x) @@ -393,7 +508,7 @@ func (t *gateTester) evalPoly(p []*big.Int, x *big.Int) *big.Int { } // evaluate executes the gate bytecode with the given inputs. -func (t *gateTester) evaluate(inputs ...*big.Int) *big.Int { +func (t *gateTester) evaluate(inputs ...[]*big.Int) []*big.Int { frameSize := t.gate.NbConstants() // Copy inputs into frame @@ -403,44 +518,40 @@ func (t *gateTester) evaluate(inputs ...*big.Int) *big.Int { // Execute instructions for _, inst := range t.gate.Instructions { - dst := t.vars[frameSize] - if dst == nil { - dst = new(big.Int) - t.vars[frameSize] = dst - } + var dst []*big.Int switch inst.Op { case OpAdd: - dst.Set(t.vars[inst.Inputs[0]]) + dst = t.vars[inst.Inputs[0]] for _, idx := range inst.Inputs[1:] { - dst.Add(dst, t.vars[idx]) + dst = t.add(dst, t.vars[idx]) } case OpSub: - dst.Set(t.vars[inst.Inputs[0]]) + dst = t.vars[inst.Inputs[0]] for _, idx := range inst.Inputs[1:] { - dst.Sub(dst, t.vars[idx]) + dst = t.sub(dst, t.vars[idx]) } case OpMul: - dst.Set(t.vars[inst.Inputs[0]]) + dst = t.vars[inst.Inputs[0]] for _, idx := range inst.Inputs[1:] { - dst.Mul(dst, t.vars[idx]) + dst = t.mul(dst, t.vars[idx]) } case OpNeg: - dst.Neg(t.vars[inst.Inputs[0]]) + dst = t.neg(t.vars[inst.Inputs[0]]) case OpMulAcc: // a + b*c - dst.Mul(t.vars[inst.Inputs[1]], t.vars[inst.Inputs[2]]) - dst.Add(dst, t.vars[inst.Inputs[0]]) + dst = t.mul(t.vars[inst.Inputs[1]], t.vars[inst.Inputs[2]]) + dst = t.add(dst, t.vars[inst.Inputs[0]]) case OpSumExp17: // (a + b + c)^17 - dst.Add(t.vars[inst.Inputs[0]], t.vars[inst.Inputs[1]]) - dst.Add(dst, t.vars[inst.Inputs[2]]) - dst.Exp(dst, big.NewInt(17), t.mod) + dst = t.add(t.vars[inst.Inputs[0]], t.vars[inst.Inputs[1]]) + dst = t.add(dst, t.vars[inst.Inputs[2]]) + dst = t.pow(dst, big.NewInt(17)) default: panic("unknown operation") } - dst.Mod(dst, t.mod) + t.vars[frameSize] = dst frameSize++ } - return new(big.Int).Set(t.vars[frameSize-1]) + return t.vars[frameSize-1] } // isAdditive returns whether xᵢ occurs only in a monomial of total degree 1 @@ -451,7 +562,7 @@ func (t *gateTester) isAdditive(i int) bool { in[i] = x y1 := t.evaluate(in...) - zero := new(big.Int) + zero := t.zero() in[i] = zero y0 := t.evaluate(in...) @@ -483,14 +594,14 @@ func (t *gateTester) isAdditive(i int) bool { // fitPoly tries to fit a polynomial of degree no more than degreeBound to the gate. // It returns the polynomial if successful, nil otherwise. -func (t *gateTester) fitPoly(maxDegree int) []*big.Int { +func (t *gateTester) fitPoly(maxDegree int) [][]*big.Int { // turn f univariate by defining p(x) as f(x, rx, ..., sx) // where r, s, ... are random constants - fIn := make([]*big.Int, t.nbIn) + fIn := make([][]*big.Int, t.nbIn) consts := t.randomElements(t.nbIn - 1) - p := make([]*big.Int, maxDegree+1) + p := make([][]*big.Int, maxDegree+1) x := t.randomElements(maxDegree + 1) for i := range x { @@ -528,17 +639,17 @@ func (t *gateTester) fitPoly(maxDegree int) []*big.Int { // 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 (t *gateTester) interpolate(X, Y []*big.Int) ([]*big.Int, error) { +func (t *gateTester) interpolate(X, Y [][]*big.Int) ([][]*big.Int, error) { if len(X) != len(Y) { return nil, errors.New("same length expected for X and Y") } - one := big.NewInt(1) + one := t.one() // solve the system of equations by Gaussian elimination - augmentedRows := make([][]*big.Int, len(X)) // the last column is the Y values + augmentedRows := make([][][]*big.Int, len(X)) // the last column is the Y values for i := range augmentedRows { - augmentedRows[i] = make([]*big.Int, len(X)+1) + augmentedRows[i] = make([][]*big.Int, len(X)+1) augmentedRows[i][0] = one augmentedRows[i][1] = X[i] for j := 2; j < len(augmentedRows[i])-1; j++ { @@ -550,7 +661,7 @@ func (t *gateTester) interpolate(X, Y []*big.Int) ([]*big.Int, error) { // make the upper triangle for i := range len(augmentedRows) - 1 { // use row i to eliminate the ith element in all rows below - var negInv *big.Int + var negInv []*big.Int if t.isZero(augmentedRows[i][i]) { return nil, errors.New("singular matrix") } @@ -567,7 +678,7 @@ func (t *gateTester) interpolate(X, Y []*big.Int) ([]*big.Int, error) { } // back substitution - res := make([]*big.Int, len(X)) + res := make([][]*big.Int, 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++ { diff --git a/internal/gkr/gkrcore/gate_test.go b/internal/gkr/gkrcore/gate_test.go index be500e3b3d..ac49775a13 100644 --- a/internal/gkr/gkrcore/gate_test.go +++ b/internal/gkr/gkrcore/gate_test.go @@ -11,7 +11,24 @@ import ( "github.com/stretchr/testify/require" ) -func field() *big.Int { return ecc.BN254.ScalarField() } +// TODO have this return what is now PrimeField(bn254()) +func bn254() *big.Int { return ecc.BN254.ScalarField() } + +// koalabearE6 describes KoalaBear's degree-6 extension E6, whose tower generator v has minimal +// polynomial X⁶ - 2X³ - 2. It is test data only here; the next PR exports this description +// alongside gnark-crypto's own E6 type. +func koalabearE6() Field { + return Field{ + Modulus: big.NewInt(2130706433), // 2^31 - 2^24 + 1 + MinPoly: []*big.Int{big.NewInt(-2), big.NewInt(0), big.NewInt(0), big.NewInt(-2), big.NewInt(0), big.NewInt(0)}, + } +} + +// testFields runs under PrimeField(bn254()), as before commit A, and under the KoalaBear E6 +// description, to check that both agree on every gate's metadata. +func testFields() []Field { + return []Field{PrimeField(bn254()), koalabearE6()} +} // test gate functions @@ -76,7 +93,7 @@ func TestConstants(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - compiled, err := CompileGateFunction(tt.f, tt.nbIn, field()) + compiled, err := CompileGateFunction(tt.f, tt.nbIn, bn254()) require.NoError(t, err) assert.Equal(t, len(tt.constants), len(compiled.Evaluate.Constants)) @@ -114,7 +131,7 @@ func TestCompileGateFunction(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - compiled, err := CompileGateFunction(tt.f, tt.nbIn, field()) + compiled, err := CompileGateFunction(tt.f, tt.nbIn, bn254()) require.NoError(t, err) assert.Equal(t, tt.nbIn, compiled.NbIn) assert.Equal(t, tt.degree, compiled.Degree) @@ -125,11 +142,13 @@ func TestCompileGateFunction(t *testing.T) { func testFitPoly(t *testing.T, name string, f gkr.GateFunction, nbIn, degree, maxDegree int) { t.Run(name, func(t *testing.T) { - tester := gateTester{mod: field()} - g, err := CompileGateFunction(f, nbIn, field()) + g, err := CompileGateFunction(f, nbIn, bn254()) require.NoError(t, err) - tester.setGate(g.Evaluate, nbIn) - require.Equal(t, degree, len(tester.fitPoly(maxDegree))-1) + for _, fd := range testFields() { + tester := gateTester{field: fd} + tester.setGate(g.Evaluate, nbIn) + require.Equal(t, degree, len(tester.fitPoly(maxDegree))-1) + } }) } @@ -145,12 +164,14 @@ func TestFitPoly(t *testing.T) { func testIsAdditive(t *testing.T, name string, f gkr.GateFunction, isAdditive ...bool) { t.Run(name, func(t *testing.T) { - tester := gateTester{mod: field()} - g, err := CompileGateFunction(f, len(isAdditive), field()) + g, err := CompileGateFunction(f, len(isAdditive), bn254()) require.NoError(t, err) - tester.setGate(g.Evaluate, len(isAdditive)) - for i := range isAdditive { - assert.Equal(t, isAdditive[i], tester.isAdditive(i)) + for _, fd := range testFields() { + tester := gateTester{field: fd} + tester.setGate(g.Evaluate, len(isAdditive)) + for i := range isAdditive { + assert.Equal(t, isAdditive[i], tester.isAdditive(i)) + } } }) } @@ -163,3 +184,15 @@ func TestIsAdditive(t *testing.T) { testIsAdditive(t, "x²+2y", sqrAdd2, false, true) testIsAdditive(t, "x*y+y", mulAdd, false, false) } + +// TestExtensionDefiningRelation checks mul's reduction against KoalaBear's E6 tower generator's +// defining relation, X⁶ = 2X³ + 2 (from its minimal polynomial X⁶ - 2X³ - 2). +func TestExtensionDefiningRelation(t *testing.T) { + tester := gateTester{field: koalabearE6()} + + x := []*big.Int{big.NewInt(0), big.NewInt(1), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0)} + x6 := tester.pow(x, big.NewInt(6)) + x3 := tester.pow(x, big.NewInt(3)) + want := tester.add(tester.mul(tester.embed(big.NewInt(2)), x3), tester.embed(big.NewInt(2))) + require.True(t, tester.equal(x6, want)) +} From f20879d0c01533b090656d29db97242a2e6a816a Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 08:50:23 -0500 Subject: [PATCH 02/37] feat: field descriptor in API Signed-off-by: Arya Tabaie --- constraint/solver/gkrgates/registry.go | 2 +- gkr/gkrapi/api.go | 10 +++++++--- gkr/gkrapi/example_test.go | 2 +- internal/gkr/gkrcore/gate.go | 4 ++-- internal/gkr/gkrcore/gate_test.go | 7 +++---- internal/gkr/gkrcore/types.go | 5 ++--- internal/gkr/gkrtesting/gkrtesting.go | 4 ++-- std/gkrapi/compile.go | 2 +- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/constraint/solver/gkrgates/registry.go b/constraint/solver/gkrgates/registry.go index 3320a7a941..004b0cede1 100644 --- a/constraint/solver/gkrgates/registry.go +++ b/constraint/solver/gkrgates/registry.go @@ -119,7 +119,7 @@ func Register(f gkr.GateFunction, nbIn int, options ...RegisterOption) error { } for _, curve := range s.curves { - compiled, err := gkrcore.CompileGateFunction(f, nbIn, curve.ScalarField()) + compiled, err := gkrcore.CompileGateFunction(f, nbIn, gkrcore.PrimeField(curve.ScalarField())) if err != nil { return err } diff --git a/gkr/gkrapi/api.go b/gkr/gkrapi/api.go index e2a49da30c..cc95b2b0dc 100644 --- a/gkr/gkrapi/api.go +++ b/gkr/gkrapi/api.go @@ -2,8 +2,6 @@ package gkrapi import ( - "math/big" - "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -12,6 +10,12 @@ import ( // Circuit is a circuit compiled by API.Compile, ready to prove and verify with gkr/. type Circuit = gkrcore.SerializableCircuit +// Field describes the field to compile a circuit for: F_p, or an extension of it. +type Field = gkrcore.Field + +// PrimeField describes F_p itself. +var PrimeField = gkrcore.PrimeField + // ConsolidationMode selects which wires DefaultProvingSchedule consolidates into level 0. type ConsolidationMode = gkrcore.ConsolidationMode @@ -65,7 +69,7 @@ func (api *API) Export(in ...gkr.Variable) { } // Compile compiles the circuit for field, and builds its default proving schedule under mode. -func (api *API) Compile(field *big.Int, mode ConsolidationMode) (Circuit, constraint.GkrProvingSchedule, error) { +func (api *API) Compile(field Field, mode ConsolidationMode) (Circuit, constraint.GkrProvingSchedule, error) { _, circuit, err := api.circuit.Compile(field) if err != nil { return nil, nil, err diff --git a/gkr/gkrapi/example_test.go b/gkr/gkrapi/example_test.go index 2d194c420a..ce611c59f5 100644 --- a/gkr/gkrapi/example_test.go +++ b/gkr/gkrapi/example_test.go @@ -21,7 +21,7 @@ func Example() { z := api.Mul(x, y) api.Export(z) - circuit, schedule, err := api.Compile(ecc.BN254.ScalarField(), gkrapi.ConsolidateAll) + circuit, schedule, err := api.Compile(gkrapi.PrimeField(ecc.BN254.ScalarField()), gkrapi.ConsolidateAll) assertNoError(err) assignment := make(bn254.WireAssignment, len(circuit)) diff --git a/internal/gkr/gkrcore/gate.go b/internal/gkr/gkrcore/gate.go index a65fe20cdd..b27bb9927d 100644 --- a/internal/gkr/gkrcore/gate.go +++ b/internal/gkr/gkrcore/gate.go @@ -242,7 +242,7 @@ func (gc *gateCompiler) remapIndices() { // CompileGateFunction converts a gate function into a SerializableGate. // This consists of compiling into bytecode as well as computing gate metadata // such as degree and solvable var index for the given field. -func CompileGateFunction(f gkr.GateFunction, nbInputs int, field *big.Int) (SerializableGate, error) { +func CompileGateFunction(f gkr.GateFunction, nbInputs int, field Field) (SerializableGate, error) { // Create compiling API compiler := gateCompiler{ constantIndex: make(map[string]uint16), @@ -291,7 +291,7 @@ func CompileGateFunction(f gkr.GateFunction, nbInputs int, field *big.Int) (Seri } // Compute degree and solvable variable - tester := gateTester{field: PrimeField(field)} + tester := gateTester{field: field} tester.setGate(bytecode, nbInputs) degree := len(tester.fitPoly(bytecode.EstimateDegree(nbInputs))) - 1 diff --git a/internal/gkr/gkrcore/gate_test.go b/internal/gkr/gkrcore/gate_test.go index ac49775a13..c7992289cf 100644 --- a/internal/gkr/gkrcore/gate_test.go +++ b/internal/gkr/gkrcore/gate_test.go @@ -11,8 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -// TODO have this return what is now PrimeField(bn254()) -func bn254() *big.Int { return ecc.BN254.ScalarField() } +func bn254() Field { return PrimeField(ecc.BN254.ScalarField()) } // koalabearE6 describes KoalaBear's degree-6 extension E6, whose tower generator v has minimal // polynomial X⁶ - 2X³ - 2. It is test data only here; the next PR exports this description @@ -24,10 +23,10 @@ func koalabearE6() Field { } } -// testFields runs under PrimeField(bn254()), as before commit A, and under the KoalaBear E6 +// testFields runs under bn254(), as before commit A, and under the KoalaBear E6 // description, to check that both agree on every gate's metadata. func testFields() []Field { - return []Field{PrimeField(bn254()), koalabearE6()} + return []Field{bn254(), koalabearE6()} } // test gate functions diff --git a/internal/gkr/gkrcore/types.go b/internal/gkr/gkrcore/types.go index 80fff6753a..8465618a2f 100644 --- a/internal/gkr/gkrcore/types.go +++ b/internal/gkr/gkrcore/types.go @@ -2,7 +2,6 @@ package gkrcore import ( "errors" - "math/big" "slices" "github.com/consensys/gnark/constraint" @@ -196,7 +195,7 @@ type Blueprints struct { // Compile compiles a raw circuit into both a gadget circuit and a serializable circuit. // It computes all wire and gate metadata (Degree, SolvableVar). -func (c RawCircuit) Compile(mod *big.Int) (GadgetCircuit, SerializableCircuit, error) { +func (c RawCircuit) Compile(field Field) (GadgetCircuit, SerializableCircuit, error) { gadget := make(GadgetCircuit, len(c)) serializable := make(SerializableCircuit, len(c)) @@ -215,7 +214,7 @@ func (c RawCircuit) Compile(mod *big.Int) (GadgetCircuit, SerializableCircuit, e } nbIn := len(c[i].Inputs) - compiledGate, err := CompileGateFunction(c[i].Gate, nbIn, mod) + compiledGate, err := CompileGateFunction(c[i].Gate, nbIn, field) if err != nil { return nil, nil, err } diff --git a/internal/gkr/gkrtesting/gkrtesting.go b/internal/gkr/gkrtesting/gkrtesting.go index 59cc9bfaa9..990ac1eb5f 100644 --- a/internal/gkr/gkrtesting/gkrtesting.go +++ b/internal/gkr/gkrtesting/gkrtesting.go @@ -22,7 +22,7 @@ import ( // The main functionality is to cache whole circuits, but this package needs to use its own gate registry, in order to avoid import cycles. // Cache is used in tests for the per-curve GKR packages, but they in turn provide gate degree discovery functions to the gkrgates package. type Cache struct { - field *big.Int + field gkrcore.Field circuits map[string]circuits gates map[string]gkr.GateFunction lock sync.Mutex @@ -70,7 +70,7 @@ func NewCache(field *big.Int) *Cache { gates["select-input-3"] = selectInput3Gate return &Cache{ - field: field, + field: gkrcore.PrimeField(field), circuits: make(map[string]circuits), gates: gates, } diff --git a/std/gkrapi/compile.go b/std/gkrapi/compile.go index 6066eac4dd..32c207ab9f 100644 --- a/std/gkrapi/compile.go +++ b/std/gkrapi/compile.go @@ -78,7 +78,7 @@ func (api *API) Compile(fiatshamirHashName string, options ...CompileOption) (*C field := compiler.Field() curveID := utils.FieldToCurve(field) - gadgetCircuit, serializableCircuit, err := api.circuit.Compile(field) + gadgetCircuit, serializableCircuit, err := api.circuit.Compile(gkrcore.PrimeField(field)) if err != nil { return nil, err } From ef98b7f1d5b5fd2a8162c5f4f12f86f7dd3014c7 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 19:59:10 -0500 Subject: [PATCH 03/37] feat: generic transcript Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr.go | 12 ++--- gkr/bls12-377/gkr_test.go | 5 +- gkr/bls12-377/sumcheck.go | 54 ++++--------------- gkr/bls12-377/sumcheck_test.go | 13 ++--- gkr/bls12-377/test_vector_utils_test.go | 14 ++--- gkr/bls12-381/gkr.go | 12 ++--- gkr/bls12-381/gkr_test.go | 5 +- gkr/bls12-381/sumcheck.go | 54 ++++--------------- gkr/bls12-381/sumcheck_test.go | 13 ++--- gkr/bls12-381/test_vector_utils_test.go | 14 ++--- gkr/bn254/gkr.go | 12 ++--- gkr/bn254/gkr_test.go | 5 +- gkr/bn254/sumcheck.go | 54 ++++--------------- gkr/bn254/sumcheck_test.go | 13 ++--- gkr/bn254/test_vector_utils_test.go | 14 ++--- gkr/bw6-761/gkr.go | 12 ++--- gkr/bw6-761/gkr_test.go | 5 +- gkr/bw6-761/sumcheck.go | 54 ++++--------------- gkr/bw6-761/sumcheck_test.go | 13 ++--- gkr/bw6-761/test_vector_utils_test.go | 14 ++--- go.mod | 2 +- go.sum | 4 +- internal/generator/backend/main.go | 4 ++ .../backend/template/gkr/gkr.go.tmpl | 12 ++--- .../backend/template/gkr/gkr.test.go.tmpl | 5 +- .../backend/template/gkr/sumcheck.go.tmpl | 54 ++++--------------- .../template/gkr/sumcheck.test.go.tmpl | 15 +++--- .../gkr/sumcheck.test.vectors.gen.go.tmpl | 15 +++--- .../template/gkr/test_vector_utils.go.tmpl | 16 +++--- internal/gkr/gkrcore/gate_test.go | 7 ++- internal/gkr/gkrcore/transcript.go | 54 +++++++++++++++++++ internal/gkr/small_rational/gkr.go | 12 ++--- internal/gkr/small_rational/sumcheck.go | 54 ++++--------------- internal/gkr/small_rational/sumcheck_test.go | 14 ++--- .../sumcheck_test_vector_gen.go | 13 ++--- .../gkr/small_rational/test_vector_utils.go | 14 ++--- internal/small_rational/small-rational.go | 3 +- internal/smallfields/tinyfield/element.go | 2 + .../smallfields/tinyfield/element_purego.go | 8 +-- 39 files changed, 297 insertions(+), 403 deletions(-) create mode 100644 internal/gkr/gkrcore/transcript.go diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index 2bb0cd7af6..477a0acbd1 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -64,7 +64,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript transcript + transcript *transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -84,7 +84,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: transcript{h: hasher}, + transcript: gkrcore.NewHashTranscript[fr.Element](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -160,7 +160,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -203,7 +203,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -248,7 +248,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { return fmt.Errorf("level %d: %v", levelI, err) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -295,7 +295,7 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} var boundOutputEvals []fr.Element diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index e3bd5a5ec4..d4ef5601e1 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -16,8 +16,9 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/mimc" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + gcHash "github.com/consensys/gnark-crypto/hash" + _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -299,7 +300,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, mimc.NewMiMC()) + _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BLS12_377.New()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bls12-377/sumcheck.go b/gkr/bls12-377/sumcheck.go index d16a087d7f..5b1435a5d6 100644 --- a/gkr/bls12-377/sumcheck.go +++ b/gkr/bls12-377/sumcheck.go @@ -7,7 +7,6 @@ package gkr import ( "errors" - "hash" "sync" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" @@ -19,39 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript is a Fiat-Shamir transcript backed by a running hash. -// Field elements are written via Bind; challenges are derived via getChallenge. -// The hash is never reset — all previous data is implicitly part of future challenges. -type transcript struct { - h hash.Hash - bound bool // whether Bind was called since the last getChallenge -} - -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *transcript) Bind(elements ...fr.Element) { - if len(elements) == 0 { - return - } - for i := range elements { - bytes := elements[i].Bytes() - t.h.Write(bytes[:]) - } - t.bound = true -} - -// getChallenge binds optional elements, then squeezes a challenge from the current hash state. -// If no bindings were added since the last squeeze, a separator byte is written first -// to advance the state and prevent repeated values. -func (t *transcript) getChallenge(bindings ...fr.Element) fr.Element { - t.Bind(bindings...) - if !t.bound { - t.h.Write([]byte{0}) - } - t.bound = false - var res fr.Element - res.SetBytes(t.h.Sum(nil)) - return res -} +// transcript specializes gkrcore's generic hash transcript to this field. +type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -85,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.getChallenge(proof.partialSumPolys[j]...) + challenges[j] = t.Challenge(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.getChallenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -111,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.getChallenge(proof.partialSumPolys[j]...) + r[j] = t.Challenge(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -386,7 +354,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.getChallenge() + c.foldingCoeff = r.transcript.Challenge() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -461,7 +429,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { } } - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } // verifyLevelSetup derives the folding coefficient, collects all claimed wire @@ -471,7 +439,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.getChallenge() + foldingCoeff = r.transcript.Challenge() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -493,7 +461,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) level := r.schedule[levelI].(*constraint.GkrSumcheckLevel) - return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), &r.transcript) + return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), r.transcript) } // singleSourceZeroCheckClaims is the prover-side claim for a single-source @@ -687,7 +655,7 @@ func (r *resources) proveSingleSourceZeroCheckLevel(levelI int) sumcheckProof { q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] claims.suffixEq = r.buildSuffixEq(q) - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) error { @@ -728,7 +696,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.getChallenge(partialPoly...) + challenges[j] = r.transcript.Challenge(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bls12-377/sumcheck_test.go b/gkr/bls12-377/sumcheck_test.go index 395a75bff3..a5e7c43851 100644 --- a/gkr/bls12-377/sumcheck_test.go +++ b/gkr/bls12-377/sumcheck_test.go @@ -10,6 +10,7 @@ import ( "hash" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/stretchr/testify/assert" "math/bits" @@ -27,9 +28,9 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } claim := singleMultilinClaim{g: poly.Clone()} - t := transcript{h: hashGenerator()} + t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) - proof := sumcheckProve(&claim, &t) + proof := sumcheckProve(&claim, t) var sb strings.Builder for _, p := range proof.partialSumPolys { @@ -45,15 +46,15 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t) == nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } return nil diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index 500bb2a6e5..460b8b888d 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -39,6 +39,9 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } +// messageCounterBlockSize is the length of an element's Marshal(). +const messageCounterBlockSize = fr.Bytes + // messageCounter is a stand-in hash whose state depends only on the number of field-element // blocks written to it, not on their values. type messageCounter struct { @@ -48,18 +51,17 @@ type messageCounter struct { } func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/fr.Bytes + 1 + inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 m.state += int64(inputBlockSize) * m.step return len(p), nil } func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/fr.Bytes + 1 + inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 resI := m.state + int64(inputBlockSize)*m.step var res fr.Element res.SetInt64(int64(resI)) - resBytes := res.Bytes() - return resBytes[:] + return (&res).Marshal() } func (m *messageCounter) Reset() { @@ -67,11 +69,11 @@ func (m *messageCounter) Reset() { } func (m *messageCounter) Size() int { - return fr.Bytes + return messageCounterBlockSize } func (m *messageCounter) BlockSize() int { - return fr.Bytes + return messageCounterBlockSize } func newMessageCounter(startState, step int) hash.Hash { diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index 3cddcc70da..8eca467e34 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -64,7 +64,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript transcript + transcript *transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -84,7 +84,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: transcript{h: hasher}, + transcript: gkrcore.NewHashTranscript[fr.Element](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -160,7 +160,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -203,7 +203,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -248,7 +248,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { return fmt.Errorf("level %d: %v", levelI, err) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -295,7 +295,7 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} var boundOutputEvals []fr.Element diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index ca5f14a5c9..fb94706ab3 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -16,8 +16,9 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/mimc" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + gcHash "github.com/consensys/gnark-crypto/hash" + _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -299,7 +300,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, mimc.NewMiMC()) + _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BLS12_381.New()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bls12-381/sumcheck.go b/gkr/bls12-381/sumcheck.go index 2dc399637c..b09a3d62bb 100644 --- a/gkr/bls12-381/sumcheck.go +++ b/gkr/bls12-381/sumcheck.go @@ -7,7 +7,6 @@ package gkr import ( "errors" - "hash" "sync" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" @@ -19,39 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript is a Fiat-Shamir transcript backed by a running hash. -// Field elements are written via Bind; challenges are derived via getChallenge. -// The hash is never reset — all previous data is implicitly part of future challenges. -type transcript struct { - h hash.Hash - bound bool // whether Bind was called since the last getChallenge -} - -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *transcript) Bind(elements ...fr.Element) { - if len(elements) == 0 { - return - } - for i := range elements { - bytes := elements[i].Bytes() - t.h.Write(bytes[:]) - } - t.bound = true -} - -// getChallenge binds optional elements, then squeezes a challenge from the current hash state. -// If no bindings were added since the last squeeze, a separator byte is written first -// to advance the state and prevent repeated values. -func (t *transcript) getChallenge(bindings ...fr.Element) fr.Element { - t.Bind(bindings...) - if !t.bound { - t.h.Write([]byte{0}) - } - t.bound = false - var res fr.Element - res.SetBytes(t.h.Sum(nil)) - return res -} +// transcript specializes gkrcore's generic hash transcript to this field. +type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -85,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.getChallenge(proof.partialSumPolys[j]...) + challenges[j] = t.Challenge(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.getChallenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -111,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.getChallenge(proof.partialSumPolys[j]...) + r[j] = t.Challenge(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -386,7 +354,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.getChallenge() + c.foldingCoeff = r.transcript.Challenge() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -461,7 +429,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { } } - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } // verifyLevelSetup derives the folding coefficient, collects all claimed wire @@ -471,7 +439,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.getChallenge() + foldingCoeff = r.transcript.Challenge() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -493,7 +461,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) level := r.schedule[levelI].(*constraint.GkrSumcheckLevel) - return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), &r.transcript) + return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), r.transcript) } // singleSourceZeroCheckClaims is the prover-side claim for a single-source @@ -687,7 +655,7 @@ func (r *resources) proveSingleSourceZeroCheckLevel(levelI int) sumcheckProof { q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] claims.suffixEq = r.buildSuffixEq(q) - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) error { @@ -728,7 +696,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.getChallenge(partialPoly...) + challenges[j] = r.transcript.Challenge(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bls12-381/sumcheck_test.go b/gkr/bls12-381/sumcheck_test.go index 5f8a12fc5a..39d93ee2a0 100644 --- a/gkr/bls12-381/sumcheck_test.go +++ b/gkr/bls12-381/sumcheck_test.go @@ -10,6 +10,7 @@ import ( "hash" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/stretchr/testify/assert" "math/bits" @@ -27,9 +28,9 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } claim := singleMultilinClaim{g: poly.Clone()} - t := transcript{h: hashGenerator()} + t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) - proof := sumcheckProve(&claim, &t) + proof := sumcheckProve(&claim, t) var sb strings.Builder for _, p := range proof.partialSumPolys { @@ -45,15 +46,15 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t) == nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } return nil diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index e082a7361c..8509cb24d4 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -39,6 +39,9 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } +// messageCounterBlockSize is the length of an element's Marshal(). +const messageCounterBlockSize = fr.Bytes + // messageCounter is a stand-in hash whose state depends only on the number of field-element // blocks written to it, not on their values. type messageCounter struct { @@ -48,18 +51,17 @@ type messageCounter struct { } func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/fr.Bytes + 1 + inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 m.state += int64(inputBlockSize) * m.step return len(p), nil } func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/fr.Bytes + 1 + inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 resI := m.state + int64(inputBlockSize)*m.step var res fr.Element res.SetInt64(int64(resI)) - resBytes := res.Bytes() - return resBytes[:] + return (&res).Marshal() } func (m *messageCounter) Reset() { @@ -67,11 +69,11 @@ func (m *messageCounter) Reset() { } func (m *messageCounter) Size() int { - return fr.Bytes + return messageCounterBlockSize } func (m *messageCounter) BlockSize() int { - return fr.Bytes + return messageCounterBlockSize } func newMessageCounter(startState, step int) hash.Hash { diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index efad7f5075..2690a1b795 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -64,7 +64,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript transcript + transcript *transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -84,7 +84,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: transcript{h: hasher}, + transcript: gkrcore.NewHashTranscript[fr.Element](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -160,7 +160,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -203,7 +203,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -248,7 +248,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { return fmt.Errorf("level %d: %v", levelI, err) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -295,7 +295,7 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} var boundOutputEvals []fr.Element diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index 6736557088..31f8c7e52a 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -16,8 +16,9 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bn254/fr" - "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + gcHash "github.com/consensys/gnark-crypto/hash" + _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -299,7 +300,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, mimc.NewMiMC()) + _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BN254.New()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bn254/sumcheck.go b/gkr/bn254/sumcheck.go index f87b9babdb..21f35fc3a9 100644 --- a/gkr/bn254/sumcheck.go +++ b/gkr/bn254/sumcheck.go @@ -7,7 +7,6 @@ package gkr import ( "errors" - "hash" "sync" "github.com/consensys/gnark-crypto/ecc/bn254/fr" @@ -19,39 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript is a Fiat-Shamir transcript backed by a running hash. -// Field elements are written via Bind; challenges are derived via getChallenge. -// The hash is never reset — all previous data is implicitly part of future challenges. -type transcript struct { - h hash.Hash - bound bool // whether Bind was called since the last getChallenge -} - -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *transcript) Bind(elements ...fr.Element) { - if len(elements) == 0 { - return - } - for i := range elements { - bytes := elements[i].Bytes() - t.h.Write(bytes[:]) - } - t.bound = true -} - -// getChallenge binds optional elements, then squeezes a challenge from the current hash state. -// If no bindings were added since the last squeeze, a separator byte is written first -// to advance the state and prevent repeated values. -func (t *transcript) getChallenge(bindings ...fr.Element) fr.Element { - t.Bind(bindings...) - if !t.bound { - t.h.Write([]byte{0}) - } - t.bound = false - var res fr.Element - res.SetBytes(t.h.Sum(nil)) - return res -} +// transcript specializes gkrcore's generic hash transcript to this field. +type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -85,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.getChallenge(proof.partialSumPolys[j]...) + challenges[j] = t.Challenge(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.getChallenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -111,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.getChallenge(proof.partialSumPolys[j]...) + r[j] = t.Challenge(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -386,7 +354,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.getChallenge() + c.foldingCoeff = r.transcript.Challenge() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -461,7 +429,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { } } - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } // verifyLevelSetup derives the folding coefficient, collects all claimed wire @@ -471,7 +439,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.getChallenge() + foldingCoeff = r.transcript.Challenge() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -493,7 +461,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) level := r.schedule[levelI].(*constraint.GkrSumcheckLevel) - return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), &r.transcript) + return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), r.transcript) } // singleSourceZeroCheckClaims is the prover-side claim for a single-source @@ -687,7 +655,7 @@ func (r *resources) proveSingleSourceZeroCheckLevel(levelI int) sumcheckProof { q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] claims.suffixEq = r.buildSuffixEq(q) - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) error { @@ -728,7 +696,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.getChallenge(partialPoly...) + challenges[j] = r.transcript.Challenge(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bn254/sumcheck_test.go b/gkr/bn254/sumcheck_test.go index 15cff2d307..a9ce79ec4a 100644 --- a/gkr/bn254/sumcheck_test.go +++ b/gkr/bn254/sumcheck_test.go @@ -10,6 +10,7 @@ import ( "hash" "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/stretchr/testify/assert" "math/bits" @@ -27,9 +28,9 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } claim := singleMultilinClaim{g: poly.Clone()} - t := transcript{h: hashGenerator()} + t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) - proof := sumcheckProve(&claim, &t) + proof := sumcheckProve(&claim, t) var sb strings.Builder for _, p := range proof.partialSumPolys { @@ -45,15 +46,15 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t) == nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } return nil diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 7b2dfc4baa..7ff4b69beb 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -39,6 +39,9 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } +// messageCounterBlockSize is the length of an element's Marshal(). +const messageCounterBlockSize = fr.Bytes + // messageCounter is a stand-in hash whose state depends only on the number of field-element // blocks written to it, not on their values. type messageCounter struct { @@ -48,18 +51,17 @@ type messageCounter struct { } func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/fr.Bytes + 1 + inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 m.state += int64(inputBlockSize) * m.step return len(p), nil } func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/fr.Bytes + 1 + inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 resI := m.state + int64(inputBlockSize)*m.step var res fr.Element res.SetInt64(int64(resI)) - resBytes := res.Bytes() - return resBytes[:] + return (&res).Marshal() } func (m *messageCounter) Reset() { @@ -67,11 +69,11 @@ func (m *messageCounter) Reset() { } func (m *messageCounter) Size() int { - return fr.Bytes + return messageCounterBlockSize } func (m *messageCounter) BlockSize() int { - return fr.Bytes + return messageCounterBlockSize } func newMessageCounter(startState, step int) hash.Hash { diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index ce538283b0..33a06a0134 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -64,7 +64,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript transcript + transcript *transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -84,7 +84,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: transcript{h: hasher}, + transcript: gkrcore.NewHashTranscript[fr.Element](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -160,7 +160,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -203,7 +203,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -248,7 +248,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { return fmt.Errorf("level %d: %v", levelI, err) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -295,7 +295,7 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} var boundOutputEvals []fr.Element diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index 826fd3955f..a35d0bc024 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -16,8 +16,9 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/mimc" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + gcHash "github.com/consensys/gnark-crypto/hash" + _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -299,7 +300,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, mimc.NewMiMC()) + _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BW6_761.New()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bw6-761/sumcheck.go b/gkr/bw6-761/sumcheck.go index 86303c4c86..e482d08d93 100644 --- a/gkr/bw6-761/sumcheck.go +++ b/gkr/bw6-761/sumcheck.go @@ -7,7 +7,6 @@ package gkr import ( "errors" - "hash" "sync" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" @@ -19,39 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript is a Fiat-Shamir transcript backed by a running hash. -// Field elements are written via Bind; challenges are derived via getChallenge. -// The hash is never reset — all previous data is implicitly part of future challenges. -type transcript struct { - h hash.Hash - bound bool // whether Bind was called since the last getChallenge -} - -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *transcript) Bind(elements ...fr.Element) { - if len(elements) == 0 { - return - } - for i := range elements { - bytes := elements[i].Bytes() - t.h.Write(bytes[:]) - } - t.bound = true -} - -// getChallenge binds optional elements, then squeezes a challenge from the current hash state. -// If no bindings were added since the last squeeze, a separator byte is written first -// to advance the state and prevent repeated values. -func (t *transcript) getChallenge(bindings ...fr.Element) fr.Element { - t.Bind(bindings...) - if !t.bound { - t.h.Write([]byte{0}) - } - t.bound = false - var res fr.Element - res.SetBytes(t.h.Sum(nil)) - return res -} +// transcript specializes gkrcore's generic hash transcript to this field. +type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -85,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.getChallenge(proof.partialSumPolys[j]...) + challenges[j] = t.Challenge(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.getChallenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -111,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.getChallenge(proof.partialSumPolys[j]...) + r[j] = t.Challenge(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -386,7 +354,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.getChallenge() + c.foldingCoeff = r.transcript.Challenge() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -461,7 +429,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { } } - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } // verifyLevelSetup derives the folding coefficient, collects all claimed wire @@ -471,7 +439,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.getChallenge() + foldingCoeff = r.transcript.Challenge() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -493,7 +461,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) level := r.schedule[levelI].(*constraint.GkrSumcheckLevel) - return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), &r.transcript) + return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), r.transcript) } // singleSourceZeroCheckClaims is the prover-side claim for a single-source @@ -687,7 +655,7 @@ func (r *resources) proveSingleSourceZeroCheckLevel(levelI int) sumcheckProof { q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] claims.suffixEq = r.buildSuffixEq(q) - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) error { @@ -728,7 +696,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.getChallenge(partialPoly...) + challenges[j] = r.transcript.Challenge(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bw6-761/sumcheck_test.go b/gkr/bw6-761/sumcheck_test.go index d5fd5d305a..fa539a23cb 100644 --- a/gkr/bw6-761/sumcheck_test.go +++ b/gkr/bw6-761/sumcheck_test.go @@ -10,6 +10,7 @@ import ( "hash" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/stretchr/testify/assert" "math/bits" @@ -27,9 +28,9 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } claim := singleMultilinClaim{g: poly.Clone()} - t := transcript{h: hashGenerator()} + t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) - proof := sumcheckProve(&claim, &t) + proof := sumcheckProve(&claim, t) var sb strings.Builder for _, p := range proof.partialSumPolys { @@ -45,15 +46,15 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t) == nil { + t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } return nil diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index e2faee1e89..2e91635638 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -39,6 +39,9 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } +// messageCounterBlockSize is the length of an element's Marshal(). +const messageCounterBlockSize = fr.Bytes + // messageCounter is a stand-in hash whose state depends only on the number of field-element // blocks written to it, not on their values. type messageCounter struct { @@ -48,18 +51,17 @@ type messageCounter struct { } func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/fr.Bytes + 1 + inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 m.state += int64(inputBlockSize) * m.step return len(p), nil } func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/fr.Bytes + 1 + inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 resI := m.state + int64(inputBlockSize)*m.step var res fr.Element res.SetInt64(int64(resI)) - resBytes := res.Bytes() - return resBytes[:] + return (&res).Marshal() } func (m *messageCounter) Reset() { @@ -67,11 +69,11 @@ func (m *messageCounter) Reset() { } func (m *messageCounter) Size() int { - return fr.Bytes + return messageCounterBlockSize } func (m *messageCounter) BlockSize() int { - return fr.Bytes + return messageCounterBlockSize } func newMessageCounter(startState, step int) hash.Hash { diff --git a/go.mod b/go.mod index 868eee3e25..07a1930905 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.21.1-0.20260913224755-1409fe592052 + github.com/consensys/gnark-crypto v0.21.1-0.20261001213005-bfa51d3c359f github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 diff --git a/go.sum b/go.sum index 1bf9ea46ff..e908f4f0d2 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.21.1-0.20260913224755-1409fe592052 h1:iw/l5FVYu5PctRwOyooaOjqVWKomojbK+L9u84B9iXM= -github.com/consensys/gnark-crypto v0.21.1-0.20260913224755-1409fe592052/go.mod h1:hdTjDNjdkYJ1oVuc8emh9XEhfM1SbyZhJigFqItiOLk= +github.com/consensys/gnark-crypto v0.21.1-0.20261001213005-bfa51d3c359f h1:xlMnEpSPcCOj1GPYgVlBvNvBIBTPmMOEx347Zcqtco0= +github.com/consensys/gnark-crypto v0.21.1-0.20261001213005-bfa51d3c359f/go.mod h1:hdTjDNjdkYJ1oVuc8emh9XEhfM1SbyZhJigFqItiOLk= github.com/consensys/gnark-solidity-checker v0.2.0 h1:i5iUEzNOkUvpaKm23UEe0wajBMwj7NzyT4EI0T2N8WQ= github.com/consensys/gnark-solidity-checker v0.2.0/go.mod h1:cEvl4g5AH+L4qGQLDOVZjqvn5IKZIAZdhSi8zAM6BiY= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index 61f29e8091..f51a900354 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -361,6 +361,10 @@ type gkrConfig struct { // EvaluatorQualifier prefixes references to the gate evaluator types, for fields whose prover // package is not the evaluators' own package. Empty when they are the same package. EvaluatorQualifier string + // ExtensionSuffix is the suffix gnark-crypto's extensions package appends to the names of an + // extension's types and constants (e.g. VectorE6, BytesE6). Empty for the curves and + // small_rational. + ExtensionSuffix string } func assertNoError(err error) { diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index 4799c807e1..26120ce1bd 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -59,7 +59,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript transcript + transcript *transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -79,7 +79,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: transcript{h: hasher}, + transcript: gkrcore.NewHashTranscript[{{ .ElementType }}](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -155,7 +155,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -198,7 +198,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]{{ .ElementType }}, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]{{ .ElementType }}{firstChallenge} @@ -243,7 +243,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { return fmt.Errorf("level %d: %v", levelI, err) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -290,7 +290,7 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]{{ .ElementType }}, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]{{ .ElementType }}{firstChallenge} var boundOutputEvals []{{ .ElementType }} diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index da54e58103..62dbb70ad5 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -9,8 +9,9 @@ import ( "time" "github.com/consensys/gnark-crypto/ecc" + gcHash "github.com/consensys/gnark-crypto/hash" + _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "{{ .FieldPackagePath }}" - "{{ .FieldPackagePath }}/mimc" "{{ .FieldPackagePath }}/polynomial" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -293,7 +294,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, mimc.NewMiMC()) + _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_{{ .FieldID }}.New()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index 86ca059ae1..c6dd55b64c 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -1,6 +1,5 @@ import ( "errors" - "hash" "sync" "{{.FieldPackagePath}}" @@ -14,39 +13,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript is a Fiat-Shamir transcript backed by a running hash. -// Field elements are written via Bind; challenges are derived via getChallenge. -// The hash is never reset — all previous data is implicitly part of future challenges. -type transcript struct { - h hash.Hash - bound bool // whether Bind was called since the last getChallenge -} - -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *transcript) Bind(elements ...{{ .ElementType }}) { - if len(elements) == 0 { - return - } - for i := range elements { - bytes := elements[i].Bytes() - t.h.Write(bytes[:]) - } - t.bound = true -} - -// getChallenge binds optional elements, then squeezes a challenge from the current hash state. -// If no bindings were added since the last squeeze, a separator byte is written first -// to advance the state and prevent repeated values. -func (t *transcript) getChallenge(bindings ...{{ .ElementType }}) {{ .ElementType }} { - t.Bind(bindings...) - if !t.bound { - t.h.Write([]byte{0}) - } - t.bound = false - var res {{ .ElementType }} - res.SetBytes(t.h.Sum(nil)) - return res -} +// transcript specializes gkrcore's generic hash transcript to this field. +type transcript = gkrcore.HashTranscript[{{ .ElementType }}, *{{ .ElementType }}] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -80,11 +48,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]{{ .ElementType }}, varsNum) for j := range varsNum - 1 { - challenges[j] = t.getChallenge(proof.partialSumPolys[j]...) + challenges[j] = t.Challenge(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.getChallenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -106,7 +74,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum { copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.getChallenge(proof.partialSumPolys[j]...) + r[j] = t.Challenge(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -381,7 +349,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.getChallenge() + c.foldingCoeff = r.transcript.Challenge() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -456,7 +424,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { } } - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } // verifyLevelSetup derives the folding coefficient, collects all claimed wire @@ -466,7 +434,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) ({{ .ElementType } level := r.schedule[levelI] var foldingCoeff {{ .ElementType }} if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.getChallenge() + foldingCoeff = r.transcript.Challenge() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -488,7 +456,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) ({{ .ElementType } func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) level := r.schedule[levelI].(*constraint.GkrSumcheckLevel) - return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), &r.transcript) + return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), r.transcript) } // singleSourceZeroCheckClaims is the prover-side claim for a single-source @@ -682,7 +650,7 @@ func (r *resources) proveSingleSourceZeroCheckLevel(levelI int) sumcheckProof { q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] claims.suffixEq = r.buildSuffixEq(q) - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) error { @@ -723,7 +691,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.getChallenge(partialPoly...) + challenges[j] = r.transcript.Challenge(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl index 146a67727d..a07aab1365 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl @@ -1,8 +1,9 @@ import ( "fmt" + "hash" "{{ .FieldPackagePath }}/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/stretchr/testify/assert" - "hash" {{ if not .GenerateTestVectors}} "{{ .FieldPackagePath }}" "math/bits" @@ -18,9 +19,9 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } claim := singleMultilinClaim{g: poly.Clone()} - t := transcript{h: hashGenerator()} + t := gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) - proof := sumcheckProve(&claim, &t) + proof := sumcheckProve(&claim, t) var sb strings.Builder for _, p := range proof.partialSumPolys { @@ -36,15 +37,15 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) + if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t) == nil { + t = gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) + if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } return nil diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl index de9055e2f4..c77e9c918e 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -1,10 +1,11 @@ import ( "encoding/json" "fmt" + "hash" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/small_rational" "github.com/consensys/gnark/internal/small_rational/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" - "hash" "math/bits" "os" "path/filepath" @@ -30,8 +31,8 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } claim := singleMultilinClaim{poly} - t := transcript{h: hsh} - proof := sumcheckProve(&claim, &t) + t := gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + proof := sumcheckProve(&claim, t) testCaseInfo.Proof = sumcheckToPrintableProof(proof) // Verification @@ -48,8 +49,8 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = transcript{h: hsh} - if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err != nil { return fmt.Errorf("proof rejected: %v", err) } @@ -57,8 +58,8 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = transcript{h: hsh} - if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, &t); err == nil { + t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 028a5b4a2a..077272cd40 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -1,8 +1,8 @@ import ( "fmt" + "hash" "{{.FieldPackagePath}}" "{{.FieldPackagePath}}/polynomial" - "hash" {{if eq .ElementType "fr.Element"}}"strings"{{- end}} "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -31,6 +31,9 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } +// messageCounterBlockSize is the length of an element's Marshal(). +const messageCounterBlockSize = {{ .FieldPackageName }}.Bytes{{ .ExtensionSuffix }} + // messageCounter is a stand-in hash whose state depends only on the number of field-element // blocks written to it, not on their values. type messageCounter struct { @@ -40,18 +43,17 @@ type messageCounter struct { } func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/{{.FieldPackageName}}.Bytes + 1 + inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 m.state += int64(inputBlockSize) * m.step return len(p), nil } func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/{{.FieldPackageName}}.Bytes + 1 + inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 resI := m.state + int64(inputBlockSize)*m.step var res {{.ElementType}} res.SetInt64(int64(resI)) - resBytes := res.Bytes() - return resBytes[:] + return (&res).Marshal() } func (m *messageCounter) Reset() { @@ -59,11 +61,11 @@ func (m *messageCounter) Reset() { } func (m *messageCounter) Size() int { - return {{.FieldPackageName}}.Bytes + return messageCounterBlockSize } func (m *messageCounter) BlockSize() int { - return {{.FieldPackageName}}.Bytes + return messageCounterBlockSize } func newMessageCounter(startState, step int) hash.Hash { diff --git a/internal/gkr/gkrcore/gate_test.go b/internal/gkr/gkrcore/gate_test.go index c7992289cf..caa4944831 100644 --- a/internal/gkr/gkrcore/gate_test.go +++ b/internal/gkr/gkrcore/gate_test.go @@ -14,8 +14,7 @@ import ( func bn254() Field { return PrimeField(ecc.BN254.ScalarField()) } // koalabearE6 describes KoalaBear's degree-6 extension E6, whose tower generator v has minimal -// polynomial X⁶ - 2X³ - 2. It is test data only here; the next PR exports this description -// alongside gnark-crypto's own E6 type. +// polynomial X⁶ - 2X³ - 2. It is test data only here, not a public description of the field. func koalabearE6() Field { return Field{ Modulus: big.NewInt(2130706433), // 2^31 - 2^24 + 1 @@ -23,8 +22,8 @@ func koalabearE6() Field { } } -// testFields runs under bn254(), as before commit A, and under the KoalaBear E6 -// description, to check that both agree on every gate's metadata. +// testFields runs under bn254() and under the KoalaBear E6 description, to check that both +// agree on every gate's metadata. func testFields() []Field { return []Field{bn254(), koalabearE6()} } diff --git a/internal/gkr/gkrcore/transcript.go b/internal/gkr/gkrcore/transcript.go new file mode 100644 index 0000000000..929a63ac45 --- /dev/null +++ b/internal/gkr/gkrcore/transcript.go @@ -0,0 +1,54 @@ +package gkrcore + +import "hash" + +// HashTranscript is a Fiat-Shamir transcript backed by a running hash. Field elements are written +// via Bind; challenges are derived via Challenge. The hash is never reset — all previous data is +// implicitly part of future challenges. +type HashTranscript[E any, PE interface { + *E + SetBytes([]byte) *E + Marshal() []byte +}] struct { + h hash.Hash + size int // length of an element's Marshal(), computed once from the zero element + bound bool // whether Bind was called since the last Challenge +} + +// NewHashTranscript returns a transcript backed by h. Bind writes each element's Marshal(). +// Challenge writes a separator byte if nothing was bound since the last challenge, then returns +// the element set by SetBytes from the first len(Marshal()) bytes of h.Sum(nil). h.Size() must be +// at least len(Marshal()). +func NewHashTranscript[E any, PE interface { + *E + SetBytes([]byte) *E + Marshal() []byte +}](h hash.Hash) *HashTranscript[E, PE] { + var zero E + return &HashTranscript[E, PE]{h: h, size: len(PE(&zero).Marshal())} +} + +// Bind writes field elements to the transcript as bindings for the next challenge. +func (t *HashTranscript[E, PE]) Bind(elements ...E) { + if len(elements) == 0 { + return + } + for i := range elements { + t.h.Write(PE(&elements[i]).Marshal()) + } + t.bound = true +} + +// Challenge binds elements as Bind would, then squeezes a challenge from the current hash state. +// If no bindings were added since the last challenge, a separator byte is written first to +// advance the state and prevent repeated values. +func (t *HashTranscript[E, PE]) Challenge(elements ...E) E { + t.Bind(elements...) + if !t.bound { + t.h.Write([]byte{0}) + } + t.bound = false + var res E + PE(&res).SetBytes(t.h.Sum(nil)[:t.size]) + return res +} diff --git a/internal/gkr/small_rational/gkr.go b/internal/gkr/small_rational/gkr.go index 1b11aaab05..99b553d7a6 100644 --- a/internal/gkr/small_rational/gkr.go +++ b/internal/gkr/small_rational/gkr.go @@ -63,7 +63,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript transcript + transcript *transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -83,7 +83,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: transcript{h: hasher}, + transcript: gkrcore.NewHashTranscript[small_rational.SmallRational](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -159,7 +159,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -202,7 +202,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]small_rational.SmallRational, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]small_rational.SmallRational{firstChallenge} @@ -247,7 +247,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { return fmt.Errorf("level %d: %v", levelI, err) } bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(&r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -294,7 +294,7 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]small_rational.SmallRational, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.getChallenge() + firstChallenge[j] = r.transcript.Challenge() } r.outgoingEvalPoints[len(schedule)] = [][]small_rational.SmallRational{firstChallenge} var boundOutputEvals []small_rational.SmallRational diff --git a/internal/gkr/small_rational/sumcheck.go b/internal/gkr/small_rational/sumcheck.go index 1667fa2532..39e8e00cb2 100644 --- a/internal/gkr/small_rational/sumcheck.go +++ b/internal/gkr/small_rational/sumcheck.go @@ -7,7 +7,6 @@ package gkr import ( "errors" - "hash" "sync" "github.com/consensys/gnark/constraint" @@ -18,39 +17,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript is a Fiat-Shamir transcript backed by a running hash. -// Field elements are written via Bind; challenges are derived via getChallenge. -// The hash is never reset — all previous data is implicitly part of future challenges. -type transcript struct { - h hash.Hash - bound bool // whether Bind was called since the last getChallenge -} - -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *transcript) Bind(elements ...small_rational.SmallRational) { - if len(elements) == 0 { - return - } - for i := range elements { - bytes := elements[i].Bytes() - t.h.Write(bytes[:]) - } - t.bound = true -} - -// getChallenge binds optional elements, then squeezes a challenge from the current hash state. -// If no bindings were added since the last squeeze, a separator byte is written first -// to advance the state and prevent repeated values. -func (t *transcript) getChallenge(bindings ...small_rational.SmallRational) small_rational.SmallRational { - t.Bind(bindings...) - if !t.bound { - t.h.Write([]byte{0}) - } - t.bound = false - var res small_rational.SmallRational - res.SetBytes(t.h.Sum(nil)) - return res -} +// transcript specializes gkrcore's generic hash transcript to this field. +type transcript = gkrcore.HashTranscript[small_rational.SmallRational, *small_rational.SmallRational] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -84,11 +52,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]small_rational.SmallRational, varsNum) for j := range varsNum - 1 { - challenges[j] = t.getChallenge(proof.partialSumPolys[j]...) + challenges[j] = t.Challenge(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.getChallenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -110,7 +78,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum s copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.getChallenge(proof.partialSumPolys[j]...) + r[j] = t.Challenge(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -385,7 +353,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.getChallenge() + c.foldingCoeff = r.transcript.Challenge() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -460,7 +428,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { } } - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } // verifyLevelSetup derives the folding coefficient, collects all claimed wire @@ -470,7 +438,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (small_rational.Sm level := r.schedule[levelI] var foldingCoeff small_rational.SmallRational if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.getChallenge() + foldingCoeff = r.transcript.Challenge() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -492,7 +460,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (small_rational.Sm func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) level := r.schedule[levelI].(*constraint.GkrSumcheckLevel) - return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), &r.transcript) + return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), r.transcript) } // singleSourceZeroCheckClaims is the prover-side claim for a single-source @@ -686,7 +654,7 @@ func (r *resources) proveSingleSourceZeroCheckLevel(levelI int) sumcheckProof { q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] claims.suffixEq = r.buildSuffixEq(q) - return sumcheckProve(&claims, &r.transcript) + return sumcheckProve(&claims, r.transcript) } func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) error { @@ -727,7 +695,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.getChallenge(partialPoly...) + challenges[j] = r.transcript.Challenge(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/internal/gkr/small_rational/sumcheck_test.go b/internal/gkr/small_rational/sumcheck_test.go index b364291f7b..e94757059a 100644 --- a/internal/gkr/small_rational/sumcheck_test.go +++ b/internal/gkr/small_rational/sumcheck_test.go @@ -9,6 +9,8 @@ import ( "fmt" "hash" + "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/small_rational" "github.com/consensys/gnark/internal/small_rational/polynomial" "github.com/stretchr/testify/assert" @@ -23,9 +25,9 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } claim := singleMultilinClaim{g: poly.Clone()} - t := transcript{h: hashGenerator()} + t := gkrcore.NewHashTranscript[small_rational.SmallRational](hashGenerator()) - proof := sumcheckProve(&claim, &t) + proof := sumcheckProve(&claim, t) var sb strings.Builder for _, p := range proof.partialSumPolys { @@ -41,15 +43,15 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[small_rational.SmallRational](hashGenerator()) + if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = transcript{h: hashGenerator()} - if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, &t) == nil { + t = gkrcore.NewHashTranscript[small_rational.SmallRational](hashGenerator()) + if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } return nil diff --git a/internal/gkr/small_rational/sumcheck_test_vector_gen.go b/internal/gkr/small_rational/sumcheck_test_vector_gen.go index 89ad304949..785c2a08c7 100644 --- a/internal/gkr/small_rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/small_rational/sumcheck_test_vector_gen.go @@ -14,6 +14,7 @@ import ( "path/filepath" "runtime/pprof" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/small_rational" "github.com/consensys/gnark/internal/small_rational/polynomial" @@ -38,8 +39,8 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } claim := singleMultilinClaim{poly} - t := transcript{h: hsh} - proof := sumcheckProve(&claim, &t) + t := gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + proof := sumcheckProve(&claim, t) testCaseInfo.Proof = sumcheckToPrintableProof(proof) // Verification @@ -56,8 +57,8 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = transcript{h: hsh} - if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, &t); err != nil { + t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err != nil { return fmt.Errorf("proof rejected: %v", err) } @@ -65,8 +66,8 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = transcript{h: hsh} - if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, &t); err == nil { + t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/gkr/small_rational/test_vector_utils.go b/internal/gkr/small_rational/test_vector_utils.go index 4c719dc17f..310ad64b09 100644 --- a/internal/gkr/small_rational/test_vector_utils.go +++ b/internal/gkr/small_rational/test_vector_utils.go @@ -39,6 +39,9 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } +// messageCounterBlockSize is the length of an element's Marshal(). +const messageCounterBlockSize = small_rational.Bytes + // messageCounter is a stand-in hash whose state depends only on the number of field-element // blocks written to it, not on their values. type messageCounter struct { @@ -48,18 +51,17 @@ type messageCounter struct { } func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/small_rational.Bytes + 1 + inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 m.state += int64(inputBlockSize) * m.step return len(p), nil } func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/small_rational.Bytes + 1 + inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 resI := m.state + int64(inputBlockSize)*m.step var res small_rational.SmallRational res.SetInt64(int64(resI)) - resBytes := res.Bytes() - return resBytes[:] + return (&res).Marshal() } func (m *messageCounter) Reset() { @@ -67,11 +69,11 @@ func (m *messageCounter) Reset() { } func (m *messageCounter) Size() int { - return small_rational.Bytes + return messageCounterBlockSize } func (m *messageCounter) BlockSize() int { - return small_rational.Bytes + return messageCounterBlockSize } func newMessageCounter(startState, step int) hash.Hash { diff --git a/internal/small_rational/small-rational.go b/internal/small_rational/small-rational.go index c561a84415..3ccdb3ae9a 100644 --- a/internal/small_rational/small-rational.go +++ b/internal/small_rational/small-rational.go @@ -439,7 +439,7 @@ func (z *SmallRational) BigInt(dst *big.Int) *big.Int { return dst } -func (z *SmallRational) SetBytes(b []byte) { +func (z *SmallRational) SetBytes(b []byte) *SmallRational { if len(b) > Bytes/2 { z.numerator = bytesToBigIntSigned(b[:Bytes/2]) z.denominator = bytesToBigIntSigned(b[Bytes/2:]) @@ -449,6 +449,7 @@ func (z *SmallRational) SetBytes(b []byte) { } z.simplify() z.UpdateText() + return z } func (z *SmallRational) SetBytesCanonical(bytes []byte) error { diff --git a/internal/smallfields/tinyfield/element.go b/internal/smallfields/tinyfield/element.go index 5f18dbb8ab..e21e81a627 100644 --- a/internal/smallfields/tinyfield/element.go +++ b/internal/smallfields/tinyfield/element.go @@ -368,6 +368,8 @@ func (z *Element) Double(x *Element) *Element { // Sub z = x - y (mod q) func (z *Element) Sub(x, y *Element) *Element { + // valid only because the modulus leaves a spare high bit: on underflow + // the wrapped difference is always > q. t := x[0] - y[0] if t > q { // underflow occurred t += q diff --git a/internal/smallfields/tinyfield/element_purego.go b/internal/smallfields/tinyfield/element_purego.go index 466b6ec1ae..c3b6b6f460 100644 --- a/internal/smallfields/tinyfield/element_purego.go +++ b/internal/smallfields/tinyfield/element_purego.go @@ -26,13 +26,13 @@ func MulBy13(x *Element) { x.Mul(x, &y) } -// Mul2ExpNegN multiplies x by -1/2^n +// Mul2ExpNegN multiplies x by 1/2^n // // Since the Montgomery constant is 2^32, the Montgomery form of 1/2^n is -// 2^{32-n}. Montgomery reduction works provided the input is < 2^32 so this -// works for 0 <= n <= 32. +// 2^{32-n}. Montgomery reduction works provided the input is +// < 2^32, so this works for 0 <= n <= 32. // -// N.B. n must be < 33. +// N.B. n must be <= 32. func (z *Element) Mul2ExpNegN(x *Element, n uint32) *Element { v := uint64(x[0]) << (32 - n) z[0] = montReduce(v) From 5e4df84419977de7d8aded93da3662bc07430d29 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 20:02:08 -0500 Subject: [PATCH 04/37] fix: benchmark error Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr_test.go | 5 ++++- gkr/bls12-381/gkr_test.go | 5 ++++- gkr/bn254/gkr_test.go | 5 ++++- gkr/bw6-761/gkr_test.go | 5 ++++- internal/generator/backend/template/gkr/gkr.test.go.tmpl | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index d4ef5601e1..2043544df7 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -293,7 +293,10 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { fmt.Println("evaluating circuit") start := time.Now().UnixMicro() - assignment := WireAssignment{in0, in1}.Complete(c) + assignment := make(WireAssignment, len(c)) + assignment[0] = in0 + assignment[1] = in1 + assignment = assignment.Complete(c) solved := time.Now().UnixMicro() - start fmt.Println("solved in", solved, "μs") diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index fb94706ab3..4843d4bc7b 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -293,7 +293,10 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { fmt.Println("evaluating circuit") start := time.Now().UnixMicro() - assignment := WireAssignment{in0, in1}.Complete(c) + assignment := make(WireAssignment, len(c)) + assignment[0] = in0 + assignment[1] = in1 + assignment = assignment.Complete(c) solved := time.Now().UnixMicro() - start fmt.Println("solved in", solved, "μs") diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index 31f8c7e52a..a887bdf6a3 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -293,7 +293,10 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { fmt.Println("evaluating circuit") start := time.Now().UnixMicro() - assignment := WireAssignment{in0, in1}.Complete(c) + assignment := make(WireAssignment, len(c)) + assignment[0] = in0 + assignment[1] = in1 + assignment = assignment.Complete(c) solved := time.Now().UnixMicro() - start fmt.Println("solved in", solved, "μs") diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index a35d0bc024..bba7a94547 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -293,7 +293,10 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { fmt.Println("evaluating circuit") start := time.Now().UnixMicro() - assignment := WireAssignment{in0, in1}.Complete(c) + assignment := make(WireAssignment, len(c)) + assignment[0] = in0 + assignment[1] = in1 + assignment = assignment.Complete(c) solved := time.Now().UnixMicro() - start fmt.Println("solved in", solved, "μs") diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index 62dbb70ad5..78f8aefac3 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -287,7 +287,10 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { fmt.Println("evaluating circuit") start := time.Now().UnixMicro() - assignment := WireAssignment{in0, in1}.Complete(c) + assignment := make(WireAssignment, len(c)) + assignment[0] = in0 + assignment[1] = in1 + assignment = assignment.Complete(c) solved := time.Now().UnixMicro() - start fmt.Println("solved in", solved, "μs") From c9bb550b1df9c70a357fdd6c7ca3b91aa1aa34f4 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 20:59:17 -0500 Subject: [PATCH 05/37] feat: config and codegen Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr_test.go | 2 +- gkr/bls12-377/test_vector_utils_test.go | 34 ++++++------- gkr/bls12-381/gkr_test.go | 2 +- gkr/bls12-381/test_vector_utils_test.go | 34 ++++++------- gkr/bn254/gkr_test.go | 2 +- gkr/bn254/test_vector_utils_test.go | 34 ++++++------- gkr/bw6-761/gkr_test.go | 2 +- gkr/bw6-761/test_vector_utils_test.go | 34 ++++++------- internal/generator/backend/main.go | 8 ++-- .../backend/template/gkr/gkr.test.go.tmpl | 8 ++-- .../template/gkr/gkr.test.vectors.gen.go.tmpl | 2 +- .../template/gkr/gkr.test.vectors.go.tmpl | 10 +--- .../template/gkr/test_vector_utils.go.tmpl | 48 +++++++------------ internal/gkr/gkr_test.go | 2 +- internal/gkr/gkrcore/schedule_test.go | 2 +- internal/gkr/gkrtesting/gkrtesting.go | 5 +- internal/gkr/gkrtesting/gkrtesting_test.go | 3 +- .../gkr/small_rational/test_vector_gen.go | 4 +- .../gkr/small_rational/test_vector_utils.go | 33 +++++++++++-- std/commitments/kzg/verifier.go | 10 ++-- std/permutation/gkr-mimc/gkr-mimc_test.go | 8 ++-- .../gkr-poseidon2/gkr-poseidon2_test.go | 8 ++-- 22 files changed, 143 insertions(+), 152 deletions(-) diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index 2043544df7..c41ecda7f7 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(ecc.BLS12_377.ScalarField()) +var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BLS12_377.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index 460b8b888d..e37168688a 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -8,7 +8,7 @@ package gkr import ( "fmt" "hash" - "strings" + "math/big" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" @@ -87,35 +87,31 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { } } +// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - - // TODO: Put this in element.SetString? + var r big.Rat switch v := value.(type) { case string: - - if sep := strings.Split(v, "/"); len(sep) == 2 { - var denom fr.Element - if _, err := z.SetString(sep[0]); err != nil { - return nil, err - } - if _, err := denom.SetString(sep[1]); err != nil { - return nil, err - } - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) } - case float64: asInt := int64(v) if float64(asInt) != v { return nil, fmt.Errorf("cannot currently parse float") } - z.SetInt64(asInt) - return z, nil + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) } - return z.SetInterface(value) + var denom fr.Element + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil } func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index 4843d4bc7b..b9f47831a7 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(ecc.BLS12_381.ScalarField()) +var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BLS12_381.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index 8509cb24d4..a94e6f4053 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -8,7 +8,7 @@ package gkr import ( "fmt" "hash" - "strings" + "math/big" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" @@ -87,35 +87,31 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { } } +// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - - // TODO: Put this in element.SetString? + var r big.Rat switch v := value.(type) { case string: - - if sep := strings.Split(v, "/"); len(sep) == 2 { - var denom fr.Element - if _, err := z.SetString(sep[0]); err != nil { - return nil, err - } - if _, err := denom.SetString(sep[1]); err != nil { - return nil, err - } - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) } - case float64: asInt := int64(v) if float64(asInt) != v { return nil, fmt.Errorf("cannot currently parse float") } - z.SetInt64(asInt) - return z, nil + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) } - return z.SetInterface(value) + var denom fr.Element + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil } func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index a887bdf6a3..ba01cf624e 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(ecc.BN254.ScalarField()) +var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 7ff4b69beb..4348dd5f48 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -8,7 +8,7 @@ package gkr import ( "fmt" "hash" - "strings" + "math/big" "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" @@ -87,35 +87,31 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { } } +// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - - // TODO: Put this in element.SetString? + var r big.Rat switch v := value.(type) { case string: - - if sep := strings.Split(v, "/"); len(sep) == 2 { - var denom fr.Element - if _, err := z.SetString(sep[0]); err != nil { - return nil, err - } - if _, err := denom.SetString(sep[1]); err != nil { - return nil, err - } - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) } - case float64: asInt := int64(v) if float64(asInt) != v { return nil, fmt.Errorf("cannot currently parse float") } - z.SetInt64(asInt) - return z, nil + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) } - return z.SetInterface(value) + var denom fr.Element + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil } func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index bba7a94547..02d1c6cf1b 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(ecc.BW6_761.ScalarField()) +var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BW6_761.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index 2e91635638..9d01aacc6e 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -8,7 +8,7 @@ package gkr import ( "fmt" "hash" - "strings" + "math/big" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" @@ -87,35 +87,31 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { } } +// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - - // TODO: Put this in element.SetString? + var r big.Rat switch v := value.(type) { case string: - - if sep := strings.Split(v, "/"); len(sep) == 2 { - var denom fr.Element - if _, err := z.SetString(sep[0]); err != nil { - return nil, err - } - if _, err := denom.SetString(sep[1]); err != nil { - return nil, err - } - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) } - case float64: asInt := int64(v) if float64(asInt) != v { return nil, fmt.Errorf("cannot currently parse float") } - z.SetInt64(asInt) - return z, nil + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) } - return z.SetInterface(value) + var denom fr.Element + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil } func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index f51a900354..ce109694ec 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -151,8 +151,8 @@ func main() { FieldPackageName: "fr", FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", FieldID: d.CurveID, + FieldDescription: "gkrcore.PrimeField(ecc." + d.CurveID + ".ScalarField())", GkrPackageName: curvePackageName, - CanUseFFT: true, EvaluatorQualifier: "evaluator.", } @@ -238,7 +238,6 @@ func main() { FieldPackagePath: "github.com/consensys/gnark/internal/small_rational", FieldPackageName: "small_rational", GkrPackageName: "small_rational", - CanUseFFT: false, NoGkrTests: true, GenerateTestVectors: true, } @@ -355,7 +354,6 @@ type gkrConfig struct { FieldPackageName string GkrPackageName string // the GKR package, relative to the repo root FieldID string // e.g. BLS12_377, BABYBEAR, etc. - CanUseFFT bool GenerateTestVectors bool NoGkrTests bool // EvaluatorQualifier prefixes references to the gate evaluator types, for fields whose prover @@ -365,6 +363,10 @@ type gkrConfig struct { // extension's types and constants (e.g. VectorE6, BytesE6). Empty for the curves and // small_rational. ExtensionSuffix string + // FieldDescription is the Go expression of the field's gkrcore.Field description, e.g. + // "gkrcore.PrimeField(ecc.BN254.ScalarField())". Used only where gkr.test.go.tmpl is + // generated (NoGkrTests false), so small_rational leaves it empty. + FieldDescription string } func assertNoError(err error) { diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index 78f8aefac3..356d627896 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -19,7 +19,7 @@ import ( "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(ecc.{{ .FieldID }}.ScalarField()) +var cache = gkrtesting.NewCache({{ .FieldDescription }}) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case @@ -175,7 +175,7 @@ func testWithSchedule(t *testing.T, circuit gkrcore.RawCircuit, schedule constra for i := range ins { insAssignment[i] = make([]{{ .ElementType }}, maxSize) - {{ .FieldPackageName }}.Vector(insAssignment[i]).MustSetRandom() + {{ .FieldPackageName }}.Vector{{ .ExtensionSuffix }}(insAssignment[i]).MustSetRandom() } fullAssignment := make(WireAssignment, len(circuit)) @@ -282,8 +282,8 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { in0 := make([]{{ .ElementType }}, nbInstances) in1 := make([]{{ .ElementType }}, nbInstances) - {{ .FieldPackageName }}.Vector(in0).MustSetRandom() - {{ .FieldPackageName }}.Vector(in1).MustSetRandom() + {{ .FieldPackageName }}.Vector{{ .ExtensionSuffix }}(in0).MustSetRandom() + {{ .FieldPackageName }}.Vector{{ .ExtensionSuffix }}(in1).MustSetRandom() fmt.Println("evaluating circuit") start := time.Now().UnixMicro() diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl index 38a55d4ac4..5bec46a3d0 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl @@ -17,7 +17,7 @@ import ( // The properties of test gates are expected to be the same across all relevant fields. // We can therefore use the gate testing functions for any curve rather than reimplementing // for small_rational. -var cache = gkrtesting.NewCache(ecc.BN254.ScalarField()) +var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) func GenerateVectors() error { testDirPath, err := filepath.Abs("../../gkr/test_vectors") diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl index b17c2638d3..fb99e2c543 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl @@ -9,7 +9,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]{{ .ElementType }}, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := {{ setElement "finalEvalProof[k]" "finalEvalSlice.Index(k).Interface()" .ElementType}}; err != nil { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -139,11 +139,3 @@ func newTestCase(path string) (*TestCase, error) { } {{ end }} - -{{- define "setElement element value elementType"}} -{{- if eq .elementType "fr.Element"}} setElement(&{{.element}}, {{.value}}) -{{- else if eq .elementType "small_rational.SmallRational"}} {{.element}}.SetInterface({{.value}}) -{{- else}} -{{print "\"UNEXPECTED TYPE" .elementType "\""}} -{{- end}} -{{- end}} diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 077272cd40..09571eb59d 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -1,9 +1,9 @@ import ( "fmt" "hash" + "math/big" "{{.FieldPackagePath}}" "{{.FieldPackagePath}}/polynomial" - {{if eq .ElementType "fr.Element"}}"strings"{{- end}} "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -79,51 +79,37 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { } } -{{ if eq .ElementType "fr.Element"}} -func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - - // TODO: Put this in element.SetString? +// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. +func setElement(z *{{.ElementType}}, value interface{}) (*{{.ElementType}}, error) { + var r big.Rat switch v := value.(type) { case string: - - if sep := strings.Split(v, "/"); len(sep) == 2 { - var denom fr.Element - if _, err := z.SetString(sep[0]); err != nil { - return nil, err - } - if _, err := denom.SetString(sep[1]); err != nil { - return nil, err - } - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) } - case float64: asInt := int64(v) if float64(asInt) != v { return nil, fmt.Errorf("cannot currently parse float") } - z.SetInt64(asInt) - return z, nil + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) } - return z.SetInterface(value) + var denom {{.ElementType}} + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil } -{{- end}} - -{{- define "setElement element value elementType"}} -{{- if eq .elementType "fr.Element"}} setElement(&{{.element}}, {{.value}}) -{{- else if eq .elementType "small_rational.SmallRational"}} {{.element}}.SetInterface({{.value}}) -{{- else}} - {{print "\"UNEXPECTED TYPE" .elementType "\""}} -{{- end}} -{{- end}} func sliceToElementSlice[T any](slice []T) ([]{{.ElementType}}, error) { elementSlice := make([]{{.ElementType}}, len(slice)) for i, v := range slice { - if _, err := {{setElement "elementSlice[i]" "v" .ElementType}}; err != nil { + if _, err := setElement(&elementSlice[i], v); err != nil { return nil, err } } diff --git a/internal/gkr/gkr_test.go b/internal/gkr/gkr_test.go index 4f48d46e11..e2603677af 100644 --- a/internal/gkr/gkr_test.go +++ b/internal/gkr/gkr_test.go @@ -354,4 +354,4 @@ func TestConstHash(t *testing.T) { ) } -var cache = gkrtesting.NewCache(ecc.BN254.ScalarField()) +var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) diff --git a/internal/gkr/gkrcore/schedule_test.go b/internal/gkr/gkrcore/schedule_test.go index c081a137b4..47a8ba766b 100644 --- a/internal/gkr/gkrcore/schedule_test.go +++ b/internal/gkr/gkrcore/schedule_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" ) -var scheduleTestCache = gkrtesting.NewCache(ecc.BN254.ScalarField()) +var scheduleTestCache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) func TestDefaultProvingSchedule(t *testing.T) { _, c := scheduleTestCache.Compile(t, gkrtesting.SingleMulGateCircuit()) diff --git a/internal/gkr/gkrtesting/gkrtesting.go b/internal/gkr/gkrtesting/gkrtesting.go index 990ac1eb5f..149d29a655 100644 --- a/internal/gkr/gkrtesting/gkrtesting.go +++ b/internal/gkr/gkrtesting/gkrtesting.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "math/big" "os" "path/filepath" "strconv" @@ -58,7 +57,7 @@ func selectInput3Gate(_ gkr.GateAPI, in ...frontend.Variable) frontend.Variable return in[2] } -func NewCache(field *big.Int) *Cache { +func NewCache(field gkrcore.Field) *Cache { gates := make(map[string]gkr.GateFunction, 7) gates[""] = nil gates["identity"] = gkrcore.Identity @@ -70,7 +69,7 @@ func NewCache(field *big.Int) *Cache { gates["select-input-3"] = selectInput3Gate return &Cache{ - field: gkrcore.PrimeField(field), + field: field, circuits: make(map[string]circuits), gates: gates, } diff --git a/internal/gkr/gkrtesting/gkrtesting_test.go b/internal/gkr/gkrtesting/gkrtesting_test.go index 56a976367a..8947bfc1b5 100644 --- a/internal/gkr/gkrtesting/gkrtesting_test.go +++ b/internal/gkr/gkrtesting/gkrtesting_test.go @@ -4,11 +4,12 @@ import ( "testing" "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/stretchr/testify/assert" ) func TestLoadCircuit(t *testing.T) { - cache := NewCache(ecc.BN254.ScalarField()) + cache := NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) _, c := cache.GetCircuit("../test_vectors/circuits/two_identity_gates_composed_single_input.json") assert.Equal(t, 0, len(c[0].Inputs)) assert.Equal(t, []int{0}, c[1].Inputs) diff --git a/internal/gkr/small_rational/test_vector_gen.go b/internal/gkr/small_rational/test_vector_gen.go index 7b61f337bb..8829361a0a 100644 --- a/internal/gkr/small_rational/test_vector_gen.go +++ b/internal/gkr/small_rational/test_vector_gen.go @@ -25,7 +25,7 @@ import ( // The properties of test gates are expected to be the same across all relevant fields. // We can therefore use the gate testing functions for any curve rather than reimplementing // for small_rational. -var cache = gkrtesting.NewCache(ecc.BN254.ScalarField()) +var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) func GenerateVectors() error { testDirPath, err := filepath.Abs("../../gkr/test_vectors") @@ -178,7 +178,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]small_rational.SmallRational, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := finalEvalProof[k].SetInterface(finalEvalSlice.Index(k).Interface()); err != nil { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } diff --git a/internal/gkr/small_rational/test_vector_utils.go b/internal/gkr/small_rational/test_vector_utils.go index 310ad64b09..6cca2581e1 100644 --- a/internal/gkr/small_rational/test_vector_utils.go +++ b/internal/gkr/small_rational/test_vector_utils.go @@ -8,11 +8,11 @@ package gkr import ( "fmt" "hash" + "math/big" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/small_rational" "github.com/consensys/gnark/internal/small_rational/polynomial" - - "github.com/consensys/gnark/internal/gkr/gkrtesting" ) func toElement(i int64) *small_rational.SmallRational { @@ -87,10 +87,37 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { } } +// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. +func setElement(z *small_rational.SmallRational, value interface{}) (*small_rational.SmallRational, error) { + var r big.Rat + switch v := value.(type) { + case string: + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) + } + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) + } + + var denom small_rational.SmallRational + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil +} + func sliceToElementSlice[T any](slice []T) ([]small_rational.SmallRational, error) { elementSlice := make([]small_rational.SmallRational, len(slice)) for i, v := range slice { - if _, err := elementSlice[i].SetInterface(v); err != nil { + if _, err := setElement(&elementSlice[i], v); err != nil { return nil, err } } diff --git a/std/commitments/kzg/verifier.go b/std/commitments/kzg/verifier.go index 809bc64f22..b3273ae952 100644 --- a/std/commitments/kzg/verifier.go +++ b/std/commitments/kzg/verifier.go @@ -609,11 +609,11 @@ func (v *Verifier[FR, G1El, G2El, GTEl]) FoldProof(digests []Commitment[G1El], b foldedEvaluations = v.scalarApi.Add(foldedEvaluations, tmp) } return OpeningProof[FR, G1El]{ - Quotient: batchOpeningProof.Quotient, - ClaimedValue: *foldedEvaluations, - }, Commitment[G1El]{ - G1El: *foldedDigests, - }, nil + Quotient: batchOpeningProof.Quotient, + ClaimedValue: *foldedEvaluations, + }, Commitment[G1El]{ + G1El: *foldedDigests, + }, nil } diff --git a/std/permutation/gkr-mimc/gkr-mimc_test.go b/std/permutation/gkr-mimc/gkr-mimc_test.go index 2ce31acfdd..d182ce1c78 100644 --- a/std/permutation/gkr-mimc/gkr-mimc_test.go +++ b/std/permutation/gkr-mimc/gkr-mimc_test.go @@ -69,8 +69,8 @@ func merkleTreeCircuits(n int) (circuit, assignment merkleTreeCircuit) { } return merkleTreeCircuit{ - Leaves: make([]frontend.Variable, len(leaves)), - }, merkleTreeCircuit{ - Leaves: leaves, - } + Leaves: make([]frontend.Variable, len(leaves)), + }, merkleTreeCircuit{ + Leaves: leaves, + } } diff --git a/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go b/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go index 10520bb7cb..b781fb04eb 100644 --- a/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go +++ b/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go @@ -21,10 +21,10 @@ func gkrCompressionsCircuits(n int) (circuit, assignment testGkrCompressionCircu } return testGkrCompressionCircuit{ - Ins: make([][2]frontend.Variable, len(ins)), - }, testGkrCompressionCircuit{ - Ins: ins, - } + Ins: make([][2]frontend.Variable, len(ins)), + }, testGkrCompressionCircuit{ + Ins: ins, + } } func TestGkrCompression(t *testing.T) { From 3fdb6b29ad29418bd51b003feeece324db80bd27 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 21:18:08 -0500 Subject: [PATCH 06/37] refac: just a big rat Signed-off-by: Arya Tabaie --- internal/small_rational/small-rational.go | 350 +++++------------- .../small_rational/small_rational_test.go | 58 ++- 2 files changed, 112 insertions(+), 296 deletions(-) diff --git a/internal/small_rational/small-rational.go b/internal/small_rational/small-rational.go index 3ccdb3ae9a..b77a8670a2 100644 --- a/internal/small_rational/small-rational.go +++ b/internal/small_rational/small-rational.go @@ -4,157 +4,68 @@ import ( "crypto/rand" "fmt" "math/big" - "strconv" - "strings" ) const Bytes = 64 // SmallRational implements the rational field, used to generate field agnostic test vectors. // It is not optimized for performance, so it is best used sparingly. -type SmallRational struct { - text string //For debugging purposes - numerator big.Int - denominator big.Int // By convention, denominator == 0 also indicates zero -} - -var smallPrimes = []*big.Int{ - big.NewInt(2), big.NewInt(3), big.NewInt(5), - big.NewInt(7), big.NewInt(11), big.NewInt(13), -} +// +// SmallRational wraps a big.Rat. Every method writes its result into a fresh big.Rat and assigns +// it to z.r, never mutating a receiver's existing storage, which makes value copies safe. +type SmallRational struct{ r big.Rat } -func bigDivides(p, a *big.Int) bool { - var remainder big.Int - remainder.Mod(a, p) - return remainder.BitLen() == 0 -} - -func (z *SmallRational) UpdateText() { - z.text = z.Text(10) -} - -func (z *SmallRational) simplify() { - - if z.numerator.BitLen() == 0 || z.denominator.BitLen() == 0 { - return - } - - var num, den big.Int - - num.Set(&z.numerator) - den.Set(&z.denominator) - - for _, p := range smallPrimes { - for bigDivides(p, &num) && bigDivides(p, &den) { - num.Div(&num, p) - den.Div(&den, p) - } - } - - if bigDivides(&den, &num) { - num.Div(&num, &den) - den.SetInt64(1) - } - - z.numerator = num - z.denominator = den - -} func (z *SmallRational) Square(x *SmallRational) *SmallRational { - var num, den big.Int - num.Mul(&x.numerator, &x.numerator) - den.Mul(&x.denominator, &x.denominator) - - z.numerator = num - z.denominator = den - - z.UpdateText() - + var res big.Rat + res.Mul(&x.r, &x.r) + z.r = res return z } func (z *SmallRational) String() string { - z.text = z.Text(10) - return z.text + return z.Text(10) } func (z *SmallRational) Add(x, y *SmallRational) *SmallRational { - if x.denominator.BitLen() == 0 { - *z = *y - } else if y.denominator.BitLen() == 0 { - *z = *x - } else { - //TODO: Exploit cases where one denom divides the other - var numDen, denNum big.Int - numDen.Mul(&x.numerator, &y.denominator) - denNum.Mul(&x.denominator, &y.numerator) - - numDen.Add(&denNum, &numDen) - z.numerator = numDen //to avoid shallow copy problems - - denNum.Mul(&x.denominator, &y.denominator) - z.denominator = denNum - z.simplify() - } - - z.UpdateText() - + var res big.Rat + res.Add(&x.r, &y.r) + z.r = res return z } func (z *SmallRational) IsZero() bool { - return z.numerator.BitLen() == 0 || z.denominator.BitLen() == 0 + return z.r.Sign() == 0 } +// Inverse sets z to 1/x, except that the inverse of 0 is 0, as for the curves' elements +// (big.Rat.Inv panics on 0). func (z *SmallRational) Inverse(x *SmallRational) *SmallRational { - if x.IsZero() { - *z = *x - } else { - *z = SmallRational{numerator: x.denominator, denominator: x.numerator} - z.UpdateText() + if x.r.Sign() == 0 { + z.r = big.Rat{} + return z } - + var res big.Rat + res.Inv(&x.r) + z.r = res return z } func (z *SmallRational) Neg(x *SmallRational) *SmallRational { - z.numerator.Neg(&x.numerator) - z.denominator = x.denominator - - if x.text == "" { - x.UpdateText() - } - - if x.text[0] == '-' { - z.text = x.text[1:] - } else { - z.text = "-" + x.text - } - + var res big.Rat + res.Neg(&x.r) + z.r = res return z } func (z *SmallRational) Double(x *SmallRational) *SmallRational { - - var y big.Int - - if x.denominator.Bit(0) == 0 { - z.numerator = x.numerator - y.Rsh(&x.denominator, 1) - z.denominator = y - } else { - y.Lsh(&x.numerator, 1) - z.numerator = y - z.denominator = x.denominator - } - - z.UpdateText() - + var res big.Rat + res.Add(&x.r, &x.r) + z.r = res return z } func (z *SmallRational) Sign() int { - return z.numerator.Sign() * z.denominator.Sign() + return z.r.Sign() } func (z *SmallRational) MarshalJSON() ([]byte, error) { @@ -171,33 +82,14 @@ func (z *SmallRational) Equal(x *SmallRational) bool { } func (z *SmallRational) Sub(x, y *SmallRational) *SmallRational { - var yNeg SmallRational - yNeg.Neg(y) - z.Add(x, &yNeg) - - z.UpdateText() + var res big.Rat + res.Sub(&x.r, &y.r) + z.r = res return z } func (z *SmallRational) Cmp(x *SmallRational) int { - zSign, xSign := z.Sign(), x.Sign() - - if zSign > xSign { - return 1 - } - if zSign < xSign { - return -1 - } - - var Z, X big.Int - Z.Mul(&z.numerator, &x.denominator) - X.Mul(&x.numerator, &z.denominator) - - Z.Abs(&Z) - X.Abs(&X) - - return Z.Cmp(&X) * zSign - + return z.r.Cmp(&x.r) } func BatchInvert(a []SmallRational) []SmallRational { @@ -209,42 +101,23 @@ func BatchInvert(a []SmallRational) []SmallRational { } func (z *SmallRational) Mul(x, y *SmallRational) *SmallRational { - var num, den big.Int - - num.Mul(&x.numerator, &y.numerator) - den.Mul(&x.denominator, &y.denominator) - - z.numerator = num - z.denominator = den - - z.simplify() - z.UpdateText() + var res big.Rat + res.Mul(&x.r, &y.r) + z.r = res 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() + var res big.Rat + res.Quo(&x.r, &y.r) + z.r = res 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() + var res big.Rat + res.Quo(&z.r, big.NewRat(2, 1)) + z.r = res return z } @@ -257,20 +130,22 @@ func (z *SmallRational) SetZero() *SmallRational { } func (z *SmallRational) SetInt64(i int64) *SmallRational { - z.numerator = *big.NewInt(i) - z.denominator = *big.NewInt(1) - z.text = strconv.FormatInt(i, 10) + var res big.Rat + res.SetInt64(i) + z.r = res return z } // SetBigInt sets z to the integer value i (denominator = 1). func (z *SmallRational) SetBigInt(i *big.Int) *SmallRational { - z.numerator.Set(i) - z.denominator.SetInt64(1) - z.text = i.String() + var res big.Rat + res.SetInt(i) + z.r = res return z } +// SetRandom sets z to a uniform random numerator in [-8, 7] over a uniform random denominator in +// [0, 15], with denominator 0 giving 0. func (z *SmallRational) SetRandom() (*SmallRational, error) { bytes := make([]byte, 1) @@ -282,11 +157,14 @@ func (z *SmallRational) SetRandom() (*SmallRational, error) { return nil, fmt.Errorf("%d bytes read instead of %d", n, len(bytes)) } - z.numerator = *big.NewInt(int64(bytes[0]%16) - 8) - z.denominator = *big.NewInt(int64((bytes[0]) / 16)) + num := int64(bytes[0]%16) - 8 + den := int64(bytes[0] / 16) - z.simplify() - z.UpdateText() + var res big.Rat + if den != 0 { + res.SetFrac64(num, den) + } + z.r = res return z, nil } @@ -299,49 +177,29 @@ func (z *SmallRational) MustSetRandom() *SmallRational { } func (z *SmallRational) SetUint64(i uint64) { - var num big.Int - num.SetUint64(i) - z.numerator = num - z.denominator = *big.NewInt(1) - z.text = strconv.FormatUint(i, 10) + var bi big.Int + bi.SetUint64(i) + var res big.Rat + res.SetInt(&bi) + z.r = res } func (z *SmallRational) IsOne() bool { - return z.numerator.Cmp(&z.denominator) == 0 && z.denominator.BitLen() != 0 + return z.r.Cmp(big.NewRat(1, 1)) == 0 } +// Text writes the numerator alone, in base, if z is an integer, else the numerator and the +// denominator, both in base, separated by "/". It does not modify z. func (z *SmallRational) Text(base int) string { - - if z.denominator.BitLen() == 0 { - return "0" - } - - if z.denominator.Sign() < 0 { - var num, den big.Int - num.Neg(&z.numerator) - den.Neg(&z.denominator) - z.numerator = num - z.denominator = den - } - - if bigDivides(&z.denominator, &z.numerator) { - var num big.Int - num.Div(&z.numerator, &z.denominator) - z.numerator = num - z.denominator = *big.NewInt(1) - } - - numerator := z.numerator.Text(base) - - if z.denominator.IsInt64() && z.denominator.Int64() == 1 { - return numerator + if z.r.IsInt() { + return z.r.Num().Text(base) } - - return numerator + "/" + z.denominator.Text(base) + return z.r.Num().Text(base) + "/" + z.r.Denom().Text(base) } +// Set sets z to x. func (z *SmallRational) Set(x *SmallRational) *SmallRational { - *z = *x // shallow copy is safe because ops are never in place + *z = *x return z } @@ -363,31 +221,11 @@ func (z *SmallRational) SetInterface(x interface{}) (*SmallRational, error) { } z.SetInt64(asInt) case string: - z.text = v - sep := strings.Split(v, "/") - switch len(sep) { - case 1: - if asInt, err := strconv.Atoi(sep[0]); err == nil { - z.SetInt64(int64(asInt)) - } else { - return nil, err - } - case 2: - var err error - var num, denom int - num, err = strconv.Atoi(sep[0]) - if err != nil { - return nil, err - } - denom, err = strconv.Atoi(sep[1]) - if err != nil { - return nil, err - } - z.numerator = *big.NewInt(int64(num)) - z.denominator = *big.NewInt(int64(denom)) - default: - return nil, fmt.Errorf("cannot parse \"%s\"", v) + var res big.Rat + if _, ok := res.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) } + z.r = res default: return nil, fmt.Errorf("cannot parse %T", x) } @@ -405,8 +243,8 @@ func bigIntToBytesSigned(dst []byte, src big.Int) { func (z *SmallRational) Bytes() [Bytes]byte { var res [Bytes]byte - bigIntToBytesSigned(res[:Bytes/2], z.numerator) - bigIntToBytesSigned(res[Bytes/2:], z.denominator) + bigIntToBytesSigned(res[:Bytes/2], *z.r.Num()) + bigIntToBytesSigned(res[Bytes/2:], *z.r.Denom()) return res } @@ -424,31 +262,34 @@ func bytesToBigIntSigned(src []byte) big.Int { return res } -// BigInt returns sets dst to the value of z if it is an integer. +// BigInt sets dst to the value of z if it is an integer, and returns dst. // if z is not an integer, nil is returned. -// if the given dst is nil, the address of the numerator is returned. -// if the given dst is non-nil, it is returned. +// if the given dst is nil, a new big.Int is allocated rather than returning a pointer into z. func (z *SmallRational) BigInt(dst *big.Int) *big.Int { - if z.denominator.Cmp(big.NewInt(1)) != 0 { + if !z.r.IsInt() { return nil } if dst == nil { - return &z.numerator + dst = new(big.Int) } - dst.Set(&z.numerator) + dst.Set(z.r.Num()) return dst } func (z *SmallRational) SetBytes(b []byte) *SmallRational { + var num, den big.Int if len(b) > Bytes/2 { - z.numerator = bytesToBigIntSigned(b[:Bytes/2]) - z.denominator = bytesToBigIntSigned(b[Bytes/2:]) + num = bytesToBigIntSigned(b[:Bytes/2]) + den = bytesToBigIntSigned(b[Bytes/2:]) } else { - z.numerator.SetBytes(b) - z.denominator.SetInt64(1) + num.SetBytes(b) + den.SetInt64(1) } - z.simplify() - z.UpdateText() + var res big.Rat + if den.BitLen() != 0 { // a zero denominator gives 0, as big.Rat.SetFrac panics on it + res.SetFrac(&num, &den) + } + z.r = res return z } @@ -458,16 +299,7 @@ func (z *SmallRational) SetBytesCanonical(bytes []byte) error { } 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) + var res SmallRational + res.SetInt64(1) return res } diff --git a/internal/small_rational/small_rational_test.go b/internal/small_rational/small_rational_test.go index 3db3ccab51..a636976857 100644 --- a/internal/small_rational/small_rational_test.go +++ b/internal/small_rational/small_rational_test.go @@ -1,48 +1,33 @@ package small_rational import ( - "math/big" + "fmt" "testing" "github.com/stretchr/testify/assert" ) -func TestBigDivides(t *testing.T) { - assert.True(t, bigDivides(big.NewInt(-1), big.NewInt(4))) - assert.False(t, bigDivides(big.NewInt(-3), big.NewInt(4))) -} - func TestCmp(t *testing.T) { - - cases := make([]SmallRational, 36) - + // 9 distinct values, (i-4)/2 for i = 0..8: -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, alternating + // between two SetInterface input forms: a plain integer and an explicit "n/2" fraction string. + cases := make([]SmallRational, 9) for i := int64(0); i < 9; i++ { + var err error if i%2 == 0 { - cases[4*i].numerator.SetInt64((i - 4) / 2) - cases[4*i].denominator.SetInt64(1) + _, err = cases[i].SetInterface((i - 4) / 2) } else { - cases[4*i].numerator.SetInt64(i - 4) - cases[4*i].denominator.SetInt64(2) + _, err = cases[i].SetInterface(fmt.Sprintf("%d/2", i-4)) } - - cases[4*i+1].numerator.Neg(&cases[4*i].numerator) - cases[4*i+1].denominator.Neg(&cases[4*i].denominator) - - cases[4*i+2].numerator.Lsh(&cases[4*i].numerator, 1) - cases[4*i+2].denominator.Lsh(&cases[4*i].denominator, 1) - - cases[4*i+3].numerator.Neg(&cases[4*i+2].numerator) - cases[4*i+3].denominator.Neg(&cases[4*i+2].denominator) + assert.NoError(t, err) } for i := range cases { for j := range cases { - I, J := i/4, j/4 var expectedCmp int cmp := cases[i].Cmp(&cases[j]) - if I < J { + if i < j { expectedCmp = -1 - } else if I == J { + } else if i == j { expectedCmp = 0 } else { expectedCmp = 1 @@ -51,29 +36,28 @@ func TestCmp(t *testing.T) { } } - zeroIndex := len(cases) / 8 - var weirdZero SmallRational + const zeroIndex = 4 // (4-4)/2 = 0 + var zero SmallRational for i := range cases { - I := i / 4 var expectedCmp int - cmp := cases[i].Cmp(&weirdZero) - cmpNeg := weirdZero.Cmp(&cases[i]) - if I < zeroIndex { + cmp := cases[i].Cmp(&zero) + cmpNeg := zero.Cmp(&cases[i]) + if i < zeroIndex { expectedCmp = -1 - } else if I == zeroIndex { + } else if i == zeroIndex { expectedCmp = 0 } else { expectedCmp = 1 } - assert.Equal(t, expectedCmp, cmp, "comparing index %d, 0/0", i) - assert.Equal(t, -expectedCmp, cmpNeg, "comparing 0/0, index %d", i) + assert.Equal(t, expectedCmp, cmp, "comparing index %d, 0", i) + assert.Equal(t, -expectedCmp, cmpNeg, "comparing 0, index %d", i) } } func TestDouble(t *testing.T) { - values := []interface{}{1, 2, 3, 4, 5, "2/3", "3/2", "-3/-2"} - valsDoubled := []interface{}{2, 4, 6, 8, 10, "-4/-3", 3, 3} + values := []interface{}{1, 2, 3, 4, 5, "2/3", "3/2", "6/4"} + valsDoubled := []interface{}{2, 4, 6, 8, 10, "8/6", 3, 3} for i := range values { var v, vDoubled, vDoubledExpected SmallRational @@ -83,7 +67,7 @@ func TestDouble(t *testing.T) { assert.NoError(t, err) vDoubled.Double(&v) assert.True(t, vDoubled.Equal(&vDoubledExpected), - "mismatch at %d: expected 2×%s = %s, saw %s", i, v.text, vDoubledExpected.text, vDoubled.text) + "mismatch at %d: expected 2×%s = %s, saw %s", i, v.String(), vDoubledExpected.String(), vDoubled.String()) } } From 655aa97de2aac39cb5859f62516199bf5bd3c747 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 21:36:34 -0500 Subject: [PATCH 07/37] refac: small_rational.SmallRational -> rational.Element Signed-off-by: Arya Tabaie --- internal/generator/backend/main.go | 16 +-- .../template/gkr/gkr.test.vectors.gen.go.tmpl | 6 +- .../gkr/sumcheck.test.vectors.gen.go.tmpl | 12 +-- .../{small_rational => rational}/evaluator.go | 16 +-- .../gkr/{small_rational => rational}/gkr.go | 38 +++---- .../{small_rational => rational}/sumcheck.go | 84 +++++++-------- .../sumcheck_test.go | 10 +- .../sumcheck_test_vector_gen.go | 22 ++-- .../test_vector_gen.go | 16 +-- .../test_vector_utils.go | 22 ++-- internal/gkr/test_vectors/generate.go | 2 +- .../polynomial/doc.go | 0 .../polynomial/multilin.go | 22 ++-- .../polynomial/polynomial.go | 24 ++--- .../polynomial/pool.go | 10 +- .../rational.go} | 93 +++++++--------- internal/rational/rational_test.go | 79 ++++++++++++++ .../{small_rational => rational}/vector.go | 4 +- .../small_rational/small_rational_test.go | 100 ------------------ 19 files changed, 268 insertions(+), 308 deletions(-) rename internal/gkr/{small_rational => rational}/evaluator.go (91%) rename internal/gkr/{small_rational => rational}/gkr.go (90%) rename internal/gkr/{small_rational => rational}/sumcheck.go (86%) rename internal/gkr/{small_rational => rational}/sumcheck_test.go (86%) rename internal/gkr/{small_rational => rational}/sumcheck_test_vector_gen.go (84%) rename internal/gkr/{small_rational => rational}/test_vector_gen.go (94%) rename internal/gkr/{small_rational => rational}/test_vector_utils.go (84%) rename internal/{small_rational => rational}/polynomial/doc.go (100%) rename internal/{small_rational => rational}/polynomial/multilin.go (87%) rename internal/{small_rational => rational}/polynomial/polynomial.go (89%) rename internal/{small_rational => rational}/polynomial/pool.go (53%) rename internal/{small_rational/small-rational.go => rational/rational.go} (60%) create mode 100644 internal/rational/rational_test.go rename internal/{small_rational => rational}/vector.go (62%) delete mode 100644 internal/small_rational/small_rational_test.go diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index ce109694ec..9e6f0fea18 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -232,12 +232,12 @@ func main() { wg.Add(1) // GKR test vectors go func() { - // generate gkr and sumcheck for small-rational + // generate gkr and sumcheck for rational cfg := gkrConfig{ - ElementType: "small_rational.SmallRational", - FieldPackagePath: "github.com/consensys/gnark/internal/small_rational", - FieldPackageName: "small_rational", - GkrPackageName: "small_rational", + ElementType: "rational.Element", + FieldPackagePath: "github.com/consensys/gnark/internal/rational", + FieldPackageName: "rational", + GkrPackageName: "rational", NoGkrTests: true, GenerateTestVectors: true, } @@ -299,7 +299,7 @@ type templateData struct { func generateGkrBackend(cfg gkrConfig) error { internalDir := filepath.Join("../../../internal/gkr", cfg.GkrPackageName) // The prover, verifier and their tests live alongside the evaluators in internalDir for - // small_rational (test-vector generation), and in the public gkr/ package for curves. + // rational (test-vector generation), and in the public gkr/ package for curves. proverDir := internalDir if !cfg.GenerateTestVectors { proverDir = filepath.Join("../../../gkr", cfg.GkrPackageName) @@ -361,11 +361,11 @@ type gkrConfig struct { EvaluatorQualifier string // ExtensionSuffix is the suffix gnark-crypto's extensions package appends to the names of an // extension's types and constants (e.g. VectorE6, BytesE6). Empty for the curves and - // small_rational. + // rational. ExtensionSuffix string // FieldDescription is the Go expression of the field's gkrcore.Field description, e.g. // "gkrcore.PrimeField(ecc.BN254.ScalarField())". Used only where gkr.test.go.tmpl is - // generated (NoGkrTests false), so small_rational leaves it empty. + // generated (NoGkrTests false), so rational leaves it empty. FieldDescription string } diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl index 5bec46a3d0..270da81e24 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl @@ -4,8 +4,8 @@ import ( "github.com/consensys/bavard" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/constraint" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/gkr/gkrcore" "hash" @@ -16,7 +16,7 @@ import ( // The properties of test gates are expected to be the same across all relevant fields. // We can therefore use the gate testing functions for any curve rather than reimplementing - // for small_rational. + // for rational. var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) func GenerateVectors() error { diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl index c77e9c918e..d94d7b9918 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -3,8 +3,8 @@ import ( "fmt" "hash" "github.com/consensys/gnark/internal/gkr/gkrcore" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" "math/bits" "os" @@ -31,7 +31,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } claim := singleMultilinClaim{poly} - t := gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + t := gkrcore.NewHashTranscript[rational.Element](hsh) proof := sumcheckProve(&claim, t) testCaseInfo.Proof = sumcheckToPrintableProof(proof) @@ -41,7 +41,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } else { return _err } - var claimedSum small_rational.SmallRational + var claimedSum rational.Element if _, err = claimedSum.SetInterface(testCaseInfo.ClaimedSum); err != nil { return err } @@ -49,7 +49,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + t = gkrcore.NewHashTranscript[rational.Element](hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err != nil { return fmt.Errorf("proof rejected: %v", err) } @@ -58,7 +58,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + t = gkrcore.NewHashTranscript[rational.Element](hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/gkr/small_rational/evaluator.go b/internal/gkr/rational/evaluator.go similarity index 91% rename from internal/gkr/small_rational/evaluator.go rename to internal/gkr/rational/evaluator.go index a95809f7df..b8ed879aab 100644 --- a/internal/gkr/small_rational/evaluator.go +++ b/internal/gkr/rational/evaluator.go @@ -10,8 +10,8 @@ import ( "sync" "github.com/consensys/gnark/internal/gkr/gkrcore" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" ) // GateEvaluator provides a high-level API for evaluating compiled gates efficiently. @@ -19,7 +19,7 @@ import ( // evaluate the same gate multiple times with different inputs. type GateEvaluator struct { gate gkrcore.GateBytecode - vars []small_rational.SmallRational + vars []rational.Element nbIn int // number of inputs expected pool *polynomial.Pool // pool vars was allocated from, if any } @@ -35,7 +35,7 @@ func NewGateEvaluator(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polyn e.pool = elementPool[0] e.vars = e.pool.Make(gate.EvaluatorSize(nbIn)) } else { - e.vars = make([]small_rational.SmallRational, gate.EvaluatorSize(nbIn)) + e.vars = make([]rational.Element, gate.EvaluatorSize(nbIn)) } e.vars = e.vars[:gate.NbConstants()] for i, constVal := range gate.Constants { @@ -54,7 +54,7 @@ func (e *GateEvaluator) Dump() { // PushInput adds an input to the evaluator's input buffer. // Inputs must be added in order, and the number of inputs must match the gate's NbInputs. -func (e *GateEvaluator) PushInput(input small_rational.SmallRational) { +func (e *GateEvaluator) PushInput(input rational.Element) { e.vars = append(e.vars, input) } @@ -62,7 +62,7 @@ func (e *GateEvaluator) PushInput(input small_rational.SmallRational) { // The stack is automatically reset after evaluation, // making the evaluator ready for the next evaluation. // NB! The result is short-lived. It will be overwritten the next time Evaluate is called. -func (e *GateEvaluator) Evaluate(top ...small_rational.SmallRational) *small_rational.SmallRational { +func (e *GateEvaluator) Evaluate(top ...rational.Element) *rational.Element { e.vars = append(e.vars, top...) if len(e.vars) != e.nbIn+e.gate.NbConstants() { @@ -97,12 +97,12 @@ func (e *GateEvaluator) Evaluate(top ...small_rational.SmallRational) *small_rat case gkrcore.OpNeg: dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: - var prod small_rational.SmallRational + var prod rational.Element prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) dst.Add(&e.vars[inst.Inputs[0]], &prod) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 - var sum small_rational.SmallRational + var sum rational.Element sum.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) sum.Add(&sum, &e.vars[inst.Inputs[2]]) dst.Mul(&sum, &sum) // x² diff --git a/internal/gkr/small_rational/gkr.go b/internal/gkr/rational/gkr.go similarity index 90% rename from internal/gkr/small_rational/gkr.go rename to internal/gkr/rational/gkr.go index 99b553d7a6..7fb60888fe 100644 --- a/internal/gkr/small_rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -14,8 +14,8 @@ import ( "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" ) // Type aliases for bytecode-based GKR types @@ -33,7 +33,7 @@ type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for eac // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[small_rational.SmallRational] +type EvaluationClaim = gkrcore.EvaluationClaim[rational.Element] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -56,7 +56,7 @@ type resources struct { // outgoingEvalPoints[i][k] is the k-th outgoing evaluation point (evaluation challenge) produced at schedule level i. // outgoingEvalPoints[len(schedule)][0] holds the initial challenge (firstChallenge / rho). // SumcheckLevels produce one point (k=0). SkipLevels pass on all their evaluation points. - outgoingEvalPoints [][][]small_rational.SmallRational + outgoingEvalPoints [][][]rational.Element nbVars int assignment WireAssignment memPool polynomial.Pool @@ -79,11 +79,11 @@ func identityGate() gkrcore.SerializableGate { // creates no pools; Prove sets those up separately afterward, since Verify needs neither. func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { return resources{ - outgoingEvalPoints: make([][][]small_rational.SmallRational, len(schedule)+1), + outgoingEvalPoints: make([][][]rational.Element, len(schedule)+1), nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[small_rational.SmallRational](hasher), + transcript: gkrcore.NewHashTranscript[rational.Element](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -97,7 +97,7 @@ func (r *resources) proveSkipLevel(levelI int) sumcheckProof { outPoints := gkrcore.CollectOutgoingEvalPoints(level, levelI, r.outgoingEvalPoints) uniqueInputs := r.circuit.UniqueGateInputs(level) - evals := make([]small_rational.SmallRational, len(uniqueInputs)*len(outPoints)) + evals := make([]rational.Element, len(uniqueInputs)*len(outPoints)) for uiI, inW := range uniqueInputs { for k, point := range outPoints { evals[level.FinalEvalProofIndex(uiI, k)] = r.assignment[inW].Evaluate(point, &r.memPool) @@ -183,7 +183,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss for wireI := range c { if len(assignment[wireI]) == 0 { const minBlockSize = 64 - assignment[wireI] = make([]small_rational.SmallRational, nbInstances) + assignment[wireI] = make([]rational.Element, nbInstances) r.workers.Submit(nbInstances, func(start, end int) { gateEval := NewGateEvaluator(c[wireI].Gate.Evaluate, len(c[wireI].Inputs), &r.memPool) for instanceI := start; instanceI < end; instanceI++ { @@ -200,19 +200,19 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss proof := make(Proof, len(schedule)+1) // Derive the initial challenge point - firstChallenge := make([]small_rational.SmallRational, r.nbVars) + firstChallenge := make([]rational.Element, r.nbVars) for j := range r.nbVars { firstChallenge[j] = r.transcript.Challenge() } - r.outgoingEvalPoints[len(schedule)] = [][]small_rational.SmallRational{firstChallenge} + r.outgoingEvalPoints[len(schedule)] = [][]rational.Element{firstChallenge} outputs := c.Outputs() - outputEvals := make([]small_rational.SmallRational, len(outputs)) + outputEvals := make([]rational.Element, len(outputs)) for i, w := range outputs { outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []small_rational.SmallRational + var boundOutputEvals []rational.Element for i, w := range outputs { if r.consolidated[w] { boundOutputEvals = append(boundOutputEvals, outputEvals[i]) @@ -292,12 +292,12 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in } // Derive the initial challenge point - firstChallenge := make([]small_rational.SmallRational, r.nbVars) + firstChallenge := make([]rational.Element, r.nbVars) for j := range r.nbVars { firstChallenge[j] = r.transcript.Challenge() } - r.outgoingEvalPoints[len(schedule)] = [][]small_rational.SmallRational{firstChallenge} - var boundOutputEvals []small_rational.SmallRational + r.outgoingEvalPoints[len(schedule)] = [][]rational.Element{firstChallenge} + var boundOutputEvals []rational.Element for i, w := range c.Outputs() { if r.consolidated[w] { boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) @@ -326,7 +326,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { for i := range circuit { if len(a[i]) != nbInstances { - a[i] = make([]small_rational.SmallRational, nbInstances) + a[i] = make([]rational.Element, nbInstances) } if !circuit[i].IsInput() { evaluators[i] = NewGateEvaluator(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) @@ -365,7 +365,7 @@ func (a WireAssignment) NumVars() int { panic("empty assignment") } -func iterateElems(elems []small_rational.SmallRational, counter *int, yield func(int, *small_rational.SmallRational) bool) bool { +func iterateElems(elems []rational.Element, counter *int, yield func(int, *rational.Element) bool) bool { for i := range elems { if !yield(*counter, &elems[i]) { return false @@ -375,8 +375,8 @@ func iterateElems(elems []small_rational.SmallRational, counter *int, yield func return true } -func (p Proof) Flatten() iter.Seq2[int, *small_rational.SmallRational] { - return func(yield func(int, *small_rational.SmallRational) bool) { +func (p Proof) Flatten() iter.Seq2[int, *rational.Element] { + return func(yield func(int, *rational.Element) bool) { var counter int for i := range p { for _, poly := range p[i].partialSumPolys { diff --git a/internal/gkr/small_rational/sumcheck.go b/internal/gkr/rational/sumcheck.go similarity index 86% rename from internal/gkr/small_rational/sumcheck.go rename to internal/gkr/rational/sumcheck.go index 39e8e00cb2..2951a66c7e 100644 --- a/internal/gkr/small_rational/sumcheck.go +++ b/internal/gkr/rational/sumcheck.go @@ -11,35 +11,35 @@ import ( "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" ) // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[small_rational.SmallRational, *small_rational.SmallRational] +type transcript = gkrcore.HashTranscript[rational.Element, *rational.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) type sumcheckClaims interface { - roundPolynomial() polynomial.Polynomial // compute gⱼ polynomial for current round - roundFold(r small_rational.SmallRational) // fold inputs and eq at challenge r - varsNum() int // number of variables - proveFinalEval(r []small_rational.SmallRational) []small_rational.SmallRational // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof + roundPolynomial() polynomial.Polynomial // compute gⱼ polynomial for current round + roundFold(r rational.Element) // fold inputs and eq at challenge r + varsNum() int // number of variables + proveFinalEval(r []rational.Element) []rational.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof } // sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. type sumcheckLazyClaims interface { varsNum() int // varsNum = n degree(i int) int // degree of the total claim in the i'th variable - verifyFinalEval(r []small_rational.SmallRational, purportedValue small_rational.SmallRational, proof []small_rational.SmallRational) error + verifyFinalEval(r []rational.Element, purportedValue rational.Element, proof []rational.Element) error } // sumcheckProof of a multi-statement. type sumcheckProof struct { partialSumPolys []polynomial.Polynomial - finalEvalProof []small_rational.SmallRational //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof + finalEvalProof []rational.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof } // sumcheckProve creates a non-interactive sumcheck proof. @@ -49,7 +49,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() - challenges := make([]small_rational.SmallRational, varsNum) + challenges := make([]rational.Element, varsNum) for j := range varsNum - 1 { challenges[j] = t.Challenge(proof.partialSumPolys[j]...) @@ -65,8 +65,8 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum small_rational.SmallRational, degree int, t *transcript) error { - r := make([]small_rational.SmallRational, claims.varsNum()) +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum rational.Element, degree int, t *transcript) error { + r := make([]rational.Element, claims.varsNum()) gJ := make(polynomial.Polynomial, degree+1) gJR := claimedSum @@ -91,7 +91,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum s // where the sum runs over all wᵢ and evaluation point xᵢ in the level. // Its purpose is to batch the checking of multiple wire evaluations at evaluation points. type zeroCheckLazyClaims struct { - foldingCoeff small_rational.SmallRational // the coefficient used to fold claims, conventionally 0 if there is only one claim + foldingCoeff rational.Element // the coefficient used to fold claims, conventionally 0 if there is only one claim resources *resources levelI int } @@ -113,8 +113,8 @@ func (e *zeroCheckLazyClaims) degree(int) int { // The prover claims evaluations of each wire's gate inputs at r via uniqueInputEvaluations; those // claims are verified by lower levels' sumchecks. The verifier checks consistency by evaluating // gateᵥ(inputEvals...) and confirming that the full sum matches purportedValue. -func (e *zeroCheckLazyClaims) verifyFinalEval(r []small_rational.SmallRational, purportedValue small_rational.SmallRational, uniqueInputEvaluations []small_rational.SmallRational) error { - e.resources.outgoingEvalPoints[e.levelI] = [][]small_rational.SmallRational{r} +func (e *zeroCheckLazyClaims) verifyFinalEval(r []rational.Element, purportedValue rational.Element, uniqueInputEvaluations []rational.Element) error { + e.resources.outgoingEvalPoints[e.levelI] = [][]rational.Element{r} level := e.resources.schedule[e.levelI] gateInputEvals := gkrcore.ReduplicateInputs(level, e.resources.circuit, uniqueInputEvaluations) @@ -132,7 +132,7 @@ func (e *zeroCheckLazyClaims) verifyFinalEval(r []small_rational.SmallRational, for _, src := range group.ClaimSources { eq := polynomial.EvalEq(e.resources.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex], r) - var term small_rational.SmallRational + var term rational.Element term.Mul(&eq, gateEval) claimedEvals = append(claimedEvals, term) } @@ -175,10 +175,10 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { sumSize := len(c.eqs[0]) / 2 - p := make([]small_rational.SmallRational, degree) + p := make([]rational.Element, degree) var mu sync.Mutex computeAll := func(start, end int) { - var step small_rational.SmallRational + var step rational.Element evaluators := make([]*GateEvaluator, nbWires) for w := range nbWires { @@ -190,14 +190,14 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { } }() - res := make([]small_rational.SmallRational, degree) + res := make([]rational.Element, degree) // evaluations of ml, laid out as: // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), // ... // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]small_rational.SmallRational, degree*len(ml)) + mlEvals := make([]rational.Element, degree*len(ml)) for h := start; h < end; h++ { evalAt1Index := sumSize + h @@ -242,7 +242,7 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { // roundFold folds all input and eq polynomials at the verifier challenge r. // After this call, j ← j+1 and rⱼ = r. -func (c *zeroCheckClaims) roundFold(r small_rational.SmallRational) { +func (c *zeroCheckClaims) roundFold(r rational.Element) { const minBlockSize = 512 n := len(c.eqs[0]) / 2 if n < minBlockSize { @@ -267,14 +267,14 @@ func (c *zeroCheckClaims) roundFold(r small_rational.SmallRational) { } // proveFinalEval provides the unique input wire values wᵢ(r₁, ..., rₙ). -func (c *zeroCheckClaims) proveFinalEval(r []small_rational.SmallRational) []small_rational.SmallRational { +func (c *zeroCheckClaims) proveFinalEval(r []rational.Element) []rational.Element { return c.zeroCheckBase.proveFinalEval(r, c.eqs) } // eqAcc sets m to an eq table at q and then adds it to e. // m <- m[0] · eq(q, -). // e <- e + m -func (r *resources) eqAcc(e, m polynomial.MultiLin, q []small_rational.SmallRational) { +func (r *resources) eqAcc(e, m polynomial.MultiLin, q []rational.Element) { n := len(q) // At the end of each iteration, m(h₁, ..., hₙ) = m[0] · eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) @@ -312,7 +312,7 @@ func (r *resources) eqAcc(e, m polynomial.MultiLin, q []small_rational.SmallRati // zeroCheckBase holds the fields and initialization logic common to both // zeroCheckClaims and singleSourceZeroCheckClaims. type zeroCheckBase struct { - foldingCoeff small_rational.SmallRational + foldingCoeff rational.Element levelI int resources *resources input []polynomial.MultiLin // UniqueGateInputs order @@ -327,9 +327,9 @@ func (c *zeroCheckBase) varsNum() int { // proveFinalEval records the outgoing eval point, folds the last variable, // collects the unique input evaluations, and releases all pooled memory. // extraPolys is the level-type-specific slice (eqs or suffixEq) to dump. -func (c *zeroCheckBase) proveFinalEval(r []small_rational.SmallRational, extraPolys []polynomial.MultiLin) []small_rational.SmallRational { - c.resources.outgoingEvalPoints[c.levelI] = [][]small_rational.SmallRational{r} - evaluations := make([]small_rational.SmallRational, len(c.input)) +func (c *zeroCheckBase) proveFinalEval(r []rational.Element, extraPolys []polynomial.MultiLin) []rational.Element { + c.resources.outgoingEvalPoints[c.levelI] = [][]rational.Element{r} + evaluations := make([]rational.Element, len(c.input)) for i := range c.input { c.input[i].Fold(r[len(r)-1]) evaluations[i] = c.input[i][0] @@ -385,7 +385,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { level := r.schedule[levelI] eqLength := 1 << r.nbVars claims.eqs = make([]polynomial.MultiLin, len(claims.gateEvaluatorPools)) - var alpha small_rational.SmallRational + var alpha rational.Element alpha.SetOne() levelWireI := 0 for _, group := range level.ClaimGroups() { @@ -406,7 +406,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { r.memPool.Dump(newEq) } - var stride small_rational.SmallRational + var stride rational.Element stride.Set(&claims.foldingCoeff) for range nbSources - 1 { stride.Mul(&stride, &claims.foldingCoeff) @@ -434,9 +434,9 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { // verifyLevelSetup derives the folding coefficient, collects all claimed wire // evaluations from the proof or the initial assignment, computes the batched // claimed sum, and builds the lazy-claims object shared by both verifiers. -func (r *resources) verifyLevelSetup(levelI int, proof Proof) (small_rational.SmallRational, *zeroCheckLazyClaims) { +func (r *resources) verifyLevelSetup(levelI int, proof Proof) (rational.Element, *zeroCheckLazyClaims) { level := r.schedule[levelI] - var foldingCoeff small_rational.SmallRational + var foldingCoeff rational.Element if level.NbClaims() >= 2 { foldingCoeff = r.transcript.Challenge() } @@ -495,10 +495,10 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] - p := make([]small_rational.SmallRational, degree) + p := make([]rational.Element, degree) var mu sync.Mutex computeAll := func(start, end int) { - var step small_rational.SmallRational + var step rational.Element evaluators := make([]*GateEvaluator, nbWires) for w := range nbWires { @@ -510,10 +510,10 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { } }() - res := make([]small_rational.SmallRational, degree) + res := make([]rational.Element, degree) // Input evaluations at m=1,2,...,degree - inputEvals := make([]small_rational.SmallRational, degree*nbUniqueInputs) + inputEvals := make([]rational.Element, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h @@ -532,7 +532,7 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { for _, inputI := range c.inputIndices[nbWires-1] { evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) } - var wireSum small_rational.SmallRational + var wireSum rational.Element wireSum.Set(evaluators[nbWires-1].Evaluate()) for w := nbWires - 2; w >= 0; w-- { wireSum.Mul(&wireSum, &c.foldingCoeff) @@ -566,7 +566,7 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { // roundFold folds only input multilinears at the verifier challenge r. // The suffix eq tables are precomputed and not folded. -func (c *singleSourceZeroCheckClaims) roundFold(r small_rational.SmallRational) { +func (c *singleSourceZeroCheckClaims) roundFold(r rational.Element) { const minBlockSize = 512 n := len(c.input[0]) / 2 if n < minBlockSize { @@ -588,7 +588,7 @@ func (c *singleSourceZeroCheckClaims) roundFold(r small_rational.SmallRational) } // proveFinalEval provides the unique input wire values at the final evaluation point. -func (c *singleSourceZeroCheckClaims) proveFinalEval(r []small_rational.SmallRational) []small_rational.SmallRational { +func (c *singleSourceZeroCheckClaims) proveFinalEval(r []rational.Element) []rational.Element { return c.zeroCheckBase.proveFinalEval(r, []polynomial.MultiLin{c.suffixEq}) } @@ -602,7 +602,7 @@ func (c *singleSourceZeroCheckClaims) proveFinalEval(r []small_rational.SmallRat // // The first two positions are unused padding so that the buffer halves cleanly: // in round j the eq segment is buf[len(buf)/2:] and buf is trimmed to buf[:len(buf)/2]. -func (r *resources) buildSuffixEq(q []small_rational.SmallRational) polynomial.MultiLin { +func (r *resources) buildSuffixEq(q []rational.Element) polynomial.MultiLin { n := len(q) buf := r.memPool.Make(1 << n) @@ -629,7 +629,7 @@ func (r *resources) buildSuffixEq(q []small_rational.SmallRational) polynomial.M srcStart := prevLen // start of suffixEq[j+1] dstStart := prevLen << 1 // start of suffixEq[j] - var oneMinusQj small_rational.SmallRational + var oneMinusQj rational.Element oneMinusQj.SetOne() oneMinusQj.Sub(&oneMinusQj, &q[j]) @@ -665,7 +665,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] degree := r.circuit.ZeroCheckDegree(level) - challenges := make([]small_rational.SmallRational, r.nbVars) + challenges := make([]rational.Element, r.nbVars) gPrime := make(polynomial.Polynomial, degree+1) // The verifier tracks claimedSum = g'_{j-1}(r_{j-1}), the "primed" value with @@ -686,7 +686,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er return errors.New("malformed proof") } - var oneMinusQj, qjTimesGPrime1 small_rational.SmallRational + var oneMinusQj, qjTimesGPrime1 rational.Element oneMinusQj.SetOne() oneMinusQj.Sub(&oneMinusQj, &q[j]) qjTimesGPrime1.Mul(&q[j], &partialPoly[0]) // partialPoly[0] = g'(1) diff --git a/internal/gkr/small_rational/sumcheck_test.go b/internal/gkr/rational/sumcheck_test.go similarity index 86% rename from internal/gkr/small_rational/sumcheck_test.go rename to internal/gkr/rational/sumcheck_test.go index e94757059a..60ac5ac522 100644 --- a/internal/gkr/small_rational/sumcheck_test.go +++ b/internal/gkr/rational/sumcheck_test.go @@ -10,8 +10,8 @@ import ( "hash" "github.com/consensys/gnark/internal/gkr/gkrcore" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" "github.com/stretchr/testify/assert" "strings" @@ -25,7 +25,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[small_rational.SmallRational](hashGenerator()) + t := gkrcore.NewHashTranscript[rational.Element](hashGenerator()) proof := sumcheckProve(&claim, t) @@ -43,14 +43,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[small_rational.SmallRational](hashGenerator()) + t = gkrcore.NewHashTranscript[rational.Element](hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[small_rational.SmallRational](hashGenerator()) + t = gkrcore.NewHashTranscript[rational.Element](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/gkr/small_rational/sumcheck_test_vector_gen.go b/internal/gkr/rational/sumcheck_test_vector_gen.go similarity index 84% rename from internal/gkr/small_rational/sumcheck_test_vector_gen.go rename to internal/gkr/rational/sumcheck_test_vector_gen.go index 785c2a08c7..c79c150a2f 100644 --- a/internal/gkr/small_rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/rational/sumcheck_test_vector_gen.go @@ -16,8 +16,8 @@ import ( "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" ) func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { @@ -39,7 +39,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } claim := singleMultilinClaim{poly} - t := gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + t := gkrcore.NewHashTranscript[rational.Element](hsh) proof := sumcheckProve(&claim, t) testCaseInfo.Proof = sumcheckToPrintableProof(proof) @@ -49,7 +49,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } else { return _err } - var claimedSum small_rational.SmallRational + var claimedSum rational.Element if _, err = claimedSum.SetInterface(testCaseInfo.ClaimedSum); err != nil { return err } @@ -57,7 +57,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + t = gkrcore.NewHashTranscript[rational.Element](hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err != nil { return fmt.Errorf("proof rejected: %v", err) } @@ -66,7 +66,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[small_rational.SmallRational](hsh) + t = gkrcore.NewHashTranscript[rational.Element](hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err == nil { return fmt.Errorf("bad proof accepted") } @@ -156,7 +156,7 @@ type singleMultilinClaim struct { g polynomial.MultiLin } -func (c *singleMultilinClaim) proveFinalEval(r []small_rational.SmallRational) []small_rational.SmallRational { +func (c *singleMultilinClaim) proveFinalEval(r []rational.Element) []rational.Element { return nil // verifier can compute the final eval itself } @@ -169,23 +169,23 @@ func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { for i := len(g)/2 + 1; i < len(g); i++ { sum.Add(&sum, &g[i]) } - return []small_rational.SmallRational{sum} + return []rational.Element{sum} } func (c *singleMultilinClaim) roundPolynomial() polynomial.Polynomial { return sumForX1One(c.g) } -func (c *singleMultilinClaim) roundFold(r small_rational.SmallRational) { +func (c *singleMultilinClaim) roundFold(r rational.Element) { c.g.Fold(r) } type singleMultilinLazyClaim struct { g polynomial.MultiLin - claimedSum small_rational.SmallRational + claimedSum rational.Element } -func (c singleMultilinLazyClaim) verifyFinalEval(r []small_rational.SmallRational, purportedValue small_rational.SmallRational, proof []small_rational.SmallRational) error { +func (c singleMultilinLazyClaim) verifyFinalEval(r []rational.Element, purportedValue rational.Element, proof []rational.Element) error { val := c.g.Evaluate(r, nil) if val.Equal(&purportedValue) { return nil diff --git a/internal/gkr/small_rational/test_vector_gen.go b/internal/gkr/rational/test_vector_gen.go similarity index 94% rename from internal/gkr/small_rational/test_vector_gen.go rename to internal/gkr/rational/test_vector_gen.go index 8829361a0a..4aa3eba6bd 100644 --- a/internal/gkr/small_rational/test_vector_gen.go +++ b/internal/gkr/rational/test_vector_gen.go @@ -18,13 +18,13 @@ import ( "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" ) // The properties of test gates are expected to be the same across all relevant fields. // We can therefore use the gate testing functions for any curve rather than reimplementing -// for small_rational. +// for rational. var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) func GenerateVectors() error { @@ -132,7 +132,7 @@ func toPrintableProof(proof Proof) (gkrtesting.PrintableProof, error) { return res, nil } -func elementToInterface(x *small_rational.SmallRational) interface{} { +func elementToInterface(x *rational.Element) interface{} { if i := x.BigInt(nil); i != nil { return i } @@ -148,7 +148,7 @@ func elementSliceToInterfaceSlice(x interface{}) []interface{} { res := make([]interface{}, X.Len()) for i := range res { - xI := X.Index(i).Interface().(small_rational.SmallRational) + xI := X.Index(i).Interface().(rational.Element) res[i] = elementToInterface(&xI) } return res @@ -172,11 +172,11 @@ func elementSliceSliceToInterfaceSliceSlice(x interface{}) [][]interface{} { func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { proof := make(Proof, len(printable)) for i := range printable { - finalEvalProof := []small_rational.SmallRational(nil) + finalEvalProof := []rational.Element(nil) if printable[i].FinalEvalProof != nil { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) - finalEvalProof = make([]small_rational.SmallRational, finalEvalSlice.Len()) + finalEvalProof = make([]rational.Element, finalEvalSlice.Len()) for k := range finalEvalProof { if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err @@ -271,7 +271,7 @@ func newTestCase(path string) (*TestCase, error) { outI++ } if assignmentRaw != nil { - var wireAssignment []small_rational.SmallRational + var wireAssignment []rational.Element if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { return nil, err } diff --git a/internal/gkr/small_rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go similarity index 84% rename from internal/gkr/small_rational/test_vector_utils.go rename to internal/gkr/rational/test_vector_utils.go index 6cca2581e1..8dbb7c680d 100644 --- a/internal/gkr/small_rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -11,12 +11,12 @@ import ( "math/big" "github.com/consensys/gnark/internal/gkr/gkrtesting" - "github.com/consensys/gnark/internal/small_rational" - "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/rational" + "github.com/consensys/gnark/internal/rational/polynomial" ) -func toElement(i int64) *small_rational.SmallRational { - var res small_rational.SmallRational +func toElement(i int64) *rational.Element { + var res rational.Element res.SetInt64(i) return &res } @@ -40,7 +40,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { } // messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = small_rational.Bytes +const messageCounterBlockSize = rational.Bytes // messageCounter is a stand-in hash whose state depends only on the number of field-element // blocks written to it, not on their values. @@ -59,7 +59,7 @@ func (m *messageCounter) Write(p []byte) (n int, err error) { func (m *messageCounter) Sum(b []byte) []byte { inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 resI := m.state + int64(inputBlockSize)*m.step - var res small_rational.SmallRational + var res rational.Element res.SetInt64(int64(resI)) return (&res).Marshal() } @@ -89,7 +89,7 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { // setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, // then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *small_rational.SmallRational, value interface{}) (*small_rational.SmallRational, error) { +func setElement(z *rational.Element, value interface{}) (*rational.Element, error) { var r big.Rat switch v := value.(type) { case string: @@ -106,7 +106,7 @@ func setElement(z *small_rational.SmallRational, value interface{}) (*small_rati return nil, fmt.Errorf("cannot parse value of type %T", value) } - var denom small_rational.SmallRational + var denom rational.Element z.SetBigInt(r.Num()) denom.SetBigInt(r.Denom()) denom.Inverse(&denom) @@ -114,8 +114,8 @@ func setElement(z *small_rational.SmallRational, value interface{}) (*small_rati return z, nil } -func sliceToElementSlice[T any](slice []T) ([]small_rational.SmallRational, error) { - elementSlice := make([]small_rational.SmallRational, len(slice)) +func sliceToElementSlice[T any](slice []T) ([]rational.Element, error) { + elementSlice := make([]rational.Element, len(slice)) for i, v := range slice { if _, err := setElement(&elementSlice[i], v); err != nil { return nil, err @@ -124,7 +124,7 @@ func sliceToElementSlice[T any](slice []T) ([]small_rational.SmallRational, erro return elementSlice, nil } -func sliceEquals(a []small_rational.SmallRational, b []small_rational.SmallRational) error { +func sliceEquals(a []rational.Element, b []rational.Element) error { if len(a) != len(b) { return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) } diff --git a/internal/gkr/test_vectors/generate.go b/internal/gkr/test_vectors/generate.go index 91e2b53c76..210ff8e0da 100644 --- a/internal/gkr/test_vectors/generate.go +++ b/internal/gkr/test_vectors/generate.go @@ -3,7 +3,7 @@ package main import ( "sync" - gkr "github.com/consensys/gnark/internal/gkr/small_rational" + gkr "github.com/consensys/gnark/internal/gkr/rational" _ "github.com/consensys/gnark/std/hash/mimc" // register MIMC hash ) diff --git a/internal/small_rational/polynomial/doc.go b/internal/rational/polynomial/doc.go similarity index 100% rename from internal/small_rational/polynomial/doc.go rename to internal/rational/polynomial/doc.go diff --git a/internal/small_rational/polynomial/multilin.go b/internal/rational/polynomial/multilin.go similarity index 87% rename from internal/small_rational/polynomial/multilin.go rename to internal/rational/polynomial/multilin.go index a857aa0a21..0f5d46a75e 100644 --- a/internal/small_rational/polynomial/multilin.go +++ b/internal/rational/polynomial/multilin.go @@ -7,22 +7,22 @@ import ( "math/bits" "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/rational" ) // MultiLin tracks the values of a (dense i.e. not sparse) multilinear polynomial // The variables are X₁ through Xₙ where n = log(len(.)) // .[∑ᵢ 2ⁱ⁻¹ bₙ₋ᵢ] = the polynomial evaluated at (b₁, b₂, ..., bₙ) // It is understood that any hypercube evaluation can be extrapolated to a multilinear polynomial -type MultiLin []small_rational.SmallRational +type MultiLin []rational.Element // Fold is partial evaluation function k[X₁, X₂, ..., Xₙ] → k[X₂, ..., Xₙ] by setting X₁=r -func (m *MultiLin) Fold(r small_rational.SmallRational) { +func (m *MultiLin) Fold(r rational.Element) { mid := len(*m) / 2 bottom, top := (*m)[:mid], (*m)[mid:] - var t small_rational.SmallRational // no need to update the top part + var t rational.Element // no need to update the top part // updating bookkeeping table // knowing that the polynomial f ∈ (k[X₂, ..., Xₙ])[X₁] is linear, we would get f(r) = f(0) + r(f(1) - f(0)) @@ -38,14 +38,14 @@ func (m *MultiLin) Fold(r small_rational.SmallRational) { *m = (*m)[:mid] } -func (m *MultiLin) FoldParallel(r small_rational.SmallRational) utils.Task { +func (m *MultiLin) FoldParallel(r rational.Element) utils.Task { mid := len(*m) / 2 bottom, top := (*m)[:mid], (*m)[mid:] *m = bottom return func(start, end int) { - var t small_rational.SmallRational // no need to update the top part + var t rational.Element // no need to update the top part for i := start; i < end; i++ { // table[i] ← table[i] + r (table[i + mid] - table[i]) t.Sub(&top[i], &bottom[i]) @@ -55,7 +55,7 @@ func (m *MultiLin) FoldParallel(r small_rational.SmallRational) utils.Task { } } -func (m MultiLin) Sum() small_rational.SmallRational { +func (m MultiLin) Sum() rational.Element { s := m[0] for i := 1; i < len(m); i++ { s.Add(&s, &m[i]) @@ -79,7 +79,7 @@ func _dump(m MultiLin, p *Pool) { // Evaluate extrapolate the value of the multilinear polynomial corresponding to m // on the given coordinates -func (m MultiLin) Evaluate(coordinates []small_rational.SmallRational, p *Pool) small_rational.SmallRational { +func (m MultiLin) Evaluate(coordinates []rational.Element, p *Pool) rational.Element { // Folding is a mutating operation bkCopy := _clone(m, p) @@ -133,8 +133,8 @@ func (m *MultiLin) Add(left, right MultiLin) { // // In other words the polynomial evaluated here is the multilinear extrapolation of // one that evaluates to q' == h' for vectors q', h' of binary values -func EvalEq(q, h []small_rational.SmallRational) small_rational.SmallRational { - var res, nxt, one, sum small_rational.SmallRational +func EvalEq(q, h []rational.Element) rational.Element { + var res, nxt, one, sum rational.Element one.SetOne() for i := 0; i < len(q); i++ { nxt.Mul(&q[i], &h[i]) // nxt <- qᵢ * hᵢ @@ -153,7 +153,7 @@ func EvalEq(q, h []small_rational.SmallRational) small_rational.SmallRational { } // Eq sets m to the representation of the polynomial Eq(q₁, ..., qₙ, *, ..., *) × m[0] -func (m *MultiLin) Eq(q []small_rational.SmallRational) { +func (m *MultiLin) Eq(q []rational.Element) { n := len(q) if len(*m) != 1<= 0; i-- { @@ -53,28 +53,28 @@ func (p *Polynomial) Set(p1 Polynomial) { } // AddConstantInPlace adds a constant to the polynomial, modifying p -func (p *Polynomial) AddConstantInPlace(c *small_rational.SmallRational) { +func (p *Polynomial) AddConstantInPlace(c *rational.Element) { for i := 0; i < len(*p); i++ { (*p)[i].Add(&(*p)[i], c) } } // SubConstantInPlace subs a constant to the polynomial, modifying p -func (p *Polynomial) SubConstantInPlace(c *small_rational.SmallRational) { +func (p *Polynomial) SubConstantInPlace(c *rational.Element) { for i := 0; i < len(*p); i++ { (*p)[i].Sub(&(*p)[i], c) } } // ScaleInPlace multiplies p by v, modifying p -func (p *Polynomial) ScaleInPlace(c *small_rational.SmallRational) { +func (p *Polynomial) ScaleInPlace(c *rational.Element) { for i := 0; i < len(*p); i++ { (*p)[i].Mul(&(*p)[i], c) } } // Scale multiplies p0 by v, storing the result in p -func (p *Polynomial) Scale(c *small_rational.SmallRational, p0 Polynomial) { +func (p *Polynomial) Scale(c *rational.Element, p0 Polynomial) { if len(*p) != len(p0) { *p = make(Polynomial, len(p0)) } @@ -211,7 +211,7 @@ func (p Polynomial) Text(base int) string { // InterpolateOnRange maps vector v to polynomial f // such that f(i) = v[i] for 0 ≤ i < len(v). // len(f) = len(v) and deg(f) ≤ len(v) - 1 -func InterpolateOnRange(v []small_rational.SmallRational) Polynomial { +func InterpolateOnRange(v []rational.Element) Polynomial { nEvals := uint8(len(v)) if int(nEvals) != len(v) { panic("interpolation method too inefficient for nEvals > 255") @@ -244,7 +244,7 @@ func getLagrangeBasis(domainSize uint8) []Polynomial { if domainSize >= 2 { res = computeLagrangeBasis(domainSize) } else if domainSize == 1 { - var oneCoeff small_rational.SmallRational + var oneCoeff rational.Element oneCoeff.SetOne() res = []Polynomial{{oneCoeff}} } @@ -258,7 +258,7 @@ func getLagrangeBasis(domainSize uint8) []Polynomial { // Note that pₗ(l) = 1 and pₗ(n) = 0 if 0 ≤ l < domainSize, n ≠ l func computeLagrangeBasis(domainSize uint8) []Polynomial { - constTerms := make([]small_rational.SmallRational, domainSize) + constTerms := make([]rational.Element, domainSize) for i := uint8(0); i < domainSize; i++ { constTerms[i].SetInt64(-int64(i)) } @@ -300,7 +300,7 @@ func computeLagrangeBasis(domainSize uint8) []Polynomial { constTerms[l].Neg(&constTerms[l]) constTerms[l] = res[l].Eval(&constTerms[l]) } - constTerms = small_rational.BatchInvert(constTerms) + constTerms = rational.BatchInvert(constTerms) for l := uint8(0); l < domainSize; l++ { res[l].ScaleInPlace(&constTerms[l]) } diff --git a/internal/small_rational/polynomial/pool.go b/internal/rational/polynomial/pool.go similarity index 53% rename from internal/small_rational/polynomial/pool.go rename to internal/rational/polynomial/pool.go index bc855ef5d4..d55782abb3 100644 --- a/internal/small_rational/polynomial/pool.go +++ b/internal/rational/polynomial/pool.go @@ -4,7 +4,7 @@ package polynomial import ( - "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/rational" ) // Do as little as possible to instantiate the interface @@ -15,14 +15,14 @@ func NewPool(...int) (pool Pool) { return Pool{} } -func (p *Pool) Make(n int) []small_rational.SmallRational { - return make([]small_rational.SmallRational, n) +func (p *Pool) Make(n int) []rational.Element { + return make([]rational.Element, n) } -func (p *Pool) Dump(...[]small_rational.SmallRational) { +func (p *Pool) Dump(...[]rational.Element) { } -func (p *Pool) Clone(slice []small_rational.SmallRational) []small_rational.SmallRational { +func (p *Pool) Clone(slice []rational.Element) []rational.Element { res := p.Make(len(slice)) copy(res, slice) return res diff --git a/internal/small_rational/small-rational.go b/internal/rational/rational.go similarity index 60% rename from internal/small_rational/small-rational.go rename to internal/rational/rational.go index b77a8670a2..4cd4799d90 100644 --- a/internal/small_rational/small-rational.go +++ b/internal/rational/rational.go @@ -1,4 +1,4 @@ -package small_rational +package rational import ( "crypto/rand" @@ -8,38 +8,34 @@ import ( const Bytes = 64 -// SmallRational implements the rational field, used to generate field agnostic test vectors. -// It is not optimized for performance, so it is best used sparingly. -// -// SmallRational wraps a big.Rat. Every method writes its result into a fresh big.Rat and assigns -// it to z.r, never mutating a receiver's existing storage, which makes value copies safe. -type SmallRational struct{ r big.Rat } +// Element wraps a big.Rat into a gnark-crypto type field interface. +type Element struct{ r big.Rat } -func (z *SmallRational) Square(x *SmallRational) *SmallRational { +func (z *Element) Square(x *Element) *Element { var res big.Rat res.Mul(&x.r, &x.r) z.r = res return z } -func (z *SmallRational) String() string { +func (z *Element) String() string { return z.Text(10) } -func (z *SmallRational) Add(x, y *SmallRational) *SmallRational { +func (z *Element) Add(x, y *Element) *Element { var res big.Rat res.Add(&x.r, &y.r) z.r = res return z } -func (z *SmallRational) IsZero() bool { +func (z *Element) IsZero() bool { return z.r.Sign() == 0 } // Inverse sets z to 1/x, except that the inverse of 0 is 0, as for the curves' elements // (big.Rat.Inv panics on 0). -func (z *SmallRational) Inverse(x *SmallRational) *SmallRational { +func (z *Element) Inverse(x *Element) *Element { if x.r.Sign() == 0 { z.r = big.Rat{} return z @@ -50,86 +46,77 @@ func (z *SmallRational) Inverse(x *SmallRational) *SmallRational { return z } -func (z *SmallRational) Neg(x *SmallRational) *SmallRational { +func (z *Element) Neg(x *Element) *Element { var res big.Rat res.Neg(&x.r) z.r = res return z } -func (z *SmallRational) Double(x *SmallRational) *SmallRational { +func (z *Element) Double(x *Element) *Element { var res big.Rat res.Add(&x.r, &x.r) z.r = res return z } -func (z *SmallRational) Sign() int { +func (z *Element) Sign() int { return z.r.Sign() } -func (z *SmallRational) MarshalJSON() ([]byte, error) { - return []byte(z.String()), nil -} - -func (z *SmallRational) UnmarshalJson(data []byte) error { - _, err := z.SetInterface(string(data)) - return err -} - -func (z *SmallRational) Equal(x *SmallRational) bool { +func (z *Element) Equal(x *Element) bool { return z.Cmp(x) == 0 } -func (z *SmallRational) Sub(x, y *SmallRational) *SmallRational { +func (z *Element) Sub(x, y *Element) *Element { var res big.Rat res.Sub(&x.r, &y.r) z.r = res return z } -func (z *SmallRational) Cmp(x *SmallRational) int { +func (z *Element) Cmp(x *Element) int { return z.r.Cmp(&x.r) } -func BatchInvert(a []SmallRational) []SmallRational { - res := make([]SmallRational, len(a)) +func BatchInvert(a []Element) []Element { + res := make([]Element, len(a)) for i := range a { res[i].Inverse(&a[i]) } return res } -func (z *SmallRational) Mul(x, y *SmallRational) *SmallRational { +func (z *Element) Mul(x, y *Element) *Element { var res big.Rat res.Mul(&x.r, &y.r) z.r = res return z } -func (z *SmallRational) Div(x, y *SmallRational) *SmallRational { +func (z *Element) Div(x, y *Element) *Element { var res big.Rat res.Quo(&x.r, &y.r) z.r = res return z } -func (z *SmallRational) Halve() *SmallRational { +func (z *Element) Halve() *Element { var res big.Rat res.Quo(&z.r, big.NewRat(2, 1)) z.r = res return z } -func (z *SmallRational) SetOne() *SmallRational { +func (z *Element) SetOne() *Element { return z.SetInt64(1) } -func (z *SmallRational) SetZero() *SmallRational { +func (z *Element) SetZero() *Element { return z.SetInt64(0) } -func (z *SmallRational) SetInt64(i int64) *SmallRational { +func (z *Element) SetInt64(i int64) *Element { var res big.Rat res.SetInt64(i) z.r = res @@ -137,7 +124,7 @@ func (z *SmallRational) SetInt64(i int64) *SmallRational { } // SetBigInt sets z to the integer value i (denominator = 1). -func (z *SmallRational) SetBigInt(i *big.Int) *SmallRational { +func (z *Element) SetBigInt(i *big.Int) *Element { var res big.Rat res.SetInt(i) z.r = res @@ -146,7 +133,7 @@ func (z *SmallRational) SetBigInt(i *big.Int) *SmallRational { // SetRandom sets z to a uniform random numerator in [-8, 7] over a uniform random denominator in // [0, 15], with denominator 0 giving 0. -func (z *SmallRational) SetRandom() (*SmallRational, error) { +func (z *Element) SetRandom() (*Element, error) { bytes := make([]byte, 1) n, err := rand.Read(bytes) @@ -169,14 +156,14 @@ func (z *SmallRational) SetRandom() (*SmallRational, error) { return z, nil } -func (z *SmallRational) MustSetRandom() *SmallRational { +func (z *Element) MustSetRandom() *Element { if _, err := z.SetRandom(); err != nil { panic(err) } return z } -func (z *SmallRational) SetUint64(i uint64) { +func (z *Element) SetUint64(i uint64) { var bi big.Int bi.SetUint64(i) var res big.Rat @@ -184,13 +171,13 @@ func (z *SmallRational) SetUint64(i uint64) { z.r = res } -func (z *SmallRational) IsOne() bool { +func (z *Element) IsOne() bool { return z.r.Cmp(big.NewRat(1, 1)) == 0 } // Text writes the numerator alone, in base, if z is an integer, else the numerator and the // denominator, both in base, separated by "/". It does not modify z. -func (z *SmallRational) Text(base int) string { +func (z *Element) Text(base int) string { if z.r.IsInt() { return z.r.Num().Text(base) } @@ -198,17 +185,17 @@ func (z *SmallRational) Text(base int) string { } // Set sets z to x. -func (z *SmallRational) Set(x *SmallRational) *SmallRational { +func (z *Element) Set(x *Element) *Element { *z = *x return z } -func (z *SmallRational) SetInterface(x interface{}) (*SmallRational, error) { +func (z *Element) SetInterface(x interface{}) (*Element, error) { switch v := x.(type) { - case *SmallRational: + case *Element: *z = *v - case SmallRational: + case Element: *z = v case int64: z.SetInt64(v) @@ -241,14 +228,14 @@ func bigIntToBytesSigned(dst []byte, src big.Int) { } } -func (z *SmallRational) Bytes() [Bytes]byte { +func (z *Element) Bytes() [Bytes]byte { var res [Bytes]byte bigIntToBytesSigned(res[:Bytes/2], *z.r.Num()) bigIntToBytesSigned(res[Bytes/2:], *z.r.Denom()) return res } -func (z *SmallRational) Marshal() []byte { +func (z *Element) Marshal() []byte { res := z.Bytes() return res[:] } @@ -265,7 +252,7 @@ func bytesToBigIntSigned(src []byte) big.Int { // BigInt sets dst to the value of z if it is an integer, and returns dst. // if z is not an integer, nil is returned. // if the given dst is nil, a new big.Int is allocated rather than returning a pointer into z. -func (z *SmallRational) BigInt(dst *big.Int) *big.Int { +func (z *Element) BigInt(dst *big.Int) *big.Int { if !z.r.IsInt() { return nil } @@ -276,7 +263,7 @@ func (z *SmallRational) BigInt(dst *big.Int) *big.Int { return dst } -func (z *SmallRational) SetBytes(b []byte) *SmallRational { +func (z *Element) SetBytes(b []byte) *Element { var num, den big.Int if len(b) > Bytes/2 { num = bytesToBigIntSigned(b[:Bytes/2]) @@ -293,13 +280,7 @@ func (z *SmallRational) SetBytes(b []byte) *SmallRational { return z } -func (z *SmallRational) SetBytesCanonical(bytes []byte) error { +func (z *Element) SetBytesCanonical(bytes []byte) error { z.SetBytes(bytes) return nil } - -func One() SmallRational { - var res SmallRational - res.SetInt64(1) - return res -} diff --git a/internal/rational/rational_test.go b/internal/rational/rational_test.go new file mode 100644 index 0000000000..5cd4fedb24 --- /dev/null +++ b/internal/rational/rational_test.go @@ -0,0 +1,79 @@ +package rational + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetInterface(t *testing.T) { + cases := []struct { + name string + value interface{} + want string // Text(10) of the expected value + }{ + {"int64", int64(-3), "-3"}, + {"int", 5, "5"}, + {"integer float64", 4.0, "4"}, + {"decimal string", "7", "7"}, + {"fraction string, unreduced", "6/4", "3/2"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var z Element + _, err := z.SetInterface(c.value) + assert.NoError(t, err) + assert.Equal(t, c.want, z.Text(10)) + }) + } + + t.Run("non-integer float64 rejected", func(t *testing.T) { + var z Element + _, err := z.SetInterface(4.5) + assert.Error(t, err) + }) + + t.Run("unparseable string rejected", func(t *testing.T) { + var z Element + _, err := z.SetInterface("not a number") + assert.Error(t, err) + }) +} + +// Inverse diverges from big.Rat.Inv, which panics on 0. +func TestInverseZero(t *testing.T) { + var zero, inv Element + inv.Inverse(&zero) + assert.True(t, inv.IsZero()) +} + +func TestBytesRoundTrip(t *testing.T) { + cases := []interface{}{0, 1, -1, "3/5", "-7/11"} + for _, v := range cases { + var x, y Element + _, err := x.SetInterface(v) + assert.NoError(t, err) + y.SetBytes(x.Marshal()) + assert.True(t, x.Equal(&y), "round trip mismatch for %v: %s != %s", v, x.String(), y.String()) + } +} + +// SetBytes diverges from big.Rat.SetFrac, which panics on a zero denominator. +func TestSetBytesZeroDenominator(t *testing.T) { + var z Element + z.SetBytes(make([]byte, Bytes)) + assert.True(t, z.IsZero()) +} + +// TestOperandConstancy checks that mutating a copy through an aliased receiver (res := p; +// res.Add(&res, &p0)) never writes into p's storage. +func TestOperandConstancy(t *testing.T) { + var p0, p, pPure Element + p0.SetInt64(1) + p.SetInt64(-3) + pPure.SetInt64(-3) + + res := p + res.Add(&res, &p0) + assert.True(t, p.Equal(&pPure)) +} diff --git a/internal/small_rational/vector.go b/internal/rational/vector.go similarity index 62% rename from internal/small_rational/vector.go rename to internal/rational/vector.go index 07fcc3afff..aa557aad5d 100644 --- a/internal/small_rational/vector.go +++ b/internal/rational/vector.go @@ -1,6 +1,6 @@ -package small_rational +package rational -type Vector []SmallRational +type Vector []Element func (v Vector) MustSetRandom() { for i := range v { diff --git a/internal/small_rational/small_rational_test.go b/internal/small_rational/small_rational_test.go deleted file mode 100644 index a636976857..0000000000 --- a/internal/small_rational/small_rational_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package small_rational - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCmp(t *testing.T) { - // 9 distinct values, (i-4)/2 for i = 0..8: -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, alternating - // between two SetInterface input forms: a plain integer and an explicit "n/2" fraction string. - cases := make([]SmallRational, 9) - for i := int64(0); i < 9; i++ { - var err error - if i%2 == 0 { - _, err = cases[i].SetInterface((i - 4) / 2) - } else { - _, err = cases[i].SetInterface(fmt.Sprintf("%d/2", i-4)) - } - assert.NoError(t, err) - } - - for i := range cases { - for j := range cases { - var expectedCmp int - cmp := cases[i].Cmp(&cases[j]) - if i < j { - expectedCmp = -1 - } else if i == j { - expectedCmp = 0 - } else { - expectedCmp = 1 - } - assert.Equal(t, expectedCmp, cmp, "comparing index %d, index %d", i, j) - } - } - - const zeroIndex = 4 // (4-4)/2 = 0 - var zero SmallRational - for i := range cases { - var expectedCmp int - cmp := cases[i].Cmp(&zero) - cmpNeg := zero.Cmp(&cases[i]) - if i < zeroIndex { - expectedCmp = -1 - } else if i == zeroIndex { - expectedCmp = 0 - } else { - expectedCmp = 1 - } - - assert.Equal(t, expectedCmp, cmp, "comparing index %d, 0", i) - assert.Equal(t, -expectedCmp, cmpNeg, "comparing 0, index %d", i) - } -} - -func TestDouble(t *testing.T) { - values := []interface{}{1, 2, 3, 4, 5, "2/3", "3/2", "6/4"} - valsDoubled := []interface{}{2, 4, 6, 8, 10, "8/6", 3, 3} - - for i := range values { - var v, vDoubled, vDoubledExpected SmallRational - _, err := v.SetInterface(values[i]) - assert.NoError(t, err) - _, err = vDoubledExpected.SetInterface(valsDoubled[i]) - assert.NoError(t, err) - vDoubled.Double(&v) - assert.True(t, vDoubled.Equal(&vDoubledExpected), - "mismatch at %d: expected 2×%s = %s, saw %s", i, v.String(), vDoubledExpected.String(), vDoubled.String()) - - } -} - -func TestOperandConstancy(t *testing.T) { - var p0, p, pPure SmallRational - p0.SetInt64(1) - p.SetInt64(-3) - pPure.SetInt64(-3) - - res := p - res.Add(&res, &p0) - assert.True(t, p.Equal(&pPure)) -} - -func TestSquare(t *testing.T) { - var two, four, x SmallRational - two.SetInt64(2) - four.SetInt64(4) - - x.Square(&two) - - assert.True(t, x.Equal(&four), "expected 4, saw %s", x.Text(10)) -} - -func TestSetBytes(t *testing.T) { - var c SmallRational - c.SetBytes([]byte("firstChallenge.0")) - -} From f4cdc80b49ed9c6f6a91b6fd1824540019bc93da Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 1 Oct 2026 21:45:35 -0500 Subject: [PATCH 08/37] style: old gofmt Signed-off-by: Arya Tabaie --- std/commitments/kzg/verifier.go | 10 +++++----- std/permutation/gkr-mimc/gkr-mimc_test.go | 8 ++++---- .../poseidon2/gkr-poseidon2/gkr-poseidon2_test.go | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/std/commitments/kzg/verifier.go b/std/commitments/kzg/verifier.go index b3273ae952..809bc64f22 100644 --- a/std/commitments/kzg/verifier.go +++ b/std/commitments/kzg/verifier.go @@ -609,11 +609,11 @@ func (v *Verifier[FR, G1El, G2El, GTEl]) FoldProof(digests []Commitment[G1El], b foldedEvaluations = v.scalarApi.Add(foldedEvaluations, tmp) } return OpeningProof[FR, G1El]{ - Quotient: batchOpeningProof.Quotient, - ClaimedValue: *foldedEvaluations, - }, Commitment[G1El]{ - G1El: *foldedDigests, - }, nil + Quotient: batchOpeningProof.Quotient, + ClaimedValue: *foldedEvaluations, + }, Commitment[G1El]{ + G1El: *foldedDigests, + }, nil } diff --git a/std/permutation/gkr-mimc/gkr-mimc_test.go b/std/permutation/gkr-mimc/gkr-mimc_test.go index d182ce1c78..2ce31acfdd 100644 --- a/std/permutation/gkr-mimc/gkr-mimc_test.go +++ b/std/permutation/gkr-mimc/gkr-mimc_test.go @@ -69,8 +69,8 @@ func merkleTreeCircuits(n int) (circuit, assignment merkleTreeCircuit) { } return merkleTreeCircuit{ - Leaves: make([]frontend.Variable, len(leaves)), - }, merkleTreeCircuit{ - Leaves: leaves, - } + Leaves: make([]frontend.Variable, len(leaves)), + }, merkleTreeCircuit{ + Leaves: leaves, + } } diff --git a/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go b/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go index b781fb04eb..10520bb7cb 100644 --- a/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go +++ b/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2_test.go @@ -21,10 +21,10 @@ func gkrCompressionsCircuits(n int) (circuit, assignment testGkrCompressionCircu } return testGkrCompressionCircuit{ - Ins: make([][2]frontend.Variable, len(ins)), - }, testGkrCompressionCircuit{ - Ins: ins, - } + Ins: make([][2]frontend.Variable, len(ins)), + }, testGkrCompressionCircuit{ + Ins: ins, + } } func TestGkrCompression(t *testing.T) { From bbda2f70ae388e2b2be2feabc955a16f8145367a Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Sun, 4 Oct 2026 21:33:50 -0500 Subject: [PATCH 09/37] fix: bellman test Signed-off-by: Arya Tabaie --- backend/groth16/bellman_test.go | 8 ++++++-- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/groth16/bellman_test.go b/backend/groth16/bellman_test.go index b47a6244c6..0c96306e34 100644 --- a/backend/groth16/bellman_test.go +++ b/backend/groth16/bellman_test.go @@ -97,8 +97,12 @@ func TestVerifyBellmanProof(t *testing.T) { proofBytes, err := base64.StdEncoding.DecodeString(test.proof) require.NoError(t, err) - // pad with 0 bytes to account for commitment stuff - proofBytes = append(proofBytes, make([]byte, bls12381.SizeOfG1AffineUncompressed+4)...) + // Pad for an empty Commitments slice (4-byte zero length), then CommitmentPok as the point + // at infinity in uncompressed form: top 3 bits 0b010 (BLS12-381's uncompressed-infinity + // flag), the rest zero. + padding := make([]byte, bls12381.SizeOfG1AffineUncompressed+4) + padding[4] = 0b010 << 5 + proofBytes = append(proofBytes, padding...) proof := NewProof(ecc.BLS12_381) _, err = proof.ReadFrom(bytes.NewReader(proofBytes)) diff --git a/go.mod b/go.mod index 07a1930905..146b3e054f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.21.1-0.20261001213005-bfa51d3c359f + github.com/consensys/gnark-crypto v0.21.1-0.20261002031104-9e7ec3b0d8ca github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 diff --git a/go.sum b/go.sum index e908f4f0d2..bfb7a16b56 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.21.1-0.20261001213005-bfa51d3c359f h1:xlMnEpSPcCOj1GPYgVlBvNvBIBTPmMOEx347Zcqtco0= -github.com/consensys/gnark-crypto v0.21.1-0.20261001213005-bfa51d3c359f/go.mod h1:hdTjDNjdkYJ1oVuc8emh9XEhfM1SbyZhJigFqItiOLk= +github.com/consensys/gnark-crypto v0.21.1-0.20261002031104-9e7ec3b0d8ca h1:7ISGNLVTn9KfL1yuqsaxNpoAo7JtBBobWUJgmt5QNSI= +github.com/consensys/gnark-crypto v0.21.1-0.20261002031104-9e7ec3b0d8ca/go.mod h1:hdTjDNjdkYJ1oVuc8emh9XEhfM1SbyZhJigFqItiOLk= github.com/consensys/gnark-solidity-checker v0.2.0 h1:i5iUEzNOkUvpaKm23UEe0wajBMwj7NzyT4EI0T2N8WQ= github.com/consensys/gnark-solidity-checker v0.2.0/go.mod h1:cEvl4g5AH+L4qGQLDOVZjqvn5IKZIAZdhSi8zAM6BiY= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= From 2bd671abe854286d251d9c0ba8fa8d3876fbc425 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 14:25:22 -0500 Subject: [PATCH 10/37] feat: mixed gate evaluator Signed-off-by: Arya Tabaie --- internal/generator/backend/main.go | 39 ++++--- .../backend/template/gkr/evaluator.go.tmpl | 107 ++++++++++++++---- .../backend/template/gkr/gkr.go.tmpl | 9 +- .../backend/template/gkr/sumcheck.go.tmpl | 13 ++- 4 files changed, 120 insertions(+), 48 deletions(-) diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index 9e6f0fea18..d8b62ca5d5 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -147,13 +147,15 @@ func main() { curvePackageName := strings.ToLower(d.Curve) cfg := gkrConfig{ - ElementType: "fr.Element", - FieldPackageName: "fr", - FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", - FieldID: d.CurveID, - FieldDescription: "gkrcore.PrimeField(ecc." + d.CurveID + ".ScalarField())", - GkrPackageName: curvePackageName, - EvaluatorQualifier: "evaluator.", + ElementType: "fr.Element", + BaseElementType: "fr.Element", + BaseFieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", + FieldPackageName: "fr", + FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", + FieldID: d.CurveID, + FieldDescription: "gkrcore.PrimeField(ecc." + d.CurveID + ".ScalarField())", + GkrPackageName: curvePackageName, + EvaluatorQualifier: "evaluator.", } assertNoError(generateGkrBackend(cfg)) @@ -234,12 +236,14 @@ func main() { go func() { // generate gkr and sumcheck for rational cfg := gkrConfig{ - ElementType: "rational.Element", - FieldPackagePath: "github.com/consensys/gnark/internal/rational", - FieldPackageName: "rational", - GkrPackageName: "rational", - NoGkrTests: true, - GenerateTestVectors: true, + ElementType: "rational.Element", + BaseElementType: "rational.Element", + BaseFieldPackagePath: "github.com/consensys/gnark/internal/rational", + FieldPackagePath: "github.com/consensys/gnark/internal/rational", + FieldPackageName: "rational", + GkrPackageName: "rational", + NoGkrTests: true, + GenerateTestVectors: true, } assertNoError(generateGkrBackend(cfg)) @@ -367,8 +371,17 @@ type gkrConfig struct { // "gkrcore.PrimeField(ecc.BN254.ScalarField())". Used only where gkr.test.go.tmpl is // generated (NoGkrTests false), so rational leaves it empty. FieldDescription string + // BaseElementType is the type of the prime subfield's elements, in which gate constants live. + // It is ElementType itself unless the field is an extension. + BaseElementType string + // BaseFieldPackagePath is the import path of BaseElementType's package. + BaseFieldPackagePath string } +// Mixed reports whether the field is an extension of its prime subfield, so that gate constants +// are not elements of the field itself. +func (c gkrConfig) Mixed() bool { return c.BaseElementType != c.ElementType } + func assertNoError(err error) { if err != nil { panic(err) diff --git a/internal/generator/backend/template/gkr/evaluator.go.tmpl b/internal/generator/backend/template/gkr/evaluator.go.tmpl index 7b07b35792..f05f02c042 100644 --- a/internal/generator/backend/template/gkr/evaluator.go.tmpl +++ b/internal/generator/backend/template/gkr/evaluator.go.tmpl @@ -2,25 +2,47 @@ import ( "fmt" "sync" - "{{.FieldPackagePath}}" - "{{.FieldPackagePath}}/polynomial" + "{{ .BaseFieldPackagePath }}" + "{{ .BaseFieldPackagePath }}/polynomial" + {{- if .Mixed }} + "{{ .FieldPackagePath }}" + extPolynomial "{{ .FieldPackagePath }}/polynomial" + {{- end }} "github.com/consensys/gnark/internal/gkr/gkrcore" ) -// GateEvaluator provides a high-level API for evaluating compiled gates efficiently. +{{ template "gateEvaluator" dict "Config" . "Mixed" false }} +{{- if .Mixed }} + +{{ template "gateEvaluator" dict "Config" . "Mixed" true }} +{{- end }} + +{{ define "gateEvaluator" }} +{{- $elem := .Config.BaseElementType }} +{{- $poly := "polynomial" }} +{{- $ge := "GateEvaluator" }} +{{- if .Mixed }} +{{- $elem = .Config.ElementType }} +{{- $poly = "extPolynomial" }} +{{- $ge = "GateEvaluatorMixed" }} +{{- end }} +// {{ $ge }} provides a high-level API for evaluating compiled gates efficiently. // It manages the stack internally and handles input buffering, making it easy to // evaluate the same gate multiple times with different inputs. -type GateEvaluator struct { +type {{ $ge }} struct { gate gkrcore.GateBytecode - vars []{{ .ElementType }} + vars []{{ $elem }} + {{- if .Mixed }} + consts []{{ .Config.BaseElementType }} // the gate's constants in the base field, multiplied by with MulByElement + {{- end }} nbIn int // number of inputs expected - pool *polynomial.Pool // pool vars was allocated from, if any + pool *{{ $poly }}.Pool // pool vars was allocated from, if any } -// NewGateEvaluator creates an evaluator for the given compiled gate. +// New{{ $ge }} creates an evaluator for the given compiled gate. // The stack is preloaded with constants and ready for evaluation. -func NewGateEvaluator(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.Pool) GateEvaluator { - e := GateEvaluator{ +func New{{ $ge }}(gate gkrcore.GateBytecode, nbIn int, elementPool ...*{{ $poly }}.Pool) {{ $ge }} { + e := {{ $ge }}{ gate: gate, nbIn: nbIn, } @@ -28,18 +50,24 @@ func NewGateEvaluator(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polyn e.pool = elementPool[0] e.vars = e.pool.Make(gate.EvaluatorSize(nbIn)) } else { - e.vars = make([]{{ .ElementType }}, gate.EvaluatorSize(nbIn)) + e.vars = make([]{{ $elem }}, gate.EvaluatorSize(nbIn)) } e.vars = e.vars[:gate.NbConstants()] for i, constVal := range gate.Constants { e.vars[i].SetBigInt(constVal) } + {{- if .Mixed }} + e.consts = make([]{{ .Config.BaseElementType }}, len(gate.Constants)) + for i, constVal := range gate.Constants { + e.consts[i].SetBigInt(constVal) + } + {{- end }} return e } // Dump returns the evaluator's memory to the pool it was allocated from, if any, and does // nothing otherwise. The evaluator must not be used after this. -func (e *GateEvaluator) Dump() { +func (e *{{ $ge }}) Dump() { if e.pool != nil { e.pool.Dump(e.vars) } @@ -47,7 +75,7 @@ func (e *GateEvaluator) Dump() { // PushInput adds an input to the evaluator's input buffer. // Inputs must be added in order, and the number of inputs must match the gate's NbInputs. -func (e *GateEvaluator) PushInput(input {{ .ElementType }}) { +func (e *{{ $ge }}) PushInput(input {{ $elem }}) { e.vars = append(e.vars, input) } @@ -55,7 +83,7 @@ func (e *GateEvaluator) PushInput(input {{ .ElementType }}) { // The stack is automatically reset after evaluation, // making the evaluator ready for the next evaluation. // NB! The result is short-lived. It will be overwritten the next time Evaluate is called. -func (e *GateEvaluator) Evaluate(top ...{{ .ElementType }}) *{{ .ElementType }} { +func (e *{{ $ge }}) Evaluate(top ...{{ $elem }}) *{{ $elem }} { e.vars = append(e.vars, top...) if len(e.vars) != e.nbIn+e.gate.NbConstants() { @@ -78,10 +106,28 @@ func (e *GateEvaluator) Evaluate(top ...{{ .ElementType }}) *{{ .ElementType }} dst.Add(dst, &e.vars[inst.Inputs[j]]) } case gkrcore.OpMul: + {{- if .Mixed }} + // Operands below NbConstants are constants, multiplied by in the base field. + if a, b := inst.Inputs[0], inst.Inputs[1]; b < e.gate.NbConstants() { + dst.MulByElement(&e.vars[a], &e.consts[b]) + } else if a < e.gate.NbConstants() { + dst.MulByElement(&e.vars[b], &e.consts[a]) + } else { + dst.Mul(&e.vars[a], &e.vars[b]) + } + for j := 2; j < len(inst.Inputs); j++ { + if k := inst.Inputs[j]; k < e.gate.NbConstants() { + dst.MulByElement(dst, &e.consts[k]) + } else { + dst.Mul(dst, &e.vars[k]) + } + } + {{- else }} dst.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) for j := 2; j < len(inst.Inputs); j++ { dst.Mul(dst, &e.vars[inst.Inputs[j]]) } + {{- end }} case gkrcore.OpSub: dst.Sub(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) for j := 2; j < len(inst.Inputs); j++ { @@ -90,12 +136,22 @@ func (e *GateEvaluator) Evaluate(top ...{{ .ElementType }}) *{{ .ElementType }} case gkrcore.OpNeg: dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: - var prod {{ .ElementType }} + var prod {{ $elem }} + {{- if .Mixed }} + if b, c := inst.Inputs[1], inst.Inputs[2]; c < e.gate.NbConstants() { + prod.MulByElement(&e.vars[b], &e.consts[c]) + } else if b < e.gate.NbConstants() { + prod.MulByElement(&e.vars[c], &e.consts[b]) + } else { + prod.Mul(&e.vars[b], &e.vars[c]) + } + {{- else }} prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) + {{- end }} dst.Add(&e.vars[inst.Inputs[0]], &prod) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 - var sum {{ .ElementType }} + var sum {{ $elem }} sum.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) sum.Add(&sum, &e.vars[inst.Inputs[2]]) dst.Mul(&sum, &sum) // x² @@ -116,25 +172,25 @@ func (e *GateEvaluator) Evaluate(top ...{{ .ElementType }}) *{{ .ElementType }} return res } -// GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. +// {{ $ge }}Pool manages a pool of gate evaluators for a specific gate type. // All evaluators share the same underlying polynomial.Pool for element slices. -type GateEvaluatorPool struct { +type {{ $ge }}Pool struct { gate gkrcore.GateBytecode nbIn int lock sync.Mutex - available []*GateEvaluator - elementPool *polynomial.Pool + available []*{{ $ge }} + elementPool *{{ $poly }}.Pool } -func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.Pool) *GateEvaluatorPool { - return &GateEvaluatorPool{ +func New{{ $ge }}Pool(gate gkrcore.GateBytecode, nbIn int, elementPool *{{ $poly }}.Pool) *{{ $ge }}Pool { + return &{{ $ge }}Pool{ gate: gate, nbIn: nbIn, elementPool: elementPool, } } -func (gep *GateEvaluatorPool) Get() *GateEvaluator { +func (gep *{{ $ge }}Pool) Get() *{{ $ge }} { gep.lock.Lock() if n := len(gep.available); n > 0 { e := gep.available[n-1] @@ -144,11 +200,11 @@ func (gep *GateEvaluatorPool) Get() *GateEvaluator { } gep.lock.Unlock() - e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + e := New{{ $ge }}(gep.gate, gep.nbIn, gep.elementPool) return &e } -func (gep *GateEvaluatorPool) Put(e *GateEvaluator) { +func (gep *{{ $ge }}Pool) Put(e *{{ $ge }}) { gep.lock.Lock() gep.available = append(gep.available, e) gep.lock.Unlock() @@ -156,7 +212,7 @@ func (gep *GateEvaluatorPool) Put(e *GateEvaluator) { // DumpAll dumps all available evaluators' memory back to the pool. It is not to be used after that. // NB! User must ensure all evaluators have been put back in the pool to prevent memory leaks. -func (gep *GateEvaluatorPool) DumpAll() { +func (gep *{{ $ge }}Pool) DumpAll() { gep.lock.Lock() defer gep.lock.Unlock() for _, e := range gep.available { @@ -164,3 +220,4 @@ func (gep *GateEvaluatorPool) DumpAll() { } gep.available = nil } +{{ end }} diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index 26120ce1bd..73e9c367ff 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -1,3 +1,4 @@ +{{ $gateEvaluator := "GateEvaluator" }}{{ if .Mixed }}{{ $gateEvaluator = "GateEvaluatorMixed" }}{{ end }} import ( "errors" "fmt" @@ -114,7 +115,7 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { for levelWireI, wI := range group.Wires { wire := r.circuit[wI] - evaluator := {{.EvaluatorQualifier}}NewGateEvaluator(wire.Gate.Evaluate, len(wire.Inputs)) + evaluator := {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}(wire.Gate.Evaluate, len(wire.Inputs)) for claimI, src := range group.ClaimSources { for _, inI := range inputIndices[levelWireI] { evaluator.PushInput(finalEval[level.FinalEvalProofIndex(inI, claimI)]) @@ -181,7 +182,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss const minBlockSize = 64 assignment[wireI] = make([]{{ .ElementType }}, nbInstances) r.workers.Submit(nbInstances, func(start, end int) { - gateEval := {{.EvaluatorQualifier}}NewGateEvaluator(c[wireI].Gate.Evaluate, len(c[wireI].Inputs), &r.memPool) + gateEval := {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}(c[wireI].Gate.Evaluate, len(c[wireI].Inputs), &r.memPool) for instanceI := start; instanceI < end; instanceI++ { for _, inputWireI := range c[wireI].Inputs { gateEval.PushInput(assignment[inputWireI][instanceI]) @@ -318,14 +319,14 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in func (a WireAssignment) Complete(circuit Circuit) WireAssignment { nbInstances := a.NumInstances() - evaluators := make([]{{.EvaluatorQualifier}}GateEvaluator, len(circuit)) + evaluators := make([]{{ .EvaluatorQualifier }}{{ $gateEvaluator }}, len(circuit)) for i := range circuit { if len(a[i]) != nbInstances { a[i] = make([]{{ .ElementType }}, nbInstances) } if !circuit[i].IsInput() { - evaluators[i] = {{.EvaluatorQualifier}}NewGateEvaluator(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) + evaluators[i] = {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) } } diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index c6dd55b64c..cebf17cbbb 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -1,3 +1,4 @@ +{{ $gateEvaluator := "GateEvaluator" }}{{ if .Mixed }}{{ $gateEvaluator = "GateEvaluatorMixed" }}{{ end }} import ( "errors" "sync" @@ -120,7 +121,7 @@ func (e *zeroCheckLazyClaims) verifyFinalEval(r []{{ .ElementType }}, purportedV for _, wI := range group.Wires { wire := e.resources.circuit[wI] - evaluator := {{.EvaluatorQualifier}}NewGateEvaluator(wire.Gate.Evaluate, len(wire.Inputs)) + evaluator := {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}(wire.Gate.Evaluate, len(wire.Inputs)) for _, v := range gateInputEvals[levelWireI] { evaluator.PushInput(v) } @@ -176,7 +177,7 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { computeAll := func(start, end int) { var step {{ .ElementType }} - evaluators := make([]*{{.EvaluatorQualifier}}GateEvaluator, nbWires) + evaluators := make([]*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}, nbWires) for w := range nbWires { evaluators[w] = c.gateEvaluatorPools[w].Get() } @@ -313,7 +314,7 @@ type zeroCheckBase struct { resources *resources input []polynomial.MultiLin // UniqueGateInputs order inputIndices [][]int // [wireInLevel][gateInputJ] → index in input - gateEvaluatorPools []*{{.EvaluatorQualifier}}GateEvaluatorPool + gateEvaluatorPools []*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}Pool } func (c *zeroCheckBase) varsNum() int { @@ -363,12 +364,12 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { for _, group := range level.ClaimGroups() { nbWires += len(group.Wires) } - c.gateEvaluatorPools = make([]*{{.EvaluatorQualifier}}GateEvaluatorPool, nbWires) + c.gateEvaluatorPools = make([]*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}Pool, nbWires) levelWireI := 0 for _, group := range level.ClaimGroups() { for _, wI := range group.Wires { wire := r.circuit[wI] - c.gateEvaluatorPools[levelWireI] = {{.EvaluatorQualifier}}NewGateEvaluatorPool(wire.Gate.Evaluate, len(inputIndices[levelWireI]), &r.memPool) + c.gateEvaluatorPools[levelWireI] = {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}Pool(wire.Gate.Evaluate, len(inputIndices[levelWireI]), &r.memPool) levelWireI++ } } @@ -496,7 +497,7 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { computeAll := func(start, end int) { var step {{ .ElementType }} - evaluators := make([]*{{.EvaluatorQualifier}}GateEvaluator, nbWires) + evaluators := make([]*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}, nbWires) for w := range nbWires { evaluators[w] = c.gateEvaluatorPools[w].Get() } From 3542e7016bb46a314d28dc18a8da25cfc49f47fc Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 16:34:28 -0500 Subject: [PATCH 11/37] refactor: use SetBytesCanonical instead Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr.go | 6 ++++++ gkr/bls12-381/gkr.go | 6 ++++++ gkr/bn254/gkr.go | 6 ++++++ gkr/bw6-761/gkr.go | 6 ++++++ go.mod | 2 +- go.sum | 4 ++-- .../generator/backend/template/gkr/gkr.go.tmpl | 6 ++++++ internal/gkr/gkrcore/transcript.go | 15 +++++++++------ internal/gkr/rational/gkr.go | 6 ++++++ 9 files changed, 48 insertions(+), 9 deletions(-) diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index 477a0acbd1..add3795df0 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -171,6 +171,9 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() + if nbVars == 0 { + return nil, nil, errors.New("number of variables must be positive") + } if 1< Date: Mon, 5 Oct 2026 16:37:57 -0500 Subject: [PATCH 12/37] feat: use extension suffixes for polynomial types and functions Signed-off-by: Arya Tabaie --- internal/generator/backend/main.go | 9 ++++ .../backend/template/gkr/blueprint.go.tmpl | 2 +- .../backend/template/gkr/evaluator.go.tmpl | 18 ++++--- .../backend/template/gkr/gkr.go.tmpl | 6 +-- .../template/gkr/gkr.test.vectors.go.tmpl | 2 +- .../backend/template/gkr/sumcheck.go.tmpl | 52 +++++++++---------- .../template/gkr/sumcheck.test.defs.go.tmpl | 8 +-- .../template/gkr/sumcheck.test.go.tmpl | 2 +- .../template/gkr/test_vector_utils.go.tmpl | 2 +- 9 files changed, 56 insertions(+), 45 deletions(-) diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index d8b62ca5d5..0938b49d44 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -382,6 +382,15 @@ type gkrConfig struct { // are not elements of the field itself. func (c gkrConfig) Mixed() bool { return c.BaseElementType != c.ElementType } +// BasePolynomial is the name under which templates import the polynomial package over +// BaseElementType: basePolynomial when the field is an extension, polynomial otherwise. +func (c gkrConfig) BasePolynomial() string { + if c.Mixed() { + return "basePolynomial" + } + return "polynomial" +} + func assertNoError(err error) { if err != nil { panic(err) diff --git a/internal/generator/backend/template/gkr/blueprint.go.tmpl b/internal/generator/backend/template/gkr/blueprint.go.tmpl index 32964bd451..2122c303d0 100644 --- a/internal/generator/backend/template/gkr/blueprint.go.tmpl +++ b/internal/generator/backend/template/gkr/blueprint.go.tmpl @@ -79,7 +79,7 @@ func (b *BlueprintSolve) Reset() { assignments := make(prover.WireAssignment, len(b.Circuit)) nbPaddedInstances := ecc.NextPowerOfTwo(uint64(b.NbInstances)) for i := range assignments { - assignments[i] = make(polynomial.MultiLin, nbPaddedInstances) + assignments[i] = make(polynomial.MultiLin{{ .ExtensionSuffix }}, nbPaddedInstances) } b.assignments = assignments } diff --git a/internal/generator/backend/template/gkr/evaluator.go.tmpl b/internal/generator/backend/template/gkr/evaluator.go.tmpl index f05f02c042..766309a5a9 100644 --- a/internal/generator/backend/template/gkr/evaluator.go.tmpl +++ b/internal/generator/backend/template/gkr/evaluator.go.tmpl @@ -3,11 +3,11 @@ import ( "sync" "{{ .BaseFieldPackagePath }}" - "{{ .BaseFieldPackagePath }}/polynomial" {{- if .Mixed }} "{{ .FieldPackagePath }}" - extPolynomial "{{ .FieldPackagePath }}/polynomial" + basePolynomial "{{ .BaseFieldPackagePath }}/polynomial" {{- end }} + "{{ .FieldPackagePath }}/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" ) @@ -19,11 +19,13 @@ import ( {{ define "gateEvaluator" }} {{- $elem := .Config.BaseElementType }} -{{- $poly := "polynomial" }} +{{- $poly := .Config.BasePolynomial }} +{{- $suffix := "" }} {{- $ge := "GateEvaluator" }} {{- if .Mixed }} {{- $elem = .Config.ElementType }} -{{- $poly = "extPolynomial" }} +{{- $poly = "polynomial" }} +{{- $suffix = .Config.ExtensionSuffix }} {{- $ge = "GateEvaluatorMixed" }} {{- end }} // {{ $ge }} provides a high-level API for evaluating compiled gates efficiently. @@ -36,12 +38,12 @@ type {{ $ge }} struct { consts []{{ .Config.BaseElementType }} // the gate's constants in the base field, multiplied by with MulByElement {{- end }} nbIn int // number of inputs expected - pool *{{ $poly }}.Pool // pool vars was allocated from, if any + pool *{{ $poly }}.Pool{{ $suffix }} // pool vars was allocated from, if any } // New{{ $ge }} creates an evaluator for the given compiled gate. // The stack is preloaded with constants and ready for evaluation. -func New{{ $ge }}(gate gkrcore.GateBytecode, nbIn int, elementPool ...*{{ $poly }}.Pool) {{ $ge }} { +func New{{ $ge }}(gate gkrcore.GateBytecode, nbIn int, elementPool ...*{{ $poly }}.Pool{{ $suffix }}) {{ $ge }} { e := {{ $ge }}{ gate: gate, nbIn: nbIn, @@ -179,10 +181,10 @@ type {{ $ge }}Pool struct { nbIn int lock sync.Mutex available []*{{ $ge }} - elementPool *{{ $poly }}.Pool + elementPool *{{ $poly }}.Pool{{ $suffix }} } -func New{{ $ge }}Pool(gate gkrcore.GateBytecode, nbIn int, elementPool *{{ $poly }}.Pool) *{{ $ge }}Pool { +func New{{ $ge }}Pool(gate gkrcore.GateBytecode, nbIn int, elementPool *{{ $poly }}.Pool{{ $suffix }}) *{{ $ge }}Pool { return &{{ $ge }}Pool{ gate: gate, nbIn: nbIn, diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index 2e9d355345..82e74e39ab 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -24,7 +24,7 @@ type ( // The goal is to prove/verify evaluations of many instances of the same circuit // WireAssignment is the assignment of values to the same wire across many instances of the circuit -type WireAssignment []polynomial.MultiLin +type WireAssignment []polynomial.MultiLin{{ .ExtensionSuffix }} type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) @@ -56,7 +56,7 @@ type resources struct { outgoingEvalPoints [][][]{{ .ElementType }} nbVars int assignment WireAssignment - memPool polynomial.Pool + memPool polynomial.Pool{{ .ExtensionSuffix }} workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule @@ -176,7 +176,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss r := newResources(c, schedule, nbVars, hasher) r.assignment = assignment - r.memPool = polynomial.NewPool(gkrcore.MemoryRequirements(c, nbInstances)...) + r.memPool = polynomial.NewPool{{ .ExtensionSuffix }}(gkrcore.MemoryRequirements(c, nbInstances)...) r.workers = utils.NewWorkerPool() defer r.workers.Stop() diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl index fb99e2c543..2f0f2af634 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl @@ -16,7 +16,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } proof[i] = sumcheckProof{ - partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + partialSumPolys: make([]polynomial.Polynomial{{ .ExtensionSuffix }}, len(printable[i].PartialSumPolys)), finalEvalProof: finalEvalProof, } for k := range printable[i].PartialSumPolys { diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index cebf17cbbb..bee8a089d1 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -20,7 +20,7 @@ type transcript = gkrcore.HashTranscript[{{ .ElementType }}, *{{ .ElementType }} // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) type sumcheckClaims interface { - roundPolynomial() polynomial.Polynomial // compute gⱼ polynomial for current round + roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} // compute gⱼ polynomial for current round roundFold(r {{ .ElementType }}) // fold inputs and eq at challenge r varsNum() int // number of variables proveFinalEval(r []{{ .ElementType }}) []{{ .ElementType }} // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof @@ -35,7 +35,7 @@ type sumcheckLazyClaims interface { // sumcheckProof of a multi-statement. type sumcheckProof struct { - partialSumPolys []polynomial.Polynomial + partialSumPolys []polynomial.Polynomial{{ .ExtensionSuffix }} finalEvalProof []{{ .ElementType }} //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof } @@ -44,7 +44,7 @@ type sumcheckProof struct { // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { varsNum := claims.varsNum() - proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial, varsNum)} + proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial{{ .ExtensionSuffix }}, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() challenges := make([]{{ .ElementType }}, varsNum) @@ -65,7 +65,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum {{ .ElementType }}, degree int, t *transcript) error { r := make([]{{ .ElementType }}, claims.varsNum()) - gJ := make(polynomial.Polynomial, degree+1) + gJ := make(polynomial.Polynomial{{ .ExtensionSuffix }}, degree+1) gJR := claimedSum for j := range claims.varsNum() { @@ -76,7 +76,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum { gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) r[j] = t.Challenge(proof.partialSumPolys[j]...) - gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) + gJCoeffs := polynomial.InterpolateOnRange{{ .ExtensionSuffix }}(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -115,7 +115,7 @@ func (e *zeroCheckLazyClaims) verifyFinalEval(r []{{ .ElementType }}, purportedV level := e.resources.schedule[e.levelI] gateInputEvals := gkrcore.ReduplicateInputs(level, e.resources.circuit, uniqueInputEvaluations) - var claimedEvals polynomial.Polynomial + var claimedEvals polynomial.Polynomial{{ .ExtensionSuffix }} levelWireI := 0 for _, group := range level.ClaimGroups() { for _, wI := range group.Wires { @@ -128,7 +128,7 @@ func (e *zeroCheckLazyClaims) verifyFinalEval(r []{{ .ElementType }}, purportedV gateEval := evaluator.Evaluate() for _, src := range group.ClaimSources { - eq := polynomial.EvalEq(e.resources.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex], r) + eq := polynomial.EvalEq{{ .ExtensionSuffix }}(e.resources.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex], r) var term {{ .ElementType }} term.Mul(&eq, gateEval) claimedEvals = append(claimedEvals, term) @@ -149,14 +149,14 @@ func (e *zeroCheckLazyClaims) verifyFinalEval(r []{{ .ElementType }}, purportedV // Each wire has its own eq table with the batching coefficients baked in. type zeroCheckClaims struct { zeroCheckBase - eqs []polynomial.MultiLin // per-wire interpolation bases for evaluating wire assignments at challenge points + eqs []polynomial.MultiLin{{ .ExtensionSuffix }} // per-wire interpolation bases for evaluating wire assignments at challenge points } // roundPolynomial computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). // The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). // The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). // By convention, g₀ is a constant polynomial equal to the claimed sum. -func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { +func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.input) @@ -166,7 +166,7 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLin, nbWires+nbUniqueInputs) + ml := make([]polynomial.MultiLin{{ .ExtensionSuffix }}, nbWires+nbUniqueInputs) copy(ml, c.eqs) copy(ml[nbWires:], c.input) @@ -271,7 +271,7 @@ func (c *zeroCheckClaims) proveFinalEval(r []{{ .ElementType }}) []{{ .ElementTy // eqAcc sets m to an eq table at q and then adds it to e. // m <- m[0] · eq(q, -). // e <- e + m -func (r *resources) eqAcc(e, m polynomial.MultiLin, q []{{ .ElementType }}) { +func (r *resources) eqAcc(e, m polynomial.MultiLin{{ .ExtensionSuffix }}, q []{{ .ElementType }}) { n := len(q) // At the end of each iteration, m(h₁, ..., hₙ) = m[0] · eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) @@ -312,7 +312,7 @@ type zeroCheckBase struct { foldingCoeff {{ .ElementType }} levelI int resources *resources - input []polynomial.MultiLin // UniqueGateInputs order + input []polynomial.MultiLin{{ .ExtensionSuffix }} // UniqueGateInputs order inputIndices [][]int // [wireInLevel][gateInputJ] → index in input gateEvaluatorPools []*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}Pool } @@ -324,7 +324,7 @@ func (c *zeroCheckBase) varsNum() int { // proveFinalEval records the outgoing eval point, folds the last variable, // collects the unique input evaluations, and releases all pooled memory. // extraPolys is the level-type-specific slice (eqs or suffixEq) to dump. -func (c *zeroCheckBase) proveFinalEval(r []{{ .ElementType }}, extraPolys []polynomial.MultiLin) []{{ .ElementType }} { +func (c *zeroCheckBase) proveFinalEval(r []{{ .ElementType }}, extraPolys []polynomial.MultiLin{{ .ExtensionSuffix }}) []{{ .ElementType }} { c.resources.outgoingEvalPoints[c.levelI] = [][]{{ .ElementType }}{r} evaluations := make([]{{ .ElementType }}, len(c.input)) for i := range c.input { @@ -354,7 +354,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { } uniqueInputs, inputIndices := r.circuit.InputMapping(level) - c.input = make([]polynomial.MultiLin, len(uniqueInputs)) + c.input = make([]polynomial.MultiLin{{ .ExtensionSuffix }}, len(uniqueInputs)) for i, inW := range uniqueInputs { c.input[i] = r.memPool.Clone(r.assignment[inW]) } @@ -381,19 +381,19 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { level := r.schedule[levelI] eqLength := 1 << r.nbVars - claims.eqs = make([]polynomial.MultiLin, len(claims.gateEvaluatorPools)) + claims.eqs = make([]polynomial.MultiLin{{ .ExtensionSuffix }}, len(claims.gateEvaluatorPools)) var alpha {{ .ElementType }} alpha.SetOne() levelWireI := 0 for _, group := range level.ClaimGroups() { nbSources := len(group.ClaimSources) - groupEq := polynomial.MultiLin(r.memPool.Make(eqLength)) + groupEq := polynomial.MultiLin{{ .ExtensionSuffix }}(r.memPool.Make(eqLength)) groupEq[0].Set(&alpha) groupEq.Eq(r.outgoingEvalPoints[group.ClaimSources[0].Level][group.ClaimSources[0].OutgoingClaimIndex]) if nbSources > 1 { - newEq := polynomial.MultiLin(r.memPool.Make(eqLength)) + newEq := polynomial.MultiLin{{ .ExtensionSuffix }}(r.memPool.Make(eqLength)) aI := alpha for k := 1; k < nbSources; k++ { aI.Mul(&aI, &claims.foldingCoeff) @@ -414,7 +414,7 @@ func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { alpha.Mul(&alpha, &stride) for w := 1; w < len(group.Wires); w++ { - claims.eqs[levelWireI] = polynomial.MultiLin(r.memPool.Make(eqLength)) + claims.eqs[levelWireI] = polynomial.MultiLin{{ .ExtensionSuffix }}(r.memPool.Make(eqLength)) r.workers.Submit(eqLength, func(start, end int) { for i := start; i < end; i++ { claims.eqs[levelWireI][i].Mul(&claims.eqs[levelWireI-1][i], &stride) @@ -438,7 +438,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) ({{ .ElementType } foldingCoeff = r.transcript.Challenge() } - claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) + claimedEvals := make(polynomial.Polynomial{{ .ExtensionSuffix }}, 0, level.NbClaims()) for _, group := range level.ClaimGroups() { for _, wI := range group.Wires { for claimI, src := range group.ClaimSources { @@ -474,14 +474,14 @@ func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { // read-only as weights in each round; only the input multilinears are folded. type singleSourceZeroCheckClaims struct { zeroCheckBase - suffixEq polynomial.MultiLin // contiguous buffer; second half is the current round's eq segment + suffixEq polynomial.MultiLin{{ .ExtensionSuffix }} // contiguous buffer; second half is the current round's eq segment } // roundPolynomial computes g'_j(1), ..., g'_j(d) where d is the gate degree. // g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) // The wire contributions are accumulated via Horner's method in α (= foldingCoeff), // eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. -func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { +func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.input) @@ -586,7 +586,7 @@ func (c *singleSourceZeroCheckClaims) roundFold(r {{ .ElementType }}) { // proveFinalEval provides the unique input wire values at the final evaluation point. func (c *singleSourceZeroCheckClaims) proveFinalEval(r []{{ .ElementType }}) []{{ .ElementType }} { - return c.zeroCheckBase.proveFinalEval(r, []polynomial.MultiLin{c.suffixEq}) + return c.zeroCheckBase.proveFinalEval(r, []polynomial.MultiLin{{ .ExtensionSuffix }}{c.suffixEq}) } // buildSuffixEq constructs suffix eq tables in a single contiguous buffer of length 2^N. @@ -599,7 +599,7 @@ func (c *singleSourceZeroCheckClaims) proveFinalEval(r []{{ .ElementType }}) []{ // // The first two positions are unused padding so that the buffer halves cleanly: // in round j the eq segment is buf[len(buf)/2:] and buf is trimmed to buf[:len(buf)/2]. -func (r *resources) buildSuffixEq(q []{{ .ElementType }}) polynomial.MultiLin { +func (r *resources) buildSuffixEq(q []{{ .ElementType }}) polynomial.MultiLin{{ .ExtensionSuffix }} { n := len(q) buf := r.memPool.Make(1 << n) @@ -663,7 +663,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er degree := r.circuit.ZeroCheckDegree(level) challenges := make([]{{ .ElementType }}, r.nbVars) - gPrime := make(polynomial.Polynomial, degree+1) + gPrime := make(polynomial.Polynomial{{ .ExtensionSuffix }}, degree+1) // The verifier tracks claimedSum = g'_{j-1}(r_{j-1}), the "primed" value with // the per-variable eq factors already absent. This is correct because the @@ -693,13 +693,13 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) challenges[j] = r.transcript.Challenge(partialPoly...) - gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) + gPrimeCoeffs := polynomial.InterpolateOnRange{{ .ExtensionSuffix }}(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } // claimedSum is now Σ_w c^w · gate_w(inputs(r)), without the eq factor. // verifyFinalEval expects Σ_w c^w · eq(q, r) · gate_w(inputs(r)), so multiply. - eqAtQR := polynomial.EvalEq(q, challenges) + eqAtQR := polynomial.EvalEq{{ .ExtensionSuffix }}(q, challenges) claimedSum.Mul(&claimedSum, &eqAtQR) return lazyClaims.verifyFinalEval(challenges, claimedSum, proof[levelI].finalEvalProof) diff --git a/internal/generator/backend/template/gkr/sumcheck.test.defs.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.defs.go.tmpl index 54a5c00b85..06b06c13ee 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.defs.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.defs.go.tmpl @@ -1,7 +1,7 @@ {{ define "sumcheckTestDefs" }} type singleMultilinClaim struct { - g polynomial.MultiLin + g polynomial.MultiLin{{ .ExtensionSuffix }} } func (c *singleMultilinClaim) proveFinalEval(r []{{ .ElementType }}) []{{ .ElementType }} { @@ -12,7 +12,7 @@ func (c *singleMultilinClaim) varsNum() int { return bits.TrailingZeros(uint(len(c.g))) } -func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { +func sumForX1One(g polynomial.MultiLin{{ .ExtensionSuffix }}) polynomial.Polynomial{{ .ExtensionSuffix }} { sum := g[len(g)/2] for i := len(g)/2 + 1; i < len(g); i++ { sum.Add(&sum, &g[i]) @@ -20,7 +20,7 @@ func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { return []{{ .ElementType }}{sum} } -func (c *singleMultilinClaim) roundPolynomial() polynomial.Polynomial { +func (c *singleMultilinClaim) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { return sumForX1One(c.g) } @@ -29,7 +29,7 @@ func (c *singleMultilinClaim) roundFold(r {{ .ElementType }}) { } type singleMultilinLazyClaim struct { - g polynomial.MultiLin + g polynomial.MultiLin{{ .ExtensionSuffix }} claimedSum {{ .ElementType }} } diff --git a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl index a07aab1365..8da218dd73 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl @@ -13,7 +13,7 @@ import ( ) func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { - poly := make(polynomial.MultiLin, len(polyInt)) + poly := make(polynomial.MultiLin{{ .ExtensionSuffix }}, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) } diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 09571eb59d..706d937fe3 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -128,7 +128,7 @@ func sliceEquals(a []{{.ElementType}}, b []{{.ElementType}}) error { return nil } -func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { +func polynomialSliceEquals(a []polynomial.Polynomial{{ .ExtensionSuffix }}, b []polynomial.Polynomial{{ .ExtensionSuffix }}) error { if len(a) != len(b) { return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) } From d5490e173a9e21f64c9685f91cd747c41fc78641 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 16:46:15 -0500 Subject: [PATCH 13/37] refactor: trim down GKR config Signed-off-by: Arya Tabaie --- internal/generator/backend/main.go | 110 ++++++++++++++++++----------- 1 file changed, 70 insertions(+), 40 deletions(-) diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index 0938b49d44..2bb596c756 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path" "path/filepath" "strings" "sync" @@ -147,15 +148,8 @@ func main() { curvePackageName := strings.ToLower(d.Curve) cfg := gkrConfig{ - ElementType: "fr.Element", - BaseElementType: "fr.Element", - BaseFieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", - FieldPackageName: "fr", - FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", - FieldID: d.CurveID, - FieldDescription: "gkrcore.PrimeField(ecc." + d.CurveID + ".ScalarField())", - GkrPackageName: curvePackageName, - EvaluatorQualifier: "evaluator.", + FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", + GkrPackageName: curvePackageName, } assertNoError(generateGkrBackend(cfg)) @@ -236,14 +230,9 @@ func main() { go func() { // generate gkr and sumcheck for rational cfg := gkrConfig{ - ElementType: "rational.Element", - BaseElementType: "rational.Element", - BaseFieldPackagePath: "github.com/consensys/gnark/internal/rational", - FieldPackagePath: "github.com/consensys/gnark/internal/rational", - FieldPackageName: "rational", - GkrPackageName: "rational", - NoGkrTests: true, - GenerateTestVectors: true, + FieldPackagePath: "github.com/consensys/gnark/internal/rational", + GkrPackageName: "rational", + GenerateTestVectors: true, } assertNoError(generateGkrBackend(cfg)) @@ -323,7 +312,7 @@ func generateGkrBackend(cfg gkrConfig) error { {File: filepath.Join(proverDir, testVectorUtilsFileName), Templates: []string{"test_vector_utils.go.tmpl"}}, } - if !cfg.NoGkrTests { + if !cfg.GenerateTestVectors { entries = append(entries, bavard.Entry{ File: filepath.Join(proverDir, "gkr_test.go"), Templates: []string{"gkr.test.go.tmpl", "gkr.test.vectors.go.tmpl"}, }) @@ -353,34 +342,52 @@ func generateGkrBackend(cfg gkrConfig) error { } type gkrConfig struct { - ElementType string - FieldPackagePath string - FieldPackageName string - GkrPackageName string // the GKR package, relative to the repo root - FieldID string // e.g. BLS12_377, BABYBEAR, etc. - GenerateTestVectors bool - NoGkrTests bool - // EvaluatorQualifier prefixes references to the gate evaluator types, for fields whose prover - // package is not the evaluators' own package. Empty when they are the same package. - EvaluatorQualifier string + // FieldPackagePath is the import path of the package of the field's element type, e.g. + // github.com/consensys/gnark-crypto/ecc/bn254/fr. + FieldPackagePath string // ExtensionSuffix is the suffix gnark-crypto's extensions package appends to the names of an - // extension's types and constants (e.g. VectorE6, BytesE6). Empty for the curves and - // rational. + // extension's types and constants (e.g. E6, VectorE6, BytesE6). Empty for a prime field. ExtensionSuffix string - // FieldDescription is the Go expression of the field's gkrcore.Field description, e.g. - // "gkrcore.PrimeField(ecc.BN254.ScalarField())". Used only where gkr.test.go.tmpl is - // generated (NoGkrTests false), so rational leaves it empty. - FieldDescription string - // BaseElementType is the type of the prime subfield's elements, in which gate constants live. - // It is ElementType itself unless the field is an extension. - BaseElementType string - // BaseFieldPackagePath is the import path of BaseElementType's package. - BaseFieldPackagePath string + // GkrPackageName is the directory of the generated packages, relative to the GKR roots. + GkrPackageName string + // Description is the Go expression of the field's gkrcore.Field description. Empty for the + // curves, whose description is derived from FieldID. + Description string + // GenerateTestVectors is set for the configuration whose package also generates the test + // vectors, instead of running tests against them. + GenerateTestVectors bool +} + +// FieldPackageName is the name of the package of the field's element type. +func (c gkrConfig) FieldPackageName() string { return path.Base(c.FieldPackagePath) } + +// ElementType is the type of the field's elements. +func (c gkrConfig) ElementType() string { + if c.Mixed() { + return c.FieldPackageName() + "." + c.ExtensionSuffix + } + return c.FieldPackageName() + ".Element" +} + +// BaseFieldPackagePath is the import path of BaseElementType's package: the prime subfield's, +// the parent of an extension's package. +func (c gkrConfig) BaseFieldPackagePath() string { + if c.Mixed() { + return path.Dir(c.FieldPackagePath) + } + return c.FieldPackagePath } +// BaseFieldPackageName is the name of the package of the prime subfield's element type. +func (c gkrConfig) BaseFieldPackageName() string { return path.Base(c.BaseFieldPackagePath()) } + +// BaseElementType is the type of the prime subfield's elements, in which gate constants live. +// It is ElementType itself unless the field is an extension. +func (c gkrConfig) BaseElementType() string { return c.BaseFieldPackageName() + ".Element" } + // Mixed reports whether the field is an extension of its prime subfield, so that gate constants // are not elements of the field itself. -func (c gkrConfig) Mixed() bool { return c.BaseElementType != c.ElementType } +func (c gkrConfig) Mixed() bool { return c.ExtensionSuffix != "" } // BasePolynomial is the name under which templates import the polynomial package over // BaseElementType: basePolynomial when the field is an extension, polynomial otherwise. @@ -391,6 +398,29 @@ func (c gkrConfig) BasePolynomial() string { return "polynomial" } +// FieldID is the name of the field's ecc.ID constant, e.g. BLS12_377. +func (c gkrConfig) FieldID() string { + return strings.ToUpper(strings.ReplaceAll(c.GkrPackageName, "-", "_")) +} + +// EvaluatorQualifier prefixes references to the gate evaluator types, for fields whose prover +// package is not the evaluators' own package. Empty when they are the same package. +func (c gkrConfig) EvaluatorQualifier() string { + if c.GenerateTestVectors { + return "" + } + return "evaluator." +} + +// FieldDescription is the Go expression of the field's gkrcore.Field description, used where +// gkr.test.go.tmpl is generated. +func (c gkrConfig) FieldDescription() string { + if c.Description != "" { + return c.Description + } + return "gkrcore.PrimeField(ecc." + c.FieldID() + ".ScalarField())" +} + func assertNoError(err error) { if err != nil { panic(err) From aa0748939d8aef12b9d3245af15389793079c7fe Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 22:46:50 -0500 Subject: [PATCH 14/37] feat: make operands constant-first, move Field out of internal Signed-off-by: Arya Tabaie --- constraint/solver/gkrgates/registry.go | 2 +- gkr/bls12-377/gkr_test.go | 3 +- gkr/bls12-381/gkr_test.go | 3 +- gkr/bn254/gkr_test.go | 3 +- gkr/bw6-761/gkr_test.go | 3 +- gkr/field.go | 30 +++ gkr/gkrapi/api.go | 10 +- internal/generator/backend/main.go | 20 +- .../backend/template/gkr/evaluator.go.tmpl | 61 ++++-- .../backend/template/gkr/gkr.test.go.tmpl | 1 + .../template/gkr/gkr.test.vectors.gen.go.tmpl | 3 +- internal/gkr/gkr_test.go | 3 +- internal/gkr/gkrcore/gate.go | 150 ++++++++++---- internal/gkr/gkrcore/gate_test.go | 188 ++++++++++++++++-- internal/gkr/gkrcore/schedule_test.go | 3 +- internal/gkr/gkrtesting/gkrtesting_test.go | 4 +- std/gkrapi/compile.go | 2 +- 17 files changed, 397 insertions(+), 92 deletions(-) create mode 100644 gkr/field.go diff --git a/constraint/solver/gkrgates/registry.go b/constraint/solver/gkrgates/registry.go index 004b0cede1..2266a64cd2 100644 --- a/constraint/solver/gkrgates/registry.go +++ b/constraint/solver/gkrgates/registry.go @@ -119,7 +119,7 @@ func Register(f gkr.GateFunction, nbIn int, options ...RegisterOption) error { } for _, curve := range s.curves { - compiled, err := gkrcore.CompileGateFunction(f, nbIn, gkrcore.PrimeField(curve.ScalarField())) + compiled, err := gkrcore.CompileGateFunction(f, nbIn, gkr.PrimeField(curve.ScalarField())) if err != nil { return err } diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index c41ecda7f7..751f7d899e 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -20,12 +20,13 @@ import ( gcHash "github.com/consensys/gnark-crypto/hash" _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BLS12_377.ScalarField())) +var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_377.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index b9f47831a7..02c3c8488f 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -20,12 +20,13 @@ import ( gcHash "github.com/consensys/gnark-crypto/hash" _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BLS12_381.ScalarField())) +var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_381.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index ba01cf624e..6284b975c6 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -20,12 +20,13 @@ import ( gcHash "github.com/consensys/gnark-crypto/hash" _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) +var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index 02d1c6cf1b..58464bbb49 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -20,12 +20,13 @@ import ( gcHash "github.com/consensys/gnark-crypto/hash" _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" ) -var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BW6_761.ScalarField())) +var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BW6_761.ScalarField())) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case diff --git a/gkr/field.go b/gkr/field.go new file mode 100644 index 0000000000..7cf178e4c2 --- /dev/null +++ b/gkr/field.go @@ -0,0 +1,30 @@ +package gkr + +import "math/big" + +// Field describes F_p[X]/(f), with f = Xⁿ + MinPoly[n-1]·Xⁿ⁻¹ + … + MinPoly[0] monic and +// irreducible over F_p, n = len(MinPoly). Coefficients are in increasing degree: X is [0], +// X² + 1 is [1, 0]. +type Field struct { + Modulus *big.Int + MinPoly []*big.Int +} + +// PrimeField describes F_p itself, as F_p[X]/(X). +func PrimeField(p *big.Int) Field { + return Field{Modulus: p, MinPoly: []*big.Int{big.NewInt(0)}} +} + +// Degree returns n, the extension degree of the field over F_p. +func (f Field) Degree() int { + return len(f.MinPoly) +} + +// KoalaBearE6 describes KoalaBear's degree-6 extension E6, whose tower generator v has minimal +// polynomial X⁶ - 2X³ - 2. +func KoalaBearE6() Field { + return Field{ + Modulus: big.NewInt(2130706433), // 2^31 - 2^24 + 1 + MinPoly: []*big.Int{big.NewInt(-2), big.NewInt(0), big.NewInt(0), big.NewInt(-2), big.NewInt(0), big.NewInt(0)}, + } +} diff --git a/gkr/gkrapi/api.go b/gkr/gkrapi/api.go index cc95b2b0dc..691ca4ae78 100644 --- a/gkr/gkrapi/api.go +++ b/gkr/gkrapi/api.go @@ -10,11 +10,11 @@ import ( // Circuit is a circuit compiled by API.Compile, ready to prove and verify with gkr/. type Circuit = gkrcore.SerializableCircuit -// Field describes the field to compile a circuit for: F_p, or an extension of it. -type Field = gkrcore.Field - // PrimeField describes F_p itself. -var PrimeField = gkrcore.PrimeField +var PrimeField = gkr.PrimeField + +// KoalaBearE6 describes KoalaBear's degree-6 extension E6. +var KoalaBearE6 = gkr.KoalaBearE6 // ConsolidationMode selects which wires DefaultProvingSchedule consolidates into level 0. type ConsolidationMode = gkrcore.ConsolidationMode @@ -69,7 +69,7 @@ func (api *API) Export(in ...gkr.Variable) { } // Compile compiles the circuit for field, and builds its default proving schedule under mode. -func (api *API) Compile(field Field, mode ConsolidationMode) (Circuit, constraint.GkrProvingSchedule, error) { +func (api *API) Compile(field gkr.Field, mode ConsolidationMode) (Circuit, constraint.GkrProvingSchedule, error) { _, circuit, err := api.circuit.Compile(field) if err != nil { return nil, nil, err diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index 2bb596c756..ef8b406f8e 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -225,6 +225,18 @@ func main() { } + wg.Add(1) + // GKR over KoalaBear's E6 + go func() { + defer wg.Done() + assertNoError(generateGkrBackend(gkrConfig{ + FieldPackagePath: "github.com/consensys/gnark-crypto/field/koalabear/extensions", + ExtensionSuffix: "E6", + GkrPackageName: "koalabear", + Description: "gkr.KoalaBearE6()", + })) + }() + wg.Add(1) // GKR test vectors go func() { @@ -329,7 +341,7 @@ func generateGkrBackend(cfg gkrConfig) error { return err } - if !cfg.GenerateTestVectors { + if !cfg.GenerateTestVectors && !cfg.Mixed() { // The blueprints call the prover, which imports the evaluators; putting them in the // evaluators' package would close an import cycle, so they get their own package. blueprintEntry := bavard.Entry{File: filepath.Join(internalDir, "blueprints", "blueprint.go"), Templates: []string{"blueprint.go.tmpl"}} @@ -350,7 +362,7 @@ type gkrConfig struct { ExtensionSuffix string // GkrPackageName is the directory of the generated packages, relative to the GKR roots. GkrPackageName string - // Description is the Go expression of the field's gkrcore.Field description. Empty for the + // Description is the Go expression of the field's gkr.Field description. Empty for the // curves, whose description is derived from FieldID. Description string // GenerateTestVectors is set for the configuration whose package also generates the test @@ -412,13 +424,13 @@ func (c gkrConfig) EvaluatorQualifier() string { return "evaluator." } -// FieldDescription is the Go expression of the field's gkrcore.Field description, used where +// FieldDescription is the Go expression of the field's gkr.Field description, used where // gkr.test.go.tmpl is generated. func (c gkrConfig) FieldDescription() string { if c.Description != "" { return c.Description } - return "gkrcore.PrimeField(ecc." + c.FieldID() + ".ScalarField())" + return "gkr.PrimeField(ecc." + c.FieldID() + ".ScalarField())" } func assertNoError(err error) { diff --git a/internal/generator/backend/template/gkr/evaluator.go.tmpl b/internal/generator/backend/template/gkr/evaluator.go.tmpl index 766309a5a9..4c8a68cdf0 100644 --- a/internal/generator/backend/template/gkr/evaluator.go.tmpl +++ b/internal/generator/backend/template/gkr/evaluator.go.tmpl @@ -81,6 +81,13 @@ func (e *{{ $ge }}) PushInput(input {{ $elem }}) { e.vars = append(e.vars, input) } +{{- if .Mixed }} +// isInputConstant returns whether the instruction's inputI-th input is one of the gate's constants. +func (e *{{ $ge }}) isInputConstant(inst *gkrcore.GateInstruction, inputI int) bool { + return int(inst.Inputs[inputI]) < e.gate.NbConstants() +} + +{{ end -}} // Evaluate adds top to the top of the stack, executes the gate on it and returns the result. // The stack is automatically reset after evaluation, // making the evaluator ready for the next evaluation. @@ -103,35 +110,41 @@ func (e *{{ $ge }}) Evaluate(top ...{{ $elem }}) *{{ $elem }} { // Use switch instead of function pointer for better inlining switch inst.Op { case gkrcore.OpAdd: + {{- if .Mixed }} + if e.isInputConstant(inst, 0) { + dst.AddElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) + } else { + dst.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + {{- else }} dst.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + {{- end }} for j := 2; j < len(inst.Inputs); j++ { dst.Add(dst, &e.vars[inst.Inputs[j]]) } case gkrcore.OpMul: {{- if .Mixed }} - // Operands below NbConstants are constants, multiplied by in the base field. - if a, b := inst.Inputs[0], inst.Inputs[1]; b < e.gate.NbConstants() { - dst.MulByElement(&e.vars[a], &e.consts[b]) - } else if a < e.gate.NbConstants() { - dst.MulByElement(&e.vars[b], &e.consts[a]) + if e.isInputConstant(inst, 0) { + dst.MulByElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) } else { - dst.Mul(&e.vars[a], &e.vars[b]) - } - for j := 2; j < len(inst.Inputs); j++ { - if k := inst.Inputs[j]; k < e.gate.NbConstants() { - dst.MulByElement(dst, &e.consts[k]) - } else { - dst.Mul(dst, &e.vars[k]) - } + dst.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) } {{- else }} dst.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + {{- end }} for j := 2; j < len(inst.Inputs); j++ { dst.Mul(dst, &e.vars[inst.Inputs[j]]) } - {{- end }} case gkrcore.OpSub: + {{- if .Mixed }} + if e.isInputConstant(inst, 0) { + dst.SubFromElement(&e.consts[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } else { + dst.Sub(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + {{- else }} dst.Sub(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + {{- end }} for j := 2; j < len(inst.Inputs); j++ { dst.Sub(dst, &e.vars[inst.Inputs[j]]) } @@ -140,21 +153,27 @@ func (e *{{ $ge }}) Evaluate(top ...{{ $elem }}) *{{ $elem }} { case gkrcore.OpMulAcc: var prod {{ $elem }} {{- if .Mixed }} - if b, c := inst.Inputs[1], inst.Inputs[2]; c < e.gate.NbConstants() { - prod.MulByElement(&e.vars[b], &e.consts[c]) - } else if b < e.gate.NbConstants() { - prod.MulByElement(&e.vars[c], &e.consts[b]) + if e.isInputConstant(inst, 0) { + prod.MulByElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) } else { - prod.Mul(&e.vars[b], &e.vars[c]) + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) } {{- else }} - prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) {{- end }} - dst.Add(&e.vars[inst.Inputs[0]], &prod) + dst.Add(&prod, &e.vars[inst.Inputs[2]]) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 var sum {{ $elem }} + {{- if .Mixed }} + if e.isInputConstant(inst, 0) { + sum.AddElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) + } else { + sum.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + {{- else }} sum.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + {{- end }} sum.Add(&sum, &e.vars[inst.Inputs[2]]) dst.Mul(&sum, &sum) // x² dst.Mul(dst, dst) // x⁴ diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index 356d627896..f26e8a8a4d 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -14,6 +14,7 @@ import ( "{{ .FieldPackagePath }}" "{{ .FieldPackagePath }}/polynomial" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl index 270da81e24..88c103b5e9 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl @@ -4,6 +4,7 @@ import ( "github.com/consensys/bavard" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/rational" "github.com/consensys/gnark/internal/rational/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -17,7 +18,7 @@ import ( // The properties of test gates are expected to be the same across all relevant fields. // We can therefore use the gate testing functions for any curve rather than reimplementing // for rational. -var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) +var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) func GenerateVectors() error { testDirPath, err := filepath.Abs("../../gkr/test_vectors") diff --git a/internal/gkr/gkr_test.go b/internal/gkr/gkr_test.go index e2603677af..6e30eab524 100644 --- a/internal/gkr/gkr_test.go +++ b/internal/gkr/gkr_test.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/std/hash" @@ -354,4 +355,4 @@ func TestConstHash(t *testing.T) { ) } -var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) +var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) diff --git a/internal/gkr/gkrcore/gate.go b/internal/gkr/gkrcore/gate.go index b27bb9927d..213d2b3d6b 100644 --- a/internal/gkr/gkrcore/gate.go +++ b/internal/gkr/gkrcore/gate.go @@ -19,8 +19,9 @@ const ( OpSub // result = src1 - src2 - ... OpMul // result = src1 * src2 * ... OpNeg // result = -src1 - OpMulAcc // result = src1 + (src2 * src3) + _ // retired: the multiply-accumulate that read its addend first OpSumExp17 // result = (src1 + src2 + src3)^17 + OpMulAcc // result = (src1 * src2) + src3 ) // GateInstruction represents a single operation in a compiled gate. @@ -78,8 +79,8 @@ func (g *GateBytecode) EstimateDegree(nbIn int) int { for _, in := range inst.Inputs { curr += deg[in] } - case OpMulAcc: // a + b*c - curr = max(deg[inst.Inputs[0]], deg[inst.Inputs[1]]+deg[inst.Inputs[2]]) + case OpMulAcc: // a*b + c + curr = max(deg[inst.Inputs[0]]+deg[inst.Inputs[1]], deg[inst.Inputs[2]]) default: panic("unknown operation") } @@ -157,14 +158,19 @@ func (gc *gateCompiler) addInstruction2Plus(op GateOp, i1, i2 frontend.Variable, return gc.addInstruction(op, ins...) } -// Add records an addition operation. +// Add records an addition operation. Its constant operands are summed into one, placed first. An +// addition of constants alone is itself a constant. func (gc *gateCompiler) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { - return gc.addInstruction2Plus(OpAdd, i1, i2, in...) + return gc.recordCommutative(OpAdd, new(big.Int), (*big.Int).Add, i1, i2, in) } -// MulAcc records a multiply-accumulate operation: a + (b * c) +// MulAcc records a multiply-accumulate operation: a + (b * c). The instruction reads the +// multiplicands first and the addend last, with a constant multiplicand before the other. func (gc *gateCompiler) MulAcc(a, b, c frontend.Variable) frontend.Variable { - return gc.addInstruction(OpMulAcc, a, b, c) + if _, ok := constantValue(c); ok { + b, c = c, b + } + return gc.addInstruction(OpMulAcc, b, c, a) } // Neg records a negation operation @@ -172,19 +178,89 @@ func (gc *gateCompiler) Neg(i1 frontend.Variable) frontend.Variable { return gc.addInstruction(OpNeg, i1) } -// Sub records a subtraction operation +// Sub records a subtraction operation. A constant minuend absorbs the constant subtrahends; +// otherwise they are summed into one, placed last. A subtraction of constants alone is itself a +// constant. func (gc *gateCompiler) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { - return gc.addInstruction2Plus(OpSub, i1, i2, in...) + operands := append([]frontend.Variable{i1, i2}, in...) + minuend, minuendIsConst := constantValue(operands[0]) + + sum := new(big.Int) // of the constant subtrahends + nbKept := 1 + for _, v := range operands[1:] { + if c, ok := constantValue(v); ok { + sum.Add(sum, c) + } else { + operands[nbKept] = v + nbKept++ + } + } + hasConst := nbKept < len(operands) + operands = operands[:nbKept] + + if minuendIsConst { + diff := minuend.Sub(minuend, sum) + if nbKept == 1 { + return diff + } + operands[0] = diff + } else if hasConst { + operands = append(operands, sum) + } + return gc.addInstruction(OpSub, operands...) } -// Mul records a multiplication operation +// Mul records a multiplication operation. Its constant operands are multiplied into one, placed +// first. A multiplication of constants alone is itself a constant. func (gc *gateCompiler) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { - return gc.addInstruction2Plus(OpMul, i1, i2, in...) + return gc.recordCommutative(OpMul, big.NewInt(1), (*big.Int).Mul, i1, i2, in) +} + +// recordCommutative records op over the operands i1, i2, in, with the constants among them folded +// into one by combine, starting from identity, and placed first. If all operands are constants, it +// returns the folded constant. +func (gc *gateCompiler) recordCommutative(op GateOp, identity *big.Int, combine func(z, x, y *big.Int) *big.Int, i1, i2 frontend.Variable, in []frontend.Variable) frontend.Variable { + folded := identity + hasConst := false + var vars []frontend.Variable + for _, v := range append([]frontend.Variable{i1, i2}, in...) { + if c, ok := constantValue(v); ok { + folded = combine(new(big.Int), folded, c) + hasConst = true + } else { + vars = append(vars, v) + } + } + if len(vars) == 0 { + return folded + } + if hasConst { + vars = append(vars, vars[0]) + vars[0] = folded + } + return gc.addInstruction(op, vars...) } -// SumExp17 records (a + b + c)^17 as a single instruction +// SumExp17 records (a + b + c)^17 as a single instruction. If any of a, b, c is a constant, the +// first one found is read first. Constants are not merged. func (gc *gateCompiler) SumExp17(a, b, c frontend.Variable) frontend.Variable { - return gc.addInstruction(OpSumExp17, a, b, c) + operands := []frontend.Variable{a, b, c} + for i, v := range operands { + if _, ok := constantValue(v); ok { + operands[0], operands[i] = operands[i], operands[0] + break + } + } + return gc.addInstruction(OpSumExp17, operands...) +} + +// constantValue returns v's value if v is a constant, that is, not a variable of the gate. +func constantValue(v frontend.Variable) (*big.Int, bool) { + if _, ok := v.(compilationVar); ok { + return nil, false + } + val := utils.FromInterface(v) + return &val, true } // getVarID extracts or creates a temporary index from a value. @@ -282,6 +358,17 @@ func CompileGateFunction(f gkr.GateFunction, nbInputs int, field Field) (Seriali lastEffectiveInstructionIndex := int(outVar.id) - compiler.nbInputs compiler.instructions = compiler.instructions[:lastEffectiveInstructionIndex+1] + // Drop the constants used only by pruned instructions. + nbUsedConstants := 0 + for _, inst := range compiler.instructions { + for _, in := range inst.Inputs { + if in&constMarker != 0 { + nbUsedConstants = max(nbUsedConstants, int(in&^constMarker)+1) + } + } + } + compiler.constants = compiler.constants[:nbUsedConstants] + // Remap indices from temporary layout to final layout compiler.remapIndices() @@ -315,27 +402,12 @@ func CompileGateFunction(f gkr.GateFunction, nbInputs int, field Field) (Seriali }, nil } -// Field describes F_p[X]/(f), with f = Xⁿ + MinPoly[n-1]·Xⁿ⁻¹ + … + MinPoly[0] monic and -// irreducible over F_p, n = len(MinPoly). Coefficients are in increasing degree: X is [0], -// X² + 1 is [1, 0]. -type Field struct { - Modulus *big.Int - MinPoly []*big.Int -} - -// PrimeField describes F_p itself, as F_p[X]/(X). -func PrimeField(p *big.Int) Field { - return Field{Modulus: p, MinPoly: []*big.Int{big.NewInt(0)}} -} - -// degree returns n, the extension degree of the field over F_p. -func (f Field) degree() int { - return len(f.MinPoly) -} +// Field describes F_p[X]/(f); see gkr.Field. +type Field = gkr.Field // gateTester evaluates gate bytecode over a field F_p[X]/(f), to discover a gate's degree and -// solvable variable. An element is a []*big.Int of length field.degree(), coefficient i the -// coefficient of Xⁱ, each reduced mod p. For PrimeField, degree() is 1, and every operation below +// solvable variable. An element is a []*big.Int of length field.Degree(), coefficient i the +// coefficient of Xⁱ, each reduced mod p. For gkr.PrimeField, Degree() is 1, and every operation below // reduces to arithmetic mod p. type gateTester struct { field Field @@ -357,7 +429,7 @@ func (t *gateTester) setGate(g GateBytecode, nbIn int) { // embed returns c as the field element [c, 0, …, 0]. func (t *gateTester) embed(c *big.Int) []*big.Int { - res := make([]*big.Int, t.field.degree()) + res := make([]*big.Int, t.field.Degree()) res[0] = new(big.Int).Mod(c, t.field.Modulus) for i := 1; i < len(res); i++ { res[i] = new(big.Int) @@ -421,7 +493,7 @@ func (t *gateTester) neg(a []*big.Int) []*big.Int { // mul multiplies a and b as polynomials, then reduces the product mod f, then mod p. func (t *gateTester) mul(a, b []*big.Int) []*big.Int { - n := t.field.degree() + n := t.field.Degree() prod := make([]*big.Int, 2*n-1) for i := range prod { prod[i] = new(big.Int) @@ -467,7 +539,7 @@ func (t *gateTester) pow(a []*big.Int, e *big.Int) []*big.Int { // inverse returns a⁻¹ = a^(pⁿ⁻²), which requires f irreducible. func (t *gateTester) inverse(a []*big.Int) []*big.Int { if t.invExponent == nil { - n := big.NewInt(int64(t.field.degree())) + n := big.NewInt(int64(t.field.Degree())) t.invExponent = new(big.Int).Exp(t.field.Modulus, n, nil) t.invExponent.Sub(t.invExponent, big.NewInt(2)) } @@ -479,7 +551,7 @@ func (t *gateTester) div(a, b []*big.Int) []*big.Int { } func (t *gateTester) randomElement() []*big.Int { - res := make([]*big.Int, t.field.degree()) + res := make([]*big.Int, t.field.Degree()) for i := range res { v, err := rand.Int(rand.Reader, t.field.Modulus) if err != nil { @@ -537,9 +609,9 @@ func (t *gateTester) evaluate(inputs ...[]*big.Int) []*big.Int { } case OpNeg: dst = t.neg(t.vars[inst.Inputs[0]]) - case OpMulAcc: // a + b*c - dst = t.mul(t.vars[inst.Inputs[1]], t.vars[inst.Inputs[2]]) - dst = t.add(dst, t.vars[inst.Inputs[0]]) + case OpMulAcc: // a*b + c + dst = t.mul(t.vars[inst.Inputs[0]], t.vars[inst.Inputs[1]]) + dst = t.add(dst, t.vars[inst.Inputs[2]]) case OpSumExp17: // (a + b + c)^17 dst = t.add(t.vars[inst.Inputs[0]], t.vars[inst.Inputs[1]]) dst = t.add(dst, t.vars[inst.Inputs[2]]) diff --git a/internal/gkr/gkrcore/gate_test.go b/internal/gkr/gkrcore/gate_test.go index caa4944831..8613e5a8e0 100644 --- a/internal/gkr/gkrcore/gate_test.go +++ b/internal/gkr/gkrcore/gate_test.go @@ -11,21 +11,12 @@ import ( "github.com/stretchr/testify/require" ) -func bn254() Field { return PrimeField(ecc.BN254.ScalarField()) } - -// koalabearE6 describes KoalaBear's degree-6 extension E6, whose tower generator v has minimal -// polynomial X⁶ - 2X³ - 2. It is test data only here, not a public description of the field. -func koalabearE6() Field { - return Field{ - Modulus: big.NewInt(2130706433), // 2^31 - 2^24 + 1 - MinPoly: []*big.Int{big.NewInt(-2), big.NewInt(0), big.NewInt(0), big.NewInt(-2), big.NewInt(0), big.NewInt(0)}, - } -} +func bn254() Field { return gkr.PrimeField(ecc.BN254.ScalarField()) } // testFields runs under bn254() and under the KoalaBear E6 description, to check that both // agree on every gate's metadata. func testFields() []Field { - return []Field{bn254(), koalabearE6()} + return []Field{bn254(), gkr.KoalaBearE6()} } // test gate functions @@ -107,6 +98,179 @@ func TestConstants(t *testing.T) { } } +// TestConstantsFirst checks the bytecode of gates with constant operands. In the expected +// instructions, constants take the indices [0, len(constants)), then the inputs follow. +func TestConstantsFirst(t *testing.T) { + tests := []struct { + name string + f gkr.GateFunction + nbIn int + constants []int64 + instructions []GateInstruction + }{ + { + "x+(2+3): constants only fold", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Add(in[0], api.Add(2, 3)) + }, + 1, []int64{5}, + []GateInstruction{{OpAdd, []uint16{0, 1}}}, + }, + { + "x+2+y+3: constants merge and come first", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Add(in[0], 2, in[1], 3) + }, + 2, []int64{5}, + []GateInstruction{{OpAdd, []uint16{0, 2, 1}}}, + }, + { + "x*2*y*3: constants merge and come first", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Mul(in[0], 2, in[1], 3) + }, + 2, []int64{6}, + []GateInstruction{{OpMul, []uint16{0, 2, 1}}}, + }, + { + "x*7: constant comes first", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Mul(in[0], 7) + }, + 1, []int64{7}, + []GateInstruction{{OpMul, []uint16{0, 1}}}, + }, + { + "x*((2*3)-1): folding nests", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Mul(in[0], api.Sub(api.Mul(2, 3), 1)) + }, + 1, []int64{5}, + []GateInstruction{{OpMul, []uint16{0, 1}}}, + }, + { + "x+(2-5): folding is exact over the integers", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Add(in[0], api.Sub(2, 5)) + }, + 1, []int64{-3}, + []GateInstruction{{OpAdd, []uint16{0, 1}}}, + }, + { + "10-x-3-y-2: a constant minuend absorbs the constant subtrahends", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Sub(10, in[0], 3, in[1], 2) + }, + 2, []int64{5}, + []GateInstruction{{OpSub, []uint16{0, 1, 2}}}, + }, + { + "x-3-y-2: constant subtrahends merge into one, placed last", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Sub(in[0], 3, in[1], 2) + }, + 2, []int64{5}, + []GateInstruction{{OpSub, []uint16{1, 2, 0}}}, + }, + { + "x-y-4: a single constant subtrahend stays", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Sub(in[0], in[1], 4) + }, + 2, []int64{4}, + []GateInstruction{{OpSub, []uint16{1, 2, 0}}}, + }, + { + "x+y*z: multiplicands first, addend last", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.MulAcc(in[0], in[1], in[2]) + }, + 3, nil, + []GateInstruction{{OpMulAcc, []uint16{1, 2, 0}}}, + }, + { + "x+3*y: a constant multiplicand stays first", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.MulAcc(in[0], 3, in[1]) + }, + 2, []int64{3}, + []GateInstruction{{OpMulAcc, []uint16{0, 2, 1}}}, + }, + { + "x+y*3: a constant multiplicand moves first", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.MulAcc(in[0], in[1], 3) + }, + 2, []int64{3}, + []GateInstruction{{OpMulAcc, []uint16{0, 2, 1}}}, + }, + { + "5+x*y: a constant addend stays last", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.MulAcc(5, in[0], in[1]) + }, + 2, []int64{5}, + []GateInstruction{{OpMulAcc, []uint16{1, 2, 0}}}, + }, + { + "(x+y+z)^17: inputs keep their order", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.SumExp17(in[0], in[1], in[2]) + }, + 3, nil, + []GateInstruction{{OpSumExp17, []uint16{0, 1, 2}}}, + }, + { + "(x+3+y)^17: a constant moves first", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.SumExp17(in[0], 3, in[1]) + }, + 2, []int64{3}, + []GateInstruction{{OpSumExp17, []uint16{0, 1, 2}}}, + }, + { + "(x+y+3)^17: a constant swaps with the first input", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.SumExp17(in[0], in[1], 3) + }, + 2, []int64{3}, + []GateInstruction{{OpSumExp17, []uint16{0, 2, 1}}}, + }, + { + "(x+4+3)^17: constants stay unmerged", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.SumExp17(in[0], 4, 3) + }, + 1, []int64{4, 3}, + []GateInstruction{{OpSumExp17, []uint16{0, 2, 1}}}, + }, + { + "x+y with a constant used only after the output: the constant gets no index", + func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + out := api.Add(in[0], in[1]) + api.Mul(in[0], 9) + return out + }, + 2, nil, + []GateInstruction{{OpAdd, []uint16{0, 1}}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiled, err := CompileGateFunction(tt.f, tt.nbIn, bn254()) + require.NoError(t, err) + + var constants []int64 + for _, c := range compiled.Evaluate.Constants { + constants = append(constants, c.Int64()) + } + assert.Equal(t, tt.constants, constants) + assert.Equal(t, tt.instructions, compiled.Evaluate.Instructions) + }) + } +} + func TestCompileGateFunction(t *testing.T) { tests := []struct { name string @@ -186,7 +350,7 @@ func TestIsAdditive(t *testing.T) { // TestExtensionDefiningRelation checks mul's reduction against KoalaBear's E6 tower generator's // defining relation, X⁶ = 2X³ + 2 (from its minimal polynomial X⁶ - 2X³ - 2). func TestExtensionDefiningRelation(t *testing.T) { - tester := gateTester{field: koalabearE6()} + tester := gateTester{field: gkr.KoalaBearE6()} x := []*big.Int{big.NewInt(0), big.NewInt(1), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0)} x6 := tester.pow(x, big.NewInt(6)) diff --git a/internal/gkr/gkrcore/schedule_test.go b/internal/gkr/gkrcore/schedule_test.go index 47a8ba766b..a199a52119 100644 --- a/internal/gkr/gkrcore/schedule_test.go +++ b/internal/gkr/gkrcore/schedule_test.go @@ -5,12 +5,13 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/require" ) -var scheduleTestCache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) +var scheduleTestCache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) func TestDefaultProvingSchedule(t *testing.T) { _, c := scheduleTestCache.Compile(t, gkrtesting.SingleMulGateCircuit()) diff --git a/internal/gkr/gkrtesting/gkrtesting_test.go b/internal/gkr/gkrtesting/gkrtesting_test.go index 8947bfc1b5..b758c623d9 100644 --- a/internal/gkr/gkrtesting/gkrtesting_test.go +++ b/internal/gkr/gkrtesting/gkrtesting_test.go @@ -4,12 +4,12 @@ import ( "testing" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/gkr" "github.com/stretchr/testify/assert" ) func TestLoadCircuit(t *testing.T) { - cache := NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) + cache := NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) _, c := cache.GetCircuit("../test_vectors/circuits/two_identity_gates_composed_single_input.json") assert.Equal(t, 0, len(c[0].Inputs)) assert.Equal(t, []int{0}, c[1].Inputs) diff --git a/std/gkrapi/compile.go b/std/gkrapi/compile.go index 32c207ab9f..3b96164ea8 100644 --- a/std/gkrapi/compile.go +++ b/std/gkrapi/compile.go @@ -78,7 +78,7 @@ func (api *API) Compile(fiatshamirHashName string, options ...CompileOption) (*C field := compiler.Field() curveID := utils.FieldToCurve(field) - gadgetCircuit, serializableCircuit, err := api.circuit.Compile(gkrcore.PrimeField(field)) + gadgetCircuit, serializableCircuit, err := api.circuit.Compile(gkr.PrimeField(field)) if err != nil { return nil, err } From 6f0bd129bd35fb794da883e82fabb2b8ff53e6e3 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 22:56:42 -0500 Subject: [PATCH 15/37] feat: ommitted from previous commit and koalabear evaluators Signed-off-by: Arya Tabaie --- .../backend/template/gkr/evaluator.go.tmpl | 2 +- internal/gkr/koalabear/evaluator.go | 363 ++++++++++++++++++ internal/gkr/rational/evaluator.go | 4 +- internal/gkr/rational/test_vector_gen.go | 3 +- 4 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 internal/gkr/koalabear/evaluator.go diff --git a/internal/generator/backend/template/gkr/evaluator.go.tmpl b/internal/generator/backend/template/gkr/evaluator.go.tmpl index 4c8a68cdf0..9216ea9420 100644 --- a/internal/generator/backend/template/gkr/evaluator.go.tmpl +++ b/internal/generator/backend/template/gkr/evaluator.go.tmpl @@ -81,7 +81,7 @@ func (e *{{ $ge }}) PushInput(input {{ $elem }}) { e.vars = append(e.vars, input) } -{{- if .Mixed }} +{{ if .Mixed -}} // isInputConstant returns whether the instruction's inputI-th input is one of the gate's constants. func (e *{{ $ge }}) isInputConstant(inst *gkrcore.GateInstruction, inputI int) bool { return int(inst.Inputs[inputI]) < e.gate.NbConstants() diff --git a/internal/gkr/koalabear/evaluator.go b/internal/gkr/koalabear/evaluator.go new file mode 100644 index 0000000000..68e2c79e97 --- /dev/null +++ b/internal/gkr/koalabear/evaluator.go @@ -0,0 +1,363 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "sync" + + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + basePolynomial "github.com/consensys/gnark-crypto/field/koalabear/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" +) + +// GateEvaluator provides a high-level API for evaluating compiled gates efficiently. +// It manages the stack internally and handles input buffering, making it easy to +// evaluate the same gate multiple times with different inputs. +type GateEvaluator struct { + gate gkrcore.GateBytecode + vars []koalabear.Element + nbIn int // number of inputs expected + pool *basePolynomial.Pool // pool vars was allocated from, if any +} + +// NewGateEvaluator creates an evaluator for the given compiled gate. +// The stack is preloaded with constants and ready for evaluation. +func NewGateEvaluator(gate gkrcore.GateBytecode, nbIn int, elementPool ...*basePolynomial.Pool) GateEvaluator { + e := GateEvaluator{ + gate: gate, + nbIn: nbIn, + } + if len(elementPool) > 0 { + e.pool = elementPool[0] + e.vars = e.pool.Make(gate.EvaluatorSize(nbIn)) + } else { + e.vars = make([]koalabear.Element, gate.EvaluatorSize(nbIn)) + } + e.vars = e.vars[:gate.NbConstants()] + for i, constVal := range gate.Constants { + e.vars[i].SetBigInt(constVal) + } + return e +} + +// Dump returns the evaluator's memory to the pool it was allocated from, if any, and does +// nothing otherwise. The evaluator must not be used after this. +func (e *GateEvaluator) Dump() { + if e.pool != nil { + e.pool.Dump(e.vars) + } +} + +// PushInput adds an input to the evaluator's input buffer. +// Inputs must be added in order, and the number of inputs must match the gate's NbInputs. +func (e *GateEvaluator) PushInput(input koalabear.Element) { + e.vars = append(e.vars, input) +} + +// Evaluate adds top to the top of the stack, executes the gate on it and returns the result. +// The stack is automatically reset after evaluation, +// making the evaluator ready for the next evaluation. +// NB! The result is short-lived. It will be overwritten the next time Evaluate is called. +func (e *GateEvaluator) Evaluate(top ...koalabear.Element) *koalabear.Element { + e.vars = append(e.vars, top...) + + if len(e.vars) != e.nbIn+e.gate.NbConstants() { + panic(fmt.Sprintf("expected a stack size of %d representing %d constants and %d inputs, got %d", e.nbIn+e.gate.NbConstants(), e.gate.NbConstants(), e.nbIn, len(e.vars))) + } + + // Execute instructions, appending results to stack + // The stack grows to: [constants | inputs | results] + for i := range e.gate.Instructions { + inst := &e.gate.Instructions[i] + // Grow len by 1 within the pre-allocated capacity + e.vars = e.vars[:len(e.vars)+1] + dst := &e.vars[len(e.vars)-1] + + // Use switch instead of function pointer for better inlining + switch inst.Op { + case gkrcore.OpAdd: + dst.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + for j := 2; j < len(inst.Inputs); j++ { + dst.Add(dst, &e.vars[inst.Inputs[j]]) + } + case gkrcore.OpMul: + dst.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + for j := 2; j < len(inst.Inputs); j++ { + dst.Mul(dst, &e.vars[inst.Inputs[j]]) + } + case gkrcore.OpSub: + dst.Sub(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + for j := 2; j < len(inst.Inputs); j++ { + dst.Sub(dst, &e.vars[inst.Inputs[j]]) + } + case gkrcore.OpNeg: + dst.Neg(&e.vars[inst.Inputs[0]]) + case gkrcore.OpMulAcc: + var prod koalabear.Element + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + dst.Add(&prod, &e.vars[inst.Inputs[2]]) + case gkrcore.OpSumExp17: + // result = (x[0] + x[1] + x[2])^17 + var sum koalabear.Element + sum.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + sum.Add(&sum, &e.vars[inst.Inputs[2]]) + dst.Mul(&sum, &sum) // x² + dst.Mul(dst, dst) // x⁴ + dst.Mul(dst, dst) // x⁸ + dst.Mul(dst, dst) // x¹⁶ + dst.Mul(dst, &sum) // x¹⁷ + default: + panic(fmt.Sprintf("unknown operation: %d", inst.Op)) + } + } + + res := &e.vars[len(e.vars)-1] + + // Reset for next evaluation + e.vars = e.vars[:e.gate.NbConstants()] + + return res +} + +// GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. +// All evaluators share the same underlying polynomial.Pool for element slices. +type GateEvaluatorPool struct { + gate gkrcore.GateBytecode + nbIn int + lock sync.Mutex + available []*GateEvaluator + elementPool *basePolynomial.Pool +} + +func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *basePolynomial.Pool) *GateEvaluatorPool { + return &GateEvaluatorPool{ + gate: gate, + nbIn: nbIn, + elementPool: elementPool, + } +} + +func (gep *GateEvaluatorPool) Get() *GateEvaluator { + gep.lock.Lock() + if n := len(gep.available); n > 0 { + e := gep.available[n-1] + gep.available = gep.available[:n-1] + gep.lock.Unlock() + return e + } + gep.lock.Unlock() + + e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + return &e +} + +func (gep *GateEvaluatorPool) Put(e *GateEvaluator) { + gep.lock.Lock() + gep.available = append(gep.available, e) + gep.lock.Unlock() +} + +// DumpAll dumps all available evaluators' memory back to the pool. It is not to be used after that. +// NB! User must ensure all evaluators have been put back in the pool to prevent memory leaks. +func (gep *GateEvaluatorPool) DumpAll() { + gep.lock.Lock() + defer gep.lock.Unlock() + for _, e := range gep.available { + e.Dump() + } + gep.available = nil +} + +// GateEvaluatorMixed provides a high-level API for evaluating compiled gates efficiently. +// It manages the stack internally and handles input buffering, making it easy to +// evaluate the same gate multiple times with different inputs. +type GateEvaluatorMixed struct { + gate gkrcore.GateBytecode + vars []extensions.E6 + consts []koalabear.Element // the gate's constants in the base field, multiplied by with MulByElement + nbIn int // number of inputs expected + pool *polynomial.PoolE6 // pool vars was allocated from, if any +} + +// NewGateEvaluatorMixed creates an evaluator for the given compiled gate. +// The stack is preloaded with constants and ready for evaluation. +func NewGateEvaluatorMixed(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.PoolE6) GateEvaluatorMixed { + e := GateEvaluatorMixed{ + gate: gate, + nbIn: nbIn, + } + if len(elementPool) > 0 { + e.pool = elementPool[0] + e.vars = e.pool.Make(gate.EvaluatorSize(nbIn)) + } else { + e.vars = make([]extensions.E6, gate.EvaluatorSize(nbIn)) + } + e.vars = e.vars[:gate.NbConstants()] + for i, constVal := range gate.Constants { + e.vars[i].SetBigInt(constVal) + } + e.consts = make([]koalabear.Element, len(gate.Constants)) + for i, constVal := range gate.Constants { + e.consts[i].SetBigInt(constVal) + } + return e +} + +// Dump returns the evaluator's memory to the pool it was allocated from, if any, and does +// nothing otherwise. The evaluator must not be used after this. +func (e *GateEvaluatorMixed) Dump() { + if e.pool != nil { + e.pool.Dump(e.vars) + } +} + +// PushInput adds an input to the evaluator's input buffer. +// Inputs must be added in order, and the number of inputs must match the gate's NbInputs. +func (e *GateEvaluatorMixed) PushInput(input extensions.E6) { + e.vars = append(e.vars, input) +} + +// isInputConstant returns whether the instruction's inputI-th input is one of the gate's constants. +func (e *GateEvaluatorMixed) isInputConstant(inst *gkrcore.GateInstruction, inputI int) bool { + return int(inst.Inputs[inputI]) < e.gate.NbConstants() +} + +// Evaluate adds top to the top of the stack, executes the gate on it and returns the result. +// The stack is automatically reset after evaluation, +// making the evaluator ready for the next evaluation. +// NB! The result is short-lived. It will be overwritten the next time Evaluate is called. +func (e *GateEvaluatorMixed) Evaluate(top ...extensions.E6) *extensions.E6 { + e.vars = append(e.vars, top...) + + if len(e.vars) != e.nbIn+e.gate.NbConstants() { + panic(fmt.Sprintf("expected a stack size of %d representing %d constants and %d inputs, got %d", e.nbIn+e.gate.NbConstants(), e.gate.NbConstants(), e.nbIn, len(e.vars))) + } + + // Execute instructions, appending results to stack + // The stack grows to: [constants | inputs | results] + for i := range e.gate.Instructions { + inst := &e.gate.Instructions[i] + // Grow len by 1 within the pre-allocated capacity + e.vars = e.vars[:len(e.vars)+1] + dst := &e.vars[len(e.vars)-1] + + // Use switch instead of function pointer for better inlining + switch inst.Op { + case gkrcore.OpAdd: + if e.isInputConstant(inst, 0) { + dst.AddElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) + } else { + dst.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + for j := 2; j < len(inst.Inputs); j++ { + dst.Add(dst, &e.vars[inst.Inputs[j]]) + } + case gkrcore.OpMul: + if e.isInputConstant(inst, 0) { + dst.MulByElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) + } else { + dst.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + for j := 2; j < len(inst.Inputs); j++ { + dst.Mul(dst, &e.vars[inst.Inputs[j]]) + } + case gkrcore.OpSub: + if e.isInputConstant(inst, 0) { + dst.SubFromElement(&e.consts[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } else { + dst.Sub(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + for j := 2; j < len(inst.Inputs); j++ { + dst.Sub(dst, &e.vars[inst.Inputs[j]]) + } + case gkrcore.OpNeg: + dst.Neg(&e.vars[inst.Inputs[0]]) + case gkrcore.OpMulAcc: + var prod extensions.E6 + if e.isInputConstant(inst, 0) { + prod.MulByElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) + } else { + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + dst.Add(&prod, &e.vars[inst.Inputs[2]]) + case gkrcore.OpSumExp17: + // result = (x[0] + x[1] + x[2])^17 + var sum extensions.E6 + if e.isInputConstant(inst, 0) { + sum.AddElement(&e.vars[inst.Inputs[1]], &e.consts[inst.Inputs[0]]) + } else { + sum.Add(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + } + sum.Add(&sum, &e.vars[inst.Inputs[2]]) + dst.Mul(&sum, &sum) // x² + dst.Mul(dst, dst) // x⁴ + dst.Mul(dst, dst) // x⁸ + dst.Mul(dst, dst) // x¹⁶ + dst.Mul(dst, &sum) // x¹⁷ + default: + panic(fmt.Sprintf("unknown operation: %d", inst.Op)) + } + } + + res := &e.vars[len(e.vars)-1] + + // Reset for next evaluation + e.vars = e.vars[:e.gate.NbConstants()] + + return res +} + +// GateEvaluatorMixedPool manages a pool of gate evaluators for a specific gate type. +// All evaluators share the same underlying polynomial.Pool for element slices. +type GateEvaluatorMixedPool struct { + gate gkrcore.GateBytecode + nbIn int + lock sync.Mutex + available []*GateEvaluatorMixed + elementPool *polynomial.PoolE6 +} + +func NewGateEvaluatorMixedPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.PoolE6) *GateEvaluatorMixedPool { + return &GateEvaluatorMixedPool{ + gate: gate, + nbIn: nbIn, + elementPool: elementPool, + } +} + +func (gep *GateEvaluatorMixedPool) Get() *GateEvaluatorMixed { + gep.lock.Lock() + if n := len(gep.available); n > 0 { + e := gep.available[n-1] + gep.available = gep.available[:n-1] + gep.lock.Unlock() + return e + } + gep.lock.Unlock() + + e := NewGateEvaluatorMixed(gep.gate, gep.nbIn, gep.elementPool) + return &e +} + +func (gep *GateEvaluatorMixedPool) Put(e *GateEvaluatorMixed) { + gep.lock.Lock() + gep.available = append(gep.available, e) + gep.lock.Unlock() +} + +// DumpAll dumps all available evaluators' memory back to the pool. It is not to be used after that. +// NB! User must ensure all evaluators have been put back in the pool to prevent memory leaks. +func (gep *GateEvaluatorMixedPool) DumpAll() { + gep.lock.Lock() + defer gep.lock.Unlock() + for _, e := range gep.available { + e.Dump() + } + gep.available = nil +} diff --git a/internal/gkr/rational/evaluator.go b/internal/gkr/rational/evaluator.go index b8ed879aab..6f38f6ad87 100644 --- a/internal/gkr/rational/evaluator.go +++ b/internal/gkr/rational/evaluator.go @@ -98,8 +98,8 @@ func (e *GateEvaluator) Evaluate(top ...rational.Element) *rational.Element { dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: var prod rational.Element - prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) - dst.Add(&e.vars[inst.Inputs[0]], &prod) + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + dst.Add(&prod, &e.vars[inst.Inputs[2]]) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 var sum rational.Element diff --git a/internal/gkr/rational/test_vector_gen.go b/internal/gkr/rational/test_vector_gen.go index 4aa3eba6bd..55221fd4fb 100644 --- a/internal/gkr/rational/test_vector_gen.go +++ b/internal/gkr/rational/test_vector_gen.go @@ -16,6 +16,7 @@ import ( "github.com/consensys/bavard" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/rational" @@ -25,7 +26,7 @@ import ( // The properties of test gates are expected to be the same across all relevant fields. // We can therefore use the gate testing functions for any curve rather than reimplementing // for rational. -var cache = gkrtesting.NewCache(gkrcore.PrimeField(ecc.BN254.ScalarField())) +var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) func GenerateVectors() error { testDirPath, err := filepath.Abs("../../gkr/test_vectors") From 07c90807525306205f544e5ba889cc0348a490f9 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 22:57:31 -0500 Subject: [PATCH 16/37] build: codegen for snark gate evals Signed-off-by: Arya Tabaie --- internal/gkr/bls12-377/evaluator.go | 4 ++-- internal/gkr/bls12-381/evaluator.go | 4 ++-- internal/gkr/bn254/evaluator.go | 4 ++-- internal/gkr/bw6-761/evaluator.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/gkr/bls12-377/evaluator.go b/internal/gkr/bls12-377/evaluator.go index 706a0e8012..6412751d15 100644 --- a/internal/gkr/bls12-377/evaluator.go +++ b/internal/gkr/bls12-377/evaluator.go @@ -98,8 +98,8 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: var prod fr.Element - prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) - dst.Add(&e.vars[inst.Inputs[0]], &prod) + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + dst.Add(&prod, &e.vars[inst.Inputs[2]]) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 var sum fr.Element diff --git a/internal/gkr/bls12-381/evaluator.go b/internal/gkr/bls12-381/evaluator.go index eb55f03e2b..ad3192ea03 100644 --- a/internal/gkr/bls12-381/evaluator.go +++ b/internal/gkr/bls12-381/evaluator.go @@ -98,8 +98,8 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: var prod fr.Element - prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) - dst.Add(&e.vars[inst.Inputs[0]], &prod) + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + dst.Add(&prod, &e.vars[inst.Inputs[2]]) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 var sum fr.Element diff --git a/internal/gkr/bn254/evaluator.go b/internal/gkr/bn254/evaluator.go index fef1993135..7615eb8c58 100644 --- a/internal/gkr/bn254/evaluator.go +++ b/internal/gkr/bn254/evaluator.go @@ -98,8 +98,8 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: var prod fr.Element - prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) - dst.Add(&e.vars[inst.Inputs[0]], &prod) + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + dst.Add(&prod, &e.vars[inst.Inputs[2]]) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 var sum fr.Element diff --git a/internal/gkr/bw6-761/evaluator.go b/internal/gkr/bw6-761/evaluator.go index 8ce1563c31..4616942f8c 100644 --- a/internal/gkr/bw6-761/evaluator.go +++ b/internal/gkr/bw6-761/evaluator.go @@ -98,8 +98,8 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: var prod fr.Element - prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) - dst.Add(&e.vars[inst.Inputs[0]], &prod) + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + dst.Add(&prod, &e.vars[inst.Inputs[2]]) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 var sum fr.Element From 81ac6e1ba8b16d972601d8af69c750ff7afc11b2 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 23:07:39 -0500 Subject: [PATCH 17/37] feat: codegen for gkr-koala Signed-off-by: Arya Tabaie --- gkr/koalabear/gkr.go | 399 +++++++++++++ gkr/koalabear/gkr_test.go | 580 +++++++++++++++++++ gkr/koalabear/sumcheck.go | 710 ++++++++++++++++++++++++ gkr/koalabear/sumcheck_test.go | 139 +++++ gkr/koalabear/test_vector_utils_test.go | 149 +++++ 5 files changed, 1977 insertions(+) create mode 100644 gkr/koalabear/gkr.go create mode 100644 gkr/koalabear/gkr_test.go create mode 100644 gkr/koalabear/sumcheck.go create mode 100644 gkr/koalabear/sumcheck_test.go create mode 100644 gkr/koalabear/test_vector_utils_test.go diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go new file mode 100644 index 0000000000..60d9d4f06f --- /dev/null +++ b/gkr/koalabear/gkr.go @@ -0,0 +1,399 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "hash" + "iter" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/internal/gkr/gkrcore" + evaluator "github.com/consensys/gnark/internal/gkr/koalabear" +) + +// Type aliases for bytecode-based GKR types +type ( + Wire = gkrcore.SerializableWire + Circuit = gkrcore.SerializableCircuit +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is the assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLinE6 + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at +// EvaluationPoint. +type EvaluationClaim = gkrcore.EvaluationClaim[extensions.E6] + +// Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by +// wire. The consumption order within a wire's slice is up to the caller. +type Claims map[int][]EvaluationClaim + +// Check asserts that every claim in c holds against assignment. +func (c Claims) Check(assignment WireAssignment) error { + for wI, wireClaims := range c { + for _, claim := range wireClaims { + eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) + if !eval.Equal(&claim.Evaluation) { + return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) + } + } + } + return nil +} + +type resources struct { + // outgoingEvalPoints[i][k] is the k-th outgoing evaluation point (evaluation challenge) produced at schedule level i. + // outgoingEvalPoints[len(schedule)][0] holds the initial challenge (firstChallenge / rho). + // SumcheckLevels produce one point (k=0). SkipLevels pass on all their evaluation points. + outgoingEvalPoints [][][]extensions.E6 + nbVars int + assignment WireAssignment + memPool polynomial.PoolE6 + workers *utils.WorkerPool + circuit Circuit + schedule constraint.GkrProvingSchedule + transcript *transcript + claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof + claims Claims + consolidated []bool // the wires of schedule[0], indexed by wire +} + +// identityGate is the identity gate LevelCircuit and ConsolidationView use to build level 0's +// view of the circuit. +func identityGate() gkrcore.SerializableGate { + return gkrcore.SerializableGate{Evaluate: gkrcore.IdentityBytecode(), NbIn: 1, Degree: 1} +} + +// newResources builds the resources shared by Prove and Verify. It takes no assignment and +// creates no pools; Prove sets those up separately afterward, since Verify needs neither. +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { + return resources{ + outgoingEvalPoints: make([][][]extensions.E6, len(schedule)+1), + nbVars: nbVars, + circuit: c, + schedule: schedule, + transcript: gkrcore.NewHashTranscript[extensions.E6](hasher), + claimValueIndices: c.ClaimValueIndices(schedule), + claims: make(Claims), + consolidated: c.LevelWires(schedule[0]), + } +} + +// proveSkipLevel evaluates each unique gate input at each inherited evaluation point and records +// the outgoing evaluation points on the resources. +func (r *resources) proveSkipLevel(levelI int) sumcheckProof { + level := r.schedule[levelI].(*constraint.GkrSkipLevel) + outPoints := gkrcore.CollectOutgoingEvalPoints(level, levelI, r.outgoingEvalPoints) + + uniqueInputs := r.circuit.UniqueGateInputs(level) + evals := make([]extensions.E6, len(uniqueInputs)*len(outPoints)) + for uiI, inW := range uniqueInputs { + for k, point := range outPoints { + evals[level.FinalEvalProofIndex(uiI, k)] = r.assignment[inW].Evaluate(point, &r.memPool) + } + } + return sumcheckProof{finalEvalProof: evals} +} + +// verifySkipLevel verifies a SkipSumcheck level: checks that the finalEvalProof is consistent with +// the gate evaluations, and records outgoing eval points. +func (r *resources) verifySkipLevel(levelI int, proof Proof) error { + level := r.schedule[levelI].(*constraint.GkrSkipLevel) + gkrcore.CollectOutgoingEvalPoints(level, levelI, r.outgoingEvalPoints) + + finalEval := proof[levelI].finalEvalProof + _, inputIndices := r.circuit.InputMapping(level) + group := constraint.GkrClaimGroup(*level) + + for levelWireI, wI := range group.Wires { + wire := r.circuit[wI] + evaluator := evaluator.NewGateEvaluatorMixed(wire.Gate.Evaluate, len(wire.Inputs)) + for claimI, src := range group.ClaimSources { + for _, inI := range inputIndices[levelWireI] { + evaluator.PushInput(finalEval[level.FinalEvalProofIndex(inI, claimI)]) + } + gateEval := evaluator.Evaluate() + claimedEval := proof[src.Level].finalEvalProof[r.claimValueIndices[wI][claimI]] + if !claimedEval.Equal(gateEval) { + return fmt.Errorf("level %d wire %d claim %d: claimed eval %v disagrees with gate eval %v", levelI, wI, claimI, &claimedEval, gateEval) + } + } + } + return nil +} + +// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. +// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation +// view); every other level binds every unique gate input that is not an unconsolidated circuit +// input, and returns claims for exactly those it withholds from binding. +func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { + if levelI == 0 { + return func(int) bool { return false }, func(int) bool { return true } + } + return func(wI int) bool { return !r.circuit.IsInput(wI) || r.consolidated[wI] }, + func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } +} + +// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +func (r *resources) proveLevel(levelI int) sumcheckProof { + var entry sumcheckProof + switch r.schedule[levelI].(type) { + case *constraint.GkrSkipLevel: + entry = r.proveSkipLevel(levelI) + case *constraint.GkrSingleSourceZeroCheckLevel: + entry = r.proveSingleSourceZeroCheckLevel(levelI) + case *constraint.GkrSumcheckLevel: + entry = r.proveSumcheckLevel(levelI) + default: + panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) + } + bind, include := r.levelPredicates(levelI) + constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) + return entry +} + +// Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's +// inputs and outputs; the caller must check them. The claim values returned to the caller, the +// output evaluations among them, are not bound into the transcript. +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { + nbInstances := assignment.NumInstances() + nbVars := assignment.NumVars() + if nbVars == 0 { + return nil, nil, errors.New("number of variables must be positive") + } + if 1<= 1; levelI-- { + proof[levelI] = r.proveLevel(levelI) + } + r.circuit = r.circuit.LevelCircuit(r.schedule, 0, identityGate()) + proof[0] = r.proveLevel(0) + + return proof, r.claims, nil +} + +// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +func (r *resources) verifyLevel(levelI int, proof Proof) error { + var err error + switch r.schedule[levelI].(type) { + case *constraint.GkrSkipLevel: + err = r.verifySkipLevel(levelI, proof) + case *constraint.GkrSingleSourceZeroCheckLevel: + err = r.verifySingleSourceZeroCheckLevel(levelI, proof) + case *constraint.GkrSumcheckLevel: + err = r.verifySumcheckLevel(levelI, proof) + default: + panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) + } + if err != nil { + return fmt.Errorf("level %d: %v", levelI, err) + } + bind, include := r.levelPredicates(levelI) + constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) + return nil +} + +// Verify the consistency of the claimed output with the claimed input, and return the evaluation +// claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim +// values returned to the caller, the output evaluations among them, are not bound into the +// transcript. +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { + if logNbInstances == 0 { + return nil, errors.New("number of variables must be positive") + } + r := newResources(c, schedule, logNbInstances, hasher) + + if len(proof) != len(schedule)+1 { + return nil, fmt.Errorf("proof has %d levels, expected %d", len(proof), len(schedule)+1) + } + outputLevel := proof[len(schedule)] + if len(outputLevel.partialSumPolys) != 0 { + return nil, errors.New("output level has partial sum polynomials") + } + if len(outputLevel.finalEvalProof) != len(c.Outputs()) { + return nil, fmt.Errorf("output level has %d evaluations, expected %d", len(outputLevel.finalEvalProof), len(c.Outputs())) + } + + view := c.LevelCircuit(schedule, 0, identityGate()) + for levelI, level := range schedule { + levelCircuit := c + if levelI == 0 { + levelCircuit = view + } + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + wantFinalEvalLen := nbUniqueInputs * level.NbOutgoingEvalPoints() + if len(proof[levelI].finalEvalProof) != wantFinalEvalLen { + return nil, fmt.Errorf("level %d: got %d final evaluations, expected %d", levelI, len(proof[levelI].finalEvalProof), wantFinalEvalLen) + } + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + if len(proof[levelI].partialSumPolys) != 0 { + return nil, fmt.Errorf("level %d: skip level has partial sum polynomials", levelI) + } + } else if len(proof[levelI].partialSumPolys) != logNbInstances { + return nil, fmt.Errorf("level %d: got %d partial sum polynomials, expected %d", levelI, len(proof[levelI].partialSumPolys), logNbInstances) + } + } + + // Derive the initial challenge point + firstChallenge := make([]extensions.E6, r.nbVars) + for j := range r.nbVars { + firstChallenge[j] = r.transcript.Challenge() + } + r.outgoingEvalPoints[len(schedule)] = [][]extensions.E6{firstChallenge} + var boundOutputEvals []extensions.E6 + for i, w := range c.Outputs() { + if r.consolidated[w] { + boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + } + } + r.transcript.Bind(boundOutputEvals...) + gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) + + for levelI := len(schedule) - 1; levelI >= 1; levelI-- { + if err := r.verifyLevel(levelI, proof); err != nil { + return nil, err + } + } + r.circuit = view + if err := r.verifyLevel(0, proof); err != nil { + return nil, err + } + return r.claims, nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(circuit Circuit) WireAssignment { + + nbInstances := a.NumInstances() + evaluators := make([]evaluator.GateEvaluatorMixed, len(circuit)) + + for i := range circuit { + if len(a[i]) != nbInstances { + a[i] = make([]extensions.E6, nbInstances) + } + if !circuit[i].IsInput() { + evaluators[i] = evaluator.NewGateEvaluatorMixed(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) + } + } + + for i := range nbInstances { + for wI := range circuit { + if !circuit[wI].IsInput() { + for _, in := range circuit[wI].Inputs { + evaluators[wI].PushInput(a[in][i]) + } + a[wI][i].Set(evaluators[wI].Evaluate()) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + if aW != nil { + return len(aW) + } + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + if aW != nil { + return aW.NumVars() + } + } + panic("empty assignment") +} + +func iterateElems(elems []extensions.E6, counter *int, yield func(int, *extensions.E6) bool) bool { + for i := range elems { + if !yield(*counter, &elems[i]) { + return false + } + *counter++ + } + return true +} + +func (p Proof) Flatten() iter.Seq2[int, *extensions.E6] { + return func(yield func(int, *extensions.E6) bool) { + var counter int + for i := range p { + for _, poly := range p[i].partialSumPolys { + if !iterateElems(poly, &counter, yield) { + return + } + } + if !iterateElems(p[i].finalEvalProof, &counter, yield) { + return + } + } + } +} diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go new file mode 100644 index 0000000000..f92a1595ba --- /dev/null +++ b/gkr/koalabear/gkr_test.go @@ -0,0 +1,580 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + gcHash "github.com/consensys/gnark-crypto/hash" + _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" + "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/stretchr/testify/assert" +) + +var cache = gkrtesting.NewCache(gkr.KoalaBearE6()) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []extensions.E6{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtesting.NoGateCircuit()) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtesting.SingleAddGateCircuit()) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtesting.SingleMulGateCircuit()) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtesting.SingleInputTwoIdentityGatesCircuit()) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtesting.SingleInputTwoIdentityGatesComposedCircuit()) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + test(t, gkrtesting.APowNTimesBCircuit(10)) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtesting.SingleMimcCipherGateCircuit()) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, gkrtesting.MiMCCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, gkrtesting.MiMCCircuit(93)) +} + +func TestPoseidon2(t *testing.T) { + test(t, gkrtesting.Poseidon2Circuit(4, 2)) +} + +// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// schedule[0] has exactly one claim, and all of them share one point; the number of +// elements in the proof, counted with proof.Flatten(), equals c.ProofSize(schedule, logNbInstances); +// tampering instance 0 of any wire with claims makes Check fail; a bad hash makes Verify or Check +// fail; and tampering the first element of any finalEvalProof or partial sum polynomial in the +// proof, the output entry included, makes Verify reject it — except for the claimed evaluation of +// an output wire that is a circuit input outside schedule[0], which nothing in the proof cross-checks: +// there Verify must accept, and only Claims.Check catches the tampering. +func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment) { + t.Helper() + proof, proverClaims, err := Prove(c, schedule, assignment, newMessageCounter(1, 1)) + assert.NoError(t, err) + assert.NoError(t, proverClaims.Check(assignment)) + + logNbInstances := assignment.NumVars() + verifierClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + assert.NoError(t, err, "proof rejected") + assert.Equal(t, proverClaims, verifierClaims) + assert.NoError(t, verifierClaims.Check(assignment)) + + consolidated := c.LevelWires(schedule[0]) + var consolidationPoint []extensions.E6 + for wI, isConsolidated := range consolidated { + if !isConsolidated { + continue + } + if assert.Len(t, verifierClaims[wI], 1, "wire %d: expected exactly one claim", wI) { + if consolidationPoint == nil { + consolidationPoint = verifierClaims[wI][0].EvaluationPoint + } else { + assert.Equal(t, consolidationPoint, verifierClaims[wI][0].EvaluationPoint, "wire %d: expected the shared consolidation point", wI) + } + } + } + + var nbProofElems int + for range proof.Flatten() { + nbProofElems++ + } + assert.Equal(t, c.ProofSize(schedule, logNbInstances), nbProofElems) + + for wI := range verifierClaims { + assignment[wI][0].Add(&assignment[wI][0], &one) + assert.NotNil(t, verifierClaims.Check(assignment), "wire %d: tampered assignment accepted", wI) + assignment[wI][0].Sub(&assignment[wI][0], &one) + } + + badHashClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(0, 1)) + if err == nil { + err = badHashClaims.Check(assignment) + } + assert.NotNil(t, err, "bad proof accepted") + + for levelI := range proof { + tamperTargets := [][]extensions.E6{proof[levelI].finalEvalProof} + for _, p := range proof[levelI].partialSumPolys { + tamperTargets = append(tamperTargets, p) + } + for _, target := range tamperTargets { + if len(target) == 0 { + continue + } + target[0].Add(&target[0], &one) + claims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + if levelI < len(schedule) || !c.IsInput(c.Outputs()[0]) || consolidated[c.Outputs()[0]] { + assert.Error(t, err, "tampered proof accepted at level %d", levelI) + } else { + assert.NoError(t, err, "level %d: Verify should accept a tampered claim it cannot cross-check", levelI) + assert.Error(t, claims.Check(assignment), "level %d: tampered output claim accepted", levelI) + } + target[0].Sub(&target[0], &one) + } + } +} + +var one, two, three, four, five, six extensions.E6 + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +func test(t *testing.T, circuit gkrcore.RawCircuit) { + testWithSchedule(t, circuit, nil) +} + +func testWithSchedule(t *testing.T, circuit gkrcore.RawCircuit, schedule constraint.GkrProvingSchedule) { + gCircuit, sCircuit := cache.Compile(t, circuit) + + schedules := []constraint.GkrProvingSchedule{schedule} + if schedule == nil { + schedules = nil + for _, mode := range []gkrcore.ConsolidationMode{gkrcore.ConsolidateAll, gkrcore.ConsolidateMultiClaimInputsOnly, gkrcore.ConsolidateNone} { + s, err := gkrcore.DefaultProvingSchedule(sCircuit, mode) + assert.NoError(t, err) + schedules = append(schedules, s) + } + } + + ins := gCircuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << gkrtesting.GetLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]extensions.E6, maxSize) + extensions.VectorE6(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(sCircuit) + + for _, s := range schedules { + proveAndVerify(t, sCircuit, s, fullAssignment) + } + } +} + +func testNoGate(t *testing.T, inputAssignments ...[]extensions.E6) { + _, c := cache.Compile(t, gkrtesting.NoGateCircuit()) + + schedule, err := gkrcore.DefaultProvingSchedule(c, gkrcore.ConsolidateNone) + assert.NoError(t, err) + + assignment := WireAssignment{0: inputAssignments[0]} + + proveAndVerify(t, c, schedule, assignment) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, _, err := Prove(testCase.Circuit, testCase.Schedule, testCase.FullAssignment, testCase.Hash) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + + logNbInstances := testCase.InOutAssignment.NumVars() + claims, err := Verify(testCase.Circuit, testCase.Schedule, logNbInstances, testCase.Proof, testCase.Hash) + assert.NoError(t, err, "proof rejected") + assert.NoError(t, claims.Check(testCase.InOutAssignment)) + + claims, err = Verify(testCase.Circuit, testCase.Schedule, logNbInstances, testCase.Proof, newMessageCounter(2, 0)) + if err == nil { + err = claims.Check(testCase.InOutAssignment) + } + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + const testDirPath = "../../internal/gkr/test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + _, c := cache.Compile(b, gkrtesting.MiMCCircuit(mimcDepth)) + + schedule, err := gkrcore.DefaultProvingSchedule(c, gkrcore.ConsolidateNone) + assert.NoError(b, err) + + in0 := make([]extensions.E6, nbInstances) + in1 := make([]extensions.E6, nbInstances) + extensions.VectorE6(in0).MustSetRandom() + extensions.VectorE6(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := make(WireAssignment, len(c)) + assignment[0] = in0 + assignment[1] = in1 + assignment = assignment.Complete(c) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_KOALABEAR.New()) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +// TestSingleMulGateExplicitSchedule tests a single mul gate with an explicit single-step schedule, +// equivalent to the default but constructed manually to exercise the schedule path. +func TestSingleMulGateExplicitSchedule(t *testing.T) { + circuit := gkrtesting.SingleMulGateCircuit() + _, sCircuit := cache.Compile(t, circuit) + + // Wire 2 is the mul gate output (inputs: 0, 1). + // Explicit schedule: an empty level 0 (consolidation), then one GkrProvingLevel for wire 2. + // GkrClaimSource{Level:2} is the initial-challenge sentinel (len(schedule)=2). + schedule := constraint.GkrProvingSchedule{ + &constraint.GkrSkipLevel{}, + &constraint.GkrSumcheckLevel{ + {Wires: []int{2}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}}}, + }, + } + testWithSchedule(t, circuit, schedule) + _ = sCircuit +} + +// TestTwoGroupsExplicitSchedule tests a level with two single-wire claim groups sharing the same +// claim source. A default schedule never produces two groups here: same-degree wires with +// identical claim sources are always merged into one group. +func TestTwoGroupsExplicitSchedule(t *testing.T) { + // Wires 0,1 = inputs; wires 2,3 = mul(0,1), both independent outputs. + circuit := gkrcore.RawCircuit{ + {}, + {}, + {Gate: gkrcore.Mul2, Inputs: []int{0, 1}}, + {Gate: gkrcore.Mul2, Inputs: []int{0, 1}}, + } + // An empty level 0 (consolidation), then one level, two claim groups, covering the whole + // circuit. GkrClaimSource{Level:2} is the initial-challenge sentinel (len(schedule)=2). + schedule := constraint.GkrProvingSchedule{ + &constraint.GkrSkipLevel{}, + &constraint.GkrSumcheckLevel{ + {Wires: []int{3}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}}}, + {Wires: []int{2}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}}}, + }, + } + testWithSchedule(t, circuit, schedule) +} + +// TestMixedGroupsExplicitSchedule tests a level combining a single-wire claim group with a +// multi-wire one, both sharing the same claim source. +func TestMixedGroupsExplicitSchedule(t *testing.T) { + // Wires 0,1 = inputs; wires 2,3,4 = mul(0,1), all independent outputs. + circuit := gkrcore.RawCircuit{ + {}, + {}, + {Gate: gkrcore.Mul2, Inputs: []int{0, 1}}, + {Gate: gkrcore.Mul2, Inputs: []int{0, 1}}, + {Gate: gkrcore.Mul2, Inputs: []int{0, 1}}, + } + // An empty level 0 (consolidation), then one level, two claim groups (one single-wire, one + // two-wire), covering the whole circuit. + schedule := constraint.GkrProvingSchedule{ + &constraint.GkrSkipLevel{}, + &constraint.GkrSumcheckLevel{ + {Wires: []int{4}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}}}, + {Wires: []int{3, 2}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}}}, + }, + } + testWithSchedule(t, circuit, schedule) +} + +// TestConsolidationLevel tests a non-empty level 0: one group consolidating inputs 0 and 1, which +// share a single claim source (the mul gate's level), and a second group consolidating output 2, +// whose only claim source is the output level. +func TestConsolidationLevel(t *testing.T) { + circuit := gkrtesting.SingleMulGateCircuit() + // Wire 2 is the mul gate output (inputs: 0, 1), and the circuit's only output. + // GkrClaimSource{Level:2} is the initial-challenge sentinel (len(schedule)=2). + schedule := constraint.GkrProvingSchedule{ + &constraint.GkrSumcheckLevel{ + {Wires: []int{1, 0}, ClaimSources: []constraint.GkrClaimSource{{Level: 1}}}, + {Wires: []int{2}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}}}, + }, + &constraint.GkrSingleSourceZeroCheckLevel{Wires: []int{2}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}}}, + } + testWithSchedule(t, circuit, schedule) +} + +// TestConsolidationLevelMultiSourceInput tests level 0 consolidating an input with two claim +// sources: wire 0 feeds two separate identity gates, each its own level and its own output. +func TestConsolidationLevelMultiSourceInput(t *testing.T) { + circuit := gkrcore.RawCircuit{ + {}, // wire 0: input + {Gate: gkrcore.Identity, Inputs: []int{0}, Exported: true}, // wire 1 + {Gate: gkrcore.Identity, Inputs: []int{0}, Exported: true}, // wire 2 + } + // GkrClaimSource{Level:3} is the initial-challenge sentinel (len(schedule)=3). + schedule := constraint.GkrProvingSchedule{ + &constraint.GkrSumcheckLevel{ + {Wires: []int{0}, ClaimSources: []constraint.GkrClaimSource{{Level: 2}, {Level: 1}}}, + }, + &constraint.GkrSkipLevel{Wires: []int{1}, ClaimSources: []constraint.GkrClaimSource{{Level: 3}}}, + &constraint.GkrSkipLevel{Wires: []int{2}, ClaimSources: []constraint.GkrClaimSource{{Level: 3}}}, + } + testWithSchedule(t, circuit, schedule) +} + +// TestConsolidationLevelInternalOutput tests level 0 consolidating a non-input output that also +// feeds a downstream gate: wire 2's row is built once, at its own gate level (level 1), whose +// claim sources put the output level first (GkrClaimGroup's order); level 0 must reuse that same row +// rather than rebuild one of its own. +func TestConsolidationLevelInternalOutput(t *testing.T) { + circuit := gkrcore.RawCircuit{ + {}, // wire 0: input + {}, // wire 1: input + {Gate: gkrcore.Mul2, Inputs: []int{0, 1}, Exported: true}, // wire 2: mul(0,1), exported, feeds wire 3 + {Gate: gkrcore.Identity, Inputs: []int{2}}, // wire 3: identity(2), output + } + // GkrClaimSource{Level:3} is the initial-challenge sentinel (len(schedule)=3). + schedule := constraint.GkrProvingSchedule{ + // Level 0: consolidates output 2, whose only claim source for level 0 is the output level. + &constraint.GkrSkipLevel{Wires: []int{2}, ClaimSources: []constraint.GkrClaimSource{{Level: 3}}}, + // Level 1: mul gate output, exported and feeding wire 3 (level 2): the output level comes + // first (GkrClaimGroup's order), then wire 3's level. + &constraint.GkrSumcheckLevel{ + {Wires: []int{2}, ClaimSources: []constraint.GkrClaimSource{{Level: 3}, {Level: 2}}}, + }, + // Level 2: identity(2), an output. + &constraint.GkrSkipLevel{Wires: []int{3}, ClaimSources: []constraint.GkrClaimSource{{Level: 3}}}, + } + testWithSchedule(t, circuit, schedule) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []extensions.E6(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]extensions.E6, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.PolynomialE6, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrcore.SerializableCircuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment + Schedule constraint.GkrProvingSchedule +} + +var testCases = make(map[string]*TestCase) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + var schedule constraint.GkrProvingSchedule + if schedule, err = info.Schedule.ToProvingSchedule(); err != nil { + return nil, err + } + if schedule == nil { + if schedule, err = gkrcore.DefaultProvingSchedule(circuit, gkrcore.SNARKConsolidationMode); err != nil { + return nil, err + } + } + + outputSet := make(map[int]bool, len(circuit)) + for _, o := range circuit.Outputs() { + outputSet[o] = true + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + inI, outI := 0, 0 + for i := range circuit { + var assignmentRaw []interface{} + if circuit[i].IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if outputSet[i] { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []extensions.E6 + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(circuit) + + for i := range circuit { + if outputSet[i] { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + Schedule: schedule, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go new file mode 100644 index 0000000000..ec88ab7336 --- /dev/null +++ b/gkr/koalabear/sumcheck.go @@ -0,0 +1,710 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "sync" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/internal/gkr/gkrcore" + evaluator "github.com/consensys/gnark/internal/gkr/koalabear" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients. + +// transcript specializes gkrcore's generic hash transcript to this field. +type transcript = gkrcore.HashTranscript[extensions.E6, *extensions.E6] + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + roundPolynomial() polynomial.PolynomialE6 // compute gⱼ polynomial for current round + roundFold(r extensions.E6) // fold inputs and eq at challenge r + varsNum() int // number of variables + proveFinalEval(r []extensions.E6) []extensions.E6 // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + varsNum() int // varsNum = n + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []extensions.E6, purportedValue extensions.E6, proof []extensions.E6) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.PolynomialE6 + finalEvalProof []extensions.E6 //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckProve creates a non-interactive sumcheck proof. +// The fold challenge is derived by the caller (proveLevel). +// Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. +func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { + varsNum := claims.varsNum() + proof := sumcheckProof{partialSumPolys: make([]polynomial.PolynomialE6, varsNum)} + proof.partialSumPolys[0] = claims.roundPolynomial() + challenges := make([]extensions.E6, varsNum) + + for j := range varsNum - 1 { + challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + claims.roundFold(challenges[j]) + proof.partialSumPolys[j+1] = claims.roundPolynomial() + } + challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + + proof.finalEvalProof = claims.proveFinalEval(challenges) + return proof +} + +// sumcheckVerify verifies a non-interactive sumcheck proof. +// The fold challenge is derived by the caller (verifyLevel). +// claimedSum is the expected sum; degree is the polynomial's degree in each variable. +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum extensions.E6, degree int, t *transcript) error { + r := make([]extensions.E6, claims.varsNum()) + + gJ := make(polynomial.PolynomialE6, degree+1) + gJR := claimedSum + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != degree { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) + + r[j] = t.Challenge(proof.partialSumPolys[j]...) + gJCoeffs := polynomial.InterpolateOnRangeE6(gJ[:(degree + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, gJR, proof.finalEvalProof) +} + +// zeroCheckLazyClaims is a lazy claim for sumcheck (verifier side). +// It checks that the polynomial ∑ᵢ cⁱ eq(-, xᵢ) wᵢ(-) sums to the expected value, +// where the sum runs over all wᵢ and evaluation point xᵢ in the level. +// Its purpose is to batch the checking of multiple wire evaluations at evaluation points. +type zeroCheckLazyClaims struct { + foldingCoeff extensions.E6 // the coefficient used to fold claims, conventionally 0 if there is only one claim + resources *resources + levelI int +} + +func (e *zeroCheckLazyClaims) varsNum() int { + return e.resources.nbVars +} + +func (e *zeroCheckLazyClaims) degree(int) int { + return e.resources.circuit.ZeroCheckDegree(e.resources.schedule[e.levelI]) +} + +// verifyFinalEval finalizes the verification of a level at the sumcheck evaluation point r. +// The sumcheck protocol has already reduced the per-wire claims w(xᵢ) = yᵢ to verifying +// ∑ᵢ cⁱ eq(xᵢ, r) · wᵢ(r) = purportedValue, where the sum runs over all +// claims on each wire and c is foldingCoeff. +// Both purportedValue and the vector r have been randomized during sumcheck. +// +// The prover claims evaluations of each wire's gate inputs at r via uniqueInputEvaluations; those +// claims are verified by lower levels' sumchecks. The verifier checks consistency by evaluating +// gateᵥ(inputEvals...) and confirming that the full sum matches purportedValue. +func (e *zeroCheckLazyClaims) verifyFinalEval(r []extensions.E6, purportedValue extensions.E6, uniqueInputEvaluations []extensions.E6) error { + e.resources.outgoingEvalPoints[e.levelI] = [][]extensions.E6{r} + level := e.resources.schedule[e.levelI] + gateInputEvals := gkrcore.ReduplicateInputs(level, e.resources.circuit, uniqueInputEvaluations) + + var claimedEvals polynomial.PolynomialE6 + levelWireI := 0 + for _, group := range level.ClaimGroups() { + for _, wI := range group.Wires { + wire := e.resources.circuit[wI] + + evaluator := evaluator.NewGateEvaluatorMixed(wire.Gate.Evaluate, len(wire.Inputs)) + for _, v := range gateInputEvals[levelWireI] { + evaluator.PushInput(v) + } + gateEval := evaluator.Evaluate() + + for _, src := range group.ClaimSources { + eq := polynomial.EvalEqE6(e.resources.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex], r) + var term extensions.E6 + term.Mul(&eq, gateEval) + claimedEvals = append(claimedEvals, term) + } + levelWireI++ + } + } + + if total := claimedEvals.Eval(&e.foldingCoeff); !total.Equal(&purportedValue) { + return errors.New("incompatible evaluations") + } + return nil +} + +// zeroCheckClaims is a claim for sumcheck (prover side). +// It checks that the polynomial ∑ᵢ cⁱ eq(-, xᵢ) wᵢ(-) sums to the expected value, +// where the sum runs over all (wire v, claim source s) pairs in the level. +// Each wire has its own eq table with the batching coefficients baked in. +type zeroCheckClaims struct { + zeroCheckBase + eqs []polynomial.MultiLinE6 // per-wire interpolation bases for evaluating wire assignments at challenge points +} + +// roundPolynomial computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). +// The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). +// By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *zeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { + level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) + degree := c.resources.circuit.ZeroCheckDegree(level) + nbUniqueInputs := len(c.input) + nbWires := len(c.eqs) + + // Both eqs and input are multilinear, thus linear in Xⱼ. + // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly + // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. + // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] + ml := make([]polynomial.MultiLinE6, nbWires+nbUniqueInputs) + copy(ml, c.eqs) + copy(ml[nbWires:], c.input) + + sumSize := len(c.eqs[0]) / 2 + + p := make([]extensions.E6, degree) + var mu sync.Mutex + computeAll := func(start, end int) { + var step extensions.E6 + + evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.gateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]extensions.E6, degree) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) + mlEvals := make([]extensions.E6, degree*len(ml)) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range ml { + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degree; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // start of the current row's eq evaluations + nextEIndex := len(ml) + for d := range degree { + for w := range nbWires { + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + } + summand := evaluators[w].Evaluate() + summand.Mul(summand, &mlEvals[eIndex+w]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + } + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + if sumSize < minBlockSize { + computeAll(0, sumSize) + } else { + c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return p +} + +// roundFold folds all input and eq polynomials at the verifier challenge r. +// After this call, j ← j+1 and rⱼ = r. +func (c *zeroCheckClaims) roundFold(r extensions.E6) { + const minBlockSize = 512 + n := len(c.eqs[0]) / 2 + if n < minBlockSize { + for i := range c.input { + c.input[i].Fold(r) + } + for i := range c.eqs { + c.eqs[i].Fold(r) + } + } else { + wgs := make([]*sync.WaitGroup, len(c.input)+len(c.eqs)) + for i := range c.input { + wgs[i] = c.resources.workers.Submit(n, c.input[i].FoldParallel(r), minBlockSize) + } + for i := range c.eqs { + wgs[len(c.input)+i] = c.resources.workers.Submit(n, c.eqs[i].FoldParallel(r), minBlockSize) + } + for _, wg := range wgs { + wg.Wait() + } + } +} + +// proveFinalEval provides the unique input wire values wᵢ(r₁, ..., rₙ). +func (c *zeroCheckClaims) proveFinalEval(r []extensions.E6) []extensions.E6 { + return c.zeroCheckBase.proveFinalEval(r, c.eqs) +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- m[0] · eq(q, -). +// e <- e + m +func (r *resources) eqAcc(e, m polynomial.MultiLinE6, q []extensions.E6) { + n := len(q) + + // At the end of each iteration, m(h₁, ..., hₙ) = m[0] · eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // 1-based in comments: q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // m(b₁,...,bᵢ,1) = m(b₁,...,bᵢ) · qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // m(b₁,...,bᵢ,0) = m(b₁,...,bᵢ) · (1 - qᵢ₊₁) + } + } else { + r.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // m(b₁,...,bᵢ,1) = m(b₁,...,bᵢ) · qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // m(b₁,...,bᵢ,0) = m(b₁,...,bᵢ) · (1 - qᵢ₊₁) + } + }, 1024).Wait() + } + } + r.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// zeroCheckBase holds the fields and initialization logic common to both +// zeroCheckClaims and singleSourceZeroCheckClaims. +type zeroCheckBase struct { + foldingCoeff extensions.E6 + levelI int + resources *resources + input []polynomial.MultiLinE6 // UniqueGateInputs order + inputIndices [][]int // [wireInLevel][gateInputJ] → index in input + gateEvaluatorPools []*evaluator.GateEvaluatorMixedPool +} + +func (c *zeroCheckBase) varsNum() int { + return c.resources.nbVars +} + +// proveFinalEval records the outgoing eval point, folds the last variable, +// collects the unique input evaluations, and releases all pooled memory. +// extraPolys is the level-type-specific slice (eqs or suffixEq) to dump. +func (c *zeroCheckBase) proveFinalEval(r []extensions.E6, extraPolys []polynomial.MultiLinE6) []extensions.E6 { + c.resources.outgoingEvalPoints[c.levelI] = [][]extensions.E6{r} + evaluations := make([]extensions.E6, len(c.input)) + for i := range c.input { + c.input[i].Fold(r[len(r)-1]) + evaluations[i] = c.input[i][0] + } + for i := range c.input { + c.resources.memPool.Dump(c.input[i]) + } + for i := range extraPolys { + c.resources.memPool.Dump(extraPolys[i]) + } + for _, pool := range c.gateEvaluatorPools { + pool.DumpAll() + } + return evaluations +} + +// init derives the folding coefficient, clones the input multilinears, +// and builds gate evaluator pools — the steps common to all sumcheck-family provers. +func (c *zeroCheckBase) init(r *resources, levelI int) { + c.levelI = levelI + c.resources = r + level := r.schedule[levelI] + if level.NbClaims() >= 2 { + c.foldingCoeff = r.transcript.Challenge() + } + + uniqueInputs, inputIndices := r.circuit.InputMapping(level) + c.input = make([]polynomial.MultiLinE6, len(uniqueInputs)) + for i, inW := range uniqueInputs { + c.input[i] = r.memPool.Clone(r.assignment[inW]) + } + c.inputIndices = inputIndices + + nbWires := 0 + for _, group := range level.ClaimGroups() { + nbWires += len(group.Wires) + } + c.gateEvaluatorPools = make([]*evaluator.GateEvaluatorMixedPool, nbWires) + levelWireI := 0 + for _, group := range level.ClaimGroups() { + for _, wI := range group.Wires { + wire := r.circuit[wI] + c.gateEvaluatorPools[levelWireI] = evaluator.NewGateEvaluatorMixedPool(wire.Gate.Evaluate, len(inputIndices[levelWireI]), &r.memPool) + levelWireI++ + } + } +} + +func (r *resources) proveSumcheckLevel(levelI int) sumcheckProof { + var claims zeroCheckClaims + claims.init(r, levelI) + + level := r.schedule[levelI] + eqLength := 1 << r.nbVars + claims.eqs = make([]polynomial.MultiLinE6, len(claims.gateEvaluatorPools)) + var alpha extensions.E6 + alpha.SetOne() + levelWireI := 0 + for _, group := range level.ClaimGroups() { + nbSources := len(group.ClaimSources) + + groupEq := polynomial.MultiLinE6(r.memPool.Make(eqLength)) + groupEq[0].Set(&alpha) + groupEq.Eq(r.outgoingEvalPoints[group.ClaimSources[0].Level][group.ClaimSources[0].OutgoingClaimIndex]) + + if nbSources > 1 { + newEq := polynomial.MultiLinE6(r.memPool.Make(eqLength)) + aI := alpha + for k := 1; k < nbSources; k++ { + aI.Mul(&aI, &claims.foldingCoeff) + newEq[0].Set(&aI) + r.eqAcc(groupEq, newEq, r.outgoingEvalPoints[group.ClaimSources[k].Level][group.ClaimSources[k].OutgoingClaimIndex]) + } + r.memPool.Dump(newEq) + } + + var stride extensions.E6 + stride.Set(&claims.foldingCoeff) + for range nbSources - 1 { + stride.Mul(&stride, &claims.foldingCoeff) + } + + claims.eqs[levelWireI] = groupEq + levelWireI++ + alpha.Mul(&alpha, &stride) + + for w := 1; w < len(group.Wires); w++ { + claims.eqs[levelWireI] = polynomial.MultiLinE6(r.memPool.Make(eqLength)) + r.workers.Submit(eqLength, func(start, end int) { + for i := start; i < end; i++ { + claims.eqs[levelWireI][i].Mul(&claims.eqs[levelWireI-1][i], &stride) + } + }, 512).Wait() + levelWireI++ + alpha.Mul(&alpha, &stride) + } + } + + return sumcheckProve(&claims, r.transcript) +} + +// verifyLevelSetup derives the folding coefficient, collects all claimed wire +// evaluations from the proof or the initial assignment, computes the batched +// claimed sum, and builds the lazy-claims object shared by both verifiers. +func (r *resources) verifyLevelSetup(levelI int, proof Proof) (extensions.E6, *zeroCheckLazyClaims) { + level := r.schedule[levelI] + var foldingCoeff extensions.E6 + if level.NbClaims() >= 2 { + foldingCoeff = r.transcript.Challenge() + } + + claimedEvals := make(polynomial.PolynomialE6, 0, level.NbClaims()) + for _, group := range level.ClaimGroups() { + for _, wI := range group.Wires { + for claimI, src := range group.ClaimSources { + claimedEvals = append(claimedEvals, proof[src.Level].finalEvalProof[r.claimValueIndices[wI][claimI]]) + } + } + } + + return claimedEvals.Eval(&foldingCoeff), &zeroCheckLazyClaims{ + foldingCoeff: foldingCoeff, + resources: r, + levelI: levelI, + } +} + +func (r *resources) verifySumcheckLevel(levelI int, proof Proof) error { + claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) + level := r.schedule[levelI].(*constraint.GkrSumcheckLevel) + return sumcheckVerify(lazyClaims, proof[levelI], claimedSum, r.circuit.ZeroCheckDegree(level), r.transcript) +} + +// singleSourceZeroCheckClaims is the prover-side claim for a single-source +// zerocheck with the eq polynomial factored out (Section 3.2 of ePrint 2024/108). +// +// When a level has exactly one claim source with evaluation point q, the standard +// round polynomial g_j(X) = Σ_h eq(q, (r₀…r_{j-1}, X, h)) · G(r₀…r_{j-1}, X, h) +// has degree d+1 (one from eq, d from the gate). Factoring out the per-variable eq +// gives g_j(X) = eq(q_j, X) · g'_j(X) where g'_j has degree d. The prover sends +// d evaluations per round instead of d+1, saving one field element per variable and +// reducing the number of gate evaluations per hypercube point from d+1 to d. +// +// The suffix eq tables eq(q_{j+1}…q_{N-1}; ·) are precomputed bottom-up and used +// read-only as weights in each round; only the input multilinears are folded. +type singleSourceZeroCheckClaims struct { + zeroCheckBase + suffixEq polynomial.MultiLinE6 // contiguous buffer; second half is the current round's eq segment +} + +// roundPolynomial computes g'_j(1), ..., g'_j(d) where d is the gate degree. +// g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) +// The wire contributions are accumulated via Horner's method in α (= foldingCoeff), +// eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. +func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { + level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) + degree := c.resources.circuit.ZeroCheckDegree(level) + nbUniqueInputs := len(c.input) + nbWires := len(c.gateEvaluatorPools) + + sumSize := len(c.input[0]) / 2 + + // The second half of suffixEq is the eq segment for this round + eqSegment := c.suffixEq[len(c.suffixEq)/2:] + + p := make([]extensions.E6, degree) + var mu sync.Mutex + computeAll := func(start, end int) { + var step extensions.E6 + + evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.gateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]extensions.E6, degree) + + // Input evaluations at m=1,2,...,degree + inputEvals := make([]extensions.E6, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) + } + } + + iIndex := 0 + nextIIndex := nbUniqueInputs + for d := range degree { + // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) + for _, inputI := range c.inputIndices[nbWires-1] { + evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + } + var wireSum extensions.E6 + wireSum.Set(evaluators[nbWires-1].Evaluate()) + for w := nbWires - 2; w >= 0; w-- { + wireSum.Mul(&wireSum, &c.foldingCoeff) + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[iIndex+inputI]) + } + wireSum.Add(&wireSum, evaluators[w].Evaluate()) + } + + wireSum.Mul(&wireSum, &eqSegment[h]) + res[d].Add(&res[d], &wireSum) + iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs + } + } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() + } + + const minBlockSize = 64 + if sumSize < minBlockSize { + computeAll(0, sumSize) + } else { + c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return p +} + +// roundFold folds only input multilinears at the verifier challenge r. +// The suffix eq tables are precomputed and not folded. +func (c *singleSourceZeroCheckClaims) roundFold(r extensions.E6) { + const minBlockSize = 512 + n := len(c.input[0]) / 2 + if n < minBlockSize { + for i := range c.input { + c.input[i].Fold(r) + } + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := range c.input { + wgs[i] = c.resources.workers.Submit(n, c.input[i].FoldParallel(r), minBlockSize) + } + for _, wg := range wgs { + wg.Wait() + } + } + if len(c.suffixEq) > 2 { + c.suffixEq = c.suffixEq[:len(c.suffixEq)/2] + } +} + +// proveFinalEval provides the unique input wire values at the final evaluation point. +func (c *singleSourceZeroCheckClaims) proveFinalEval(r []extensions.E6) []extensions.E6 { + return c.zeroCheckBase.proveFinalEval(r, []polynomial.MultiLinE6{c.suffixEq}) +} + +// buildSuffixEq constructs suffix eq tables in a single contiguous buffer of length 2^N. +// The layout packs tables smallest-first, left to right: +// +// buf[2:4] = suffixEq[N-1] = eq(q_{N-1}; ·) (length 2) +// buf[4:8] = suffixEq[N-2] = eq(q_{N-2}, q_{N-1}; ·) (length 4) +// ... +// buf[2^(N-1):2^N] = suffixEq[1] = eq(q_1, ..., q_{N-1}; ·) (length 2^(N-1)) +// +// The first two positions are unused padding so that the buffer halves cleanly: +// in round j the eq segment is buf[len(buf)/2:] and buf is trimmed to buf[:len(buf)/2]. +func (r *resources) buildSuffixEq(q []extensions.E6) polynomial.MultiLinE6 { + n := len(q) + buf := r.memPool.Make(1 << n) + + // buf[1] = 1 so the last round's eqSegment = buf[1:2] yields eqWeight = 1, + // eliminating the need for a lastRound conditional in roundPolynomial. + if len(buf) > 1 { + buf[1].SetOne() + } + + if n < 2 { + return buf + } + + // Seed: suffixEq[N-1] = [1-q_{N-1}, q_{N-1}] at buf[2:4] + buf[2].SetOne() + buf[2].Sub(&buf[2], &q[n-1]) + buf[3].Set(&q[n-1]) + + // Each suffixEq[j] is built by tensoring [1-q_j, q_j] with suffixEq[j+1]. + // suffixEq[j+1] sits at buf[srcStart : srcStart+prevLen], + // suffixEq[j] goes to buf[dstStart : dstStart+2*prevLen]. + for j := n - 2; j >= 1; j-- { + prevLen := 1 << (n - j - 1) + srcStart := prevLen // start of suffixEq[j+1] + dstStart := prevLen << 1 // start of suffixEq[j] + + var oneMinusQj extensions.E6 + oneMinusQj.SetOne() + oneMinusQj.Sub(&oneMinusQj, &q[j]) + + r.workers.Submit(prevLen, func(start, end int) { + for h := start; h < end; h++ { + buf[dstStart+h].Mul(&oneMinusQj, &buf[srcStart+h]) + buf[dstStart+prevLen+h].Mul(&q[j], &buf[srcStart+h]) + } + }, 512).Wait() + } + return buf +} + +func (r *resources) proveSingleSourceZeroCheckLevel(levelI int) sumcheckProof { + level := r.schedule[levelI].(*constraint.GkrSingleSourceZeroCheckLevel) + + var claims singleSourceZeroCheckClaims + claims.init(r, levelI) + + // Build suffix eq table from the single claim source's eval point + src := level.ClaimSources[0] + q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] + claims.suffixEq = r.buildSuffixEq(q) + + return sumcheckProve(&claims, r.transcript) +} + +func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) error { + claimedSum, lazyClaims := r.verifyLevelSetup(levelI, proof) + + level := r.schedule[levelI].(*constraint.GkrSingleSourceZeroCheckLevel) + src := level.ClaimSources[0] + q := r.outgoingEvalPoints[src.Level][src.OutgoingClaimIndex] + degree := r.circuit.ZeroCheckDegree(level) + + challenges := make([]extensions.E6, r.nbVars) + gPrime := make(polynomial.PolynomialE6, degree+1) + + // The verifier tracks claimedSum = g'_{j-1}(r_{j-1}), the "primed" value with + // the per-variable eq factors already absent. This is correct because the + // full round polynomial is g_j(X) = eq(q_j, X) · g'_j(X), where + // g'_j(X) = Σ_h suffixEq[j+1](h) · G(r₀…r_{j-1}, X, h), and + // + // g'_{j-1}(r_{j-1}) = Σ_{h_j ∈ {0,1}} eq(q_j, h_j) · g'_j(h_j) + // = (1-q_j)·g'_j(0) + q_j·g'_j(1) + // + // so the recovery formula g'_j(0) = (claimedSum - q_j·g'_j(1)) / (1-q_j) + // applies directly without dividing out accumulated prefix eq factors. + // After interpolating and evaluating g'_j(r_j), the update is simply + // claimedSum ← g'_j(r_j), carrying no eq baggage into the next round. + for j := range r.nbVars { + partialPoly := proof[levelI].partialSumPolys[j] + if len(partialPoly) != degree { + return errors.New("malformed proof") + } + + var oneMinusQj, qjTimesGPrime1 extensions.E6 + oneMinusQj.SetOne() + oneMinusQj.Sub(&oneMinusQj, &q[j]) + qjTimesGPrime1.Mul(&q[j], &partialPoly[0]) // partialPoly[0] = g'(1) + gPrime[0].Sub(&claimedSum, &qjTimesGPrime1) + gPrime[0].Div(&gPrime[0], &oneMinusQj) + + copy(gPrime[1:], partialPoly) + + challenges[j] = r.transcript.Challenge(partialPoly...) + gPrimeCoeffs := polynomial.InterpolateOnRangeE6(gPrime[:(degree + 1)]) + claimedSum = gPrimeCoeffs.Eval(&challenges[j]) + } + + // claimedSum is now Σ_w c^w · gate_w(inputs(r)), without the eq factor. + // verifyFinalEval expects Σ_w c^w · eq(q, r) · gate_w(inputs(r)), so multiply. + eqAtQR := polynomial.EvalEqE6(q, challenges) + claimedSum.Mul(&claimedSum, &eqAtQR) + + return lazyClaims.verifyFinalEval(challenges, claimedSum, proof[levelI].finalEvalProof) +} diff --git a/gkr/koalabear/sumcheck_test.go b/gkr/koalabear/sumcheck_test.go new file mode 100644 index 0000000000..ef929cbe9a --- /dev/null +++ b/gkr/koalabear/sumcheck_test.go @@ -0,0 +1,139 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLinE6, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + t := gkrcore.NewHashTranscript[extensions.E6](hashGenerator()) + + proof := sumcheckProve(&claim, t) + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + t = gkrcore.NewHashTranscript[extensions.E6](hashGenerator()) + if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + t = gkrcore.NewHashTranscript[extensions.E6](hashGenerator()) + if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLinE6 +} + +func (c *singleMultilinClaim) proveFinalEval(r []extensions.E6) []extensions.E6 { + return nil // verifier can compute the final eval itself +} + +func (c *singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func sumForX1One(g polynomial.MultiLinE6) polynomial.PolynomialE6 { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []extensions.E6{sum} +} + +func (c *singleMultilinClaim) roundPolynomial() polynomial.PolynomialE6 { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) roundFold(r extensions.E6) { + c.g.Fold(r) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLinE6 + claimedSum extensions.E6 +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []extensions.E6, purportedValue extensions.E6, proof []extensions.E6) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) degree(int) int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go new file mode 100644 index 0000000000..e91c9e4a4f --- /dev/null +++ b/gkr/koalabear/test_vector_utils_test.go @@ -0,0 +1,149 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "math/big" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *extensions.E6 { + var res extensions.E6 + res.SetInt64(i) + return &res +} + +// hashFromDescription returns the hash a test vector names. The only type is "const": a +// messageCounter with step 0, so every challenge equals the given value regardless of what was +// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and +// end to end by the std/gkrapi tests, which use real hashes. +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +// messageCounterBlockSize is the length of an element's Marshal(). +const messageCounterBlockSize = extensions.BytesE6 + +// messageCounter is a stand-in hash whose state depends only on the number of field-element +// blocks written to it, not on their values. +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res extensions.E6 + res.SetInt64(int64(resI)) + return (&res).Marshal() +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return messageCounterBlockSize +} + +func (m *messageCounter) BlockSize() int { + return messageCounterBlockSize +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. +func setElement(z *extensions.E6, value interface{}) (*extensions.E6, error) { + var r big.Rat + switch v := value.(type) { + case string: + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) + } + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) + } + + var denom extensions.E6 + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil +} + +func sliceToElementSlice[T any](slice []T) ([]extensions.E6, error) { + elementSlice := make([]extensions.E6, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []extensions.E6, b []extensions.E6) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.PolynomialE6, b []polynomial.PolynomialE6) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} From d78e6890a4ebb0598db0783c83c64f234ffed833 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 23:27:23 -0500 Subject: [PATCH 18/37] refactor: assignments are base field Signed-off-by: Arya Tabaie --- gkr/koalabear/gkr.go | 30 +++++++--- gkr/koalabear/gkr_test.go | 32 ++++++----- gkr/koalabear/sumcheck.go | 6 +- gkr/koalabear/test_vector_utils_test.go | 50 ++++++++++++++++ .../backend/template/gkr/gkr.go.tmpl | 46 +++++++++++++-- .../backend/template/gkr/gkr.test.go.tmpl | 32 +++++++---- .../template/gkr/gkr.test.vectors.go.tmpl | 12 +++- .../backend/template/gkr/sumcheck.go.tmpl | 8 +++ .../template/gkr/test_vector_utils.go.tmpl | 57 ++++++++++++------- 9 files changed, 210 insertions(+), 63 deletions(-) diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index 60d9d4f06f..83990c33c6 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -11,8 +11,10 @@ import ( "hash" "iter" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + basePolynomial "github.com/consensys/gnark-crypto/field/koalabear/polynomial" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -28,7 +30,7 @@ type ( // The goal is to prove/verify evaluations of many instances of the same circuit // WireAssignment is the assignment of values to the same wire across many instances of the circuit -type WireAssignment []polynomial.MultiLinE6 +type WireAssignment []basePolynomial.MultiLin type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) @@ -42,9 +44,10 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { + var scratch polynomial.MultiLinE6 for wI, wireClaims := range c { for _, claim := range wireClaims { - eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) + eval := scratch.EvaluateBase(assignment[wI], claim.EvaluationPoint) if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) } @@ -70,6 +73,15 @@ type resources struct { consolidated []bool // the wires of schedule[0], indexed by wire } +// evaluateBase returns the evaluation of the multilinear extension of the base field column col at +// point. +func (r *resources) evaluateBase(col basePolynomial.MultiLin, point []extensions.E6) extensions.E6 { + buf := polynomial.MultiLinE6(r.memPool.Make(len(col) / 2)) + eval := buf.EvaluateBase(col, point) + r.memPool.Dump(buf) + return eval +} + // identityGate is the identity gate LevelCircuit and ConsolidationView use to build level 0's // view of the circuit. func identityGate() gkrcore.SerializableGate { @@ -101,7 +113,7 @@ func (r *resources) proveSkipLevel(levelI int) sumcheckProof { evals := make([]extensions.E6, len(uniqueInputs)*len(outPoints)) for uiI, inW := range uniqueInputs { for k, point := range outPoints { - evals[level.FinalEvalProofIndex(uiI, k)] = r.assignment[inW].Evaluate(point, &r.memPool) + evals[level.FinalEvalProofIndex(uiI, k)] = r.evaluateBase(r.assignment[inW], point) } } return sumcheckProof{finalEvalProof: evals} @@ -187,9 +199,9 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss for wireI := range c { if len(assignment[wireI]) == 0 { const minBlockSize = 64 - assignment[wireI] = make([]extensions.E6, nbInstances) + assignment[wireI] = make([]koalabear.Element, nbInstances) r.workers.Submit(nbInstances, func(start, end int) { - gateEval := evaluator.NewGateEvaluatorMixed(c[wireI].Gate.Evaluate, len(c[wireI].Inputs), &r.memPool) + gateEval := evaluator.NewGateEvaluator(c[wireI].Gate.Evaluate, len(c[wireI].Inputs)) for instanceI := start; instanceI < end; instanceI++ { for _, inputWireI := range c[wireI].Inputs { gateEval.PushInput(assignment[inputWireI][instanceI]) @@ -213,7 +225,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputs := c.Outputs() outputEvals := make([]extensions.E6, len(outputs)) for i, w := range outputs { - outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) + outputEvals[i] = r.evaluateBase(r.assignment[w], firstChallenge) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} var boundOutputEvals []extensions.E6 @@ -329,14 +341,14 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in func (a WireAssignment) Complete(circuit Circuit) WireAssignment { nbInstances := a.NumInstances() - evaluators := make([]evaluator.GateEvaluatorMixed, len(circuit)) + evaluators := make([]evaluator.GateEvaluator, len(circuit)) for i := range circuit { if len(a[i]) != nbInstances { - a[i] = make([]extensions.E6, nbInstances) + a[i] = make([]koalabear.Element, nbInstances) } if !circuit[i].IsInput() { - evaluators[i] = evaluator.NewGateEvaluatorMixed(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) + evaluators[i] = evaluator.NewGateEvaluator(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) } } diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go index f92a1595ba..a8c8a43ac1 100644 --- a/gkr/koalabear/gkr_test.go +++ b/gkr/koalabear/gkr_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" gcHash "github.com/consensys/gnark-crypto/hash" @@ -29,7 +30,7 @@ var cache = gkrtesting.NewCache(gkr.KoalaBearE6()) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case - testNoGate(t, []extensions.E6{four, three}) + testNoGate(t, []koalabear.Element{four, three}) } func TestNoGate(t *testing.T) { @@ -134,7 +135,7 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched if len(target) == 0 { continue } - target[0].Add(&target[0], &one) + target[0].Add(&target[0], &oneExt) claims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) if levelI < len(schedule) || !c.IsInput(c.Outputs()[0]) || consolidated[c.Outputs()[0]] { assert.Error(t, err, "tampered proof accepted at level %d", levelI) @@ -142,14 +143,17 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err, "level %d: Verify should accept a tampered claim it cannot cross-check", levelI) assert.Error(t, claims.Check(assignment), "level %d: tampered output claim accepted", levelI) } - target[0].Sub(&target[0], &one) + target[0].Sub(&target[0], &oneExt) } } } -var one, two, three, four, five, six extensions.E6 +var one, two, three, four, five, six koalabear.Element + +var oneExt extensions.E6 func init() { + oneExt.SetOne() one.SetOne() two.Double(&one) three.Add(&two, &one) @@ -180,8 +184,8 @@ func testWithSchedule(t *testing.T, circuit gkrcore.RawCircuit, schedule constra maxSize := 1 << gkrtesting.GetLogMaxInstances(t) for i := range ins { - insAssignment[i] = make([]extensions.E6, maxSize) - extensions.VectorE6(insAssignment[i]).MustSetRandom() + insAssignment[i] = make([]koalabear.Element, maxSize) + koalabear.Vector(insAssignment[i]).MustSetRandom() } fullAssignment := make(WireAssignment, len(circuit)) @@ -198,7 +202,7 @@ func testWithSchedule(t *testing.T, circuit gkrcore.RawCircuit, schedule constra } } -func testNoGate(t *testing.T, inputAssignments ...[]extensions.E6) { +func testNoGate(t *testing.T, inputAssignments ...[]koalabear.Element) { _, c := cache.Compile(t, gkrtesting.NoGateCircuit()) schedule, err := gkrcore.DefaultProvingSchedule(c, gkrcore.ConsolidateNone) @@ -286,10 +290,10 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { schedule, err := gkrcore.DefaultProvingSchedule(c, gkrcore.ConsolidateNone) assert.NoError(b, err) - in0 := make([]extensions.E6, nbInstances) - in1 := make([]extensions.E6, nbInstances) - extensions.VectorE6(in0).MustSetRandom() - extensions.VectorE6(in1).MustSetRandom() + in0 := make([]koalabear.Element, nbInstances) + in1 := make([]koalabear.Element, nbInstances) + koalabear.Vector(in0).MustSetRandom() + koalabear.Vector(in1).MustSetRandom() fmt.Println("evaluating circuit") start := time.Now().UnixMicro() @@ -545,8 +549,8 @@ func newTestCase(path string) (*TestCase, error) { outI++ } if assignmentRaw != nil { - var wireAssignment []extensions.E6 - if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + var wireAssignment []koalabear.Element + if wireAssignment, err = sliceToBaseElementSlice(assignmentRaw); err != nil { return nil, err } @@ -559,7 +563,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = baseSliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index ec88ab7336..2dc37fe255 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -360,7 +360,11 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { uniqueInputs, inputIndices := r.circuit.InputMapping(level) c.input = make([]polynomial.MultiLinE6, len(uniqueInputs)) for i, inW := range uniqueInputs { - c.input[i] = r.memPool.Clone(r.assignment[inW]) + column := r.assignment[inW] + c.input[i] = polynomial.MultiLinE6(r.memPool.Make(len(column))) + for j := range column { + c.input[i][j].SetElement(&column[j]) + } } c.inputIndices = inputIndices diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index e91c9e4a4f..3bae467128 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -10,6 +10,7 @@ import ( "hash" "math/big" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -136,6 +137,55 @@ func sliceEquals(a []extensions.E6, b []extensions.E6) error { return nil } +// setBaseElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. +func setBaseElement(z *koalabear.Element, value interface{}) (*koalabear.Element, error) { + var r big.Rat + switch v := value.(type) { + case string: + if _, ok := r.SetString(v); !ok { + return nil, fmt.Errorf("cannot parse %q", v) + } + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + r.SetFloat64(v) + default: + return nil, fmt.Errorf("cannot parse value of type %T", value) + } + + var denom koalabear.Element + z.SetBigInt(r.Num()) + denom.SetBigInt(r.Denom()) + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil +} + +func sliceToBaseElementSlice[T any](slice []T) ([]koalabear.Element, error) { + elementSlice := make([]koalabear.Element, len(slice)) + for i, v := range slice { + if _, err := setBaseElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func baseSliceEquals(a []koalabear.Element, b []koalabear.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + func polynomialSliceEquals(a []polynomial.PolynomialE6, b []polynomial.PolynomialE6) error { if len(a) != len(b) { return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index 82e74e39ab..66927d946a 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -7,6 +7,10 @@ import ( "{{.FieldPackagePath}}" "{{.FieldPackagePath}}/polynomial" + {{- if .Mixed }} + "{{ .BaseFieldPackagePath }}" + basePolynomial "{{ .BaseFieldPackagePath }}/polynomial" + {{- end }} "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" {{- if .EvaluatorQualifier }} @@ -24,7 +28,7 @@ type ( // The goal is to prove/verify evaluations of many instances of the same circuit // WireAssignment is the assignment of values to the same wire across many instances of the circuit -type WireAssignment []polynomial.MultiLin{{ .ExtensionSuffix }} +type WireAssignment []{{ .BasePolynomial }}.MultiLin type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) @@ -38,9 +42,16 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { + {{- if .Mixed }} + var scratch polynomial.MultiLin{{ .ExtensionSuffix }} + {{- end }} for wI, wireClaims := range c { for _, claim := range wireClaims { + {{- if .Mixed }} + eval := scratch.EvaluateBase(assignment[wI], claim.EvaluationPoint) + {{- else }} eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) + {{- end }} if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) } @@ -66,6 +77,17 @@ type resources struct { consolidated []bool // the wires of schedule[0], indexed by wire } +{{ if .Mixed -}} +// evaluateBase returns the evaluation of the multilinear extension of the base field column col at +// point. +func (r *resources) evaluateBase(col {{ .BasePolynomial }}.MultiLin, point []{{ .ElementType }}) {{ .ElementType }} { + buf := polynomial.MultiLin{{ .ExtensionSuffix }}(r.memPool.Make(len(col) / 2)) + eval := buf.EvaluateBase(col, point) + r.memPool.Dump(buf) + return eval +} + +{{ end -}} // identityGate is the identity gate LevelCircuit and ConsolidationView use to build level 0's // view of the circuit. func identityGate() gkrcore.SerializableGate { @@ -97,7 +119,11 @@ func (r *resources) proveSkipLevel(levelI int) sumcheckProof { evals := make([]{{ .ElementType }}, len(uniqueInputs)*len(outPoints)) for uiI, inW := range uniqueInputs { for k, point := range outPoints { + {{- if .Mixed }} + evals[level.FinalEvalProofIndex(uiI, k)] = r.evaluateBase(r.assignment[inW], point) + {{- else }} evals[level.FinalEvalProofIndex(uiI, k)] = r.assignment[inW].Evaluate(point, &r.memPool) + {{- end }} } } return sumcheckProof{finalEvalProof: evals} @@ -183,9 +209,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss for wireI := range c { if len(assignment[wireI]) == 0 { const minBlockSize = 64 - assignment[wireI] = make([]{{ .ElementType }}, nbInstances) + assignment[wireI] = make([]{{ .BaseElementType }}, nbInstances) r.workers.Submit(nbInstances, func(start, end int) { - gateEval := {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}(c[wireI].Gate.Evaluate, len(c[wireI].Inputs), &r.memPool) + {{- if .Mixed }} + gateEval := {{ .EvaluatorQualifier }}NewGateEvaluator(c[wireI].Gate.Evaluate, len(c[wireI].Inputs)) + {{- else }} + gateEval := {{ .EvaluatorQualifier }}NewGateEvaluator(c[wireI].Gate.Evaluate, len(c[wireI].Inputs), &r.memPool) + {{- end }} for instanceI := start; instanceI < end; instanceI++ { for _, inputWireI := range c[wireI].Inputs { gateEval.PushInput(assignment[inputWireI][instanceI]) @@ -209,7 +239,11 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputs := c.Outputs() outputEvals := make([]{{ .ElementType }}, len(outputs)) for i, w := range outputs { + {{- if .Mixed }} + outputEvals[i] = r.evaluateBase(r.assignment[w], firstChallenge) + {{- else }} outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) + {{- end }} } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} var boundOutputEvals []{{ .ElementType }} @@ -325,14 +359,14 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in func (a WireAssignment) Complete(circuit Circuit) WireAssignment { nbInstances := a.NumInstances() - evaluators := make([]{{ .EvaluatorQualifier }}{{ $gateEvaluator }}, len(circuit)) + evaluators := make([]{{ .EvaluatorQualifier }}GateEvaluator, len(circuit)) for i := range circuit { if len(a[i]) != nbInstances { - a[i] = make([]{{ .ElementType }}, nbInstances) + a[i] = make([]{{ .BaseElementType }}, nbInstances) } if !circuit[i].IsInput() { - evaluators[i] = {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) + evaluators[i] = {{ .EvaluatorQualifier }}NewGateEvaluator(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) } } diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index f26e8a8a4d..d07961e114 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -13,6 +13,9 @@ import ( _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "{{ .FieldPackagePath }}" "{{ .FieldPackagePath }}/polynomial" + {{- if .Mixed }} + "{{ .BaseFieldPackagePath }}" + {{- end }} "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -24,7 +27,7 @@ var cache = gkrtesting.NewCache({{ .FieldDescription }}) func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case - testNoGate(t, []{{ .ElementType }}{four, three}) + testNoGate(t, []{{ .BaseElementType }}{four, three}) } func TestNoGate(t *testing.T) { @@ -129,7 +132,7 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched if len(target) == 0 { continue } - target[0].Add(&target[0], &one) + target[0].Add(&target[0], &{{ if .Mixed }}oneExt{{ else }}one{{ end }}) claims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) if levelI < len(schedule) || !c.IsInput(c.Outputs()[0]) || consolidated[c.Outputs()[0]] { assert.Error(t, err, "tampered proof accepted at level %d", levelI) @@ -137,14 +140,21 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err, "level %d: Verify should accept a tampered claim it cannot cross-check", levelI) assert.Error(t, claims.Check(assignment), "level %d: tampered output claim accepted", levelI) } - target[0].Sub(&target[0], &one) + target[0].Sub(&target[0], &{{ if .Mixed }}oneExt{{ else }}one{{ end }}) } } } -var one, two, three, four, five, six {{ .ElementType }} +var one, two, three, four, five, six {{ .BaseElementType }} +{{- if .Mixed }} + +var oneExt {{ .ElementType }} +{{- end }} func init() { + {{- if .Mixed }} + oneExt.SetOne() + {{- end }} one.SetOne() two.Double(&one) three.Add(&two, &one) @@ -175,8 +185,8 @@ func testWithSchedule(t *testing.T, circuit gkrcore.RawCircuit, schedule constra maxSize := 1 << gkrtesting.GetLogMaxInstances(t) for i := range ins { - insAssignment[i] = make([]{{ .ElementType }}, maxSize) - {{ .FieldPackageName }}.Vector{{ .ExtensionSuffix }}(insAssignment[i]).MustSetRandom() + insAssignment[i] = make([]{{ .BaseElementType }}, maxSize) + {{ .BaseFieldPackageName }}.Vector(insAssignment[i]).MustSetRandom() } fullAssignment := make(WireAssignment, len(circuit)) @@ -193,7 +203,7 @@ func testWithSchedule(t *testing.T, circuit gkrcore.RawCircuit, schedule constra } } -func testNoGate(t *testing.T, inputAssignments ...[]{{ .ElementType }}) { +func testNoGate(t *testing.T, inputAssignments ...[]{{ .BaseElementType }}) { _, c := cache.Compile(t, gkrtesting.NoGateCircuit()) schedule, err := gkrcore.DefaultProvingSchedule(c, gkrcore.ConsolidateNone) @@ -281,10 +291,10 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { schedule, err := gkrcore.DefaultProvingSchedule(c, gkrcore.ConsolidateNone) assert.NoError(b, err) - in0 := make([]{{ .ElementType }}, nbInstances) - in1 := make([]{{ .ElementType }}, nbInstances) - {{ .FieldPackageName }}.Vector{{ .ExtensionSuffix }}(in0).MustSetRandom() - {{ .FieldPackageName }}.Vector{{ .ExtensionSuffix }}(in1).MustSetRandom() + in0 := make([]{{ .BaseElementType }}, nbInstances) + in1 := make([]{{ .BaseElementType }}, nbInstances) + {{ .BaseFieldPackageName }}.Vector(in0).MustSetRandom() + {{ .BaseFieldPackageName }}.Vector(in1).MustSetRandom() fmt.Println("evaluating circuit") start := time.Now().UnixMicro() diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl index 2f0f2af634..b383b1dee2 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl @@ -1,4 +1,10 @@ {{ define "gkrTestVectors" }} +{{- $sliceToBase := "sliceToElementSlice" }} +{{- $baseEquals := "sliceEquals" }} +{{- if .Mixed }} +{{- $sliceToBase = "sliceToBaseElementSlice" }} +{{- $baseEquals = "baseSliceEquals" }} +{{- end }} func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { proof := make(Proof, len(printable)) @@ -103,8 +109,8 @@ func newTestCase(path string) (*TestCase, error) { outI++ } if assignmentRaw != nil { - var wireAssignment []{{ .ElementType }} - if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + var wireAssignment []{{ .BaseElementType }} + if wireAssignment, err = {{ $sliceToBase }}(assignmentRaw); err != nil { return nil, err } @@ -117,7 +123,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = {{ $baseEquals }}(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index bee8a089d1..bf231be895 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -356,7 +356,15 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { uniqueInputs, inputIndices := r.circuit.InputMapping(level) c.input = make([]polynomial.MultiLin{{ .ExtensionSuffix }}, len(uniqueInputs)) for i, inW := range uniqueInputs { + {{- if .Mixed }} + column := r.assignment[inW] + c.input[i] = polynomial.MultiLin{{ .ExtensionSuffix }}(r.memPool.Make(len(column))) + for j := range column { + c.input[i][j].SetElement(&column[j]) + } + {{- else }} c.input[i] = r.memPool.Clone(r.assignment[inW]) + {{- end }} } c.inputIndices = inputIndices diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 706d937fe3..51f8b5a04d 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -4,6 +4,9 @@ import ( "math/big" "{{.FieldPackagePath}}" "{{.FieldPackagePath}}/polynomial" + {{- if .Mixed }} + "{{ .BaseFieldPackagePath }}" + {{- end }} "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -79,9 +82,36 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { } } -// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +{{ template "elementHelpers" dict "Config" . "Base" false }} +{{- if .Mixed }} + +{{ template "elementHelpers" dict "Config" . "Base" true }} +{{- end }} + +func polynomialSliceEquals(a []polynomial.Polynomial{{ .ExtensionSuffix }}, b []polynomial.Polynomial{{ .ExtensionSuffix }}) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i],b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} + +{{ define "elementHelpers" }} +{{- $elem := .Config.ElementType }} +{{- $name := "Element" }} +{{- $equals := "sliceEquals" }} +{{- if .Base }} +{{- $elem = .Config.BaseElementType }} +{{- $name = "BaseElement" }} +{{- $equals = "baseSliceEquals" }} +{{- end }} +// set{{ $name }} parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, // then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *{{.ElementType}}, value interface{}) (*{{.ElementType}}, error) { +func set{{ $name }}(z *{{ $elem }}, value interface{}) (*{{ $elem }}, error) { var r big.Rat switch v := value.(type) { case string: @@ -98,7 +128,7 @@ func setElement(z *{{.ElementType}}, value interface{}) (*{{.ElementType}}, erro return nil, fmt.Errorf("cannot parse value of type %T", value) } - var denom {{.ElementType}} + var denom {{ $elem }} z.SetBigInt(r.Num()) denom.SetBigInt(r.Denom()) denom.Inverse(&denom) @@ -106,17 +136,17 @@ func setElement(z *{{.ElementType}}, value interface{}) (*{{.ElementType}}, erro return z, nil } -func sliceToElementSlice[T any](slice []T) ([]{{.ElementType}}, error) { - elementSlice := make([]{{.ElementType}}, len(slice)) +func sliceTo{{ $name }}Slice[T any](slice []T) ([]{{ $elem }}, error) { + elementSlice := make([]{{ $elem }}, len(slice)) for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { + if _, err := set{{ $name }}(&elementSlice[i], v); err != nil { return nil, err } } return elementSlice, nil } -func sliceEquals(a []{{.ElementType}}, b []{{.ElementType}}) error { +func {{ $equals }}(a []{{ $elem }}, b []{{ $elem }}) error { if len(a) != len(b) { return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) } @@ -127,15 +157,4 @@ func sliceEquals(a []{{.ElementType}}, b []{{.ElementType}}) error { } return nil } - -func polynomialSliceEquals(a []polynomial.Polynomial{{ .ExtensionSuffix }}, b []polynomial.Polynomial{{ .ExtensionSuffix }}) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i],b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} \ No newline at end of file +{{ end }} From 5543ab7fb07e651b2282bc92a48461ffc0ea6716 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 23:39:19 -0500 Subject: [PATCH 19/37] refactor: generify some helper functions Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr_test.go | 12 +- gkr/bls12-377/test_vector_utils_test.go | 63 ---------- gkr/bls12-381/gkr_test.go | 12 +- gkr/bls12-381/test_vector_utils_test.go | 63 ---------- gkr/bn254/gkr_test.go | 12 +- gkr/bn254/test_vector_utils_test.go | 63 ---------- gkr/bw6-761/gkr_test.go | 12 +- gkr/bw6-761/test_vector_utils_test.go | 63 ---------- gkr/koalabear/gkr_test.go | 12 +- gkr/koalabear/test_vector_utils_test.go | 113 ------------------ .../backend/template/gkr/gkr.test.go.tmpl | 4 +- .../template/gkr/gkr.test.vectors.go.tmpl | 14 +-- .../gkr/sumcheck.test.vectors.gen.go.tmpl | 4 +- .../template/gkr/test_vector_utils.go.tmpl | 82 ------------- internal/gkr/gkrtesting/elements.go | 90 ++++++++++++++ .../gkr/rational/sumcheck_test_vector_gen.go | 4 +- internal/gkr/rational/test_vector_gen.go | 8 +- internal/gkr/rational/test_vector_utils.go | 63 ---------- 18 files changed, 134 insertions(+), 560 deletions(-) create mode 100644 internal/gkr/gkrtesting/elements.go diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index 751f7d899e..2480f108e9 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -269,11 +269,11 @@ func proofEquals(expected Proof, seen Proof) error { return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) } } else { - if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + if err := gkrtesting.SliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { return fmt.Errorf("final evaluation proof mismatch") } } - if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + if err := gkrtesting.PolynomialSliceEquals(gkrtesting.ToSlices(x.partialSumPolys), gkrtesting.ToSlices(xSeen.partialSumPolys)); err != nil { return err } } @@ -454,7 +454,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + if err := gkrtesting.SetElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -466,7 +466,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } for k := range printable[i].PartialSumPolys { var err error - if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + if proof[i].partialSumPolys[k], err = gkrtesting.SliceToElementSlice[fr.Element](printable[i].PartialSumPolys[k]); err != nil { return nil, err } } @@ -547,7 +547,7 @@ func newTestCase(path string) (*TestCase, error) { } if assignmentRaw != nil { var wireAssignment []fr.Element - if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + if wireAssignment, err = gkrtesting.SliceToElementSlice[fr.Element](assignmentRaw); err != nil { return nil, err } @@ -560,7 +560,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = gkrtesting.SliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index e37168688a..3c150f2b0b 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -8,10 +8,8 @@ package gkr import ( "fmt" "hash" - "math/big" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -86,64 +84,3 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { return newMessageCounter(startState, step) } } - -// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom fr.Element - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { - elementSlice := make([]fr.Element, len(slice)) - for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func sliceEquals(a []fr.Element, b []fr.Element) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} - -func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i], b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index 02c3c8488f..071e02df2c 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -269,11 +269,11 @@ func proofEquals(expected Proof, seen Proof) error { return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) } } else { - if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + if err := gkrtesting.SliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { return fmt.Errorf("final evaluation proof mismatch") } } - if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + if err := gkrtesting.PolynomialSliceEquals(gkrtesting.ToSlices(x.partialSumPolys), gkrtesting.ToSlices(xSeen.partialSumPolys)); err != nil { return err } } @@ -454,7 +454,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + if err := gkrtesting.SetElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -466,7 +466,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } for k := range printable[i].PartialSumPolys { var err error - if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + if proof[i].partialSumPolys[k], err = gkrtesting.SliceToElementSlice[fr.Element](printable[i].PartialSumPolys[k]); err != nil { return nil, err } } @@ -547,7 +547,7 @@ func newTestCase(path string) (*TestCase, error) { } if assignmentRaw != nil { var wireAssignment []fr.Element - if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + if wireAssignment, err = gkrtesting.SliceToElementSlice[fr.Element](assignmentRaw); err != nil { return nil, err } @@ -560,7 +560,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = gkrtesting.SliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index a94e6f4053..abd92c739b 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -8,10 +8,8 @@ package gkr import ( "fmt" "hash" - "math/big" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -86,64 +84,3 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { return newMessageCounter(startState, step) } } - -// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom fr.Element - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { - elementSlice := make([]fr.Element, len(slice)) - for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func sliceEquals(a []fr.Element, b []fr.Element) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} - -func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i], b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index 6284b975c6..5ce63bc6f4 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -269,11 +269,11 @@ func proofEquals(expected Proof, seen Proof) error { return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) } } else { - if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + if err := gkrtesting.SliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { return fmt.Errorf("final evaluation proof mismatch") } } - if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + if err := gkrtesting.PolynomialSliceEquals(gkrtesting.ToSlices(x.partialSumPolys), gkrtesting.ToSlices(xSeen.partialSumPolys)); err != nil { return err } } @@ -454,7 +454,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + if err := gkrtesting.SetElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -466,7 +466,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } for k := range printable[i].PartialSumPolys { var err error - if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + if proof[i].partialSumPolys[k], err = gkrtesting.SliceToElementSlice[fr.Element](printable[i].PartialSumPolys[k]); err != nil { return nil, err } } @@ -547,7 +547,7 @@ func newTestCase(path string) (*TestCase, error) { } if assignmentRaw != nil { var wireAssignment []fr.Element - if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + if wireAssignment, err = gkrtesting.SliceToElementSlice[fr.Element](assignmentRaw); err != nil { return nil, err } @@ -560,7 +560,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = gkrtesting.SliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 4348dd5f48..4b3b58b7cc 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -8,10 +8,8 @@ package gkr import ( "fmt" "hash" - "math/big" "github.com/consensys/gnark-crypto/ecc/bn254/fr" - "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -86,64 +84,3 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { return newMessageCounter(startState, step) } } - -// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom fr.Element - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { - elementSlice := make([]fr.Element, len(slice)) - for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func sliceEquals(a []fr.Element, b []fr.Element) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} - -func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i], b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index 58464bbb49..fb1f3b22c5 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -269,11 +269,11 @@ func proofEquals(expected Proof, seen Proof) error { return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) } } else { - if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + if err := gkrtesting.SliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { return fmt.Errorf("final evaluation proof mismatch") } } - if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + if err := gkrtesting.PolynomialSliceEquals(gkrtesting.ToSlices(x.partialSumPolys), gkrtesting.ToSlices(xSeen.partialSumPolys)); err != nil { return err } } @@ -454,7 +454,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + if err := gkrtesting.SetElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -466,7 +466,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } for k := range printable[i].PartialSumPolys { var err error - if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + if proof[i].partialSumPolys[k], err = gkrtesting.SliceToElementSlice[fr.Element](printable[i].PartialSumPolys[k]); err != nil { return nil, err } } @@ -547,7 +547,7 @@ func newTestCase(path string) (*TestCase, error) { } if assignmentRaw != nil { var wireAssignment []fr.Element - if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + if wireAssignment, err = gkrtesting.SliceToElementSlice[fr.Element](assignmentRaw); err != nil { return nil, err } @@ -560,7 +560,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = gkrtesting.SliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index 9d01aacc6e..05e61315ef 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -8,10 +8,8 @@ package gkr import ( "fmt" "hash" - "math/big" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -86,64 +84,3 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { return newMessageCounter(startState, step) } } - -// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom fr.Element - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { - elementSlice := make([]fr.Element, len(slice)) - for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func sliceEquals(a []fr.Element, b []fr.Element) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} - -func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i], b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go index a8c8a43ac1..58b02d9652 100644 --- a/gkr/koalabear/gkr_test.go +++ b/gkr/koalabear/gkr_test.go @@ -272,11 +272,11 @@ func proofEquals(expected Proof, seen Proof) error { return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) } } else { - if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + if err := gkrtesting.SliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { return fmt.Errorf("final evaluation proof mismatch") } } - if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + if err := gkrtesting.PolynomialSliceEquals(gkrtesting.ToSlices(x.partialSumPolys), gkrtesting.ToSlices(xSeen.partialSumPolys)); err != nil { return err } } @@ -457,7 +457,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]extensions.E6, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + if err := gkrtesting.SetElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -469,7 +469,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } for k := range printable[i].PartialSumPolys { var err error - if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + if proof[i].partialSumPolys[k], err = gkrtesting.SliceToElementSlice[extensions.E6](printable[i].PartialSumPolys[k]); err != nil { return nil, err } } @@ -550,7 +550,7 @@ func newTestCase(path string) (*TestCase, error) { } if assignmentRaw != nil { var wireAssignment []koalabear.Element - if wireAssignment, err = sliceToBaseElementSlice(assignmentRaw); err != nil { + if wireAssignment, err = gkrtesting.SliceToElementSlice[koalabear.Element](assignmentRaw); err != nil { return nil, err } @@ -563,7 +563,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = baseSliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = gkrtesting.SliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index 3bae467128..4f097f0072 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -8,11 +8,8 @@ package gkr import ( "fmt" "hash" - "math/big" - "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" - "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -87,113 +84,3 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { return newMessageCounter(startState, step) } } - -// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *extensions.E6, value interface{}) (*extensions.E6, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom extensions.E6 - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToElementSlice[T any](slice []T) ([]extensions.E6, error) { - elementSlice := make([]extensions.E6, len(slice)) - for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func sliceEquals(a []extensions.E6, b []extensions.E6) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} - -// setBaseElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setBaseElement(z *koalabear.Element, value interface{}) (*koalabear.Element, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom koalabear.Element - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToBaseElementSlice[T any](slice []T) ([]koalabear.Element, error) { - elementSlice := make([]koalabear.Element, len(slice)) - for i, v := range slice { - if _, err := setBaseElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func baseSliceEquals(a []koalabear.Element, b []koalabear.Element) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} - -func polynomialSliceEquals(a []polynomial.PolynomialE6, b []polynomial.PolynomialE6) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i], b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index d07961e114..cf46baf160 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -273,11 +273,11 @@ func proofEquals(expected Proof, seen Proof) error { return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) } } else { - if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + if err := gkrtesting.SliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { return fmt.Errorf("final evaluation proof mismatch") } } - if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + if err := gkrtesting.PolynomialSliceEquals(gkrtesting.ToSlices(x.partialSumPolys), gkrtesting.ToSlices(xSeen.partialSumPolys)); err != nil { return err } } diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl index b383b1dee2..ba5b41b58e 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl @@ -1,10 +1,4 @@ {{ define "gkrTestVectors" }} -{{- $sliceToBase := "sliceToElementSlice" }} -{{- $baseEquals := "sliceEquals" }} -{{- if .Mixed }} -{{- $sliceToBase = "sliceToBaseElementSlice" }} -{{- $baseEquals = "baseSliceEquals" }} -{{- end }} func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { proof := make(Proof, len(printable)) @@ -15,7 +9,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]{{ .ElementType }}, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + if err := gkrtesting.SetElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -27,7 +21,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } for k := range printable[i].PartialSumPolys { var err error - if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + if proof[i].partialSumPolys[k], err = gkrtesting.SliceToElementSlice[{{ .ElementType }}](printable[i].PartialSumPolys[k]); err != nil { return nil, err } } @@ -110,7 +104,7 @@ func newTestCase(path string) (*TestCase, error) { } if assignmentRaw != nil { var wireAssignment []{{ .BaseElementType }} - if wireAssignment, err = {{ $sliceToBase }}(assignmentRaw); err != nil { + if wireAssignment, err = gkrtesting.SliceToElementSlice[{{ .BaseElementType }}](assignmentRaw); err != nil { return nil, err } @@ -123,7 +117,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = {{ $baseEquals }}(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = gkrtesting.SliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl index d94d7b9918..a86882d5c5 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -15,7 +15,7 @@ import ( func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { var poly polynomial.MultiLin - if v, err := sliceToElementSlice(testCaseInfo.Values); err == nil { + if v, err := gkrtesting.SliceToElementSlice[{{ .ElementType }}](testCaseInfo.Values); err == nil { poly = v } else { return err @@ -36,7 +36,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { testCaseInfo.Proof = sumcheckToPrintableProof(proof) // Verification - if v, _err := sliceToElementSlice(testCaseInfo.Values); _err == nil { + if v, _err := gkrtesting.SliceToElementSlice[{{ .ElementType }}](testCaseInfo.Values); _err == nil { poly = v } else { return _err diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 51f8b5a04d..d539df24fb 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -1,12 +1,7 @@ import ( "fmt" "hash" - "math/big" "{{.FieldPackagePath}}" - "{{.FieldPackagePath}}/polynomial" - {{- if .Mixed }} - "{{ .BaseFieldPackagePath }}" - {{- end }} "github.com/consensys/gnark/internal/gkr/gkrtesting" ) @@ -81,80 +76,3 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { return newMessageCounter(startState, step) } } - -{{ template "elementHelpers" dict "Config" . "Base" false }} -{{- if .Mixed }} - -{{ template "elementHelpers" dict "Config" . "Base" true }} -{{- end }} - -func polynomialSliceEquals(a []polynomial.Polynomial{{ .ExtensionSuffix }}, b []polynomial.Polynomial{{ .ExtensionSuffix }}) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i],b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} - -{{ define "elementHelpers" }} -{{- $elem := .Config.ElementType }} -{{- $name := "Element" }} -{{- $equals := "sliceEquals" }} -{{- if .Base }} -{{- $elem = .Config.BaseElementType }} -{{- $name = "BaseElement" }} -{{- $equals = "baseSliceEquals" }} -{{- end }} -// set{{ $name }} parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func set{{ $name }}(z *{{ $elem }}, value interface{}) (*{{ $elem }}, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom {{ $elem }} - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceTo{{ $name }}Slice[T any](slice []T) ([]{{ $elem }}, error) { - elementSlice := make([]{{ $elem }}, len(slice)) - for i, v := range slice { - if _, err := set{{ $name }}(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func {{ $equals }}(a []{{ $elem }}, b []{{ $elem }}) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} -{{ end }} diff --git a/internal/gkr/gkrtesting/elements.go b/internal/gkr/gkrtesting/elements.go new file mode 100644 index 0000000000..3cf6e8f4bc --- /dev/null +++ b/internal/gkr/gkrtesting/elements.go @@ -0,0 +1,90 @@ +package gkrtesting + +import ( + "fmt" + "math/big" +) + +// testElement is the part of a field element's API the test-vector helpers need. +type testElement[E any] interface { + *E + SetBigInt(*big.Int) *E + Inverse(*E) *E + Mul(*E, *E) *E + Equal(*E) bool + String() string +} + +// SetElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, +// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. +func SetElement[E any, PE testElement[E]](z *E, value any) error { + var r big.Rat + switch v := value.(type) { + case string: + if _, ok := r.SetString(v); !ok { + return fmt.Errorf("cannot parse %q", v) + } + case float64: + asInt := int64(v) + if float64(asInt) != v { + return fmt.Errorf("cannot currently parse float") + } + r.SetFloat64(v) + default: + return fmt.Errorf("cannot parse value of type %T", value) + } + + var denom E + pz, pDenom := PE(z), PE(&denom) + pz.SetBigInt(r.Num()) + pDenom.SetBigInt(r.Denom()) + pDenom.Inverse(pDenom) + pz.Mul(pz, pDenom) + return nil +} + +// SliceToElementSlice parses each element of slice with SetElement. +func SliceToElementSlice[E any, PE testElement[E], T any](slice []T) ([]E, error) { + elementSlice := make([]E, len(slice)) + for i, v := range slice { + if err := SetElement[E, PE](&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +// SliceEquals returns an error describing the first difference between a and b, if any. +func SliceEquals[E any, PE testElement[E]](a, b []E) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !PE(&a[i]).Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, PE(&a[i]).String(), PE(&b[i]).String()) + } + } + return nil +} + +// PolynomialSliceEquals returns an error describing the first difference between a and b, if any. +func PolynomialSliceEquals[E any, PE testElement[E]](a, b [][]E) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := SliceEquals[E, PE](a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} + +// ToSlices converts a slice of slices of a named slice type to a slice of []T. +func ToSlices[T any, S ~[]T](slices []S) [][]T { + res := make([][]T, len(slices)) + for i := range slices { + res[i] = slices[i] + } + return res +} diff --git a/internal/gkr/rational/sumcheck_test_vector_gen.go b/internal/gkr/rational/sumcheck_test_vector_gen.go index c79c150a2f..82d4204319 100644 --- a/internal/gkr/rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/rational/sumcheck_test_vector_gen.go @@ -23,7 +23,7 @@ import ( func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { var poly polynomial.MultiLin - if v, err := sliceToElementSlice(testCaseInfo.Values); err == nil { + if v, err := gkrtesting.SliceToElementSlice[rational.Element](testCaseInfo.Values); err == nil { poly = v } else { return err @@ -44,7 +44,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { testCaseInfo.Proof = sumcheckToPrintableProof(proof) // Verification - if v, _err := sliceToElementSlice(testCaseInfo.Values); _err == nil { + if v, _err := gkrtesting.SliceToElementSlice[rational.Element](testCaseInfo.Values); _err == nil { poly = v } else { return _err diff --git a/internal/gkr/rational/test_vector_gen.go b/internal/gkr/rational/test_vector_gen.go index 55221fd4fb..517fb29a2d 100644 --- a/internal/gkr/rational/test_vector_gen.go +++ b/internal/gkr/rational/test_vector_gen.go @@ -179,7 +179,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof = make([]rational.Element, finalEvalSlice.Len()) for k := range finalEvalProof { - if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + if err := gkrtesting.SetElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { return nil, err } } @@ -191,7 +191,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { } for k := range printable[i].PartialSumPolys { var err error - if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + if proof[i].partialSumPolys[k], err = gkrtesting.SliceToElementSlice[rational.Element](printable[i].PartialSumPolys[k]); err != nil { return nil, err } } @@ -273,7 +273,7 @@ func newTestCase(path string) (*TestCase, error) { } if assignmentRaw != nil { var wireAssignment []rational.Element - if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + if wireAssignment, err = gkrtesting.SliceToElementSlice[rational.Element](assignmentRaw); err != nil { return nil, err } @@ -286,7 +286,7 @@ func newTestCase(path string) (*TestCase, error) { for i := range circuit { if outputSet[i] { - if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + if err = gkrtesting.SliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { return nil, fmt.Errorf("assignment mismatch: %v", err) } } diff --git a/internal/gkr/rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go index 8dbb7c680d..7fd545b34c 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -8,11 +8,9 @@ package gkr import ( "fmt" "hash" - "math/big" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/rational" - "github.com/consensys/gnark/internal/rational/polynomial" ) func toElement(i int64) *rational.Element { @@ -86,64 +84,3 @@ func newMessageCounterGenerator(startState, step int) func() hash.Hash { return newMessageCounter(startState, step) } } - -// setElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, -// then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. -func setElement(z *rational.Element, value interface{}) (*rational.Element, error) { - var r big.Rat - switch v := value.(type) { - case string: - if _, ok := r.SetString(v); !ok { - return nil, fmt.Errorf("cannot parse %q", v) - } - case float64: - asInt := int64(v) - if float64(asInt) != v { - return nil, fmt.Errorf("cannot currently parse float") - } - r.SetFloat64(v) - default: - return nil, fmt.Errorf("cannot parse value of type %T", value) - } - - var denom rational.Element - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToElementSlice[T any](slice []T) ([]rational.Element, error) { - elementSlice := make([]rational.Element, len(slice)) - for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func sliceEquals(a []rational.Element, b []rational.Element) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if !a[i].Equal(&b[i]) { - return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) - } - } - return nil -} - -func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { - if len(a) != len(b) { - return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) - } - for i := range a { - if err := sliceEquals(a[i], b[i]); err != nil { - return fmt.Errorf("at index %d: %w", i, err) - } - } - return nil -} From d807a17e95f57bfa45883f96f6ff275f5b02897f Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 5 Oct 2026 23:42:27 -0500 Subject: [PATCH 20/37] refactor: some more generification Signed-off-by: Arya Tabaie --- gkr/bls12-377/sumcheck_test.go | 3 ++- gkr/bls12-377/test_vector_utils_test.go | 6 ------ gkr/bls12-381/sumcheck_test.go | 3 ++- gkr/bls12-381/test_vector_utils_test.go | 6 ------ gkr/bn254/sumcheck_test.go | 3 ++- gkr/bn254/test_vector_utils_test.go | 6 ------ gkr/bw6-761/sumcheck_test.go | 3 ++- gkr/bw6-761/test_vector_utils_test.go | 6 ------ gkr/koalabear/sumcheck_test.go | 3 ++- gkr/koalabear/test_vector_utils_test.go | 6 ------ .../generator/backend/template/gkr/sumcheck.test.go.tmpl | 3 ++- .../template/gkr/sumcheck.test.vectors.gen.go.tmpl | 2 +- .../backend/template/gkr/test_vector_utils.go.tmpl | 6 ------ internal/gkr/gkrtesting/elements.go | 8 ++++++++ internal/gkr/rational/sumcheck_test.go | 3 ++- internal/gkr/rational/sumcheck_test_vector_gen.go | 2 +- internal/gkr/rational/test_vector_utils.go | 6 ------ 17 files changed, 24 insertions(+), 51 deletions(-) diff --git a/gkr/bls12-377/sumcheck_test.go b/gkr/bls12-377/sumcheck_test.go index a5e7c43851..9838ed2dd2 100644 --- a/gkr/bls12-377/sumcheck_test.go +++ b/gkr/bls12-377/sumcheck_test.go @@ -11,6 +11,7 @@ import ( "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" "math/bits" @@ -51,7 +52,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash return err } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index 3c150f2b0b..821aae9797 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -13,12 +13,6 @@ import ( "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} - // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. diff --git a/gkr/bls12-381/sumcheck_test.go b/gkr/bls12-381/sumcheck_test.go index 39d93ee2a0..476a8f26d2 100644 --- a/gkr/bls12-381/sumcheck_test.go +++ b/gkr/bls12-381/sumcheck_test.go @@ -11,6 +11,7 @@ import ( "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" "math/bits" @@ -51,7 +52,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash return err } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index abd92c739b..6cf4564337 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -13,12 +13,6 @@ import ( "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} - // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. diff --git a/gkr/bn254/sumcheck_test.go b/gkr/bn254/sumcheck_test.go index a9ce79ec4a..b6bbfe0ecb 100644 --- a/gkr/bn254/sumcheck_test.go +++ b/gkr/bn254/sumcheck_test.go @@ -11,6 +11,7 @@ import ( "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" "math/bits" @@ -51,7 +52,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash return err } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 4b3b58b7cc..00e9a9344d 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -13,12 +13,6 @@ import ( "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} - // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. diff --git a/gkr/bw6-761/sumcheck_test.go b/gkr/bw6-761/sumcheck_test.go index fa539a23cb..0c30101b84 100644 --- a/gkr/bw6-761/sumcheck_test.go +++ b/gkr/bw6-761/sumcheck_test.go @@ -11,6 +11,7 @@ import ( "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" "math/bits" @@ -51,7 +52,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash return err } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index 05e61315ef..664437952b 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -13,12 +13,6 @@ import ( "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} - // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. diff --git a/gkr/koalabear/sumcheck_test.go b/gkr/koalabear/sumcheck_test.go index ef929cbe9a..0427849edd 100644 --- a/gkr/koalabear/sumcheck_test.go +++ b/gkr/koalabear/sumcheck_test.go @@ -11,6 +11,7 @@ import ( "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" "math/bits" @@ -51,7 +52,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash return err } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[extensions.E6](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} t = gkrcore.NewHashTranscript[extensions.E6](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index 4f097f0072..fbf592e284 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -13,12 +13,6 @@ import ( "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *extensions.E6 { - var res extensions.E6 - res.SetInt64(i) - return &res -} - // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. diff --git a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl index 8da218dd73..9974e0af35 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl @@ -3,6 +3,7 @@ import ( "hash" "{{ .FieldPackagePath }}/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" {{ if not .GenerateTestVectors}} "{{ .FieldPackagePath }}" @@ -42,7 +43,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash return err } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[{{ .ElementType }}](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} t = gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl index a86882d5c5..ef6994c607 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -54,7 +54,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { return fmt.Errorf("proof rejected: %v", err) } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[{{ .ElementType }}](1)) if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index d539df24fb..55a9786e32 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -5,12 +5,6 @@ import ( "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *{{.ElementType}} { - var res {{.ElementType}} - res.SetInt64(i) - return &res -} - // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. diff --git a/internal/gkr/gkrtesting/elements.go b/internal/gkr/gkrtesting/elements.go index 3cf6e8f4bc..3c30b79752 100644 --- a/internal/gkr/gkrtesting/elements.go +++ b/internal/gkr/gkrtesting/elements.go @@ -9,12 +9,20 @@ import ( type testElement[E any] interface { *E SetBigInt(*big.Int) *E + SetInt64(int64) *E Inverse(*E) *E Mul(*E, *E) *E Equal(*E) bool String() string } +// ToElement returns i as an element. +func ToElement[E any, PE testElement[E]](i int64) *E { + var res E + PE(&res).SetInt64(i) + return &res +} + // SetElement parses value — a decimal or "num/den" string, or a JSON number — into a big.Rat, // then sets z to its numerator divided by its denominator, via SetBigInt, Inverse and Mul. func SetElement[E any, PE testElement[E]](z *E, value any) error { diff --git a/internal/gkr/rational/sumcheck_test.go b/internal/gkr/rational/sumcheck_test.go index 60ac5ac522..aa80054e70 100644 --- a/internal/gkr/rational/sumcheck_test.go +++ b/internal/gkr/rational/sumcheck_test.go @@ -10,6 +10,7 @@ import ( "hash" "github.com/consensys/gnark/internal/gkr/gkrcore" + "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/rational" "github.com/consensys/gnark/internal/rational/polynomial" "github.com/stretchr/testify/assert" @@ -48,7 +49,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash return err } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[rational.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} t = gkrcore.NewHashTranscript[rational.Element](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { diff --git a/internal/gkr/rational/sumcheck_test_vector_gen.go b/internal/gkr/rational/sumcheck_test_vector_gen.go index 82d4204319..056b64d385 100644 --- a/internal/gkr/rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/rational/sumcheck_test_vector_gen.go @@ -62,7 +62,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { return fmt.Errorf("proof rejected: %v", err) } - proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[rational.Element](1)) if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } diff --git a/internal/gkr/rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go index 7fd545b34c..b22ea13cd3 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -13,12 +13,6 @@ import ( "github.com/consensys/gnark/internal/rational" ) -func toElement(i int64) *rational.Element { - var res rational.Element - res.SetInt64(i) - return &res -} - // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. From c85dcc7093ebffbcc530298593da6076f3b2969b Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 6 Oct 2026 00:07:12 -0500 Subject: [PATCH 21/37] perf: mixed protocol Signed-off-by: Arya Tabaie --- gkr/koalabear/sumcheck.go | 353 ++++++++++---- .../backend/template/gkr/evaluator.go.tmpl | 6 +- .../backend/template/gkr/sumcheck.go.tmpl | 453 +++++++++++++----- internal/gkr/bls12-377/evaluator.go | 6 +- internal/gkr/bls12-381/evaluator.go | 6 +- internal/gkr/bn254/evaluator.go | 6 +- internal/gkr/bw6-761/evaluator.go | 6 +- internal/gkr/koalabear/evaluator.go | 12 +- internal/gkr/rational/evaluator.go | 6 +- 9 files changed, 621 insertions(+), 233 deletions(-) diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index 2dc37fe255..26bfd66514 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -9,8 +9,10 @@ import ( "errors" "sync" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" + basePolynomial "github.com/consensys/gnark-crypto/field/koalabear/polynomial" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/gkr/gkrcore" evaluator "github.com/consensys/gnark/internal/gkr/koalabear" @@ -178,57 +180,116 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { p := make([]extensions.E6, degree) var mu sync.Mutex - computeAll := func(start, end int) { - var step extensions.E6 + var computeAll func(start, end int) + if c.baseInput != nil { + computeAll = func(start, end int) { + var eqStep extensions.E6 + var step koalabear.Element - evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) - for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() - } - defer func() { + evaluators := make([]*evaluator.GateEvaluator, nbWires) for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) + evaluators[w] = c.baseGateEvaluatorPools[w].Get() } - }() - - res := make([]extensions.E6, degree) - - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]extensions.E6, degree*len(ml)) - - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) - for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + defer func() { + for w := range nbWires { + c.baseGateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]extensions.E6, degree) + var term extensions.E6 + + // Evaluations of the eq tables and of the base field input tables at Xⱼ = 1, 2, ..., degree, + // row by row: eqEvals[d*nbWires+w] and inputEvals[d*nbUniqueInputs+k] are at Xⱼ = d+1. + eqEvals := make([]extensions.E6, degree*nbWires) + inputEvals := make([]koalabear.Element, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) + for d := 1; d < degree; d++ { + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.baseInput[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) + } + } + + for d := range degree { + for w := range nbWires { + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[d*nbUniqueInputs+inputI]) + } + term.MulByElement(&eqEvals[d*nbWires+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) + } } } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() + } + } else { + computeAll = func(start, end int) { + var step extensions.E6 - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) - for d := range degree { + evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { for w := range nbWires { - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + c.gateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]extensions.E6, degree) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) + mlEvals := make([]extensions.E6, degree*len(ml)) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range ml { + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degree; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // start of the current row's eq evaluations + nextEIndex := len(ml) + for d := range degree { + for w := range nbWires { + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + } + summand := evaluators[w].Evaluate() + summand.Mul(summand, &mlEvals[eIndex+w]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) // collect into the complete sum + } + mu.Unlock() } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) // collect into the complete sum - } - mu.Unlock() } const minBlockSize = 64 @@ -248,7 +309,7 @@ func (c *zeroCheckClaims) roundFold(r extensions.E6) { n := len(c.eqs[0]) / 2 if n < minBlockSize { for i := range c.input { - c.input[i].Fold(r) + c.foldInput(i, &r) } for i := range c.eqs { c.eqs[i].Fold(r) @@ -256,7 +317,7 @@ func (c *zeroCheckClaims) roundFold(r extensions.E6) { } else { wgs := make([]*sync.WaitGroup, len(c.input)+len(c.eqs)) for i := range c.input { - wgs[i] = c.resources.workers.Submit(n, c.input[i].FoldParallel(r), minBlockSize) + wgs[i] = c.resources.workers.Submit(n, c.foldInputParallel(i, &r), minBlockSize) } for i := range c.eqs { wgs[len(c.input)+i] = c.resources.workers.Submit(n, c.eqs[i].FoldParallel(r), minBlockSize) @@ -265,6 +326,7 @@ func (c *zeroCheckClaims) roundFold(r extensions.E6) { wg.Wait() } } + c.baseInput = nil } // proveFinalEval provides the unique input wire values wᵢ(r₁, ..., rₙ). @@ -313,18 +375,50 @@ func (r *resources) eqAcc(e, m polynomial.MultiLinE6, q []extensions.E6) { // zeroCheckBase holds the fields and initialization logic common to both // zeroCheckClaims and singleSourceZeroCheckClaims. type zeroCheckBase struct { - foldingCoeff extensions.E6 - levelI int - resources *resources - input []polynomial.MultiLinE6 // UniqueGateInputs order - inputIndices [][]int // [wireInLevel][gateInputJ] → index in input - gateEvaluatorPools []*evaluator.GateEvaluatorMixedPool + foldingCoeff extensions.E6 + levelI int + resources *resources + input []polynomial.MultiLinE6 // UniqueGateInputs order + inputIndices [][]int // [wireInLevel][gateInputJ] → index in input + gateEvaluatorPools []*evaluator.GateEvaluatorMixedPool + baseInput []basePolynomial.MultiLin // the assignment's columns in UniqueGateInputs order, while the first round runs on them + baseGateEvaluatorPools []*evaluator.GateEvaluatorPool } func (c *zeroCheckBase) varsNum() int { return c.resources.nbVars } +// tableLen returns the length of the input tables, which are the base field columns until the +// first fold. +func (c *zeroCheckBase) tableLen() int { + if c.baseInput != nil { + return len(c.baseInput[0]) + } + return len(c.input[0]) +} + +// foldInput folds input[i] at r. Until the first fold, it sets input[i] to a buffer from the pool +// holding the fold of the base field column baseInput[i]. +func (c *zeroCheckBase) foldInput(i int, r *extensions.E6) { + if c.baseInput == nil { + c.input[i].Fold(*r) + return + } + c.input[i] = polynomial.MultiLinE6(c.resources.memPool.Make(len(c.baseInput[i]) / 2)) + c.input[i].FoldFromBase(c.baseInput[i], r) +} + +// foldInputParallel is the parallel version of foldInput. *r must not change until the returned +// task has completed. +func (c *zeroCheckBase) foldInputParallel(i int, r *extensions.E6) func(start, end int) { + if c.baseInput == nil { + return c.input[i].FoldParallel(*r) + } + c.input[i] = polynomial.MultiLinE6(c.resources.memPool.Make(len(c.baseInput[i]) / 2)) + return c.input[i].FoldFromBaseParallel(c.baseInput[i], r) +} + // proveFinalEval records the outgoing eval point, folds the last variable, // collects the unique input evaluations, and releases all pooled memory. // extraPolys is the level-type-specific slice (eqs or suffixEq) to dump. @@ -332,7 +426,7 @@ func (c *zeroCheckBase) proveFinalEval(r []extensions.E6, extraPolys []polynomia c.resources.outgoingEvalPoints[c.levelI] = [][]extensions.E6{r} evaluations := make([]extensions.E6, len(c.input)) for i := range c.input { - c.input[i].Fold(r[len(r)-1]) + c.foldInput(i, &r[len(r)-1]) evaluations[i] = c.input[i][0] } for i := range c.input { @@ -344,6 +438,9 @@ func (c *zeroCheckBase) proveFinalEval(r []extensions.E6, extraPolys []polynomia for _, pool := range c.gateEvaluatorPools { pool.DumpAll() } + for _, pool := range c.baseGateEvaluatorPools { + pool.DumpAll() + } return evaluations } @@ -359,12 +456,9 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { uniqueInputs, inputIndices := r.circuit.InputMapping(level) c.input = make([]polynomial.MultiLinE6, len(uniqueInputs)) + c.baseInput = make([]basePolynomial.MultiLin, len(uniqueInputs)) for i, inW := range uniqueInputs { - column := r.assignment[inW] - c.input[i] = polynomial.MultiLinE6(r.memPool.Make(len(column))) - for j := range column { - c.input[i][j].SetElement(&column[j]) - } + c.baseInput[i] = r.assignment[inW] } c.inputIndices = inputIndices @@ -373,11 +467,13 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { nbWires += len(group.Wires) } c.gateEvaluatorPools = make([]*evaluator.GateEvaluatorMixedPool, nbWires) + c.baseGateEvaluatorPools = make([]*evaluator.GateEvaluatorPool, nbWires) levelWireI := 0 for _, group := range level.ClaimGroups() { for _, wI := range group.Wires { wire := r.circuit[wI] c.gateEvaluatorPools[levelWireI] = evaluator.NewGateEvaluatorMixedPool(wire.Gate.Evaluate, len(inputIndices[levelWireI]), &r.memPool) + c.baseGateEvaluatorPools[levelWireI] = evaluator.NewGateEvaluatorPool(wire.Gate.Evaluate, len(inputIndices[levelWireI])) levelWireI++ } } @@ -495,68 +591,128 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) - sumSize := len(c.input[0]) / 2 + sumSize := c.tableLen() / 2 // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] p := make([]extensions.E6, degree) var mu sync.Mutex - computeAll := func(start, end int) { - var step extensions.E6 + var computeAll func(start, end int) + if c.baseInput != nil { + computeAll = func(start, end int) { + var step koalabear.Element - evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) - for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() - } - defer func() { + evaluators := make([]*evaluator.GateEvaluator, nbWires) for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) + evaluators[w] = c.baseGateEvaluatorPools[w].Get() } - }() + defer func() { + for w := range nbWires { + c.baseGateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]extensions.E6, degree) - res := make([]extensions.E6, degree) + // Base field input evaluations at m=1,2,...,degree + inputEvals := make([]koalabear.Element, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.baseInput[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) + } + } - // Input evaluations at m=1,2,...,degree - inputEvals := make([]extensions.E6, degree*nbUniqueInputs) + iIndex := 0 + nextIIndex := nbUniqueInputs + for d := range degree { + // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) + for _, inputI := range c.inputIndices[nbWires-1] { + evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + } + var wireSum extensions.E6 + wireSum.SetElement(evaluators[nbWires-1].Evaluate()) + for w := nbWires - 2; w >= 0; w-- { + wireSum.Mul(&wireSum, &c.foldingCoeff) + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[iIndex+inputI]) + } + wireSum.AddElement(&wireSum, evaluators[w].Evaluate()) + } - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range nbUniqueInputs { - inputEvals[k].Set(&c.input[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.input[k][h]) - for d := 1; d < degree; d++ { - inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) + wireSum.Mul(&wireSum, &eqSegment[h]) + res[d].Add(&res[d], &wireSum) + iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs } } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() + } + } else { + computeAll = func(start, end int) { + var step extensions.E6 - iIndex := 0 - nextIIndex := nbUniqueInputs - for d := range degree { - // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) - for _, inputI := range c.inputIndices[nbWires-1] { - evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.gateEvaluatorPools[w].Put(evaluators[w]) } - var wireSum extensions.E6 - wireSum.Set(evaluators[nbWires-1].Evaluate()) - for w := nbWires - 2; w >= 0; w-- { - wireSum.Mul(&wireSum, &c.foldingCoeff) - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(inputEvals[iIndex+inputI]) + }() + + res := make([]extensions.E6, degree) + + // Input evaluations at m=1,2,...,degree + inputEvals := make([]extensions.E6, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } - wireSum.Add(&wireSum, evaluators[w].Evaluate()) } - wireSum.Mul(&wireSum, &eqSegment[h]) - res[d].Add(&res[d], &wireSum) - iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs + iIndex := 0 + nextIIndex := nbUniqueInputs + for d := range degree { + // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) + for _, inputI := range c.inputIndices[nbWires-1] { + evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + } + var wireSum extensions.E6 + wireSum.Set(evaluators[nbWires-1].Evaluate()) + for w := nbWires - 2; w >= 0; w-- { + wireSum.Mul(&wireSum, &c.foldingCoeff) + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[iIndex+inputI]) + } + wireSum.Add(&wireSum, evaluators[w].Evaluate()) + } + + wireSum.Mul(&wireSum, &eqSegment[h]) + res[d].Add(&res[d], &wireSum) + iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs + } } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) - } - mu.Unlock() } const minBlockSize = 64 @@ -573,20 +729,21 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 // The suffix eq tables are precomputed and not folded. func (c *singleSourceZeroCheckClaims) roundFold(r extensions.E6) { const minBlockSize = 512 - n := len(c.input[0]) / 2 + n := c.tableLen() / 2 if n < minBlockSize { for i := range c.input { - c.input[i].Fold(r) + c.foldInput(i, &r) } } else { wgs := make([]*sync.WaitGroup, len(c.input)) for i := range c.input { - wgs[i] = c.resources.workers.Submit(n, c.input[i].FoldParallel(r), minBlockSize) + wgs[i] = c.resources.workers.Submit(n, c.foldInputParallel(i, &r), minBlockSize) } for _, wg := range wgs { wg.Wait() } } + c.baseInput = nil if len(c.suffixEq) > 2 { c.suffixEq = c.suffixEq[:len(c.suffixEq)/2] } diff --git a/internal/generator/backend/template/gkr/evaluator.go.tmpl b/internal/generator/backend/template/gkr/evaluator.go.tmpl index 9216ea9420..9f3dc2d250 100644 --- a/internal/generator/backend/template/gkr/evaluator.go.tmpl +++ b/internal/generator/backend/template/gkr/evaluator.go.tmpl @@ -200,10 +200,10 @@ type {{ $ge }}Pool struct { nbIn int lock sync.Mutex available []*{{ $ge }} - elementPool *{{ $poly }}.Pool{{ $suffix }} + elementPool []*{{ $poly }}.Pool{{ $suffix }} } -func New{{ $ge }}Pool(gate gkrcore.GateBytecode, nbIn int, elementPool *{{ $poly }}.Pool{{ $suffix }}) *{{ $ge }}Pool { +func New{{ $ge }}Pool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*{{ $poly }}.Pool{{ $suffix }}) *{{ $ge }}Pool { return &{{ $ge }}Pool{ gate: gate, nbIn: nbIn, @@ -221,7 +221,7 @@ func (gep *{{ $ge }}Pool) Get() *{{ $ge }} { } gep.lock.Unlock() - e := New{{ $ge }}(gep.gate, gep.nbIn, gep.elementPool) + e := New{{ $ge }}(gep.gate, gep.nbIn, gep.elementPool...) return &e } diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index bf231be895..83ab1ec71a 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -5,6 +5,10 @@ import ( "{{.FieldPackagePath}}" "{{.FieldPackagePath}}/polynomial" + {{- if .Mixed }} + "{{ .BaseFieldPackagePath }}" + basePolynomial "{{ .BaseFieldPackagePath }}/polynomial" + {{- end }} "github.com/consensys/gnark/constraint" {{- if .EvaluatorQualifier }} evaluator "github.com/consensys/gnark/internal/gkr/{{.GkrPackageName}}" @@ -174,58 +178,16 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSu p := make([]{{ .ElementType }}, degree) var mu sync.Mutex - computeAll := func(start, end int) { - var step {{ .ElementType }} - - evaluators := make([]*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}, nbWires) - for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() - } - defer func() { - for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) - } - }() - - res := make([]{{ .ElementType }}, degree) - - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]{{ .ElementType }}, degree*len(ml)) - - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) - for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) - } - } - - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) - for d := range degree { - for w := range nbWires { - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) - } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end - } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) - } - } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) // collect into the complete sum - } - mu.Unlock() + {{- if .Mixed }} + var computeAll func(start, end int) + if c.baseInput != nil { + computeAll = {{ template "zeroCheckBlock" dict "Config" . "Base" true }} + } else { + computeAll = {{ template "zeroCheckBlock" dict "Config" . "Base" false }} } + {{- else }} + computeAll := {{ template "zeroCheckBlock" dict "Config" . "Base" false }} + {{- end }} const minBlockSize = 64 if sumSize < minBlockSize { @@ -244,7 +206,11 @@ func (c *zeroCheckClaims) roundFold(r {{ .ElementType }}) { n := len(c.eqs[0]) / 2 if n < minBlockSize { for i := range c.input { + {{- if .Mixed }} + c.foldInput(i, &r) + {{- else }} c.input[i].Fold(r) + {{- end }} } for i := range c.eqs { c.eqs[i].Fold(r) @@ -252,7 +218,11 @@ func (c *zeroCheckClaims) roundFold(r {{ .ElementType }}) { } else { wgs := make([]*sync.WaitGroup, len(c.input)+len(c.eqs)) for i := range c.input { + {{- if .Mixed }} + wgs[i] = c.resources.workers.Submit(n, c.foldInputParallel(i, &r), minBlockSize) + {{- else }} wgs[i] = c.resources.workers.Submit(n, c.input[i].FoldParallel(r), minBlockSize) + {{- end }} } for i := range c.eqs { wgs[len(c.input)+i] = c.resources.workers.Submit(n, c.eqs[i].FoldParallel(r), minBlockSize) @@ -261,6 +231,9 @@ func (c *zeroCheckClaims) roundFold(r {{ .ElementType }}) { wg.Wait() } } + {{- if .Mixed }} + c.baseInput = nil + {{- end }} } // proveFinalEval provides the unique input wire values wᵢ(r₁, ..., rₙ). @@ -315,12 +288,48 @@ type zeroCheckBase struct { input []polynomial.MultiLin{{ .ExtensionSuffix }} // UniqueGateInputs order inputIndices [][]int // [wireInLevel][gateInputJ] → index in input gateEvaluatorPools []*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}Pool + {{- if .Mixed }} + baseInput []basePolynomial.MultiLin // the assignment's columns in UniqueGateInputs order, while the first round runs on them + baseGateEvaluatorPools []*{{ .EvaluatorQualifier }}GateEvaluatorPool + {{- end }} } func (c *zeroCheckBase) varsNum() int { return c.resources.nbVars } +{{ if .Mixed -}} +// tableLen returns the length of the input tables, which are the base field columns until the +// first fold. +func (c *zeroCheckBase) tableLen() int { + if c.baseInput != nil { + return len(c.baseInput[0]) + } + return len(c.input[0]) +} + +// foldInput folds input[i] at r. Until the first fold, it sets input[i] to a buffer from the pool +// holding the fold of the base field column baseInput[i]. +func (c *zeroCheckBase) foldInput(i int, r *{{ .ElementType }}) { + if c.baseInput == nil { + c.input[i].Fold(*r) + return + } + c.input[i] = polynomial.MultiLin{{ .ExtensionSuffix }}(c.resources.memPool.Make(len(c.baseInput[i]) / 2)) + c.input[i].FoldFromBase(c.baseInput[i], r) +} + +// foldInputParallel is the parallel version of foldInput. *r must not change until the returned +// task has completed. +func (c *zeroCheckBase) foldInputParallel(i int, r *{{ .ElementType }}) func(start, end int) { + if c.baseInput == nil { + return c.input[i].FoldParallel(*r) + } + c.input[i] = polynomial.MultiLin{{ .ExtensionSuffix }}(c.resources.memPool.Make(len(c.baseInput[i]) / 2)) + return c.input[i].FoldFromBaseParallel(c.baseInput[i], r) +} + +{{ end -}} // proveFinalEval records the outgoing eval point, folds the last variable, // collects the unique input evaluations, and releases all pooled memory. // extraPolys is the level-type-specific slice (eqs or suffixEq) to dump. @@ -328,7 +337,11 @@ func (c *zeroCheckBase) proveFinalEval(r []{{ .ElementType }}, extraPolys []poly c.resources.outgoingEvalPoints[c.levelI] = [][]{{ .ElementType }}{r} evaluations := make([]{{ .ElementType }}, len(c.input)) for i := range c.input { + {{- if .Mixed }} + c.foldInput(i, &r[len(r)-1]) + {{- else }} c.input[i].Fold(r[len(r)-1]) + {{- end }} evaluations[i] = c.input[i][0] } for i := range c.input { @@ -340,6 +353,11 @@ func (c *zeroCheckBase) proveFinalEval(r []{{ .ElementType }}, extraPolys []poly for _, pool := range c.gateEvaluatorPools { pool.DumpAll() } + {{- if .Mixed }} + for _, pool := range c.baseGateEvaluatorPools { + pool.DumpAll() + } + {{- end }} return evaluations } @@ -355,13 +373,12 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { uniqueInputs, inputIndices := r.circuit.InputMapping(level) c.input = make([]polynomial.MultiLin{{ .ExtensionSuffix }}, len(uniqueInputs)) + {{- if .Mixed }} + c.baseInput = make([]basePolynomial.MultiLin, len(uniqueInputs)) + {{- end }} for i, inW := range uniqueInputs { {{- if .Mixed }} - column := r.assignment[inW] - c.input[i] = polynomial.MultiLin{{ .ExtensionSuffix }}(r.memPool.Make(len(column))) - for j := range column { - c.input[i][j].SetElement(&column[j]) - } + c.baseInput[i] = r.assignment[inW] {{- else }} c.input[i] = r.memPool.Clone(r.assignment[inW]) {{- end }} @@ -373,11 +390,17 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { nbWires += len(group.Wires) } c.gateEvaluatorPools = make([]*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}Pool, nbWires) + {{- if .Mixed }} + c.baseGateEvaluatorPools = make([]*{{ .EvaluatorQualifier }}GateEvaluatorPool, nbWires) + {{- end }} levelWireI := 0 for _, group := range level.ClaimGroups() { for _, wI := range group.Wires { wire := r.circuit[wI] c.gateEvaluatorPools[levelWireI] = {{ .EvaluatorQualifier }}New{{ $gateEvaluator }}Pool(wire.Gate.Evaluate, len(inputIndices[levelWireI]), &r.memPool) + {{- if .Mixed }} + c.baseGateEvaluatorPools[levelWireI] = {{ .EvaluatorQualifier }}NewGateEvaluatorPool(wire.Gate.Evaluate, len(inputIndices[levelWireI])) + {{- end }} levelWireI++ } } @@ -495,69 +518,27 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) + {{ if .Mixed -}} + sumSize := c.tableLen() / 2 + {{- else -}} sumSize := len(c.input[0]) / 2 + {{- end }} // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] p := make([]{{ .ElementType }}, degree) var mu sync.Mutex - computeAll := func(start, end int) { - var step {{ .ElementType }} - - evaluators := make([]*{{ .EvaluatorQualifier }}{{ $gateEvaluator }}, nbWires) - for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() - } - defer func() { - for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) - } - }() - - res := make([]{{ .ElementType }}, degree) - - // Input evaluations at m=1,2,...,degree - inputEvals := make([]{{ .ElementType }}, degree*nbUniqueInputs) - - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range nbUniqueInputs { - inputEvals[k].Set(&c.input[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.input[k][h]) - for d := 1; d < degree; d++ { - inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) - } - } - - iIndex := 0 - nextIIndex := nbUniqueInputs - for d := range degree { - // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) - for _, inputI := range c.inputIndices[nbWires-1] { - evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) - } - var wireSum {{ .ElementType }} - wireSum.Set(evaluators[nbWires-1].Evaluate()) - for w := nbWires - 2; w >= 0; w-- { - wireSum.Mul(&wireSum, &c.foldingCoeff) - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(inputEvals[iIndex+inputI]) - } - wireSum.Add(&wireSum, evaluators[w].Evaluate()) - } - - wireSum.Mul(&wireSum, &eqSegment[h]) - res[d].Add(&res[d], &wireSum) - iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs - } - } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) - } - mu.Unlock() + {{- if .Mixed }} + var computeAll func(start, end int) + if c.baseInput != nil { + computeAll = {{ template "singleSourceZeroCheckBlock" dict "Config" . "Base" true }} + } else { + computeAll = {{ template "singleSourceZeroCheckBlock" dict "Config" . "Base" false }} } + {{- else }} + computeAll := {{ template "singleSourceZeroCheckBlock" dict "Config" . "Base" false }} + {{- end }} const minBlockSize = 64 if sumSize < minBlockSize { @@ -573,20 +554,35 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ // The suffix eq tables are precomputed and not folded. func (c *singleSourceZeroCheckClaims) roundFold(r {{ .ElementType }}) { const minBlockSize = 512 + {{- if .Mixed }} + n := c.tableLen() / 2 + {{- else }} n := len(c.input[0]) / 2 + {{- end }} if n < minBlockSize { for i := range c.input { + {{- if .Mixed }} + c.foldInput(i, &r) + {{- else }} c.input[i].Fold(r) + {{- end }} } } else { wgs := make([]*sync.WaitGroup, len(c.input)) for i := range c.input { + {{- if .Mixed }} + wgs[i] = c.resources.workers.Submit(n, c.foldInputParallel(i, &r), minBlockSize) + {{- else }} wgs[i] = c.resources.workers.Submit(n, c.input[i].FoldParallel(r), minBlockSize) + {{- end }} } for _, wg := range wgs { wg.Wait() } } + {{- if .Mixed }} + c.baseInput = nil + {{- end }} if len(c.suffixEq) > 2 { c.suffixEq = c.suffixEq[:len(c.suffixEq)/2] } @@ -712,3 +708,238 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er return lazyClaims.verifyFinalEval(challenges, claimedSum, proof[levelI].finalEvalProof) } + +{{ define "zeroCheckBlock" -}} +{{- $ge := "GateEvaluator" }}{{ if .Config.Mixed }}{{ $ge = "GateEvaluatorMixed" }}{{ end }} +{{- if .Base -}} +func(start, end int) { + var eqStep {{ .Config.ElementType }} + var step {{ .Config.BaseElementType }} + + evaluators := make([]*{{ .Config.EvaluatorQualifier }}GateEvaluator, nbWires) + for w := range nbWires { + evaluators[w] = c.baseGateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.baseGateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]{{ .Config.ElementType }}, degree) + var term {{ .Config.ElementType }} + + // Evaluations of the eq tables and of the base field input tables at Xⱼ = 1, 2, ..., degree, + // row by row: eqEvals[d*nbWires+w] and inputEvals[d*nbUniqueInputs+k] are at Xⱼ = d+1. + eqEvals := make([]{{ .Config.ElementType }}, degree*nbWires) + inputEvals := make([]{{ .Config.BaseElementType }}, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) + for d := 1; d < degree; d++ { + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.baseInput[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) + } + } + + for d := range degree { + for w := range nbWires { + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[d*nbUniqueInputs+inputI]) + } + term.MulByElement(&eqEvals[d*nbWires+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) + } + } + } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() + } +{{- else -}} +func(start, end int) { + var step {{ .Config.ElementType }} + + evaluators := make([]*{{ .Config.EvaluatorQualifier }}{{ $ge }}, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.gateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]{{ .Config.ElementType }}, degree) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) + mlEvals := make([]{{ .Config.ElementType }}, degree*len(ml)) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range ml { + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degree; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // start of the current row's eq evaluations + nextEIndex := len(ml) + for d := range degree { + for w := range nbWires { + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + } + summand := evaluators[w].Evaluate() + summand.Mul(summand, &mlEvals[eIndex+w]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + } + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + +{{- end }} +{{- end }} + +{{ define "singleSourceZeroCheckBlock" -}} +{{- $ge := "GateEvaluator" }}{{ if .Config.Mixed }}{{ $ge = "GateEvaluatorMixed" }}{{ end }} +{{- if .Base -}} +func(start, end int) { + var step {{ .Config.BaseElementType }} + + evaluators := make([]*{{ .Config.EvaluatorQualifier }}GateEvaluator, nbWires) + for w := range nbWires { + evaluators[w] = c.baseGateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.baseGateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]{{ .Config.ElementType }}, degree) + + // Base field input evaluations at m=1,2,...,degree + inputEvals := make([]{{ .Config.BaseElementType }}, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.baseInput[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) + } + } + + iIndex := 0 + nextIIndex := nbUniqueInputs + for d := range degree { + // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) + for _, inputI := range c.inputIndices[nbWires-1] { + evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + } + var wireSum {{ .Config.ElementType }} + wireSum.SetElement(evaluators[nbWires-1].Evaluate()) + for w := nbWires - 2; w >= 0; w-- { + wireSum.Mul(&wireSum, &c.foldingCoeff) + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[iIndex+inputI]) + } + wireSum.AddElement(&wireSum, evaluators[w].Evaluate()) + } + + wireSum.Mul(&wireSum, &eqSegment[h]) + res[d].Add(&res[d], &wireSum) + iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs + } + } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() + } +{{- else -}} +func(start, end int) { + var step {{ .Config.ElementType }} + + evaluators := make([]*{{ .Config.EvaluatorQualifier }}{{ $ge }}, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.gateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]{{ .Config.ElementType }}, degree) + + // Input evaluations at m=1,2,...,degree + inputEvals := make([]{{ .Config.ElementType }}, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) + } + } + + iIndex := 0 + nextIIndex := nbUniqueInputs + for d := range degree { + // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) + for _, inputI := range c.inputIndices[nbWires-1] { + evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + } + var wireSum {{ .Config.ElementType }} + wireSum.Set(evaluators[nbWires-1].Evaluate()) + for w := nbWires - 2; w >= 0; w-- { + wireSum.Mul(&wireSum, &c.foldingCoeff) + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[iIndex+inputI]) + } + wireSum.Add(&wireSum, evaluators[w].Evaluate()) + } + + wireSum.Mul(&wireSum, &eqSegment[h]) + res[d].Add(&res[d], &wireSum) + iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs + } + } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() + } + +{{- end }} +{{- end }} diff --git a/internal/gkr/bls12-377/evaluator.go b/internal/gkr/bls12-377/evaluator.go index 6412751d15..d04a84aefa 100644 --- a/internal/gkr/bls12-377/evaluator.go +++ b/internal/gkr/bls12-377/evaluator.go @@ -130,10 +130,10 @@ type GateEvaluatorPool struct { nbIn int lock sync.Mutex available []*GateEvaluator - elementPool *polynomial.Pool + elementPool []*polynomial.Pool } -func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.Pool) *GateEvaluatorPool { +func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.Pool) *GateEvaluatorPool { return &GateEvaluatorPool{ gate: gate, nbIn: nbIn, @@ -151,7 +151,7 @@ func (gep *GateEvaluatorPool) Get() *GateEvaluator { } gep.lock.Unlock() - e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool...) return &e } diff --git a/internal/gkr/bls12-381/evaluator.go b/internal/gkr/bls12-381/evaluator.go index ad3192ea03..23927dcecf 100644 --- a/internal/gkr/bls12-381/evaluator.go +++ b/internal/gkr/bls12-381/evaluator.go @@ -130,10 +130,10 @@ type GateEvaluatorPool struct { nbIn int lock sync.Mutex available []*GateEvaluator - elementPool *polynomial.Pool + elementPool []*polynomial.Pool } -func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.Pool) *GateEvaluatorPool { +func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.Pool) *GateEvaluatorPool { return &GateEvaluatorPool{ gate: gate, nbIn: nbIn, @@ -151,7 +151,7 @@ func (gep *GateEvaluatorPool) Get() *GateEvaluator { } gep.lock.Unlock() - e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool...) return &e } diff --git a/internal/gkr/bn254/evaluator.go b/internal/gkr/bn254/evaluator.go index 7615eb8c58..391c0d886f 100644 --- a/internal/gkr/bn254/evaluator.go +++ b/internal/gkr/bn254/evaluator.go @@ -130,10 +130,10 @@ type GateEvaluatorPool struct { nbIn int lock sync.Mutex available []*GateEvaluator - elementPool *polynomial.Pool + elementPool []*polynomial.Pool } -func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.Pool) *GateEvaluatorPool { +func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.Pool) *GateEvaluatorPool { return &GateEvaluatorPool{ gate: gate, nbIn: nbIn, @@ -151,7 +151,7 @@ func (gep *GateEvaluatorPool) Get() *GateEvaluator { } gep.lock.Unlock() - e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool...) return &e } diff --git a/internal/gkr/bw6-761/evaluator.go b/internal/gkr/bw6-761/evaluator.go index 4616942f8c..fdea34e115 100644 --- a/internal/gkr/bw6-761/evaluator.go +++ b/internal/gkr/bw6-761/evaluator.go @@ -130,10 +130,10 @@ type GateEvaluatorPool struct { nbIn int lock sync.Mutex available []*GateEvaluator - elementPool *polynomial.Pool + elementPool []*polynomial.Pool } -func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.Pool) *GateEvaluatorPool { +func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.Pool) *GateEvaluatorPool { return &GateEvaluatorPool{ gate: gate, nbIn: nbIn, @@ -151,7 +151,7 @@ func (gep *GateEvaluatorPool) Get() *GateEvaluator { } gep.lock.Unlock() - e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool...) return &e } diff --git a/internal/gkr/koalabear/evaluator.go b/internal/gkr/koalabear/evaluator.go index 68e2c79e97..669fac82d4 100644 --- a/internal/gkr/koalabear/evaluator.go +++ b/internal/gkr/koalabear/evaluator.go @@ -132,10 +132,10 @@ type GateEvaluatorPool struct { nbIn int lock sync.Mutex available []*GateEvaluator - elementPool *basePolynomial.Pool + elementPool []*basePolynomial.Pool } -func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *basePolynomial.Pool) *GateEvaluatorPool { +func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*basePolynomial.Pool) *GateEvaluatorPool { return &GateEvaluatorPool{ gate: gate, nbIn: nbIn, @@ -153,7 +153,7 @@ func (gep *GateEvaluatorPool) Get() *GateEvaluator { } gep.lock.Unlock() - e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool...) return &e } @@ -320,10 +320,10 @@ type GateEvaluatorMixedPool struct { nbIn int lock sync.Mutex available []*GateEvaluatorMixed - elementPool *polynomial.PoolE6 + elementPool []*polynomial.PoolE6 } -func NewGateEvaluatorMixedPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.PoolE6) *GateEvaluatorMixedPool { +func NewGateEvaluatorMixedPool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.PoolE6) *GateEvaluatorMixedPool { return &GateEvaluatorMixedPool{ gate: gate, nbIn: nbIn, @@ -341,7 +341,7 @@ func (gep *GateEvaluatorMixedPool) Get() *GateEvaluatorMixed { } gep.lock.Unlock() - e := NewGateEvaluatorMixed(gep.gate, gep.nbIn, gep.elementPool) + e := NewGateEvaluatorMixed(gep.gate, gep.nbIn, gep.elementPool...) return &e } diff --git a/internal/gkr/rational/evaluator.go b/internal/gkr/rational/evaluator.go index 6f38f6ad87..18a8434b35 100644 --- a/internal/gkr/rational/evaluator.go +++ b/internal/gkr/rational/evaluator.go @@ -130,10 +130,10 @@ type GateEvaluatorPool struct { nbIn int lock sync.Mutex available []*GateEvaluator - elementPool *polynomial.Pool + elementPool []*polynomial.Pool } -func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool *polynomial.Pool) *GateEvaluatorPool { +func NewGateEvaluatorPool(gate gkrcore.GateBytecode, nbIn int, elementPool ...*polynomial.Pool) *GateEvaluatorPool { return &GateEvaluatorPool{ gate: gate, nbIn: nbIn, @@ -151,7 +151,7 @@ func (gep *GateEvaluatorPool) Get() *GateEvaluator { } gep.lock.Unlock() - e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool) + e := NewGateEvaluator(gep.gate, gep.nbIn, gep.elementPool...) return &e } From e14386f95f71fbcda3adc84a1f9698dada8f6edf Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 6 Oct 2026 00:43:20 -0500 Subject: [PATCH 22/37] refactor: unify code generation Signed-off-by: Arya Tabaie --- gkr/bls12-377/sumcheck.go | 50 +- gkr/bls12-381/sumcheck.go | 50 +- gkr/bn254/sumcheck.go | 50 +- gkr/bw6-761/sumcheck.go | 50 +- gkr/koalabear/sumcheck.go | 452 ++++++++++-------- .../backend/template/gkr/evaluator.go.tmpl | 4 +- .../backend/template/gkr/sumcheck.go.tmpl | 349 ++++++-------- internal/gkr/bls12-377/evaluator.go | 2 +- internal/gkr/bls12-381/evaluator.go | 2 +- internal/gkr/bn254/evaluator.go | 2 +- internal/gkr/bw6-761/evaluator.go | 2 +- internal/gkr/koalabear/evaluator.go | 6 +- internal/gkr/rational/evaluator.go | 2 +- internal/gkr/rational/sumcheck.go | 50 +- 14 files changed, 536 insertions(+), 535 deletions(-) diff --git a/gkr/bls12-377/sumcheck.go b/gkr/bls12-377/sumcheck.go index 5b1435a5d6..110c18a720 100644 --- a/gkr/bls12-377/sumcheck.go +++ b/gkr/bls12-377/sumcheck.go @@ -169,16 +169,13 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { // Both eqs and input are multilinear, thus linear in Xⱼ. // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLin, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) sumSize := len(c.eqs[0]) / 2 p := make([]fr.Element, degree) var mu sync.Mutex computeAll := func(start, end int) { + var eqStep fr.Element var step fr.Element evaluators := make([]*evaluator.GateEvaluator, nbWires) @@ -192,36 +189,42 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { }() res := make([]fr.Element, degree) + var term fr.Element - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]fr.Element, degree*len(ml)) + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). + eqEvals := make([]fr.Element, degree*nbWires) + inputEvals := make([]fr.Element, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } } - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals for d := range degree { for w := range nbWires { for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + term.Mul(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + eqIndex += nbWires + inputIndex += nbUniqueInputs } } mu.Lock() @@ -347,7 +350,7 @@ func (c *zeroCheckBase) proveFinalEval(r []fr.Element, extraPolys []polynomial.M return evaluations } -// init derives the folding coefficient, clones the input multilinears, +// init derives the folding coefficient, sets up the input multilinears, // and builds gate evaluator pools — the steps common to all sumcheck-family provers. func (c *zeroCheckBase) init(r *resources, levelI int) { c.levelI = levelI @@ -491,10 +494,9 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) - sumSize := len(c.input[0]) / 2 - // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) p := make([]fr.Element, degree) var mu sync.Mutex diff --git a/gkr/bls12-381/sumcheck.go b/gkr/bls12-381/sumcheck.go index b09a3d62bb..c6b4524de1 100644 --- a/gkr/bls12-381/sumcheck.go +++ b/gkr/bls12-381/sumcheck.go @@ -169,16 +169,13 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { // Both eqs and input are multilinear, thus linear in Xⱼ. // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLin, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) sumSize := len(c.eqs[0]) / 2 p := make([]fr.Element, degree) var mu sync.Mutex computeAll := func(start, end int) { + var eqStep fr.Element var step fr.Element evaluators := make([]*evaluator.GateEvaluator, nbWires) @@ -192,36 +189,42 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { }() res := make([]fr.Element, degree) + var term fr.Element - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]fr.Element, degree*len(ml)) + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). + eqEvals := make([]fr.Element, degree*nbWires) + inputEvals := make([]fr.Element, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } } - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals for d := range degree { for w := range nbWires { for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + term.Mul(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + eqIndex += nbWires + inputIndex += nbUniqueInputs } } mu.Lock() @@ -347,7 +350,7 @@ func (c *zeroCheckBase) proveFinalEval(r []fr.Element, extraPolys []polynomial.M return evaluations } -// init derives the folding coefficient, clones the input multilinears, +// init derives the folding coefficient, sets up the input multilinears, // and builds gate evaluator pools — the steps common to all sumcheck-family provers. func (c *zeroCheckBase) init(r *resources, levelI int) { c.levelI = levelI @@ -491,10 +494,9 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) - sumSize := len(c.input[0]) / 2 - // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) p := make([]fr.Element, degree) var mu sync.Mutex diff --git a/gkr/bn254/sumcheck.go b/gkr/bn254/sumcheck.go index 21f35fc3a9..47c8414309 100644 --- a/gkr/bn254/sumcheck.go +++ b/gkr/bn254/sumcheck.go @@ -169,16 +169,13 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { // Both eqs and input are multilinear, thus linear in Xⱼ. // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLin, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) sumSize := len(c.eqs[0]) / 2 p := make([]fr.Element, degree) var mu sync.Mutex computeAll := func(start, end int) { + var eqStep fr.Element var step fr.Element evaluators := make([]*evaluator.GateEvaluator, nbWires) @@ -192,36 +189,42 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { }() res := make([]fr.Element, degree) + var term fr.Element - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]fr.Element, degree*len(ml)) + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). + eqEvals := make([]fr.Element, degree*nbWires) + inputEvals := make([]fr.Element, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } } - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals for d := range degree { for w := range nbWires { for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + term.Mul(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + eqIndex += nbWires + inputIndex += nbUniqueInputs } } mu.Lock() @@ -347,7 +350,7 @@ func (c *zeroCheckBase) proveFinalEval(r []fr.Element, extraPolys []polynomial.M return evaluations } -// init derives the folding coefficient, clones the input multilinears, +// init derives the folding coefficient, sets up the input multilinears, // and builds gate evaluator pools — the steps common to all sumcheck-family provers. func (c *zeroCheckBase) init(r *resources, levelI int) { c.levelI = levelI @@ -491,10 +494,9 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) - sumSize := len(c.input[0]) / 2 - // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) p := make([]fr.Element, degree) var mu sync.Mutex diff --git a/gkr/bw6-761/sumcheck.go b/gkr/bw6-761/sumcheck.go index e482d08d93..8dbf8b38dd 100644 --- a/gkr/bw6-761/sumcheck.go +++ b/gkr/bw6-761/sumcheck.go @@ -169,16 +169,13 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { // Both eqs and input are multilinear, thus linear in Xⱼ. // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLin, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) sumSize := len(c.eqs[0]) / 2 p := make([]fr.Element, degree) var mu sync.Mutex computeAll := func(start, end int) { + var eqStep fr.Element var step fr.Element evaluators := make([]*evaluator.GateEvaluator, nbWires) @@ -192,36 +189,42 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { }() res := make([]fr.Element, degree) + var term fr.Element - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]fr.Element, degree*len(ml)) + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). + eqEvals := make([]fr.Element, degree*nbWires) + inputEvals := make([]fr.Element, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } } - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals for d := range degree { for w := range nbWires { for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + term.Mul(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + eqIndex += nbWires + inputIndex += nbUniqueInputs } } mu.Lock() @@ -347,7 +350,7 @@ func (c *zeroCheckBase) proveFinalEval(r []fr.Element, extraPolys []polynomial.M return evaluations } -// init derives the folding coefficient, clones the input multilinears, +// init derives the folding coefficient, sets up the input multilinears, // and builds gate evaluator pools — the steps common to all sumcheck-family provers. func (c *zeroCheckBase) init(r *resources, levelI int) { c.levelI = levelI @@ -491,10 +494,9 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) - sumSize := len(c.input[0]) / 2 - // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) p := make([]fr.Element, degree) var mu sync.Mutex diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index 26bfd66514..446a3e17b4 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -158,11 +158,11 @@ type zeroCheckClaims struct { eqs []polynomial.MultiLinE6 // per-wire interpolation bases for evaluating wire assignments at challenge points } -// roundPolynomial computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). +// roundPolynomialMixed computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). // The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). // The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). // By convention, g₀ is a constant polynomial equal to the claimed sum. -func (c *zeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { +func (c *zeroCheckClaims) roundPolynomialMixed() polynomial.PolynomialE6 { level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.input) @@ -171,125 +171,157 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { // Both eqs and input are multilinear, thus linear in Xⱼ. // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLinE6, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) sumSize := len(c.eqs[0]) / 2 p := make([]extensions.E6, degree) var mu sync.Mutex - var computeAll func(start, end int) - if c.baseInput != nil { - computeAll = func(start, end int) { - var eqStep extensions.E6 - var step koalabear.Element + computeAll := func(start, end int) { + var eqStep extensions.E6 + var step extensions.E6 - evaluators := make([]*evaluator.GateEvaluator, nbWires) + evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { for w := range nbWires { - evaluators[w] = c.baseGateEvaluatorPools[w].Get() + c.gateEvaluatorPools[w].Put(evaluators[w]) } - defer func() { - for w := range nbWires { - c.baseGateEvaluatorPools[w].Put(evaluators[w]) + }() + + res := make([]extensions.E6, degree) + var term extensions.E6 + + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). + eqEvals := make([]extensions.E6, degree*nbWires) + inputEvals := make([]extensions.E6, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) + for d := 1; d < degree; d++ { + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) } - }() - - res := make([]extensions.E6, degree) - var term extensions.E6 - - // Evaluations of the eq tables and of the base field input tables at Xⱼ = 1, 2, ..., degree, - // row by row: eqEvals[d*nbWires+w] and inputEvals[d*nbUniqueInputs+k] are at Xⱼ = d+1. - eqEvals := make([]extensions.E6, degree*nbWires) - inputEvals := make([]koalabear.Element, degree*nbUniqueInputs) - - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range nbWires { - eqEvals[k].Set(&c.eqs[k][evalAt1Index]) - eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) - for d := 1; d < degree; d++ { - eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) - } - } - for k := range nbUniqueInputs { - inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.baseInput[k][h]) - for d := 1; d < degree; d++ { - inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) - } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } + } - for d := range degree { - for w := range nbWires { - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(inputEvals[d*nbUniqueInputs+inputI]) - } - term.MulByElement(&eqEvals[d*nbWires+w], evaluators[w].Evaluate()) - res[d].Add(&res[d], &term) + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals + for d := range degree { + for w := range nbWires { + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } + term.Mul(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } + eqIndex += nbWires + inputIndex += nbUniqueInputs } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) - } - mu.Unlock() } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + if sumSize < minBlockSize { + computeAll(0, sumSize) } else { - computeAll = func(start, end int) { - var step extensions.E6 + c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } - evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) - for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() - } - defer func() { - for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) - } - }() + return p +} - res := make([]extensions.E6, degree) +// roundPolynomialBase computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). +// The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). +// By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *zeroCheckClaims) roundPolynomialBase() polynomial.PolynomialE6 { + level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) + degree := c.resources.circuit.ZeroCheckDegree(level) + nbUniqueInputs := len(c.baseInput) + nbWires := len(c.eqs) + + // Both eqs and input are multilinear, thus linear in Xⱼ. + // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly + // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]extensions.E6, degree*len(ml)) + sumSize := len(c.eqs[0]) / 2 - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) - for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) - } + p := make([]extensions.E6, degree) + var mu sync.Mutex + computeAll := func(start, end int) { + var eqStep extensions.E6 + var step koalabear.Element + + evaluators := make([]*evaluator.GateEvaluator, nbWires) + for w := range nbWires { + evaluators[w] = c.baseGateEvaluatorPools[w].Get() + } + defer func() { + for w := range nbWires { + c.baseGateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]extensions.E6, degree) + var term extensions.E6 + + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). + eqEvals := make([]extensions.E6, degree*nbWires) + inputEvals := make([]koalabear.Element, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) + for d := 1; d < degree; d++ { + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.baseInput[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } + } - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) - for d := range degree { - for w := range nbWires { - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) - } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals + for d := range degree { + for w := range nbWires { + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + term.MulByElement(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } + eqIndex += nbWires + inputIndex += nbUniqueInputs } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) // collect into the complete sum - } - mu.Unlock() } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) // collect into the complete sum + } + mu.Unlock() } const minBlockSize = 64 @@ -302,6 +334,15 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { return p } +// roundPolynomial computes the round polynomial from the base field input tables while +// baseInput is set, and from input otherwise. +func (c *zeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { + if c.baseInput != nil { + return c.roundPolynomialBase() + } + return c.roundPolynomialMixed() +} + // roundFold folds all input and eq polynomials at the verifier challenge r. // After this call, j ← j+1 and rⱼ = r. func (c *zeroCheckClaims) roundFold(r extensions.E6) { @@ -444,7 +485,7 @@ func (c *zeroCheckBase) proveFinalEval(r []extensions.E6, extraPolys []polynomia return evaluations } -// init derives the folding coefficient, clones the input multilinears, +// init derives the folding coefficient, sets up the input multilinears, // and builds gate evaluator pools — the steps common to all sumcheck-family provers. func (c *zeroCheckBase) init(r *resources, levelI int) { c.levelI = levelI @@ -581,138 +622,160 @@ type singleSourceZeroCheckClaims struct { suffixEq polynomial.MultiLinE6 // contiguous buffer; second half is the current round's eq segment } -// roundPolynomial computes g'_j(1), ..., g'_j(d) where d is the gate degree. +// roundPolynomialMixed computes g'_j(1), ..., g'_j(d) where d is the gate degree. // g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) // The wire contributions are accumulated via Horner's method in α (= foldingCoeff), // eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. -func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { +func (c *singleSourceZeroCheckClaims) roundPolynomialMixed() polynomial.PolynomialE6 { level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) - sumSize := c.tableLen() / 2 - // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) p := make([]extensions.E6, degree) var mu sync.Mutex - var computeAll func(start, end int) - if c.baseInput != nil { - computeAll = func(start, end int) { - var step koalabear.Element + computeAll := func(start, end int) { + var step extensions.E6 - evaluators := make([]*evaluator.GateEvaluator, nbWires) + evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) + for w := range nbWires { + evaluators[w] = c.gateEvaluatorPools[w].Get() + } + defer func() { for w := range nbWires { - evaluators[w] = c.baseGateEvaluatorPools[w].Get() + c.gateEvaluatorPools[w].Put(evaluators[w]) } - defer func() { - for w := range nbWires { - c.baseGateEvaluatorPools[w].Put(evaluators[w]) - } - }() + }() - res := make([]extensions.E6, degree) + res := make([]extensions.E6, degree) - // Base field input evaluations at m=1,2,...,degree - inputEvals := make([]koalabear.Element, degree*nbUniqueInputs) + // Input evaluations at m=1,2,...,degree + inputEvals := make([]extensions.E6, degree*nbUniqueInputs) - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range nbUniqueInputs { - inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.baseInput[k][h]) - for d := 1; d < degree; d++ { - inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) - } + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } + } - iIndex := 0 - nextIIndex := nbUniqueInputs - for d := range degree { - // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) - for _, inputI := range c.inputIndices[nbWires-1] { - evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) - } - var wireSum extensions.E6 - wireSum.SetElement(evaluators[nbWires-1].Evaluate()) - for w := nbWires - 2; w >= 0; w-- { - wireSum.Mul(&wireSum, &c.foldingCoeff) - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(inputEvals[iIndex+inputI]) - } - wireSum.AddElement(&wireSum, evaluators[w].Evaluate()) + iIndex := 0 + nextIIndex := nbUniqueInputs + for d := range degree { + // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) + for _, inputI := range c.inputIndices[nbWires-1] { + evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + } + var wireSum extensions.E6 + wireSum.Set(evaluators[nbWires-1].Evaluate()) + for w := nbWires - 2; w >= 0; w-- { + wireSum.Mul(&wireSum, &c.foldingCoeff) + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[iIndex+inputI]) } - - wireSum.Mul(&wireSum, &eqSegment[h]) - res[d].Add(&res[d], &wireSum) - iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs + wireSum.Add(&wireSum, evaluators[w].Evaluate()) } + + wireSum.Mul(&wireSum, &eqSegment[h]) + res[d].Add(&res[d], &wireSum) + iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) - } - mu.Unlock() } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() + } + + const minBlockSize = 64 + if sumSize < minBlockSize { + computeAll(0, sumSize) } else { - computeAll = func(start, end int) { - var step extensions.E6 + c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } - evaluators := make([]*evaluator.GateEvaluatorMixed, nbWires) + return p +} + +// roundPolynomialBase computes g'_j(1), ..., g'_j(d) where d is the gate degree. +// g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) +// The wire contributions are accumulated via Horner's method in α (= foldingCoeff), +// eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. +func (c *singleSourceZeroCheckClaims) roundPolynomialBase() polynomial.PolynomialE6 { + level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) + degree := c.resources.circuit.ZeroCheckDegree(level) + nbUniqueInputs := len(c.baseInput) + nbWires := len(c.gateEvaluatorPools) + + // The second half of suffixEq is the eq segment for this round + eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) + + p := make([]extensions.E6, degree) + var mu sync.Mutex + computeAll := func(start, end int) { + var step koalabear.Element + + evaluators := make([]*evaluator.GateEvaluator, nbWires) + for w := range nbWires { + evaluators[w] = c.baseGateEvaluatorPools[w].Get() + } + defer func() { for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() + c.baseGateEvaluatorPools[w].Put(evaluators[w]) } - defer func() { - for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) - } - }() + }() - res := make([]extensions.E6, degree) + res := make([]extensions.E6, degree) - // Input evaluations at m=1,2,...,degree - inputEvals := make([]extensions.E6, degree*nbUniqueInputs) + // Input evaluations at m=1,2,...,degree + inputEvals := make([]koalabear.Element, degree*nbUniqueInputs) - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range nbUniqueInputs { - inputEvals[k].Set(&c.input[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.input[k][h]) - for d := 1; d < degree; d++ { - inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) - } + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.baseInput[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } + } - iIndex := 0 - nextIIndex := nbUniqueInputs - for d := range degree { - // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) - for _, inputI := range c.inputIndices[nbWires-1] { - evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) - } - var wireSum extensions.E6 - wireSum.Set(evaluators[nbWires-1].Evaluate()) - for w := nbWires - 2; w >= 0; w-- { - wireSum.Mul(&wireSum, &c.foldingCoeff) - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(inputEvals[iIndex+inputI]) - } - wireSum.Add(&wireSum, evaluators[w].Evaluate()) + iIndex := 0 + nextIIndex := nbUniqueInputs + for d := range degree { + // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) + for _, inputI := range c.inputIndices[nbWires-1] { + evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) + } + var wireSum extensions.E6 + wireSum.SetElement(evaluators[nbWires-1].Evaluate()) + for w := nbWires - 2; w >= 0; w-- { + wireSum.Mul(&wireSum, &c.foldingCoeff) + for _, inputI := range c.inputIndices[w] { + evaluators[w].PushInput(inputEvals[iIndex+inputI]) } - - wireSum.Mul(&wireSum, &eqSegment[h]) - res[d].Add(&res[d], &wireSum) - iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs + wireSum.AddElement(&wireSum, evaluators[w].Evaluate()) } + + wireSum.Mul(&wireSum, &eqSegment[h]) + res[d].Add(&res[d], &wireSum) + iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) - } - mu.Unlock() } + mu.Lock() + for i := range p { + p[i].Add(&p[i], &res[i]) + } + mu.Unlock() } const minBlockSize = 64 @@ -725,6 +788,15 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 return p } +// roundPolynomial computes the round polynomial from the base field input tables while +// baseInput is set, and from input otherwise. +func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { + if c.baseInput != nil { + return c.roundPolynomialBase() + } + return c.roundPolynomialMixed() +} + // roundFold folds only input multilinears at the verifier challenge r. // The suffix eq tables are precomputed and not folded. func (c *singleSourceZeroCheckClaims) roundFold(r extensions.E6) { diff --git a/internal/generator/backend/template/gkr/evaluator.go.tmpl b/internal/generator/backend/template/gkr/evaluator.go.tmpl index 9f3dc2d250..4ed1515cc8 100644 --- a/internal/generator/backend/template/gkr/evaluator.go.tmpl +++ b/internal/generator/backend/template/gkr/evaluator.go.tmpl @@ -35,7 +35,7 @@ type {{ $ge }} struct { gate gkrcore.GateBytecode vars []{{ $elem }} {{- if .Mixed }} - consts []{{ .Config.BaseElementType }} // the gate's constants in the base field, multiplied by with MulByElement + consts []{{ .Config.BaseElementType }} // the gate's constants in the base field, for the extension's methods taking a base field element {{- end }} nbIn int // number of inputs expected pool *{{ $poly }}.Pool{{ $suffix }} // pool vars was allocated from, if any @@ -194,7 +194,7 @@ func (e *{{ $ge }}) Evaluate(top ...{{ $elem }}) *{{ $elem }} { } // {{ $ge }}Pool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type {{ $ge }}Pool struct { gate gkrcore.GateBytecode nbIn int diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index 83ab1ec71a..0f5021b88c 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -156,48 +156,20 @@ type zeroCheckClaims struct { eqs []polynomial.MultiLin{{ .ExtensionSuffix }} // per-wire interpolation bases for evaluating wire assignments at challenge points } -// roundPolynomial computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). -// The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). -// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). -// By convention, g₀ is a constant polynomial equal to the claimed sum. -func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { - level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) - degree := c.resources.circuit.ZeroCheckDegree(level) - nbUniqueInputs := len(c.input) - nbWires := len(c.eqs) - - // Both eqs and input are multilinear, thus linear in Xⱼ. - // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly - // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLin{{ .ExtensionSuffix }}, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) +{{ template "zeroCheckRoundPolynomial" dict "Config" . "Base" false }} +{{- if .Mixed }} - sumSize := len(c.eqs[0]) / 2 +{{ template "zeroCheckRoundPolynomial" dict "Config" . "Base" true }} - p := make([]{{ .ElementType }}, degree) - var mu sync.Mutex - {{- if .Mixed }} - var computeAll func(start, end int) +// roundPolynomial computes the round polynomial from the base field input tables while +// baseInput is set, and from input otherwise. +func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { if c.baseInput != nil { - computeAll = {{ template "zeroCheckBlock" dict "Config" . "Base" true }} - } else { - computeAll = {{ template "zeroCheckBlock" dict "Config" . "Base" false }} - } - {{- else }} - computeAll := {{ template "zeroCheckBlock" dict "Config" . "Base" false }} - {{- end }} - - const minBlockSize = 64 - if sumSize < minBlockSize { - computeAll(0, sumSize) - } else { - c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + return c.roundPolynomialBase() } - - return p + return c.roundPolynomialMixed() } +{{- end }} // roundFold folds all input and eq polynomials at the verifier challenge r. // After this call, j ← j+1 and rⱼ = r. @@ -361,7 +333,7 @@ func (c *zeroCheckBase) proveFinalEval(r []{{ .ElementType }}, extraPolys []poly return evaluations } -// init derives the folding coefficient, clones the input multilinears, +// init derives the folding coefficient, sets up the input multilinears, // and builds gate evaluator pools — the steps common to all sumcheck-family provers. func (c *zeroCheckBase) init(r *resources, levelI int) { c.levelI = levelI @@ -508,47 +480,20 @@ type singleSourceZeroCheckClaims struct { suffixEq polynomial.MultiLin{{ .ExtensionSuffix }} // contiguous buffer; second half is the current round's eq segment } -// roundPolynomial computes g'_j(1), ..., g'_j(d) where d is the gate degree. -// g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) -// The wire contributions are accumulated via Horner's method in α (= foldingCoeff), -// eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. -func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { - level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) - degree := c.resources.circuit.ZeroCheckDegree(level) - nbUniqueInputs := len(c.input) - nbWires := len(c.gateEvaluatorPools) - - {{ if .Mixed -}} - sumSize := c.tableLen() / 2 - {{- else -}} - sumSize := len(c.input[0]) / 2 - {{- end }} +{{ template "singleSourceRoundPolynomial" dict "Config" . "Base" false }} +{{- if .Mixed }} - // The second half of suffixEq is the eq segment for this round - eqSegment := c.suffixEq[len(c.suffixEq)/2:] +{{ template "singleSourceRoundPolynomial" dict "Config" . "Base" true }} - p := make([]{{ .ElementType }}, degree) - var mu sync.Mutex - {{- if .Mixed }} - var computeAll func(start, end int) +// roundPolynomial computes the round polynomial from the base field input tables while +// baseInput is set, and from input otherwise. +func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { if c.baseInput != nil { - computeAll = {{ template "singleSourceZeroCheckBlock" dict "Config" . "Base" true }} - } else { - computeAll = {{ template "singleSourceZeroCheckBlock" dict "Config" . "Base" false }} + return c.roundPolynomialBase() } - {{- else }} - computeAll := {{ template "singleSourceZeroCheckBlock" dict "Config" . "Base" false }} - {{- end }} - - const minBlockSize = 64 - if sumSize < minBlockSize { - computeAll(0, sumSize) - } else { - c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() - } - - return p + return c.roundPolynomialMixed() } +{{- end }} // roundFold folds only input multilinears at the verifier challenge r. // The suffix eq tables are precomputed and not folded. @@ -709,204 +654,170 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er return lazyClaims.verifyFinalEval(challenges, claimedSum, proof[levelI].finalEvalProof) } -{{ define "zeroCheckBlock" -}} -{{- $ge := "GateEvaluator" }}{{ if .Config.Mixed }}{{ $ge = "GateEvaluatorMixed" }}{{ end }} -{{- if .Base -}} -func(start, end int) { +{{ define "zeroCheckRoundPolynomial" -}} +{{- $name := "roundPolynomial" }} +{{- $inputs := "c.input" }} +{{- $inElem := .Config.ElementType }} +{{- $pools := "c.gateEvaluatorPools" }} +{{- $ge := "GateEvaluator" }} +{{- $mul := "Mul" }} +{{- if .Base }} +{{- $name = "roundPolynomialBase" }} +{{- $inputs = "c.baseInput" }} +{{- $inElem = .Config.BaseElementType }} +{{- $pools = "c.baseGateEvaluatorPools" }} +{{- $mul = "MulByElement" }} +{{- else if .Config.Mixed }} +{{- $name = "roundPolynomialMixed" }} +{{- $ge = "GateEvaluatorMixed" }} +{{- end }} +// {{ $name }} computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). +// The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). +// By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *zeroCheckClaims) {{ $name }}() polynomial.Polynomial{{ .Config.ExtensionSuffix }} { + level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) + degree := c.resources.circuit.ZeroCheckDegree(level) + nbUniqueInputs := len({{ $inputs }}) + nbWires := len(c.eqs) + + // Both eqs and input are multilinear, thus linear in Xⱼ. + // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly + // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. + + sumSize := len(c.eqs[0]) / 2 + + p := make([]{{ .Config.ElementType }}, degree) + var mu sync.Mutex + computeAll := func(start, end int) { var eqStep {{ .Config.ElementType }} - var step {{ .Config.BaseElementType }} + var step {{ $inElem }} - evaluators := make([]*{{ .Config.EvaluatorQualifier }}GateEvaluator, nbWires) + evaluators := make([]*{{ .Config.EvaluatorQualifier }}{{ $ge }}, nbWires) for w := range nbWires { - evaluators[w] = c.baseGateEvaluatorPools[w].Get() + evaluators[w] = {{ $pools }}[w].Get() } defer func() { for w := range nbWires { - c.baseGateEvaluatorPools[w].Put(evaluators[w]) + {{ $pools }}[w].Put(evaluators[w]) } }() res := make([]{{ .Config.ElementType }}, degree) var term {{ .Config.ElementType }} - // Evaluations of the eq tables and of the base field input tables at Xⱼ = 1, 2, ..., degree, - // row by row: eqEvals[d*nbWires+w] and inputEvals[d*nbUniqueInputs+k] are at Xⱼ = d+1. + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). eqEvals := make([]{{ .Config.ElementType }}, degree*nbWires) - inputEvals := make([]{{ .Config.BaseElementType }}, degree*nbUniqueInputs) + inputEvals := make([]{{ $inElem }}, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h for k := range nbWires { - eqEvals[k].Set(&c.eqs[k][evalAt1Index]) - eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) for d := 1; d < degree; d++ { eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) } } for k := range nbUniqueInputs { - inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.baseInput[k][h]) + inputEvals[k].Set(&{{ $inputs }}[k][evalAt1Index]) + step.Sub(&inputEvals[k], &{{ $inputs }}[k][h]) for d := 1; d < degree; d++ { inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } } + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals for d := range degree { for w := range nbWires { for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(inputEvals[d*nbUniqueInputs+inputI]) + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } - term.MulByElement(&eqEvals[d*nbWires+w], evaluators[w].Evaluate()) - res[d].Add(&res[d], &term) + term.{{ $mul }}(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } + eqIndex += nbWires + inputIndex += nbUniqueInputs } } mu.Lock() for i := range p { - p[i].Add(&p[i], &res[i]) + p[i].Add(&p[i], &res[i]) // collect into the complete sum } mu.Unlock() } -{{- else -}} -func(start, end int) { - var step {{ .Config.ElementType }} - - evaluators := make([]*{{ .Config.EvaluatorQualifier }}{{ $ge }}, nbWires) - for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() - } - defer func() { - for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) - } - }() - - res := make([]{{ .Config.ElementType }}, degree) - - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]{{ .Config.ElementType }}, degree*len(ml)) - - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) - for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) - } - } - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) - for d := range degree { - for w := range nbWires { - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) - } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end - } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) - } - } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) // collect into the complete sum - } - mu.Unlock() + const minBlockSize = 64 + if sumSize < minBlockSize { + computeAll(0, sumSize) + } else { + c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() } + return p +} +{{ end }} + +{{ define "singleSourceRoundPolynomial" -}} +{{- $name := "roundPolynomial" }} +{{- $inputs := "c.input" }} +{{- $inElem := .Config.ElementType }} +{{- $pools := "c.gateEvaluatorPools" }} +{{- $ge := "GateEvaluator" }} +{{- $set := "Set" }} +{{- $add := "Add" }} +{{- if .Base }} +{{- $name = "roundPolynomialBase" }} +{{- $inputs = "c.baseInput" }} +{{- $inElem = .Config.BaseElementType }} +{{- $pools = "c.baseGateEvaluatorPools" }} +{{- $set = "SetElement" }} +{{- $add = "AddElement" }} +{{- else if .Config.Mixed }} +{{- $name = "roundPolynomialMixed" }} +{{- $ge = "GateEvaluatorMixed" }} {{- end }} -{{- end }} - -{{ define "singleSourceZeroCheckBlock" -}} -{{- $ge := "GateEvaluator" }}{{ if .Config.Mixed }}{{ $ge = "GateEvaluatorMixed" }}{{ end }} -{{- if .Base -}} -func(start, end int) { - var step {{ .Config.BaseElementType }} - - evaluators := make([]*{{ .Config.EvaluatorQualifier }}GateEvaluator, nbWires) - for w := range nbWires { - evaluators[w] = c.baseGateEvaluatorPools[w].Get() - } - defer func() { - for w := range nbWires { - c.baseGateEvaluatorPools[w].Put(evaluators[w]) - } - }() - - res := make([]{{ .Config.ElementType }}, degree) - - // Base field input evaluations at m=1,2,...,degree - inputEvals := make([]{{ .Config.BaseElementType }}, degree*nbUniqueInputs) - - for h := start; h < end; h++ { - evalAt1Index := sumSize + h - for k := range nbUniqueInputs { - inputEvals[k].Set(&c.baseInput[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.baseInput[k][h]) - for d := 1; d < degree; d++ { - inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) - } - } +// {{ $name }} computes g'_j(1), ..., g'_j(d) where d is the gate degree. +// g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) +// The wire contributions are accumulated via Horner's method in α (= foldingCoeff), +// eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. +func (c *singleSourceZeroCheckClaims) {{ $name }}() polynomial.Polynomial{{ .Config.ExtensionSuffix }} { + level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) + degree := c.resources.circuit.ZeroCheckDegree(level) + nbUniqueInputs := len({{ $inputs }}) + nbWires := len(c.gateEvaluatorPools) - iIndex := 0 - nextIIndex := nbUniqueInputs - for d := range degree { - // Horner accumulation: gate_0 + α·(gate_1 + α·(... + α·gate_{W-1})) - for _, inputI := range c.inputIndices[nbWires-1] { - evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) - } - var wireSum {{ .Config.ElementType }} - wireSum.SetElement(evaluators[nbWires-1].Evaluate()) - for w := nbWires - 2; w >= 0; w-- { - wireSum.Mul(&wireSum, &c.foldingCoeff) - for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(inputEvals[iIndex+inputI]) - } - wireSum.AddElement(&wireSum, evaluators[w].Evaluate()) - } + // The second half of suffixEq is the eq segment for this round + eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) - wireSum.Mul(&wireSum, &eqSegment[h]) - res[d].Add(&res[d], &wireSum) - iIndex, nextIIndex = nextIIndex, nextIIndex+nbUniqueInputs - } - } - mu.Lock() - for i := range p { - p[i].Add(&p[i], &res[i]) - } - mu.Unlock() - } -{{- else -}} -func(start, end int) { - var step {{ .Config.ElementType }} + p := make([]{{ .Config.ElementType }}, degree) + var mu sync.Mutex + computeAll := func(start, end int) { + var step {{ $inElem }} evaluators := make([]*{{ .Config.EvaluatorQualifier }}{{ $ge }}, nbWires) for w := range nbWires { - evaluators[w] = c.gateEvaluatorPools[w].Get() + evaluators[w] = {{ $pools }}[w].Get() } defer func() { for w := range nbWires { - c.gateEvaluatorPools[w].Put(evaluators[w]) + {{ $pools }}[w].Put(evaluators[w]) } }() res := make([]{{ .Config.ElementType }}, degree) // Input evaluations at m=1,2,...,degree - inputEvals := make([]{{ .Config.ElementType }}, degree*nbUniqueInputs) + inputEvals := make([]{{ $inElem }}, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h for k := range nbUniqueInputs { - inputEvals[k].Set(&c.input[k][evalAt1Index]) - step.Sub(&inputEvals[k], &c.input[k][h]) + inputEvals[k].Set(&{{ $inputs }}[k][evalAt1Index]) + step.Sub(&inputEvals[k], &{{ $inputs }}[k][h]) for d := 1; d < degree; d++ { inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } @@ -920,13 +831,13 @@ func(start, end int) { evaluators[nbWires-1].PushInput(inputEvals[iIndex+inputI]) } var wireSum {{ .Config.ElementType }} - wireSum.Set(evaluators[nbWires-1].Evaluate()) + wireSum.{{ $set }}(evaluators[nbWires-1].Evaluate()) for w := nbWires - 2; w >= 0; w-- { wireSum.Mul(&wireSum, &c.foldingCoeff) for _, inputI := range c.inputIndices[w] { evaluators[w].PushInput(inputEvals[iIndex+inputI]) } - wireSum.Add(&wireSum, evaluators[w].Evaluate()) + wireSum.{{ $add }}(&wireSum, evaluators[w].Evaluate()) } wireSum.Mul(&wireSum, &eqSegment[h]) @@ -941,5 +852,13 @@ func(start, end int) { mu.Unlock() } -{{- end }} -{{- end }} + const minBlockSize = 64 + if sumSize < minBlockSize { + computeAll(0, sumSize) + } else { + c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return p +} +{{ end }} diff --git a/internal/gkr/bls12-377/evaluator.go b/internal/gkr/bls12-377/evaluator.go index d04a84aefa..dc1286a066 100644 --- a/internal/gkr/bls12-377/evaluator.go +++ b/internal/gkr/bls12-377/evaluator.go @@ -124,7 +124,7 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { } // GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type GateEvaluatorPool struct { gate gkrcore.GateBytecode nbIn int diff --git a/internal/gkr/bls12-381/evaluator.go b/internal/gkr/bls12-381/evaluator.go index 23927dcecf..dd5749e197 100644 --- a/internal/gkr/bls12-381/evaluator.go +++ b/internal/gkr/bls12-381/evaluator.go @@ -124,7 +124,7 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { } // GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type GateEvaluatorPool struct { gate gkrcore.GateBytecode nbIn int diff --git a/internal/gkr/bn254/evaluator.go b/internal/gkr/bn254/evaluator.go index 391c0d886f..cd633d5fa0 100644 --- a/internal/gkr/bn254/evaluator.go +++ b/internal/gkr/bn254/evaluator.go @@ -124,7 +124,7 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { } // GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type GateEvaluatorPool struct { gate gkrcore.GateBytecode nbIn int diff --git a/internal/gkr/bw6-761/evaluator.go b/internal/gkr/bw6-761/evaluator.go index fdea34e115..9d9756cffb 100644 --- a/internal/gkr/bw6-761/evaluator.go +++ b/internal/gkr/bw6-761/evaluator.go @@ -124,7 +124,7 @@ func (e *GateEvaluator) Evaluate(top ...fr.Element) *fr.Element { } // GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type GateEvaluatorPool struct { gate gkrcore.GateBytecode nbIn int diff --git a/internal/gkr/koalabear/evaluator.go b/internal/gkr/koalabear/evaluator.go index 669fac82d4..0c8d61f555 100644 --- a/internal/gkr/koalabear/evaluator.go +++ b/internal/gkr/koalabear/evaluator.go @@ -126,7 +126,7 @@ func (e *GateEvaluator) Evaluate(top ...koalabear.Element) *koalabear.Element { } // GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type GateEvaluatorPool struct { gate gkrcore.GateBytecode nbIn int @@ -180,7 +180,7 @@ func (gep *GateEvaluatorPool) DumpAll() { type GateEvaluatorMixed struct { gate gkrcore.GateBytecode vars []extensions.E6 - consts []koalabear.Element // the gate's constants in the base field, multiplied by with MulByElement + consts []koalabear.Element // the gate's constants in the base field, for the extension's methods taking a base field element nbIn int // number of inputs expected pool *polynomial.PoolE6 // pool vars was allocated from, if any } @@ -314,7 +314,7 @@ func (e *GateEvaluatorMixed) Evaluate(top ...extensions.E6) *extensions.E6 { } // GateEvaluatorMixedPool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type GateEvaluatorMixedPool struct { gate gkrcore.GateBytecode nbIn int diff --git a/internal/gkr/rational/evaluator.go b/internal/gkr/rational/evaluator.go index 18a8434b35..c5162288f6 100644 --- a/internal/gkr/rational/evaluator.go +++ b/internal/gkr/rational/evaluator.go @@ -124,7 +124,7 @@ func (e *GateEvaluator) Evaluate(top ...rational.Element) *rational.Element { } // GateEvaluatorPool manages a pool of gate evaluators for a specific gate type. -// All evaluators share the same underlying polynomial.Pool for element slices. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. type GateEvaluatorPool struct { gate gkrcore.GateBytecode nbIn int diff --git a/internal/gkr/rational/sumcheck.go b/internal/gkr/rational/sumcheck.go index 2951a66c7e..5b2a3c30e9 100644 --- a/internal/gkr/rational/sumcheck.go +++ b/internal/gkr/rational/sumcheck.go @@ -168,16 +168,13 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { // Both eqs and input are multilinear, thus linear in Xⱼ. // For any such f, f(m) = m·(f(1) - f(0)) + f(0), and f(0), f(1) are read directly // from the bookkeeping tables. This allows stepwise evaluation at Xⱼ = 1, 2, ..., degree. - // Layout: [eq₀, eq₁, ..., eq_{nbWires-1}, input₀, input₁, ..., input_{nbUniqueInputs-1}] - ml := make([]polynomial.MultiLin, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) sumSize := len(c.eqs[0]) / 2 p := make([]rational.Element, degree) var mu sync.Mutex computeAll := func(start, end int) { + var eqStep rational.Element var step rational.Element evaluators := make([]*GateEvaluator, nbWires) @@ -191,36 +188,42 @@ func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial { }() res := make([]rational.Element, degree) + var term rational.Element - // evaluations of ml, laid out as: - // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), - // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), - // ... - // ml[0](degree, h...), ml[1](degree, h...), ..., ml[len(ml)-1](degree, h...) - mlEvals := make([]rational.Element, degree*len(ml)) + // The evaluations are laid out row by row, row d being Xⱼ = d+1: + // eqEvals[d*nbWires+w] is eqs[w](d+1, h...) and inputEvals[d*nbUniqueInputs+k] is the + // k-th input table at (d+1, h...). + eqEvals := make([]rational.Element, degree*nbWires) + inputEvals := make([]rational.Element, degree*nbUniqueInputs) for h := start; h < end; h++ { evalAt1Index := sumSize + h - for k := range ml { - mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table - step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for k := range nbWires { + eqEvals[k].Set(&c.eqs[k][evalAt1Index]) // evaluation at Xⱼ = 1, taken directly from the table + eqStep.Sub(&eqEvals[k], &c.eqs[k][h]) // eqStep = eqs[k](1) - eqs[k](0) for d := 1; d < degree; d++ { - mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + eqEvals[d*nbWires+k].Add(&eqEvals[(d-1)*nbWires+k], &eqStep) + } + } + for k := range nbUniqueInputs { + inputEvals[k].Set(&c.input[k][evalAt1Index]) + step.Sub(&inputEvals[k], &c.input[k][h]) + for d := 1; d < degree; d++ { + inputEvals[d*nbUniqueInputs+k].Add(&inputEvals[(d-1)*nbUniqueInputs+k], &step) } } - eIndex := 0 // start of the current row's eq evaluations - nextEIndex := len(ml) + eqIndex, inputIndex := 0, 0 // start of the current row in eqEvals and inputEvals for d := range degree { for w := range nbWires { for _, inputI := range c.inputIndices[w] { - evaluators[w].PushInput(mlEvals[eIndex+nbWires+inputI]) + evaluators[w].PushInput(inputEvals[inputIndex+inputI]) } - summand := evaluators[w].Evaluate() - summand.Mul(summand, &mlEvals[eIndex+w]) - res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + term.Mul(&eqEvals[eqIndex+w], evaluators[w].Evaluate()) + res[d].Add(&res[d], &term) // collect contributions into the sum from start to end } - eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + eqIndex += nbWires + inputIndex += nbUniqueInputs } } mu.Lock() @@ -346,7 +349,7 @@ func (c *zeroCheckBase) proveFinalEval(r []rational.Element, extraPolys []polyno return evaluations } -// init derives the folding coefficient, clones the input multilinears, +// init derives the folding coefficient, sets up the input multilinears, // and builds gate evaluator pools — the steps common to all sumcheck-family provers. func (c *zeroCheckBase) init(r *resources, levelI int) { c.levelI = levelI @@ -490,10 +493,9 @@ func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial { nbUniqueInputs := len(c.input) nbWires := len(c.gateEvaluatorPools) - sumSize := len(c.input[0]) / 2 - // The second half of suffixEq is the eq segment for this round eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) p := make([]rational.Element, degree) var mu sync.Mutex From 2b915922eb6c98eee414695c4bbae72abf45d36d Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 6 Oct 2026 00:57:41 -0500 Subject: [PATCH 23/37] refactor: some API tightening Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr.go | 62 +++++++++++++++---- gkr/bls12-377/gkr_test.go | 14 ++++- gkr/bls12-377/sumcheck.go | 2 +- gkr/bls12-377/sumcheck_test.go | 2 +- gkr/bls12-377/test_vector_utils_test.go | 2 +- gkr/bls12-381/gkr.go | 62 +++++++++++++++---- gkr/bls12-381/gkr_test.go | 14 ++++- gkr/bls12-381/sumcheck.go | 2 +- gkr/bls12-381/sumcheck_test.go | 2 +- gkr/bls12-381/test_vector_utils_test.go | 2 +- gkr/bn254/gkr.go | 62 +++++++++++++++---- gkr/bn254/gkr_test.go | 14 ++++- gkr/bn254/sumcheck.go | 2 +- gkr/bn254/sumcheck_test.go | 2 +- gkr/bn254/test_vector_utils_test.go | 2 +- gkr/bw6-761/gkr.go | 62 +++++++++++++++---- gkr/bw6-761/gkr_test.go | 14 ++++- gkr/bw6-761/sumcheck.go | 2 +- gkr/bw6-761/sumcheck_test.go | 2 +- gkr/bw6-761/test_vector_utils_test.go | 2 +- gkr/claims.go | 8 +++ gkr/gkrapi/example_test.go | 19 ++++-- gkr/koalabear/gkr.go | 62 +++++++++++++++---- gkr/koalabear/gkr_test.go | 14 ++++- gkr/koalabear/sumcheck.go | 2 +- gkr/koalabear/sumcheck_test.go | 2 +- gkr/koalabear/test_vector_utils_test.go | 2 +- internal/generator/backend/main.go | 12 +++- .../backend/template/gkr/gkr.go.tmpl | 61 +++++++++++++++--- .../backend/template/gkr/gkr.test.go.tmpl | 12 +++- internal/gkr/bls12-377/evaluator.go | 2 +- internal/gkr/bls12-381/evaluator.go | 2 +- internal/gkr/bn254/evaluator.go | 2 +- internal/gkr/bw6-761/evaluator.go | 2 +- internal/gkr/gkr.go | 36 ++--------- internal/gkr/gkr_test.go | 3 +- internal/gkr/gkrcore/claims.go | 12 ++-- internal/gkr/gkrcore/gate.go | 8 +-- internal/gkr/gkrcore/schedule.go | 32 ++++++++++ internal/gkr/koalabear/evaluator.go | 2 +- internal/gkr/rational/evaluator.go | 2 +- internal/gkr/rational/gkr.go | 62 +++++++++++++++---- internal/gkr/rational/sumcheck.go | 2 +- internal/gkr/rational/sumcheck_test.go | 2 +- .../gkr/rational/sumcheck_test_vector_gen.go | 2 +- internal/gkr/rational/test_vector_gen.go | 2 +- internal/gkr/rational/test_vector_utils.go | 2 +- 47 files changed, 527 insertions(+), 170 deletions(-) create mode 100644 gkr/claims.go diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index add3795df0..06f3b55b9a 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "errors" @@ -15,26 +15,26 @@ import ( "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" evaluator "github.com/consensys/gnark/internal/gkr/bls12-377" "github.com/consensys/gnark/internal/gkr/gkrcore" ) -// Type aliases for bytecode-based GKR types -type ( - Wire = gkrcore.SerializableWire - Circuit = gkrcore.SerializableCircuit -) - -// The goal is to prove/verify evaluations of many instances of the same circuit +// Circuit is a circuit as compiled by gkrapi.API.Compile. +type Circuit = gkrcore.SerializableCircuit -// WireAssignment is the assignment of values to the same wire across many instances of the circuit +// WireAssignment is the assignment of values to the same wire across many instances of the +// circuit: WireAssignment[wireI][instanceI] is the value of wire wireI at instance instanceI. The +// values are fr.Element elements. type WireAssignment []polynomial.MultiLin -type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) +// Proof is a GKR proof: one sumcheck proof per level of the proving schedule, followed by the +// output level's entry. +type Proof []sumcheckProof // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[fr.Element] +type EvaluationClaim = gkr.EvaluationClaim[fr.Element] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -354,6 +354,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { return a } +// NumInstances returns the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumInstances() int { for _, aW := range a { if aW != nil { @@ -363,6 +364,7 @@ func (a WireAssignment) NumInstances() int { panic("empty assignment") } +// NumVars returns log₂ of the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumVars() int { for _, aW := range a { if aW != nil { @@ -382,6 +384,9 @@ func iterateElems(elems []fr.Element, counter *int, yield func(int, *fr.Element) return true } +// Flatten yields the proof's elements by position. For each entry of the proof, in order, it +// yields the coefficients of its partial sum polynomials, then its final evaluations. +// DeserializeProof reads the same order. func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { return func(yield func(int, *fr.Element) bool) { var counter int @@ -397,3 +402,38 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { } } } + +// DeserializeProof is the inverse of Proof.Flatten: it rebuilds the proof for c and schedule from +// its elements. The result aliases serialized. It returns an error if the length of serialized +// does not match c and schedule. +func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { + identity := identityGate() + logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) + if err != nil { + return nil, err + } + + next := func(n int) []fr.Element { + res := serialized[:n] + serialized = serialized[n:] + return res + } + + proof := make(Proof, len(schedule)+1) + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + proof[levelI].finalEvalProof = next(nbUniqueInputs * level.NbOutgoingEvalPoints()) + } else { + degree := levelCircuit.ZeroCheckDegree(level) + proof[levelI].partialSumPolys = make([]polynomial.Polynomial, logNbInstances) + for j := range proof[levelI].partialSumPolys { + proof[levelI].partialSumPolys[j] = next(degree) + } + proof[levelI].finalEvalProof = next(nbUniqueInputs) + } + } + proof[len(schedule)].finalEvalProof = next(len(c.Outputs())) + return proof, nil +} diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index 2480f108e9..069a3b4c29 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "fmt" @@ -73,7 +73,8 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// proveAndVerify proves assignment against schedule and verifies the proof rebuilt by +// DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of // elements in the proof, counted with proof.Flatten(), equals c.ProofSize(schedule, logNbInstances); // tampering instance 0 of any wire with claims makes Check fail; a bad hash makes Verify or Check @@ -87,8 +88,15 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err) assert.NoError(t, proverClaims.Check(assignment)) + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + deserialized, err := DeserializeProof(c, schedule, flattened) + assert.NoError(t, err) + logNbInstances := assignment.NumVars() - verifierClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + verifierClaims, err := Verify(c, schedule, logNbInstances, deserialized, newMessageCounter(1, 1)) assert.NoError(t, err, "proof rejected") assert.Equal(t, proverClaims, verifierClaims) assert.NoError(t, verifierClaims.Check(assignment)) diff --git a/gkr/bls12-377/sumcheck.go b/gkr/bls12-377/sumcheck.go index 110c18a720..193f249bf9 100644 --- a/gkr/bls12-377/sumcheck.go +++ b/gkr/bls12-377/sumcheck.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "errors" diff --git a/gkr/bls12-377/sumcheck_test.go b/gkr/bls12-377/sumcheck_test.go index 9838ed2dd2..0db18c30e3 100644 --- a/gkr/bls12-377/sumcheck_test.go +++ b/gkr/bls12-377/sumcheck_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "fmt" diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index 821aae9797..c9d60f9804 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "fmt" diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index 185ffe98af..54a1a22d72 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "errors" @@ -15,26 +15,26 @@ import ( "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" evaluator "github.com/consensys/gnark/internal/gkr/bls12-381" "github.com/consensys/gnark/internal/gkr/gkrcore" ) -// Type aliases for bytecode-based GKR types -type ( - Wire = gkrcore.SerializableWire - Circuit = gkrcore.SerializableCircuit -) - -// The goal is to prove/verify evaluations of many instances of the same circuit +// Circuit is a circuit as compiled by gkrapi.API.Compile. +type Circuit = gkrcore.SerializableCircuit -// WireAssignment is the assignment of values to the same wire across many instances of the circuit +// WireAssignment is the assignment of values to the same wire across many instances of the +// circuit: WireAssignment[wireI][instanceI] is the value of wire wireI at instance instanceI. The +// values are fr.Element elements. type WireAssignment []polynomial.MultiLin -type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) +// Proof is a GKR proof: one sumcheck proof per level of the proving schedule, followed by the +// output level's entry. +type Proof []sumcheckProof // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[fr.Element] +type EvaluationClaim = gkr.EvaluationClaim[fr.Element] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -354,6 +354,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { return a } +// NumInstances returns the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumInstances() int { for _, aW := range a { if aW != nil { @@ -363,6 +364,7 @@ func (a WireAssignment) NumInstances() int { panic("empty assignment") } +// NumVars returns log₂ of the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumVars() int { for _, aW := range a { if aW != nil { @@ -382,6 +384,9 @@ func iterateElems(elems []fr.Element, counter *int, yield func(int, *fr.Element) return true } +// Flatten yields the proof's elements by position. For each entry of the proof, in order, it +// yields the coefficients of its partial sum polynomials, then its final evaluations. +// DeserializeProof reads the same order. func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { return func(yield func(int, *fr.Element) bool) { var counter int @@ -397,3 +402,38 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { } } } + +// DeserializeProof is the inverse of Proof.Flatten: it rebuilds the proof for c and schedule from +// its elements. The result aliases serialized. It returns an error if the length of serialized +// does not match c and schedule. +func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { + identity := identityGate() + logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) + if err != nil { + return nil, err + } + + next := func(n int) []fr.Element { + res := serialized[:n] + serialized = serialized[n:] + return res + } + + proof := make(Proof, len(schedule)+1) + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + proof[levelI].finalEvalProof = next(nbUniqueInputs * level.NbOutgoingEvalPoints()) + } else { + degree := levelCircuit.ZeroCheckDegree(level) + proof[levelI].partialSumPolys = make([]polynomial.Polynomial, logNbInstances) + for j := range proof[levelI].partialSumPolys { + proof[levelI].partialSumPolys[j] = next(degree) + } + proof[levelI].finalEvalProof = next(nbUniqueInputs) + } + } + proof[len(schedule)].finalEvalProof = next(len(c.Outputs())) + return proof, nil +} diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index 071e02df2c..46cd8bf762 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "fmt" @@ -73,7 +73,8 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// proveAndVerify proves assignment against schedule and verifies the proof rebuilt by +// DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of // elements in the proof, counted with proof.Flatten(), equals c.ProofSize(schedule, logNbInstances); // tampering instance 0 of any wire with claims makes Check fail; a bad hash makes Verify or Check @@ -87,8 +88,15 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err) assert.NoError(t, proverClaims.Check(assignment)) + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + deserialized, err := DeserializeProof(c, schedule, flattened) + assert.NoError(t, err) + logNbInstances := assignment.NumVars() - verifierClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + verifierClaims, err := Verify(c, schedule, logNbInstances, deserialized, newMessageCounter(1, 1)) assert.NoError(t, err, "proof rejected") assert.Equal(t, proverClaims, verifierClaims) assert.NoError(t, verifierClaims.Check(assignment)) diff --git a/gkr/bls12-381/sumcheck.go b/gkr/bls12-381/sumcheck.go index c6b4524de1..0bb48512a2 100644 --- a/gkr/bls12-381/sumcheck.go +++ b/gkr/bls12-381/sumcheck.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "errors" diff --git a/gkr/bls12-381/sumcheck_test.go b/gkr/bls12-381/sumcheck_test.go index 476a8f26d2..3813bd91a4 100644 --- a/gkr/bls12-381/sumcheck_test.go +++ b/gkr/bls12-381/sumcheck_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "fmt" diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index 6cf4564337..8c99eb643a 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "fmt" diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index a75f8395ea..d683d72b4b 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "errors" @@ -15,26 +15,26 @@ import ( "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" evaluator "github.com/consensys/gnark/internal/gkr/bn254" "github.com/consensys/gnark/internal/gkr/gkrcore" ) -// Type aliases for bytecode-based GKR types -type ( - Wire = gkrcore.SerializableWire - Circuit = gkrcore.SerializableCircuit -) - -// The goal is to prove/verify evaluations of many instances of the same circuit +// Circuit is a circuit as compiled by gkrapi.API.Compile. +type Circuit = gkrcore.SerializableCircuit -// WireAssignment is the assignment of values to the same wire across many instances of the circuit +// WireAssignment is the assignment of values to the same wire across many instances of the +// circuit: WireAssignment[wireI][instanceI] is the value of wire wireI at instance instanceI. The +// values are fr.Element elements. type WireAssignment []polynomial.MultiLin -type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) +// Proof is a GKR proof: one sumcheck proof per level of the proving schedule, followed by the +// output level's entry. +type Proof []sumcheckProof // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[fr.Element] +type EvaluationClaim = gkr.EvaluationClaim[fr.Element] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -354,6 +354,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { return a } +// NumInstances returns the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumInstances() int { for _, aW := range a { if aW != nil { @@ -363,6 +364,7 @@ func (a WireAssignment) NumInstances() int { panic("empty assignment") } +// NumVars returns log₂ of the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumVars() int { for _, aW := range a { if aW != nil { @@ -382,6 +384,9 @@ func iterateElems(elems []fr.Element, counter *int, yield func(int, *fr.Element) return true } +// Flatten yields the proof's elements by position. For each entry of the proof, in order, it +// yields the coefficients of its partial sum polynomials, then its final evaluations. +// DeserializeProof reads the same order. func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { return func(yield func(int, *fr.Element) bool) { var counter int @@ -397,3 +402,38 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { } } } + +// DeserializeProof is the inverse of Proof.Flatten: it rebuilds the proof for c and schedule from +// its elements. The result aliases serialized. It returns an error if the length of serialized +// does not match c and schedule. +func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { + identity := identityGate() + logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) + if err != nil { + return nil, err + } + + next := func(n int) []fr.Element { + res := serialized[:n] + serialized = serialized[n:] + return res + } + + proof := make(Proof, len(schedule)+1) + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + proof[levelI].finalEvalProof = next(nbUniqueInputs * level.NbOutgoingEvalPoints()) + } else { + degree := levelCircuit.ZeroCheckDegree(level) + proof[levelI].partialSumPolys = make([]polynomial.Polynomial, logNbInstances) + for j := range proof[levelI].partialSumPolys { + proof[levelI].partialSumPolys[j] = next(degree) + } + proof[levelI].finalEvalProof = next(nbUniqueInputs) + } + } + proof[len(schedule)].finalEvalProof = next(len(c.Outputs())) + return proof, nil +} diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index 5ce63bc6f4..2c195ebbd7 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "fmt" @@ -73,7 +73,8 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// proveAndVerify proves assignment against schedule and verifies the proof rebuilt by +// DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of // elements in the proof, counted with proof.Flatten(), equals c.ProofSize(schedule, logNbInstances); // tampering instance 0 of any wire with claims makes Check fail; a bad hash makes Verify or Check @@ -87,8 +88,15 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err) assert.NoError(t, proverClaims.Check(assignment)) + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + deserialized, err := DeserializeProof(c, schedule, flattened) + assert.NoError(t, err) + logNbInstances := assignment.NumVars() - verifierClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + verifierClaims, err := Verify(c, schedule, logNbInstances, deserialized, newMessageCounter(1, 1)) assert.NoError(t, err, "proof rejected") assert.Equal(t, proverClaims, verifierClaims) assert.NoError(t, verifierClaims.Check(assignment)) diff --git a/gkr/bn254/sumcheck.go b/gkr/bn254/sumcheck.go index 47c8414309..4158dcee3e 100644 --- a/gkr/bn254/sumcheck.go +++ b/gkr/bn254/sumcheck.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "errors" diff --git a/gkr/bn254/sumcheck_test.go b/gkr/bn254/sumcheck_test.go index b6bbfe0ecb..253c54b9e8 100644 --- a/gkr/bn254/sumcheck_test.go +++ b/gkr/bn254/sumcheck_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "fmt" diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 00e9a9344d..c94eb6ca0a 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "fmt" diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index 538d49109a..ad1e8eacdc 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "errors" @@ -15,26 +15,26 @@ import ( "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" evaluator "github.com/consensys/gnark/internal/gkr/bw6-761" "github.com/consensys/gnark/internal/gkr/gkrcore" ) -// Type aliases for bytecode-based GKR types -type ( - Wire = gkrcore.SerializableWire - Circuit = gkrcore.SerializableCircuit -) - -// The goal is to prove/verify evaluations of many instances of the same circuit +// Circuit is a circuit as compiled by gkrapi.API.Compile. +type Circuit = gkrcore.SerializableCircuit -// WireAssignment is the assignment of values to the same wire across many instances of the circuit +// WireAssignment is the assignment of values to the same wire across many instances of the +// circuit: WireAssignment[wireI][instanceI] is the value of wire wireI at instance instanceI. The +// values are fr.Element elements. type WireAssignment []polynomial.MultiLin -type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) +// Proof is a GKR proof: one sumcheck proof per level of the proving schedule, followed by the +// output level's entry. +type Proof []sumcheckProof // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[fr.Element] +type EvaluationClaim = gkr.EvaluationClaim[fr.Element] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -354,6 +354,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { return a } +// NumInstances returns the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumInstances() int { for _, aW := range a { if aW != nil { @@ -363,6 +364,7 @@ func (a WireAssignment) NumInstances() int { panic("empty assignment") } +// NumVars returns log₂ of the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumVars() int { for _, aW := range a { if aW != nil { @@ -382,6 +384,9 @@ func iterateElems(elems []fr.Element, counter *int, yield func(int, *fr.Element) return true } +// Flatten yields the proof's elements by position. For each entry of the proof, in order, it +// yields the coefficients of its partial sum polynomials, then its final evaluations. +// DeserializeProof reads the same order. func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { return func(yield func(int, *fr.Element) bool) { var counter int @@ -397,3 +402,38 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { } } } + +// DeserializeProof is the inverse of Proof.Flatten: it rebuilds the proof for c and schedule from +// its elements. The result aliases serialized. It returns an error if the length of serialized +// does not match c and schedule. +func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { + identity := identityGate() + logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) + if err != nil { + return nil, err + } + + next := func(n int) []fr.Element { + res := serialized[:n] + serialized = serialized[n:] + return res + } + + proof := make(Proof, len(schedule)+1) + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + proof[levelI].finalEvalProof = next(nbUniqueInputs * level.NbOutgoingEvalPoints()) + } else { + degree := levelCircuit.ZeroCheckDegree(level) + proof[levelI].partialSumPolys = make([]polynomial.Polynomial, logNbInstances) + for j := range proof[levelI].partialSumPolys { + proof[levelI].partialSumPolys[j] = next(degree) + } + proof[levelI].finalEvalProof = next(nbUniqueInputs) + } + } + proof[len(schedule)].finalEvalProof = next(len(c.Outputs())) + return proof, nil +} diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index fb1f3b22c5..b2549f0e97 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "fmt" @@ -73,7 +73,8 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// proveAndVerify proves assignment against schedule and verifies the proof rebuilt by +// DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of // elements in the proof, counted with proof.Flatten(), equals c.ProofSize(schedule, logNbInstances); // tampering instance 0 of any wire with claims makes Check fail; a bad hash makes Verify or Check @@ -87,8 +88,15 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err) assert.NoError(t, proverClaims.Check(assignment)) + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + deserialized, err := DeserializeProof(c, schedule, flattened) + assert.NoError(t, err) + logNbInstances := assignment.NumVars() - verifierClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + verifierClaims, err := Verify(c, schedule, logNbInstances, deserialized, newMessageCounter(1, 1)) assert.NoError(t, err, "proof rejected") assert.Equal(t, proverClaims, verifierClaims) assert.NoError(t, verifierClaims.Check(assignment)) diff --git a/gkr/bw6-761/sumcheck.go b/gkr/bw6-761/sumcheck.go index 8dbf8b38dd..98875cea86 100644 --- a/gkr/bw6-761/sumcheck.go +++ b/gkr/bw6-761/sumcheck.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "errors" diff --git a/gkr/bw6-761/sumcheck_test.go b/gkr/bw6-761/sumcheck_test.go index 0c30101b84..6433f253f8 100644 --- a/gkr/bw6-761/sumcheck_test.go +++ b/gkr/bw6-761/sumcheck_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "fmt" diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index 664437952b..a2eac5982f 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "fmt" diff --git a/gkr/claims.go b/gkr/claims.go new file mode 100644 index 0000000000..6ec59ecefa --- /dev/null +++ b/gkr/claims.go @@ -0,0 +1,8 @@ +package gkr + +// EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at +// EvaluationPoint. EvaluationPoint may be aliased across claims; callers must not modify it. +type EvaluationClaim[E any] struct { + EvaluationPoint []E + Evaluation E +} diff --git a/gkr/gkrapi/example_test.go b/gkr/gkrapi/example_test.go index ce611c59f5..3e2a4ade65 100644 --- a/gkr/gkrapi/example_test.go +++ b/gkr/gkrapi/example_test.go @@ -7,13 +7,13 @@ import ( "github.com/consensys/gnark-crypto/ecc/bn254/fr" _ "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" // registers the MiMC hash used below gcHash "github.com/consensys/gnark-crypto/hash" - bn254 "github.com/consensys/gnark/gkr/bn254" + gkrbn254 "github.com/consensys/gnark/gkr/bn254" "github.com/consensys/gnark/gkr/gkrapi" ) // Example builds a single-multiplication-gate circuit natively through the API, compiles it for -// BN254, proves and verifies two instances of it with gkr/bn254, and checks the claims against -// the assignment. +// BN254, proves two instances of it with gkr/bn254, flattens and deserializes the proof, verifies +// it, and checks the claims against the assignment. func Example() { api := gkrapi.New() x := api.NewInput() @@ -24,16 +24,23 @@ func Example() { circuit, schedule, err := api.Compile(gkrapi.PrimeField(ecc.BN254.ScalarField()), gkrapi.ConsolidateAll) assertNoError(err) - assignment := make(bn254.WireAssignment, len(circuit)) + assignment := make(gkrbn254.WireAssignment, len(circuit)) assignment[x] = []fr.Element{fr.NewElement(2), fr.NewElement(3)} assignment[y] = []fr.Element{fr.NewElement(5), fr.NewElement(7)} // assignment[z] is left nil: Prove computes it. - proof, proverClaims, err := bn254.Prove(circuit, schedule, assignment, gcHash.MIMC_BN254.New()) + proof, proverClaims, err := gkrbn254.Prove(circuit, schedule, assignment, gcHash.MIMC_BN254.New()) assertNoError(err) assertNoError(proverClaims.Check(assignment)) - verifierClaims, err := bn254.Verify(circuit, schedule, 1, proof, gcHash.MIMC_BN254.New()) + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + deserialized, err := gkrbn254.DeserializeProof(circuit, schedule, flattened) + assertNoError(err) + + verifierClaims, err := gkrbn254.Verify(circuit, schedule, 1, deserialized, gcHash.MIMC_BN254.New()) assertNoError(err) assertNoError(verifierClaims.Check(assignment)) diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index 83990c33c6..9acc41d211 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrkoalabear import ( "errors" @@ -17,26 +17,26 @@ import ( basePolynomial "github.com/consensys/gnark-crypto/field/koalabear/polynomial" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" evaluator "github.com/consensys/gnark/internal/gkr/koalabear" ) -// Type aliases for bytecode-based GKR types -type ( - Wire = gkrcore.SerializableWire - Circuit = gkrcore.SerializableCircuit -) - -// The goal is to prove/verify evaluations of many instances of the same circuit +// Circuit is a circuit as compiled by gkrapi.API.Compile. +type Circuit = gkrcore.SerializableCircuit -// WireAssignment is the assignment of values to the same wire across many instances of the circuit +// WireAssignment is the assignment of values to the same wire across many instances of the +// circuit: WireAssignment[wireI][instanceI] is the value of wire wireI at instance instanceI. The +// values are koalabear.Element elements; the claims that Prove and Verify return are over extensions.E6. type WireAssignment []basePolynomial.MultiLin -type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) +// Proof is a GKR proof: one sumcheck proof per level of the proving schedule, followed by the +// output level's entry. +type Proof []sumcheckProof // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[extensions.E6] +type EvaluationClaim = gkr.EvaluationClaim[extensions.E6] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -366,6 +366,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { return a } +// NumInstances returns the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumInstances() int { for _, aW := range a { if aW != nil { @@ -375,6 +376,7 @@ func (a WireAssignment) NumInstances() int { panic("empty assignment") } +// NumVars returns log₂ of the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumVars() int { for _, aW := range a { if aW != nil { @@ -394,6 +396,9 @@ func iterateElems(elems []extensions.E6, counter *int, yield func(int, *extensio return true } +// Flatten yields the proof's elements by position. For each entry of the proof, in order, it +// yields the coefficients of its partial sum polynomials, then its final evaluations. +// DeserializeProof reads the same order. func (p Proof) Flatten() iter.Seq2[int, *extensions.E6] { return func(yield func(int, *extensions.E6) bool) { var counter int @@ -409,3 +414,38 @@ func (p Proof) Flatten() iter.Seq2[int, *extensions.E6] { } } } + +// DeserializeProof is the inverse of Proof.Flatten: it rebuilds the proof for c and schedule from +// its elements. The result aliases serialized. It returns an error if the length of serialized +// does not match c and schedule. +func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []extensions.E6) (Proof, error) { + identity := identityGate() + logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) + if err != nil { + return nil, err + } + + next := func(n int) []extensions.E6 { + res := serialized[:n] + serialized = serialized[n:] + return res + } + + proof := make(Proof, len(schedule)+1) + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + proof[levelI].finalEvalProof = next(nbUniqueInputs * level.NbOutgoingEvalPoints()) + } else { + degree := levelCircuit.ZeroCheckDegree(level) + proof[levelI].partialSumPolys = make([]polynomial.PolynomialE6, logNbInstances) + for j := range proof[levelI].partialSumPolys { + proof[levelI].partialSumPolys[j] = next(degree) + } + proof[levelI].finalEvalProof = next(nbUniqueInputs) + } + } + proof[len(schedule)].finalEvalProof = next(len(c.Outputs())) + return proof, nil +} diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go index 58b02d9652..010eb37598 100644 --- a/gkr/koalabear/gkr_test.go +++ b/gkr/koalabear/gkr_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrkoalabear import ( "fmt" @@ -73,7 +73,8 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// proveAndVerify proves assignment against schedule and verifies the proof rebuilt by +// DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of // elements in the proof, counted with proof.Flatten(), equals c.ProofSize(schedule, logNbInstances); // tampering instance 0 of any wire with claims makes Check fail; a bad hash makes Verify or Check @@ -87,8 +88,15 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err) assert.NoError(t, proverClaims.Check(assignment)) + var flattened []extensions.E6 + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + deserialized, err := DeserializeProof(c, schedule, flattened) + assert.NoError(t, err) + logNbInstances := assignment.NumVars() - verifierClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + verifierClaims, err := Verify(c, schedule, logNbInstances, deserialized, newMessageCounter(1, 1)) assert.NoError(t, err, "proof rejected") assert.Equal(t, proverClaims, verifierClaims) assert.NoError(t, verifierClaims.Check(assignment)) diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index 446a3e17b4..30c4cceb05 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrkoalabear import ( "errors" diff --git a/gkr/koalabear/sumcheck_test.go b/gkr/koalabear/sumcheck_test.go index 0427849edd..621ea5a83e 100644 --- a/gkr/koalabear/sumcheck_test.go +++ b/gkr/koalabear/sumcheck_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrkoalabear import ( "fmt" diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index fbf592e284..88ba671630 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrkoalabear import ( "fmt" diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index ef8b406f8e..f617cc8fd5 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -337,7 +337,7 @@ func generateGkrBackend(cfg gkrConfig) error { }...) } - if err := bgen.Generate(cfg, "gkr", "./template/gkr/", entries...); err != nil { + if err := bgen.Generate(cfg, cfg.PackageName(), "./template/gkr/", entries...); err != nil { return err } @@ -410,7 +410,15 @@ func (c gkrConfig) BasePolynomial() string { return "polynomial" } -// FieldID is the name of the field's ecc.ID constant, e.g. BLS12_377. +// PackageName is the name of the generated packages: "gkr" followed by GkrPackageName with its +// hyphens removed, e.g. gkrbls12377. The name gkr alone clashes with github.com/consensys/gnark/gkr, +// and GkrPackageName alone with gnark-crypto's packages of the same name. +func (c gkrConfig) PackageName() string { + return "gkr" + strings.ReplaceAll(c.GkrPackageName, "-", "") +} + +// FieldID names the field in gnark-crypto's identifiers: the ecc.ID of a curve, and the hash +// registered for the field, as in POSEIDON2_KOALABEAR. For example BLS12_377. func (c gkrConfig) FieldID() string { return strings.ToUpper(strings.ReplaceAll(c.GkrPackageName, "-", "_")) } diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index 66927d946a..f763fad24d 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -13,28 +13,29 @@ import ( {{- end }} "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" {{- if .EvaluatorQualifier }} evaluator "github.com/consensys/gnark/internal/gkr/{{.GkrPackageName}}" {{- end }} "github.com/consensys/gnark/internal/gkr/gkrcore" ) -// Type aliases for bytecode-based GKR types -type ( - Wire = gkrcore.SerializableWire - Circuit = gkrcore.SerializableCircuit -) - -// The goal is to prove/verify evaluations of many instances of the same circuit +// Circuit is a circuit as compiled by gkrapi.API.Compile. +type Circuit = gkrcore.SerializableCircuit -// WireAssignment is the assignment of values to the same wire across many instances of the circuit +// WireAssignment is the assignment of values to the same wire across many instances of the +// circuit: WireAssignment[wireI][instanceI] is the value of wire wireI at instance instanceI. The +// values are {{ .BaseElementType }} elements +{{- if .Mixed }}; the claims that Prove and Verify return are over {{ .ElementType }}{{ end }}. type WireAssignment []{{ .BasePolynomial }}.MultiLin -type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) +// Proof is a GKR proof: one sumcheck proof per level of the proving schedule, followed by the +// output level's entry. +type Proof []sumcheckProof // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[{{ .ElementType }}] +type EvaluationClaim = gkr.EvaluationClaim[{{ .ElementType }}] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -384,6 +385,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { return a } +// NumInstances returns the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumInstances() int { for _, aW := range a { if aW != nil { @@ -393,6 +395,7 @@ func (a WireAssignment) NumInstances() int { panic("empty assignment") } +// NumVars returns log₂ of the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumVars() int { for _, aW := range a { if aW != nil { @@ -412,6 +415,9 @@ func iterateElems(elems []{{ .ElementType }}, counter *int, yield func(int, *{{ return true } +// Flatten yields the proof's elements by position. For each entry of the proof, in order, it +// yields the coefficients of its partial sum polynomials, then its final evaluations. +// DeserializeProof reads the same order. func (p Proof) Flatten() iter.Seq2[int, *{{ .ElementType }}] { return func(yield func(int, *{{ .ElementType }}) bool) { var counter int @@ -427,3 +433,38 @@ func (p Proof) Flatten() iter.Seq2[int, *{{ .ElementType }}] { } } } + +// DeserializeProof is the inverse of Proof.Flatten: it rebuilds the proof for c and schedule from +// its elements. The result aliases serialized. It returns an error if the length of serialized +// does not match c and schedule. +func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []{{ .ElementType }}) (Proof, error) { + identity := identityGate() + logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) + if err != nil { + return nil, err + } + + next := func(n int) []{{ .ElementType }} { + res := serialized[:n] + serialized = serialized[n:] + return res + } + + proof := make(Proof, len(schedule)+1) + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + proof[levelI].finalEvalProof = next(nbUniqueInputs * level.NbOutgoingEvalPoints()) + } else { + degree := levelCircuit.ZeroCheckDegree(level) + proof[levelI].partialSumPolys = make([]polynomial.Polynomial{{ .ExtensionSuffix }}, logNbInstances) + for j := range proof[levelI].partialSumPolys { + proof[levelI].partialSumPolys[j] = next(degree) + } + proof[levelI].finalEvalProof = next(nbUniqueInputs) + } + } + proof[len(schedule)].finalEvalProof = next(len(c.Outputs())) + return proof, nil +} diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index cf46baf160..4fe9e70818 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -70,7 +70,8 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// proveAndVerify proves assignment against schedule and verifies the proof rebuilt by +// DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of // elements in the proof, counted with proof.Flatten(), equals c.ProofSize(schedule, logNbInstances); // tampering instance 0 of any wire with claims makes Check fail; a bad hash makes Verify or Check @@ -84,8 +85,15 @@ func proveAndVerify(t *testing.T, c Circuit, schedule constraint.GkrProvingSched assert.NoError(t, err) assert.NoError(t, proverClaims.Check(assignment)) + var flattened []{{ .ElementType }} + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + deserialized, err := DeserializeProof(c, schedule, flattened) + assert.NoError(t, err) + logNbInstances := assignment.NumVars() - verifierClaims, err := Verify(c, schedule, logNbInstances, proof, newMessageCounter(1, 1)) + verifierClaims, err := Verify(c, schedule, logNbInstances, deserialized, newMessageCounter(1, 1)) assert.NoError(t, err, "proof rejected") assert.Equal(t, proverClaims, verifierClaims) assert.NoError(t, verifierClaims.Check(assignment)) diff --git a/internal/gkr/bls12-377/evaluator.go b/internal/gkr/bls12-377/evaluator.go index dc1286a066..023b788b53 100644 --- a/internal/gkr/bls12-377/evaluator.go +++ b/internal/gkr/bls12-377/evaluator.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "fmt" diff --git a/internal/gkr/bls12-381/evaluator.go b/internal/gkr/bls12-381/evaluator.go index dd5749e197..ab8be70e3b 100644 --- a/internal/gkr/bls12-381/evaluator.go +++ b/internal/gkr/bls12-381/evaluator.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "fmt" diff --git a/internal/gkr/bn254/evaluator.go b/internal/gkr/bn254/evaluator.go index cd633d5fa0..47716f6aa6 100644 --- a/internal/gkr/bn254/evaluator.go +++ b/internal/gkr/bn254/evaluator.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "fmt" diff --git a/internal/gkr/bw6-761/evaluator.go b/internal/gkr/bw6-761/evaluator.go index 9d9756cffb..7ce76941b0 100644 --- a/internal/gkr/bw6-761/evaluator.go +++ b/internal/gkr/bw6-761/evaluator.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "fmt" diff --git a/internal/gkr/gkr.go b/internal/gkr/gkr.go index 5a8aa55b05..f14ec75c2f 100644 --- a/internal/gkr/gkr.go +++ b/internal/gkr/gkr.go @@ -353,37 +353,6 @@ func (p Proof) Serialize() []frontend.Variable { return res } -// ComputeLogNbInstances derives n such that the number of instances is 2ⁿ -// from the size of the proof and the circuit/schedule structure. -func ComputeLogNbInstances(circuit Circuit, schedule constraint.GkrProvingSchedule, serializedProofLen int) int { - serializedProofLen -= len(circuit.Outputs()) - identity := identityGate() - perVar := 0 - for levelI, level := range schedule { - levelCircuit := circuit.LevelCircuit(schedule, levelI, identity) - nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) - switch level.(type) { - case *constraint.GkrSkipLevel: - serializedProofLen -= nbUniqueInputs * level.NbOutgoingEvalPoints() - default: - perVar += levelCircuit.ZeroCheckDegree(level) - serializedProofLen -= nbUniqueInputs - } - } - if perVar == 0 { - if serializedProofLen == 0 { - return -1 - } - } else { - res := serializedProofLen / perVar - if res*perVar == serializedProofLen { - return res - } - } - - panic("cannot compute logNbInstances") -} - type variablesReader []frontend.Variable func (r *variablesReader) nextN(n int) []frontend.Variable { @@ -398,7 +367,10 @@ func (r *variablesReader) hasNextN(n int) bool { func DeserializeProof(circuit Circuit, schedule constraint.GkrProvingSchedule, serializedProof []frontend.Variable) (Proof, error) { proof := make(Proof, len(schedule)+1) - logNbInstances := ComputeLogNbInstances(circuit, schedule, len(serializedProof)) + logNbInstances, err := gkrcore.ComputeLogNbInstances(circuit, schedule, len(serializedProof), identityGate()) + if err != nil { + return nil, err + } identity := identityGate() reader := variablesReader(serializedProof) diff --git a/internal/gkr/gkr_test.go b/internal/gkr/gkr_test.go index 6e30eab524..36b0af6f2d 100644 --- a/internal/gkr/gkr_test.go +++ b/internal/gkr/gkr_test.go @@ -263,7 +263,8 @@ func TestLogNbInstances(t *testing.T) { testCase, err := getTestCase(path) assert.NoError(t, err) serializedProof := testCase.Proof.Serialize() - logNbInstances := ComputeLogNbInstances(testCase.Circuit, testCase.Schedule, len(serializedProof)) + logNbInstances, err := gkrcore.ComputeLogNbInstances(testCase.Circuit, testCase.Schedule, len(serializedProof), identityGate()) + assert.NoError(t, err) if hasSumcheck(testCase.Schedule) { assert.Equal(t, 1, logNbInstances) } else { diff --git a/internal/gkr/gkrcore/claims.go b/internal/gkr/gkrcore/claims.go index b20fdd8f5f..b363013b77 100644 --- a/internal/gkr/gkrcore/claims.go +++ b/internal/gkr/gkrcore/claims.go @@ -1,13 +1,13 @@ package gkrcore -import "github.com/consensys/gnark/constraint" +import ( + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" +) // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at -// EvaluationPoint. EvaluationPoint may be aliased across claims; callers must not modify it. -type EvaluationClaim[F any] struct { - EvaluationPoint []F - Evaluation F -} +// EvaluationPoint. See gkr.EvaluationClaim. +type EvaluationClaim[F any] = gkr.EvaluationClaim[F] // AppendOutputClaims appends (point, evals[i]) to claims[w] for every output wire w of c that // include selects, in c.Outputs() order. diff --git a/internal/gkr/gkrcore/gate.go b/internal/gkr/gkrcore/gate.go index 213d2b3d6b..29d72f0b68 100644 --- a/internal/gkr/gkrcore/gate.go +++ b/internal/gkr/gkrcore/gate.go @@ -185,11 +185,11 @@ func (gc *gateCompiler) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) f operands := append([]frontend.Variable{i1, i2}, in...) minuend, minuendIsConst := constantValue(operands[0]) - sum := new(big.Int) // of the constant subtrahends + subtrahends := new(big.Int) nbKept := 1 for _, v := range operands[1:] { if c, ok := constantValue(v); ok { - sum.Add(sum, c) + subtrahends.Add(subtrahends, c) } else { operands[nbKept] = v nbKept++ @@ -199,13 +199,13 @@ func (gc *gateCompiler) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) f operands = operands[:nbKept] if minuendIsConst { - diff := minuend.Sub(minuend, sum) + diff := minuend.Sub(minuend, subtrahends) if nbKept == 1 { return diff } operands[0] = diff } else if hasConst { - operands = append(operands, sum) + operands = append(operands, subtrahends) } return gc.addInstruction(OpSub, operands...) } diff --git a/internal/gkr/gkrcore/schedule.go b/internal/gkr/gkrcore/schedule.go index 991a987064..b919f6bc08 100644 --- a/internal/gkr/gkrcore/schedule.go +++ b/internal/gkr/gkrcore/schedule.go @@ -2,6 +2,7 @@ package gkrcore import ( "cmp" + "errors" "fmt" "slices" @@ -518,3 +519,34 @@ func CollectOutgoingEvalPoints[F any](level *constraint.GkrSkipLevel, levelI int outgoingEvalPoints[levelI] = outPoints return outPoints } + +// ComputeLogNbInstances derives n such that the number of instances is 2ⁿ from the length of the +// serialized proof and the circuit/schedule structure, identity being the gate that level 0's view +// of the circuit uses. It returns -1 if the schedule has no level that runs a sumcheck, as then +// the proof's length does not depend on n, and an error if no n matches the length. +func ComputeLogNbInstances[G any](c Circuit[G], schedule constraint.GkrProvingSchedule, serializedProofLen int, identity Gate[G]) (int, error) { + serializedProofLen -= len(c.Outputs()) + perVar := 0 + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + switch level.(type) { + case *constraint.GkrSkipLevel: + serializedProofLen -= nbUniqueInputs * level.NbOutgoingEvalPoints() + default: + perVar += levelCircuit.ZeroCheckDegree(level) + serializedProofLen -= nbUniqueInputs + } + } + if serializedProofLen < 0 { + return 0, errors.New("proof too short") + } + if perVar == 0 { + if serializedProofLen == 0 { + return -1, nil + } + } else if serializedProofLen%perVar == 0 { + return serializedProofLen / perVar, nil + } + return 0, errors.New("proof length matches no number of instances") +} diff --git a/internal/gkr/koalabear/evaluator.go b/internal/gkr/koalabear/evaluator.go index 0c8d61f555..ab4b51aa27 100644 --- a/internal/gkr/koalabear/evaluator.go +++ b/internal/gkr/koalabear/evaluator.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrkoalabear import ( "fmt" diff --git a/internal/gkr/rational/evaluator.go b/internal/gkr/rational/evaluator.go index c5162288f6..fc31dfd5d1 100644 --- a/internal/gkr/rational/evaluator.go +++ b/internal/gkr/rational/evaluator.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "fmt" diff --git a/internal/gkr/rational/gkr.go b/internal/gkr/rational/gkr.go index 542b57c0bd..471c67f734 100644 --- a/internal/gkr/rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "errors" @@ -13,27 +13,27 @@ import ( "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/rational" "github.com/consensys/gnark/internal/rational/polynomial" ) -// Type aliases for bytecode-based GKR types -type ( - Wire = gkrcore.SerializableWire - Circuit = gkrcore.SerializableCircuit -) - -// The goal is to prove/verify evaluations of many instances of the same circuit +// Circuit is a circuit as compiled by gkrapi.API.Compile. +type Circuit = gkrcore.SerializableCircuit -// WireAssignment is the assignment of values to the same wire across many instances of the circuit +// WireAssignment is the assignment of values to the same wire across many instances of the +// circuit: WireAssignment[wireI][instanceI] is the value of wire wireI at instance instanceI. The +// values are rational.Element elements. type WireAssignment []polynomial.MultiLin -type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) +// Proof is a GKR proof: one sumcheck proof per level of the proving schedule, followed by the +// output level's entry. +type Proof []sumcheckProof // EvaluationClaim is an assertion that a wire's multilinear extension evaluates to Evaluation at // EvaluationPoint. -type EvaluationClaim = gkrcore.EvaluationClaim[rational.Element] +type EvaluationClaim = gkr.EvaluationClaim[rational.Element] // Claims are the evaluation claims on circuit inputs and outputs that Prove and Verify return, by // wire. The consumption order within a wire's slice is up to the caller. @@ -353,6 +353,7 @@ func (a WireAssignment) Complete(circuit Circuit) WireAssignment { return a } +// NumInstances returns the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumInstances() int { for _, aW := range a { if aW != nil { @@ -362,6 +363,7 @@ func (a WireAssignment) NumInstances() int { panic("empty assignment") } +// NumVars returns log₂ of the number of instances a assigns values to. It panics if a is empty. func (a WireAssignment) NumVars() int { for _, aW := range a { if aW != nil { @@ -381,6 +383,9 @@ func iterateElems(elems []rational.Element, counter *int, yield func(int, *ratio return true } +// Flatten yields the proof's elements by position. For each entry of the proof, in order, it +// yields the coefficients of its partial sum polynomials, then its final evaluations. +// DeserializeProof reads the same order. func (p Proof) Flatten() iter.Seq2[int, *rational.Element] { return func(yield func(int, *rational.Element) bool) { var counter int @@ -396,3 +401,38 @@ func (p Proof) Flatten() iter.Seq2[int, *rational.Element] { } } } + +// DeserializeProof is the inverse of Proof.Flatten: it rebuilds the proof for c and schedule from +// its elements. The result aliases serialized. It returns an error if the length of serialized +// does not match c and schedule. +func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []rational.Element) (Proof, error) { + identity := identityGate() + logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) + if err != nil { + return nil, err + } + + next := func(n int) []rational.Element { + res := serialized[:n] + serialized = serialized[n:] + return res + } + + proof := make(Proof, len(schedule)+1) + for levelI, level := range schedule { + levelCircuit := c.LevelCircuit(schedule, levelI, identity) + nbUniqueInputs := len(levelCircuit.UniqueGateInputs(level)) + if _, isSkip := level.(*constraint.GkrSkipLevel); isSkip { + proof[levelI].finalEvalProof = next(nbUniqueInputs * level.NbOutgoingEvalPoints()) + } else { + degree := levelCircuit.ZeroCheckDegree(level) + proof[levelI].partialSumPolys = make([]polynomial.Polynomial, logNbInstances) + for j := range proof[levelI].partialSumPolys { + proof[levelI].partialSumPolys[j] = next(degree) + } + proof[levelI].finalEvalProof = next(nbUniqueInputs) + } + } + proof[len(schedule)].finalEvalProof = next(len(c.Outputs())) + return proof, nil +} diff --git a/internal/gkr/rational/sumcheck.go b/internal/gkr/rational/sumcheck.go index 5b2a3c30e9..7719fb1d99 100644 --- a/internal/gkr/rational/sumcheck.go +++ b/internal/gkr/rational/sumcheck.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "errors" diff --git a/internal/gkr/rational/sumcheck_test.go b/internal/gkr/rational/sumcheck_test.go index aa80054e70..9280007b99 100644 --- a/internal/gkr/rational/sumcheck_test.go +++ b/internal/gkr/rational/sumcheck_test.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "fmt" diff --git a/internal/gkr/rational/sumcheck_test_vector_gen.go b/internal/gkr/rational/sumcheck_test_vector_gen.go index 056b64d385..eeb5d658ca 100644 --- a/internal/gkr/rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/rational/sumcheck_test_vector_gen.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "encoding/json" diff --git a/internal/gkr/rational/test_vector_gen.go b/internal/gkr/rational/test_vector_gen.go index 517fb29a2d..8d6958ee11 100644 --- a/internal/gkr/rational/test_vector_gen.go +++ b/internal/gkr/rational/test_vector_gen.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "encoding/json" diff --git a/internal/gkr/rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go index b22ea13cd3..d0fe08e212 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -3,7 +3,7 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "fmt" From 6c9963157057df907c98a6b9ebb59c4855af0b9b Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 6 Oct 2026 12:50:34 -0500 Subject: [PATCH 24/37] test: end to end Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr_test.go | 53 ++++++++++++++++ gkr/bls12-381/gkr_test.go | 53 ++++++++++++++++ gkr/bn254/gkr_test.go | 53 ++++++++++++++++ gkr/bw6-761/gkr_test.go | 53 ++++++++++++++++ gkr/koalabear/gkr_test.go | 61 ++++++++++++++++++ .../backend/template/gkr/gkr.test.go.tmpl | 63 +++++++++++++++++++ internal/gkr/gkrcore/transcript_test.go | 39 ++++++++++++ internal/gkr/gkrtesting/gkrtesting.go | 8 +++ 8 files changed, 383 insertions(+) create mode 100644 internal/gkr/gkrcore/transcript_test.go diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index 069a3b4c29..a9d10ce8aa 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" "time" @@ -21,9 +22,11 @@ import ( _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" + "github.com/consensys/gnark/gkr/gkrapi" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_377.ScalarField())) @@ -73,6 +76,56 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } +// TestEndToEndPoseidon2 compiles a circuit with gkrapi, then proves and verifies it with Poseidon2, +// the proof being rebuilt by DeserializeProof from the elements of its Flatten, and checks that +// tampering with the proof is rejected. +func TestEndToEndPoseidon2(t *testing.T) { + const nbInstances = 16 + + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + api.Export(api.Gate(gkrtesting.ConstantPositionsGate, x, y)) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BLS12_377.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + assignment := make(WireAssignment, len(circuit)) + for _, w := range []gkr.Variable{x, y} { + assignment[w] = make([]fr.Element, nbInstances) + fr.Vector(assignment[w]).MustSetRandom() + } + // The output column is left nil: Prove computes it. + + proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BLS12_377.New()) + require.NoError(t, err) + + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + + verify := func(serialized []fr.Element) (Claims, error) { + deserialized, err := DeserializeProof(circuit, schedule, serialized) + if err != nil { + return nil, err + } + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BLS12_377.New()) + if err != nil { + return nil, err + } + return claims, claims.Check(assignment) + } + + verifierClaims, err := verify(flattened) + require.NoError(t, err) + assert.Equal(t, proverClaims, verifierClaims) + + tampered := slices.Clone(flattened) + tampered[0].Add(&tampered[0], &one) + _, err = verify(tampered) + assert.Error(t, err, "tampered proof accepted") +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index 46cd8bf762..965d996a49 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" "time" @@ -21,9 +22,11 @@ import ( _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" + "github.com/consensys/gnark/gkr/gkrapi" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_381.ScalarField())) @@ -73,6 +76,56 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } +// TestEndToEndPoseidon2 compiles a circuit with gkrapi, then proves and verifies it with Poseidon2, +// the proof being rebuilt by DeserializeProof from the elements of its Flatten, and checks that +// tampering with the proof is rejected. +func TestEndToEndPoseidon2(t *testing.T) { + const nbInstances = 16 + + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + api.Export(api.Gate(gkrtesting.ConstantPositionsGate, x, y)) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BLS12_381.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + assignment := make(WireAssignment, len(circuit)) + for _, w := range []gkr.Variable{x, y} { + assignment[w] = make([]fr.Element, nbInstances) + fr.Vector(assignment[w]).MustSetRandom() + } + // The output column is left nil: Prove computes it. + + proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BLS12_381.New()) + require.NoError(t, err) + + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + + verify := func(serialized []fr.Element) (Claims, error) { + deserialized, err := DeserializeProof(circuit, schedule, serialized) + if err != nil { + return nil, err + } + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BLS12_381.New()) + if err != nil { + return nil, err + } + return claims, claims.Check(assignment) + } + + verifierClaims, err := verify(flattened) + require.NoError(t, err) + assert.Equal(t, proverClaims, verifierClaims) + + tampered := slices.Clone(flattened) + tampered[0].Add(&tampered[0], &one) + _, err = verify(tampered) + assert.Error(t, err, "tampered proof accepted") +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index 2c195ebbd7..c6e2e71d65 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" "time" @@ -21,9 +22,11 @@ import ( _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" + "github.com/consensys/gnark/gkr/gkrapi" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) @@ -73,6 +76,56 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } +// TestEndToEndPoseidon2 compiles a circuit with gkrapi, then proves and verifies it with Poseidon2, +// the proof being rebuilt by DeserializeProof from the elements of its Flatten, and checks that +// tampering with the proof is rejected. +func TestEndToEndPoseidon2(t *testing.T) { + const nbInstances = 16 + + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + api.Export(api.Gate(gkrtesting.ConstantPositionsGate, x, y)) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BN254.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + assignment := make(WireAssignment, len(circuit)) + for _, w := range []gkr.Variable{x, y} { + assignment[w] = make([]fr.Element, nbInstances) + fr.Vector(assignment[w]).MustSetRandom() + } + // The output column is left nil: Prove computes it. + + proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BN254.New()) + require.NoError(t, err) + + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + + verify := func(serialized []fr.Element) (Claims, error) { + deserialized, err := DeserializeProof(circuit, schedule, serialized) + if err != nil { + return nil, err + } + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BN254.New()) + if err != nil { + return nil, err + } + return claims, claims.Check(assignment) + } + + verifierClaims, err := verify(flattened) + require.NoError(t, err) + assert.Equal(t, proverClaims, verifierClaims) + + tampered := slices.Clone(flattened) + tampered[0].Add(&tampered[0], &one) + _, err = verify(tampered) + assert.Error(t, err, "tampered proof accepted") +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index b2549f0e97..7853035d0b 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" "time" @@ -21,9 +22,11 @@ import ( _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" + "github.com/consensys/gnark/gkr/gkrapi" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BW6_761.ScalarField())) @@ -73,6 +76,56 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } +// TestEndToEndPoseidon2 compiles a circuit with gkrapi, then proves and verifies it with Poseidon2, +// the proof being rebuilt by DeserializeProof from the elements of its Flatten, and checks that +// tampering with the proof is rejected. +func TestEndToEndPoseidon2(t *testing.T) { + const nbInstances = 16 + + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + api.Export(api.Gate(gkrtesting.ConstantPositionsGate, x, y)) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BW6_761.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + assignment := make(WireAssignment, len(circuit)) + for _, w := range []gkr.Variable{x, y} { + assignment[w] = make([]fr.Element, nbInstances) + fr.Vector(assignment[w]).MustSetRandom() + } + // The output column is left nil: Prove computes it. + + proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BW6_761.New()) + require.NoError(t, err) + + var flattened []fr.Element + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + + verify := func(serialized []fr.Element) (Claims, error) { + deserialized, err := DeserializeProof(circuit, schedule, serialized) + if err != nil { + return nil, err + } + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BW6_761.New()) + if err != nil { + return nil, err + } + return claims, claims.Check(assignment) + } + + verifierClaims, err := verify(flattened) + require.NoError(t, err) + assert.Equal(t, proverClaims, verifierClaims) + + tampered := slices.Clone(flattened) + tampered[0].Add(&tampered[0], &one) + _, err = verify(tampered) + assert.Error(t, err, "tampered proof accepted") +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go index 010eb37598..1a359ef157 100644 --- a/gkr/koalabear/gkr_test.go +++ b/gkr/koalabear/gkr_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" "time" @@ -21,9 +22,11 @@ import ( _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" + "github.com/consensys/gnark/gkr/gkrapi" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var cache = gkrtesting.NewCache(gkr.KoalaBearE6()) @@ -73,6 +76,64 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } +// TestEndToEndPoseidon2 compiles a circuit with gkrapi, then proves and verifies it with Poseidon2, +// the proof being rebuilt by DeserializeProof from the elements of its Flatten, and checks that +// tampering with the proof is rejected. +func TestEndToEndPoseidon2(t *testing.T) { + const nbInstances = 16 + + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + api.Export(api.Gate(gkrtesting.ConstantPositionsGate, x, y)) + circuit, schedule, err := api.Compile(gkr.KoalaBearE6(), gkrapi.ConsolidateAll) + require.NoError(t, err) + + assignment := make(WireAssignment, len(circuit)) + for _, w := range []gkr.Variable{x, y} { + assignment[w] = make([]koalabear.Element, nbInstances) + koalabear.Vector(assignment[w]).MustSetRandom() + } + // The output column is left nil: Prove computes it. + + proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_KOALABEAR.New()) + require.NoError(t, err) + + var flattened []extensions.E6 + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + + verify := func(serialized []extensions.E6) (Claims, error) { + deserialized, err := DeserializeProof(circuit, schedule, serialized) + if err != nil { + return nil, err + } + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_KOALABEAR.New()) + if err != nil { + return nil, err + } + return claims, claims.Check(assignment) + } + + verifierClaims, err := verify(flattened) + require.NoError(t, err) + assert.Equal(t, proverClaims, verifierClaims) + + outsideBaseField := false + for _, wireClaims := range verifierClaims { + for _, claim := range wireClaims { + outsideBaseField = outsideBaseField || claim.Evaluation.BigInt(nil) == nil + } + } + assert.True(t, outsideBaseField, "no claim is outside the base field: the challenges are not extension elements") + + tampered := slices.Clone(flattened) + tampered[0].Add(&tampered[0], &oneExt) + _, err = verify(tampered) + assert.Error(t, err, "tampered proof accepted") +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index 4fe9e70818..5ec4e36cd1 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "testing" "time" @@ -18,9 +19,11 @@ import ( {{- end }} "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" + "github.com/consensys/gnark/gkr/gkrapi" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var cache = gkrtesting.NewCache({{ .FieldDescription }}) @@ -70,6 +73,66 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } +// TestEndToEndPoseidon2 compiles a circuit with gkrapi, then proves and verifies it with Poseidon2, +// the proof being rebuilt by DeserializeProof from the elements of its Flatten, and checks that +// tampering with the proof is rejected. +func TestEndToEndPoseidon2(t *testing.T) { + const nbInstances = 16 + + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + api.Export(api.Gate(gkrtesting.ConstantPositionsGate, x, y)) + circuit, schedule, err := api.Compile({{ .FieldDescription }}, gkrapi.ConsolidateAll) + require.NoError(t, err) + + assignment := make(WireAssignment, len(circuit)) + for _, w := range []gkr.Variable{x, y} { + assignment[w] = make([]{{ .BaseElementType }}, nbInstances) + {{ .BaseFieldPackageName }}.Vector(assignment[w]).MustSetRandom() + } + // The output column is left nil: Prove computes it. + + proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_{{ .FieldID }}.New()) + require.NoError(t, err) + + var flattened []{{ .ElementType }} + for _, element := range proof.Flatten() { + flattened = append(flattened, *element) + } + + verify := func(serialized []{{ .ElementType }}) (Claims, error) { + deserialized, err := DeserializeProof(circuit, schedule, serialized) + if err != nil { + return nil, err + } + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_{{ .FieldID }}.New()) + if err != nil { + return nil, err + } + return claims, claims.Check(assignment) + } + + verifierClaims, err := verify(flattened) + require.NoError(t, err) + assert.Equal(t, proverClaims, verifierClaims) + {{- if .Mixed }} + + outsideBaseField := false + for _, wireClaims := range verifierClaims { + for _, claim := range wireClaims { + outsideBaseField = outsideBaseField || claim.Evaluation.BigInt(nil) == nil + } + } + assert.True(t, outsideBaseField, "no claim is outside the base field: the challenges are not extension elements") + {{- end }} + + tampered := slices.Clone(flattened) + tampered[0].Add(&tampered[0], &{{ if .Mixed }}oneExt{{ else }}one{{ end }}) + _, err = verify(tampered) + assert.Error(t, err, "tampered proof accepted") +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/internal/gkr/gkrcore/transcript_test.go b/internal/gkr/gkrcore/transcript_test.go new file mode 100644 index 0000000000..0b9f56ca20 --- /dev/null +++ b/internal/gkr/gkrcore/transcript_test.go @@ -0,0 +1,39 @@ +package gkrcore + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + gcHash "github.com/consensys/gnark-crypto/hash" + _ "github.com/consensys/gnark-crypto/hash/all" // registers POSEIDON2_KOALABEAR + "github.com/stretchr/testify/assert" +) + +// TestHashTranscriptBindsWholeElement checks that every coordinate of a bound extension element +// affects the next challenge. +func TestHashTranscriptBindsWholeElement(t *testing.T) { + var element extensions.E6 + element.MustSetRandom() + + var one koalabear.Element + one.SetOne() + + coordinates := func(e *extensions.E6) []*koalabear.Element { + return []*koalabear.Element{&e.B0.A0, &e.B0.A1, &e.B1.A0, &e.B1.A1, &e.B2.A0, &e.B2.A1} + } + + challenge := func(e extensions.E6) extensions.E6 { + transcript := NewHashTranscript[extensions.E6](gcHash.POSEIDON2_KOALABEAR.New()) + transcript.Bind(e) + return transcript.Challenge() + } + + reference := challenge(element) + for i := range coordinates(&element) { + modified := element + c := coordinates(&modified)[i] + c.Add(c, &one) + assert.NotEqual(t, reference, challenge(modified), "coordinate %d does not affect the challenge", i) + } +} diff --git a/internal/gkr/gkrtesting/gkrtesting.go b/internal/gkr/gkrtesting/gkrtesting.go index 149d29a655..2427cd1b76 100644 --- a/internal/gkr/gkrtesting/gkrtesting.go +++ b/internal/gkr/gkrtesting/gkrtesting.go @@ -32,6 +32,14 @@ type circuits struct { gadget gkrcore.GadgetCircuit } +// ConstantPositionsGate is a gate of two inputs, x and y, with a constant in every position the +// gate compiler allows one: the first operand of Add, Sub, Mul and SumExp17, a multiplicand of +// MulAcc, and the addend of MulAcc. +func ConstantPositionsGate(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + x, y := in[0], in[1] + return api.Add(5, api.MulAcc(api.Sub(7, x), 3, y), api.Mul(2, x, y), api.MulAcc(4, x, y), api.SumExp17(1, x, y)) +} + func mimcGate(api gkr.GateAPI, input ...frontend.Variable) frontend.Variable { sum := api.Add(input[0], input[1]) //.Add(&sum, &m.ark) TODO: add ark res := api.Mul(sum, sum) // sum^2 From 4e2ba04f39d7d686f52f8e095c8e4d2a67e76c26 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Thu, 8 Oct 2026 09:03:38 -0500 Subject: [PATCH 25/37] build: update gnark-crypto dep Signed-off-by: Arya Tabaie --- go.mod | 24 +++++++++++------------- go.sum | 50 ++++++++++++++++++++------------------------------ 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index 146b3e054f..4521bec3f3 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/consensys/gnark -go 1.25.7 +go 1.26.8 require ( - github.com/bits-and-blooms/bitset v1.24.6 + github.com/bits-and-blooms/bitset v1.25.0 github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.21.1-0.20261002031104-9e7ec3b0d8ca + github.com/consensys/gnark-crypto v0.22.1-0.20261008102146-f49b800722be github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 @@ -16,28 +16,26 @@ require ( github.com/leanovate/gopter v0.2.11 github.com/ronanh/intcomp v1.1.1 github.com/rs/zerolog v1.35.1 - github.com/stretchr/testify v1.11.1 - golang.org/x/crypto v0.54.0 - golang.org/x/sync v0.22.0 + github.com/stretchr/testify v1.12.1 + golang.org/x/crypto v0.57.0 + golang.org/x/sync v0.23.0 ) require ( github.com/consensys/gnark-solidity-checker v0.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/telemetry v0.0.0-20260804195142-bdd03c3c8848 // indirect - golang.org/x/tools v0.48.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect + golang.org/x/tools v0.50.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index bfb7a16b56..6dd0363d54 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bits-and-blooms/bitset v1.24.6 h1:qcrftZUVBIwfs+m+nhoCBAPT+ZPZZjti8SbHbDQQkZ4= -github.com/bits-and-blooms/bitset v1.24.6/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.25.0 h1:0Ro0qF4abCkM6SqWPVj29sFhAbMPAZpaDD7xhJ10beM= +github.com/bits-and-blooms/bitset v1.25.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.21.1-0.20261002031104-9e7ec3b0d8ca h1:7ISGNLVTn9KfL1yuqsaxNpoAo7JtBBobWUJgmt5QNSI= -github.com/consensys/gnark-crypto v0.21.1-0.20261002031104-9e7ec3b0d8ca/go.mod h1:hdTjDNjdkYJ1oVuc8emh9XEhfM1SbyZhJigFqItiOLk= +github.com/consensys/gnark-crypto v0.22.1-0.20261008102146-f49b800722be h1:Ws/hHxds2g3wPAVt+tFw0Hrh53SR0XGsHFLrLLL3Zfw= +github.com/consensys/gnark-crypto v0.22.1-0.20261008102146-f49b800722be/go.mod h1:HL8XflRG4cvau1PEG3CWBTD2kkzUzdlqZF6BBgy57F8= github.com/consensys/gnark-solidity-checker v0.2.0 h1:i5iUEzNOkUvpaKm23UEe0wajBMwj7NzyT4EI0T2N8WQ= github.com/consensys/gnark-solidity-checker v0.2.0/go.mod h1:cEvl4g5AH+L4qGQLDOVZjqvn5IKZIAZdhSi8zAM6BiY= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -70,7 +70,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -200,12 +199,8 @@ github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -236,14 +231,11 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/ronanh/intcomp v1.1.1 h1:+1bGV/wEBiHI0FvzS7RHgzqOpfbBJzLIxkqMJ9e6yxY= github.com/ronanh/intcomp v1.1.1/go.mod h1:7FOLy3P3Zj3er/kVrU/pl+Ql7JFZj7bwliMGketo0IU= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= @@ -279,8 +271,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= @@ -304,6 +296,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -313,8 +307,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -353,8 +347,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -419,8 +413,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -469,10 +463,10 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260804195142-bdd03c3c8848 h1:gw6MYMPjxGBM77qPFYzz0c2naANqd2FF66wZRteou4s= -golang.org/x/telemetry v0.0.0-20260804195142-bdd03c3c8848/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 h1:F5BWKvW126NXR74uxkxuc1jQHhm/rwm/J3rSiFyuRs4= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -545,8 +539,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -655,8 +649,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -666,8 +658,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 9a455f88f119f8205751a86026e81a077ed45ec2 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Fri, 9 Oct 2026 09:31:23 -0500 Subject: [PATCH 26/37] build: bump gnark-crypto dep Signed-off-by: Arya Tabaie --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4521bec3f3..f16c825cdf 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.22.1-0.20261008102146-f49b800722be + github.com/consensys/gnark-crypto v0.22.1-0.20261009142911-8911ee45d22b github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 diff --git a/go.sum b/go.sum index 6dd0363d54..572ea11123 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.22.1-0.20261008102146-f49b800722be h1:Ws/hHxds2g3wPAVt+tFw0Hrh53SR0XGsHFLrLLL3Zfw= -github.com/consensys/gnark-crypto v0.22.1-0.20261008102146-f49b800722be/go.mod h1:HL8XflRG4cvau1PEG3CWBTD2kkzUzdlqZF6BBgy57F8= +github.com/consensys/gnark-crypto v0.22.1-0.20261009142911-8911ee45d22b h1:IDT/Waq2euwKsSOVZGufUN1tjYTN94Jf7FdSqt0XrH0= +github.com/consensys/gnark-crypto v0.22.1-0.20261009142911-8911ee45d22b/go.mod h1:HL8XflRG4cvau1PEG3CWBTD2kkzUzdlqZF6BBgy57F8= github.com/consensys/gnark-solidity-checker v0.2.0 h1:i5iUEzNOkUvpaKm23UEe0wajBMwj7NzyT4EI0T2N8WQ= github.com/consensys/gnark-solidity-checker v0.2.0/go.mod h1:cEvl4g5AH+L4qGQLDOVZjqvn5IKZIAZdhSi8zAM6BiY= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= From 9f472eaa3b8a27141f8fbea0403931b6d82bbbab Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Fri, 9 Oct 2026 11:58:50 -0500 Subject: [PATCH 27/37] refactor: some checks and documentation Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr.go | 30 +++++++++++--- gkr/bls12-377/gkr_test.go | 40 +++++++++++++++++++ gkr/bls12-381/gkr.go | 30 +++++++++++--- gkr/bls12-381/gkr_test.go | 40 +++++++++++++++++++ gkr/bn254/gkr.go | 30 +++++++++++--- gkr/bn254/gkr_test.go | 40 +++++++++++++++++++ gkr/bw6-761/gkr.go | 30 +++++++++++--- gkr/bw6-761/gkr_test.go | 40 +++++++++++++++++++ gkr/gkrapi/example_test.go | 4 +- gkr/koalabear/gkr.go | 30 +++++++++++--- gkr/koalabear/gkr_test.go | 40 +++++++++++++++++++ .../backend/template/gkr/gkr.go.tmpl | 30 +++++++++++--- .../backend/template/gkr/gkr.test.go.tmpl | 40 +++++++++++++++++++ internal/gkr/rational/gkr.go | 30 +++++++++++--- 14 files changed, 418 insertions(+), 36 deletions(-) diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index 06f3b55b9a..00f492d894 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -42,8 +42,22 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { + if len(c[0]) == 0 { // wire 0 is always an input, so it must have a claim + return errors.New("no claims on wire 0") + } + nbVars := len(c[0][0].EvaluationPoint) + nbInstances := 1 << nbVars for wI, wireClaims := range c { - for _, claim := range wireClaims { + if wI < 0 || wI >= len(assignment) || len(assignment[wI]) == 0 { + return fmt.Errorf("wire %d: no assignment", wI) + } + if len(assignment[wI]) != nbInstances { + return fmt.Errorf("wire %d: %d instances, expected %d", wI, len(assignment[wI]), nbInstances) + } + for claimI, claim := range wireClaims { + if len(claim.EvaluationPoint) != nbVars { + return fmt.Errorf("wire %d claim %d: evaluation point has %d coordinates, expected %d = log(%d)", wI, claimI, len(claim.EvaluationPoint), nbVars, nbInstances) + } eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) @@ -167,7 +181,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's // inputs and outputs; the caller must check them. The claim values returned to the caller, the -// output evaluations among them, are not bound into the transcript. +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() @@ -258,9 +273,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // Verify the consistency of the claimed output with the claimed input, and return the evaluation // claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims -// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim -// values returned to the caller, the output evaluations among them, are not bound into the -// transcript. +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself, on every +// claim, including those on exported inputs under gkrapi.ConsolidateNone, which no level +// cross-checks. The claim values returned to the caller, the output evaluations among them, are +// not bound into the transcript. +// +// The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, +// with a commitment to the circuit, the schedule, the number of instances and the input and output +// columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { if logNbInstances == 0 { return nil, errors.New("number of variables must be positive") diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index a9d10ce8aa..9969dcd7e2 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -126,6 +126,46 @@ func TestEndToEndPoseidon2(t *testing.T) { assert.Error(t, err, "tampered proof accepted") } +// TestCheckRejectsMismatchedLengths checks that Claims.Check rejects assignments of another size +// than the claims', and ones missing a claimed wire. +func TestCheckRejectsMismatchedLengths(t *testing.T) { + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + z := api.Mul(x, y) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BLS12_377.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + elements := func(values ...uint64) []fr.Element { + res := make([]fr.Element, len(values)) + for i, v := range values { + res[i] = fr.NewElement(v) + } + return res + } + + // The prover proves instances 0 and 2 of the larger assignment below only. + proven := make(WireAssignment, len(circuit)) + proven[x], proven[y] = elements(2, 4), elements(5, 6) + proof, _, err := Prove(circuit, schedule, proven, newMessageCounter(1, 1)) + require.NoError(t, err) + + claims, err := Verify(circuit, schedule, 1, proof, newMessageCounter(1, 1)) + require.NoError(t, err) + assert.NoError(t, claims.Check(proven)) + + // Instances 1 and 3 of z are false. + larger := make(WireAssignment, len(circuit)) + larger[x], larger[y], larger[z] = elements(2, 3, 4, 5), elements(5, 7, 6, 8), elements(10, 999, 24, 12345) + assert.Error(t, claims.Check(larger), "claims about 2 instances accepted against an assignment of 4") + + inputsOnly := make(WireAssignment, len(circuit)) + inputsOnly[x], inputsOnly[y] = elements(2, 4), elements(5, 6) + assert.NotPanics(t, func() { + assert.Error(t, claims.Check(inputsOnly), "claims on a wire accepted without an assignment") + }) +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index 54a1a22d72..c786c83bc2 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -42,8 +42,22 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { + if len(c[0]) == 0 { // wire 0 is always an input, so it must have a claim + return errors.New("no claims on wire 0") + } + nbVars := len(c[0][0].EvaluationPoint) + nbInstances := 1 << nbVars for wI, wireClaims := range c { - for _, claim := range wireClaims { + if wI < 0 || wI >= len(assignment) || len(assignment[wI]) == 0 { + return fmt.Errorf("wire %d: no assignment", wI) + } + if len(assignment[wI]) != nbInstances { + return fmt.Errorf("wire %d: %d instances, expected %d", wI, len(assignment[wI]), nbInstances) + } + for claimI, claim := range wireClaims { + if len(claim.EvaluationPoint) != nbVars { + return fmt.Errorf("wire %d claim %d: evaluation point has %d coordinates, expected %d = log(%d)", wI, claimI, len(claim.EvaluationPoint), nbVars, nbInstances) + } eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) @@ -167,7 +181,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's // inputs and outputs; the caller must check them. The claim values returned to the caller, the -// output evaluations among them, are not bound into the transcript. +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() @@ -258,9 +273,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // Verify the consistency of the claimed output with the claimed input, and return the evaluation // claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims -// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim -// values returned to the caller, the output evaluations among them, are not bound into the -// transcript. +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself, on every +// claim, including those on exported inputs under gkrapi.ConsolidateNone, which no level +// cross-checks. The claim values returned to the caller, the output evaluations among them, are +// not bound into the transcript. +// +// The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, +// with a commitment to the circuit, the schedule, the number of instances and the input and output +// columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { if logNbInstances == 0 { return nil, errors.New("number of variables must be positive") diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index 965d996a49..6d924348a0 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -126,6 +126,46 @@ func TestEndToEndPoseidon2(t *testing.T) { assert.Error(t, err, "tampered proof accepted") } +// TestCheckRejectsMismatchedLengths checks that Claims.Check rejects assignments of another size +// than the claims', and ones missing a claimed wire. +func TestCheckRejectsMismatchedLengths(t *testing.T) { + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + z := api.Mul(x, y) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BLS12_381.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + elements := func(values ...uint64) []fr.Element { + res := make([]fr.Element, len(values)) + for i, v := range values { + res[i] = fr.NewElement(v) + } + return res + } + + // The prover proves instances 0 and 2 of the larger assignment below only. + proven := make(WireAssignment, len(circuit)) + proven[x], proven[y] = elements(2, 4), elements(5, 6) + proof, _, err := Prove(circuit, schedule, proven, newMessageCounter(1, 1)) + require.NoError(t, err) + + claims, err := Verify(circuit, schedule, 1, proof, newMessageCounter(1, 1)) + require.NoError(t, err) + assert.NoError(t, claims.Check(proven)) + + // Instances 1 and 3 of z are false. + larger := make(WireAssignment, len(circuit)) + larger[x], larger[y], larger[z] = elements(2, 3, 4, 5), elements(5, 7, 6, 8), elements(10, 999, 24, 12345) + assert.Error(t, claims.Check(larger), "claims about 2 instances accepted against an assignment of 4") + + inputsOnly := make(WireAssignment, len(circuit)) + inputsOnly[x], inputsOnly[y] = elements(2, 4), elements(5, 6) + assert.NotPanics(t, func() { + assert.Error(t, claims.Check(inputsOnly), "claims on a wire accepted without an assignment") + }) +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index d683d72b4b..46024c2802 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -42,8 +42,22 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { + if len(c[0]) == 0 { // wire 0 is always an input, so it must have a claim + return errors.New("no claims on wire 0") + } + nbVars := len(c[0][0].EvaluationPoint) + nbInstances := 1 << nbVars for wI, wireClaims := range c { - for _, claim := range wireClaims { + if wI < 0 || wI >= len(assignment) || len(assignment[wI]) == 0 { + return fmt.Errorf("wire %d: no assignment", wI) + } + if len(assignment[wI]) != nbInstances { + return fmt.Errorf("wire %d: %d instances, expected %d", wI, len(assignment[wI]), nbInstances) + } + for claimI, claim := range wireClaims { + if len(claim.EvaluationPoint) != nbVars { + return fmt.Errorf("wire %d claim %d: evaluation point has %d coordinates, expected %d = log(%d)", wI, claimI, len(claim.EvaluationPoint), nbVars, nbInstances) + } eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) @@ -167,7 +181,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's // inputs and outputs; the caller must check them. The claim values returned to the caller, the -// output evaluations among them, are not bound into the transcript. +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() @@ -258,9 +273,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // Verify the consistency of the claimed output with the claimed input, and return the evaluation // claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims -// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim -// values returned to the caller, the output evaluations among them, are not bound into the -// transcript. +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself, on every +// claim, including those on exported inputs under gkrapi.ConsolidateNone, which no level +// cross-checks. The claim values returned to the caller, the output evaluations among them, are +// not bound into the transcript. +// +// The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, +// with a commitment to the circuit, the schedule, the number of instances and the input and output +// columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { if logNbInstances == 0 { return nil, errors.New("number of variables must be positive") diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index c6e2e71d65..62af0587ab 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -126,6 +126,46 @@ func TestEndToEndPoseidon2(t *testing.T) { assert.Error(t, err, "tampered proof accepted") } +// TestCheckRejectsMismatchedLengths checks that Claims.Check rejects assignments of another size +// than the claims', and ones missing a claimed wire. +func TestCheckRejectsMismatchedLengths(t *testing.T) { + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + z := api.Mul(x, y) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BN254.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + elements := func(values ...uint64) []fr.Element { + res := make([]fr.Element, len(values)) + for i, v := range values { + res[i] = fr.NewElement(v) + } + return res + } + + // The prover proves instances 0 and 2 of the larger assignment below only. + proven := make(WireAssignment, len(circuit)) + proven[x], proven[y] = elements(2, 4), elements(5, 6) + proof, _, err := Prove(circuit, schedule, proven, newMessageCounter(1, 1)) + require.NoError(t, err) + + claims, err := Verify(circuit, schedule, 1, proof, newMessageCounter(1, 1)) + require.NoError(t, err) + assert.NoError(t, claims.Check(proven)) + + // Instances 1 and 3 of z are false. + larger := make(WireAssignment, len(circuit)) + larger[x], larger[y], larger[z] = elements(2, 3, 4, 5), elements(5, 7, 6, 8), elements(10, 999, 24, 12345) + assert.Error(t, claims.Check(larger), "claims about 2 instances accepted against an assignment of 4") + + inputsOnly := make(WireAssignment, len(circuit)) + inputsOnly[x], inputsOnly[y] = elements(2, 4), elements(5, 6) + assert.NotPanics(t, func() { + assert.Error(t, claims.Check(inputsOnly), "claims on a wire accepted without an assignment") + }) +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index ad1e8eacdc..6c14b40159 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -42,8 +42,22 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { + if len(c[0]) == 0 { // wire 0 is always an input, so it must have a claim + return errors.New("no claims on wire 0") + } + nbVars := len(c[0][0].EvaluationPoint) + nbInstances := 1 << nbVars for wI, wireClaims := range c { - for _, claim := range wireClaims { + if wI < 0 || wI >= len(assignment) || len(assignment[wI]) == 0 { + return fmt.Errorf("wire %d: no assignment", wI) + } + if len(assignment[wI]) != nbInstances { + return fmt.Errorf("wire %d: %d instances, expected %d", wI, len(assignment[wI]), nbInstances) + } + for claimI, claim := range wireClaims { + if len(claim.EvaluationPoint) != nbVars { + return fmt.Errorf("wire %d claim %d: evaluation point has %d coordinates, expected %d = log(%d)", wI, claimI, len(claim.EvaluationPoint), nbVars, nbInstances) + } eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) @@ -167,7 +181,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's // inputs and outputs; the caller must check them. The claim values returned to the caller, the -// output evaluations among them, are not bound into the transcript. +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() @@ -258,9 +273,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // Verify the consistency of the claimed output with the claimed input, and return the evaluation // claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims -// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim -// values returned to the caller, the output evaluations among them, are not bound into the -// transcript. +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself, on every +// claim, including those on exported inputs under gkrapi.ConsolidateNone, which no level +// cross-checks. The claim values returned to the caller, the output evaluations among them, are +// not bound into the transcript. +// +// The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, +// with a commitment to the circuit, the schedule, the number of instances and the input and output +// columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { if logNbInstances == 0 { return nil, errors.New("number of variables must be positive") diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index 7853035d0b..fac98092bf 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -126,6 +126,46 @@ func TestEndToEndPoseidon2(t *testing.T) { assert.Error(t, err, "tampered proof accepted") } +// TestCheckRejectsMismatchedLengths checks that Claims.Check rejects assignments of another size +// than the claims', and ones missing a claimed wire. +func TestCheckRejectsMismatchedLengths(t *testing.T) { + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + z := api.Mul(x, y) + circuit, schedule, err := api.Compile(gkr.PrimeField(ecc.BW6_761.ScalarField()), gkrapi.ConsolidateAll) + require.NoError(t, err) + + elements := func(values ...uint64) []fr.Element { + res := make([]fr.Element, len(values)) + for i, v := range values { + res[i] = fr.NewElement(v) + } + return res + } + + // The prover proves instances 0 and 2 of the larger assignment below only. + proven := make(WireAssignment, len(circuit)) + proven[x], proven[y] = elements(2, 4), elements(5, 6) + proof, _, err := Prove(circuit, schedule, proven, newMessageCounter(1, 1)) + require.NoError(t, err) + + claims, err := Verify(circuit, schedule, 1, proof, newMessageCounter(1, 1)) + require.NoError(t, err) + assert.NoError(t, claims.Check(proven)) + + // Instances 1 and 3 of z are false. + larger := make(WireAssignment, len(circuit)) + larger[x], larger[y], larger[z] = elements(2, 3, 4, 5), elements(5, 7, 6, 8), elements(10, 999, 24, 12345) + assert.Error(t, claims.Check(larger), "claims about 2 instances accepted against an assignment of 4") + + inputsOnly := make(WireAssignment, len(circuit)) + inputsOnly[x], inputsOnly[y] = elements(2, 4), elements(5, 6) + assert.NotPanics(t, func() { + assert.Error(t, claims.Check(inputsOnly), "claims on a wire accepted without an assignment") + }) +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/gkr/gkrapi/example_test.go b/gkr/gkrapi/example_test.go index 3e2a4ade65..d2699d647d 100644 --- a/gkr/gkrapi/example_test.go +++ b/gkr/gkrapi/example_test.go @@ -29,6 +29,8 @@ func Example() { assignment[y] = []fr.Element{fr.NewElement(5), fr.NewElement(7)} // assignment[z] is left nil: Prove computes it. + // Prove and Verify must be given hashes seeded identically with a commitment to the circuit, + // the schedule, the number of instances and the inputs and outputs. The example omits it. proof, proverClaims, err := gkrbn254.Prove(circuit, schedule, assignment, gcHash.MIMC_BN254.New()) assertNoError(err) assertNoError(proverClaims.Check(assignment)) @@ -42,7 +44,7 @@ func Example() { verifierClaims, err := gkrbn254.Verify(circuit, schedule, 1, deserialized, gcHash.MIMC_BN254.New()) assertNoError(err) - assertNoError(verifierClaims.Check(assignment)) + assertNoError(verifierClaims.Check(assignment)) // every returned claim must be checked fmt.Println("proof verified") // Output: proof verified diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index 9acc41d211..7e149983a5 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -45,8 +45,22 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { var scratch polynomial.MultiLinE6 + if len(c[0]) == 0 { // wire 0 is always an input, so it must have a claim + return errors.New("no claims on wire 0") + } + nbVars := len(c[0][0].EvaluationPoint) + nbInstances := 1 << nbVars for wI, wireClaims := range c { - for _, claim := range wireClaims { + if wI < 0 || wI >= len(assignment) || len(assignment[wI]) == 0 { + return fmt.Errorf("wire %d: no assignment", wI) + } + if len(assignment[wI]) != nbInstances { + return fmt.Errorf("wire %d: %d instances, expected %d", wI, len(assignment[wI]), nbInstances) + } + for claimI, claim := range wireClaims { + if len(claim.EvaluationPoint) != nbVars { + return fmt.Errorf("wire %d claim %d: evaluation point has %d coordinates, expected %d = log(%d)", wI, claimI, len(claim.EvaluationPoint), nbVars, nbInstances) + } eval := scratch.EvaluateBase(assignment[wI], claim.EvaluationPoint) if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) @@ -179,7 +193,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's // inputs and outputs; the caller must check them. The claim values returned to the caller, the -// output evaluations among them, are not bound into the transcript. +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() @@ -270,9 +285,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // Verify the consistency of the claimed output with the claimed input, and return the evaluation // claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims -// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim -// values returned to the caller, the output evaluations among them, are not bound into the -// transcript. +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself, on every +// claim, including those on exported inputs under gkrapi.ConsolidateNone, which no level +// cross-checks. The claim values returned to the caller, the output evaluations among them, are +// not bound into the transcript. +// +// The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, +// with a commitment to the circuit, the schedule, the number of instances and the input and output +// columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { if logNbInstances == 0 { return nil, errors.New("number of variables must be positive") diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go index 1a359ef157..a8cef54425 100644 --- a/gkr/koalabear/gkr_test.go +++ b/gkr/koalabear/gkr_test.go @@ -134,6 +134,46 @@ func TestEndToEndPoseidon2(t *testing.T) { assert.Error(t, err, "tampered proof accepted") } +// TestCheckRejectsMismatchedLengths checks that Claims.Check rejects assignments of another size +// than the claims', and ones missing a claimed wire. +func TestCheckRejectsMismatchedLengths(t *testing.T) { + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + z := api.Mul(x, y) + circuit, schedule, err := api.Compile(gkr.KoalaBearE6(), gkrapi.ConsolidateAll) + require.NoError(t, err) + + elements := func(values ...uint64) []koalabear.Element { + res := make([]koalabear.Element, len(values)) + for i, v := range values { + res[i] = koalabear.NewElement(v) + } + return res + } + + // The prover proves instances 0 and 2 of the larger assignment below only. + proven := make(WireAssignment, len(circuit)) + proven[x], proven[y] = elements(2, 4), elements(5, 6) + proof, _, err := Prove(circuit, schedule, proven, newMessageCounter(1, 1)) + require.NoError(t, err) + + claims, err := Verify(circuit, schedule, 1, proof, newMessageCounter(1, 1)) + require.NoError(t, err) + assert.NoError(t, claims.Check(proven)) + + // Instances 1 and 3 of z are false. + larger := make(WireAssignment, len(circuit)) + larger[x], larger[y], larger[z] = elements(2, 3, 4, 5), elements(5, 7, 6, 8), elements(10, 999, 24, 12345) + assert.Error(t, claims.Check(larger), "claims about 2 instances accepted against an assignment of 4") + + inputsOnly := make(WireAssignment, len(circuit)) + inputsOnly[x], inputsOnly[y] = elements(2, 4), elements(5, 6) + assert.NotPanics(t, func() { + assert.Error(t, claims.Check(inputsOnly), "claims on a wire accepted without an assignment") + }) +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index f763fad24d..ec9b1ddabc 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -46,8 +46,22 @@ func (c Claims) Check(assignment WireAssignment) error { {{- if .Mixed }} var scratch polynomial.MultiLin{{ .ExtensionSuffix }} {{- end }} + if len(c[0]) == 0 { // wire 0 is always an input, so it must have a claim + return errors.New("no claims on wire 0") + } + nbVars := len(c[0][0].EvaluationPoint) + nbInstances := 1 << nbVars for wI, wireClaims := range c { - for _, claim := range wireClaims { + if wI < 0 || wI >= len(assignment) || len(assignment[wI]) == 0 { + return fmt.Errorf("wire %d: no assignment", wI) + } + if len(assignment[wI]) != nbInstances { + return fmt.Errorf("wire %d: %d instances, expected %d", wI, len(assignment[wI]), nbInstances) + } + for claimI, claim := range wireClaims { + if len(claim.EvaluationPoint) != nbVars { + return fmt.Errorf("wire %d claim %d: evaluation point has %d coordinates, expected %d = log(%d)", wI, claimI, len(claim.EvaluationPoint), nbVars, nbInstances) + } {{- if .Mixed }} eval := scratch.EvaluateBase(assignment[wI], claim.EvaluationPoint) {{- else }} @@ -190,7 +204,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's // inputs and outputs; the caller must check them. The claim values returned to the caller, the -// output evaluations among them, are not bound into the transcript. +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() @@ -289,9 +304,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // Verify the consistency of the claimed output with the claimed input, and return the evaluation // claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims -// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim -// values returned to the caller, the output evaluations among them, are not bound into the -// transcript. +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself, on every +// claim, including those on exported inputs under gkrapi.ConsolidateNone, which no level +// cross-checks. The claim values returned to the caller, the output evaluations among them, are +// not bound into the transcript. +// +// The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, +// with a commitment to the circuit, the schedule, the number of instances and the input and output +// columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { if logNbInstances == 0 { return nil, errors.New("number of variables must be positive") diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index 5ec4e36cd1..aba15280ba 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -133,6 +133,46 @@ func TestEndToEndPoseidon2(t *testing.T) { assert.Error(t, err, "tampered proof accepted") } +// TestCheckRejectsMismatchedLengths checks that Claims.Check rejects assignments of another size +// than the claims', and ones missing a claimed wire. +func TestCheckRejectsMismatchedLengths(t *testing.T) { + api := gkrapi.New() + x := api.NewInput() + y := api.NewInput() + z := api.Mul(x, y) + circuit, schedule, err := api.Compile({{ .FieldDescription }}, gkrapi.ConsolidateAll) + require.NoError(t, err) + + elements := func(values ...uint64) []{{ .BaseElementType }} { + res := make([]{{ .BaseElementType }}, len(values)) + for i, v := range values { + res[i] = {{ .BaseFieldPackageName }}.NewElement(v) + } + return res + } + + // The prover proves instances 0 and 2 of the larger assignment below only. + proven := make(WireAssignment, len(circuit)) + proven[x], proven[y] = elements(2, 4), elements(5, 6) + proof, _, err := Prove(circuit, schedule, proven, newMessageCounter(1, 1)) + require.NoError(t, err) + + claims, err := Verify(circuit, schedule, 1, proof, newMessageCounter(1, 1)) + require.NoError(t, err) + assert.NoError(t, claims.Check(proven)) + + // Instances 1 and 3 of z are false. + larger := make(WireAssignment, len(circuit)) + larger[x], larger[y], larger[z] = elements(2, 3, 4, 5), elements(5, 7, 6, 8), elements(10, 999, 24, 12345) + assert.Error(t, claims.Check(larger), "claims about 2 instances accepted against an assignment of 4") + + inputsOnly := make(WireAssignment, len(circuit)) + inputsOnly[x], inputsOnly[y] = elements(2, 4), elements(5, 6) + assert.NotPanics(t, func() { + assert.Error(t, claims.Check(inputsOnly), "claims on a wire accepted without an assignment") + }) +} + // proveAndVerify proves assignment against schedule and verifies the proof rebuilt by // DeserializeProof from the elements of its Flatten, then asserts that: every wire of // schedule[0] has exactly one claim, and all of them share one point; the number of diff --git a/internal/gkr/rational/gkr.go b/internal/gkr/rational/gkr.go index 471c67f734..11df8ecceb 100644 --- a/internal/gkr/rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -41,8 +41,22 @@ type Claims map[int][]EvaluationClaim // Check asserts that every claim in c holds against assignment. func (c Claims) Check(assignment WireAssignment) error { + if len(c[0]) == 0 { // wire 0 is always an input, so it must have a claim + return errors.New("no claims on wire 0") + } + nbVars := len(c[0][0].EvaluationPoint) + nbInstances := 1 << nbVars for wI, wireClaims := range c { - for _, claim := range wireClaims { + if wI < 0 || wI >= len(assignment) || len(assignment[wI]) == 0 { + return fmt.Errorf("wire %d: no assignment", wI) + } + if len(assignment[wI]) != nbInstances { + return fmt.Errorf("wire %d: %d instances, expected %d", wI, len(assignment[wI]), nbInstances) + } + for claimI, claim := range wireClaims { + if len(claim.EvaluationPoint) != nbVars { + return fmt.Errorf("wire %d claim %d: evaluation point has %d coordinates, expected %d = log(%d)", wI, claimI, len(claim.EvaluationPoint), nbVars, nbInstances) + } eval := assignment[wI].Evaluate(claim.EvaluationPoint, nil) if !eval.Equal(&claim.Evaluation) { return fmt.Errorf("wire %d: claimed evaluation %v, computed %v", wI, &claim.Evaluation, &eval) @@ -166,7 +180,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // Prove consistency of the claimed assignment. It returns the evaluation claims on the circuit's // inputs and outputs; the caller must check them. The claim values returned to the caller, the -// output evaluations among them, are not bound into the transcript. +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() @@ -257,9 +272,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // Verify the consistency of the claimed output with the claimed input, and return the evaluation // claims on the circuit's inputs and outputs. A nil error means nothing until the returned Claims -// are checked: Verify reads no assignment, so the caller must call Claims.Check itself. The claim -// values returned to the caller, the output evaluations among them, are not bound into the -// transcript. +// are checked: Verify reads no assignment, so the caller must call Claims.Check itself, on every +// claim, including those on exported inputs under gkrapi.ConsolidateNone, which no level +// cross-checks. The claim values returned to the caller, the output evaluations among them, are +// not bound into the transcript. +// +// The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, +// with a commitment to the circuit, the schedule, the number of instances and the input and output +// columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { if logNbInstances == 0 { return nil, errors.New("number of variables must be positive") From 76277ea1c813034e3cd904b950da03115c66a4cf Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Fri, 9 Oct 2026 12:14:08 -0500 Subject: [PATCH 28/37] fix: error when nbInstances undeterminable Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr.go | 4 ++-- gkr/bls12-381/gkr.go | 4 ++-- gkr/bn254/gkr.go | 4 ++-- gkr/bw6-761/gkr.go | 4 ++-- gkr/koalabear/gkr.go | 4 ++-- internal/generator/backend/template/gkr/gkr.go.tmpl | 4 ++-- internal/gkr/gkr.go | 2 +- internal/gkr/gkr_test.go | 4 ++-- internal/gkr/gkrcore/schedule.go | 7 ++++--- internal/gkr/rational/gkr.go | 4 ++-- 10 files changed, 21 insertions(+), 20 deletions(-) diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index 00f492d894..d87653db80 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -282,7 +282,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // with a commitment to the circuit, the schedule, the number of instances and the input and output // columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { - if logNbInstances == 0 { + if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } r := newResources(c, schedule, logNbInstances, hasher) @@ -429,7 +429,7 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { identity := identityGate() logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index c786c83bc2..230413739c 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -282,7 +282,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // with a commitment to the circuit, the schedule, the number of instances and the input and output // columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { - if logNbInstances == 0 { + if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } r := newResources(c, schedule, logNbInstances, hasher) @@ -429,7 +429,7 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { identity := identityGate() logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index 46024c2802..1086aca79d 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -282,7 +282,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // with a commitment to the circuit, the schedule, the number of instances and the input and output // columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { - if logNbInstances == 0 { + if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } r := newResources(c, schedule, logNbInstances, hasher) @@ -429,7 +429,7 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { identity := identityGate() logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index 6c14b40159..1390b24a08 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -282,7 +282,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // with a commitment to the circuit, the schedule, the number of instances and the input and output // columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { - if logNbInstances == 0 { + if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } r := newResources(c, schedule, logNbInstances, hasher) @@ -429,7 +429,7 @@ func (p Proof) Flatten() iter.Seq2[int, *fr.Element] { func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []fr.Element) (Proof, error) { identity := identityGate() logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index 7e149983a5..e1fd4fe158 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -294,7 +294,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // with a commitment to the circuit, the schedule, the number of instances and the input and output // columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { - if logNbInstances == 0 { + if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } r := newResources(c, schedule, logNbInstances, hasher) @@ -441,7 +441,7 @@ func (p Proof) Flatten() iter.Seq2[int, *extensions.E6] { func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []extensions.E6) (Proof, error) { identity := identityGate() logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index ec9b1ddabc..eab01ccf9b 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -313,7 +313,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // with a commitment to the circuit, the schedule, the number of instances and the input and output // columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { - if logNbInstances == 0 { + if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } r := newResources(c, schedule, logNbInstances, hasher) @@ -460,7 +460,7 @@ func (p Proof) Flatten() iter.Seq2[int, *{{ .ElementType }}] { func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []{{ .ElementType }}) (Proof, error) { identity := identityGate() logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } diff --git a/internal/gkr/gkr.go b/internal/gkr/gkr.go index f14ec75c2f..706668eb22 100644 --- a/internal/gkr/gkr.go +++ b/internal/gkr/gkr.go @@ -368,7 +368,7 @@ func (r *variablesReader) hasNextN(n int) bool { func DeserializeProof(circuit Circuit, schedule constraint.GkrProvingSchedule, serializedProof []frontend.Variable) (Proof, error) { proof := make(Proof, len(schedule)+1) logNbInstances, err := gkrcore.ComputeLogNbInstances(circuit, schedule, len(serializedProof), identityGate()) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } diff --git a/internal/gkr/gkr_test.go b/internal/gkr/gkr_test.go index 36b0af6f2d..78b55efed6 100644 --- a/internal/gkr/gkr_test.go +++ b/internal/gkr/gkr_test.go @@ -264,11 +264,11 @@ func TestLogNbInstances(t *testing.T) { assert.NoError(t, err) serializedProof := testCase.Proof.Serialize() logNbInstances, err := gkrcore.ComputeLogNbInstances(testCase.Circuit, testCase.Schedule, len(serializedProof), identityGate()) - assert.NoError(t, err) if hasSumcheck(testCase.Schedule) { + assert.NoError(t, err) assert.Equal(t, 1, logNbInstances) } else { - assert.Equal(t, -1, logNbInstances, "no-sumcheck schedule should have logNbInstances=-1, got %d instead", logNbInstances) + assert.ErrorIs(t, err, gkrcore.ErrNbInstancesUndetermined) } } } diff --git a/internal/gkr/gkrcore/schedule.go b/internal/gkr/gkrcore/schedule.go index b919f6bc08..3137b08480 100644 --- a/internal/gkr/gkrcore/schedule.go +++ b/internal/gkr/gkrcore/schedule.go @@ -520,10 +520,11 @@ func CollectOutgoingEvalPoints[F any](level *constraint.GkrSkipLevel, levelI int return outPoints } +var ErrNbInstancesUndetermined = errors.New("the proof's length does not determine the number of instances") + // ComputeLogNbInstances derives n such that the number of instances is 2ⁿ from the length of the // serialized proof and the circuit/schedule structure, identity being the gate that level 0's view -// of the circuit uses. It returns -1 if the schedule has no level that runs a sumcheck, as then -// the proof's length does not depend on n, and an error if no n matches the length. +// of the circuit uses. func ComputeLogNbInstances[G any](c Circuit[G], schedule constraint.GkrProvingSchedule, serializedProofLen int, identity Gate[G]) (int, error) { serializedProofLen -= len(c.Outputs()) perVar := 0 @@ -543,7 +544,7 @@ func ComputeLogNbInstances[G any](c Circuit[G], schedule constraint.GkrProvingSc } if perVar == 0 { if serializedProofLen == 0 { - return -1, nil + return 0, ErrNbInstancesUndetermined } } else if serializedProofLen%perVar == 0 { return serializedProofLen / perVar, nil diff --git a/internal/gkr/rational/gkr.go b/internal/gkr/rational/gkr.go index 11df8ecceb..b671efb367 100644 --- a/internal/gkr/rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -281,7 +281,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // with a commitment to the circuit, the schedule, the number of instances and the input and output // columns, computed from values it trusts. func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { - if logNbInstances == 0 { + if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } r := newResources(c, schedule, logNbInstances, hasher) @@ -428,7 +428,7 @@ func (p Proof) Flatten() iter.Seq2[int, *rational.Element] { func DeserializeProof(c Circuit, schedule constraint.GkrProvingSchedule, serialized []rational.Element) (Proof, error) { identity := identityGate() logNbInstances, err := gkrcore.ComputeLogNbInstances(c, schedule, len(serialized), identity) - if err != nil { + if err != nil && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { return nil, err } From 7593d9949cda67c7b788d93358ef3b6b96c5287c Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Fri, 9 Oct 2026 12:17:56 -0500 Subject: [PATCH 29/37] refactor: renames Signed-off-by: Arya Tabaie --- gkr/koalabear/sumcheck.go | 28 +++++++++++-------- .../backend/template/gkr/sumcheck.go.tmpl | 26 ++++++++++------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index 30c4cceb05..58ddc24a21 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -158,11 +158,12 @@ type zeroCheckClaims struct { eqs []polynomial.MultiLinE6 // per-wire interpolation bases for evaluating wire assignments at challenge points } -// roundPolynomialMixed computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). +// roundPolynomialExt computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). // The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). // The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). // By convention, g₀ is a constant polynomial equal to the claimed sum. -func (c *zeroCheckClaims) roundPolynomialMixed() polynomial.PolynomialE6 { +// It is used for every round after the first, which runs entirely over the extension. +func (c *zeroCheckClaims) roundPolynomialExt() polynomial.PolynomialE6 { level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.input) @@ -246,11 +247,12 @@ func (c *zeroCheckClaims) roundPolynomialMixed() polynomial.PolynomialE6 { return p } -// roundPolynomialBase computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). +// roundPolynomialBaseInputs computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). // The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). // The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). // By convention, g₀ is a constant polynomial equal to the claimed sum. -func (c *zeroCheckClaims) roundPolynomialBase() polynomial.PolynomialE6 { +// It is used for the first round, whose input tables are base field columns. +func (c *zeroCheckClaims) roundPolynomialBaseInputs() polynomial.PolynomialE6 { level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.baseInput) @@ -338,9 +340,9 @@ func (c *zeroCheckClaims) roundPolynomialBase() polynomial.PolynomialE6 { // baseInput is set, and from input otherwise. func (c *zeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { if c.baseInput != nil { - return c.roundPolynomialBase() + return c.roundPolynomialBaseInputs() } - return c.roundPolynomialMixed() + return c.roundPolynomialExt() } // roundFold folds all input and eq polynomials at the verifier challenge r. @@ -622,11 +624,12 @@ type singleSourceZeroCheckClaims struct { suffixEq polynomial.MultiLinE6 // contiguous buffer; second half is the current round's eq segment } -// roundPolynomialMixed computes g'_j(1), ..., g'_j(d) where d is the gate degree. +// roundPolynomialExt computes g'_j(1), ..., g'_j(d) where d is the gate degree. // g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) // The wire contributions are accumulated via Horner's method in α (= foldingCoeff), // eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. -func (c *singleSourceZeroCheckClaims) roundPolynomialMixed() polynomial.PolynomialE6 { +// It is used for every round after the first, which runs entirely over the extension. +func (c *singleSourceZeroCheckClaims) roundPolynomialExt() polynomial.PolynomialE6 { level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.input) @@ -705,11 +708,12 @@ func (c *singleSourceZeroCheckClaims) roundPolynomialMixed() polynomial.Polynomi return p } -// roundPolynomialBase computes g'_j(1), ..., g'_j(d) where d is the gate degree. +// roundPolynomialBaseInputs computes g'_j(1), ..., g'_j(d) where d is the gate degree. // g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) // The wire contributions are accumulated via Horner's method in α (= foldingCoeff), // eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. -func (c *singleSourceZeroCheckClaims) roundPolynomialBase() polynomial.PolynomialE6 { +// It is used for the first round, whose input tables are base field columns. +func (c *singleSourceZeroCheckClaims) roundPolynomialBaseInputs() polynomial.PolynomialE6 { level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) nbUniqueInputs := len(c.baseInput) @@ -792,9 +796,9 @@ func (c *singleSourceZeroCheckClaims) roundPolynomialBase() polynomial.Polynomia // baseInput is set, and from input otherwise. func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.PolynomialE6 { if c.baseInput != nil { - return c.roundPolynomialBase() + return c.roundPolynomialBaseInputs() } - return c.roundPolynomialMixed() + return c.roundPolynomialExt() } // roundFold folds only input multilinears at the verifier challenge r. diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index 0f5021b88c..a9ae13bf12 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -165,9 +165,9 @@ type zeroCheckClaims struct { // baseInput is set, and from input otherwise. func (c *zeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { if c.baseInput != nil { - return c.roundPolynomialBase() + return c.roundPolynomialBaseInputs() } - return c.roundPolynomialMixed() + return c.roundPolynomialExt() } {{- end }} @@ -489,9 +489,9 @@ type singleSourceZeroCheckClaims struct { // baseInput is set, and from input otherwise. func (c *singleSourceZeroCheckClaims) roundPolynomial() polynomial.Polynomial{{ .ExtensionSuffix }} { if c.baseInput != nil { - return c.roundPolynomialBase() + return c.roundPolynomialBaseInputs() } - return c.roundPolynomialMixed() + return c.roundPolynomialExt() } {{- end }} @@ -656,25 +656,28 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er {{ define "zeroCheckRoundPolynomial" -}} {{- $name := "roundPolynomial" }} +{{- $use := "" }} {{- $inputs := "c.input" }} {{- $inElem := .Config.ElementType }} {{- $pools := "c.gateEvaluatorPools" }} {{- $ge := "GateEvaluator" }} {{- $mul := "Mul" }} {{- if .Base }} -{{- $name = "roundPolynomialBase" }} +{{- $name = "roundPolynomialBaseInputs" }} +{{- $use = "\n// It is used for the first round, whose input tables are base field columns." }} {{- $inputs = "c.baseInput" }} {{- $inElem = .Config.BaseElementType }} {{- $pools = "c.baseGateEvaluatorPools" }} {{- $mul = "MulByElement" }} {{- else if .Config.Mixed }} -{{- $name = "roundPolynomialMixed" }} +{{- $name = "roundPolynomialExt" }} +{{- $use = "\n// It is used for every round after the first, which runs entirely over the extension." }} {{- $ge = "GateEvaluatorMixed" }} {{- end }} // {{ $name }} computes gⱼ = ∑ₕ ∑ᵥ eqs[v](Xⱼ, h...) · gateᵥ(inputs(Xⱼ, h...)). // The polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). // The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). -// By convention, g₀ is a constant polynomial equal to the claimed sum. +// By convention, g₀ is a constant polynomial equal to the claimed sum.{{ $use }} func (c *zeroCheckClaims) {{ $name }}() polynomial.Polynomial{{ .Config.ExtensionSuffix }} { level := c.resources.schedule[c.levelI].(*constraint.GkrSumcheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) @@ -762,6 +765,7 @@ func (c *zeroCheckClaims) {{ $name }}() polynomial.Polynomial{{ .Config.Extensio {{ define "singleSourceRoundPolynomial" -}} {{- $name := "roundPolynomial" }} +{{- $use := "" }} {{- $inputs := "c.input" }} {{- $inElem := .Config.ElementType }} {{- $pools := "c.gateEvaluatorPools" }} @@ -769,20 +773,22 @@ func (c *zeroCheckClaims) {{ $name }}() polynomial.Polynomial{{ .Config.Extensio {{- $set := "Set" }} {{- $add := "Add" }} {{- if .Base }} -{{- $name = "roundPolynomialBase" }} +{{- $name = "roundPolynomialBaseInputs" }} +{{- $use = "\n// It is used for the first round, whose input tables are base field columns." }} {{- $inputs = "c.baseInput" }} {{- $inElem = .Config.BaseElementType }} {{- $pools = "c.baseGateEvaluatorPools" }} {{- $set = "SetElement" }} {{- $add = "AddElement" }} {{- else if .Config.Mixed }} -{{- $name = "roundPolynomialMixed" }} +{{- $name = "roundPolynomialExt" }} +{{- $use = "\n// It is used for every round after the first, which runs entirely over the extension." }} {{- $ge = "GateEvaluatorMixed" }} {{- end }} // {{ $name }} computes g'_j(1), ..., g'_j(d) where d is the gate degree. // g'_j(m) = ∑_h suffixEq[j+1][h] · ∑_w α^w · gate_w(inputs at (m, h)) // The wire contributions are accumulated via Horner's method in α (= foldingCoeff), -// eliminating precomputed batchCoeffs and the multiply-by-one for wire 0. +// eliminating precomputed batchCoeffs and the multiply-by-one for wire 0.{{ $use }} func (c *singleSourceZeroCheckClaims) {{ $name }}() polynomial.Polynomial{{ .Config.ExtensionSuffix }} { level := c.resources.schedule[c.levelI].(*constraint.GkrSingleSourceZeroCheckLevel) degree := c.resources.circuit.ZeroCheckDegree(level) From 64b1b91b87397b915c96b8a3e4573e611a68c4f4 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Sun, 18 Oct 2026 15:06:06 -0500 Subject: [PATCH 30/37] feat: use field hashers in transcript Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr.go | 12 ++-- gkr/bls12-377/gkr_test.go | 19 +++--- gkr/bls12-377/sumcheck.go | 2 +- gkr/bls12-377/sumcheck_test.go | 5 +- gkr/bls12-377/test_vector_utils_test.go | 58 +++++++------------ gkr/bls12-381/gkr.go | 12 ++-- gkr/bls12-381/gkr_test.go | 19 +++--- gkr/bls12-381/sumcheck.go | 2 +- gkr/bls12-381/sumcheck_test.go | 5 +- gkr/bls12-381/test_vector_utils_test.go | 58 +++++++------------ gkr/bn254/gkr.go | 12 ++-- gkr/bn254/gkr_test.go | 19 +++--- gkr/bn254/sumcheck.go | 2 +- gkr/bn254/sumcheck_test.go | 5 +- gkr/bn254/test_vector_utils_test.go | 58 +++++++------------ gkr/bw6-761/gkr.go | 12 ++-- gkr/bw6-761/gkr_test.go | 19 +++--- gkr/bw6-761/sumcheck.go | 2 +- gkr/bw6-761/sumcheck_test.go | 5 +- gkr/bw6-761/test_vector_utils_test.go | 58 +++++++------------ gkr/gkrapi/example_test.go | 9 ++- gkr/koalabear/gkr.go | 15 +++-- gkr/koalabear/gkr_test.go | 20 ++++--- gkr/koalabear/sumcheck.go | 2 +- gkr/koalabear/sumcheck_test.go | 5 +- gkr/koalabear/test_vector_utils_test.go | 58 +++++++------------ go.mod | 2 +- go.sum | 4 +- .../backend/template/gkr/blueprint.go.tmpl | 9 ++- .../backend/template/gkr/gkr.go.tmpl | 17 ++++-- .../backend/template/gkr/gkr.test.go.tmpl | 24 ++++++-- .../template/gkr/gkr.test.vectors.gen.go.tmpl | 1 - .../template/gkr/gkr.test.vectors.go.tmpl | 4 +- .../backend/template/gkr/sumcheck.go.tmpl | 2 +- .../template/gkr/sumcheck.test.go.tmpl | 5 +- .../gkr/sumcheck.test.vectors.gen.go.tmpl | 3 +- .../template/gkr/test_vector_utils.go.tmpl | 58 +++++++------------ .../gkr/bls12-377/blueprints/blueprint.go | 9 ++- .../gkr/bls12-381/blueprints/blueprint.go | 9 ++- internal/gkr/bn254/blueprints/blueprint.go | 9 ++- internal/gkr/bw6-761/blueprints/blueprint.go | 9 ++- internal/gkr/gkrcore/transcript.go | 57 +++++++----------- internal/gkr/gkrcore/transcript_test.go | 6 +- internal/gkr/rational/gkr.go | 12 ++-- internal/gkr/rational/sumcheck.go | 2 +- internal/gkr/rational/sumcheck_test.go | 5 +- .../gkr/rational/sumcheck_test_vector_gen.go | 3 +- internal/gkr/rational/test_vector_gen.go | 5 +- internal/gkr/rational/test_vector_utils.go | 58 +++++++------------ std/gkrapi/api_test.go | 43 +++++++++++--- 50 files changed, 394 insertions(+), 455 deletions(-) diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index d87653db80..22834f03a5 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -8,11 +8,11 @@ package gkrbls12377 import ( "errors" "fmt" - "hash" "iter" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" @@ -92,7 +92,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[fr.Element, fr.Element]) resources { return resources{ outgoingEvalPoints: make([][][]fr.Element, len(schedule)+1), nbVars: nbVars, @@ -183,7 +183,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[fr.Element, fr.Element]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -279,9 +279,9 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // not bound into the transcript. // // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, -// with a commitment to the circuit, the schedule, the number of instances and the input and output -// columns, computed from values it trusts. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { +// by writing to it a commitment to the circuit, the schedule, the number of instances and the input +// and output columns, computed from values it trusts. +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[fr.Element, fr.Element]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index 9969dcd7e2..252717ca14 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -7,7 +7,6 @@ package gkrbls12377 import ( "fmt" - "hash" "os" "path/filepath" "reflect" @@ -18,8 +17,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" - gcHash "github.com/consensys/gnark-crypto/hash" - _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/gkr/gkrapi" @@ -31,6 +29,11 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_377.ScalarField())) +// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. +func newPoseidon2Hasher() fieldHasher { + return poseidon2.NewMerkleDamgardHasher() +} + func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -96,7 +99,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BLS12_377.New()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) require.NoError(t, err) var flattened []fr.Element @@ -109,7 +112,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BLS12_377.New()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) if err != nil { return nil, err } @@ -405,7 +408,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BLS12_377.New()) + _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -577,7 +580,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { type TestCase struct { Circuit gkrcore.SerializableCircuit - Hash hash.Hash + Hash fieldHasher Proof Proof FullAssignment WireAssignment InOutAssignment WireAssignment @@ -604,7 +607,7 @@ func newTestCase(path string) (*TestCase, error) { } circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) - var _hash hash.Hash + var _hash fieldHasher if _hash, err = hashFromDescription(info.Hash); err != nil { return nil, err } diff --git a/gkr/bls12-377/sumcheck.go b/gkr/bls12-377/sumcheck.go index 193f249bf9..0fc6a24fe5 100644 --- a/gkr/bls12-377/sumcheck.go +++ b/gkr/bls12-377/sumcheck.go @@ -19,7 +19,7 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] +type transcript = gkrcore.HashTranscript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) diff --git a/gkr/bls12-377/sumcheck_test.go b/gkr/bls12-377/sumcheck_test.go index 0db18c30e3..3d0f0e9c3f 100644 --- a/gkr/bls12-377/sumcheck_test.go +++ b/gkr/bls12-377/sumcheck_test.go @@ -7,7 +7,6 @@ package gkrbls12377 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -22,7 +21,7 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { poly := make(polynomial.MultiLin, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) @@ -71,7 +70,7 @@ func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { const MaxStep = 4 const MaxStart = 4 - hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + hashGens := make([]func() fieldHasher, 0, MaxStart*MaxStep) for step := 0; step < MaxStep; step++ { for startState := 0; startState < MaxStart; startState++ { diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index c9d60f9804..db8253244f 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -7,23 +7,26 @@ package gkrbls12377 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) +// fieldHasher is the hasher the transcript of this field's GKR runs on. +type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] + // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. -func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { +func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, step: 0, state: startState}, nil + return &messageCounter{state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -31,50 +34,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } -// messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = fr.Bytes - -// messageCounter is a stand-in hash whose state depends only on the number of field-element -// blocks written to it, not on their values. +// messageCounter is a stand-in hasher whose state depends only on the number of field elements +// written to it, not on their values. type messageCounter struct { - startState int64 - state int64 - step int64 + state int64 + step int64 } -func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 - m.state += int64(inputBlockSize) * m.step - return len(p), nil +func (m *messageCounter) WriteField(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 - resI := m.state + int64(inputBlockSize)*m.step - var res fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) Reset() { - m.state = m.startState -} - -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} - -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +func (m *messageCounter) SumField() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -func newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{state: int64(startState), step: int64(step)} } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { return newMessageCounter(startState, step) } } diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index 230413739c..7bf9a9a0e8 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -8,11 +8,11 @@ package gkrbls12381 import ( "errors" "fmt" - "hash" "iter" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" @@ -92,7 +92,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[fr.Element, fr.Element]) resources { return resources{ outgoingEvalPoints: make([][][]fr.Element, len(schedule)+1), nbVars: nbVars, @@ -183,7 +183,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[fr.Element, fr.Element]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -279,9 +279,9 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // not bound into the transcript. // // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, -// with a commitment to the circuit, the schedule, the number of instances and the input and output -// columns, computed from values it trusts. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { +// by writing to it a commitment to the circuit, the schedule, the number of instances and the input +// and output columns, computed from values it trusts. +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[fr.Element, fr.Element]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index 6d924348a0..8e68a30910 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -7,7 +7,6 @@ package gkrbls12381 import ( "fmt" - "hash" "os" "path/filepath" "reflect" @@ -18,8 +17,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" - gcHash "github.com/consensys/gnark-crypto/hash" - _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/poseidon2" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/gkr/gkrapi" @@ -31,6 +29,11 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_381.ScalarField())) +// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. +func newPoseidon2Hasher() fieldHasher { + return poseidon2.NewMerkleDamgardHasher() +} + func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -96,7 +99,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BLS12_381.New()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) require.NoError(t, err) var flattened []fr.Element @@ -109,7 +112,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BLS12_381.New()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) if err != nil { return nil, err } @@ -405,7 +408,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BLS12_381.New()) + _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -577,7 +580,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { type TestCase struct { Circuit gkrcore.SerializableCircuit - Hash hash.Hash + Hash fieldHasher Proof Proof FullAssignment WireAssignment InOutAssignment WireAssignment @@ -604,7 +607,7 @@ func newTestCase(path string) (*TestCase, error) { } circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) - var _hash hash.Hash + var _hash fieldHasher if _hash, err = hashFromDescription(info.Hash); err != nil { return nil, err } diff --git a/gkr/bls12-381/sumcheck.go b/gkr/bls12-381/sumcheck.go index 0bb48512a2..1372ec2038 100644 --- a/gkr/bls12-381/sumcheck.go +++ b/gkr/bls12-381/sumcheck.go @@ -19,7 +19,7 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] +type transcript = gkrcore.HashTranscript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) diff --git a/gkr/bls12-381/sumcheck_test.go b/gkr/bls12-381/sumcheck_test.go index 3813bd91a4..557b03c1b3 100644 --- a/gkr/bls12-381/sumcheck_test.go +++ b/gkr/bls12-381/sumcheck_test.go @@ -7,7 +7,6 @@ package gkrbls12381 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -22,7 +21,7 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { poly := make(polynomial.MultiLin, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) @@ -71,7 +70,7 @@ func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { const MaxStep = 4 const MaxStart = 4 - hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + hashGens := make([]func() fieldHasher, 0, MaxStart*MaxStep) for step := 0; step < MaxStep; step++ { for startState := 0; startState < MaxStart; startState++ { diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index 8c99eb643a..6b99cc6857 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -7,23 +7,26 @@ package gkrbls12381 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) +// fieldHasher is the hasher the transcript of this field's GKR runs on. +type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] + // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. -func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { +func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, step: 0, state: startState}, nil + return &messageCounter{state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -31,50 +34,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } -// messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = fr.Bytes - -// messageCounter is a stand-in hash whose state depends only on the number of field-element -// blocks written to it, not on their values. +// messageCounter is a stand-in hasher whose state depends only on the number of field elements +// written to it, not on their values. type messageCounter struct { - startState int64 - state int64 - step int64 + state int64 + step int64 } -func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 - m.state += int64(inputBlockSize) * m.step - return len(p), nil +func (m *messageCounter) WriteField(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 - resI := m.state + int64(inputBlockSize)*m.step - var res fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) Reset() { - m.state = m.startState -} - -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} - -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +func (m *messageCounter) SumField() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -func newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{state: int64(startState), step: int64(step)} } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { return newMessageCounter(startState, step) } } diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index 1086aca79d..d1f4fbcc1a 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -8,11 +8,11 @@ package gkrbn254 import ( "errors" "fmt" - "hash" "iter" "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" @@ -92,7 +92,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[fr.Element, fr.Element]) resources { return resources{ outgoingEvalPoints: make([][][]fr.Element, len(schedule)+1), nbVars: nbVars, @@ -183,7 +183,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[fr.Element, fr.Element]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -279,9 +279,9 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // not bound into the transcript. // // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, -// with a commitment to the circuit, the schedule, the number of instances and the input and output -// columns, computed from values it trusts. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { +// by writing to it a commitment to the circuit, the schedule, the number of instances and the input +// and output columns, computed from values it trusts. +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[fr.Element, fr.Element]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index 62af0587ab..d58257e39e 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -7,7 +7,6 @@ package gkrbn254 import ( "fmt" - "hash" "os" "path/filepath" "reflect" @@ -18,8 +17,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" - gcHash "github.com/consensys/gnark-crypto/hash" - _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses + "github.com/consensys/gnark-crypto/ecc/bn254/fr/poseidon2" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/gkr/gkrapi" @@ -31,6 +29,11 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) +// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. +func newPoseidon2Hasher() fieldHasher { + return poseidon2.NewMerkleDamgardHasher() +} + func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -96,7 +99,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BN254.New()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) require.NoError(t, err) var flattened []fr.Element @@ -109,7 +112,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BN254.New()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) if err != nil { return nil, err } @@ -405,7 +408,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BN254.New()) + _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -577,7 +580,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { type TestCase struct { Circuit gkrcore.SerializableCircuit - Hash hash.Hash + Hash fieldHasher Proof Proof FullAssignment WireAssignment InOutAssignment WireAssignment @@ -604,7 +607,7 @@ func newTestCase(path string) (*TestCase, error) { } circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) - var _hash hash.Hash + var _hash fieldHasher if _hash, err = hashFromDescription(info.Hash); err != nil { return nil, err } diff --git a/gkr/bn254/sumcheck.go b/gkr/bn254/sumcheck.go index 4158dcee3e..adc94a782d 100644 --- a/gkr/bn254/sumcheck.go +++ b/gkr/bn254/sumcheck.go @@ -19,7 +19,7 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] +type transcript = gkrcore.HashTranscript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) diff --git a/gkr/bn254/sumcheck_test.go b/gkr/bn254/sumcheck_test.go index 253c54b9e8..cb046e930a 100644 --- a/gkr/bn254/sumcheck_test.go +++ b/gkr/bn254/sumcheck_test.go @@ -7,7 +7,6 @@ package gkrbn254 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -22,7 +21,7 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { poly := make(polynomial.MultiLin, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) @@ -71,7 +70,7 @@ func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { const MaxStep = 4 const MaxStart = 4 - hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + hashGens := make([]func() fieldHasher, 0, MaxStart*MaxStep) for step := 0; step < MaxStep; step++ { for startState := 0; startState < MaxStart; startState++ { diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index c94eb6ca0a..69dd38d2cb 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -7,23 +7,26 @@ package gkrbn254 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) +// fieldHasher is the hasher the transcript of this field's GKR runs on. +type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] + // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. -func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { +func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, step: 0, state: startState}, nil + return &messageCounter{state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -31,50 +34,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } -// messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = fr.Bytes - -// messageCounter is a stand-in hash whose state depends only on the number of field-element -// blocks written to it, not on their values. +// messageCounter is a stand-in hasher whose state depends only on the number of field elements +// written to it, not on their values. type messageCounter struct { - startState int64 - state int64 - step int64 + state int64 + step int64 } -func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 - m.state += int64(inputBlockSize) * m.step - return len(p), nil +func (m *messageCounter) WriteField(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 - resI := m.state + int64(inputBlockSize)*m.step - var res fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) Reset() { - m.state = m.startState -} - -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} - -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +func (m *messageCounter) SumField() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -func newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{state: int64(startState), step: int64(step)} } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { return newMessageCounter(startState, step) } } diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index 1390b24a08..f94d15a602 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -8,11 +8,11 @@ package gkrbw6761 import ( "errors" "fmt" - "hash" "iter" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" @@ -92,7 +92,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[fr.Element, fr.Element]) resources { return resources{ outgoingEvalPoints: make([][][]fr.Element, len(schedule)+1), nbVars: nbVars, @@ -183,7 +183,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[fr.Element, fr.Element]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -279,9 +279,9 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // not bound into the transcript. // // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, -// with a commitment to the circuit, the schedule, the number of instances and the input and output -// columns, computed from values it trusts. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { +// by writing to it a commitment to the circuit, the schedule, the number of instances and the input +// and output columns, computed from values it trusts. +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[fr.Element, fr.Element]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index fac98092bf..21aa97af1d 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -7,7 +7,6 @@ package gkrbw6761 import ( "fmt" - "hash" "os" "path/filepath" "reflect" @@ -18,8 +17,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" - gcHash "github.com/consensys/gnark-crypto/hash" - _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/poseidon2" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/gkr/gkrapi" @@ -31,6 +29,11 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BW6_761.ScalarField())) +// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. +func newPoseidon2Hasher() fieldHasher { + return poseidon2.NewMerkleDamgardHasher() +} + func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -96,7 +99,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_BW6_761.New()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) require.NoError(t, err) var flattened []fr.Element @@ -109,7 +112,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_BW6_761.New()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) if err != nil { return nil, err } @@ -405,7 +408,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_BW6_761.New()) + _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -577,7 +580,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { type TestCase struct { Circuit gkrcore.SerializableCircuit - Hash hash.Hash + Hash fieldHasher Proof Proof FullAssignment WireAssignment InOutAssignment WireAssignment @@ -604,7 +607,7 @@ func newTestCase(path string) (*TestCase, error) { } circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) - var _hash hash.Hash + var _hash fieldHasher if _hash, err = hashFromDescription(info.Hash); err != nil { return nil, err } diff --git a/gkr/bw6-761/sumcheck.go b/gkr/bw6-761/sumcheck.go index 98875cea86..c269175910 100644 --- a/gkr/bw6-761/sumcheck.go +++ b/gkr/bw6-761/sumcheck.go @@ -19,7 +19,7 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element, *fr.Element] +type transcript = gkrcore.HashTranscript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) diff --git a/gkr/bw6-761/sumcheck_test.go b/gkr/bw6-761/sumcheck_test.go index 6433f253f8..f235a5cdd9 100644 --- a/gkr/bw6-761/sumcheck_test.go +++ b/gkr/bw6-761/sumcheck_test.go @@ -7,7 +7,6 @@ package gkrbw6761 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -22,7 +21,7 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { poly := make(polynomial.MultiLin, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) @@ -71,7 +70,7 @@ func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { const MaxStep = 4 const MaxStart = 4 - hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + hashGens := make([]func() fieldHasher, 0, MaxStart*MaxStep) for step := 0; step < MaxStep; step++ { for startState := 0; startState < MaxStart; startState++ { diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index a2eac5982f..56fe33620f 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -7,23 +7,26 @@ package gkrbw6761 import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) +// fieldHasher is the hasher the transcript of this field's GKR runs on. +type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] + // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. -func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { +func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, step: 0, state: startState}, nil + return &messageCounter{state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -31,50 +34,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } -// messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = fr.Bytes - -// messageCounter is a stand-in hash whose state depends only on the number of field-element -// blocks written to it, not on their values. +// messageCounter is a stand-in hasher whose state depends only on the number of field elements +// written to it, not on their values. type messageCounter struct { - startState int64 - state int64 - step int64 + state int64 + step int64 } -func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 - m.state += int64(inputBlockSize) * m.step - return len(p), nil +func (m *messageCounter) WriteField(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 - resI := m.state + int64(inputBlockSize)*m.step - var res fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) Reset() { - m.state = m.startState -} - -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} - -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +func (m *messageCounter) SumField() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -func newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{state: int64(startState), step: int64(step)} } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { return newMessageCounter(startState, step) } } diff --git a/gkr/gkrapi/example_test.go b/gkr/gkrapi/example_test.go index d2699d647d..9338dff485 100644 --- a/gkr/gkrapi/example_test.go +++ b/gkr/gkrapi/example_test.go @@ -5,8 +5,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bn254/fr" - _ "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" // registers the MiMC hash used below - gcHash "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" gkrbn254 "github.com/consensys/gnark/gkr/bn254" "github.com/consensys/gnark/gkr/gkrapi" ) @@ -29,9 +28,9 @@ func Example() { assignment[y] = []fr.Element{fr.NewElement(5), fr.NewElement(7)} // assignment[z] is left nil: Prove computes it. - // Prove and Verify must be given hashes seeded identically with a commitment to the circuit, + // Prove and Verify must be given hashers seeded identically with a commitment to the circuit, // the schedule, the number of instances and the inputs and outputs. The example omits it. - proof, proverClaims, err := gkrbn254.Prove(circuit, schedule, assignment, gcHash.MIMC_BN254.New()) + proof, proverClaims, err := gkrbn254.Prove(circuit, schedule, assignment, mimc.NewFieldHasher()) assertNoError(err) assertNoError(proverClaims.Check(assignment)) @@ -42,7 +41,7 @@ func Example() { deserialized, err := gkrbn254.DeserializeProof(circuit, schedule, flattened) assertNoError(err) - verifierClaims, err := gkrbn254.Verify(circuit, schedule, 1, deserialized, gcHash.MIMC_BN254.New()) + verifierClaims, err := gkrbn254.Verify(circuit, schedule, 1, deserialized, mimc.NewFieldHasher()) assertNoError(err) assertNoError(verifierClaims.Check(assignment)) // every returned claim must be checked diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index e1fd4fe158..27f612a316 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -8,13 +8,13 @@ package gkrkoalabear import ( "errors" "fmt" - "hash" "iter" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" basePolynomial "github.com/consensys/gnark-crypto/field/koalabear/polynomial" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" @@ -104,7 +104,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[extensions.E6, extensions.E6]) resources { return resources{ outgoingEvalPoints: make([][][]extensions.E6, len(schedule)+1), nbVars: nbVars, @@ -195,7 +195,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[extensions.E6, extensions.E6]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -291,9 +291,12 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // not bound into the transcript. // // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, -// with a commitment to the circuit, the schedule, the number of instances and the input and output -// columns, computed from values it trusts. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { +// by writing to it a commitment to the circuit, the schedule, the number of instances and the input +// and output columns, computed from values it trusts. +// Over the extension, wrap a base field hasher with gnark-crypto's +// NewBaseFieldHashAdapterE6; base field seed elements can be written to the +// base hasher directly. +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[extensions.E6, extensions.E6]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go index a8cef54425..ce3fa92ba8 100644 --- a/gkr/koalabear/gkr_test.go +++ b/gkr/koalabear/gkr_test.go @@ -7,7 +7,6 @@ package gkrkoalabear import ( "fmt" - "hash" "os" "path/filepath" "reflect" @@ -17,9 +16,9 @@ import ( "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" + extensionsHash "github.com/consensys/gnark-crypto/field/koalabear/extensions/hash" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" - gcHash "github.com/consensys/gnark-crypto/hash" - _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses + poseidon2 "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/gkr/gkrapi" @@ -31,6 +30,11 @@ import ( var cache = gkrtesting.NewCache(gkr.KoalaBearE6()) +// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. +func newPoseidon2Hasher() fieldHasher { + return extensionsHash.NewBaseFieldHashAdapterE6(poseidon2.NewMerkleDamgardHasher()) +} + func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []koalabear.Element{four, three}) @@ -96,7 +100,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_KOALABEAR.New()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) require.NoError(t, err) var flattened []extensions.E6 @@ -109,7 +113,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_KOALABEAR.New()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) if err != nil { return nil, err } @@ -416,7 +420,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_KOALABEAR.New()) + _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -588,7 +592,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { type TestCase struct { Circuit gkrcore.SerializableCircuit - Hash hash.Hash + Hash fieldHasher Proof Proof FullAssignment WireAssignment InOutAssignment WireAssignment @@ -615,7 +619,7 @@ func newTestCase(path string) (*TestCase, error) { } circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) - var _hash hash.Hash + var _hash fieldHasher if _hash, err = hashFromDescription(info.Hash); err != nil { return nil, err } diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index 58ddc24a21..f1757cac15 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -21,7 +21,7 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[extensions.E6, *extensions.E6] +type transcript = gkrcore.HashTranscript[extensions.E6] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) diff --git a/gkr/koalabear/sumcheck_test.go b/gkr/koalabear/sumcheck_test.go index 621ea5a83e..6df2fc6ac0 100644 --- a/gkr/koalabear/sumcheck_test.go +++ b/gkr/koalabear/sumcheck_test.go @@ -7,7 +7,6 @@ package gkrkoalabear import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" @@ -22,7 +21,7 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { poly := make(polynomial.MultiLinE6, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) @@ -71,7 +70,7 @@ func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { const MaxStep = 4 const MaxStart = 4 - hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + hashGens := make([]func() fieldHasher, 0, MaxStart*MaxStep) for step := 0; step < MaxStep; step++ { for startState := 0; startState < MaxStart; startState++ { diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index 88ba671630..4b91a67b2c 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -7,23 +7,26 @@ package gkrkoalabear import ( "fmt" - "hash" "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) +// fieldHasher is the hasher the transcript of this field's GKR runs on. +type fieldHasher = hash.FieldHasher[extensions.E6, extensions.E6] + // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. -func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { +func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, step: 0, state: startState}, nil + return &messageCounter{state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -31,50 +34,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } -// messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = extensions.BytesE6 - -// messageCounter is a stand-in hash whose state depends only on the number of field-element -// blocks written to it, not on their values. +// messageCounter is a stand-in hasher whose state depends only on the number of field elements +// written to it, not on their values. type messageCounter struct { - startState int64 - state int64 - step int64 + state int64 + step int64 } -func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 - m.state += int64(inputBlockSize) * m.step - return len(p), nil +func (m *messageCounter) WriteField(e ...extensions.E6) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 - resI := m.state + int64(inputBlockSize)*m.step - var res extensions.E6 - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) Reset() { - m.state = m.startState -} - -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} - -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +func (m *messageCounter) SumField() extensions.E6 { + var res extensions.E6 + res.SetInt64(m.state + m.step) + return res } -func newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{state: int64(startState), step: int64(step)} } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { return newMessageCounter(startState, step) } } diff --git a/go.mod b/go.mod index be7d1b4674..2d72fec0d9 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.22.1-0.20261009142911-8911ee45d22b + github.com/consensys/gnark-crypto v0.22.1-0.20261018191837-1782a7d6eb90 github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 diff --git a/go.sum b/go.sum index 5f09b940b1..7b77b4742c 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.22.1-0.20261009142911-8911ee45d22b h1:IDT/Waq2euwKsSOVZGufUN1tjYTN94Jf7FdSqt0XrH0= -github.com/consensys/gnark-crypto v0.22.1-0.20261009142911-8911ee45d22b/go.mod h1:HL8XflRG4cvau1PEG3CWBTD2kkzUzdlqZF6BBgy57F8= +github.com/consensys/gnark-crypto v0.22.1-0.20261018191837-1782a7d6eb90 h1:PWLwzhmT1Cu1/Zyd66vv4PZOSJIewHr1ssYfyb8cwxA= +github.com/consensys/gnark-crypto v0.22.1-0.20261018191837-1782a7d6eb90/go.mod h1:HL8XflRG4cvau1PEG3CWBTD2kkzUzdlqZF6BBgy57F8= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= diff --git a/internal/generator/backend/template/gkr/blueprint.go.tmpl b/internal/generator/backend/template/gkr/blueprint.go.tmpl index 2122c303d0..6a4ec3aa9f 100644 --- a/internal/generator/backend/template/gkr/blueprint.go.tmpl +++ b/internal/generator/backend/template/gkr/blueprint.go.tmpl @@ -254,7 +254,11 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_{{.FieldID}}") + hashName := b.HashName + "_{{.FieldID}}" + hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) + if !ok { + return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -266,8 +270,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to {{ .ElementType }} (both in Montgomery form) var challenge {{ .ElementType }} copy(challenge[:], val[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteField(challenge) } // Call the {{.FieldID}}-specific Prove function (assignments already WireAssignment type) diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index eab01ccf9b..bd2a771742 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -2,7 +2,6 @@ import ( "errors" "fmt" - "hash" "iter" "{{.FieldPackagePath}}" @@ -11,6 +10,7 @@ import ( "{{ .BaseFieldPackagePath }}" basePolynomial "{{ .BaseFieldPackagePath }}/polynomial" {{- end }} + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" @@ -111,7 +111,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) resources { return resources{ outgoingEvalPoints: make([][][]{{ .ElementType }}, len(schedule)+1), nbVars: nbVars, @@ -206,7 +206,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -310,9 +310,14 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // not bound into the transcript. // // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, -// with a commitment to the circuit, the schedule, the number of instances and the input and output -// columns, computed from values it trusts. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { +// by writing to it a commitment to the circuit, the schedule, the number of instances and the input +// and output columns, computed from values it trusts. +{{- if .Mixed }} +// Over the extension, wrap a base field hasher with gnark-crypto's +// NewBaseFieldHashAdapter{{ .ExtensionSuffix }}; base field seed elements can be written to the +// base hasher directly. +{{- end }} +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index aba15280ba..bf6b2d1f73 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -1,7 +1,6 @@ import ( "fmt" - "hash" "os" "path/filepath" "reflect" @@ -10,8 +9,12 @@ import ( "time" "github.com/consensys/gnark-crypto/ecc" - gcHash "github.com/consensys/gnark-crypto/hash" - _ "github.com/consensys/gnark-crypto/hash/all" // registers the hash benchmarkGkrMiMC uses +{{- if .Mixed }} + extensionsHash "{{ .FieldPackagePath }}/hash" + poseidon2 "{{ .BaseFieldPackagePath }}/poseidon2" +{{- else }} + "{{ .FieldPackagePath }}/poseidon2" +{{- end }} "{{ .FieldPackagePath }}" "{{ .FieldPackagePath }}/polynomial" {{- if .Mixed }} @@ -28,6 +31,15 @@ import ( var cache = gkrtesting.NewCache({{ .FieldDescription }}) +// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. +func newPoseidon2Hasher() fieldHasher { + {{- if .Mixed }} + return extensionsHash.NewBaseFieldHashAdapter{{ .ExtensionSuffix }}(poseidon2.NewMerkleDamgardHasher()) + {{- else }} + return poseidon2.NewMerkleDamgardHasher() + {{- end }} +} + func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []{{ .BaseElementType }}{four, three}) @@ -93,7 +105,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, gcHash.POSEIDON2_{{ .FieldID }}.New()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) require.NoError(t, err) var flattened []{{ .ElementType }} @@ -106,7 +118,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, gcHash.POSEIDON2_{{ .FieldID }}.New()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) if err != nil { return nil, err } @@ -419,7 +431,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, gcHash.POSEIDON2_{{ .FieldID }}.New()) + _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl index 88c103b5e9..ddedc0abd1 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl @@ -9,7 +9,6 @@ import ( "github.com/consensys/gnark/internal/rational/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/gkr/gkrcore" - "hash" "os" "path/filepath" "reflect" diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl index ba5b41b58e..09a2f24047 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl @@ -31,7 +31,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { type TestCase struct { Circuit gkrcore.SerializableCircuit - Hash hash.Hash + Hash fieldHasher Proof Proof FullAssignment WireAssignment InOutAssignment WireAssignment @@ -60,7 +60,7 @@ func newTestCase(path string) (*TestCase, error) { } circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) - var _hash hash.Hash + var _hash fieldHasher if _hash, err = hashFromDescription(info.Hash); err != nil { return nil, err } diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index a9ae13bf12..bddc85884e 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -19,7 +19,7 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[{{ .ElementType }}, *{{ .ElementType }}] +type transcript = gkrcore.HashTranscript[{{ .ElementType }}] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) diff --git a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl index 9974e0af35..1581dcc363 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl @@ -1,6 +1,5 @@ import ( "fmt" - "hash" "{{ .FieldPackagePath }}/polynomial" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -13,7 +12,7 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { poly := make(polynomial.MultiLin{{ .ExtensionSuffix }}, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) @@ -62,7 +61,7 @@ func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { const MaxStep = 4 const MaxStart = 4 - hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + hashGens := make([]func() fieldHasher, 0, MaxStart*MaxStep) for step := 0; step < MaxStep; step++ { for startState := 0; startState < MaxStart; startState++ { diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl index ef6994c607..2c945a9a12 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -1,7 +1,6 @@ import ( "encoding/json" "fmt" - "hash" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/rational" "github.com/consensys/gnark/internal/rational/polynomial" @@ -22,7 +21,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } var ( - hsh hash.Hash + hsh fieldHasher err error ) diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 55a9786e32..7eab25c1cc 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -1,21 +1,24 @@ import ( "fmt" - "hash" "{{.FieldPackagePath}}" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) +// fieldHasher is the hasher the transcript of this field's GKR runs on. +type fieldHasher = hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}] + // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. -func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { +func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter {startState: startState, step: 0, state: startState}, nil + return &messageCounter{state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -23,50 +26,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } -// messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = {{ .FieldPackageName }}.Bytes{{ .ExtensionSuffix }} - -// messageCounter is a stand-in hash whose state depends only on the number of field-element -// blocks written to it, not on their values. +// messageCounter is a stand-in hasher whose state depends only on the number of field elements +// written to it, not on their values. type messageCounter struct { - startState int64 - state int64 - step int64 -} - -func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 - m.state += int64(inputBlockSize) * m.step - return len(p), nil + state int64 + step int64 } -func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 - resI := m.state + int64(inputBlockSize)*m.step - var res {{.ElementType}} - res.SetInt64(int64(resI)) - return (&res).Marshal() +func (m *messageCounter) WriteField(e ...{{ .ElementType }}) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Reset() { - m.state = m.startState -} - -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +func (m *messageCounter) SumField() {{ .ElementType }} { + var res {{ .ElementType }} + res.SetInt64(m.state + m.step) + return res } -func newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{state: int64(startState), step: int64(step)} } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { return newMessageCounter(startState, step) } } diff --git a/internal/gkr/bls12-377/blueprints/blueprint.go b/internal/gkr/bls12-377/blueprints/blueprint.go index 19fe8fe26c..63c98f6cbe 100644 --- a/internal/gkr/bls12-377/blueprints/blueprint.go +++ b/internal/gkr/bls12-377/blueprints/blueprint.go @@ -259,7 +259,11 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BLS12_377") + hashName := b.HashName + "_BLS12_377" + hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) + if !ok { + return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +275,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteField(challenge) } // Call the BLS12_377-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bls12-381/blueprints/blueprint.go b/internal/gkr/bls12-381/blueprints/blueprint.go index 035587f952..8c8dc604e7 100644 --- a/internal/gkr/bls12-381/blueprints/blueprint.go +++ b/internal/gkr/bls12-381/blueprints/blueprint.go @@ -259,7 +259,11 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BLS12_381") + hashName := b.HashName + "_BLS12_381" + hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) + if !ok { + return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +275,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteField(challenge) } // Call the BLS12_381-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bn254/blueprints/blueprint.go b/internal/gkr/bn254/blueprints/blueprint.go index 455ecf4722..58b9790cf6 100644 --- a/internal/gkr/bn254/blueprints/blueprint.go +++ b/internal/gkr/bn254/blueprints/blueprint.go @@ -259,7 +259,11 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BN254") + hashName := b.HashName + "_BN254" + hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) + if !ok { + return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +275,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteField(challenge) } // Call the BN254-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bw6-761/blueprints/blueprint.go b/internal/gkr/bw6-761/blueprints/blueprint.go index 92aca944f6..431c35e2eb 100644 --- a/internal/gkr/bw6-761/blueprints/blueprint.go +++ b/internal/gkr/bw6-761/blueprints/blueprint.go @@ -259,7 +259,11 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BW6_761") + hashName := b.HashName + "_BW6_761" + hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) + if !ok { + return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +275,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteField(challenge) } // Call the BW6_761-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/gkrcore/transcript.go b/internal/gkr/gkrcore/transcript.go index acf354cb7f..be7a8d2024 100644 --- a/internal/gkr/gkrcore/transcript.go +++ b/internal/gkr/gkrcore/transcript.go @@ -1,57 +1,40 @@ package gkrcore -import "hash" +import "github.com/consensys/gnark-crypto/hash" -// HashTranscript is a Fiat-Shamir transcript backed by a running hash. Field elements are written -// via Bind; challenges are derived via Challenge. The hash is never reset — all previous data is -// implicitly part of future challenges. -type HashTranscript[E any, PE interface { - *E - SetBytesCanonical([]byte) error - Marshal() []byte -}] struct { - h hash.Hash - size int // length of an element's Marshal(), computed once from the zero element +// HashTranscript is a Fiat-Shamir transcript backed by a running field hasher. Field elements are +// written via Bind; challenges are derived via Challenge. The hasher is never reset — all previous +// data is implicitly part of future challenges. +type HashTranscript[E any] struct { + h hash.FieldHasher[E, E] bound bool // whether Bind was called since the last Challenge } -// NewHashTranscript returns a transcript backed by h. Bind writes each element's Marshal(). -// Challenge writes a separator byte if nothing was bound since the last challenge, then returns -// the element set by SetBytesCanonical from the first len(Marshal()) bytes of h.Sum(nil). -// h.Size() must be at least len(Marshal()). -func NewHashTranscript[E any, PE interface { - *E - SetBytesCanonical([]byte) error - Marshal() []byte -}](h hash.Hash) *HashTranscript[E, PE] { - var zero E - return &HashTranscript[E, PE]{h: h, size: len(PE(&zero).Marshal())} +// NewHashTranscript returns a transcript backed by h. Bind writes the elements with one WriteField call. +// Challenge writes the zero element if nothing was bound since the last challenge, then returns +// h.SumField(). +func NewHashTranscript[E any](h hash.FieldHasher[E, E]) *HashTranscript[E] { + return &HashTranscript[E]{h: h} } // Bind writes field elements to the transcript as bindings for the next challenge. -func (t *HashTranscript[E, PE]) Bind(elements ...E) { +func (t *HashTranscript[E]) Bind(elements ...E) { if len(elements) == 0 { return } - for i := range elements { - t.h.Write(PE(&elements[i]).Marshal()) - } + t.h.WriteField(elements...) t.bound = true } -// Challenge binds elements as Bind would, then squeezes a challenge from the current hash state. -// If no bindings were added since the last challenge, a separator byte is written first to -// advance the state and prevent repeated values. It panics if the digest prefix is not a canonical -// encoding of an element, which a hash producing field elements never yields. -func (t *HashTranscript[E, PE]) Challenge(elements ...E) E { +// Challenge binds elements as Bind would, then squeezes a challenge from the current hasher state. +// If no bindings were added since the last challenge, the zero element is written first to advance +// the state and prevent repeated values. +func (t *HashTranscript[E]) Challenge(elements ...E) E { t.Bind(elements...) if !t.bound { - t.h.Write([]byte{0}) + var zero E + t.h.WriteField(zero) } t.bound = false - var res E - if err := PE(&res).SetBytesCanonical(t.h.Sum(nil)[:t.size]); err != nil { - panic(err) - } - return res + return t.h.SumField() } diff --git a/internal/gkr/gkrcore/transcript_test.go b/internal/gkr/gkrcore/transcript_test.go index 0b9f56ca20..7c41d311b7 100644 --- a/internal/gkr/gkrcore/transcript_test.go +++ b/internal/gkr/gkrcore/transcript_test.go @@ -5,8 +5,8 @@ import ( "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" - gcHash "github.com/consensys/gnark-crypto/hash" - _ "github.com/consensys/gnark-crypto/hash/all" // registers POSEIDON2_KOALABEAR + extensionsHash "github.com/consensys/gnark-crypto/field/koalabear/extensions/hash" + "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" "github.com/stretchr/testify/assert" ) @@ -24,7 +24,7 @@ func TestHashTranscriptBindsWholeElement(t *testing.T) { } challenge := func(e extensions.E6) extensions.E6 { - transcript := NewHashTranscript[extensions.E6](gcHash.POSEIDON2_KOALABEAR.New()) + transcript := NewHashTranscript[extensions.E6](extensionsHash.NewBaseFieldHashAdapterE6(poseidon2.NewMerkleDamgardHasher())) transcript.Bind(e) return transcript.Challenge() } diff --git a/internal/gkr/rational/gkr.go b/internal/gkr/rational/gkr.go index b671efb367..66454e6451 100644 --- a/internal/gkr/rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -8,9 +8,9 @@ package gkrrational import ( "errors" "fmt" - "hash" "iter" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark-crypto/utils" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/gkr" @@ -91,7 +91,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.Hash) resources { +func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[rational.Element, rational.Element]) resources { return resources{ outgoingEvalPoints: make([][][]rational.Element, len(schedule)+1), nbVars: nbVars, @@ -182,7 +182,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.Hash) (Proof, Claims, error) { +func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[rational.Element, rational.Element]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -278,9 +278,9 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // not bound into the transcript. // // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, -// with a commitment to the circuit, the schedule, the number of instances and the input and output -// columns, computed from values it trusts. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.Hash) (Claims, error) { +// by writing to it a commitment to the circuit, the schedule, the number of instances and the input +// and output columns, computed from values it trusts. +func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[rational.Element, rational.Element]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/internal/gkr/rational/sumcheck.go b/internal/gkr/rational/sumcheck.go index 7719fb1d99..4fe90fce2b 100644 --- a/internal/gkr/rational/sumcheck.go +++ b/internal/gkr/rational/sumcheck.go @@ -18,7 +18,7 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. // transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[rational.Element, *rational.Element] +type transcript = gkrcore.HashTranscript[rational.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) diff --git a/internal/gkr/rational/sumcheck_test.go b/internal/gkr/rational/sumcheck_test.go index 9280007b99..677dda5308 100644 --- a/internal/gkr/rational/sumcheck_test.go +++ b/internal/gkr/rational/sumcheck_test.go @@ -7,7 +7,6 @@ package gkrrational import ( "fmt" - "hash" "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -19,7 +18,7 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { poly := make(polynomial.MultiLin, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) @@ -68,7 +67,7 @@ func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { const MaxStep = 4 const MaxStart = 4 - hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + hashGens := make([]func() fieldHasher, 0, MaxStart*MaxStep) for step := 0; step < MaxStep; step++ { for startState := 0; startState < MaxStart; startState++ { diff --git a/internal/gkr/rational/sumcheck_test_vector_gen.go b/internal/gkr/rational/sumcheck_test_vector_gen.go index eeb5d658ca..6d1f687338 100644 --- a/internal/gkr/rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/rational/sumcheck_test_vector_gen.go @@ -8,7 +8,6 @@ package gkrrational import ( "encoding/json" "fmt" - "hash" "math/bits" "os" "path/filepath" @@ -30,7 +29,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } var ( - hsh hash.Hash + hsh fieldHasher err error ) diff --git a/internal/gkr/rational/test_vector_gen.go b/internal/gkr/rational/test_vector_gen.go index 8d6958ee11..c2d968614b 100644 --- a/internal/gkr/rational/test_vector_gen.go +++ b/internal/gkr/rational/test_vector_gen.go @@ -8,7 +8,6 @@ package gkrrational import ( "encoding/json" "fmt" - "hash" "os" "path/filepath" "reflect" @@ -201,7 +200,7 @@ func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { type TestCase struct { Circuit gkrcore.SerializableCircuit - Hash hash.Hash + Hash fieldHasher Proof Proof FullAssignment WireAssignment InOutAssignment WireAssignment @@ -229,7 +228,7 @@ func newTestCase(path string) (*TestCase, error) { } circuit, _ := cache.GetCircuit(filepath.Join(dir, info.Circuit)) - var _hash hash.Hash + var _hash fieldHasher if _hash, err = hashFromDescription(info.Hash); err != nil { return nil, err } diff --git a/internal/gkr/rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go index d0fe08e212..e2ea3e9e1d 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -7,23 +7,26 @@ package gkrrational import ( "fmt" - "hash" + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/consensys/gnark/internal/rational" ) +// fieldHasher is the hasher the transcript of this field's GKR runs on. +type fieldHasher = hash.FieldHasher[rational.Element, rational.Element] + // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what was // bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. -func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { +func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, step: 0, state: startState}, nil + return &messageCounter{state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -31,50 +34,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { return nil, fmt.Errorf("hash description missing type") } -// messageCounterBlockSize is the length of an element's Marshal(). -const messageCounterBlockSize = rational.Bytes - -// messageCounter is a stand-in hash whose state depends only on the number of field-element -// blocks written to it, not on their values. +// messageCounter is a stand-in hasher whose state depends only on the number of field elements +// written to it, not on their values. type messageCounter struct { - startState int64 - state int64 - step int64 + state int64 + step int64 } -func (m *messageCounter) Write(p []byte) (n int, err error) { - inputBlockSize := (len(p)-1)/messageCounterBlockSize + 1 - m.state += int64(inputBlockSize) * m.step - return len(p), nil +func (m *messageCounter) WriteField(e ...rational.Element) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Sum(b []byte) []byte { - inputBlockSize := (len(b)-1)/messageCounterBlockSize + 1 - resI := m.state + int64(inputBlockSize)*m.step - var res rational.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) Reset() { - m.state = m.startState -} - -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} - -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +func (m *messageCounter) SumField() rational.Element { + var res rational.Element + res.SetInt64(m.state + m.step) + return res } -func newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{state: int64(startState), step: int64(step)} } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { return newMessageCounter(startState, step) } } diff --git a/std/gkrapi/api_test.go b/std/gkrapi/api_test.go index 13ef3910c5..c1a27d5d77 100644 --- a/std/gkrapi/api_test.go +++ b/std/gkrapi/api_test.go @@ -11,8 +11,11 @@ import ( "testing" "time" - "github.com/consensys/gnark" "github.com/consensys/gnark-crypto/ecc" + frbls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + frbls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + frbn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr" + frbw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" gcHash "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/frontend" @@ -596,6 +599,34 @@ func newConstBytesPseudoHash(c int64, mod *big.Int) constBytesPseudoHash { return b } +// constFieldPseudoHash is a constant "hash" over the elements of F: it is both a hash.Hash, as +// RegisterCustomHash requires, and a gnark-crypto hash.FieldHasher, as the GKR blueprints require. +type constFieldPseudoHash[F any] struct { + constBytesPseudoHash + digest F +} + +func (h constFieldPseudoHash[F]) WriteField(...F) {} + +func (h constFieldPseudoHash[F]) PadBlock() {} + +func (h constFieldPseudoHash[F]) SumField() F { + return h.digest +} + +// registerConstPseudoHash registers the constant hash v, over the scalar field of curve, as +// name_CURVE. +func registerConstPseudoHash[F any, PF interface { + *F + SetBigInt(*big.Int) *F +}](name string, v int64, curve ecc.ID) { + h := constFieldPseudoHash[F]{constBytesPseudoHash: newConstBytesPseudoHash(v, curve.ScalarField())} + PF(&h.digest).SetBigInt(big.NewInt(v)) + gcHash.RegisterCustomHash(name+"_"+strings.ToUpper(curve.String()), func() hash.Hash { + return h + }) +} + func init() { // Register custom (constant) "hash" functions for _, v := range []int64{-1, -20} { @@ -603,12 +634,10 @@ func init() { stdHash.RegisterCustomHash(name, func(api frontend.API) (stdHash.FieldHasher, error) { return constPseudoHash(v), nil }) - for _, curve := range gnark.Curves() { - h := newConstBytesPseudoHash(v, curve.ScalarField()) - gcHash.RegisterCustomHash(name+"_"+strings.ToUpper(curve.String()), func() hash.Hash { - return h - }) - } + registerConstPseudoHash[frbn254.Element](name, v, ecc.BN254) + registerConstPseudoHash[frbls12377.Element](name, v, ecc.BLS12_377) + registerConstPseudoHash[frbls12381.Element](name, v, ecc.BLS12_381) + registerConstPseudoHash[frbw6761.Element](name, v, ecc.BW6_761) } } From 81c88ace4b05fcba1eb8862b0d7b630812b0f7d1 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Sun, 18 Oct 2026 15:15:21 -0500 Subject: [PATCH 31/37] fix: always pad blocks before summing Signed-off-by: Arya Tabaie --- internal/gkr/gkrcore/transcript.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/gkr/gkrcore/transcript.go b/internal/gkr/gkrcore/transcript.go index be7a8d2024..c46a24a631 100644 --- a/internal/gkr/gkrcore/transcript.go +++ b/internal/gkr/gkrcore/transcript.go @@ -10,9 +10,7 @@ type HashTranscript[E any] struct { bound bool // whether Bind was called since the last Challenge } -// NewHashTranscript returns a transcript backed by h. Bind writes the elements with one WriteField call. -// Challenge writes the zero element if nothing was bound since the last challenge, then returns -// h.SumField(). +// NewHashTranscript returns a transcript backed by h. func NewHashTranscript[E any](h hash.FieldHasher[E, E]) *HashTranscript[E] { return &HashTranscript[E]{h: h} } @@ -36,5 +34,6 @@ func (t *HashTranscript[E]) Challenge(elements ...E) E { t.h.WriteField(zero) } t.bound = false + t.h.PadBlock() return t.h.SumField() } From e09460efbd71f9b16db9c84d43cc08aaaf1c416d Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Sun, 18 Oct 2026 15:25:05 -0500 Subject: [PATCH 32/37] docs: clarify field matching Signed-off-by: Arya Tabaie --- gkr/gkrapi/api.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gkr/gkrapi/api.go b/gkr/gkrapi/api.go index 691ca4ae78..fd31acea02 100644 --- a/gkr/gkrapi/api.go +++ b/gkr/gkrapi/api.go @@ -68,7 +68,8 @@ func (api *API) Export(in ...gkr.Variable) { api.circuit.Export(in...) } -// Compile compiles the circuit for field, and builds its default proving schedule under mode. +// Compile compiles the circuit for field, and builds its default proving schedule under mode. The +// circuit must be proven and verified with the backend for field. func (api *API) Compile(field gkr.Field, mode ConsolidationMode) (Circuit, constraint.GkrProvingSchedule, error) { _, circuit, err := api.circuit.Compile(field) if err != nil { From 1c8a037f3801c913db90d6d1c94d22f029c3e871 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Mon, 19 Oct 2026 16:53:54 -0500 Subject: [PATCH 33/37] refactor: transcript renames Signed-off-by: Arya Tabaie --- constraint/gkr.go | 13 ++++--- gkr/bls12-377/gkr.go | 39 ++++++++++--------- gkr/bls12-377/sumcheck.go | 12 +++--- gkr/bls12-377/test_vector_utils_test.go | 4 +- gkr/bls12-381/gkr.go | 39 ++++++++++--------- gkr/bls12-381/sumcheck.go | 12 +++--- gkr/bls12-381/test_vector_utils_test.go | 4 +- gkr/bn254/gkr.go | 39 ++++++++++--------- gkr/bn254/sumcheck.go | 12 +++--- gkr/bn254/test_vector_utils_test.go | 4 +- gkr/bw6-761/gkr.go | 39 ++++++++++--------- gkr/bw6-761/sumcheck.go | 12 +++--- gkr/bw6-761/test_vector_utils_test.go | 4 +- gkr/koalabear/gkr.go | 39 ++++++++++--------- gkr/koalabear/sumcheck.go | 12 +++--- gkr/koalabear/test_vector_utils_test.go | 4 +- .../backend/template/gkr/gkr.go.tmpl | 39 ++++++++++--------- .../backend/template/gkr/sumcheck.go.tmpl | 12 +++--- .../template/gkr/test_vector_utils.go.tmpl | 4 +- internal/gkr/gkr.go | 29 +++++++------- internal/gkr/gkr_test.go | 4 +- internal/gkr/gkrcore/transcript.go | 28 ++++++------- internal/gkr/gkrcore/transcript_test.go | 8 ++-- internal/gkr/rational/gkr.go | 39 ++++++++++--------- internal/gkr/rational/sumcheck.go | 12 +++--- internal/gkr/rational/test_vector_utils.go | 4 +- internal/gkr/sumcheck.go | 20 +++++----- 27 files changed, 248 insertions(+), 239 deletions(-) diff --git a/constraint/gkr.go b/constraint/gkr.go index 7cfc8034a1..2741d9801b 100644 --- a/constraint/gkr.go +++ b/constraint/gkr.go @@ -83,13 +83,14 @@ func (l *GkrSingleSourceZeroCheckLevel) ClaimGroups() []GkrClaimGroup { } func (l *GkrSingleSourceZeroCheckLevel) FinalEvalProofIndex(wireI, _ int) int { return wireI } -// BindGkrFinalEvalProof binds the entries of finalEvalProof belonging to the unique gate inputs -// bind selects. A claimed value returned to the caller unconsolidated is checked directly against -// the assignment (see Claims.Check), never reduced by a further level, so it is not bound. -func BindGkrFinalEvalProof[F any](transcript interface{ Bind(...F) }, finalEvalProof []F, uniqueGateInputs []int, bind func(wireI int) bool, level GkrProvingLevel) { +// AbsorbGkrFinalEvalProof absorbs the entries of finalEvalProof belonging to the unique gate inputs +// wireI for which absorb(wireI) holds. A claimed value returned to the caller unconsolidated is +// checked directly against the assignment (see Claims.Check), never reduced by a further level, so +// it is not absorbed. +func AbsorbGkrFinalEvalProof[F any](transcript interface{ Absorb(...F) }, finalEvalProof []F, uniqueGateInputs []int, absorb func(wireI int) bool, level GkrProvingLevel) { for i, inputWireI := range uniqueGateInputs { - if bind(inputWireI) { - transcript.Bind(finalEvalProof[level.FinalEvalProofIndex(i, 0):level.FinalEvalProofIndex(i+1, 0)]...) + if absorb(inputWireI) { + transcript.Absorb(finalEvalProof[level.FinalEvalProofIndex(i, 0):level.FinalEvalProofIndex(i+1, 0)]...) } } } diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index 22834f03a5..dc96b79afa 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -148,11 +148,12 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { return nil } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -160,7 +161,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +// proveLevel proves level levelI: computes its proof entry, absorbs its values, and appends its claims. func (r *resources) proveLevel(levelI int) sumcheckProof { var entry sumcheckProof switch r.schedule[levelI].(type) { @@ -173,8 +174,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { default: panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -221,7 +222,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -231,13 +232,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range outputs { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputEvals[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputEvals[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputEvals, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { @@ -249,7 +250,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss return proof, r.claims, nil } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { var err error switch r.schedule[levelI].(type) { @@ -265,8 +266,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { if err != nil { return fmt.Errorf("level %d: %v", levelI, err) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -321,16 +322,16 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/gkr/bls12-377/sumcheck.go b/gkr/bls12-377/sumcheck.go index 0fc6a24fe5..ef416179e3 100644 --- a/gkr/bls12-377/sumcheck.go +++ b/gkr/bls12-377/sumcheck.go @@ -53,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + challenges[j] = t.Squeeze(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Squeeze(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -79,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.Challenge(proof.partialSumPolys[j]...) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -357,7 +357,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.Challenge() + c.foldingCoeff = r.transcript.Squeeze() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -442,7 +442,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.Challenge() + foldingCoeff = r.transcript.Squeeze() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -698,7 +698,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.Challenge(partialPoly...) + challenges[j] = r.transcript.Squeeze(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index db8253244f..0565728ea7 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -17,8 +17,8 @@ import ( type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index 7bf9a9a0e8..29ddae9872 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -148,11 +148,12 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { return nil } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -160,7 +161,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +// proveLevel proves level levelI: computes its proof entry, absorbs its values, and appends its claims. func (r *resources) proveLevel(levelI int) sumcheckProof { var entry sumcheckProof switch r.schedule[levelI].(type) { @@ -173,8 +174,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { default: panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -221,7 +222,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -231,13 +232,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range outputs { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputEvals[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputEvals[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputEvals, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { @@ -249,7 +250,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss return proof, r.claims, nil } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { var err error switch r.schedule[levelI].(type) { @@ -265,8 +266,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { if err != nil { return fmt.Errorf("level %d: %v", levelI, err) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -321,16 +322,16 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/gkr/bls12-381/sumcheck.go b/gkr/bls12-381/sumcheck.go index 1372ec2038..156ad60fa0 100644 --- a/gkr/bls12-381/sumcheck.go +++ b/gkr/bls12-381/sumcheck.go @@ -53,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + challenges[j] = t.Squeeze(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Squeeze(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -79,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.Challenge(proof.partialSumPolys[j]...) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -357,7 +357,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.Challenge() + c.foldingCoeff = r.transcript.Squeeze() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -442,7 +442,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.Challenge() + foldingCoeff = r.transcript.Squeeze() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -698,7 +698,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.Challenge(partialPoly...) + challenges[j] = r.transcript.Squeeze(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index 6b99cc6857..eef044f25b 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -17,8 +17,8 @@ import ( type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index d1f4fbcc1a..63a3d63c27 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -148,11 +148,12 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { return nil } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -160,7 +161,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +// proveLevel proves level levelI: computes its proof entry, absorbs its values, and appends its claims. func (r *resources) proveLevel(levelI int) sumcheckProof { var entry sumcheckProof switch r.schedule[levelI].(type) { @@ -173,8 +174,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { default: panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -221,7 +222,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -231,13 +232,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range outputs { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputEvals[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputEvals[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputEvals, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { @@ -249,7 +250,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss return proof, r.claims, nil } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { var err error switch r.schedule[levelI].(type) { @@ -265,8 +266,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { if err != nil { return fmt.Errorf("level %d: %v", levelI, err) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -321,16 +322,16 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/gkr/bn254/sumcheck.go b/gkr/bn254/sumcheck.go index adc94a782d..8584d65ce5 100644 --- a/gkr/bn254/sumcheck.go +++ b/gkr/bn254/sumcheck.go @@ -53,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + challenges[j] = t.Squeeze(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Squeeze(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -79,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.Challenge(proof.partialSumPolys[j]...) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -357,7 +357,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.Challenge() + c.foldingCoeff = r.transcript.Squeeze() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -442,7 +442,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.Challenge() + foldingCoeff = r.transcript.Squeeze() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -698,7 +698,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.Challenge(partialPoly...) + challenges[j] = r.transcript.Squeeze(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 69dd38d2cb..3599fc22cd 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -17,8 +17,8 @@ import ( type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index f94d15a602..dca927d368 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -148,11 +148,12 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { return nil } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -160,7 +161,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +// proveLevel proves level levelI: computes its proof entry, absorbs its values, and appends its claims. func (r *resources) proveLevel(levelI int) sumcheckProof { var entry sumcheckProof switch r.schedule[levelI].(type) { @@ -173,8 +174,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { default: panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -221,7 +222,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} @@ -231,13 +232,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range outputs { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputEvals[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputEvals[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputEvals, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { @@ -249,7 +250,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss return proof, r.claims, nil } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { var err error switch r.schedule[levelI].(type) { @@ -265,8 +266,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { if err != nil { return fmt.Errorf("level %d: %v", levelI, err) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -321,16 +322,16 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]fr.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]fr.Element{firstChallenge} - var boundOutputEvals []fr.Element + var absorbedOutputEvals []fr.Element for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/gkr/bw6-761/sumcheck.go b/gkr/bw6-761/sumcheck.go index c269175910..a4cd7d97d2 100644 --- a/gkr/bw6-761/sumcheck.go +++ b/gkr/bw6-761/sumcheck.go @@ -53,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]fr.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + challenges[j] = t.Squeeze(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Squeeze(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -79,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum f copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.Challenge(proof.partialSumPolys[j]...) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -357,7 +357,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.Challenge() + c.foldingCoeff = r.transcript.Squeeze() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -442,7 +442,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (fr.Element, *zero level := r.schedule[levelI] var foldingCoeff fr.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.Challenge() + foldingCoeff = r.transcript.Squeeze() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -698,7 +698,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.Challenge(partialPoly...) + challenges[j] = r.transcript.Squeeze(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index 56fe33620f..7301fd67ad 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -17,8 +17,8 @@ import ( type fieldHasher = hash.FieldHasher[fr.Element, fr.Element] // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index 27f612a316..7acf66be38 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -160,11 +160,12 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { return nil } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -172,7 +173,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +// proveLevel proves level levelI: computes its proof entry, absorbs its values, and appends its claims. func (r *resources) proveLevel(levelI int) sumcheckProof { var entry sumcheckProof switch r.schedule[levelI].(type) { @@ -185,8 +186,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { default: panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -233,7 +234,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]extensions.E6, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]extensions.E6{firstChallenge} @@ -243,13 +244,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputEvals[i] = r.evaluateBase(r.assignment[w], firstChallenge) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []extensions.E6 + var absorbedOutputEvals []extensions.E6 for i, w := range outputs { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputEvals[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputEvals[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputEvals, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { @@ -261,7 +262,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss return proof, r.claims, nil } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { var err error switch r.schedule[levelI].(type) { @@ -277,8 +278,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { if err != nil { return fmt.Errorf("level %d: %v", levelI, err) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -336,16 +337,16 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]extensions.E6, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]extensions.E6{firstChallenge} - var boundOutputEvals []extensions.E6 + var absorbedOutputEvals []extensions.E6 for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index f1757cac15..ea5803ae14 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -55,11 +55,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]extensions.E6, varsNum) for j := range varsNum - 1 { - challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + challenges[j] = t.Squeeze(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Squeeze(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -81,7 +81,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum e copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.Challenge(proof.partialSumPolys[j]...) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRangeE6(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -494,7 +494,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.Challenge() + c.foldingCoeff = r.transcript.Squeeze() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -582,7 +582,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (extensions.E6, *z level := r.schedule[levelI] var foldingCoeff extensions.E6 if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.Challenge() + foldingCoeff = r.transcript.Squeeze() } claimedEvals := make(polynomial.PolynomialE6, 0, level.NbClaims()) @@ -933,7 +933,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.Challenge(partialPoly...) + challenges[j] = r.transcript.Squeeze(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRangeE6(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index 4b91a67b2c..dea7a8cad6 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -17,8 +17,8 @@ import ( type fieldHasher = hash.FieldHasher[extensions.E6, extensions.E6] // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index bd2a771742..ace561eb81 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -171,11 +171,12 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { return nil } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -183,7 +184,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +// proveLevel proves level levelI: computes its proof entry, absorbs its values, and appends its claims. func (r *resources) proveLevel(levelI int) sumcheckProof { var entry sumcheckProof switch r.schedule[levelI].(type) { @@ -196,8 +197,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { default: panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -248,7 +249,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]{{ .ElementType }}, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]{{ .ElementType }}{firstChallenge} @@ -262,13 +263,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss {{- end }} } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []{{ .ElementType }} + var absorbedOutputEvals []{{ .ElementType }} for i, w := range outputs { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputEvals[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputEvals[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputEvals, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { @@ -280,7 +281,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss return proof, r.claims, nil } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { var err error switch r.schedule[levelI].(type) { @@ -296,8 +297,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { if err != nil { return fmt.Errorf("level %d: %v", levelI, err) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -357,16 +358,16 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]{{ .ElementType }}, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]{{ .ElementType }}{firstChallenge} - var boundOutputEvals []{{ .ElementType }} + var absorbedOutputEvals []{{ .ElementType }} for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index bddc85884e..9eb7e8ba38 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -53,11 +53,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]{{ .ElementType }}, varsNum) for j := range varsNum - 1 { - challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + challenges[j] = t.Squeeze(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Squeeze(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -79,7 +79,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum { copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.Challenge(proof.partialSumPolys[j]...) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange{{ .ExtensionSuffix }}(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -340,7 +340,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.Challenge() + c.foldingCoeff = r.transcript.Squeeze() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -438,7 +438,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) ({{ .ElementType } level := r.schedule[levelI] var foldingCoeff {{ .ElementType }} if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.Challenge() + foldingCoeff = r.transcript.Squeeze() } claimedEvals := make(polynomial.Polynomial{{ .ExtensionSuffix }}, 0, level.NbClaims()) @@ -641,7 +641,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.Challenge(partialPoly...) + challenges[j] = r.transcript.Squeeze(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange{{ .ExtensionSuffix }}(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 7eab25c1cc..06dcab738e 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -9,8 +9,8 @@ import ( type fieldHasher = hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}] // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { diff --git a/internal/gkr/gkr.go b/internal/gkr/gkr.go index 706668eb22..53978a796c 100644 --- a/internal/gkr/gkr.go +++ b/internal/gkr/gkr.go @@ -167,7 +167,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (frontend.Variable foldingCoeff := frontend.Variable(0) if level.NbClaims() >= 2 { - foldingCoeff = r.t.getChallenge() + foldingCoeff = r.t.Squeeze() } var claimedEvals []frontend.Variable @@ -221,7 +221,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialSumPoly) - challenges[j] = r.t.getChallenge(proof[levelI].PartialSumPolys[j]...) + challenges[j] = r.t.Squeeze(proof[levelI].PartialSumPolys[j]...) claimedSum = polynomial.InterpolateLDE(r.api, challenges[j], gPrime[:(degree+1)]) } @@ -233,11 +233,12 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er return lazyClaims.verifyFinalEval(r.api, challenges, claimedSum, proof[levelI].FinalEvalProof) } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -245,7 +246,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { switch r.schedule[levelI].(type) { case *constraint.GkrSkipLevel: @@ -259,8 +260,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { return err } } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.t, proof[levelI].FinalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.t, proof[levelI].FinalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].FinalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -317,16 +318,16 @@ func Verify(api frontend.API, c Circuit, schedule constraint.GkrProvingSchedule, firstChallenge := make([]frontend.Variable, logNbInstances) for j := range logNbInstances { - firstChallenge[j] = r.t.getChallenge() + firstChallenge[j] = r.t.Squeeze() } r.outgoingEvalPoints[initialChallengeI] = [][]frontend.Variable{firstChallenge} - var boundOutputEvals []frontend.Variable + var absorbedOutputEvals []frontend.Variable for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.FinalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.FinalEvalProof[i]) } } - r.t.Bind(boundOutputEvals...) + r.t.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.FinalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/internal/gkr/gkr_test.go b/internal/gkr/gkr_test.go index 78b55efed6..b7fec5d9c0 100644 --- a/internal/gkr/gkr_test.go +++ b/internal/gkr/gkr_test.go @@ -283,8 +283,8 @@ func TestLogNbInstances(t *testing.T) { type hashDescription map[string]interface{} // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(api frontend.API, d hashDescription) (hash.FieldHasher, error) { if _type, ok := d["type"]; ok { diff --git a/internal/gkr/gkrcore/transcript.go b/internal/gkr/gkrcore/transcript.go index c46a24a631..449a1637f3 100644 --- a/internal/gkr/gkrcore/transcript.go +++ b/internal/gkr/gkrcore/transcript.go @@ -2,12 +2,12 @@ package gkrcore import "github.com/consensys/gnark-crypto/hash" -// HashTranscript is a Fiat-Shamir transcript backed by a running field hasher. Field elements are -// written via Bind; challenges are derived via Challenge. The hasher is never reset — all previous -// data is implicitly part of future challenges. +// HashTranscript is a Fiat-Shamir transcript backed by a running field hasher. Absorb writes field +// elements; Squeeze derives challenges from them. The hasher is never reset — all previous data is +// implicitly part of future challenges. type HashTranscript[E any] struct { - h hash.FieldHasher[E, E] - bound bool // whether Bind was called since the last Challenge + h hash.FieldHasher[E, E] + absorbed bool // whether Absorb was called since the last Squeeze } // NewHashTranscript returns a transcript backed by h. @@ -15,25 +15,25 @@ func NewHashTranscript[E any](h hash.FieldHasher[E, E]) *HashTranscript[E] { return &HashTranscript[E]{h: h} } -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *HashTranscript[E]) Bind(elements ...E) { +// Absorb writes field elements to the transcript, to be part of the next squeeze. +func (t *HashTranscript[E]) Absorb(elements ...E) { if len(elements) == 0 { return } t.h.WriteField(elements...) - t.bound = true + t.absorbed = true } -// Challenge binds elements as Bind would, then squeezes a challenge from the current hasher state. -// If no bindings were added since the last challenge, the zero element is written first to advance +// Squeeze absorbs elements as Absorb would, then derives a challenge from the hasher's state. +// If nothing was absorbed since the last squeeze, the zero element is written first to advance // the state and prevent repeated values. -func (t *HashTranscript[E]) Challenge(elements ...E) E { - t.Bind(elements...) - if !t.bound { +func (t *HashTranscript[E]) Squeeze(elements ...E) E { + t.Absorb(elements...) + if !t.absorbed { var zero E t.h.WriteField(zero) } - t.bound = false + t.absorbed = false t.h.PadBlock() return t.h.SumField() } diff --git a/internal/gkr/gkrcore/transcript_test.go b/internal/gkr/gkrcore/transcript_test.go index 7c41d311b7..ad054a8b13 100644 --- a/internal/gkr/gkrcore/transcript_test.go +++ b/internal/gkr/gkrcore/transcript_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" ) -// TestHashTranscriptBindsWholeElement checks that every coordinate of a bound extension element -// affects the next challenge. +// TestHashTranscriptBindsWholeElement checks that every coordinate of an absorbed extension +// element affects the next challenge. func TestHashTranscriptBindsWholeElement(t *testing.T) { var element extensions.E6 element.MustSetRandom() @@ -25,8 +25,8 @@ func TestHashTranscriptBindsWholeElement(t *testing.T) { challenge := func(e extensions.E6) extensions.E6 { transcript := NewHashTranscript[extensions.E6](extensionsHash.NewBaseFieldHashAdapterE6(poseidon2.NewMerkleDamgardHasher())) - transcript.Bind(e) - return transcript.Challenge() + transcript.Absorb(e) + return transcript.Squeeze() } reference := challenge(element) diff --git a/internal/gkr/rational/gkr.go b/internal/gkr/rational/gkr.go index 66454e6451..f899136e86 100644 --- a/internal/gkr/rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -147,11 +147,12 @@ func (r *resources) verifySkipLevel(levelI int, proof Proof) error { return nil } -// levelPredicates returns the bind and include predicates for level levelI's unique gate inputs. -// Level 0 binds nothing and returns every one of its wires (self-referencing on the consolidation -// view); every other level binds every unique gate input that is not an unconsolidated circuit -// input, and returns claims for exactly those it withholds from binding. -func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool) { +// levelPredicates returns, for level levelI's unique gate inputs, the predicate absorb, which selects +// those absorbed into the transcript, and the predicate include, which selects those whose claims are +// returned. Level 0 absorbs nothing and returns every one of its wires (self-referencing on the +// consolidation view); every other level absorbs every unique gate input that is not an +// unconsolidated circuit input, and returns claims for exactly those it withholds from absorbing. +func (r *resources) levelPredicates(levelI int) (absorb, include func(wI int) bool) { if levelI == 0 { return func(int) bool { return false }, func(int) bool { return true } } @@ -159,7 +160,7 @@ func (r *resources) levelPredicates(levelI int) (bind, include func(wI int) bool func(wI int) bool { return r.circuit.IsInput(wI) && !r.consolidated[wI] } } -// proveLevel proves level levelI: computes its proof entry, binds its values, and appends its claims. +// proveLevel proves level levelI: computes its proof entry, absorbs its values, and appends its claims. func (r *resources) proveLevel(levelI int) sumcheckProof { var entry sumcheckProof switch r.schedule[levelI].(type) { @@ -172,8 +173,8 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { default: panic(fmt.Sprintf("level %d: unknown proving level type %T", levelI, r.schedule[levelI])) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, entry.finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], entry.finalEvalProof, r.outgoingEvalPoints[levelI], include) return entry } @@ -220,7 +221,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss // Derive the initial challenge point firstChallenge := make([]rational.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]rational.Element{firstChallenge} @@ -230,13 +231,13 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss outputEvals[i] = r.assignment[w].Evaluate(firstChallenge, &r.memPool) } proof[len(schedule)] = sumcheckProof{finalEvalProof: outputEvals} - var boundOutputEvals []rational.Element + var absorbedOutputEvals []rational.Element for i, w := range outputs { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputEvals[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputEvals[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputEvals, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { @@ -248,7 +249,7 @@ func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAss return proof, r.claims, nil } -// verifyLevel verifies level levelI: checks its proof entry, binds its values, and appends its claims. +// verifyLevel verifies level levelI: checks its proof entry, absorbs its values, and appends its claims. func (r *resources) verifyLevel(levelI int, proof Proof) error { var err error switch r.schedule[levelI].(type) { @@ -264,8 +265,8 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { if err != nil { return fmt.Errorf("level %d: %v", levelI, err) } - bind, include := r.levelPredicates(levelI) - constraint.BindGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), bind, r.schedule[levelI]) + absorb, include := r.levelPredicates(levelI) + constraint.AbsorbGkrFinalEvalProof(r.transcript, proof[levelI].finalEvalProof, r.circuit.UniqueGateInputs(r.schedule[levelI]), absorb, r.schedule[levelI]) gkrcore.AppendLevelClaims(r.claims, r.circuit, r.schedule[levelI], proof[levelI].finalEvalProof, r.outgoingEvalPoints[levelI], include) return nil } @@ -320,16 +321,16 @@ func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances in // Derive the initial challenge point firstChallenge := make([]rational.Element, r.nbVars) for j := range r.nbVars { - firstChallenge[j] = r.transcript.Challenge() + firstChallenge[j] = r.transcript.Squeeze() } r.outgoingEvalPoints[len(schedule)] = [][]rational.Element{firstChallenge} - var boundOutputEvals []rational.Element + var absorbedOutputEvals []rational.Element for i, w := range c.Outputs() { if r.consolidated[w] { - boundOutputEvals = append(boundOutputEvals, outputLevel.finalEvalProof[i]) + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) } } - r.transcript.Bind(boundOutputEvals...) + r.transcript.Absorb(absorbedOutputEvals...) gkrcore.AppendOutputClaims(r.claims, c, firstChallenge, outputLevel.finalEvalProof, func(wI int) bool { return !r.consolidated[wI] }) for levelI := len(schedule) - 1; levelI >= 1; levelI-- { diff --git a/internal/gkr/rational/sumcheck.go b/internal/gkr/rational/sumcheck.go index 4fe90fce2b..8921735444 100644 --- a/internal/gkr/rational/sumcheck.go +++ b/internal/gkr/rational/sumcheck.go @@ -52,11 +52,11 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { challenges := make([]rational.Element, varsNum) for j := range varsNum - 1 { - challenges[j] = t.Challenge(proof.partialSumPolys[j]...) + challenges[j] = t.Squeeze(proof.partialSumPolys[j]...) claims.roundFold(challenges[j]) proof.partialSumPolys[j+1] = claims.roundPolynomial() } - challenges[varsNum-1] = t.Challenge(proof.partialSumPolys[varsNum-1]...) + challenges[varsNum-1] = t.Squeeze(proof.partialSumPolys[varsNum-1]...) proof.finalEvalProof = claims.proveFinalEval(challenges) return proof @@ -78,7 +78,7 @@ func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum r copy(gJ[1:], proof.partialSumPolys[j]) gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) - r[j] = t.Challenge(proof.partialSumPolys[j]...) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -356,7 +356,7 @@ func (c *zeroCheckBase) init(r *resources, levelI int) { c.resources = r level := r.schedule[levelI] if level.NbClaims() >= 2 { - c.foldingCoeff = r.transcript.Challenge() + c.foldingCoeff = r.transcript.Squeeze() } uniqueInputs, inputIndices := r.circuit.InputMapping(level) @@ -441,7 +441,7 @@ func (r *resources) verifyLevelSetup(levelI int, proof Proof) (rational.Element, level := r.schedule[levelI] var foldingCoeff rational.Element if level.NbClaims() >= 2 { - foldingCoeff = r.transcript.Challenge() + foldingCoeff = r.transcript.Squeeze() } claimedEvals := make(polynomial.Polynomial, 0, level.NbClaims()) @@ -697,7 +697,7 @@ func (r *resources) verifySingleSourceZeroCheckLevel(levelI int, proof Proof) er copy(gPrime[1:], partialPoly) - challenges[j] = r.transcript.Challenge(partialPoly...) + challenges[j] = r.transcript.Squeeze(partialPoly...) gPrimeCoeffs := polynomial.InterpolateOnRange(gPrime[:(degree + 1)]) claimedSum = gPrimeCoeffs.Eval(&challenges[j]) } diff --git a/internal/gkr/rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go index e2ea3e9e1d..4a8e32b3ce 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -17,8 +17,8 @@ import ( type fieldHasher = hash.FieldHasher[rational.Element, rational.Element] // hashFromDescription returns the hash a test vector names. The only type is "const": a -// messageCounter with step 0, so every challenge equals the given value regardless of what was -// bound to the transcript. Test vectors therefore pin down the GKR and sumcheck arithmetic only. +// messageCounter with step 0, so every challenge equals the given value regardless of what the +// transcript absorbed. Test vectors therefore pin down the GKR and sumcheck arithmetic only. // Transcript handling is exercised by the tests using newMessageCounter with a nonzero step, and // end to end by the std/gkrapi tests, which use real hashes. func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { diff --git a/internal/gkr/sumcheck.go b/internal/gkr/sumcheck.go index a3deee14f7..b87c6fb9a5 100644 --- a/internal/gkr/sumcheck.go +++ b/internal/gkr/sumcheck.go @@ -24,27 +24,27 @@ type sumcheckProof struct { } // transcript is a Fiat-Shamir transcript backed by a running hash. -// Field elements are written via Bind; challenges are derived via getChallenge. +// Absorb writes field elements; Squeeze derives challenges from them. // The hash is never reset — all previous data is implicitly part of future challenges. type transcript struct { - h hash.FieldHasher - bound bool + h hash.FieldHasher + absorbed bool } -func (t *transcript) Bind(elements ...frontend.Variable) { +func (t *transcript) Absorb(elements ...frontend.Variable) { if len(elements) == 0 { return } t.h.Write(elements...) - t.bound = true + t.absorbed = true } -func (t *transcript) getChallenge(bindings ...frontend.Variable) frontend.Variable { - t.Bind(bindings...) - if !t.bound { +func (t *transcript) Squeeze(elements ...frontend.Variable) frontend.Variable { + t.Absorb(elements...) + if !t.absorbed { t.h.Write(0) // separator to prevent repeated values } - t.bound = false + t.absorbed = false return t.h.Sum() } @@ -62,7 +62,7 @@ func verifySumcheck(api frontend.API, claims sumcheckLazyClaims, proof sumcheckP copy(gJ[1:], partialSumPoly) gJ[0] = api.Sub(gJR, partialSumPoly[0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) - r[j] = t.getChallenge(proof.PartialSumPolys[j]...) + r[j] = t.Squeeze(proof.PartialSumPolys[j]...) gJR = polynomial.InterpolateLDE(api, r[j], gJ[:(degree+1)]) } From f1f3233786b0c3a95d88f22b5099d67f7dbe539c Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 3 Nov 2026 00:33:27 -0600 Subject: [PATCH 34/37] build: update gnark-crypto pin Signed-off-by: Arya Tabaie --- gkr/bls12-377/gkr.go | 4 +- gkr/bls12-377/gkr_test.go | 11 +- gkr/bls12-377/sumcheck.go | 8 +- gkr/bls12-377/sumcheck_test.go | 6 +- gkr/bls12-377/test_vector_utils_test.go | 19 ++-- gkr/bls12-381/gkr.go | 4 +- gkr/bls12-381/gkr_test.go | 11 +- gkr/bls12-381/sumcheck.go | 8 +- gkr/bls12-381/sumcheck_test.go | 6 +- gkr/bls12-381/test_vector_utils_test.go | 19 ++-- gkr/bn254/gkr.go | 4 +- gkr/bn254/gkr_test.go | 11 +- gkr/bn254/sumcheck.go | 8 +- gkr/bn254/sumcheck_test.go | 6 +- gkr/bn254/test_vector_utils_test.go | 19 ++-- gkr/bw6-761/gkr.go | 4 +- gkr/bw6-761/gkr_test.go | 11 +- gkr/bw6-761/sumcheck.go | 8 +- gkr/bw6-761/sumcheck_test.go | 6 +- gkr/bw6-761/test_vector_utils_test.go | 19 ++-- gkr/gkrapi/example_test.go | 4 +- gkr/koalabear/gkr.go | 15 ++- gkr/koalabear/gkr_test.go | 12 +-- gkr/koalabear/sumcheck.go | 8 +- gkr/koalabear/sumcheck_test.go | 6 +- gkr/koalabear/test_vector_utils_test.go | 28 +++-- go.mod | 2 +- go.sum | 4 +- .../backend/template/gkr/blueprint.go.tmpl | 9 +- .../backend/template/gkr/gkr.go.tmpl | 17 +-- .../backend/template/gkr/gkr.test.go.tmpl | 16 +-- .../backend/template/gkr/sumcheck.go.tmpl | 8 +- .../template/gkr/sumcheck.test.go.tmpl | 9 +- .../gkr/sumcheck.test.vectors.gen.go.tmpl | 9 +- .../template/gkr/test_vector_utils.go.tmpl | 32 ++++-- .../gkr/bls12-377/blueprints/blueprint.go | 9 +- .../gkr/bls12-381/blueprints/blueprint.go | 9 +- internal/gkr/bn254/blueprints/blueprint.go | 9 +- internal/gkr/bw6-761/blueprints/blueprint.go | 9 +- internal/gkr/gkrcore/transcript.go | 100 +++++++++++++++--- internal/gkr/gkrcore/transcript_test.go | 12 ++- internal/gkr/rational/gkr.go | 4 +- internal/gkr/rational/sumcheck.go | 8 +- internal/gkr/rational/sumcheck_test.go | 6 +- .../gkr/rational/sumcheck_test_vector_gen.go | 6 +- internal/gkr/rational/test_vector_utils.go | 19 ++-- internal/smallfields/tinyfield/doc.go | 2 +- internal/smallfields/tinyfield/element.go | 2 +- .../smallfields/tinyfield/element_purego.go | 2 +- .../smallfields/tinyfield/element_test.go | 2 +- internal/smallfields/tinyfield/vector.go | 6 +- .../smallfields/tinyfield/vector_purego.go | 2 +- internal/smallfields/tinyfield/vector_test.go | 2 +- std/gkrapi/api_test.go | 49 ++------- 54 files changed, 350 insertions(+), 279 deletions(-) diff --git a/gkr/bls12-377/gkr.go b/gkr/bls12-377/gkr.go index dc96b79afa..1d5beb4130 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -78,7 +78,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript *transcript + transcript transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -98,7 +98,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[fr.Element](hasher), + transcript: gkrcore.NewNativeTranscript(hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), diff --git a/gkr/bls12-377/gkr_test.go b/gkr/bls12-377/gkr_test.go index 252717ca14..7a73473af6 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -29,11 +29,6 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_377.ScalarField())) -// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. -func newPoseidon2Hasher() fieldHasher { - return poseidon2.NewMerkleDamgardHasher() -} - func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -99,7 +94,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, poseidon2.NewHasher()) require.NoError(t, err) var flattened []fr.Element @@ -112,7 +107,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, poseidon2.NewHasher()) if err != nil { return nil, err } @@ -408,7 +403,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bls12-377/sumcheck.go b/gkr/bls12-377/sumcheck.go index ef416179e3..288a347247 100644 --- a/gkr/bls12-377/sumcheck.go +++ b/gkr/bls12-377/sumcheck.go @@ -18,8 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element] +// transcript is the Fiat-Shamir transcript over this field. +type transcript = gkrcore.Transcript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -46,7 +46,7 @@ type sumcheckProof struct { // sumcheckProve creates a non-interactive sumcheck proof. // The fold challenge is derived by the caller (proveLevel). // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. -func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { +func sumcheckProve(claims sumcheckClaims, t transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() @@ -66,7 +66,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t *transcript) error { +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t transcript) error { r := make([]fr.Element, claims.varsNum()) gJ := make(polynomial.Polynomial, degree+1) diff --git a/gkr/bls12-377/sumcheck_test.go b/gkr/bls12-377/sumcheck_test.go index 3d0f0e9c3f..8a2ab0567f 100644 --- a/gkr/bls12-377/sumcheck_test.go +++ b/gkr/bls12-377/sumcheck_test.go @@ -28,7 +28,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t := gkrcore.NewNativeTranscript(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -46,14 +46,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index 0565728ea7..575ebf5ae0 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -26,7 +26,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{state: startState, step: 0}, nil + return &messageCounter{startState: startState, state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -37,24 +37,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { // messageCounter is a stand-in hasher whose state depends only on the number of field elements // written to it, not on their values. type messageCounter struct { - state int64 - step int64 + startState int64 + state int64 + step int64 } -func (m *messageCounter) WriteField(e ...fr.Element) { +func (m *messageCounter) Reset() { m.state = m.startState } + +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } + +func (m *messageCounter) WriteElements(e ...fr.Element) { m.state += int64(len(e)) * m.step } func (m *messageCounter) PadBlock() {} -func (m *messageCounter) SumField() fr.Element { +func (m *messageCounter) SumDigest() fr.Element { var res fr.Element res.SetInt64(m.state + m.step) return res } +func (m *messageCounter) BlockLen() int { return 1 } + func newMessageCounter(startState, step int) fieldHasher { - return &messageCounter{state: int64(startState), step: int64(step)} + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } func newMessageCounterGenerator(startState, step int) func() fieldHasher { diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index 29ddae9872..4f1fc4418c 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -78,7 +78,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript *transcript + transcript transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -98,7 +98,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[fr.Element](hasher), + transcript: gkrcore.NewNativeTranscript(hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), diff --git a/gkr/bls12-381/gkr_test.go b/gkr/bls12-381/gkr_test.go index 8e68a30910..b71bc3d5d0 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -29,11 +29,6 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BLS12_381.ScalarField())) -// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. -func newPoseidon2Hasher() fieldHasher { - return poseidon2.NewMerkleDamgardHasher() -} - func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -99,7 +94,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, poseidon2.NewHasher()) require.NoError(t, err) var flattened []fr.Element @@ -112,7 +107,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, poseidon2.NewHasher()) if err != nil { return nil, err } @@ -408,7 +403,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bls12-381/sumcheck.go b/gkr/bls12-381/sumcheck.go index 156ad60fa0..0eee1c90fa 100644 --- a/gkr/bls12-381/sumcheck.go +++ b/gkr/bls12-381/sumcheck.go @@ -18,8 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element] +// transcript is the Fiat-Shamir transcript over this field. +type transcript = gkrcore.Transcript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -46,7 +46,7 @@ type sumcheckProof struct { // sumcheckProve creates a non-interactive sumcheck proof. // The fold challenge is derived by the caller (proveLevel). // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. -func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { +func sumcheckProve(claims sumcheckClaims, t transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() @@ -66,7 +66,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t *transcript) error { +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t transcript) error { r := make([]fr.Element, claims.varsNum()) gJ := make(polynomial.Polynomial, degree+1) diff --git a/gkr/bls12-381/sumcheck_test.go b/gkr/bls12-381/sumcheck_test.go index 557b03c1b3..0f663e48bc 100644 --- a/gkr/bls12-381/sumcheck_test.go +++ b/gkr/bls12-381/sumcheck_test.go @@ -28,7 +28,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t := gkrcore.NewNativeTranscript(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -46,14 +46,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index eef044f25b..4fd013cace 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -26,7 +26,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{state: startState, step: 0}, nil + return &messageCounter{startState: startState, state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -37,24 +37,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { // messageCounter is a stand-in hasher whose state depends only on the number of field elements // written to it, not on their values. type messageCounter struct { - state int64 - step int64 + startState int64 + state int64 + step int64 } -func (m *messageCounter) WriteField(e ...fr.Element) { +func (m *messageCounter) Reset() { m.state = m.startState } + +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } + +func (m *messageCounter) WriteElements(e ...fr.Element) { m.state += int64(len(e)) * m.step } func (m *messageCounter) PadBlock() {} -func (m *messageCounter) SumField() fr.Element { +func (m *messageCounter) SumDigest() fr.Element { var res fr.Element res.SetInt64(m.state + m.step) return res } +func (m *messageCounter) BlockLen() int { return 1 } + func newMessageCounter(startState, step int) fieldHasher { - return &messageCounter{state: int64(startState), step: int64(step)} + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } func newMessageCounterGenerator(startState, step int) func() fieldHasher { diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index 63a3d63c27..4cc69b4cd3 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -78,7 +78,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript *transcript + transcript transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -98,7 +98,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[fr.Element](hasher), + transcript: gkrcore.NewNativeTranscript(hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), diff --git a/gkr/bn254/gkr_test.go b/gkr/bn254/gkr_test.go index d58257e39e..0f8c493780 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -29,11 +29,6 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BN254.ScalarField())) -// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. -func newPoseidon2Hasher() fieldHasher { - return poseidon2.NewMerkleDamgardHasher() -} - func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -99,7 +94,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, poseidon2.NewHasher()) require.NoError(t, err) var flattened []fr.Element @@ -112,7 +107,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, poseidon2.NewHasher()) if err != nil { return nil, err } @@ -408,7 +403,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bn254/sumcheck.go b/gkr/bn254/sumcheck.go index 8584d65ce5..3f61bb68ee 100644 --- a/gkr/bn254/sumcheck.go +++ b/gkr/bn254/sumcheck.go @@ -18,8 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element] +// transcript is the Fiat-Shamir transcript over this field. +type transcript = gkrcore.Transcript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -46,7 +46,7 @@ type sumcheckProof struct { // sumcheckProve creates a non-interactive sumcheck proof. // The fold challenge is derived by the caller (proveLevel). // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. -func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { +func sumcheckProve(claims sumcheckClaims, t transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() @@ -66,7 +66,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t *transcript) error { +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t transcript) error { r := make([]fr.Element, claims.varsNum()) gJ := make(polynomial.Polynomial, degree+1) diff --git a/gkr/bn254/sumcheck_test.go b/gkr/bn254/sumcheck_test.go index cb046e930a..f4bc769139 100644 --- a/gkr/bn254/sumcheck_test.go +++ b/gkr/bn254/sumcheck_test.go @@ -28,7 +28,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t := gkrcore.NewNativeTranscript(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -46,14 +46,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 3599fc22cd..1ed3d7b6d1 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -26,7 +26,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{state: startState, step: 0}, nil + return &messageCounter{startState: startState, state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -37,24 +37,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { // messageCounter is a stand-in hasher whose state depends only on the number of field elements // written to it, not on their values. type messageCounter struct { - state int64 - step int64 + startState int64 + state int64 + step int64 } -func (m *messageCounter) WriteField(e ...fr.Element) { +func (m *messageCounter) Reset() { m.state = m.startState } + +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } + +func (m *messageCounter) WriteElements(e ...fr.Element) { m.state += int64(len(e)) * m.step } func (m *messageCounter) PadBlock() {} -func (m *messageCounter) SumField() fr.Element { +func (m *messageCounter) SumDigest() fr.Element { var res fr.Element res.SetInt64(m.state + m.step) return res } +func (m *messageCounter) BlockLen() int { return 1 } + func newMessageCounter(startState, step int) fieldHasher { - return &messageCounter{state: int64(startState), step: int64(step)} + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } func newMessageCounterGenerator(startState, step int) func() fieldHasher { diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index dca927d368..a0c80a4665 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -78,7 +78,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript *transcript + transcript transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -98,7 +98,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[fr.Element](hasher), + transcript: gkrcore.NewNativeTranscript(hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), diff --git a/gkr/bw6-761/gkr_test.go b/gkr/bw6-761/gkr_test.go index 21aa97af1d..e2895631ab 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -29,11 +29,6 @@ import ( var cache = gkrtesting.NewCache(gkr.PrimeField(ecc.BW6_761.ScalarField())) -// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. -func newPoseidon2Hasher() fieldHasher { - return poseidon2.NewMerkleDamgardHasher() -} - func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []fr.Element{four, three}) @@ -99,7 +94,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, poseidon2.NewHasher()) require.NoError(t, err) var flattened []fr.Element @@ -112,7 +107,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, poseidon2.NewHasher()) if err != nil { return nil, err } @@ -408,7 +403,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/bw6-761/sumcheck.go b/gkr/bw6-761/sumcheck.go index a4cd7d97d2..f74e2724d7 100644 --- a/gkr/bw6-761/sumcheck.go +++ b/gkr/bw6-761/sumcheck.go @@ -18,8 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[fr.Element] +// transcript is the Fiat-Shamir transcript over this field. +type transcript = gkrcore.Transcript[fr.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -46,7 +46,7 @@ type sumcheckProof struct { // sumcheckProve creates a non-interactive sumcheck proof. // The fold challenge is derived by the caller (proveLevel). // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. -func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { +func sumcheckProve(claims sumcheckClaims, t transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() @@ -66,7 +66,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t *transcript) error { +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum fr.Element, degree int, t transcript) error { r := make([]fr.Element, claims.varsNum()) gJ := make(polynomial.Polynomial, degree+1) diff --git a/gkr/bw6-761/sumcheck_test.go b/gkr/bw6-761/sumcheck_test.go index f235a5cdd9..a91592524a 100644 --- a/gkr/bw6-761/sumcheck_test.go +++ b/gkr/bw6-761/sumcheck_test.go @@ -28,7 +28,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t := gkrcore.NewNativeTranscript(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -46,14 +46,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[fr.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[fr.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index 7301fd67ad..4693139871 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -26,7 +26,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{state: startState, step: 0}, nil + return &messageCounter{startState: startState, state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -37,24 +37,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { // messageCounter is a stand-in hasher whose state depends only on the number of field elements // written to it, not on their values. type messageCounter struct { - state int64 - step int64 + startState int64 + state int64 + step int64 } -func (m *messageCounter) WriteField(e ...fr.Element) { +func (m *messageCounter) Reset() { m.state = m.startState } + +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } + +func (m *messageCounter) WriteElements(e ...fr.Element) { m.state += int64(len(e)) * m.step } func (m *messageCounter) PadBlock() {} -func (m *messageCounter) SumField() fr.Element { +func (m *messageCounter) SumDigest() fr.Element { var res fr.Element res.SetInt64(m.state + m.step) return res } +func (m *messageCounter) BlockLen() int { return 1 } + func newMessageCounter(startState, step int) fieldHasher { - return &messageCounter{state: int64(startState), step: int64(step)} + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } func newMessageCounterGenerator(startState, step int) func() fieldHasher { diff --git a/gkr/gkrapi/example_test.go b/gkr/gkrapi/example_test.go index 9338dff485..8843ddb16b 100644 --- a/gkr/gkrapi/example_test.go +++ b/gkr/gkrapi/example_test.go @@ -30,7 +30,7 @@ func Example() { // Prove and Verify must be given hashers seeded identically with a commitment to the circuit, // the schedule, the number of instances and the inputs and outputs. The example omits it. - proof, proverClaims, err := gkrbn254.Prove(circuit, schedule, assignment, mimc.NewFieldHasher()) + proof, proverClaims, err := gkrbn254.Prove(circuit, schedule, assignment, mimc.NewHasher()) assertNoError(err) assertNoError(proverClaims.Check(assignment)) @@ -41,7 +41,7 @@ func Example() { deserialized, err := gkrbn254.DeserializeProof(circuit, schedule, flattened) assertNoError(err) - verifierClaims, err := gkrbn254.Verify(circuit, schedule, 1, deserialized, mimc.NewFieldHasher()) + verifierClaims, err := gkrbn254.Verify(circuit, schedule, 1, deserialized, mimc.NewHasher()) assertNoError(err) assertNoError(verifierClaims.Check(assignment)) // every returned claim must be checked diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index 7acf66be38..950d6c2ccb 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -81,7 +81,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript *transcript + transcript transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -104,13 +104,13 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[extensions.E6, extensions.E6]) resources { +func newResources[D gkrcore.Digest[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[koalabear.Element, D]) resources { return resources{ outgoingEvalPoints: make([][][]extensions.E6, len(schedule)+1), nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[extensions.E6](hasher), + transcript: gkrcore.NewExtensionTranscript[extensions.E6](hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -196,7 +196,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[extensions.E6, extensions.E6]) (Proof, Claims, error) { +func Prove[D gkrcore.Digest[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[koalabear.Element, D]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -294,10 +294,9 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, // by writing to it a commitment to the circuit, the schedule, the number of instances and the input // and output columns, computed from values it trusts. -// Over the extension, wrap a base field hasher with gnark-crypto's -// NewBaseFieldHashAdapterE6; base field seed elements can be written to the -// base hasher directly. -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[extensions.E6, extensions.E6]) (Claims, error) { +// The hasher is a hasher of koalabear.Element, the prime subfield of extensions.E6, and the +// seed is made of koalabear.Element elements. +func Verify[D gkrcore.Digest[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[koalabear.Element, D]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/gkr/koalabear/gkr_test.go b/gkr/koalabear/gkr_test.go index ce3fa92ba8..ab1dc9aa2e 100644 --- a/gkr/koalabear/gkr_test.go +++ b/gkr/koalabear/gkr_test.go @@ -16,7 +16,6 @@ import ( "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" - extensionsHash "github.com/consensys/gnark-crypto/field/koalabear/extensions/hash" "github.com/consensys/gnark-crypto/field/koalabear/extensions/polynomial" poseidon2 "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" "github.com/consensys/gnark/constraint" @@ -30,11 +29,6 @@ import ( var cache = gkrtesting.NewCache(gkr.KoalaBearE6()) -// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. -func newPoseidon2Hasher() fieldHasher { - return extensionsHash.NewBaseFieldHashAdapterE6(poseidon2.NewMerkleDamgardHasher()) -} - func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []koalabear.Element{four, three}) @@ -100,7 +94,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, poseidon2.NewHasher()) require.NoError(t, err) var flattened []extensions.E6 @@ -113,7 +107,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, poseidon2.NewHasher()) if err != nil { return nil, err } @@ -420,7 +414,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/gkr/koalabear/sumcheck.go b/gkr/koalabear/sumcheck.go index ea5803ae14..0439058be1 100644 --- a/gkr/koalabear/sumcheck.go +++ b/gkr/koalabear/sumcheck.go @@ -20,8 +20,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[extensions.E6] +// transcript is the Fiat-Shamir transcript over this field. +type transcript = gkrcore.Transcript[extensions.E6] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -48,7 +48,7 @@ type sumcheckProof struct { // sumcheckProve creates a non-interactive sumcheck proof. // The fold challenge is derived by the caller (proveLevel). // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. -func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { +func sumcheckProve(claims sumcheckClaims, t transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.PolynomialE6, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() @@ -68,7 +68,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum extensions.E6, degree int, t *transcript) error { +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum extensions.E6, degree int, t transcript) error { r := make([]extensions.E6, claims.varsNum()) gJ := make(polynomial.PolynomialE6, degree+1) diff --git a/gkr/koalabear/sumcheck_test.go b/gkr/koalabear/sumcheck_test.go index 6df2fc6ac0..b95511a169 100644 --- a/gkr/koalabear/sumcheck_test.go +++ b/gkr/koalabear/sumcheck_test.go @@ -28,7 +28,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[extensions.E6](hashGenerator()) + t := gkrcore.NewExtensionTranscript[extensions.E6](hashGenerator()) proof := sumcheckProve(&claim, t) @@ -46,14 +46,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[extensions.E6](hashGenerator()) + t = gkrcore.NewExtensionTranscript[extensions.E6](hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[extensions.E6](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[extensions.E6](hashGenerator()) + t = gkrcore.NewExtensionTranscript[extensions.E6](hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index dea7a8cad6..38ba58d8a6 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -8,13 +8,14 @@ package gkrkoalabear import ( "fmt" - "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) // fieldHasher is the hasher the transcript of this field's GKR runs on. -type fieldHasher = hash.FieldHasher[extensions.E6, extensions.E6] +type fieldHasher = hash.FieldHasher[koalabear.Element, [poseidon2.BlockLen]koalabear.Element] // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what the @@ -26,7 +27,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{state: startState, step: 0}, nil + return &messageCounter{startState: startState, state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -37,24 +38,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { // messageCounter is a stand-in hasher whose state depends only on the number of field elements // written to it, not on their values. type messageCounter struct { - state int64 - step int64 + startState int64 + state int64 + step int64 } -func (m *messageCounter) WriteField(e ...extensions.E6) { +func (m *messageCounter) Reset() { m.state = m.startState } + +func (m *messageCounter) WriteElement(koalabear.Element) { m.state += m.step } + +func (m *messageCounter) WriteElements(e ...koalabear.Element) { m.state += int64(len(e)) * m.step } func (m *messageCounter) PadBlock() {} -func (m *messageCounter) SumField() extensions.E6 { - var res extensions.E6 - res.SetInt64(m.state + m.step) +func (m *messageCounter) SumDigest() [poseidon2.BlockLen]koalabear.Element { + var res [poseidon2.BlockLen]koalabear.Element + res[0].SetInt64(m.state + m.step) return res } +func (m *messageCounter) BlockLen() int { return 1 } + func newMessageCounter(startState, step int) fieldHasher { - return &messageCounter{state: int64(startState), step: int64(step)} + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } func newMessageCounterGenerator(startState, step int) func() fieldHasher { diff --git a/go.mod b/go.mod index 2d72fec0d9..2a6ada9dbe 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.22.1-0.20261018191837-1782a7d6eb90 + github.com/consensys/gnark-crypto v0.22.1-0.20261102214826-c1cb9b4629e6 github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 diff --git a/go.sum b/go.sum index 7b77b4742c..78f1387256 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.22.1-0.20261018191837-1782a7d6eb90 h1:PWLwzhmT1Cu1/Zyd66vv4PZOSJIewHr1ssYfyb8cwxA= -github.com/consensys/gnark-crypto v0.22.1-0.20261018191837-1782a7d6eb90/go.mod h1:HL8XflRG4cvau1PEG3CWBTD2kkzUzdlqZF6BBgy57F8= +github.com/consensys/gnark-crypto v0.22.1-0.20261102214826-c1cb9b4629e6 h1:MN8JNC1TXtDdjsyzwpnoj180DHbct9lIGg5G2g43owA= +github.com/consensys/gnark-crypto v0.22.1-0.20261102214826-c1cb9b4629e6/go.mod h1:+GBvEPnGIwDMfw6RJdxtImW8wHcBDaveR3s8URlDAc0= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= diff --git a/internal/generator/backend/template/gkr/blueprint.go.tmpl b/internal/generator/backend/template/gkr/blueprint.go.tmpl index 6a4ec3aa9f..abb3933d97 100644 --- a/internal/generator/backend/template/gkr/blueprint.go.tmpl +++ b/internal/generator/backend/template/gkr/blueprint.go.tmpl @@ -254,10 +254,9 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hashName := b.HashName + "_{{.FieldID}}" - hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) - if !ok { - return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + hsh, err := hash.NewFieldHasher[{{ .ElementType }}, {{ .ElementType }}](b.HashName + "_{{.FieldID}}") + if err != nil { + return err } // Read initial challenges from instruction calldata (parse dynamically, no metadata) @@ -270,7 +269,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to {{ .ElementType }} (both in Montgomery form) var challenge {{ .ElementType }} copy(challenge[:], val[:]) - hsh.WriteField(challenge) + hsh.WriteElement(challenge) } // Call the {{.FieldID}}-specific Prove function (assignments already WireAssignment type) diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index ace561eb81..6b195ab42c 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -1,4 +1,6 @@ {{ $gateEvaluator := "GateEvaluator" }}{{ if .Mixed }}{{ $gateEvaluator = "GateEvaluatorMixed" }}{{ end }} +{{ $newTranscript := "gkrcore.NewNativeTranscript" }}{{ if .Mixed }}{{ $newTranscript = printf "gkrcore.NewExtensionTranscript[%s]" .ElementType }}{{ end }} +{{ $digest := .ElementType }}{{ $typeParams := "" }}{{ if .Mixed }}{{ $digest = "D" }}{{ $typeParams = printf "[D gkrcore.Digest[%s]]" .BaseElementType }}{{ end }} import ( "errors" "fmt" @@ -86,7 +88,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript *transcript + transcript transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -111,13 +113,13 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) resources { +func newResources{{ $typeParams }}(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[{{ .BaseElementType }}, {{ $digest }}]) resources { return resources{ outgoingEvalPoints: make([][][]{{ .ElementType }}, len(schedule)+1), nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[{{ .ElementType }}](hasher), + transcript: {{ $newTranscript }}(hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -207,7 +209,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) (Proof, Claims, error) { +func Prove{{ $typeParams }}(c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[{{ .BaseElementType }}, {{ $digest }}]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -314,11 +316,10 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // by writing to it a commitment to the circuit, the schedule, the number of instances and the input // and output columns, computed from values it trusts. {{- if .Mixed }} -// Over the extension, wrap a base field hasher with gnark-crypto's -// NewBaseFieldHashAdapter{{ .ExtensionSuffix }}; base field seed elements can be written to the -// base hasher directly. +// The hasher is a hasher of {{ .BaseElementType }}, the prime subfield of {{ .ElementType }}, and the +// seed is made of {{ .BaseElementType }} elements. {{- end }} -func Verify(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}]) (Claims, error) { +func Verify{{ $typeParams }}(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[{{ .BaseElementType }}, {{ $digest }}]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl index bf6b2d1f73..6e86d4e3f1 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -10,7 +10,6 @@ import ( "github.com/consensys/gnark-crypto/ecc" {{- if .Mixed }} - extensionsHash "{{ .FieldPackagePath }}/hash" poseidon2 "{{ .BaseFieldPackagePath }}/poseidon2" {{- else }} "{{ .FieldPackagePath }}/poseidon2" @@ -31,15 +30,6 @@ import ( var cache = gkrtesting.NewCache({{ .FieldDescription }}) -// newPoseidon2Hasher returns a Poseidon2 hasher over the field's elements. -func newPoseidon2Hasher() fieldHasher { - {{- if .Mixed }} - return extensionsHash.NewBaseFieldHashAdapter{{ .ExtensionSuffix }}(poseidon2.NewMerkleDamgardHasher()) - {{- else }} - return poseidon2.NewMerkleDamgardHasher() - {{- end }} -} - func TestNoGateTwoInstances(t *testing.T) { // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case testNoGate(t, []{{ .BaseElementType }}{four, three}) @@ -105,7 +95,7 @@ func TestEndToEndPoseidon2(t *testing.T) { } // The output column is left nil: Prove computes it. - proof, proverClaims, err := Prove(circuit, schedule, assignment, newPoseidon2Hasher()) + proof, proverClaims, err := Prove(circuit, schedule, assignment, poseidon2.NewHasher()) require.NoError(t, err) var flattened []{{ .ElementType }} @@ -118,7 +108,7 @@ func TestEndToEndPoseidon2(t *testing.T) { if err != nil { return nil, err } - claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, newPoseidon2Hasher()) + claims, err := Verify(circuit, schedule, assignment.NumVars(), deserialized, poseidon2.NewHasher()) if err != nil { return nil, err } @@ -431,7 +421,7 @@ func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { //b.ResetTimer() fmt.Println("constructing proof") start = time.Now().UnixMicro() - _, _, err = Prove(c, schedule, assignment, newPoseidon2Hasher()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index 9eb7e8ba38..54dbfd50e6 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -18,8 +18,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[{{ .ElementType }}] +// transcript is the Fiat-Shamir transcript over this field. +type transcript = gkrcore.Transcript[{{ .ElementType }}] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -46,7 +46,7 @@ type sumcheckProof struct { // sumcheckProve creates a non-interactive sumcheck proof. // The fold challenge is derived by the caller (proveLevel). // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. -func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { +func sumcheckProve(claims sumcheckClaims, t transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial{{ .ExtensionSuffix }}, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() @@ -66,7 +66,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum {{ .ElementType }}, degree int, t *transcript) error { +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum {{ .ElementType }}, degree int, t transcript) error { r := make([]{{ .ElementType }}, claims.varsNum()) gJ := make(polynomial.Polynomial{{ .ExtensionSuffix }}, degree+1) diff --git a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl index 1581dcc363..00a9197d23 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl @@ -1,7 +1,8 @@ +{{ $newTranscript := "gkrcore.NewNativeTranscript" }}{{ if .Mixed }}{{ $newTranscript = printf "gkrcore.NewExtensionTranscript[%s]" .ElementType }}{{ end }} import ( + "github.com/consensys/gnark/internal/gkr/gkrcore" "fmt" "{{ .FieldPackagePath }}/polynomial" - "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" {{ if not .GenerateTestVectors}} @@ -19,7 +20,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) + t := {{ $newTranscript }}(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -37,14 +38,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) + t = {{ $newTranscript }}(hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[{{ .ElementType }}](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) + t = {{ $newTranscript }}(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl index 2c945a9a12..93ba2ece20 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -1,7 +1,8 @@ +{{ $newTranscript := "gkrcore.NewNativeTranscript" }}{{ if .Mixed }}{{ $newTranscript = printf "gkrcore.NewExtensionTranscript[%s]" .ElementType }}{{ end }} import ( + "github.com/consensys/gnark/internal/gkr/gkrcore" "encoding/json" "fmt" - "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/rational" "github.com/consensys/gnark/internal/rational/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -30,7 +31,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } claim := singleMultilinClaim{poly} - t := gkrcore.NewHashTranscript[rational.Element](hsh) + t := {{ $newTranscript }}(hsh) proof := sumcheckProve(&claim, t) testCaseInfo.Proof = sumcheckToPrintableProof(proof) @@ -48,7 +49,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[rational.Element](hsh) + t = {{ $newTranscript }}(hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err != nil { return fmt.Errorf("proof rejected: %v", err) } @@ -57,7 +58,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[rational.Element](hsh) + t = {{ $newTranscript }}(hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 06dcab738e..85c3786a56 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -1,12 +1,17 @@ +{{ $digest := .ElementType }}{{ if .Mixed }}{{ $digest = printf "[poseidon2.BlockLen]%s" .BaseElementType }}{{ end }} import ( "fmt" "{{.FieldPackagePath}}" + {{- if .Mixed }} + "{{ .BaseFieldPackagePath }}" + "{{ .BaseFieldPackagePath }}/poseidon2" + {{- end }} "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) // fieldHasher is the hasher the transcript of this field's GKR runs on. -type fieldHasher = hash.FieldHasher[{{ .ElementType }}, {{ .ElementType }}] +type fieldHasher = hash.FieldHasher[{{ .BaseElementType }}, {{ $digest }}] // hashFromDescription returns the hash a test vector names. The only type is "const": a // messageCounter with step 0, so every challenge equals the given value regardless of what the @@ -18,7 +23,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{state: startState, step: 0}, nil + return &messageCounter{startState: startState, state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -29,24 +34,35 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { // messageCounter is a stand-in hasher whose state depends only on the number of field elements // written to it, not on their values. type messageCounter struct { - state int64 - step int64 + startState int64 + state int64 + step int64 } -func (m *messageCounter) WriteField(e ...{{ .ElementType }}) { +func (m *messageCounter) Reset() { m.state = m.startState } + +func (m *messageCounter) WriteElement({{ .BaseElementType }}) { m.state += m.step } + +func (m *messageCounter) WriteElements(e ...{{ .BaseElementType }}) { m.state += int64(len(e)) * m.step } func (m *messageCounter) PadBlock() {} -func (m *messageCounter) SumField() {{ .ElementType }} { - var res {{ .ElementType }} +func (m *messageCounter) SumDigest() {{ $digest }} { + var res {{ $digest }} + {{- if .Mixed }} + res[0].SetInt64(m.state + m.step) + {{- else }} res.SetInt64(m.state + m.step) + {{- end }} return res } +func (m *messageCounter) BlockLen() int { return 1 } + func newMessageCounter(startState, step int) fieldHasher { - return &messageCounter{state: int64(startState), step: int64(step)} + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } func newMessageCounterGenerator(startState, step int) func() fieldHasher { diff --git a/internal/gkr/bls12-377/blueprints/blueprint.go b/internal/gkr/bls12-377/blueprints/blueprint.go index 63c98f6cbe..cc3ffc81b8 100644 --- a/internal/gkr/bls12-377/blueprints/blueprint.go +++ b/internal/gkr/bls12-377/blueprints/blueprint.go @@ -259,10 +259,9 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hashName := b.HashName + "_BLS12_377" - hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) - if !ok { - return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BLS12_377") + if err != nil { + return err } // Read initial challenges from instruction calldata (parse dynamically, no metadata) @@ -275,7 +274,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - hsh.WriteField(challenge) + hsh.WriteElement(challenge) } // Call the BLS12_377-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bls12-381/blueprints/blueprint.go b/internal/gkr/bls12-381/blueprints/blueprint.go index 8c8dc604e7..1103861606 100644 --- a/internal/gkr/bls12-381/blueprints/blueprint.go +++ b/internal/gkr/bls12-381/blueprints/blueprint.go @@ -259,10 +259,9 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hashName := b.HashName + "_BLS12_381" - hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) - if !ok { - return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BLS12_381") + if err != nil { + return err } // Read initial challenges from instruction calldata (parse dynamically, no metadata) @@ -275,7 +274,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - hsh.WriteField(challenge) + hsh.WriteElement(challenge) } // Call the BLS12_381-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bn254/blueprints/blueprint.go b/internal/gkr/bn254/blueprints/blueprint.go index 58b9790cf6..8666d85c87 100644 --- a/internal/gkr/bn254/blueprints/blueprint.go +++ b/internal/gkr/bn254/blueprints/blueprint.go @@ -259,10 +259,9 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hashName := b.HashName + "_BN254" - hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) - if !ok { - return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BN254") + if err != nil { + return err } // Read initial challenges from instruction calldata (parse dynamically, no metadata) @@ -275,7 +274,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - hsh.WriteField(challenge) + hsh.WriteElement(challenge) } // Call the BN254-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bw6-761/blueprints/blueprint.go b/internal/gkr/bw6-761/blueprints/blueprint.go index 431c35e2eb..517e94b365 100644 --- a/internal/gkr/bw6-761/blueprints/blueprint.go +++ b/internal/gkr/bw6-761/blueprints/blueprint.go @@ -259,10 +259,9 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hashName := b.HashName + "_BW6_761" - hsh, ok := hash.NewHash(hashName).(hash.FieldHasher[fr.Element, fr.Element]) - if !ok { - return fmt.Errorf("hash %s does not implement hash.FieldHasher", hashName) + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BW6_761") + if err != nil { + return err } // Read initial challenges from instruction calldata (parse dynamically, no metadata) @@ -275,7 +274,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra // Copy directly from constraint.U64 to fr.Element (both in Montgomery form) var challenge fr.Element copy(challenge[:], val[:]) - hsh.WriteField(challenge) + hsh.WriteElement(challenge) } // Call the BW6_761-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/gkrcore/transcript.go b/internal/gkr/gkrcore/transcript.go index 449a1637f3..d59a3c019d 100644 --- a/internal/gkr/gkrcore/transcript.go +++ b/internal/gkr/gkrcore/transcript.go @@ -2,38 +2,114 @@ package gkrcore import "github.com/consensys/gnark-crypto/hash" -// HashTranscript is a Fiat-Shamir transcript backed by a running field hasher. Absorb writes field -// elements; Squeeze derives challenges from them. The hasher is never reset — all previous data is -// implicitly part of future challenges. -type HashTranscript[E any] struct { +// Transcript is a Fiat-Shamir transcript over the elements of E. Absorb writes field elements; +// Squeeze derives challenges from them. +type Transcript[E any] interface { + Absorb(elements ...E) + Squeeze(elements ...E) E +} + +// NativeTranscript is a Fiat-Shamir transcript backed by a running hasher of the field's elements. +// The hasher is never reset — all previous data is implicitly part of future challenges. +type NativeTranscript[E any] struct { h hash.FieldHasher[E, E] absorbed bool // whether Absorb was called since the last Squeeze } -// NewHashTranscript returns a transcript backed by h. -func NewHashTranscript[E any](h hash.FieldHasher[E, E]) *HashTranscript[E] { - return &HashTranscript[E]{h: h} +// NewNativeTranscript returns a transcript backed by h. +func NewNativeTranscript[E any](h hash.FieldHasher[E, E]) *NativeTranscript[E] { + return &NativeTranscript[E]{h: h} +} + +// Absorb writes field elements to the transcript, to be part of the next squeeze. +func (t *NativeTranscript[E]) Absorb(elements ...E) { + if len(elements) == 0 { + return + } + t.h.WriteElements(elements...) + t.absorbed = true +} + +// Squeeze absorbs elements as Absorb would, then derives a challenge from the hasher's state. +// If nothing was absorbed since the last squeeze, the zero element is written first to advance +// the state and prevent repeated values. +func (t *NativeTranscript[E]) Squeeze(elements ...E) E { + t.Absorb(elements...) + if !t.absorbed { + var zero E + t.h.WriteElements(zero) + } + t.absorbed = false + t.h.PadBlock() + return t.h.SumDigest() +} + +// Digest constrains the digest types of the hashers of gnark-crypto's small fields. +type Digest[F any] interface { + ~[4]F | ~[6]F | ~[8]F | ~[12]F +} + +// ExtensionTranscript is a Fiat-Shamir transcript over an extension E of a field F, backed by a +// running hasher of F. An element is written as its coordinates, and a challenge is made of the +// first coordinates of the hasher's digest. Absorb and Squeeze are otherwise those of +// NativeTranscript. +type ExtensionTranscript[E interface{ AppendBase([]F) []F }, PE interface { + *E + SetBase([]F) *E +}, F any, D Digest[F]] struct { + h hash.FieldHasher[F, D] + degree int // the number of coordinates of an element of E + buf []F // the coordinates being written or read + absorbed bool // whether Absorb was called since the last Squeeze +} + +// NewExtensionTranscript returns a transcript backed by h. It panics if a digest of h has fewer +// elements than E has coordinates. +func NewExtensionTranscript[E interface{ AppendBase([]F) []F }, PE interface { + *E + SetBase([]F) *E +}, F any, D Digest[F]](h hash.FieldHasher[F, D]) *ExtensionTranscript[E, PE, F, D] { + var zero E + var d D + degree := len(zero.AppendBase(nil)) + if len(d) < degree { + panic("the digest is shorter than an element") + } + return &ExtensionTranscript[E, PE, F, D]{h: h, degree: degree, buf: make([]F, 0, degree)} +} + +func (t *ExtensionTranscript[E, PE, F, D]) write(e E) { + t.buf = e.AppendBase(t.buf[:0]) + t.h.WriteElements(t.buf...) } // Absorb writes field elements to the transcript, to be part of the next squeeze. -func (t *HashTranscript[E]) Absorb(elements ...E) { +func (t *ExtensionTranscript[E, PE, F, D]) Absorb(elements ...E) { if len(elements) == 0 { return } - t.h.WriteField(elements...) + for _, e := range elements { + t.write(e) + } t.absorbed = true } // Squeeze absorbs elements as Absorb would, then derives a challenge from the hasher's state. // If nothing was absorbed since the last squeeze, the zero element is written first to advance // the state and prevent repeated values. -func (t *HashTranscript[E]) Squeeze(elements ...E) E { +func (t *ExtensionTranscript[E, PE, F, D]) Squeeze(elements ...E) E { t.Absorb(elements...) if !t.absorbed { var zero E - t.h.WriteField(zero) + t.write(zero) } t.absorbed = false t.h.PadBlock() - return t.h.SumField() + d := t.h.SumDigest() + t.buf = t.buf[:t.degree] + for i := range t.buf { + t.buf[i] = d[i] + } + var res E + return *PE(&res).SetBase(t.buf) } diff --git a/internal/gkr/gkrcore/transcript_test.go b/internal/gkr/gkrcore/transcript_test.go index ad054a8b13..41a3a7d095 100644 --- a/internal/gkr/gkrcore/transcript_test.go +++ b/internal/gkr/gkrcore/transcript_test.go @@ -5,14 +5,18 @@ import ( "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" - extensionsHash "github.com/consensys/gnark-crypto/field/koalabear/extensions/hash" "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" "github.com/stretchr/testify/assert" ) -// TestHashTranscriptBindsWholeElement checks that every coordinate of an absorbed extension +var ( + _ Transcript[koalabear.Element] = (*NativeTranscript[koalabear.Element])(nil) + _ Transcript[extensions.E6] = (*ExtensionTranscript[extensions.E6, *extensions.E6, koalabear.Element, [poseidon2.BlockLen]koalabear.Element])(nil) +) + +// TestExtensionTranscriptBindsWholeElement checks that every coordinate of an absorbed extension // element affects the next challenge. -func TestHashTranscriptBindsWholeElement(t *testing.T) { +func TestExtensionTranscriptBindsWholeElement(t *testing.T) { var element extensions.E6 element.MustSetRandom() @@ -24,7 +28,7 @@ func TestHashTranscriptBindsWholeElement(t *testing.T) { } challenge := func(e extensions.E6) extensions.E6 { - transcript := NewHashTranscript[extensions.E6](extensionsHash.NewBaseFieldHashAdapterE6(poseidon2.NewMerkleDamgardHasher())) + transcript := NewExtensionTranscript[extensions.E6](poseidon2.NewHasher()) transcript.Absorb(e) return transcript.Squeeze() } diff --git a/internal/gkr/rational/gkr.go b/internal/gkr/rational/gkr.go index f899136e86..87c45c9790 100644 --- a/internal/gkr/rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -77,7 +77,7 @@ type resources struct { workers *utils.WorkerPool circuit Circuit schedule constraint.GkrProvingSchedule - transcript *transcript + transcript transcript claimValueIndices [][]int // claimValueIndices[wI][claimI]: index of w's claimI-th claimed value in its source level's finalEvalProof claims Claims consolidated []bool // the wires of schedule[0], indexed by wire @@ -97,7 +97,7 @@ func newResources(c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, nbVars: nbVars, circuit: c, schedule: schedule, - transcript: gkrcore.NewHashTranscript[rational.Element](hasher), + transcript: gkrcore.NewNativeTranscript(hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), diff --git a/internal/gkr/rational/sumcheck.go b/internal/gkr/rational/sumcheck.go index 8921735444..37ca8396d0 100644 --- a/internal/gkr/rational/sumcheck.go +++ b/internal/gkr/rational/sumcheck.go @@ -17,8 +17,8 @@ import ( // This does not make use of parallelism and represents polynomials as lists of coefficients. -// transcript specializes gkrcore's generic hash transcript to this field. -type transcript = gkrcore.HashTranscript[rational.Element] +// transcript is the Fiat-Shamir transcript over this field. +type transcript = gkrcore.Transcript[rational.Element] // sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. // Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) @@ -45,7 +45,7 @@ type sumcheckProof struct { // sumcheckProve creates a non-interactive sumcheck proof. // The fold challenge is derived by the caller (proveLevel). // Pattern: roundPolynomial, [roundFold, roundPolynomial]*, proveFinalEval. -func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { +func sumcheckProve(claims sumcheckClaims, t transcript) sumcheckProof { varsNum := claims.varsNum() proof := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() @@ -65,7 +65,7 @@ func sumcheckProve(claims sumcheckClaims, t *transcript) sumcheckProof { // sumcheckVerify verifies a non-interactive sumcheck proof. // The fold challenge is derived by the caller (verifyLevel). // claimedSum is the expected sum; degree is the polynomial's degree in each variable. -func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum rational.Element, degree int, t *transcript) error { +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, claimedSum rational.Element, degree int, t transcript) error { r := make([]rational.Element, claims.varsNum()) gJ := make(polynomial.Polynomial, degree+1) diff --git a/internal/gkr/rational/sumcheck_test.go b/internal/gkr/rational/sumcheck_test.go index 677dda5308..892913b36f 100644 --- a/internal/gkr/rational/sumcheck_test.go +++ b/internal/gkr/rational/sumcheck_test.go @@ -25,7 +25,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[rational.Element](hashGenerator()) + t := gkrcore.NewNativeTranscript(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -43,14 +43,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[rational.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[rational.Element](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = gkrcore.NewHashTranscript[rational.Element](hashGenerator()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/gkr/rational/sumcheck_test_vector_gen.go b/internal/gkr/rational/sumcheck_test_vector_gen.go index 6d1f687338..b8de5093d2 100644 --- a/internal/gkr/rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/rational/sumcheck_test_vector_gen.go @@ -38,7 +38,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } claim := singleMultilinClaim{poly} - t := gkrcore.NewHashTranscript[rational.Element](hsh) + t := gkrcore.NewNativeTranscript(hsh) proof := sumcheckProve(&claim, t) testCaseInfo.Proof = sumcheckToPrintableProof(proof) @@ -56,7 +56,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[rational.Element](hsh) + t = gkrcore.NewNativeTranscript(hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err != nil { return fmt.Errorf("proof rejected: %v", err) } @@ -65,7 +65,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = gkrcore.NewHashTranscript[rational.Element](hsh) + t = gkrcore.NewNativeTranscript(hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/gkr/rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go index 4a8e32b3ce..8b5ff68a0e 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -26,7 +26,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { switch _type { case "const": startState := int64(d["val"].(float64)) - return &messageCounter{state: startState, step: 0}, nil + return &messageCounter{startState: startState, state: startState, step: 0}, nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -37,24 +37,31 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { // messageCounter is a stand-in hasher whose state depends only on the number of field elements // written to it, not on their values. type messageCounter struct { - state int64 - step int64 + startState int64 + state int64 + step int64 } -func (m *messageCounter) WriteField(e ...rational.Element) { +func (m *messageCounter) Reset() { m.state = m.startState } + +func (m *messageCounter) WriteElement(rational.Element) { m.state += m.step } + +func (m *messageCounter) WriteElements(e ...rational.Element) { m.state += int64(len(e)) * m.step } func (m *messageCounter) PadBlock() {} -func (m *messageCounter) SumField() rational.Element { +func (m *messageCounter) SumDigest() rational.Element { var res rational.Element res.SetInt64(m.state + m.step) return res } +func (m *messageCounter) BlockLen() int { return 1 } + func newMessageCounter(startState, step int) fieldHasher { - return &messageCounter{state: int64(startState), step: int64(step)} + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } func newMessageCounterGenerator(startState, step int) func() fieldHasher { diff --git a/internal/smallfields/tinyfield/doc.go b/internal/smallfields/tinyfield/doc.go index 59490a05b9..1297e748d3 100644 --- a/internal/smallfields/tinyfield/doc.go +++ b/internal/smallfields/tinyfield/doc.go @@ -1,4 +1,4 @@ -// Copyright 2020-2026 Consensys Software Inc. +// Copyright 2020-2026 Consensys Incorporated // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT diff --git a/internal/smallfields/tinyfield/element.go b/internal/smallfields/tinyfield/element.go index e21e81a627..00887287ac 100644 --- a/internal/smallfields/tinyfield/element.go +++ b/internal/smallfields/tinyfield/element.go @@ -1,4 +1,4 @@ -// Copyright 2020-2026 Consensys Software Inc. +// Copyright 2020-2026 Consensys Incorporated // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT diff --git a/internal/smallfields/tinyfield/element_purego.go b/internal/smallfields/tinyfield/element_purego.go index c3b6b6f460..09bb045517 100644 --- a/internal/smallfields/tinyfield/element_purego.go +++ b/internal/smallfields/tinyfield/element_purego.go @@ -1,4 +1,4 @@ -// Copyright 2020-2026 Consensys Software Inc. +// Copyright 2020-2026 Consensys Incorporated // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT diff --git a/internal/smallfields/tinyfield/element_test.go b/internal/smallfields/tinyfield/element_test.go index ac777e36cc..0cd80efc9b 100644 --- a/internal/smallfields/tinyfield/element_test.go +++ b/internal/smallfields/tinyfield/element_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2026 Consensys Software Inc. +// Copyright 2020-2026 Consensys Incorporated // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT diff --git a/internal/smallfields/tinyfield/vector.go b/internal/smallfields/tinyfield/vector.go index d9e2e29da0..433c2adc89 100644 --- a/internal/smallfields/tinyfield/vector.go +++ b/internal/smallfields/tinyfield/vector.go @@ -1,4 +1,4 @@ -// Copyright 2020-2026 Consensys Software Inc. +// Copyright 2020-2026 Consensys Incorporated // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT @@ -313,8 +313,8 @@ func (vector Vector) Exp(a Vector, k int64) { exp = -k // if k == math.MinInt64, -k overflows, but uint64(-k) is correct } else if N > 0 { // ensure that vector and a are not the same slice; else we need to copy a into base - v0 := &vector[0] // #nosec G602 we check that N > 0 above - a0 := &a[0] // #nosec G602 we check that N > 0 above + v0 := &vector[0] + a0 := &a[0] if v0 == a0 { base = make(Vector, N) copy(base, a) diff --git a/internal/smallfields/tinyfield/vector_purego.go b/internal/smallfields/tinyfield/vector_purego.go index b9ef324619..57b51c58ec 100644 --- a/internal/smallfields/tinyfield/vector_purego.go +++ b/internal/smallfields/tinyfield/vector_purego.go @@ -1,4 +1,4 @@ -// Copyright 2020-2026 Consensys Software Inc. +// Copyright 2020-2026 Consensys Incorporated // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT diff --git a/internal/smallfields/tinyfield/vector_test.go b/internal/smallfields/tinyfield/vector_test.go index 656d550fa6..f148ee6679 100644 --- a/internal/smallfields/tinyfield/vector_test.go +++ b/internal/smallfields/tinyfield/vector_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2026 Consensys Software Inc. +// Copyright 2020-2026 Consensys Incorporated // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT diff --git a/std/gkrapi/api_test.go b/std/gkrapi/api_test.go index c1a27d5d77..9794c00a84 100644 --- a/std/gkrapi/api_test.go +++ b/std/gkrapi/api_test.go @@ -2,10 +2,8 @@ package gkrapi import ( "fmt" - "hash" "math/big" "math/rand" - "slices" "strconv" "strings" "testing" @@ -570,59 +568,32 @@ func (m MiMCCipherGate) Degree() int { return 7 } -type constBytesPseudoHash []byte +// constFieldPseudoHash is a constant "hash" over the elements of F. +type constFieldPseudoHash[F any] struct{ digest F } -func (c constBytesPseudoHash) Write(p []byte) (n int, err error) { - return len(p), nil -} - -func (c constBytesPseudoHash) Sum([]byte) []byte { - return slices.Clone(c) -} +func (h constFieldPseudoHash[F]) Reset() {} -func (c constBytesPseudoHash) Reset() { -} +func (h constFieldPseudoHash[F]) WriteElement(F) {} -func (c constBytesPseudoHash) Size() int { - return len(c) -} - -func (c constBytesPseudoHash) BlockSize() int { - return len(c) -} - -func newConstBytesPseudoHash(c int64, mod *big.Int) constBytesPseudoHash { - i := big.NewInt(c) - i.Mod(i, mod) - b := make([]byte, len(mod.Bytes())) - i.FillBytes(b) - return b -} - -// constFieldPseudoHash is a constant "hash" over the elements of F: it is both a hash.Hash, as -// RegisterCustomHash requires, and a gnark-crypto hash.FieldHasher, as the GKR blueprints require. -type constFieldPseudoHash[F any] struct { - constBytesPseudoHash - digest F -} - -func (h constFieldPseudoHash[F]) WriteField(...F) {} +func (h constFieldPseudoHash[F]) WriteElements(...F) {} func (h constFieldPseudoHash[F]) PadBlock() {} -func (h constFieldPseudoHash[F]) SumField() F { +func (h constFieldPseudoHash[F]) SumDigest() F { return h.digest } +func (h constFieldPseudoHash[F]) BlockLen() int { return 1 } + // registerConstPseudoHash registers the constant hash v, over the scalar field of curve, as // name_CURVE. func registerConstPseudoHash[F any, PF interface { *F SetBigInt(*big.Int) *F }](name string, v int64, curve ecc.ID) { - h := constFieldPseudoHash[F]{constBytesPseudoHash: newConstBytesPseudoHash(v, curve.ScalarField())} + var h constFieldPseudoHash[F] PF(&h.digest).SetBigInt(big.NewInt(v)) - gcHash.RegisterCustomHash(name+"_"+strings.ToUpper(curve.String()), func() hash.Hash { + gcHash.RegisterCustomFieldHasher(name+"_"+strings.ToUpper(curve.String()), func() gcHash.FieldHasher[F, F] { return h }) } From f6603cd12bbc3ccfd9602ba730462742a9bbbe4e Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 3 Nov 2026 00:40:49 -0600 Subject: [PATCH 35/37] deslopify Signed-off-by: Arya Tabaie --- gkr/bls12-377/test_vector_utils_test.go | 3 +-- gkr/bls12-381/test_vector_utils_test.go | 3 +-- gkr/bn254/test_vector_utils_test.go | 3 +-- gkr/bw6-761/test_vector_utils_test.go | 3 +-- gkr/koalabear/gkr.go | 8 +++----- gkr/koalabear/test_vector_utils_test.go | 3 +-- internal/generator/backend/main.go | 9 +++++++++ internal/generator/backend/template/gkr/gkr.go.tmpl | 9 ++------- .../generator/backend/template/gkr/sumcheck.test.go.tmpl | 9 ++++----- .../template/gkr/sumcheck.test.vectors.gen.go.tmpl | 9 ++++----- .../backend/template/gkr/test_vector_utils.go.tmpl | 3 +-- internal/gkr/gkrcore/transcript.go | 9 ++------- internal/gkr/gkrcore/transcript_test.go | 5 ----- internal/gkr/rational/test_vector_utils.go | 3 +-- 14 files changed, 31 insertions(+), 48 deletions(-) diff --git a/gkr/bls12-377/test_vector_utils_test.go b/gkr/bls12-377/test_vector_utils_test.go index 575ebf5ae0..ce001bf901 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -25,8 +25,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": - startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, state: startState, step: 0}, nil + return newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } diff --git a/gkr/bls12-381/test_vector_utils_test.go b/gkr/bls12-381/test_vector_utils_test.go index 4fd013cace..eccce6472f 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -25,8 +25,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": - startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, state: startState, step: 0}, nil + return newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } diff --git a/gkr/bn254/test_vector_utils_test.go b/gkr/bn254/test_vector_utils_test.go index 1ed3d7b6d1..46cc7fdbec 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -25,8 +25,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": - startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, state: startState, step: 0}, nil + return newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } diff --git a/gkr/bw6-761/test_vector_utils_test.go b/gkr/bw6-761/test_vector_utils_test.go index 4693139871..2bf9909a6a 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -25,8 +25,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": - startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, state: startState, step: 0}, nil + return newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } diff --git a/gkr/koalabear/gkr.go b/gkr/koalabear/gkr.go index 950d6c2ccb..3450c4a5bd 100644 --- a/gkr/koalabear/gkr.go +++ b/gkr/koalabear/gkr.go @@ -104,7 +104,7 @@ func identityGate() gkrcore.SerializableGate { // newResources builds the resources shared by Prove and Verify. It takes no assignment and // creates no pools; Prove sets those up separately afterward, since Verify needs neither. -func newResources[D gkrcore.Digest[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[koalabear.Element, D]) resources { +func newResources[D hash.ArrayBlock[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, nbVars int, hasher hash.FieldHasher[koalabear.Element, D]) resources { return resources{ outgoingEvalPoints: make([][][]extensions.E6, len(schedule)+1), nbVars: nbVars, @@ -196,7 +196,7 @@ func (r *resources) proveLevel(levelI int) sumcheckProof { // inputs and outputs; the caller must check them. The claim values returned to the caller, the // output evaluations among them, are not bound into the transcript. hasher must be seeded as // Verify requires. -func Prove[D gkrcore.Digest[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[koalabear.Element, D]) (Proof, Claims, error) { +func Prove[D hash.ArrayBlock[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, assignment WireAssignment, hasher hash.FieldHasher[koalabear.Element, D]) (Proof, Claims, error) { nbInstances := assignment.NumInstances() nbVars := assignment.NumVars() if nbVars == 0 { @@ -294,9 +294,7 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, // by writing to it a commitment to the circuit, the schedule, the number of instances and the input // and output columns, computed from values it trusts. -// The hasher is a hasher of koalabear.Element, the prime subfield of extensions.E6, and the -// seed is made of koalabear.Element elements. -func Verify[D gkrcore.Digest[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[koalabear.Element, D]) (Claims, error) { +func Verify[D hash.ArrayBlock[koalabear.Element]](c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[koalabear.Element, D]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") } diff --git a/gkr/koalabear/test_vector_utils_test.go b/gkr/koalabear/test_vector_utils_test.go index 38ba58d8a6..b46e4b5769 100644 --- a/gkr/koalabear/test_vector_utils_test.go +++ b/gkr/koalabear/test_vector_utils_test.go @@ -26,8 +26,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": - startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, state: startState, step: 0}, nil + return newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index 5f37fc2471..83e0e504ba 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -441,6 +441,15 @@ func (c gkrConfig) BasePolynomial() string { return "polynomial" } +// NewTranscript is the constructor of the field's transcript over a hasher: gkrcore.NewNativeTranscript +// for a prime field, gkrcore.NewExtensionTranscript instantiated at ElementType for an extension. +func (c gkrConfig) NewTranscript() string { + if c.Mixed() { + return "gkrcore.NewExtensionTranscript[" + c.ElementType() + "]" + } + return "gkrcore.NewNativeTranscript" +} + // PackageName is the name of the generated packages: "gkr" followed by GkrPackageName with its // hyphens removed, e.g. gkrbls12377. The name gkr alone clashes with github.com/consensys/gnark/gkr, // and GkrPackageName alone with gnark-crypto's packages of the same name. diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl index 6b195ab42c..98389c73b6 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -1,6 +1,5 @@ {{ $gateEvaluator := "GateEvaluator" }}{{ if .Mixed }}{{ $gateEvaluator = "GateEvaluatorMixed" }}{{ end }} -{{ $newTranscript := "gkrcore.NewNativeTranscript" }}{{ if .Mixed }}{{ $newTranscript = printf "gkrcore.NewExtensionTranscript[%s]" .ElementType }}{{ end }} -{{ $digest := .ElementType }}{{ $typeParams := "" }}{{ if .Mixed }}{{ $digest = "D" }}{{ $typeParams = printf "[D gkrcore.Digest[%s]]" .BaseElementType }}{{ end }} +{{ $digest := .ElementType }}{{ $typeParams := "" }}{{ if .Mixed }}{{ $digest = "D" }}{{ $typeParams = printf "[D hash.ArrayBlock[%s]]" .BaseElementType }}{{ end }} import ( "errors" "fmt" @@ -119,7 +118,7 @@ func newResources{{ $typeParams }}(c Circuit, schedule constraint.GkrProvingSche nbVars: nbVars, circuit: c, schedule: schedule, - transcript: {{ $newTranscript }}(hasher), + transcript: {{ .NewTranscript }}(hasher), claimValueIndices: c.ClaimValueIndices(schedule), claims: make(Claims), consolidated: c.LevelWires(schedule[0]), @@ -315,10 +314,6 @@ func (r *resources) verifyLevel(levelI int, proof Proof) error { // The transcript starts empty, so for soundness the verifier must seed hasher, as the prover did, // by writing to it a commitment to the circuit, the schedule, the number of instances and the input // and output columns, computed from values it trusts. -{{- if .Mixed }} -// The hasher is a hasher of {{ .BaseElementType }}, the prime subfield of {{ .ElementType }}, and the -// seed is made of {{ .BaseElementType }} elements. -{{- end }} func Verify{{ $typeParams }}(c Circuit, schedule constraint.GkrProvingSchedule, logNbInstances int, proof Proof, hasher hash.FieldHasher[{{ .BaseElementType }}, {{ $digest }}]) (Claims, error) { if logNbInstances < 1 { return nil, errors.New("number of variables must be positive") diff --git a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl index 00a9197d23..e2d6fb2826 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl @@ -1,8 +1,7 @@ -{{ $newTranscript := "gkrcore.NewNativeTranscript" }}{{ if .Mixed }}{{ $newTranscript = printf "gkrcore.NewExtensionTranscript[%s]" .ElementType }}{{ end }} import ( - "github.com/consensys/gnark/internal/gkr/gkrcore" "fmt" "{{ .FieldPackagePath }}/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/gkr/gkrtesting" "github.com/stretchr/testify/assert" {{ if not .GenerateTestVectors}} @@ -20,7 +19,7 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } claim := singleMultilinClaim{g: poly.Clone()} - t := {{ $newTranscript }}(hashGenerator()) + t := {{ .NewTranscript }}(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -38,14 +37,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fiel } lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = {{ $newTranscript }}(hashGenerator()) + t = {{ .NewTranscript }}(hashGenerator()) if err := sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t); err != nil { return err } proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], gkrtesting.ToElement[{{ .ElementType }}](1)) lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} - t = {{ $newTranscript }}(hashGenerator()) + t = {{ .NewTranscript }}(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl index 93ba2ece20..342d03aefe 100644 --- a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -1,8 +1,7 @@ -{{ $newTranscript := "gkrcore.NewNativeTranscript" }}{{ if .Mixed }}{{ $newTranscript = printf "gkrcore.NewExtensionTranscript[%s]" .ElementType }}{{ end }} import ( - "github.com/consensys/gnark/internal/gkr/gkrcore" "encoding/json" "fmt" + "github.com/consensys/gnark/internal/gkr/gkrcore" "github.com/consensys/gnark/internal/rational" "github.com/consensys/gnark/internal/rational/polynomial" "github.com/consensys/gnark/internal/gkr/gkrtesting" @@ -31,7 +30,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { } claim := singleMultilinClaim{poly} - t := {{ $newTranscript }}(hsh) + t := gkrcore.NewNativeTranscript(hsh) proof := sumcheckProve(&claim, t) testCaseInfo.Proof = sumcheckToPrintableProof(proof) @@ -49,7 +48,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = {{ $newTranscript }}(hsh) + t = gkrcore.NewNativeTranscript(hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err != nil { return fmt.Errorf("proof rejected: %v", err) } @@ -58,7 +57,7 @@ func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { return err } - t = {{ $newTranscript }}(hsh) + t = gkrcore.NewNativeTranscript(hsh) if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, claimedSum, 1, t); err == nil { return fmt.Errorf("bad proof accepted") } diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 85c3786a56..bce7024c2f 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -22,8 +22,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": - startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, state: startState, step: 0}, nil + return newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } diff --git a/internal/gkr/gkrcore/transcript.go b/internal/gkr/gkrcore/transcript.go index d59a3c019d..dedd78bec0 100644 --- a/internal/gkr/gkrcore/transcript.go +++ b/internal/gkr/gkrcore/transcript.go @@ -44,11 +44,6 @@ func (t *NativeTranscript[E]) Squeeze(elements ...E) E { return t.h.SumDigest() } -// Digest constrains the digest types of the hashers of gnark-crypto's small fields. -type Digest[F any] interface { - ~[4]F | ~[6]F | ~[8]F | ~[12]F -} - // ExtensionTranscript is a Fiat-Shamir transcript over an extension E of a field F, backed by a // running hasher of F. An element is written as its coordinates, and a challenge is made of the // first coordinates of the hasher's digest. Absorb and Squeeze are otherwise those of @@ -56,7 +51,7 @@ type Digest[F any] interface { type ExtensionTranscript[E interface{ AppendBase([]F) []F }, PE interface { *E SetBase([]F) *E -}, F any, D Digest[F]] struct { +}, F any, D hash.ArrayBlock[F]] struct { h hash.FieldHasher[F, D] degree int // the number of coordinates of an element of E buf []F // the coordinates being written or read @@ -68,7 +63,7 @@ type ExtensionTranscript[E interface{ AppendBase([]F) []F }, PE interface { func NewExtensionTranscript[E interface{ AppendBase([]F) []F }, PE interface { *E SetBase([]F) *E -}, F any, D Digest[F]](h hash.FieldHasher[F, D]) *ExtensionTranscript[E, PE, F, D] { +}, F any, D hash.ArrayBlock[F]](h hash.FieldHasher[F, D]) *ExtensionTranscript[E, PE, F, D] { var zero E var d D degree := len(zero.AppendBase(nil)) diff --git a/internal/gkr/gkrcore/transcript_test.go b/internal/gkr/gkrcore/transcript_test.go index 41a3a7d095..3654b84611 100644 --- a/internal/gkr/gkrcore/transcript_test.go +++ b/internal/gkr/gkrcore/transcript_test.go @@ -9,11 +9,6 @@ import ( "github.com/stretchr/testify/assert" ) -var ( - _ Transcript[koalabear.Element] = (*NativeTranscript[koalabear.Element])(nil) - _ Transcript[extensions.E6] = (*ExtensionTranscript[extensions.E6, *extensions.E6, koalabear.Element, [poseidon2.BlockLen]koalabear.Element])(nil) -) - // TestExtensionTranscriptBindsWholeElement checks that every coordinate of an absorbed extension // element affects the next challenge. func TestExtensionTranscriptBindsWholeElement(t *testing.T) { diff --git a/internal/gkr/rational/test_vector_utils.go b/internal/gkr/rational/test_vector_utils.go index 8b5ff68a0e..3bacf8657d 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -25,8 +25,7 @@ func hashFromDescription(d gkrtesting.HashDescription) (fieldHasher, error) { if _type, ok := d["type"]; ok { switch _type { case "const": - startState := int64(d["val"].(float64)) - return &messageCounter{startState: startState, state: startState, step: 0}, nil + return newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } From d81db5532352cc80bd65d95598405b6708ba34a3 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 3 Nov 2026 01:31:15 -0600 Subject: [PATCH 36/37] fix: use hash registry Signed-off-by: Arya Tabaie --- go.mod | 2 +- go.sum | 4 ++-- .../backend/template/gkr/blueprint.go.tmpl | 2 +- internal/gkr/bls12-377/blueprints/blueprint.go | 2 +- internal/gkr/bls12-381/blueprints/blueprint.go | 2 +- internal/gkr/bn254/blueprints/blueprint.go | 2 +- internal/gkr/bw6-761/blueprints/blueprint.go | 2 +- std/gkrapi/api_test.go | 16 +++++++--------- std/permutation/poseidon2/smallfields_test.go | 2 +- 9 files changed, 16 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 2a6ada9dbe..acccfbefcf 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.22.1-0.20261102214826-c1cb9b4629e6 + github.com/consensys/gnark-crypto v0.22.1-0.20261103071746-2967fffbc8b4 github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 diff --git a/go.sum b/go.sum index 78f1387256..400ff3fe78 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.22.1-0.20261102214826-c1cb9b4629e6 h1:MN8JNC1TXtDdjsyzwpnoj180DHbct9lIGg5G2g43owA= -github.com/consensys/gnark-crypto v0.22.1-0.20261102214826-c1cb9b4629e6/go.mod h1:+GBvEPnGIwDMfw6RJdxtImW8wHcBDaveR3s8URlDAc0= +github.com/consensys/gnark-crypto v0.22.1-0.20261103071746-2967fffbc8b4 h1:PiU+gJQF8HZ+/XJzgKsChbUpujc6RUoqfng27OVWiFY= +github.com/consensys/gnark-crypto v0.22.1-0.20261103071746-2967fffbc8b4/go.mod h1:+GBvEPnGIwDMfw6RJdxtImW8wHcBDaveR3s8URlDAc0= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= diff --git a/internal/generator/backend/template/gkr/blueprint.go.tmpl b/internal/generator/backend/template/gkr/blueprint.go.tmpl index abb3933d97..e961f9aba7 100644 --- a/internal/generator/backend/template/gkr/blueprint.go.tmpl +++ b/internal/generator/backend/template/gkr/blueprint.go.tmpl @@ -254,7 +254,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh, err := hash.NewFieldHasher[{{ .ElementType }}, {{ .ElementType }}](b.HashName + "_{{.FieldID}}") + hsh, err := hash.NewFieldHasher[{{ .ElementType }}, {{ .ElementType }}](b.HashName) if err != nil { return err } diff --git a/internal/gkr/bls12-377/blueprints/blueprint.go b/internal/gkr/bls12-377/blueprints/blueprint.go index cc3ffc81b8..42168d0000 100644 --- a/internal/gkr/bls12-377/blueprints/blueprint.go +++ b/internal/gkr/bls12-377/blueprints/blueprint.go @@ -259,7 +259,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BLS12_377") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) if err != nil { return err } diff --git a/internal/gkr/bls12-381/blueprints/blueprint.go b/internal/gkr/bls12-381/blueprints/blueprint.go index 1103861606..1043f58ec1 100644 --- a/internal/gkr/bls12-381/blueprints/blueprint.go +++ b/internal/gkr/bls12-381/blueprints/blueprint.go @@ -259,7 +259,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BLS12_381") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) if err != nil { return err } diff --git a/internal/gkr/bn254/blueprints/blueprint.go b/internal/gkr/bn254/blueprints/blueprint.go index 8666d85c87..1aa3c0f0e8 100644 --- a/internal/gkr/bn254/blueprints/blueprint.go +++ b/internal/gkr/bn254/blueprints/blueprint.go @@ -259,7 +259,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BN254") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) if err != nil { return err } diff --git a/internal/gkr/bw6-761/blueprints/blueprint.go b/internal/gkr/bw6-761/blueprints/blueprint.go index 517e94b365..830a9ce0a7 100644 --- a/internal/gkr/bw6-761/blueprints/blueprint.go +++ b/internal/gkr/bw6-761/blueprints/blueprint.go @@ -259,7 +259,7 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName + "_BW6_761") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) if err != nil { return err } diff --git a/std/gkrapi/api_test.go b/std/gkrapi/api_test.go index 9794c00a84..a5631da080 100644 --- a/std/gkrapi/api_test.go +++ b/std/gkrapi/api_test.go @@ -5,7 +5,6 @@ import ( "math/big" "math/rand" "strconv" - "strings" "testing" "time" @@ -585,15 +584,14 @@ func (h constFieldPseudoHash[F]) SumDigest() F { func (h constFieldPseudoHash[F]) BlockLen() int { return 1 } -// registerConstPseudoHash registers the constant hash v, over the scalar field of curve, as -// name_CURVE. +// registerConstPseudoHash registers the constant hash v over F as name. func registerConstPseudoHash[F any, PF interface { *F SetBigInt(*big.Int) *F -}](name string, v int64, curve ecc.ID) { +}](name string, v int64) { var h constFieldPseudoHash[F] PF(&h.digest).SetBigInt(big.NewInt(v)) - gcHash.RegisterCustomFieldHasher(name+"_"+strings.ToUpper(curve.String()), func() gcHash.FieldHasher[F, F] { + gcHash.RegisterFieldHasher(name, func() gcHash.FieldHasher[F, F] { return h }) } @@ -605,10 +603,10 @@ func init() { stdHash.RegisterCustomHash(name, func(api frontend.API) (stdHash.FieldHasher, error) { return constPseudoHash(v), nil }) - registerConstPseudoHash[frbn254.Element](name, v, ecc.BN254) - registerConstPseudoHash[frbls12377.Element](name, v, ecc.BLS12_377) - registerConstPseudoHash[frbls12381.Element](name, v, ecc.BLS12_381) - registerConstPseudoHash[frbw6761.Element](name, v, ecc.BW6_761) + registerConstPseudoHash[frbn254.Element](name, v) + registerConstPseudoHash[frbls12377.Element](name, v) + registerConstPseudoHash[frbls12381.Element](name, v) + registerConstPseudoHash[frbw6761.Element](name, v) } } diff --git a/std/permutation/poseidon2/smallfields_test.go b/std/permutation/poseidon2/smallfields_test.go index af04b2dce2..ac77a0ee2e 100644 --- a/std/permutation/poseidon2/smallfields_test.go +++ b/std/permutation/poseidon2/smallfields_test.go @@ -69,7 +69,7 @@ func TestMamabearMatchesNative(t *testing.T) { func TestMamabearParameters(t *testing.T) { p, err := mamabearParameters() require.NoError(t, err) - require.Equal(t, 16, p.Width) + require.Equal(t, 12, p.Width) require.Equal(t, 3, p.DegreeSBox) // Round numbers follow Eq. (1) of the Poseidon2 paper for n = 49, t = 16, // d = 3 at kappa = 128: R_F = 8, R_P = ceil(1.075 * 29) = 32. They are NOT From 3a5662a1a4c0549a3cc0d15e6f85c6023252dbe7 Mon Sep 17 00:00:00 2001 From: Arya Tabaie Date: Tue, 3 Nov 2026 10:10:36 -0600 Subject: [PATCH 37/37] refactor: use new gnark-crypto pin Signed-off-by: Arya Tabaie --- go.mod | 2 +- go.sum | 4 ++-- internal/backend/circuits/gkr.go | 3 ++- std/gkrapi/api_test.go | 10 +++++----- std/gkrapi/example_test.go | 3 ++- std/permutation/gkr-mimc/gkr-mimc.go | 3 ++- .../poseidon2/gkr-poseidon2/gkr-poseidon2.go | 3 ++- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index acccfbefcf..9ae9a1ecb5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 github.com/consensys/compress v0.3.0 - github.com/consensys/gnark-crypto v0.22.1-0.20261103071746-2967fffbc8b4 + github.com/consensys/gnark-crypto v0.22.1-0.20261103155519-1a9aede5b1c3 github.com/fxamacker/cbor/v2 v2.9.2 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 diff --git a/go.sum b/go.sum index 400ff3fe78..2018272ca0 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432 h1:4ACburMEVC+u github.com/consensys/bavard v0.2.2-0.20260118153501-cba9f5475432/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.3.0 h1:HRIcHvWkW9C9req0ZWg7mhYHzBarohXhcszIwHONVkM= github.com/consensys/compress v0.3.0/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.22.1-0.20261103071746-2967fffbc8b4 h1:PiU+gJQF8HZ+/XJzgKsChbUpujc6RUoqfng27OVWiFY= -github.com/consensys/gnark-crypto v0.22.1-0.20261103071746-2967fffbc8b4/go.mod h1:+GBvEPnGIwDMfw6RJdxtImW8wHcBDaveR3s8URlDAc0= +github.com/consensys/gnark-crypto v0.22.1-0.20261103155519-1a9aede5b1c3 h1:XYMiyi5Qn8+4QBQE9UzmFw1hwNPMkJaYtExUexWkDak= +github.com/consensys/gnark-crypto v0.22.1-0.20261103155519-1a9aede5b1c3/go.mod h1:+GBvEPnGIwDMfw6RJdxtImW8wHcBDaveR3s8URlDAc0= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= diff --git a/internal/backend/circuits/gkr.go b/internal/backend/circuits/gkr.go index a87c4b2c65..e4737117d1 100644 --- a/internal/backend/circuits/gkr.go +++ b/internal/backend/circuits/gkr.go @@ -2,6 +2,7 @@ package circuits import ( "github.com/consensys/gnark" + gcHash "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/std/gkrapi" @@ -25,7 +26,7 @@ func (c *gkrCubeCircuit) Define(api frontend.API) error { gkrApi.Export(y) - gkrCircuit, err := gkrApi.Compile("MIMC") + gkrCircuit, err := gkrApi.Compile(gcHash.FieldHasherMiMC) if err != nil { return err } diff --git a/std/gkrapi/api_test.go b/std/gkrapi/api_test.go index a5631da080..e743661b00 100644 --- a/std/gkrapi/api_test.go +++ b/std/gkrapi/api_test.go @@ -522,7 +522,7 @@ func TestMiMCNoDepSolve(t *testing.T) { func TestMiMCShallowNoDepSolveWithMiMCHash(t *testing.T) { assert := test.NewAssert(t) - circuit, assignment := mimcNoDepCircuits(5, 1<<3, "MIMC") + circuit, assignment := mimcNoDepCircuits(5, 1<<3, gcHash.FieldHasherMiMC) assert.CheckCircuit(circuit, test.WithValidAssignment(assignment), test.WithCurves(ecc.BN254)) } @@ -626,7 +626,7 @@ func (c *pow4Circuit) Define(api frontend.API) error { x2 := gkrApi.Mul(x, x) // x² x4 := gkrApi.Mul(x2, x2) // x⁴ - gkrCircuit, err := gkrApi.Compile("MIMC") + gkrCircuit, err := gkrApi.Compile(gcHash.FieldHasherMiMC) if err != nil { return err } @@ -684,7 +684,7 @@ func TestSingleInstance(t *testing.T) { X: make([]frontend.Variable, 1), Y: make([]frontend.Variable, 1), mimcDepth: 2, - hashName: "MIMC", + hashName: gcHash.FieldHasherMiMC, } assignment := mimcNoDepCircuit{ X: []frontend.Variable{10}, @@ -714,7 +714,7 @@ func (c *testNoInstanceCircuit) Define(api frontend.API) error { y := gkrApi.Mul(x, x) gkrApi.Mul(x, y) - _, err = gkrApi.Compile("MIMC") + _, err = gkrApi.Compile(gcHash.FieldHasherMiMC) return err } @@ -741,7 +741,7 @@ func (c *testMultiCircuit) Define(api frontend.API) error { y2 := api2.NewInput() z2 := api2.Mul(x2, y2) - circuit2, err := api2.Compile("MIMC") + circuit2, err := api2.Compile(gcHash.FieldHasherMiMC) if err != nil { return err } diff --git a/std/gkrapi/example_test.go b/std/gkrapi/example_test.go index 576ed2cc3e..64168320b4 100644 --- a/std/gkrapi/example_test.go +++ b/std/gkrapi/example_test.go @@ -6,6 +6,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" bls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377" + gcHash "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/std/gkrapi" @@ -100,7 +101,7 @@ func (c *exampleCircuit) Define(api frontend.API) error { // Mark XOut as an output, even though it is fed into another wire (YOut) gkrApi.Export(XOut) - gkrCircuit, err := gkrApi.Compile("MIMC") + gkrCircuit, err := gkrApi.Compile(gcHash.FieldHasherMiMC) if err != nil { return err } diff --git a/std/permutation/gkr-mimc/gkr-mimc.go b/std/permutation/gkr-mimc/gkr-mimc.go index e4f05b9de8..f7cb6a5117 100644 --- a/std/permutation/gkr-mimc/gkr-mimc.go +++ b/std/permutation/gkr-mimc/gkr-mimc.go @@ -9,6 +9,7 @@ import ( bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/mimc" bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" bw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/mimc" + gcHash "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/kvstore" @@ -85,7 +86,7 @@ func NewCompressor(api frontend.API) (hash.Compressor, error) { y = gkrApi.Gate(lastLayerSBox(&constants[len(constants)-1]), in0, y, in1) - gkrCircuit, err := gkrApi.Compile("POSEIDON2") + gkrCircuit, err := gkrApi.Compile(gcHash.FieldHasherPoseidon2) if err != nil { return nil, err } diff --git a/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2.go b/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2.go index 46c3b8032a..576d6b4da2 100644 --- a/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2.go +++ b/std/permutation/poseidon2/gkr-poseidon2/gkr-poseidon2.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/consensys/gnark-crypto/ecc" + gcHash "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/gkr" "github.com/consensys/gnark/internal/kvstore" @@ -265,7 +266,7 @@ func defineCircuit(api frontend.API) (gkrCircuit *gkrapi.Circuit, in1, in2, out // apply the external matrix one last time to obtain the final value of y out = gkrApi.Gate(extAddGate, y, x, in2) - gkrCircuit, err = gkrApi.Compile("MIMC") + gkrCircuit, err = gkrApi.Compile(gcHash.FieldHasherMiMC) return }