diff --git a/go.mod b/go.mod index 4805c741f0..7879abea87 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/consensys/bavard v0.1.13 github.com/consensys/compress v0.2.5 - github.com/consensys/gnark-crypto v0.12.2-0.20240504013751-564b6f724c3b + github.com/consensys/gnark-crypto v0.12.2-0.20240703135258-5d8b5fab1afb github.com/fxamacker/cbor/v2 v2.5.0 github.com/google/go-cmp v0.5.9 github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b diff --git a/go.sum b/go.sum index 99806d860f..e3ee06ca09 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/Yj github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/compress v0.2.5 h1:gJr1hKzbOD36JFsF1AN8lfXz1yevnJi1YolffY19Ntk= github.com/consensys/compress v0.2.5/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.12.2-0.20240504013751-564b6f724c3b h1:tu0NaVr64o6vXzy9rYSK/LCZXmS+u/k9eP1F8OtRUWQ= -github.com/consensys/gnark-crypto v0.12.2-0.20240504013751-564b6f724c3b/go.mod h1:wKqwsieaKPThcFkHe0d0zMsbHEUWFmZcG7KBCse210o= +github.com/consensys/gnark-crypto v0.12.2-0.20240703135258-5d8b5fab1afb h1:LMfC1GSeYv1TKp3zNIuwYNEqb9RCVrMkCV4Y9k5ZJ6o= +github.com/consensys/gnark-crypto v0.12.2-0.20240703135258-5d8b5fab1afb/go.mod h1:wKqwsieaKPThcFkHe0d0zMsbHEUWFmZcG7KBCse210o= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/stats/latest.stats b/internal/stats/latest.stats index f4d2c47616..a38756342e 100644 Binary files a/internal/stats/latest.stats and b/internal/stats/latest.stats differ diff --git a/std/algebra/emulated/fields_bn254/e12.go b/std/algebra/emulated/fields_bn254/e12.go index fb50d82676..22218351f7 100644 --- a/std/algebra/emulated/fields_bn254/e12.go +++ b/std/algebra/emulated/fields_bn254/e12.go @@ -114,6 +114,65 @@ func (e Ext12) Square(x *E12) *E12 { } } +// Granger--Scott cyclotomic square +func (e Ext12) CyclotomicSquare(x *E12) *E12 { + t0 := e.Ext2.Square(&x.C1.B1) + t1 := e.Ext2.Square(&x.C0.B0) + t6 := e.Ext2.Add(&x.C1.B1, &x.C0.B0) + t6 = e.Ext2.Square(t6) + t6 = e.Ext2.Sub(t6, t0) + t6 = e.Ext2.Sub(t6, t1) + t2 := e.Ext2.Square(&x.C0.B2) + t3 := e.Ext2.Square(&x.C1.B0) + t7 := e.Ext2.Add(&x.C0.B2, &x.C1.B0) + t7 = e.Ext2.Square(t7) + t7 = e.Ext2.Sub(t7, t2) + t7 = e.Ext2.Sub(t7, t3) + t4 := e.Ext2.Square(&x.C1.B2) + t5 := e.Ext2.Square(&x.C0.B1) + t8 := e.Ext2.Add(&x.C1.B2, &x.C0.B1) + t8 = e.Ext2.Square(t8) + t8 = e.Ext2.Sub(t8, t4) + t8 = e.Ext2.Sub(t8, t5) + t8 = e.Ext2.MulByNonResidue(t8) + t0 = e.Ext2.MulByNonResidue(t0) + t0 = e.Ext2.Add(t0, t1) + t2 = e.Ext2.MulByNonResidue(t2) + t2 = e.Ext2.Add(t2, t3) + t4 = e.Ext2.MulByNonResidue(t4) + t4 = e.Ext2.Add(t4, t5) + z00 := e.Ext2.Sub(t0, &x.C0.B0) + z00 = e.Ext2.Double(z00) + z00 = e.Ext2.Add(z00, t0) + z01 := e.Ext2.Sub(t2, &x.C0.B1) + z01 = e.Ext2.Double(z01) + z01 = e.Ext2.Add(z01, t2) + z02 := e.Ext2.Sub(t4, &x.C0.B2) + z02 = e.Ext2.Double(z02) + z02 = e.Ext2.Add(z02, t4) + z10 := e.Ext2.Add(t8, &x.C1.B0) + z10 = e.Ext2.Double(z10) + z10 = e.Ext2.Add(z10, t8) + z11 := e.Ext2.Add(t6, &x.C1.B1) + z11 = e.Ext2.Double(z11) + z11 = e.Ext2.Add(z11, t6) + z12 := e.Ext2.Add(t7, &x.C1.B2) + z12 = e.Ext2.Double(z12) + z12 = e.Ext2.Add(z12, t7) + return &E12{ + C0: E6{ + B0: *z00, + B1: *z01, + B2: *z02, + }, + C1: E6{ + B0: *z10, + B1: *z11, + B2: *z12, + }, + } +} + func (e Ext12) AssertIsEqual(x, y *E12) { e.Ext6.AssertIsEqual(&x.C0, &y.C0) e.Ext6.AssertIsEqual(&x.C1, &y.C1) diff --git a/std/algebra/emulated/fields_bn254/e12_pairing.go b/std/algebra/emulated/fields_bn254/e12_pairing.go index 9f896630ba..41acd1dfbb 100644 --- a/std/algebra/emulated/fields_bn254/e12_pairing.go +++ b/std/algebra/emulated/fields_bn254/e12_pairing.go @@ -4,6 +4,76 @@ import ( "github.com/consensys/gnark/std/math/emulated" ) +func (e Ext12) nSquareGS(z *E12, n int) *E12 { + for i := 0; i < n; i++ { + z = e.CyclotomicSquare(z) + } + return z +} + +// Exponentiation by U=6u+2 where t is the seed u=4965661367192848881 +func (e Ext12) ExpByU(x *E12) *E12 { + // ExpByU computation is derived from the addition chain: + // _10 = 2*1 + // _11 = 1 + _10 + // _110 = 2*_11 + // _111 = 1 + _110 + // _1100 = 2*_110 + // _1111 = _11 + _1100 + // _1100000 = _1100 << 3 + // _1100111 = _111 + _1100000 + // i22 = ((_1100111 << 2 + 1) << 5 + _1111) << 3 + // i38 = ((1 + i22) << 4 + _111) << 9 + _111 + // i50 = 2*((i38 << 4 + _11) << 5 + _1111) + // i61 = ((1 + i50) << 5 + _111) << 3 + _11 + // i75 = ((i61 << 6 + _111) << 4 + _111) << 2 + // return ((1 + i75) << 2 + 1) << 3 + // + // Operations: 64 squares 18 multiplies + // + // Generated by github.com/mmcloughlin/addchain v0.4.0. + + z := e.Square(x) + t0 := e.Mul(x, z) + t1 := e.Square(t0) + z = e.Mul(x, t1) + t2 := e.Square(t1) + t1 = e.Mul(t0, t2) + t2 = e.nSquareGS(t2, 3) + t2 = e.Mul(z, t2) + t2 = e.nSquareGS(t2, 2) + t2 = e.Mul(x, t2) + t2 = e.nSquareGS(t2, 5) + t2 = e.Mul(t1, t2) + t2 = e.nSquareGS(t2, 3) + t2 = e.Mul(x, t2) + t2 = e.nSquareGS(t2, 4) + t2 = e.Mul(z, t2) + t2 = e.nSquareGS(t2, 9) + t2 = e.Mul(z, t2) + t2 = e.nSquareGS(t2, 4) + t2 = e.Mul(t0, t2) + t2 = e.nSquareGS(t2, 5) + t1 = e.Mul(t1, t2) + t1 = e.Square(t1) + t1 = e.Mul(x, t1) + t1 = e.nSquareGS(t1, 5) + t1 = e.Mul(z, t1) + t1 = e.nSquareGS(t1, 3) + t0 = e.Mul(t0, t1) + t0 = e.nSquareGS(t0, 6) + t0 = e.Mul(z, t0) + t0 = e.nSquareGS(t0, 4) + z = e.Mul(z, t0) + z = e.nSquareGS(z, 2) + z = e.Mul(x, z) + z = e.nSquareGS(z, 2) + z = e.Mul(x, z) + z = e.nSquareGS(z, 3) + + return z +} + func (e Ext12) nSquareTorus(z *E6, n int) *E6 { for i := 0; i < n; i++ { z = e.SquareTorus(z) @@ -14,7 +84,7 @@ func (e Ext12) nSquareTorus(z *E6, n int) *E6 { // Exponentiation by the seed t=4965661367192848881 // The computations are performed on E6 compressed form using Torus-based arithmetic. func (e Ext12) ExptTorus(x *E6) *E6 { - // Expt computation is derived from the addition chain: + // ExptTorus computation is derived from the addition chain: // // _10 = 2*1 // _100 = 2*_10 @@ -351,3 +421,127 @@ func (e Ext12) FrobeniusCubeTorus(y *E6) *E6 { return res } + +// FinalExponentiationCheck checks that a Miller function output x lies in the +// same equivalence class as the reduced pairing. This replaces the final +// exponentiation step in-circuit. +// The method follows Section 4 of [On Proving Pairings] paper by A. Novakovic and L. Eagen. +// +// [On Proving Pairings]: https://eprint.iacr.org/2024/640.pdf +func (e Ext12) FinalExponentiationCheck(x *E12) *E12 { + res, err := e.fp.NewHint(finalExpHint, 24, &x.C0.B0.A0, &x.C0.B0.A1, &x.C0.B1.A0, &x.C0.B1.A1, &x.C0.B2.A0, &x.C0.B2.A1, &x.C1.B0.A0, &x.C1.B0.A1, &x.C1.B1.A0, &x.C1.B1.A1, &x.C1.B2.A0, &x.C1.B2.A1) + if err != nil { + // err is non-nil only for invalid number of inputs + panic(err) + } + + residueWitness := E12{ + C0: E6{ + B0: E2{A0: *res[0], A1: *res[1]}, + B1: E2{A0: *res[2], A1: *res[3]}, + B2: E2{A0: *res[4], A1: *res[5]}, + }, + C1: E6{ + B0: E2{A0: *res[6], A1: *res[7]}, + B1: E2{A0: *res[8], A1: *res[9]}, + B2: E2{A0: *res[10], A1: *res[11]}, + }, + } + cubicNonResiduePower := E12{ + C0: E6{ + B0: E2{A0: *res[12], A1: *res[13]}, + B1: E2{A0: *res[14], A1: *res[15]}, + B2: E2{A0: *res[16], A1: *res[17]}, + }, + C1: E6{ + B0: E2{A0: *res[18], A1: *res[19]}, + B1: E2{A0: *res[20], A1: *res[21]}, + B2: E2{A0: *res[22], A1: *res[23]}, + }, + } + + // Check that x * cubicNonResiduePower == residueWitness^λ + // where λ = 6u + 2 + q^3 - q^2 + q, with u the BN254 seed + // and residueWitness, cubicNonResiduePower from the hint. + t2 := e.Mul(&cubicNonResiduePower, x) + + t1 := e.FrobeniusCube(&residueWitness) + t0 := e.FrobeniusSquare(&residueWitness) + t1 = e.DivUnchecked(t1, t0) + t0 = e.Frobenius(&residueWitness) + t1 = e.Mul(t1, t0) + + // exponentiation by U=6u+2 + t0 = e.ExpByU(&residueWitness) + + t0 = e.Mul(t0, t1) + + e.AssertIsEqual(t0, t2) + + return nil +} + +func (e Ext12) Frobenius(x *E12) *E12 { + t0 := e.Ext2.Conjugate(&x.C0.B0) + t1 := e.Ext2.Conjugate(&x.C0.B1) + t2 := e.Ext2.Conjugate(&x.C0.B2) + t3 := e.Ext2.Conjugate(&x.C1.B0) + t4 := e.Ext2.Conjugate(&x.C1.B1) + t5 := e.Ext2.Conjugate(&x.C1.B2) + t1 = e.Ext2.MulByNonResidue1Power2(t1) + t2 = e.Ext2.MulByNonResidue1Power4(t2) + t3 = e.Ext2.MulByNonResidue1Power1(t3) + t4 = e.Ext2.MulByNonResidue1Power3(t4) + t5 = e.Ext2.MulByNonResidue1Power5(t5) + return &E12{ + C0: E6{ + B0: *t0, + B1: *t1, + B2: *t2, + }, + C1: E6{ + B0: *t3, + B1: *t4, + B2: *t5, + }, + } +} + +func (e Ext12) FrobeniusSquare(x *E12) *E12 { + z00 := &x.C0.B0 + z01 := e.Ext2.MulByNonResidue2Power2(&x.C0.B1) + z02 := e.Ext2.MulByNonResidue2Power4(&x.C0.B2) + z10 := e.Ext2.MulByNonResidue2Power1(&x.C1.B0) + z11 := e.Ext2.MulByNonResidue2Power3(&x.C1.B1) + z12 := e.Ext2.MulByNonResidue2Power5(&x.C1.B2) + return &E12{ + C0: E6{B0: *z00, B1: *z01, B2: *z02}, + C1: E6{B0: *z10, B1: *z11, B2: *z12}, + } +} + +func (e Ext12) FrobeniusCube(x *E12) *E12 { + t0 := e.Ext2.Conjugate(&x.C0.B0) + t1 := e.Ext2.Conjugate(&x.C0.B1) + t2 := e.Ext2.Conjugate(&x.C0.B2) + t3 := e.Ext2.Conjugate(&x.C1.B0) + t4 := e.Ext2.Conjugate(&x.C1.B1) + t5 := e.Ext2.Conjugate(&x.C1.B2) + t1 = e.Ext2.MulByNonResidue3Power2(t1) + t2 = e.Ext2.MulByNonResidue3Power4(t2) + t3 = e.Ext2.MulByNonResidue3Power1(t3) + t4 = e.Ext2.MulByNonResidue3Power3(t4) + t5 = e.Ext2.MulByNonResidue3Power5(t5) + return &E12{ + C0: E6{ + B0: *t0, + B1: *t1, + B2: *t2, + }, + C1: E6{ + B0: *t3, + B1: *t4, + B2: *t5, + }, + } +} diff --git a/std/algebra/emulated/fields_bn254/hints.go b/std/algebra/emulated/fields_bn254/hints.go index e9409af18a..c5d68ed67d 100644 --- a/std/algebra/emulated/fields_bn254/hints.go +++ b/std/algebra/emulated/fields_bn254/hints.go @@ -27,6 +27,7 @@ func GetHints() []solver.Hint { // E12 divE12Hint, inverseE12Hint, + finalExpHint, } } @@ -268,3 +269,126 @@ func divE12Hint(nativeMod *big.Int, nativeInputs, nativeOutputs []*big.Int) erro return nil }) } + +func finalExpHint(nativeMod *big.Int, nativeInputs, nativeOutputs []*big.Int) error { + // This follows section 4.3.2 of https://eprint.iacr.org/2024/640.pdf + return emulated.UnwrapHint(nativeInputs, nativeOutputs, + func(mod *big.Int, inputs, outputs []*big.Int) error { + var tmp, x3, cubicNonResiduePower, x, millerLoop, residueWitness, residueWitnessInv, one, root27thOf1 bn254.E12 + var exp1, exp2, rInv, mInv big.Int + + millerLoop.C0.B0.A0.SetBigInt(inputs[0]) + millerLoop.C0.B0.A1.SetBigInt(inputs[1]) + millerLoop.C0.B1.A0.SetBigInt(inputs[2]) + millerLoop.C0.B1.A1.SetBigInt(inputs[3]) + millerLoop.C0.B2.A0.SetBigInt(inputs[4]) + millerLoop.C0.B2.A1.SetBigInt(inputs[5]) + millerLoop.C1.B0.A0.SetBigInt(inputs[6]) + millerLoop.C1.B0.A1.SetBigInt(inputs[7]) + millerLoop.C1.B1.A0.SetBigInt(inputs[8]) + millerLoop.C1.B1.A1.SetBigInt(inputs[9]) + millerLoop.C1.B2.A0.SetBigInt(inputs[10]) + millerLoop.C1.B2.A1.SetBigInt(inputs[11]) + + // exp1 = (p^12-1)/3 + exp1.SetString("4030969696062745741797811005853058291874379204406359442560681893891674450106959530046539719647151210908190211459382793062006703141168852426020468083171325367934590379984666859998399967609544754664110191464072930598755441160008826659219834762354786403012110463250131961575955268597858015384895449311534622125256548620283853223733396368939858981844663598065852816056384933498610930035891058807598891752166582271931875150099691598048016175399382213304673796601585080509443902692818733420199004555566113537482054218823936116647313678747500267068559627206777530424029211671772692598157901876223857571299238046741502089890557442500582300718504160740314926185458079985126192563953772118929726791041828902047546977272656240744693339962973939047279285351052107950250121751682659529260304162131862468322644288196213423232132152125277136333208005221619443705106431645884840489295409272576227859206166894626854018093044908314720", 10) + // root27thOf1 = (0, c010, c011, 0, 0, 0, 0, 0, 0, 0, 0, 0) + // is a 27-th root of unity which is necessarily a cubic non-residue + // since h/r = (p^12-1)/r = 27·l and 3 does not divide l. + // it was computed as w^((p^12-1)/27) = c2 * w^2 + c8 * w^8 where + // Fp12 = Fp[w]/w^12-18w^6+82 which is isomorphic to our Fp12 tower + // then c010 = (c2 + 9 * c8) % p and c011 = c8 + root27thOf1.C0.B1.A0.SetString("9483667112135124394372960210728142145589475128897916459350428495526310884707") + root27thOf1.C0.B1.A1.SetString("4534159768373982659291990808346042891252278737770656686799127720849666919525") + + if one.Exp(millerLoop, &exp1).IsOne() { + // residueWitness = millerLoop is a cubic residue + cubicNonResiduePower.SetOne() + residueWitness.Set(&millerLoop) + } else if one.Exp(*millerLoop.Mul(&millerLoop, &root27thOf1), &exp1).IsOne() { + // residueWitness = millerLoop * root27thOf1 is a cubic residue + cubicNonResiduePower.Set(&root27thOf1) + residueWitness.Set(&millerLoop) + } else { + // residueWitness = millerLoop * root27thOf1^2 is a cubic residue + cubicNonResiduePower.Square(&root27thOf1) + residueWitness.Mul(&millerLoop, &root27thOf1) + } + + // 1. compute r-th root: + // Exponentiate to rInv where + // rInv = 1/r mod (p^12-1)/r + rInv.SetString("495819184011867778744231927046742333492451180917315223017345540833046880485481720031136878341141903241966521818658471092566752321606779256340158678675679238405722886654128392203338228575623261160538734808887996935946888297414610216445334190959815200956855428635568184508263913274453942864817234480763055154719338281461936129150171789463489422401982681230261920147923652438266934726901346095892093443898852488218812468761027620988447655860644584419583586883569984588067403598284748297179498734419889699245081714359110559679136004228878808158639412436468707589339209058958785568729925402190575720856279605832146553573981587948304340677613460685405477047119496887534881410757668344088436651291444274840864486870663164657544390995506448087189408281061890434467956047582679858345583941396130713046072603335601764495918026585155498301896749919393", 10) + residueWitness.Exp(residueWitness, &rInv) + + // 2. compute m-th root: + // where m = (6x + 2 + q^3 - q^2 + q)/(3r) + // Exponentiate to mInv where + // mInv = 1/m mod p^12-1 + mInv.SetString("17840267520054779749190587238017784600702972825655245554504342129614427201836516118803396948809179149954197175783449826546445899524065131269177708416982407215963288737761615699967145070776364294542559324079147363363059480104341231360692143673915822421222230661528586799190306058519400019024762424366780736540525310403098758015600523609594113357130678138304964034267260758692953579514899054295817541844330584721967571697039986079722203518034173581264955381924826388858518077894154909963532054519350571947910625755075099598588672669612434444513251495355121627496067454526862754597351094345783576387352673894873931328099247263766690688395096280633426669535619271711975898132416216382905928886703963310231865346128293216316379527200971959980873989485521004596686352787540034457467115536116148612884807380187255514888720048664139404687086409399", 10) + residueWitness.Exp(residueWitness, &mInv) + + // 3. compute cube root: + // since gcd(3, (p^12-1)/r) ≠ 1 we use a modified Toneelli-Shanks algorithm + // see Alg.4 of https://eprint.iacr.org/2024/640.pdf + // Typo in the paper: p^k-1 = 3^n * s instead of p-1 = 3^r * s + // where k=12 and n=3 here and exp2 = (s+1)/3 + residueWitnessInv.Inverse(&residueWitness) + exp2.SetString("149295173928249842288807815031594751550902933496531831205951181255247201855813315927649619246190785589192230054051214557852100116339587126889646966043382421034614458517950624444385183985538694617189266350521219651805757080000326913304438324531658755667115202342597480058368713651772519088329461085612393412046538837788290860138273939590365147475728281409846400594680923462911515927255224400281440435265428973034513894448136725853630228718495637529802733207466114092942366766400693830377740909465411612499335341437923559875826432546203713595131838044695464089778859691547136762894737106526809539677749557286722299625576201574095640767352005953344997266128077036486155280146436004404804695964512181557316554713802082990544197776406442186936269827816744738898152657469728130713344598597476387715653492155415311971560450078713968012341037230430349766855793764662401499603533676762082513303932107208402000670112774382027", 10) + x.Exp(residueWitness, &exp2) + + // 3^t is ord(x^3 / residueWitness) + x3.Square(&x).Mul(&x3, &x).Mul(&x3, &residueWitnessInv) + t := 0 + for !x3.IsOne() { + t++ + tmp.Square(&x3) + x3.Mul(&tmp, &x3) + } + + for t != 0 { + x.Mul(&x, tmp.Exp(root27thOf1, &exp2)) + + // 3^t is ord(x^3 / residueWitness) + x3.Square(&x).Mul(&x3, &x).Mul(&x3, &residueWitnessInv) + t = 0 + for !x3.IsOne() { + t++ + tmp.Square(&x3) + x3.Mul(&tmp, &x3) + } + } + + // x is now the cube root of residueWitness + residueWitness.Set(&x) + + residueWitness.C0.B0.A0.BigInt(outputs[0]) + residueWitness.C0.B0.A1.BigInt(outputs[1]) + residueWitness.C0.B1.A0.BigInt(outputs[2]) + residueWitness.C0.B1.A1.BigInt(outputs[3]) + residueWitness.C0.B2.A0.BigInt(outputs[4]) + residueWitness.C0.B2.A1.BigInt(outputs[5]) + residueWitness.C1.B0.A0.BigInt(outputs[6]) + residueWitness.C1.B0.A1.BigInt(outputs[7]) + residueWitness.C1.B1.A0.BigInt(outputs[8]) + residueWitness.C1.B1.A1.BigInt(outputs[9]) + residueWitness.C1.B2.A0.BigInt(outputs[10]) + residueWitness.C1.B2.A1.BigInt(outputs[11]) + + // we also need to return the cubic non-residue power + cubicNonResiduePower.C0.B0.A0.BigInt(outputs[12]) + cubicNonResiduePower.C0.B0.A1.BigInt(outputs[13]) + cubicNonResiduePower.C0.B1.A0.BigInt(outputs[14]) + cubicNonResiduePower.C0.B1.A1.BigInt(outputs[15]) + cubicNonResiduePower.C0.B2.A0.BigInt(outputs[16]) + cubicNonResiduePower.C0.B2.A1.BigInt(outputs[17]) + cubicNonResiduePower.C1.B0.A0.BigInt(outputs[18]) + cubicNonResiduePower.C1.B0.A1.BigInt(outputs[19]) + cubicNonResiduePower.C1.B1.A0.BigInt(outputs[20]) + cubicNonResiduePower.C1.B1.A1.BigInt(outputs[21]) + cubicNonResiduePower.C1.B2.A0.BigInt(outputs[22]) + cubicNonResiduePower.C1.B2.A1.BigInt(outputs[23]) + + return nil + }) +} diff --git a/std/algebra/emulated/sw_bn254/hints.go b/std/algebra/emulated/sw_bn254/hints.go new file mode 100644 index 0000000000..a6d85da8d6 --- /dev/null +++ b/std/algebra/emulated/sw_bn254/hints.go @@ -0,0 +1,164 @@ +package sw_bn254 + +import ( + "math/big" + + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/std/math/emulated" +) + +func init() { + solver.RegisterHint(GetHints()...) +} + +// GetHints returns all hint functions used in the package. +func GetHints() []solver.Hint { + return []solver.Hint{ + millerLoopAndCheckFinalExpHint, + } +} + +func millerLoopAndCheckFinalExpHint(nativeMod *big.Int, nativeInputs, nativeOutputs []*big.Int) error { + // This follows section 4.3.2 of https://eprint.iacr.org/2024/640.pdf + return emulated.UnwrapHint(nativeInputs, nativeOutputs, + func(mod *big.Int, inputs, outputs []*big.Int) error { + var previous, tmp, x3, cubicNonResiduePower, x, millerLoop, residueWitness, residueWitnessInv, one, root27thOf1 bn254.E12 + var exp1, exp2, rInv, mInv big.Int + var P bn254.G1Affine + var Q bn254.G2Affine + + P.X.SetBigInt(inputs[0]) + P.Y.SetBigInt(inputs[1]) + Q.X.A0.SetBigInt(inputs[2]) + Q.X.A1.SetBigInt(inputs[3]) + Q.Y.A0.SetBigInt(inputs[4]) + Q.Y.A1.SetBigInt(inputs[5]) + + previous.C0.B0.A0.SetBigInt(inputs[6]) + previous.C0.B0.A1.SetBigInt(inputs[7]) + previous.C0.B1.A0.SetBigInt(inputs[8]) + previous.C0.B1.A1.SetBigInt(inputs[9]) + previous.C0.B2.A0.SetBigInt(inputs[10]) + previous.C0.B2.A1.SetBigInt(inputs[11]) + previous.C1.B0.A0.SetBigInt(inputs[12]) + previous.C1.B0.A1.SetBigInt(inputs[13]) + previous.C1.B1.A0.SetBigInt(inputs[14]) + previous.C1.B1.A1.SetBigInt(inputs[15]) + previous.C1.B2.A0.SetBigInt(inputs[16]) + previous.C1.B2.A1.SetBigInt(inputs[17]) + + lines := bn254.PrecomputeLines(Q) + millerLoop, err := bn254.MillerLoopFixedQ( + []bn254.G1Affine{P}, + [][2][len(bn254.LoopCounter)]bn254.LineEvaluationAff{lines}, + ) + if err != nil { + return err + } + + millerLoop.Mul(&millerLoop, &previous) + + // exp1 = (p^12-1)/3 + exp1.SetString("4030969696062745741797811005853058291874379204406359442560681893891674450106959530046539719647151210908190211459382793062006703141168852426020468083171325367934590379984666859998399967609544754664110191464072930598755441160008826659219834762354786403012110463250131961575955268597858015384895449311534622125256548620283853223733396368939858981844663598065852816056384933498610930035891058807598891752166582271931875150099691598048016175399382213304673796601585080509443902692818733420199004555566113537482054218823936116647313678747500267068559627206777530424029211671772692598157901876223857571299238046741502089890557442500582300718504160740314926185458079985126192563953772118929726791041828902047546977272656240744693339962973939047279285351052107950250121751682659529260304162131862468322644288196213423232132152125277136333208005221619443705106431645884840489295409272576227859206166894626854018093044908314720", 10) + // root27thOf1 = (0, c010, c011, 0, 0, 0, 0, 0, 0, 0, 0, 0) + // is a 27-th root of unity which is necessarily a cubic non-residue + // since h/r = (p^12-1)/r = 27·l and 3 does not divide l. + // it was computed as w^((p^12-1)/27) = c2 * w^2 + c8 * w^8 where + // Fp12 = Fp[w]/w^12-18w^6+82 which is isomorphic to our Fp12 tower + // then c010 = (c2 + 9 * c8) % p and c011 = c8 + root27thOf1.C0.B1.A0.SetString("9483667112135124394372960210728142145589475128897916459350428495526310884707") + root27thOf1.C0.B1.A1.SetString("4534159768373982659291990808346042891252278737770656686799127720849666919525") + + if one.Exp(millerLoop, &exp1).IsOne() { + // residueWitness = millerLoop is a cubic residue + cubicNonResiduePower.SetOne() + residueWitness.Set(&millerLoop) + } else if one.Exp(*millerLoop.Mul(&millerLoop, &root27thOf1), &exp1).IsOne() { + // residueWitness = millerLoop * root27thOf1 is a cubic residue + cubicNonResiduePower.Set(&root27thOf1) + residueWitness.Set(&millerLoop) + } else { + // residueWitness = millerLoop * root27thOf1^2 is a cubic residue + cubicNonResiduePower.Square(&root27thOf1) + residueWitness.Mul(&millerLoop, &root27thOf1) + } + + // 1. compute r-th root: + // Exponentiate to rInv where + // rInv = 1/r mod (p^12-1)/r + rInv.SetString("495819184011867778744231927046742333492451180917315223017345540833046880485481720031136878341141903241966521818658471092566752321606779256340158678675679238405722886654128392203338228575623261160538734808887996935946888297414610216445334190959815200956855428635568184508263913274453942864817234480763055154719338281461936129150171789463489422401982681230261920147923652438266934726901346095892093443898852488218812468761027620988447655860644584419583586883569984588067403598284748297179498734419889699245081714359110559679136004228878808158639412436468707589339209058958785568729925402190575720856279605832146553573981587948304340677613460685405477047119496887534881410757668344088436651291444274840864486870663164657544390995506448087189408281061890434467956047582679858345583941396130713046072603335601764495918026585155498301896749919393", 10) + residueWitness.Exp(residueWitness, &rInv) + + // 2. compute m-th root: + // where m = (6x + 2 + q^3 - q^2 + q)/(3r) + // Exponentiate to mInv where + // mInv = 1/m mod p^12-1 + mInv.SetString("17840267520054779749190587238017784600702972825655245554504342129614427201836516118803396948809179149954197175783449826546445899524065131269177708416982407215963288737761615699967145070776364294542559324079147363363059480104341231360692143673915822421222230661528586799190306058519400019024762424366780736540525310403098758015600523609594113357130678138304964034267260758692953579514899054295817541844330584721967571697039986079722203518034173581264955381924826388858518077894154909963532054519350571947910625755075099598588672669612434444513251495355121627496067454526862754597351094345783576387352673894873931328099247263766690688395096280633426669535619271711975898132416216382905928886703963310231865346128293216316379527200971959980873989485521004596686352787540034457467115536116148612884807380187255514888720048664139404687086409399", 10) + residueWitness.Exp(residueWitness, &mInv) + + // 3. compute cube root: + // since gcd(3, (p^12-1)/r) ≠ 1 we use a modified Toneelli-Shanks algorithm + // see Alg.4 of https://eprint.iacr.org/2024/640.pdf + // Typo in the paper: p^k-1 = 3^n * s instead of p-1 = 3^r * s + // where k=12 and n=3 here and exp2 = (s+1)/3 + residueWitnessInv.Inverse(&residueWitness) + exp2.SetString("149295173928249842288807815031594751550902933496531831205951181255247201855813315927649619246190785589192230054051214557852100116339587126889646966043382421034614458517950624444385183985538694617189266350521219651805757080000326913304438324531658755667115202342597480058368713651772519088329461085612393412046538837788290860138273939590365147475728281409846400594680923462911515927255224400281440435265428973034513894448136725853630228718495637529802733207466114092942366766400693830377740909465411612499335341437923559875826432546203713595131838044695464089778859691547136762894737106526809539677749557286722299625576201574095640767352005953344997266128077036486155280146436004404804695964512181557316554713802082990544197776406442186936269827816744738898152657469728130713344598597476387715653492155415311971560450078713968012341037230430349766855793764662401499603533676762082513303932107208402000670112774382027", 10) + x.Exp(residueWitness, &exp2) + + // 3^t is ord(x^3 / residueWitness) + x3.Square(&x).Mul(&x3, &x).Mul(&x3, &residueWitnessInv) + t := 0 + for !x3.IsOne() { + t++ + tmp.Square(&x3) + x3.Mul(&tmp, &x3) + } + + for t != 0 { + x.Mul(&x, tmp.Exp(root27thOf1, &exp2)) + + // 3^t is ord(x^3 / residueWitness) + x3.Square(&x).Mul(&x3, &x).Mul(&x3, &residueWitnessInv) + t = 0 + for !x3.IsOne() { + t++ + tmp.Square(&x3) + x3.Mul(&tmp, &x3) + } + } + + // x is now the cube root of residueWitness + residueWitness.Set(&x) + residueWitnessInv.Inverse(&residueWitness) + + residueWitness.C0.B0.A0.BigInt(outputs[0]) + residueWitness.C0.B0.A1.BigInt(outputs[1]) + residueWitness.C0.B1.A0.BigInt(outputs[2]) + residueWitness.C0.B1.A1.BigInt(outputs[3]) + residueWitness.C0.B2.A0.BigInt(outputs[4]) + residueWitness.C0.B2.A1.BigInt(outputs[5]) + residueWitness.C1.B0.A0.BigInt(outputs[6]) + residueWitness.C1.B0.A1.BigInt(outputs[7]) + residueWitness.C1.B1.A0.BigInt(outputs[8]) + residueWitness.C1.B1.A1.BigInt(outputs[9]) + residueWitness.C1.B2.A0.BigInt(outputs[10]) + residueWitness.C1.B2.A1.BigInt(outputs[11]) + + // we also need to return the cubic non-residue power + cubicNonResiduePower.C0.B0.A0.BigInt(outputs[12]) + cubicNonResiduePower.C0.B0.A1.BigInt(outputs[13]) + cubicNonResiduePower.C0.B1.A0.BigInt(outputs[14]) + cubicNonResiduePower.C0.B1.A1.BigInt(outputs[15]) + cubicNonResiduePower.C0.B2.A0.BigInt(outputs[16]) + cubicNonResiduePower.C0.B2.A1.BigInt(outputs[17]) + cubicNonResiduePower.C1.B0.A0.BigInt(outputs[18]) + cubicNonResiduePower.C1.B0.A1.BigInt(outputs[19]) + cubicNonResiduePower.C1.B1.A0.BigInt(outputs[20]) + cubicNonResiduePower.C1.B1.A1.BigInt(outputs[21]) + cubicNonResiduePower.C1.B2.A0.BigInt(outputs[22]) + cubicNonResiduePower.C1.B2.A1.BigInt(outputs[23]) + + return nil + }) +} diff --git a/std/algebra/emulated/sw_bn254/pairing.go b/std/algebra/emulated/sw_bn254/pairing.go index ddcecfef05..d346b6f00e 100644 --- a/std/algebra/emulated/sw_bn254/pairing.go +++ b/std/algebra/emulated/sw_bn254/pairing.go @@ -245,13 +245,22 @@ func (pr Pairing) Pair(P []*G1Affine, Q []*G2Affine) (*GTEl, error) { // // This function doesn't check that the inputs are in the correct subgroups. See AssertIsOnG1 and AssertIsOnG2. func (pr Pairing) PairingCheck(P []*G1Affine, Q []*G2Affine) error { - f, err := pr.Pair(P, Q) + f, err := pr.MillerLoop(P, Q) if err != nil { return err } - one := pr.One() - pr.AssertIsEqual(f, one) + // We perform the easy part of the final exp to push f to the cyclotomic + // subgroup so that FinalExponentiationCheck is carried with optimized + // cyclotomic squaring (e.g. Karabina12345). + // + // f = f^(p⁶-1)(p²+1) + buf := pr.Conjugate(f) + buf = pr.DivUnchecked(buf, f) + f = pr.FrobeniusSquare(buf) + f = pr.Mul(f, buf) + + pr.FinalExponentiationCheck(f) return nil } @@ -365,7 +374,6 @@ func (pr Pairing) millerLoopLines(P []*G1Affine, lines []lineEvaluations) (*GTEl xNegOverY[k] = pr.curveF.Neg(xNegOverY[k]) } - // f_{x₀+1+λ(x₀³-x₀²-x₀),Q}(P), Q is known in advance var prodLines [5]*fields_bn254.E2 res := pr.Ext12.One() @@ -644,12 +652,141 @@ func (pr Pairing) MillerLoopAndMul(P *G1Affine, Q *G2Affine, previous *GTEl) (*G return res, err } -// FinalExponentiationIsOne performs the final exponentiation on e -// and checks that the result in 1 in GT. +// MillerLoopAndFinalExpCheck computes the Miller loop between P and Q, +// multiplies it in 𝔽p¹² by previous and checks that the result lies in the +// same equivalence class as the reduced pairing purported to be 1. This check +// replaces the final exponentiation step in-circuit and follows Section 4 of +// [On Proving Pairings] paper by A. Novakovic and L. Eagen. // // This method is needed for evmprecompiles/ecpair. -func (pr Pairing) FinalExponentiationIsOne(e *GTEl) { - res := pr.finalExponentiation(e, false) - one := pr.One() - pr.AssertIsEqual(res, one) +// +// [On Proving Pairings]: https://eprint.iacr.org/2024/640.pdf +func (pr Pairing) MillerLoopAndFinalExpCheck(P *G1Affine, Q *G2Affine, previous *GTEl) error { + + // hint the non-residue witness + hint, err := pr.curveF.NewHint(millerLoopAndCheckFinalExpHint, 24, &P.X, &P.Y, &Q.P.X.A0, &Q.P.X.A1, &Q.P.Y.A0, &Q.P.Y.A1, &previous.C0.B0.A0, &previous.C0.B0.A1, &previous.C0.B1.A0, &previous.C0.B1.A1, &previous.C0.B2.A0, &previous.C0.B2.A1, &previous.C1.B0.A0, &previous.C1.B0.A1, &previous.C1.B1.A0, &previous.C1.B1.A1, &previous.C1.B2.A0, &previous.C1.B2.A1) + if err != nil { + // err is non-nil only for invalid number of inputs + panic(err) + } + + residueWitness := fields_bn254.E12{ + C0: fields_bn254.E6{ + B0: fields_bn254.E2{A0: *hint[0], A1: *hint[1]}, + B1: fields_bn254.E2{A0: *hint[2], A1: *hint[3]}, + B2: fields_bn254.E2{A0: *hint[4], A1: *hint[5]}, + }, + C1: fields_bn254.E6{ + B0: fields_bn254.E2{A0: *hint[6], A1: *hint[7]}, + B1: fields_bn254.E2{A0: *hint[8], A1: *hint[9]}, + B2: fields_bn254.E2{A0: *hint[10], A1: *hint[11]}, + }, + } + cubicNonResiduePower := fields_bn254.E12{ + C0: fields_bn254.E6{ + B0: fields_bn254.E2{A0: *hint[12], A1: *hint[13]}, + B1: fields_bn254.E2{A0: *hint[14], A1: *hint[15]}, + B2: fields_bn254.E2{A0: *hint[16], A1: *hint[17]}, + }, + C1: fields_bn254.E6{ + B0: fields_bn254.E2{A0: *hint[18], A1: *hint[19]}, + B1: fields_bn254.E2{A0: *hint[20], A1: *hint[21]}, + B2: fields_bn254.E2{A0: *hint[22], A1: *hint[23]}, + }, + } + + // residueWitnessInv = 1 / residueWitness + residueWitnessInv := pr.Inverse(&residueWitness) + + if Q.Lines == nil { + Qlines := pr.computeLines(&Q.P) + Q.Lines = &Qlines + } + lines := *Q.Lines + + // precomputations + yInv := pr.curveF.Inverse(&P.Y) + xNegOverY := pr.curveF.MulMod(&P.X, yInv) + xNegOverY = pr.curveF.Neg(xNegOverY) + + // init Miller loop accumulator to residueWitnessInv to share the squarings + // of residueWitnessInv^{6x₀+2} + res := residueWitnessInv + + // Compute f_{6x₀+2,Q}(P) + for i := 64; i >= 0; i-- { + res = pr.Square(res) + + switch loopCounter[i] { + case 0: + // ℓ × res + res = pr.MulBy034( + res, + pr.MulByElement(&lines[0][i].R0, xNegOverY), + pr.MulByElement(&lines[0][i].R1, yInv), + ) + case 1: + // multiply by residueWitnessInv when bit=1 + res = pr.Mul(res, residueWitnessInv) + // lines evaluations at P + // and ℓ × ℓ + prodLines := pr.Mul034By034( + pr.MulByElement(&lines[0][i].R0, xNegOverY), + pr.MulByElement(&lines[0][i].R1, yInv), + pr.MulByElement(&lines[1][i].R0, xNegOverY), + pr.MulByElement(&lines[1][i].R1, yInv), + ) + // (ℓ × ℓ) × res + res = pr.MulBy01234(res, prodLines) + case -1: + // multiply by residueWitness when bit=-1 + res = pr.Mul(res, &residueWitness) + // lines evaluations at P + // and ℓ × ℓ + prodLines := pr.Mul034By034( + pr.MulByElement(&lines[0][i].R0, xNegOverY), + pr.MulByElement(&lines[0][i].R1, yInv), + pr.MulByElement(&lines[1][i].R0, xNegOverY), + pr.MulByElement(&lines[1][i].R1, yInv), + ) + // (ℓ × ℓ) × res + res = pr.MulBy01234(res, prodLines) + default: + return nil + } + } + + // Compute ℓ_{[6x₀+2]Q,π(Q)}(P) · ℓ_{[6x₀+2]Q+π(Q),-π²(Q)}(P) + // lines evaluations at P + // and ℓ × ℓ + prodLines := pr.Mul034By034( + pr.MulByElement(&lines[0][65].R0, xNegOverY), + pr.MulByElement(&lines[0][65].R1, yInv), + pr.MulByElement(&lines[1][65].R0, xNegOverY), + pr.MulByElement(&lines[1][65].R1, yInv), + ) + // (ℓ × ℓ) × res + res = pr.MulBy01234(res, prodLines) + + // multiply by previous multi-Miller function + res = pr.Mul(res, previous) + + // Check that res * cubicNonResiduePower * residueWitnessInv^λ' == 1 + // where λ' = q^3 - q^2 + q, with u the BN254 seed + // and residueWitnessInv, cubicNonResiduePower from the hint. + // Note that res is already MillerLoop(P,Q) * residueWitnessInv^{6x₀+2} since + // we initialized the Miller loop accumulator with residueWitnessInv. + t2 := pr.Mul(&cubicNonResiduePower, res) + + t1 := pr.FrobeniusCube(residueWitnessInv) + t0 := pr.FrobeniusSquare(residueWitnessInv) + t1 = pr.DivUnchecked(t1, t0) + t0 = pr.Frobenius(residueWitnessInv) + t1 = pr.Mul(t1, t0) + + t2 = pr.Mul(t2, t1) + + pr.AssertIsEqual(t2, pr.One()) + + return nil } diff --git a/std/algebra/emulated/sw_bn254/pairing_test.go b/std/algebra/emulated/sw_bn254/pairing_test.go index bc41582cde..10a3913ce6 100644 --- a/std/algebra/emulated/sw_bn254/pairing_test.go +++ b/std/algebra/emulated/sw_bn254/pairing_test.go @@ -63,6 +63,43 @@ func TestFinalExponentiationTestSolve(t *testing.T) { assert.NoError(err) } +type MillerLoopCircuit struct { + InG1 G1Affine + InG2 G2Affine + Res GTEl +} + +func (c *MillerLoopCircuit) Define(api frontend.API) error { + pairing, err := NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + res, err := pairing.MillerLoop([]*G1Affine{&c.InG1}, []*G2Affine{&c.InG2}) + if err != nil { + return fmt.Errorf("pair: %w", err) + } + pairing.AssertIsEqual(res, &c.Res) + return nil +} + +func TestMillerLoopTestSolve(t *testing.T) { + assert := test.NewAssert(t) + p, q := randomG1G2Affines() + lines := bn254.PrecomputeLines(q) + res, err := bn254.MillerLoopFixedQ( + []bn254.G1Affine{p}, + [][2][len(bn254.LoopCounter)]bn254.LineEvaluationAff{lines}, + ) + assert.NoError(err) + witness := MillerLoopCircuit{ + InG1: NewG1Affine(p), + InG2: NewG2Affine(q), + Res: NewGTEl(res), + } + err = test.IsSolved(&MillerLoopCircuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} + type PairCircuit struct { InG1 G1Affine InG2 G2Affine diff --git a/std/evmprecompiles/08-bnpairing.go b/std/evmprecompiles/08-bnpairing.go index 9d25cd20ec..b796ee458e 100644 --- a/std/evmprecompiles/08-bnpairing.go +++ b/std/evmprecompiles/08-bnpairing.go @@ -10,12 +10,20 @@ import ( // [ALT_BN128_PAIRING_CHECK]: https://ethereum.github.io/execution-specs/autoapi/ethereum/paris/vm/precompiled_contracts/alt_bn128/index.html#alt-bn128-pairing-check // // To have a fixed-circuit regardless of the number of inputs, we need 2 fixed circuits: -// - A Miller loop of fixed size 1 followed with a multiplication in 𝔽p¹² (MillerLoopAndMul) -// - A final exponentiation followed with an equality check in GT (FinalExponentiationIsOne) +// - MillerLoopAndMul: +// A Miller loop of fixed size 1 followed by a multiplication in 𝔽p¹². +// - MillerLoopAndFinalExpCheck: +// A Miller loop of fixed size 1 followed by a multiplication in 𝔽p¹², and +// a check that the result lies in the same equivalence class as the +// reduced pairing purported to be 1. This check replaces the final +// exponentiation step in-circuit and follows Section 4 of [On Proving +// Pairings] paper by A. Novakovic and L. Eagen. // -// N.B.: This is a sub-optimal routine but defines a fixed circuit regardless -// of the number of inputs. We can extend this routine to handle a 2-by-2 -// logic but we prefer a minimal number of circuits (2). +// [On Proving Pairings]: https://eprint.iacr.org/2024/640.pdf +// +// N.B.: This is a sub-optimal routine but defines a fixed circuit regardless +// of the number of inputs. We can extend this routine to handle a 2-by-2 +// logic but we prefer a minimal number of circuits (2). func ECPair(api frontend.API, P []*sw_bn254.G1Affine, Q []*sw_bn254.G2Affine) { if len(P) != len(Q) { @@ -37,7 +45,7 @@ func ECPair(api frontend.API, P []*sw_bn254.G1Affine, Q []*sw_bn254.G2Affine) { // 3- Check that ∏ᵢ e(Pᵢ, Qᵢ) == 1 ml := pair.One() - for i := 0; i < n; i++ { + for i := 0; i < n-1; i++ { // fixed circuit 1 ml, err = pair.MillerLoopAndMul(P[i], Q[i], ml) if err != nil { @@ -46,5 +54,5 @@ func ECPair(api frontend.API, P []*sw_bn254.G1Affine, Q []*sw_bn254.G2Affine) { } // fixed circuit 2 - pair.FinalExponentiationIsOne(ml) + pair.MillerLoopAndFinalExpCheck(P[n-1], Q[n-1], ml) }