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/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.go b/gkr/bls12-377/gkr.go index 477a0acbd1..1d5beb4130 100644 --- a/gkr/bls12-377/gkr.go +++ b/gkr/bls12-377/gkr.go @@ -3,38 +3,38 @@ // Code generated by gnark DO NOT EDIT -package gkr +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" 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. @@ -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) @@ -64,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 @@ -78,13 +92,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.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, 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]), @@ -134,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 } } @@ -146,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) { @@ -159,18 +174,22 @@ 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 } // 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) { +// 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[fr.Element, fr.Element]) (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-- { @@ -231,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) { @@ -247,18 +266,26 @@ 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 } // 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) { +// 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, +// 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") + } r := newResources(c, schedule, logNbInstances, hasher) if len(proof) != len(schedule)+1 { @@ -295,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-- { @@ -348,6 +375,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 { @@ -357,6 +385,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 { @@ -376,6 +405,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 @@ -391,3 +423,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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + 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 c41ecda7f7..7a73473af6 100644 --- a/gkr/bls12-377/gkr_test.go +++ b/gkr/bls12-377/gkr_test.go @@ -3,29 +3,31 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "fmt" - "hash" "os" "path/filepath" "reflect" + "slices" "testing" "time" "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" "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(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 @@ -72,7 +74,98 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// 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, poseidon2.NewHasher()) + 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, poseidon2.NewHasher()) + 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") +} + +// 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 // 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 @@ -86,8 +179,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)) @@ -268,11 +368,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 } } @@ -303,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, gcHash.POSEIDON2_BLS12_377.New()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -453,7 +553,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 } } @@ -465,7 +565,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 } } @@ -475,7 +575,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 @@ -502,7 +602,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 } @@ -546,7 +646,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 } @@ -559,7 +659,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/sumcheck.go b/gkr/bls12-377/sumcheck.go index 5b1435a5d6..288a347247 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" @@ -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, *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,18 +46,18 @@ 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() 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 @@ -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) @@ -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]) } @@ -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,14 +350,14 @@ 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 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) @@ -439,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()) @@ -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 @@ -696,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/sumcheck_test.go b/gkr/bls12-377/sumcheck_test.go index a5e7c43851..8a2ab0567f 100644 --- a/gkr/bls12-377/sumcheck_test.go +++ b/gkr/bls12-377/sumcheck_test.go @@ -3,14 +3,14 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 import ( "fmt" - "hash" "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" @@ -21,14 +21,14 @@ 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) } 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() hash } 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], 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()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } @@ -70,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 e37168688a..ce001bf901 100644 --- a/gkr/bls12-377/test_vector_utils_test.go +++ b/gkr/bls12-377/test_vector_utils_test.go @@ -3,35 +3,29 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12377 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-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} +// 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. +// 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) (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 newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -39,111 +33,38 @@ 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 } -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 fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) Reset() { m.state = m.startState } -func (m *messageCounter) Reset() { - m.state = m.startState -} +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } -func (m *messageCounter) Size() int { - return messageCounterBlockSize +func (m *messageCounter) WriteElements(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -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) - } -} +func (m *messageCounter) PadBlock() {} -// 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 (m *messageCounter) SumDigest() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -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 (m *messageCounter) BlockLen() int { return 1 } -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 newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } -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) - } +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { + return newMessageCounter(startState, step) } - return nil } diff --git a/gkr/bls12-381/gkr.go b/gkr/bls12-381/gkr.go index 8eca467e34..4f1fc4418c 100644 --- a/gkr/bls12-381/gkr.go +++ b/gkr/bls12-381/gkr.go @@ -3,38 +3,38 @@ // Code generated by gnark DO NOT EDIT -package gkr +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" 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. @@ -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) @@ -64,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 @@ -78,13 +92,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.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, 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]), @@ -134,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 } } @@ -146,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) { @@ -159,18 +174,22 @@ 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 } // 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) { +// 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[fr.Element, fr.Element]) (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-- { @@ -231,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) { @@ -247,18 +266,26 @@ 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 } // 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) { +// 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, +// 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") + } r := newResources(c, schedule, logNbInstances, hasher) if len(proof) != len(schedule)+1 { @@ -295,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-- { @@ -348,6 +375,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 { @@ -357,6 +385,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 { @@ -376,6 +405,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 @@ -391,3 +423,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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + 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 b9f47831a7..b71bc3d5d0 100644 --- a/gkr/bls12-381/gkr_test.go +++ b/gkr/bls12-381/gkr_test.go @@ -3,29 +3,31 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "fmt" - "hash" "os" "path/filepath" "reflect" + "slices" "testing" "time" "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" "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(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 @@ -72,7 +74,98 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// 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, poseidon2.NewHasher()) + 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, poseidon2.NewHasher()) + 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") +} + +// 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 // 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 @@ -86,8 +179,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)) @@ -268,11 +368,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 } } @@ -303,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, gcHash.POSEIDON2_BLS12_381.New()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -453,7 +553,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 } } @@ -465,7 +565,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 } } @@ -475,7 +575,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 @@ -502,7 +602,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 } @@ -546,7 +646,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 } @@ -559,7 +659,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/sumcheck.go b/gkr/bls12-381/sumcheck.go index b09a3d62bb..0eee1c90fa 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" @@ -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, *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,18 +46,18 @@ 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() 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 @@ -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) @@ -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]) } @@ -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,14 +350,14 @@ 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 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) @@ -439,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()) @@ -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 @@ -696,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/sumcheck_test.go b/gkr/bls12-381/sumcheck_test.go index 39d93ee2a0..0f663e48bc 100644 --- a/gkr/bls12-381/sumcheck_test.go +++ b/gkr/bls12-381/sumcheck_test.go @@ -3,14 +3,14 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 import ( "fmt" - "hash" "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" @@ -21,14 +21,14 @@ 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) } 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() hash } 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], 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()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } @@ -70,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 a94e6f4053..eccce6472f 100644 --- a/gkr/bls12-381/test_vector_utils_test.go +++ b/gkr/bls12-381/test_vector_utils_test.go @@ -3,35 +3,29 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbls12381 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-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} +// 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. +// 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) (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 newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -39,111 +33,38 @@ 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 } -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 fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) Reset() { m.state = m.startState } -func (m *messageCounter) Reset() { - m.state = m.startState -} +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } -func (m *messageCounter) Size() int { - return messageCounterBlockSize +func (m *messageCounter) WriteElements(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -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) - } -} +func (m *messageCounter) PadBlock() {} -// 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 (m *messageCounter) SumDigest() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -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 (m *messageCounter) BlockLen() int { return 1 } -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 newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } -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) - } +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { + return newMessageCounter(startState, step) } - return nil } diff --git a/gkr/bn254/gkr.go b/gkr/bn254/gkr.go index 2690a1b795..4cc69b4cd3 100644 --- a/gkr/bn254/gkr.go +++ b/gkr/bn254/gkr.go @@ -3,38 +3,38 @@ // Code generated by gnark DO NOT EDIT -package gkr +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" 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. @@ -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) @@ -64,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 @@ -78,13 +92,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.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, 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]), @@ -134,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 } } @@ -146,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) { @@ -159,18 +174,22 @@ 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 } // 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) { +// 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[fr.Element, fr.Element]) (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-- { @@ -231,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) { @@ -247,18 +266,26 @@ 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 } // 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) { +// 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, +// 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") + } r := newResources(c, schedule, logNbInstances, hasher) if len(proof) != len(schedule)+1 { @@ -295,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-- { @@ -348,6 +375,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 { @@ -357,6 +385,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 { @@ -376,6 +405,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 @@ -391,3 +423,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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + 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 ba01cf624e..0f8c493780 100644 --- a/gkr/bn254/gkr_test.go +++ b/gkr/bn254/gkr_test.go @@ -3,29 +3,31 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "fmt" - "hash" "os" "path/filepath" "reflect" + "slices" "testing" "time" "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" "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(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 @@ -72,7 +74,98 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// 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, poseidon2.NewHasher()) + 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, poseidon2.NewHasher()) + 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") +} + +// 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 // 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 @@ -86,8 +179,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)) @@ -268,11 +368,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 } } @@ -303,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, gcHash.POSEIDON2_BN254.New()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -453,7 +553,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 } } @@ -465,7 +565,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 } } @@ -475,7 +575,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 @@ -502,7 +602,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 } @@ -546,7 +646,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 } @@ -559,7 +659,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/sumcheck.go b/gkr/bn254/sumcheck.go index 21f35fc3a9..3f61bb68ee 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" @@ -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, *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,18 +46,18 @@ 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() 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 @@ -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) @@ -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]) } @@ -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,14 +350,14 @@ 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 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) @@ -439,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()) @@ -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 @@ -696,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/sumcheck_test.go b/gkr/bn254/sumcheck_test.go index a9ce79ec4a..f4bc769139 100644 --- a/gkr/bn254/sumcheck_test.go +++ b/gkr/bn254/sumcheck_test.go @@ -3,14 +3,14 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 import ( "fmt" - "hash" "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" @@ -21,14 +21,14 @@ 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) } 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() hash } 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], 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()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } @@ -70,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 4348dd5f48..46cc7fdbec 100644 --- a/gkr/bn254/test_vector_utils_test.go +++ b/gkr/bn254/test_vector_utils_test.go @@ -3,35 +3,29 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbn254 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-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} +// 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. +// 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) (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 newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -39,111 +33,38 @@ 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 } -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 fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) Reset() { m.state = m.startState } -func (m *messageCounter) Reset() { - m.state = m.startState -} +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } -func (m *messageCounter) Size() int { - return messageCounterBlockSize +func (m *messageCounter) WriteElements(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -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) - } -} +func (m *messageCounter) PadBlock() {} -// 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 (m *messageCounter) SumDigest() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -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 (m *messageCounter) BlockLen() int { return 1 } -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 newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } -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) - } +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { + return newMessageCounter(startState, step) } - return nil } diff --git a/gkr/bw6-761/gkr.go b/gkr/bw6-761/gkr.go index 33a06a0134..a0c80a4665 100644 --- a/gkr/bw6-761/gkr.go +++ b/gkr/bw6-761/gkr.go @@ -3,38 +3,38 @@ // Code generated by gnark DO NOT EDIT -package gkr +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" 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. @@ -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) @@ -64,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 @@ -78,13 +92,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.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, 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]), @@ -134,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 } } @@ -146,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) { @@ -159,18 +174,22 @@ 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 } // 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) { +// 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[fr.Element, fr.Element]) (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-- { @@ -231,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) { @@ -247,18 +266,26 @@ 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 } // 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) { +// 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, +// 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") + } r := newResources(c, schedule, logNbInstances, hasher) if len(proof) != len(schedule)+1 { @@ -295,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-- { @@ -348,6 +375,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 { @@ -357,6 +385,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 { @@ -376,6 +405,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 @@ -391,3 +423,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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + 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 02d1c6cf1b..e2895631ab 100644 --- a/gkr/bw6-761/gkr_test.go +++ b/gkr/bw6-761/gkr_test.go @@ -3,29 +3,31 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "fmt" - "hash" "os" "path/filepath" "reflect" + "slices" "testing" "time" "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" "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(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 @@ -72,7 +74,98 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// 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, poseidon2.NewHasher()) + 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, poseidon2.NewHasher()) + 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") +} + +// 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 // 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 @@ -86,8 +179,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)) @@ -268,11 +368,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 } } @@ -303,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, gcHash.POSEIDON2_BW6_761.New()) + _, _, err = Prove(c, schedule, assignment, poseidon2.NewHasher()) proved := time.Now().UnixMicro() - start fmt.Println("proved in", proved, "μs") assert.NoError(b, err) @@ -453,7 +553,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 } } @@ -465,7 +565,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 } } @@ -475,7 +575,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 @@ -502,7 +602,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 } @@ -546,7 +646,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 } @@ -559,7 +659,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/sumcheck.go b/gkr/bw6-761/sumcheck.go index e482d08d93..f74e2724d7 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" @@ -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, *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,18 +46,18 @@ 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() 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 @@ -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) @@ -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]) } @@ -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,14 +350,14 @@ 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 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) @@ -439,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()) @@ -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 @@ -696,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/sumcheck_test.go b/gkr/bw6-761/sumcheck_test.go index fa539a23cb..a91592524a 100644 --- a/gkr/bw6-761/sumcheck_test.go +++ b/gkr/bw6-761/sumcheck_test.go @@ -3,14 +3,14 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 import ( "fmt" - "hash" "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" @@ -21,14 +21,14 @@ 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) } 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() hash } 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], 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()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } @@ -70,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 9d01aacc6e..2bf9909a6a 100644 --- a/gkr/bw6-761/test_vector_utils_test.go +++ b/gkr/bw6-761/test_vector_utils_test.go @@ -3,35 +3,29 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrbw6761 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-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *fr.Element { - var res fr.Element - res.SetInt64(i) - return &res -} +// 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. +// 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) (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 newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -39,111 +33,38 @@ 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 } -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 fr.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) Reset() { m.state = m.startState } -func (m *messageCounter) Reset() { - m.state = m.startState -} +func (m *messageCounter) WriteElement(fr.Element) { m.state += m.step } -func (m *messageCounter) Size() int { - return messageCounterBlockSize +func (m *messageCounter) WriteElements(e ...fr.Element) { + m.state += int64(len(e)) * m.step } -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) - } -} +func (m *messageCounter) PadBlock() {} -// 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 (m *messageCounter) SumDigest() fr.Element { + var res fr.Element + res.SetInt64(m.state + m.step) + return res } -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 (m *messageCounter) BlockLen() int { return 1 } -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 newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } -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) - } +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { + return newMessageCounter(startState, step) } - return nil } 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/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..fd31acea02 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 @@ -68,8 +68,9 @@ 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. -func (api *API) Compile(field Field, mode ConsolidationMode) (Circuit, constraint.GkrProvingSchedule, error) { +// 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 { return nil, nil, err diff --git a/gkr/gkrapi/example_test.go b/gkr/gkrapi/example_test.go index ce611c59f5..8843ddb16b 100644 --- a/gkr/gkrapi/example_test.go +++ b/gkr/gkrapi/example_test.go @@ -5,15 +5,14 @@ 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" - bn254 "github.com/consensys/gnark/gkr/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" + 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,18 +23,27 @@ 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()) + // 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.NewHasher()) 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, mimc.NewHasher()) 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 new file mode 100644 index 0000000000..3450c4a5bd --- /dev/null +++ b/gkr/koalabear/gkr.go @@ -0,0 +1,472 @@ +// 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 gkrkoalabear + +import ( + "errors" + "fmt" + "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" + "github.com/consensys/gnark/internal/gkr/gkrcore" + evaluator "github.com/consensys/gnark/internal/gkr/koalabear" +) + +// 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[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 + +// 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 = 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. +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 { + 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) + } + } + } + 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 +} + +// 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 { + 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[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, + circuit: c, + schedule: schedule, + transcript: gkrcore.NewExtensionTranscript[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.evaluateBase(r.assignment[inW], point) + } + } + 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, 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 } + } + 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, absorbs 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])) + } + 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 +} + +// 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. hasher must be seeded as +// Verify requires. +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 { + 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, absorbs 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) + } + 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 +} + +// 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, 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, +// 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[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") + } + 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.Squeeze() + } + r.outgoingEvalPoints[len(schedule)] = [][]extensions.E6{firstChallenge} + var absorbedOutputEvals []extensions.E6 + for i, w := range c.Outputs() { + if r.consolidated[w] { + absorbedOutputEvals = append(absorbedOutputEvals, outputLevel.finalEvalProof[i]) + } + } + 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-- { + 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.GateEvaluator, len(circuit)) + + for i := range circuit { + if len(a[i]) != nbInstances { + a[i] = make([]koalabear.Element, nbInstances) + } + if !circuit[i].IsInput() { + evaluators[i] = evaluator.NewGateEvaluator(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 +} + +// 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 { + return len(aW) + } + } + 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 { + 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 +} + +// 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 + for i := range p { + for _, poly := range p[i].partialSumPolys { + if !iterateElems(poly, &counter, yield) { + return + } + } + if !iterateElems(p[i].finalEvalProof, &counter, yield) { + return + } + } + } +} + +// 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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + 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 new file mode 100644 index 0000000000..ab1dc9aa2e --- /dev/null +++ b/gkr/koalabear/gkr_test.go @@ -0,0 +1,691 @@ +// 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 gkrkoalabear + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "slices" + "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" + 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" + "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()) + +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}) +} + +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)) +} + +// 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, poseidon2.NewHasher()) + 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, poseidon2.NewHasher()) + 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") +} + +// 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 +// 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)) + + 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, deserialized, 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], &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) + } 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], &oneExt) + } + } +} + +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) + 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([]koalabear.Element, maxSize) + koalabear.Vector(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 ...[]koalabear.Element) { + _, 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 := gkrtesting.SliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := gkrtesting.PolynomialSliceEquals(gkrtesting.ToSlices(x.partialSumPolys), gkrtesting.ToSlices(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([]koalabear.Element, nbInstances) + in1 := make([]koalabear.Element, nbInstances) + koalabear.Vector(in0).MustSetRandom() + koalabear.Vector(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, poseidon2.NewHasher()) + 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 := gkrtesting.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 = gkrtesting.SliceToElementSlice[extensions.E6](printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrcore.SerializableCircuit + Hash fieldHasher + 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 fieldHasher + 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 []koalabear.Element + if wireAssignment, err = gkrtesting.SliceToElementSlice[koalabear.Element](assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(circuit) + + for i := range circuit { + if outputSet[i] { + if err = gkrtesting.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..0439058be1 --- /dev/null +++ b/gkr/koalabear/sumcheck.go @@ -0,0 +1,947 @@ +// 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 gkrkoalabear + +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" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients. + +// 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...) +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.Squeeze(proof.partialSumPolys[j]...) + claims.roundFold(challenges[j]) + proof.partialSumPolys[j+1] = claims.roundPolynomial() + } + challenges[varsNum-1] = t.Squeeze(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.Squeeze(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 +} + +// 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. +// 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) + 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([]extensions.E6, degree) + var mu sync.Mutex + computeAll := func(start, end int) { + var eqStep extensions.E6 + 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) + 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) + } + } + 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) + } + } + + 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]) // 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 +} + +// 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. +// 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) + 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([]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) + } + } + + 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.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() + } + + const minBlockSize = 64 + if sumSize < minBlockSize { + computeAll(0, sumSize) + } else { + c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + 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.roundPolynomialBaseInputs() + } + return c.roundPolynomialExt() +} + +// 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.foldInput(i, &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.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) + } + for _, wg := range wgs { + wg.Wait() + } + } + c.baseInput = nil +} + +// 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 + 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. +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.foldInput(i, &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() + } + for _, pool := range c.baseGateEvaluatorPools { + pool.DumpAll() + } + return evaluations +} + +// 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 + c.resources = r + level := r.schedule[levelI] + if level.NbClaims() >= 2 { + c.foldingCoeff = r.transcript.Squeeze() + } + + 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 { + c.baseInput[i] = r.assignment[inW] + } + c.inputIndices = inputIndices + + nbWires := 0 + for _, group := range level.ClaimGroups() { + 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++ + } + } +} + +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.Squeeze() + } + + 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 +} + +// 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. +// 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) + 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 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 +} + +// 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. +// 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) + 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 { + c.baseGateEvaluatorPools[w].Put(evaluators[w]) + } + }() + + res := make([]extensions.E6, degree) + + // 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) + } + } + + 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()) + } + + 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 +} + +// 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.roundPolynomialBaseInputs() + } + return c.roundPolynomialExt() +} + +// 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 := c.tableLen() / 2 + if n < minBlockSize { + for i := range c.input { + 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.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] + } +} + +// 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.Squeeze(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..b95511a169 --- /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 gkrkoalabear + +import ( + "fmt" + + "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" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { + poly := make(polynomial.MultiLinE6, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + t := gkrcore.NewExtensionTranscript[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.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.NewExtensionTranscript[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() fieldHasher, 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..b46e4b5769 --- /dev/null +++ b/gkr/koalabear/test_vector_utils_test.go @@ -0,0 +1,71 @@ +// 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 gkrkoalabear + +import ( + "fmt" + + "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[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 +// 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) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + return newMessageCounter(int(d["val"].(float64)), 0), nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +// 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) 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) 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{startState: int64(startState), state: int64(startState), step: int64(step)} +} + +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { + return newMessageCounter(startState, step) + } +} diff --git a/go.mod b/go.mod index be7d1b4674..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.20261009142911-8911ee45d22b + 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 5f09b940b1..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.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.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/internal/generator/backend/main.go b/internal/generator/backend/main.go index 937b0e6a5a..83e0e504ba 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" @@ -164,13 +165,8 @@ 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.", + FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", + GkrPackageName: curvePackageName, } assertNoError(generateGkrBackend(cfg)) @@ -246,16 +242,25 @@ 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() { // 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, } assertNoError(generateGkrBackend(cfg)) @@ -350,7 +355,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"}, }) @@ -363,11 +368,11 @@ 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 } - 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"}} @@ -380,24 +385,100 @@ 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 + // GkrPackageName is the directory of the generated packages, relative to the GKR roots. + GkrPackageName string + // 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 + // 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.ExtensionSuffix != "" } + +// 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" +} + +// 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. +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, "-", "_")) +} + +// 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 gkr.Field description, used where +// gkr.test.go.tmpl is generated. +func (c gkrConfig) FieldDescription() string { + if c.Description != "" { + return c.Description + } + return "gkr.PrimeField(ecc." + c.FieldID() + ".ScalarField())" } func assertNoError(err error) { diff --git a/internal/generator/backend/template/gkr/blueprint.go.tmpl b/internal/generator/backend/template/gkr/blueprint.go.tmpl index 32964bd451..e961f9aba7 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 } @@ -254,7 +254,10 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_{{.FieldID}}") + hsh, err := hash.NewFieldHasher[{{ .ElementType }}, {{ .ElementType }}](b.HashName) + if err != nil { + return err + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -266,8 +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[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteElement(challenge) } // Call the {{.FieldID}}-specific Prove function (assignments already WireAssignment type) diff --git a/internal/generator/backend/template/gkr/evaluator.go.tmpl b/internal/generator/backend/template/gkr/evaluator.go.tmpl index 7b07b35792..4ed1515cc8 100644 --- a/internal/generator/backend/template/gkr/evaluator.go.tmpl +++ b/internal/generator/backend/template/gkr/evaluator.go.tmpl @@ -2,25 +2,49 @@ import ( "fmt" "sync" - "{{.FieldPackagePath}}" - "{{.FieldPackagePath}}/polynomial" + "{{ .BaseFieldPackagePath }}" + {{- if .Mixed }} + "{{ .FieldPackagePath }}" + basePolynomial "{{ .BaseFieldPackagePath }}/polynomial" + {{- end }} + "{{ .FieldPackagePath }}/polynomial" "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 := .Config.BasePolynomial }} +{{- $suffix := "" }} +{{- $ge := "GateEvaluator" }} +{{- if .Mixed }} +{{- $elem = .Config.ElementType }} +{{- $poly = "polynomial" }} +{{- $suffix = .Config.ExtensionSuffix }} +{{- $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, for the extension's methods taking a base field element + {{- end }} nbIn int // number of inputs expected - pool *polynomial.Pool // pool vars was allocated from, if any + pool *{{ $poly }}.Pool{{ $suffix }} // 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{{ $suffix }}) {{ $ge }} { + e := {{ $ge }}{ gate: gate, nbIn: nbIn, } @@ -28,18 +52,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,15 +77,22 @@ 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) } +{{ 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. // 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() { @@ -73,30 +110,70 @@ func (e *GateEvaluator) Evaluate(top ...{{ .ElementType }}) *{{ .ElementType }} // 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 }} + 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]]) + } + {{- 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]]) } 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]]) } case gkrcore.OpNeg: dst.Neg(&e.vars[inst.Inputs[0]]) case gkrcore.OpMulAcc: - var prod {{ .ElementType }} - prod.Mul(&e.vars[inst.Inputs[1]], &e.vars[inst.Inputs[2]]) - dst.Add(&e.vars[inst.Inputs[0]], &prod) + var prod {{ $elem }} + {{- if .Mixed }} + 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]]) + } + {{- else }} + prod.Mul(&e.vars[inst.Inputs[0]], &e.vars[inst.Inputs[1]]) + {{- end }} + dst.Add(&prod, &e.vars[inst.Inputs[2]]) case gkrcore.OpSumExp17: // result = (x[0] + x[1] + x[2])^17 - var sum {{ .ElementType }} + 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⁴ @@ -116,25 +193,25 @@ func (e *GateEvaluator) Evaluate(top ...{{ .ElementType }}) *{{ .ElementType }} 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 { +// {{ $ge }}Pool manages a pool of gate evaluators for a specific gate type. +// 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 lock sync.Mutex - available []*GateEvaluator - elementPool *polynomial.Pool + available []*{{ $ge }} + elementPool []*{{ $poly }}.Pool{{ $suffix }} } -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{{ $suffix }}) *{{ $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 +221,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 +233,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 +241,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..98389c73b6 100644 --- a/internal/generator/backend/template/gkr/gkr.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -1,35 +1,42 @@ +{{ $gateEvaluator := "GateEvaluator" }}{{ if .Mixed }}{{ $gateEvaluator = "GateEvaluatorMixed" }}{{ end }} +{{ $digest := .ElementType }}{{ $typeParams := "" }}{{ if .Mixed }}{{ $digest = "D" }}{{ $typeParams = printf "[D hash.ArrayBlock[%s]]" .BaseElementType }}{{ end }} import ( "errors" "fmt" - "hash" "iter" "{{.FieldPackagePath}}" "{{.FieldPackagePath}}/polynomial" + {{- if .Mixed }} + "{{ .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" {{- 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 -type WireAssignment []polynomial.MultiLin +// 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. @@ -37,9 +44,30 @@ 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 }} + 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 }} 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) } @@ -55,16 +83,27 @@ type resources struct { outgoingEvalPoints [][][]{{ .ElementType }} nbVars int assignment WireAssignment - memPool polynomial.Pool + memPool polynomial.Pool{{ .ExtensionSuffix }} 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 } +{{ 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 { @@ -73,13 +112,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.Hash) 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]), @@ -96,7 +135,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} @@ -114,7 +157,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)]) @@ -129,11 +172,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 } } @@ -141,7 +185,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) { @@ -154,34 +198,42 @@ 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 } // 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) { +// output evaluations among them, are not bound into the transcript. hasher must be seeded as +// Verify requires. +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 { + return nil, nil, errors.New("number of variables must be positive") + } if 1<= 1; levelI-- { @@ -226,7 +282,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) { @@ -242,18 +298,26 @@ 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 } // 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) { +// 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, +// 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{{ $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") + } r := newResources(c, schedule, logNbInstances, hasher) if len(proof) != len(schedule)+1 { @@ -290,16 +354,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-- { @@ -318,14 +382,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}}NewGateEvaluator(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) + evaluators[i] = {{ .EvaluatorQualifier }}NewGateEvaluator(circuit[i].Gate.Evaluate, len(circuit[i].Inputs)) } } @@ -343,6 +407,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 { @@ -352,6 +417,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 { @@ -371,6 +437,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 @@ -386,3 +455,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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + 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 356d627896..6e86d4e3f1 100644 --- a/internal/generator/backend/template/gkr/gkr.test.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -1,29 +1,38 @@ import ( "fmt" - "hash" "os" "path/filepath" "reflect" + "slices" "testing" "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 }} + poseidon2 "{{ .BaseFieldPackagePath }}/poseidon2" +{{- else }} + "{{ .FieldPackagePath }}/poseidon2" +{{- end }} "{{ .FieldPackagePath }}" "{{ .FieldPackagePath }}/polynomial" + {{- if .Mixed }} + "{{ .BaseFieldPackagePath }}" + {{- 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 }}) 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) { @@ -66,7 +75,108 @@ func TestPoseidon2(t *testing.T) { test(t, gkrtesting.Poseidon2Circuit(4, 2)) } -// proveAndVerify proves and verifies assignment against schedule, then asserts that: every wire of +// 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, poseidon2.NewHasher()) + 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, poseidon2.NewHasher()) + 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") +} + +// 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 // 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 @@ -80,8 +190,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)) @@ -128,7 +245,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) @@ -136,14 +253,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) @@ -174,8 +298,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)) @@ -192,7 +316,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) @@ -262,11 +386,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 } } @@ -280,10 +404,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() @@ -297,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, gcHash.POSEIDON2_{{ .FieldID }}.New()) + _, _, 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/gkr.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl index 270da81e24..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 @@ -4,11 +4,11 @@ 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" "github.com/consensys/gnark/internal/gkr/gkrcore" - "hash" "os" "path/filepath" "reflect" @@ -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 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/generator/backend/template/gkr/gkr.test.vectors.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl index fb99e2c543..09a2f24047 100644 --- a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl @@ -9,19 +9,19 @@ 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 } } } 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 { 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 } } @@ -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 } @@ -103,8 +103,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 = gkrtesting.SliceToElementSlice[{{ .BaseElementType }}](assignmentRaw); err != nil { return nil, err } @@ -117,7 +117,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/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl index c6dd55b64c..54dbfd50e6 100644 --- a/internal/generator/backend/template/gkr/sumcheck.go.tmpl +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -1,9 +1,14 @@ +{{ $gateEvaluator := "GateEvaluator" }}{{ if .Mixed }}{{ $gateEvaluator = "GateEvaluatorMixed" }}{{ end }} import ( "errors" "sync" "{{.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}}" @@ -13,13 +18,13 @@ 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 }}] +// 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...) 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 @@ -34,25 +39,25 @@ 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 } // 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 := sumcheckProof{partialSumPolys: make([]polynomial.Polynomial{{ .ExtensionSuffix }}, varsNum)} proof.partialSumPolys[0] = claims.roundPolynomial() 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 @@ -61,10 +66,10 @@ 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, degree+1) + gJ := make(polynomial.Polynomial{{ .ExtensionSuffix }}, degree+1) gJR := claimedSum for j := range claims.varsNum() { @@ -74,8 +79,8 @@ 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]...) - gJCoeffs := polynomial.InterpolateOnRange(gJ[:(degree + 1)]) + r[j] = t.Squeeze(proof.partialSumPolys[j]...) + gJCoeffs := polynomial.InterpolateOnRange{{ .ExtensionSuffix }}(gJ[:(degree + 1)]) gJR = gJCoeffs.Eval(&r[j]) } @@ -114,20 +119,20 @@ 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 { 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) } 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) @@ -148,93 +153,23 @@ 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 { - 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, nbWires+nbUniqueInputs) - copy(ml, c.eqs) - copy(ml[nbWires:], c.input) - - sumSize := len(c.eqs[0]) / 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) - - // 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)) +{{ template "zeroCheckRoundPolynomial" dict "Config" . "Base" false }} +{{- if .Mixed }} - 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() - } +{{ template "zeroCheckRoundPolynomial" dict "Config" . "Base" true }} - const minBlockSize = 64 - if sumSize < minBlockSize { - computeAll(0, sumSize) - } else { - c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() +// 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 { + return c.roundPolynomialBaseInputs() } - - return p + return c.roundPolynomialExt() } +{{- end }} // roundFold folds all input and eq polynomials at the verifier challenge r. // After this call, j ← j+1 and rⱼ = r. @@ -243,7 +178,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) @@ -251,7 +190,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) @@ -260,6 +203,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ₙ). @@ -270,7 +216,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ᵢ₊₁) @@ -311,23 +257,63 @@ 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}}GateEvaluatorPool + 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. -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 { + {{- 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 { @@ -339,23 +325,35 @@ 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 } -// 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 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) - c.input = make([]polynomial.MultiLin, len(uniqueInputs)) + 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 }} + c.baseInput[i] = r.assignment[inW] + {{- else }} c.input[i] = r.memPool.Clone(r.assignment[inW]) + {{- end }} } c.inputIndices = inputIndices @@ -363,12 +361,18 @@ 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) + {{- 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}}NewGateEvaluatorPool(wire.Gate.Evaluate, len(inputIndices[levelWireI]), &r.memPool) + 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++ } } @@ -380,19 +384,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) @@ -413,7 +417,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) @@ -434,10 +438,10 @@ 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, 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 { @@ -473,111 +477,57 @@ 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 { - 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([]{{ .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) +{{ template "singleSourceRoundPolynomial" dict "Config" . "Base" false }} +{{- if .Mixed }} - 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) - } - } +{{ template "singleSourceRoundPolynomial" dict "Config" . "Base" true }} - 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() +// 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 { + return c.roundPolynomialBaseInputs() } - - const minBlockSize = 64 - if sumSize < minBlockSize { - computeAll(0, sumSize) - } else { - c.resources.workers.Submit(sumSize, computeAll, minBlockSize).Wait() - } - - return p + return c.roundPolynomialExt() } +{{- end }} // roundFold folds only input multilinears at the verifier challenge r. // 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] } @@ -585,7 +535,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. @@ -598,7 +548,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) @@ -662,7 +612,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 @@ -691,15 +641,230 @@ 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)]) + challenges[j] = r.transcript.Squeeze(partialPoly...) + 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) } + +{{ define "zeroCheckRoundPolynomial" -}} +{{- $name := "roundPolynomial" }} +{{- $use := "" }} +{{- $inputs := "c.input" }} +{{- $inElem := .Config.ElementType }} +{{- $pools := "c.gateEvaluatorPools" }} +{{- $ge := "GateEvaluator" }} +{{- $mul := "Mul" }} +{{- if .Base }} +{{- $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 = "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.{{ $use }} +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 {{ $inElem }} + + evaluators := make([]*{{ .Config.EvaluatorQualifier }}{{ $ge }}, nbWires) + for w := range nbWires { + evaluators[w] = {{ $pools }}[w].Get() + } + defer func() { + for w := range nbWires { + {{ $pools }}[w].Put(evaluators[w]) + } + }() + + res := make([]{{ .Config.ElementType }}, degree) + var term {{ .Config.ElementType }} + + // 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([]{{ $inElem }}, 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(&{{ $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[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]) // 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" }} +{{- $use := "" }} +{{- $inputs := "c.input" }} +{{- $inElem := .Config.ElementType }} +{{- $pools := "c.gateEvaluatorPools" }} +{{- $ge := "GateEvaluator" }} +{{- $set := "Set" }} +{{- $add := "Add" }} +{{- if .Base }} +{{- $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 = "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.{{ $use }} +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) + + // The second half of suffixEq is the eq segment for this round + eqSegment := c.suffixEq[len(c.suffixEq)/2:] + sumSize := len(eqSegment) + + 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] = {{ $pools }}[w].Get() + } + defer func() { + for w := range nbWires { + {{ $pools }}[w].Put(evaluators[w]) + } + }() + + res := make([]{{ .Config.ElementType }}, degree) + + // Input evaluations at m=1,2,...,degree + inputEvals := make([]{{ $inElem }}, degree*nbUniqueInputs) + + for h := start; h < end; h++ { + evalAt1Index := sumSize + h + for k := range nbUniqueInputs { + 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) + } + } + + 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() + } + + 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/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..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,8 @@ import ( "fmt" - "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 }}" @@ -12,14 +12,14 @@ import ( "testing" ) -func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { - poly := make(polynomial.MultiLin, len(polyInt)) +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() fieldHasher) error { + poly := make(polynomial.MultiLin{{ .ExtensionSuffix }}, len(polyInt)) for i, n := range polyInt { poly[i].SetUint64(n) } claim := singleMultilinClaim{g: poly.Clone()} - t := gkrcore.NewHashTranscript[{{ .ElementType }}](hashGenerator()) + t := {{ .NewTranscript }}(hashGenerator()) proof := sumcheckProve(&claim, t) @@ -37,14 +37,14 @@ func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash } 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], 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()) + t = {{ .NewTranscript }}(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } @@ -61,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 d94d7b9918..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,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" @@ -15,14 +14,14 @@ 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 } var ( - hsh hash.Hash + hsh fieldHasher err error ) @@ -31,12 +30,12 @@ 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) // Verification - if v, _err := sliceToElementSlice(testCaseInfo.Values); _err == nil { + if v, _err := gkrtesting.SliceToElementSlice[{{ .ElementType }}](testCaseInfo.Values); _err == nil { poly = v } else { return _err @@ -49,16 +48,16 @@ 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) } - 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 } - 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/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl index 09571eb59d..bce7024c2f 100644 --- a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -1,29 +1,28 @@ +{{ $digest := .ElementType }}{{ if .Mixed }}{{ $digest = printf "[poseidon2.BlockLen]%s" .BaseElementType }}{{ end }} import ( "fmt" - "hash" - "math/big" "{{.FieldPackagePath}}" - "{{.FieldPackagePath}}/polynomial" + {{- if .Mixed }} + "{{ .BaseFieldPackagePath }}" + "{{ .BaseFieldPackagePath }}/poseidon2" + {{- end }} + "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/internal/gkr/gkrtesting" ) -func toElement(i int64) *{{.ElementType}} { - var res {{.ElementType}} - res.SetInt64(i) - return &res -} +// fieldHasher is the hasher the transcript of this field's GKR runs on. +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 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) (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 newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -31,111 +30,42 @@ 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 -} +func (m *messageCounter) Reset() { m.state = m.startState } -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) WriteElement({{ .BaseElementType }}) { m.state += m.step } -func (m *messageCounter) Reset() { - m.state = m.startState +func (m *messageCounter) WriteElements(e ...{{ .BaseElementType }}) { + m.state += int64(len(e)) * m.step } -func (m *messageCounter) Size() int { - return messageCounterBlockSize -} +func (m *messageCounter) PadBlock() {} -func (m *messageCounter) BlockSize() int { - return messageCounterBlockSize +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 newMessageCounter(startState, step int) hash.Hash { - transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} - return transcript -} +func (m *messageCounter) BlockLen() int { return 1 } -func newMessageCounterGenerator(startState, step int) func() hash.Hash { - return func() hash.Hash { - return newMessageCounter(startState, step) - } +func newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(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 *{{.ElementType}}, value interface{}) (*{{.ElementType}}, 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 {{.ElementType}} - z.SetBigInt(r.Num()) - denom.SetBigInt(r.Denom()) - denom.Inverse(&denom) - z.Mul(z, &denom) - return z, nil -} - -func sliceToElementSlice[T any](slice []T) ([]{{.ElementType}}, error) { - elementSlice := make([]{{.ElementType}}, len(slice)) - for i, v := range slice { - if _, err := setElement(&elementSlice[i], v); err != nil { - return nil, err - } - } - return elementSlice, nil -} - -func sliceEquals(a []{{.ElementType}}, b []{{.ElementType}}) 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()) - } +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { + return newMessageCounter(startState, step) } - 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 -} \ No newline at end of file diff --git a/internal/gkr/bls12-377/blueprints/blueprint.go b/internal/gkr/bls12-377/blueprints/blueprint.go index 19fe8fe26c..42168d0000 100644 --- a/internal/gkr/bls12-377/blueprints/blueprint.go +++ b/internal/gkr/bls12-377/blueprints/blueprint.go @@ -259,7 +259,10 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BLS12_377") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) + if err != nil { + return err + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +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[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteElement(challenge) } // Call the BLS12_377-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bls12-377/evaluator.go b/internal/gkr/bls12-377/evaluator.go index 706a0e8012..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" @@ -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 @@ -124,16 +124,16 @@ 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 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/blueprints/blueprint.go b/internal/gkr/bls12-381/blueprints/blueprint.go index 035587f952..1043f58ec1 100644 --- a/internal/gkr/bls12-381/blueprints/blueprint.go +++ b/internal/gkr/bls12-381/blueprints/blueprint.go @@ -259,7 +259,10 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BLS12_381") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) + if err != nil { + return err + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +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[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteElement(challenge) } // Call the BLS12_381-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bls12-381/evaluator.go b/internal/gkr/bls12-381/evaluator.go index eb55f03e2b..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" @@ -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 @@ -124,16 +124,16 @@ 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 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/blueprints/blueprint.go b/internal/gkr/bn254/blueprints/blueprint.go index 455ecf4722..1aa3c0f0e8 100644 --- a/internal/gkr/bn254/blueprints/blueprint.go +++ b/internal/gkr/bn254/blueprints/blueprint.go @@ -259,7 +259,10 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BN254") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) + if err != nil { + return err + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +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[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteElement(challenge) } // Call the BN254-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bn254/evaluator.go b/internal/gkr/bn254/evaluator.go index fef1993135..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" @@ -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 @@ -124,16 +124,16 @@ 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 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/blueprints/blueprint.go b/internal/gkr/bw6-761/blueprints/blueprint.go index 92aca944f6..830a9ce0a7 100644 --- a/internal/gkr/bw6-761/blueprints/blueprint.go +++ b/internal/gkr/bw6-761/blueprints/blueprint.go @@ -259,7 +259,10 @@ func (b *BlueprintProve) Solve(s constraint.Solver[constraint.U64], inst constra } // Create hasher and write base challenges - hsh := hash.NewHash(b.HashName + "_BW6_761") + hsh, err := hash.NewFieldHasher[fr.Element, fr.Element](b.HashName) + if err != nil { + return err + } // Read initial challenges from instruction calldata (parse dynamically, no metadata) // Format: [0]=totalSize, [1...]=challenge linear expressions @@ -271,8 +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[:]) - challengeBytes := challenge.Bytes() - hsh.Write(challengeBytes[:]) + hsh.WriteElement(challenge) } // Call the BW6_761-specific Prove function (assignments already WireAssignment type) diff --git a/internal/gkr/bw6-761/evaluator.go b/internal/gkr/bw6-761/evaluator.go index 8ce1563c31..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" @@ -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 @@ -124,16 +124,16 @@ 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 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/gkr.go b/internal/gkr/gkr.go index 5a8aa55b05..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-- { @@ -353,37 +354,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 +368,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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + return nil, err + } identity := identityGate() reader := variablesReader(serializedProof) diff --git a/internal/gkr/gkr_test.go b/internal/gkr/gkr_test.go index e2603677af..b7fec5d9c0 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" @@ -262,11 +263,12 @@ 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()) 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) } } } @@ -281,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 { @@ -354,4 +356,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/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 b27bb9927d..29d72f0b68 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]) + + subtrahends := new(big.Int) + nbKept := 1 + for _, v := range operands[1:] { + if c, ok := constantValue(v); ok { + subtrahends.Add(subtrahends, c) + } else { + operands[nbKept] = v + nbKept++ + } + } + hasConst := nbKept < len(operands) + operands = operands[:nbKept] + + if minuendIsConst { + diff := minuend.Sub(minuend, subtrahends) + if nbKept == 1 { + return diff + } + operands[0] = diff + } else if hasConst { + operands = append(operands, subtrahends) + } + 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.go b/internal/gkr/gkrcore/schedule.go index 991a987064..3137b08480 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,35 @@ func CollectOutgoingEvalPoints[F any](level *constraint.GkrSkipLevel, levelI int outgoingEvalPoints[levelI] = outPoints 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. +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 0, ErrNbInstancesUndetermined + } + } 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/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/gkrcore/transcript.go b/internal/gkr/gkrcore/transcript.go index 929a63ac45..dedd78bec0 100644 --- a/internal/gkr/gkrcore/transcript.go +++ b/internal/gkr/gkrcore/transcript.go @@ -1,54 +1,110 @@ 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 { +// 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 +} + +// 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() +} + +// 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 - 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 { + SetBase([]F) *E +}, 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 + 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 - SetBytes([]byte) *E - Marshal() []byte -}](h hash.Hash) *HashTranscript[E, PE] { + SetBase([]F) *E +}, F any, D hash.ArrayBlock[F]](h hash.FieldHasher[F, D]) *ExtensionTranscript[E, PE, F, D] { var zero E - return &HashTranscript[E, PE]{h: h, size: len(PE(&zero).Marshal())} + 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)} } -// Bind writes field elements to the transcript as bindings for the next challenge. -func (t *HashTranscript[E, PE]) Bind(elements ...E) { +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 *ExtensionTranscript[E, PE, F, D]) Absorb(elements ...E) { if len(elements) == 0 { return } - for i := range elements { - t.h.Write(PE(&elements[i]).Marshal()) + for _, e := range elements { + t.write(e) } - t.bound = true + t.absorbed = 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}) +// 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 *ExtensionTranscript[E, PE, F, D]) Squeeze(elements ...E) E { + t.Absorb(elements...) + if !t.absorbed { + var zero E + t.write(zero) + } + t.absorbed = false + t.h.PadBlock() + d := t.h.SumDigest() + t.buf = t.buf[:t.degree] + for i := range t.buf { + t.buf[i] = d[i] } - t.bound = false var res E - PE(&res).SetBytes(t.h.Sum(nil)[:t.size]) - return res + return *PE(&res).SetBase(t.buf) } diff --git a/internal/gkr/gkrcore/transcript_test.go b/internal/gkr/gkrcore/transcript_test.go new file mode 100644 index 0000000000..3654b84611 --- /dev/null +++ b/internal/gkr/gkrcore/transcript_test.go @@ -0,0 +1,38 @@ +package gkrcore + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" + "github.com/stretchr/testify/assert" +) + +// TestExtensionTranscriptBindsWholeElement checks that every coordinate of an absorbed extension +// element affects the next challenge. +func TestExtensionTranscriptBindsWholeElement(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 := NewExtensionTranscript[extensions.E6](poseidon2.NewHasher()) + transcript.Absorb(e) + return transcript.Squeeze() + } + + 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/elements.go b/internal/gkr/gkrtesting/elements.go new file mode 100644 index 0000000000..3c30b79752 --- /dev/null +++ b/internal/gkr/gkrtesting/elements.go @@ -0,0 +1,98 @@ +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 + 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 { + 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/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 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/internal/gkr/koalabear/evaluator.go b/internal/gkr/koalabear/evaluator.go new file mode 100644 index 0000000000..ab4b51aa27 --- /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 gkrkoalabear + +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. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. +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, 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 +} + +// 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. +// The evaluators allocate their element slices from the polynomial.Pool the pool was given, if any. +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..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" @@ -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 @@ -124,16 +124,16 @@ 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 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/rational/gkr.go b/internal/gkr/rational/gkr.go index 7fb60888fe..87c45c9790 100644 --- a/internal/gkr/rational/gkr.go +++ b/internal/gkr/rational/gkr.go @@ -3,37 +3,37 @@ // Code generated by gnark DO NOT EDIT -package gkr +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" "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. @@ -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) @@ -63,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 @@ -77,13 +91,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.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, 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]), @@ -133,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 } } @@ -145,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) { @@ -158,18 +173,22 @@ 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 } // 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) { +// 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[rational.Element, rational.Element]) (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-- { @@ -230,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) { @@ -246,18 +265,26 @@ 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 } // 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) { +// 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, +// 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") + } r := newResources(c, schedule, logNbInstances, hasher) if len(proof) != len(schedule)+1 { @@ -294,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-- { @@ -347,6 +374,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 { @@ -356,6 +384,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 { @@ -375,6 +404,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 @@ -390,3 +422,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 && !errors.Is(err, gkrcore.ErrNbInstancesUndetermined) { + 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 2951a66c7e..37ca8396d0 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" @@ -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, *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,18 +45,18 @@ 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() 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 @@ -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) @@ -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]) } @@ -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,14 +349,14 @@ 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 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 +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()) @@ -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 @@ -695,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/sumcheck_test.go b/internal/gkr/rational/sumcheck_test.go index 60ac5ac522..892913b36f 100644 --- a/internal/gkr/rational/sumcheck_test.go +++ b/internal/gkr/rational/sumcheck_test.go @@ -3,13 +3,13 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "fmt" - "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" @@ -18,14 +18,14 @@ 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) } 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() hash } 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], 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()) + t = gkrcore.NewNativeTranscript(hashGenerator()) if sumcheckVerify(lazyClaim, proof, lazyClaim.claimedSum, 1, t) == nil { return fmt.Errorf("bad proof accepted") } @@ -67,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 c79c150a2f..b8de5093d2 100644 --- a/internal/gkr/rational/sumcheck_test_vector_gen.go +++ b/internal/gkr/rational/sumcheck_test_vector_gen.go @@ -3,12 +3,11 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "encoding/json" "fmt" - "hash" "math/bits" "os" "path/filepath" @@ -23,14 +22,14 @@ 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 } var ( - hsh hash.Hash + hsh fieldHasher err error ) @@ -39,12 +38,12 @@ 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) // 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 @@ -57,16 +56,16 @@ 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) } - 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 } - 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_gen.go b/internal/gkr/rational/test_vector_gen.go index 4aa3eba6bd..c2d968614b 100644 --- a/internal/gkr/rational/test_vector_gen.go +++ b/internal/gkr/rational/test_vector_gen.go @@ -3,12 +3,11 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "encoding/json" "fmt" - "hash" "os" "path/filepath" "reflect" @@ -16,6 +15,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 +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 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") @@ -178,7 +178,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 } } @@ -190,7 +190,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 } } @@ -200,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 @@ -228,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 } @@ -272,7 +272,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 } @@ -285,7 +285,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..3bacf8657d 100644 --- a/internal/gkr/rational/test_vector_utils.go +++ b/internal/gkr/rational/test_vector_utils.go @@ -3,35 +3,29 @@ // Code generated by gnark DO NOT EDIT -package gkr +package gkrrational import ( "fmt" - "hash" - "math/big" + "github.com/consensys/gnark-crypto/hash" "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 { - var res rational.Element - res.SetInt64(i) - return &res -} +// 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. +// 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) (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 newMessageCounter(int(d["val"].(float64)), 0), nil default: return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) } @@ -39,111 +33,38 @@ 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 } -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 rational.Element - res.SetInt64(int64(resI)) - return (&res).Marshal() -} +func (m *messageCounter) Reset() { m.state = m.startState } -func (m *messageCounter) Reset() { - m.state = m.startState -} +func (m *messageCounter) WriteElement(rational.Element) { m.state += m.step } -func (m *messageCounter) Size() int { - return messageCounterBlockSize +func (m *messageCounter) WriteElements(e ...rational.Element) { + m.state += int64(len(e)) * m.step } -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) - } -} +func (m *messageCounter) PadBlock() {} -// 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 (m *messageCounter) SumDigest() rational.Element { + var res rational.Element + res.SetInt64(m.state + m.step) + return res } -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 (m *messageCounter) BlockLen() int { return 1 } -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 newMessageCounter(startState, step int) fieldHasher { + return &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} } -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) - } +func newMessageCounterGenerator(startState, step int) func() fieldHasher { + return func() fieldHasher { + return newMessageCounter(startState, step) } - return nil } 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)]) } 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 13ef3910c5..e743661b00 100644 --- a/std/gkrapi/api_test.go +++ b/std/gkrapi/api_test.go @@ -2,17 +2,17 @@ package gkrapi import ( "fmt" - "hash" "math/big" "math/rand" - "slices" "strconv" - "strings" "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" @@ -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)) } @@ -567,33 +567,33 @@ 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 (h constFieldPseudoHash[F]) Reset() {} -func (c constBytesPseudoHash) Sum([]byte) []byte { - return slices.Clone(c) -} +func (h constFieldPseudoHash[F]) WriteElement(F) {} -func (c constBytesPseudoHash) Reset() { -} +func (h constFieldPseudoHash[F]) WriteElements(...F) {} -func (c constBytesPseudoHash) Size() int { - return len(c) -} +func (h constFieldPseudoHash[F]) PadBlock() {} -func (c constBytesPseudoHash) BlockSize() int { - return len(c) +func (h constFieldPseudoHash[F]) SumDigest() F { + return h.digest } -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 +func (h constFieldPseudoHash[F]) BlockLen() int { return 1 } + +// 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) { + var h constFieldPseudoHash[F] + PF(&h.digest).SetBigInt(big.NewInt(v)) + gcHash.RegisterFieldHasher(name, func() gcHash.FieldHasher[F, F] { + return h + }) } func init() { @@ -603,12 +603,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) + registerConstPseudoHash[frbls12377.Element](name, v) + registerConstPseudoHash[frbls12381.Element](name, v) + registerConstPseudoHash[frbw6761.Element](name, v) } } @@ -628,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 } @@ -686,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}, @@ -716,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 } @@ -743,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/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 } 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 } 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