diff --git a/internal/smallfields/circuits_test.go b/internal/smallfields/circuits_test.go index 8673811c0f..aeaa609325 100644 --- a/internal/smallfields/circuits_test.go +++ b/internal/smallfields/circuits_test.go @@ -1,20 +1,28 @@ package smallfields_test import ( + "crypto/rand" "fmt" "math/big" "testing" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/field/babybear" - "github.com/consensys/gnark-crypto/field/goldilocks" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/smallfields/tinyfield" + "github.com/consensys/gnark/internal/widecommitter" + "github.com/consensys/gnark/std/algebra/emulated/sw_bn254" + "github.com/consensys/gnark/std/math/emulated" + "github.com/consensys/gnark/std/math/emulated/emparams" "github.com/consensys/gnark/test" ) +var testSmallField = koalabear.Modulus() + type NativeCircuit struct { A frontend.Variable `gnark:",public"` B frontend.Variable `gnark:",secret"` @@ -31,7 +39,6 @@ var testCases = []struct { modulus *big.Int supportsCompile bool }{ - {"goldilocks", goldilocks.Modulus(), false}, {"tinyfield", tinyfield.Modulus(), true}, {"babybear", babybear.Modulus(), true}, {"koalabear", koalabear.Modulus(), true}, @@ -59,9 +66,8 @@ func TestNativeCircuitCompileAndSolve(t *testing.T) { assignment := &NativeCircuit{A: 2, B: 4} wit, err := frontend.NewWitness(assignment, tc.modulus) assert.NoError(err) - solution, err := ccs.Solve(wit) + err = ccs.IsSolved(wit) assert.NoError(err) - _ = solution }, fmt.Sprintf("ccs=r1cs/field=%s", tc.name)) assert.Run(func(assert *test.Assert) { @@ -70,9 +76,145 @@ func TestNativeCircuitCompileAndSolve(t *testing.T) { assignment := &NativeCircuit{A: 2, B: 4} wit, err := frontend.NewWitness(assignment, tc.modulus) assert.NoError(err) - solution, err := ccs.Solve(wit) + err = ccs.IsSolved(wit) assert.NoError(err) - _ = solution }, fmt.Sprintf("ccs=scs/field=%s", tc.name)) } } + +type EmulatedCircuit[T emulated.FieldParams] struct { + A emulated.Element[T] `gnark:",public"` + B emulated.Element[T] `gnark:",secret"` +} + +func (c *EmulatedCircuit[T]) Define(api frontend.API) error { + f, err := emulated.NewField[T](api) + if err != nil { + return err + } + res := f.Mul(&c.A, &c.A) + f.AssertIsEqual(res, &c.B) + return nil +} + +func TestEmulatedCircuit(t *testing.T) { + assert := test.NewAssert(t) + + a, err := rand.Int(rand.Reader, emparams.BN254Fp{}.Modulus()) + assert.NoError(err) + b := new(big.Int).Mul(a, a) + b.Mod(b, emparams.BN254Fp{}.Modulus()) + + err = test.IsSolved(&EmulatedCircuit[emparams.BN254Fp]{}, &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](a), B: emulated.ValueOf[emparams.BN254Fp](b)}, ecc.BN254.ScalarField()) + assert.NoError(err) + + err = test.IsSolved(&EmulatedCircuit[emparams.BN254Fp]{}, &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](a), B: emulated.ValueOf[emparams.BN254Fp](b)}, testSmallField) + assert.NoError(err) + + // assert that when the compiled doesn't have specific support for small fields (rangechecker and widecommit), then it would fail + err = test.IsSolved(&EmulatedCircuit[emparams.BN254Fp]{}, &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](a), B: emulated.ValueOf[emparams.BN254Fp](b)}, testSmallField, test.WithNoSmallFieldCompatibility()) + assert.Error(err) +} + +func TestCompileEmulatedCircuit(t *testing.T) { + assert := test.NewAssert(t) + f := testSmallField + + assignment := &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](2), B: emulated.ValueOf[emparams.BN254Fp](4)} + + ccs, err := frontend.CompileU32(f, widecommitter.From(scs.NewBuilder), &EmulatedCircuit[emparams.BN254Fp]{}) + assert.NoError(err) + + w, err := frontend.NewWitness(assignment, f) + assert.NoError(err) + + err = ccs.IsSolved(w) + assert.NoError(err) + + ccs2, err := frontend.CompileU32(f, widecommitter.From(r1cs.NewBuilder), &EmulatedCircuit[emparams.BN254Fp]{}) + assert.NoError(err) + + err = ccs2.IsSolved(w) + assert.NoError(err) + + // ensure the compilation fails in case we compile over a small field but we don't have rangechecker and widecommit support + _, err = frontend.CompileU32(f, scs.NewBuilder, &EmulatedCircuit[emparams.BN254Fp]{}) + assert.Error(err) + _, err = frontend.CompileU32(f, r1cs.NewBuilder, &EmulatedCircuit[emparams.BN254Fp]{}) + assert.Error(err) +} + +type PairCircuit struct { + InG1 sw_bn254.G1Affine + InG2 sw_bn254.G2Affine + Res sw_bn254.GTEl +} + +func (c *PairCircuit) Define(api frontend.API) error { + pairing, err := sw_bn254.NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + pairing.AssertIsOnG1(&c.InG1) + pairing.AssertIsOnG2(&c.InG2) + res, err := pairing.Pair([]*sw_bn254.G1Affine{&c.InG1}, []*sw_bn254.G2Affine{&c.InG2}) + if err != nil { + return fmt.Errorf("pair: %w", err) + } + pairing.AssertIsEqual(res, &c.Res) + return nil +} + +func TestPairTestSolve(t *testing.T) { + assert := test.NewAssert(t) + p, q := randomG1G2Affines() + res, err := bn254.Pair([]bn254.G1Affine{p}, []bn254.G2Affine{q}) + assert.NoError(err) + witness := PairCircuit{ + InG1: sw_bn254.NewG1Affine(p), + InG2: sw_bn254.NewG2Affine(q), + Res: sw_bn254.NewGTEl(res), + } + err = test.IsSolved(&PairCircuit{}, &witness, testSmallField) + assert.NoError(err) + + ccs, err := frontend.CompileU32(testSmallField, widecommitter.From(scs.NewBuilder), &PairCircuit{}) + assert.NoError(err) + + w, err := frontend.NewWitness(&witness, testSmallField) + assert.NoError(err) + + err = ccs.IsSolved(w) + assert.NoError(err) + + ccs2, err := frontend.Compile(ecc.BLS12_377.ScalarField(), scs.NewBuilder, &PairCircuit{}) + assert.NoError(err) + // we define it again as the field is different + witness = PairCircuit{ + InG1: sw_bn254.NewG1Affine(p), + InG2: sw_bn254.NewG2Affine(q), + Res: sw_bn254.NewGTEl(res), + } + w2, err := frontend.NewWitness(&witness, ecc.BLS12_377.ScalarField()) + assert.NoError(err) + err = ccs2.IsSolved(w2) + assert.NoError(err) +} + +func randomG1G2Affines() (bn254.G1Affine, bn254.G2Affine) { + _, _, G1AffGen, G2AffGen := bn254.Generators() + mod := bn254.ID.ScalarField() + s1, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + s2, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + var p bn254.G1Affine + p.ScalarMultiplication(&G1AffGen, s1) + var q bn254.G2Affine + q.ScalarMultiplication(&G2AffGen, s2) + return p, q +} diff --git a/internal/widecommitter/widecommitter.go b/internal/widecommitter/widecommitter.go index 3f6dc62a66..59ce05571a 100644 --- a/internal/widecommitter/widecommitter.go +++ b/internal/widecommitter/widecommitter.go @@ -57,7 +57,7 @@ func (w *wrappedBuilder) Compiler() frontend.Compiler { } func (w *wrappedBuilder) Check(in frontend.Variable, width int) { - _, err := w.NewHint(mockedRangecheckHint, 0, width, in) + _, err := w.NewHint(mockedRangecheckHint, 1, width, in) if err != nil { panic(fmt.Sprintf("failed to check range: %v", err)) } diff --git a/std/internal/logderivarg/logderivarg.go b/std/internal/logderivarg/logderivarg.go index a52961410d..ba73040db4 100644 --- a/std/internal/logderivarg/logderivarg.go +++ b/std/internal/logderivarg/logderivarg.go @@ -148,6 +148,10 @@ func Build(api frontend.API, table Table, queries Table) error { } func randLinearCoefficients(api frontend.API, nbRow int, commitment frontend.Variable) (rowCoeffs []frontend.Variable, challenge frontend.Variable) { + if nbRow == 1 { + // to avoid initializing the hasher. + return []frontend.Variable{1}, commitment + } hasher, err := mimc.NewMiMC(api) if err != nil { panic(err) diff --git a/std/math/emulated/field.go b/std/math/emulated/field.go index 43d3c59a85..5bbf26f646 100644 --- a/std/math/emulated/field.go +++ b/std/math/emulated/field.go @@ -8,9 +8,11 @@ import ( "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/internal/kvstore" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" limbs "github.com/consensys/gnark/std/internal/limbcomposition" + "github.com/consensys/gnark/std/math/fieldextension" "github.com/consensys/gnark/std/rangecheck" "github.com/rs/zerolog" "golang.org/x/exp/constraints" @@ -23,6 +25,8 @@ import ( type Field[T FieldParams] struct { // api is the native API api frontend.API + // extensionApi is the extension API when we need to perform multiplication checks over the extension field + extensionApi fieldextension.Field // fParams carries the ring parameters fParams staticFieldParams[T] @@ -32,16 +36,14 @@ type Field[T FieldParams] struct { maxOfOnce sync.Once // constants for often used elements n, 0 and 1. Allocated only once - nConstOnce sync.Once - nConst *Element[T] - nprevConstOnce sync.Once - nprevConst *Element[T] - zeroConstOnce sync.Once - zeroConst *Element[T] - oneConstOnce sync.Once - oneConst *Element[T] - shortOneConstOnce sync.Once - shortOneConst *Element[T] + nConstOnce sync.Once + nConst *Element[T] + nprevConstOnce sync.Once + nprevConst *Element[T] + zeroConstOnce sync.Once + zeroConst *Element[T] + oneConstOnce sync.Once + oneConst *Element[T] log zerolog.Logger @@ -70,6 +72,14 @@ func NewField[T FieldParams](native frontend.API) (*Field[T], error) { checker: rangecheck.New(native), fParams: newStaticFieldParams[T](native.Compiler().Field()), } + if smallfields.IsSmallField(native.Compiler().Field()) { + f.log.Debug().Msg("using small native field, multiplication checks will be performed in extension field") + extapi, err := fieldextension.NewExtension(native) + if err != nil { + return nil, fmt.Errorf("extension field: %w", err) + } + f.extensionApi = extapi + } // ensure prime is correctly set if f.fParams.IsPrime() { diff --git a/std/math/emulated/field_mul.go b/std/math/emulated/field_mul.go index 02226e6a7b..15269a5509 100644 --- a/std/math/emulated/field_mul.go +++ b/std/math/emulated/field_mul.go @@ -9,6 +9,7 @@ import ( "github.com/consensys/gnark/frontend" limbs "github.com/consensys/gnark/std/internal/limbcomposition" + "github.com/consensys/gnark/std/math/fieldextension" "github.com/consensys/gnark/std/multicommit" ) @@ -19,6 +20,11 @@ import ( // checks. // // Currently used for multiplication and multivariate evaluation checks. +// +// The methods [evalRound1], [evalRound2] and [check] may receive as inputs +// either [frontend.Variable] or [fieldextension.Element]. The +// implementation should differentiate on the different input types and use the +// appropriate API (native or extension). type deferredChecker interface { // toCommit outputs the variable which should be committed to. The checker // then uses the commitment to obtain the verifier challenge for the @@ -166,9 +172,37 @@ func (mc *mulCheck[T]) check(api frontend.API, peval, coef frontend.Variable) { if mc.p != nil { peval = mc.p.evaluation } - ls := api.Mul(mc.a.evaluation, mc.b.evaluation) - rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) - api.AssertIsEqual(ls, rs) + // we either have to perform the equality check in the native field or in + // the extension field. It was already determined at the [Field] + // initialization time which kind of check needs to be done. + if mc.f.extensionApi == nil { + ls := api.Mul(mc.a.evaluation, mc.b.evaluation) + rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) + api.AssertIsEqual(ls, rs) + } else { + // here we use the fact that [frontend.Variable] is defined as any, but + // we have actually provided [ExtensionVariable]. We type assert to be + // able to use the fieldextension API. + // + // the computations are same as in the previous conditional block, but + // only in the extension. + aext := mc.a.evaluation.(fieldextension.Element) + bext := mc.b.evaluation.(fieldextension.Element) + ls := mc.f.extensionApi.Mul(aext, bext) + + rext := mc.r.evaluation.(fieldextension.Element) + pevalext := peval.(fieldextension.Element) + cext := mc.c.evaluation.(fieldextension.Element) + kext := mc.k.evaluation.(fieldextension.Element) + coefext := coef.(fieldextension.Element) + pkext := mc.f.extensionApi.Mul(pevalext, kext) + ccoefext := mc.f.extensionApi.Mul(coefext, cext) + + rs := mc.f.extensionApi.Add(rext, pkext) + rs = mc.f.extensionApi.Add(rs, ccoefext) + + mc.f.extensionApi.AssertIsEqual(ls, rs) + } } // cleanEvaluations cleans the cached evaluation values. This is necessary for @@ -255,6 +289,18 @@ func (f *Field[T]) evalWithChallenge(a *Element[T], at []frontend.Variable) *Ele if len(at) < len(a.Limbs)-1 { panic("evaluation powers less than limbs") } + var sum frontend.Variable + if f.extensionApi != nil { + sum = f.evalWithChallengeExtension(a, at) + } else { + sum = f.evalWithChallengeNative(a, at) + } + a.isEvaluated = true + a.evaluation = sum + return a +} + +func (f *Field[T]) evalWithChallengeNative(a *Element[T], at []frontend.Variable) frontend.Variable { var sum frontend.Variable = 0 if len(a.Limbs) > 0 { sum = f.api.Mul(a.Limbs[0], 1) // copy because we use MulAcc @@ -262,9 +308,26 @@ func (f *Field[T]) evalWithChallenge(a *Element[T], at []frontend.Variable) *Ele for i := 1; i < len(a.Limbs); i++ { sum = f.api.MulAcc(sum, a.Limbs[i], at[i-1]) } - a.isEvaluated = true - a.evaluation = sum - return a + return sum +} + +func (f *Field[T]) evalWithChallengeExtension(a *Element[T], at []frontend.Variable) frontend.Variable { + // even though at is []frontend.Variable, then we abuse the fact that + // frontend.Variable is defined as any and at is []ExtensionVariable. We + // type assert it. + atext := make([]fieldextension.Element, len(at)) + for i := 0; i < len(at); i++ { + atext[i] = at[i].(fieldextension.Element) + } + sum := f.extensionApi.Zero() + if len(a.Limbs) > 0 { + sum = f.extensionApi.AsExtensionVariable(a.Limbs[0]) + } + for i := 1; i < len(a.Limbs); i++ { + toAdd := f.extensionApi.MulByElement(atext[i-1], a.Limbs[i]) + sum = f.extensionApi.Add(sum, toAdd) + } + return sum } // performDeferredChecks should be deferred to actually perform all the @@ -289,43 +352,91 @@ func (f *Field[T]) performDeferredChecks(api frontend.API) error { for i := range f.deferredChecks { toCommit = append(toCommit, f.deferredChecks[i].toCommit()...) } - // we give all the inputs as inputs to obtain random verifier challenge. - multicommit.WithCommitment(api, func(api frontend.API, commitment frontend.Variable) error { - // for efficiency, we compute all powers of the challenge as slice at. - coefsLen := int(f.fParams.NbLimbs()) - for i := range f.deferredChecks { - coefsLen = max(coefsLen, f.deferredChecks[i].maxLen()) - } - at := make([]frontend.Variable, coefsLen) - at[0] = commitment - for i := 1; i < len(at); i++ { - at[i] = api.Mul(at[i-1], commitment) - } - // evaluate all r, k, c - for i := range f.deferredChecks { - f.deferredChecks[i].evalRound1(at) - } - // assuming r is input to some other multiplication, then is already evaluated - for i := range f.deferredChecks { - f.deferredChecks[i].evalRound2(at) - } - // evaluate p(X) at challenge - pval := f.evalWithChallenge(f.Modulus(), at) - // compute (2^t-X) at challenge - coef := big.NewInt(1) - coef.Lsh(coef, f.fParams.BitsPerLimb()) - ccoef := api.Sub(coef, commitment) - // verify all mulchecks - for i := range f.deferredChecks { - f.deferredChecks[i].check(api, pval.evaluation, ccoef) - } - // clean cached evaluation. Helps in case we compile the same circuit - // multiple times. - for i := range f.deferredChecks { - f.deferredChecks[i].cleanEvaluations() - } - return nil - }, toCommit...) + if f.extensionApi == nil { + // we give all the inputs as inputs to obtain random verifier challenge. + multicommit.WithCommitment(api, func(api frontend.API, commitment frontend.Variable) error { + // for efficiency, we compute all powers of the challenge as slice at. + coefsLen := int(f.fParams.NbLimbs()) + for i := range f.deferredChecks { + coefsLen = max(coefsLen, f.deferredChecks[i].maxLen()) + } + at := make([]frontend.Variable, coefsLen) + at[0] = commitment + for i := 1; i < len(at); i++ { + at[i] = api.Mul(at[i-1], commitment) + } + // evaluate all r, k, c + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound1(at) + } + // assuming r is input to some other multiplication, then is already evaluated + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound2(at) + } + // evaluate p(X) at challenge + pval := f.evalWithChallenge(f.Modulus(), at) + // compute (2^t-X) at challenge + coef := big.NewInt(1) + coef.Lsh(coef, f.fParams.BitsPerLimb()) + ccoef := api.Sub(coef, commitment) + // verify all mulchecks + for i := range f.deferredChecks { + f.deferredChecks[i].check(api, pval.evaluation, ccoef) + } + // clean cached evaluation. Helps in case we compile the same circuit + // multiple times. + for i := range f.deferredChecks { + f.deferredChecks[i].cleanEvaluations() + } + return nil + }, toCommit...) + } else { + // this is the same as above, but we have challenges in the extension + // field. The commitment argument below is actually extension field + // element, but we give it as []frontend.Variable for interface + // compatibility. + multicommit.WithWideCommitment(api, func(api frontend.API, commitment []frontend.Variable) error { + // for efficiency, we compute all powers of the challenge as slice at. + coefsLen := int(f.fParams.NbLimbs()) + for i := range f.deferredChecks { + coefsLen = max(coefsLen, f.deferredChecks[i].maxLen()) + } + at := make([]fieldextension.Element, coefsLen) + at[0] = commitment + for i := 1; i < len(at); i++ { + at[i] = f.extensionApi.Mul(at[i-1], commitment) + } + atv := make([]frontend.Variable, len(at)) + for i := range at { + atv[i] = at[i] + } + // evaluate all r, k, c + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound1(atv) + } + // assuming r is input to some other multiplication, then is already evaluated + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound2(atv) + } + // evaluate p(X) at challenge + pval := f.evalWithChallenge(f.Modulus(), atv) + // compute (2^t-X) at challenge + coef := big.NewInt(1) + coef.Lsh(coef, f.fParams.BitsPerLimb()) + coefext := f.extensionApi.AsExtensionVariable(coef) + ccoef := f.extensionApi.Sub(coefext, commitment) + // verify all mulchecks + for i := range f.deferredChecks { + f.deferredChecks[i].check(api, pval.evaluation, ccoef) + } + // clean cached evaluation. Helps in case we compile the same circuit + // multiple times. + for i := range f.deferredChecks { + f.deferredChecks[i].cleanEvaluations() + } + return nil + }, f.extensionApi.Degree(), toCommit...) + } return nil } @@ -830,19 +941,58 @@ func (mc *mvCheck[T]) evalRound2(at []frontend.Variable) { } } +// check checks that the multivariate polynomial f(x1(ch), x2(ch), ...) = r(ch) +// + k(ch)*p(ch) + (2^t-ch) c(ch) holds. As p and (2^t-ch) are same over all +// checks then we get them as arguments to this method. func (mc *mvCheck[T]) check(api frontend.API, peval, coef frontend.Variable) { - ls := frontend.Variable(0) - for i, term := range mc.mv.Terms { - termProd := frontend.Variable(mc.mv.Coefficients[i]) - for i, pow := range term { - for j := 0; j < pow; j++ { - termProd = api.Mul(termProd, mc.vals[i].evaluation) + // we either have to perform the equality check in the native field or in + // the extension field. It was already determined at the [Field] + // initialization time which kind of check needs to be done. + if mc.f.extensionApi == nil { + ls := frontend.Variable(0) + for i, term := range mc.mv.Terms { + termProd := frontend.Variable(mc.mv.Coefficients[i]) + for i, pow := range term { + for j := 0; j < pow; j++ { + termProd = api.Mul(termProd, mc.vals[i].evaluation) + } } + ls = api.Add(ls, termProd) } - ls = api.Add(ls, termProd) + rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) + api.AssertIsEqual(ls, rs) + } else { + // here we use the fact that [frontend.Variable] is defined as any, but + // we have actually provided [ExtensionVariable]. We type assert to be + // able to use the fieldextension API. + // + // the computations are same as in the previous conditional block, but + // only in the extension. + ls := mc.f.extensionApi.Zero() + for i, term := range mc.mv.Terms { + termProd := mc.f.extensionApi.AsExtensionVariable(mc.mv.Coefficients[i]) + for i, pow := range term { + for j := 0; j < pow; j++ { + valsexti := mc.vals[i].evaluation.(fieldextension.Element) + termProd = mc.f.extensionApi.Mul(termProd, valsexti) + } + } + ls = mc.f.extensionApi.Add(ls, termProd) + } + rext := mc.r.evaluation.(fieldextension.Element) + pevalext := peval.(fieldextension.Element) + kext := mc.k.evaluation.(fieldextension.Element) + cext := mc.c.evaluation.(fieldextension.Element) + coefext := coef.(fieldextension.Element) + + pkext := mc.f.extensionApi.Mul(pevalext, kext) + ccoefext := mc.f.extensionApi.Mul(coefext, cext) + + rs := mc.f.extensionApi.Add(rext, pkext) + rs = mc.f.extensionApi.Add(rs, ccoefext) + + mc.f.extensionApi.AssertIsEqual(ls, rs) } - rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) - api.AssertIsEqual(ls, rs) } func (mc *mvCheck[T]) cleanEvaluations() {