From f6d170f21ef6f03c745112282fccac1d51b93440 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 30 Apr 2026 14:18:43 -0400 Subject: [PATCH 01/15] feat: multiset hash kb8 --- std/algebra/native/fields_kb8/doc.go | 5 + std/algebra/native/fields_kb8/e2.go | 117 ++++++++++ std/algebra/native/fields_kb8/e2_test.go | 72 ++++++ std/algebra/native/fields_kb8/e4.go | 119 ++++++++++ std/algebra/native/fields_kb8/e4_test.go | 72 ++++++ std/algebra/native/fields_kb8/e8.go | 170 ++++++++++++++ std/algebra/native/fields_kb8/e8_test.go | 91 ++++++++ std/algebra/native/fields_kb8/hints.go | 68 ++++++ std/algebra/native/maptocurve_kb8/doc.go | 3 + std/algebra/native/maptocurve_kb8/hints.go | 53 +++++ .../native/maptocurve_kb8/maptocurve.go | 55 +++++ .../native/maptocurve_kb8/maptocurve_test.go | 36 +++ std/algebra/native/maptocurve_kb8/types.go | 32 +++ std/algebra/native/sw_kb8/doc.go | 7 + std/algebra/native/sw_kb8/g1.go | 210 ++++++++++++++++++ std/algebra/native/sw_kb8/g1_test.go | 143 ++++++++++++ std/algebra/native/sw_kb8/hints.go | 10 + std/algebra/native/sw_kb8/multisethash.go | 65 ++++++ .../native/sw_kb8/multisethash_test.go | 108 +++++++++ std/algebra/native/sw_kb8/types.go | 29 +++ std/hints.go | 4 + 21 files changed, 1469 insertions(+) create mode 100644 std/algebra/native/fields_kb8/doc.go create mode 100644 std/algebra/native/fields_kb8/e2.go create mode 100644 std/algebra/native/fields_kb8/e2_test.go create mode 100644 std/algebra/native/fields_kb8/e4.go create mode 100644 std/algebra/native/fields_kb8/e4_test.go create mode 100644 std/algebra/native/fields_kb8/e8.go create mode 100644 std/algebra/native/fields_kb8/e8_test.go create mode 100644 std/algebra/native/fields_kb8/hints.go create mode 100644 std/algebra/native/maptocurve_kb8/doc.go create mode 100644 std/algebra/native/maptocurve_kb8/hints.go create mode 100644 std/algebra/native/maptocurve_kb8/maptocurve.go create mode 100644 std/algebra/native/maptocurve_kb8/maptocurve_test.go create mode 100644 std/algebra/native/maptocurve_kb8/types.go create mode 100644 std/algebra/native/sw_kb8/doc.go create mode 100644 std/algebra/native/sw_kb8/g1.go create mode 100644 std/algebra/native/sw_kb8/g1_test.go create mode 100644 std/algebra/native/sw_kb8/hints.go create mode 100644 std/algebra/native/sw_kb8/multisethash.go create mode 100644 std/algebra/native/sw_kb8/multisethash_test.go create mode 100644 std/algebra/native/sw_kb8/types.go diff --git a/std/algebra/native/fields_kb8/doc.go b/std/algebra/native/fields_kb8/doc.go new file mode 100644 index 0000000000..c3ad405595 --- /dev/null +++ b/std/algebra/native/fields_kb8/doc.go @@ -0,0 +1,5 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Package fields_kb8 implements KoalaBear-native Fp^8 arithmetic for kb8-based gadgets. +package fields_kb8 diff --git a/std/algebra/native/fields_kb8/e2.go b/std/algebra/native/fields_kb8/e2.go new file mode 100644 index 0000000000..55f8d491e8 --- /dev/null +++ b/std/algebra/native/fields_kb8/e2.go @@ -0,0 +1,117 @@ +package fields_kb8 + +import ( + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/frontend" +) + +var uSquare = koalabear.NewElement(3) + +type E2 struct { + A0, A1 frontend.Variable +} + +func (e *E2) SetZero() *E2 { + e.A0 = 0 + e.A1 = 0 + return e +} + +func (e *E2) SetOne() *E2 { + e.A0 = 1 + e.A1 = 0 + return e +} + +func (e *E2) IsZero(api frontend.API) frontend.Variable { + return api.And(api.IsZero(e.A0), api.IsZero(e.A1)) +} + +func (e *E2) assign(e1 []frontend.Variable) { + e.A0 = e1[0] + e.A1 = e1[1] +} + +func (e *E2) Neg(api frontend.API, e1 E2) *E2 { + e.A0 = api.Neg(e1.A0) + e.A1 = api.Neg(e1.A1) + return e +} + +func (e *E2) Add(api frontend.API, e1, e2 E2) *E2 { + e.A0 = api.Add(e1.A0, e2.A0) + e.A1 = api.Add(e1.A1, e2.A1) + return e +} + +func (e *E2) Double(api frontend.API, e1 E2) *E2 { + e.A0 = api.Mul(e1.A0, 2) + e.A1 = api.Mul(e1.A1, 2) + return e +} + +func (e *E2) Sub(api frontend.API, e1, e2 E2) *E2 { + e.A0 = api.Sub(e1.A0, e2.A0) + e.A1 = api.Sub(e1.A1, e2.A1) + return e +} + +func (e *E2) Mul(api frontend.API, e1, e2 E2) *E2 { + l1 := api.Add(e1.A0, e1.A1) + l2 := api.Add(e2.A0, e2.A1) + u := api.Mul(l1, l2) + ac := api.Mul(e1.A0, e2.A0) + bd := api.Mul(e1.A1, e2.A1) + e.A1 = api.Sub(u, api.Add(ac, bd)) + e.A0 = api.Add(ac, api.Mul(bd, uSquare)) + return e +} + +func (e *E2) Square(api frontend.API, x E2) *E2 { + // Algorithm 22 from https://eprint.iacr.org/2010/354.pdf adapted to u^2 = 3. + c0 := api.Add(x.A0, x.A1) + c2 := api.Mul(x.A1, uSquare) + c2 = api.Add(c2, x.A0) + + c0 = api.Mul(c0, c2) + c2 = api.Mul(x.A0, x.A1) + c2 = api.Mul(c2, 2) + e.A1 = c2 + c2 = api.Mul(c2, 2) + e.A0 = api.Sub(c0, c2) + return e +} + +func (e *E2) MulByFp(api frontend.API, e1 E2, c interface{}) *E2 { + e.A0 = api.Mul(e1.A0, c) + e.A1 = api.Mul(e1.A1, c) + return e +} + +func (e *E2) MulByNonResidue(api frontend.API, e1 E2) *E2 { + x := e1.A0 + e.A0 = api.Mul(e1.A1, uSquare) + e.A1 = x + return e +} + +func (e *E2) AssertIsEqual(api frontend.API, other E2) { + api.AssertIsEqual(e.A0, other.A0) + api.AssertIsEqual(e.A1, other.A1) +} + +func (e *E2) IsEqual(api frontend.API, other E2) frontend.Variable { + return api.And(api.IsZero(api.Sub(e.A0, other.A0)), api.IsZero(api.Sub(e.A1, other.A1))) +} + +func (e *E2) Select(api frontend.API, b frontend.Variable, r1, r2 E2) *E2 { + e.A0 = api.Select(b, r1.A0, r2.A0) + e.A1 = api.Select(b, r1.A1, r2.A1) + return e +} + +func (e *E2) Assign(a *extensions.E2) { + e.A0 = a.A0 + e.A1 = a.A1 +} diff --git a/std/algebra/native/fields_kb8/e2_test.go b/std/algebra/native/fields_kb8/e2_test.go new file mode 100644 index 0000000000..dce64cc0dc --- /dev/null +++ b/std/algebra/native/fields_kb8/e2_test.go @@ -0,0 +1,72 @@ +package fields_kb8 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" +) + +type e2Add struct{ A, B, C E2 } + +func (c *e2Add) Define(api frontend.API) error { + var e E2 + e.Add(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestAddE2(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E2 + a.SetRandom() + b.SetRandom() + c.Add(&a, &b) + var w e2Add + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e2Add{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type e2Sub struct{ A, B, C E2 } + +func (c *e2Sub) Define(api frontend.API) error { + var e E2 + e.Sub(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestSubE2(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E2 + a.SetRandom() + b.SetRandom() + c.Sub(&a, &b) + var w e2Sub + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e2Sub{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type e2Mul struct{ A, B, C E2 } + +func (c *e2Mul) Define(api frontend.API) error { + var e E2 + e.Mul(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestMulE2(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E2 + a.SetRandom() + b.SetRandom() + c.Mul(&a, &b) + var w e2Mul + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e2Mul{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} diff --git a/std/algebra/native/fields_kb8/e4.go b/std/algebra/native/fields_kb8/e4.go new file mode 100644 index 0000000000..1c72207862 --- /dev/null +++ b/std/algebra/native/fields_kb8/e4.go @@ -0,0 +1,119 @@ +package fields_kb8 + +import ( + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/frontend" +) + +type E4 struct { + B0, B1 E2 +} + +func (e *E4) SetZero() *E4 { + e.B0.SetZero() + e.B1.SetZero() + return e +} + +func (e *E4) SetOne() *E4 { + e.B0.SetOne() + e.B1.SetZero() + return e +} + +func (e *E4) IsZero(api frontend.API) frontend.Variable { + return api.And(e.B0.IsZero(api), e.B1.IsZero(api)) +} + +func (e *E4) assign(e1 []frontend.Variable) { + e.B0.A0 = e1[0] + e.B0.A1 = e1[1] + e.B1.A0 = e1[2] + e.B1.A1 = e1[3] +} + +func (e *E4) Neg(api frontend.API, e1 E4) *E4 { + e.B0.Neg(api, e1.B0) + e.B1.Neg(api, e1.B1) + return e +} + +func (e *E4) Add(api frontend.API, e1, e2 E4) *E4 { + e.B0.Add(api, e1.B0, e2.B0) + e.B1.Add(api, e1.B1, e2.B1) + return e +} + +func (e *E4) Double(api frontend.API, e1 E4) *E4 { + e.B0.Double(api, e1.B0) + e.B1.Double(api, e1.B1) + return e +} + +func (e *E4) Sub(api frontend.API, e1, e2 E4) *E4 { + e.B0.Sub(api, e1.B0, e2.B0) + e.B1.Sub(api, e1.B1, e2.B1) + return e +} + +func (e *E4) Mul(api frontend.API, e1, e2 E4) *E4 { + var l1, l2, u, ac, bd E2 + l1.Add(api, e1.B0, e1.B1) + l2.Add(api, e2.B0, e2.B1) + u.Mul(api, l1, l2) + ac.Mul(api, e1.B0, e2.B0) + bd.Mul(api, e1.B1, e2.B1) + e.B0.MulByNonResidue(api, bd).Add(api, e.B0, ac) + e.B1.Add(api, ac, bd) + e.B1.Sub(api, u, e.B1) + return e +} + +func (e *E4) Square(api frontend.API, x E4) *E4 { + // Quadratic-extension square over E2 with v^2 = u. + var c0, c2, tmp, tmpNR E2 + tmp.MulByNonResidue(api, x.B1) + c0.Add(api, x.B0, x.B1) + tmp.Add(api, tmp, x.B0) + c0.Mul(api, c0, tmp) + + c2.Mul(api, x.B0, x.B1) + e.B1.Double(api, c2) + + tmpNR.MulByNonResidue(api, c2) + e.B0.Sub(api, c0, c2) + e.B0.Sub(api, e.B0, tmpNR) + return e +} + +func (e *E4) MulByFp(api frontend.API, e1 E4, c interface{}) *E4 { + e.B0.MulByFp(api, e1.B0, c) + e.B1.MulByFp(api, e1.B1, c) + return e +} + +func (e *E4) MulByNonResidue(api frontend.API, e1 E4) *E4 { + e.B0.MulByNonResidue(api, e1.B1) + e.B1 = e1.B0 + return e +} + +func (e *E4) AssertIsEqual(api frontend.API, other E4) { + e.B0.AssertIsEqual(api, other.B0) + e.B1.AssertIsEqual(api, other.B1) +} + +func (e *E4) IsEqual(api frontend.API, other E4) frontend.Variable { + return api.And(e.B0.IsEqual(api, other.B0), e.B1.IsEqual(api, other.B1)) +} + +func (e *E4) Select(api frontend.API, b frontend.Variable, r1, r2 E4) *E4 { + e.B0.Select(api, b, r1.B0, r2.B0) + e.B1.Select(api, b, r1.B1, r2.B1) + return e +} + +func (e *E4) Assign(a *extensions.E4) { + e.B0.Assign(&a.B0) + e.B1.Assign(&a.B1) +} diff --git a/std/algebra/native/fields_kb8/e4_test.go b/std/algebra/native/fields_kb8/e4_test.go new file mode 100644 index 0000000000..0b5fd38760 --- /dev/null +++ b/std/algebra/native/fields_kb8/e4_test.go @@ -0,0 +1,72 @@ +package fields_kb8 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" +) + +type e4Add struct{ A, B, C E4 } + +func (c *e4Add) Define(api frontend.API) error { + var e E4 + e.Add(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestAddE4(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E4 + a.SetRandom() + b.SetRandom() + c.Add(&a, &b) + var w e4Add + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e4Add{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type e4Sub struct{ A, B, C E4 } + +func (c *e4Sub) Define(api frontend.API) error { + var e E4 + e.Sub(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestSubE4(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E4 + a.SetRandom() + b.SetRandom() + c.Sub(&a, &b) + var w e4Sub + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e4Sub{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type e4Mul struct{ A, B, C E4 } + +func (c *e4Mul) Define(api frontend.API) error { + var e E4 + e.Mul(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestMulE4(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E4 + a.SetRandom() + b.SetRandom() + c.Mul(&a, &b) + var w e4Mul + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e4Mul{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} diff --git a/std/algebra/native/fields_kb8/e8.go b/std/algebra/native/fields_kb8/e8.go new file mode 100644 index 0000000000..5d82f9d32f --- /dev/null +++ b/std/algebra/native/fields_kb8/e8.go @@ -0,0 +1,170 @@ +package fields_kb8 + +import ( + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/frontend" +) + +type E8 struct { + C0, C1 E4 +} + +func NewE8(v extensions.E8) E8 { + return E8{ + C0: E4{ + B0: E2{A0: v.C0.B0.A0, A1: v.C0.B0.A1}, + B1: E2{A0: v.C0.B1.A0, A1: v.C0.B1.A1}, + }, + C1: E4{ + B0: E2{A0: v.C1.B0.A0, A1: v.C1.B0.A1}, + B1: E2{A0: v.C1.B1.A0, A1: v.C1.B1.A1}, + }, + } +} + +func (e *E8) SetZero() *E8 { + e.C0.SetZero() + e.C1.SetZero() + return e +} + +func (e *E8) SetOne() *E8 { + e.C0.SetOne() + e.C1.SetZero() + return e +} + +func (e *E8) IsZero(api frontend.API) frontend.Variable { + return api.And(e.C0.IsZero(api), e.C1.IsZero(api)) +} + +func (e *E8) assign(e1 []frontend.Variable) { + e.C0.B0.A0 = e1[0] + e.C0.B0.A1 = e1[1] + e.C0.B1.A0 = e1[2] + e.C0.B1.A1 = e1[3] + e.C1.B0.A0 = e1[4] + e.C1.B0.A1 = e1[5] + e.C1.B1.A0 = e1[6] + e.C1.B1.A1 = e1[7] +} + +func (e *E8) Neg(api frontend.API, e1 E8) *E8 { + e.C0.Neg(api, e1.C0) + e.C1.Neg(api, e1.C1) + return e +} + +func (e *E8) Add(api frontend.API, e1, e2 E8) *E8 { + e.C0.Add(api, e1.C0, e2.C0) + e.C1.Add(api, e1.C1, e2.C1) + return e +} + +func (e *E8) Double(api frontend.API, e1 E8) *E8 { + e.C0.Double(api, e1.C0) + e.C1.Double(api, e1.C1) + return e +} + +func (e *E8) Sub(api frontend.API, e1, e2 E8) *E8 { + e.C0.Sub(api, e1.C0, e2.C0) + e.C1.Sub(api, e1.C1, e2.C1) + return e +} + +func (e *E8) Mul(api frontend.API, e1, e2 E8) *E8 { + var l1, l2, u, ac, bd E4 + l1.Add(api, e1.C0, e1.C1) + l2.Add(api, e2.C0, e2.C1) + u.Mul(api, l1, l2) + ac.Mul(api, e1.C0, e2.C0) + bd.Mul(api, e1.C1, e2.C1) + e.C0.MulByNonResidue(api, bd).Add(api, e.C0, ac) + e.C1.Add(api, ac, bd) + e.C1.Sub(api, u, e.C1) + return e +} + +func (e *E8) Square(api frontend.API, x E8) *E8 { + // Quadratic-extension square over E4 with w^2 = v. + var c0, c2, tmp, tmpNR E4 + tmp.MulByNonResidue(api, x.C1) + c0.Add(api, x.C0, x.C1) + tmp.Add(api, tmp, x.C0) + c0.Mul(api, c0, tmp) + + c2.Mul(api, x.C0, x.C1) + e.C1.Double(api, c2) + + tmpNR.MulByNonResidue(api, c2) + e.C0.Sub(api, c0, c2) + e.C0.Sub(api, e.C0, tmpNR) + return e +} + +func (e *E8) MulByFp(api frontend.API, e1 E8, c interface{}) *E8 { + e.C0.MulByFp(api, e1.C0, c) + e.C1.MulByFp(api, e1.C1, c) + return e +} + +func (e *E8) MulByNonResidue(api frontend.API, e1 E8) *E8 { + e.C0.MulByNonResidue(api, e1.C1) + e.C1 = e1.C0 + return e +} + +func (e *E8) coeffs() []frontend.Variable { + return []frontend.Variable{ + e.C0.B0.A0, e.C0.B0.A1, e.C0.B1.A0, e.C0.B1.A1, + e.C1.B0.A0, e.C1.B0.A1, e.C1.B1.A0, e.C1.B1.A1, + } +} + +func (e *E8) Inverse(api frontend.API, e1 E8) *E8 { + in := e1.coeffs() + out, err := api.Compiler().NewHint(inverseE8Hint, 8, in...) + if err != nil { + panic(err) + } + e.assign(out) + var check, one E8 + check.Mul(api, *e, e1) + one.SetOne() + check.AssertIsEqual(api, one) + return e +} + +func (e *E8) DivUnchecked(api frontend.API, e1, e2 E8) *E8 { + in := append(e1.coeffs(), e2.coeffs()...) + out, err := api.Compiler().NewHint(divE8Hint, 8, in...) + if err != nil { + panic(err) + } + e.assign(out) + var check E8 + check.Mul(api, *e, e2) + check.AssertIsEqual(api, e1) + return e +} + +func (e *E8) AssertIsEqual(api frontend.API, other E8) { + e.C0.AssertIsEqual(api, other.C0) + e.C1.AssertIsEqual(api, other.C1) +} + +func (e *E8) IsEqual(api frontend.API, other E8) frontend.Variable { + return api.And(e.C0.IsEqual(api, other.C0), e.C1.IsEqual(api, other.C1)) +} + +func (e *E8) Select(api frontend.API, b frontend.Variable, r1, r2 E8) *E8 { + e.C0.Select(api, b, r1.C0, r2.C0) + e.C1.Select(api, b, r1.C1, r2.C1) + return e +} + +func (e *E8) Assign(a *extensions.E8) { + e.C0.Assign(&a.C0) + e.C1.Assign(&a.C1) +} diff --git a/std/algebra/native/fields_kb8/e8_test.go b/std/algebra/native/fields_kb8/e8_test.go new file mode 100644 index 0000000000..e0c494bdb0 --- /dev/null +++ b/std/algebra/native/fields_kb8/e8_test.go @@ -0,0 +1,91 @@ +package fields_kb8 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" +) + +type e8Add struct{ A, B, C E8 } + +func (c *e8Add) Define(api frontend.API) error { + var e E8 + e.Add(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestAddE8(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E8 + a.SetRandom() + b.SetRandom() + c.Add(&a, &b) + var w e8Add + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e8Add{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type e8Sub struct{ A, B, C E8 } + +func (c *e8Sub) Define(api frontend.API) error { + var e E8 + e.Sub(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestSubE8(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E8 + a.SetRandom() + b.SetRandom() + c.Sub(&a, &b) + var w e8Sub + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e8Sub{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type e8Mul struct{ A, B, C E8 } + +func (c *e8Mul) Define(api frontend.API) error { + var e E8 + e.Mul(api, c.A, c.B) + e.AssertIsEqual(api, c.C) + return nil +} +func TestMulE8(t *testing.T) { + assert := test.NewAssert(t) + var a, b, c extensions.E8 + a.SetRandom() + b.SetRandom() + c.Mul(&a, &b) + var w e8Mul + w.A.Assign(&a) + w.B.Assign(&b) + w.C.Assign(&c) + assert.CheckCircuit(&e8Mul{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type e8Inv struct{ A, C E8 } + +func (c *e8Inv) Define(api frontend.API) error { + var e E8 + e.Inverse(api, c.A) + e.AssertIsEqual(api, c.C) + return nil +} +func TestInverseE8(t *testing.T) { + assert := test.NewAssert(t) + var a, c extensions.E8 + a.SetRandom() + c.Inverse(&a) + var w e8Inv + w.A.Assign(&a) + w.C.Assign(&c) + assert.CheckCircuit(&e8Inv{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} diff --git a/std/algebra/native/fields_kb8/hints.go b/std/algebra/native/fields_kb8/hints.go new file mode 100644 index 0000000000..1929c695b6 --- /dev/null +++ b/std/algebra/native/fields_kb8/hints.go @@ -0,0 +1,68 @@ +package fields_kb8 + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/constraint/solver" +) + +func init() { + solver.RegisterHint(GetHints()...) +} + +func GetHints() []solver.Hint { + return []solver.Hint{divE8Hint, inverseE8Hint} +} + +func divE8Hint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { + if len(inputs) != 16 { + return fmt.Errorf("divE8Hint: expected 16 inputs, got %d", len(inputs)) + } + if len(outputs) != 8 { + return fmt.Errorf("divE8Hint: expected 8 outputs, got %d", len(outputs)) + } + var a, b, c extensions.E8 + SetNativeE8(&a, inputs[:8]) + SetNativeE8(&b, inputs[8:]) + c.Inverse(&b).Mul(&c, &a) + GetNativeE8(&c, outputs) + return nil +} + +func inverseE8Hint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { + if len(inputs) != 8 { + return fmt.Errorf("inverseE8Hint: expected 8 inputs, got %d", len(inputs)) + } + if len(outputs) != 8 { + return fmt.Errorf("inverseE8Hint: expected 8 outputs, got %d", len(outputs)) + } + var a, c extensions.E8 + SetNativeE8(&a, inputs) + c.Inverse(&a) + GetNativeE8(&c, outputs) + return nil +} + +func SetNativeE8(dst *extensions.E8, inputs []*big.Int) { + dst.C0.B0.A0.SetBigInt(inputs[0]) + dst.C0.B0.A1.SetBigInt(inputs[1]) + dst.C0.B1.A0.SetBigInt(inputs[2]) + dst.C0.B1.A1.SetBigInt(inputs[3]) + dst.C1.B0.A0.SetBigInt(inputs[4]) + dst.C1.B0.A1.SetBigInt(inputs[5]) + dst.C1.B1.A0.SetBigInt(inputs[6]) + dst.C1.B1.A1.SetBigInt(inputs[7]) +} + +func GetNativeE8(src *extensions.E8, outputs []*big.Int) { + src.C0.B0.A0.BigInt(outputs[0]) + src.C0.B0.A1.BigInt(outputs[1]) + src.C0.B1.A0.BigInt(outputs[2]) + src.C0.B1.A1.BigInt(outputs[3]) + src.C1.B0.A0.BigInt(outputs[4]) + src.C1.B0.A1.BigInt(outputs[5]) + src.C1.B1.A0.BigInt(outputs[6]) + src.C1.B1.A1.BigInt(outputs[7]) +} diff --git a/std/algebra/native/maptocurve_kb8/doc.go b/std/algebra/native/maptocurve_kb8/doc.go new file mode 100644 index 0000000000..c31fae831b --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/doc.go @@ -0,0 +1,3 @@ +// Package maptocurve_kb8 implements the y-increment map-to-curve gadget for +// the kb8 curve over the KoalaBear field. +package maptocurve_kb8 diff --git a/std/algebra/native/maptocurve_kb8/hints.go b/std/algebra/native/maptocurve_kb8/hints.go new file mode 100644 index 0000000000..2308a80cd1 --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/hints.go @@ -0,0 +1,53 @@ +package maptocurve_kb8 + +import ( + "fmt" + "math/big" + + multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/constraint/solver" +) + +func init() { + solver.RegisterHint(GetHints()...) +} + +// GetHints returns all hint functions used in the package. +func GetHints() []solver.Hint { + return []solver.Hint{yIncrementHint} +} + +func yIncrementHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { + if len(inputs) != 1 { + return fmt.Errorf("yIncrementHint: expected 1 input, got %d", len(inputs)) + } + if len(outputs) != 9 { + return fmt.Errorf("yIncrementHint: expected 9 outputs, got %d", len(outputs)) + } + if !inputs[0].IsUint64() { + return fmt.Errorf("yIncrementHint: input does not fit in uint64") + } + msg := inputs[0].Uint64() + if msg > (1<<16)-1 { + return fmt.Errorf("yIncrementHint: input %d exceeds uint16 range", msg) + } + p, k, err := multisethash.Map(uint16(msg)) + if err != nil { + return err + } + outputs[0].SetUint64(uint64(k)) + getNativeE8(&p.X, outputs[1:]) + return nil +} + +func getNativeE8(src *extensions.E8, outputs []*big.Int) { + src.C0.B0.A0.BigInt(outputs[0]) + src.C0.B0.A1.BigInt(outputs[1]) + src.C0.B1.A0.BigInt(outputs[2]) + src.C0.B1.A1.BigInt(outputs[3]) + src.C1.B0.A0.BigInt(outputs[4]) + src.C1.B0.A1.BigInt(outputs[5]) + src.C1.B1.A0.BigInt(outputs[6]) + src.C1.B1.A1.BigInt(outputs[7]) +} diff --git a/std/algebra/native/maptocurve_kb8/maptocurve.go b/std/algebra/native/maptocurve_kb8/maptocurve.go new file mode 100644 index 0000000000..e748b9c520 --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/maptocurve.go @@ -0,0 +1,55 @@ +package maptocurve_kb8 + +import ( + "errors" + + "github.com/consensys/gnark-crypto/ecc/kb8" + kbfp "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/rangecheck" +) + +const T = 256 + +// YIncrement maps msg to a point on kb8 with y = msg*256 + k. +func YIncrement(api frontend.API, msg frontend.Variable) (G1Affine, error) { + if !IsCompatible(api) { + return G1Affine{}, errors.New("expected KoalaBear native field for kb8 map-to-curve") + } + _ = api.ToBinary(msg, 16) + + res, err := api.Compiler().NewHint(yIncrementHint, 9, msg) + if err != nil { + return G1Affine{}, err + } + k := res[0] + rangecheck.New(api).Check(k, 8) + + x := fromCoeffs(res[1:]) + var y fields_kb8.E8 + y0 := api.Add(api.Mul(msg, T), k) + y.SetZero() + y.C0.B0.A0 = y0 + p := G1Affine{X: x, Y: y} + + assertIsOnCurve(api, &p) + return p, nil +} + +func assertIsOnCurve(api frontend.API, p *G1Affine) { + isInf := api.And(p.X.IsZero(api), p.Y.IsZero(api)) + _, b := kb8.CurveCoefficients() + left := *new(E8).Square(api, p.Y) + x2 := *new(E8).Square(api, p.X) + right := *new(E8).Mul(api, x2, p.X) + right.Sub(api, right, *new(E8).MulByFp(api, p.X, 3)) + right.Add(api, right, newE8(b)) + diff := *new(E8).Sub(api, left, right) + isCurve := diff.IsZero(api) + api.AssertIsEqual(api.Or(isInf, isCurve), 1) +} + +func IsCompatible(api frontend.API) bool { + return api.Compiler().Field().Cmp(kbfp.Modulus()) == 0 +} diff --git a/std/algebra/native/maptocurve_kb8/maptocurve_test.go b/std/algebra/native/maptocurve_kb8/maptocurve_test.go new file mode 100644 index 0000000000..ee36507693 --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/maptocurve_test.go @@ -0,0 +1,36 @@ +package maptocurve_kb8 + +import ( + "testing" + + nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" +) + +type yIncrementCircuit struct { + Msg frontend.Variable + P G1Affine +} + +func (c *yIncrementCircuit) Define(api frontend.API) error { + p, err := YIncrement(api, c.Msg) + if err != nil { + return err + } + p.X.AssertIsEqual(api, c.P.X) + p.Y.AssertIsEqual(api, c.P.Y) + return nil +} + +func TestYIncrement(t *testing.T) { + assert := test.NewAssert(t) + msg := uint16(12345) + p, _, err := nativemsh.Map(msg) + assert.NoError(err) + witness := &yIncrementCircuit{ + Msg: msg, + P: G1Affine{X: newE8(p.X), Y: newE8(p.Y)}, + } + assert.CheckCircuit(&yIncrementCircuit{}, test.WithValidAssignment(witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} diff --git a/std/algebra/native/maptocurve_kb8/types.go b/std/algebra/native/maptocurve_kb8/types.go new file mode 100644 index 0000000000..0a245457a8 --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/types.go @@ -0,0 +1,32 @@ +package maptocurve_kb8 + +import ( + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/fields_kb8" +) + +type E2 = fields_kb8.E2 +type E4 = fields_kb8.E4 +type E8 = fields_kb8.E8 + +type G1Affine struct { + X, Y E8 +} + +func newE8(v extensions.E8) E8 { + return fields_kb8.NewE8(v) +} + +func fromCoeffs(v []frontend.Variable) E8 { + return E8{ + C0: E4{ + B0: E2{A0: v[0], A1: v[1]}, + B1: E2{A0: v[2], A1: v[3]}, + }, + C1: E4{ + B0: E2{A0: v[4], A1: v[5]}, + B1: E2{A0: v[6], A1: v[7]}, + }, + } +} diff --git a/std/algebra/native/sw_kb8/doc.go b/std/algebra/native/sw_kb8/doc.go new file mode 100644 index 0000000000..c06f339a20 --- /dev/null +++ b/std/algebra/native/sw_kb8/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Package sw_kb8 provides native KoalaBear-field circuit gadgets for the kb8 +// elliptic curve and its 1-point multiset-hash construction. The y-increment +// map-to-curve gadget lives in package maptocurve_kb8. +package sw_kb8 diff --git a/std/algebra/native/sw_kb8/g1.go b/std/algebra/native/sw_kb8/g1.go new file mode 100644 index 0000000000..40dc15861b --- /dev/null +++ b/std/algebra/native/sw_kb8/g1.go @@ -0,0 +1,210 @@ +package sw_kb8 + +import ( + "errors" + + "github.com/consensys/gnark-crypto/ecc/kb8" + kbfp "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" +) + +// Curve exposes kb8 point operations in circuits over the KoalaBear field. +type Curve struct { + api frontend.API +} + +var ( + curveA, curveB = func() (E8, E8) { + a, b := kb8.CurveCoefficients() + return fields_kb8.NewE8(a), fields_kb8.NewE8(b) + }() +) + +func fromMapE2(v maptocurve_kb8.E2) E2 { + return E2{A0: v.A0, A1: v.A1} +} + +func fromMapE4(v maptocurve_kb8.E4) E4 { + return E4{ + B0: fromMapE2(v.B0), + B1: fromMapE2(v.B1), + } +} + +func fromMapE8(v maptocurve_kb8.E8) E8 { + return E8{ + C0: fromMapE4(v.C0), + C1: fromMapE4(v.C1), + } +} + +func fromMapPoint(v maptocurve_kb8.G1Affine) G1Affine { + return G1Affine{ + X: fromMapE8(v.X), + Y: fromMapE8(v.Y), + } +} + +// NewCurve initializes a new kb8 curve gadget. +func NewCurve(api frontend.API) (*Curve, error) { + if api.Compiler().Field().Cmp(kbfp.Modulus()) != 0 { + return nil, errors.New("expected KoalaBear native field for kb8 operations") + } + return &Curve{api: api}, nil +} + +// Infinity returns the point at infinity represented as (0,0). +func (c *Curve) Infinity() G1Affine { + var z E8 + z.SetZero() + return G1Affine{X: z, Y: z} +} + +// Neg outputs -p1. +func (p *G1Affine) Neg(api frontend.API, p1 G1Affine) *G1Affine { + p.X = p1.X + p.Y.Neg(api, p1.Y) + return p +} + +// Select sets p1 if b=1, p2 if b=0, and returns it. +func (p *G1Affine) Select(api frontend.API, b frontend.Variable, p1, p2 G1Affine) *G1Affine { + p.X.Select(api, b, p1.X, p2.X) + p.Y.Select(api, b, p1.Y, p2.Y) + return p +} + +// AddAssign adds p1 to p using the affine formulas and returns p. +func (p *G1Affine) AddAssign(api frontend.API, p1 G1Affine) *G1Affine { + var dx, dy, lambda, xr, yr E8 + dx.Sub(api, p1.X, p.X) + dy.Sub(api, p1.Y, p.Y) + lambda.DivUnchecked(api, dy, dx) + xr.Square(api, lambda) + xr.Sub(api, xr, p.X) + xr.Sub(api, xr, p1.X) + yr.Sub(api, p.X, xr) + yr.Mul(api, lambda, yr) + yr.Sub(api, yr, p.Y) + p.X = xr + p.Y = yr + return p +} + +// Double doubles p1 in affine coordinates and returns p. +func (p *G1Affine) Double(api frontend.API, p1 G1Affine) *G1Affine { + var twoY, num, lambda, xr, yr, den, one E8 + twoY.MulByFp(api, p1.Y, 2) + yIsZero := twoY.IsZero(api) + one.SetOne() + den.Select(api, yIsZero, one, twoY) + num.Square(api, p1.X) + num.MulByFp(api, num, 3) + num.Add(api, num, curveA) + lambda.DivUnchecked(api, num, den) + xr.Square(api, lambda) + xr.Sub(api, xr, *new(E8).MulByFp(api, p1.X, 2)) + yr.Sub(api, p1.X, xr) + yr.Mul(api, lambda, yr) + yr.Sub(api, yr, p1.Y) + var inf, res G1Affine + inf.X.SetZero() + inf.Y.SetZero() + res = G1Affine{X: xr, Y: yr} + p.Select(api, yIsZero, inf, res) + return p +} + +// AddUnified adds q to p and handles infinity, doubling, and opposite points. +func (p *G1Affine) AddUnified(api frontend.API, q G1Affine) *G1Affine { + selector1 := api.And(p.X.IsZero(api), p.Y.IsZero(api)) + selector2 := api.And(q.X.IsZero(api), q.Y.IsZero(api)) + var pxqx, pxplusqx, num, den, one, lambda, xr, yr E8 + pxqx.Mul(api, p.X, q.X) + pxplusqx.Add(api, p.X, q.X) + num.Square(api, pxplusqx) + num.Sub(api, num, pxqx) + num.Add(api, num, curveA) + den.Add(api, p.Y, q.Y) + selector3 := den.IsZero(api) + one.SetOne() + den.Select(api, selector3, one, den) + lambda.DivUnchecked(api, num, den) + xr.Square(api, lambda) + xr.Sub(api, xr, pxplusqx) + yr.Sub(api, p.X, xr) + yr.Mul(api, lambda, yr) + yr.Sub(api, yr, p.Y) + result := G1Affine{X: xr, Y: yr} + + var inf G1Affine + inf.X.SetZero() + inf.Y.SetZero() + result.Select(api, selector1, q, result) + result.Select(api, selector2, *p, result) + result.Select(api, selector3, inf, result) + + p.X = result.X + p.Y = result.Y + return p +} + +// DoubleAndAdd computes 2*p1+p2 in affine coordinates and returns p. +func (p *G1Affine) DoubleAndAdd(api frontend.API, p1, p2 *G1Affine) *G1Affine { + var dx, dy, l1, x3, den2, l2, x4, y4 E8 + dx.Sub(api, p1.X, p2.X) + dy.Sub(api, p1.Y, p2.Y) + l1.DivUnchecked(api, dy, dx) + + x3.Square(api, l1) + x3.Sub(api, x3, p1.X) + x3.Sub(api, x3, p2.X) + + den2.Sub(api, x3, p1.X) + l2.MulByFp(api, p1.Y, 2) + l2.DivUnchecked(api, l2, den2) + l2.Add(api, l2, l1) + + x4.Square(api, l2) + x4.Sub(api, x4, p1.X) + x4.Sub(api, x4, x3) + + y4.Sub(api, x4, p1.X) + y4.Mul(api, y4, l2) + y4.Sub(api, y4, p1.Y) + + p.X = x4 + p.Y = y4 + return p +} + +// AssertIsEqual asserts equality of two points. +func (c *Curve) AssertIsEqual(p, q *G1Affine) { + p.X.AssertIsEqual(c.api, q.X) + p.Y.AssertIsEqual(c.api, q.Y) +} + +func (c *Curve) isInfinity(p *G1Affine) frontend.Variable { + return c.api.And(p.X.IsZero(c.api), p.Y.IsZero(c.api)) +} + +// AssertIsOnCurve asserts that p is infinity or lies on kb8. +func (c *Curve) AssertIsOnCurve(p *G1Affine) { + isInf := c.isInfinity(p) + left := *new(E8).Square(c.api, p.Y) + x2 := *new(E8).Square(c.api, p.X) + right := *new(E8).Mul(c.api, x2, p.X) + right.Sub(c.api, right, *new(E8).MulByFp(c.api, p.X, 3)) + right.Add(c.api, right, curveB) + diff := *new(E8).Sub(c.api, left, right) + isCurve := diff.IsZero(c.api) + c.api.AssertIsEqual(c.api.Or(isInf, isCurve), 1) +} + +// AssertIsInSubGroup asserts subgroup membership. kb8 has prime order, so this +// is equivalent to the on-curve check. +func (c *Curve) AssertIsInSubGroup(p *G1Affine) { + c.AssertIsOnCurve(p) +} diff --git a/std/algebra/native/sw_kb8/g1_test.go b/std/algebra/native/sw_kb8/g1_test.go new file mode 100644 index 0000000000..84a33b30a4 --- /dev/null +++ b/std/algebra/native/sw_kb8/g1_test.go @@ -0,0 +1,143 @@ +package sw_kb8 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" +) + +type g1AddAssignAffine struct { + A, B G1Affine + C G1Affine `gnark:",public"` +} + +func (circuit *g1AddAssignAffine) Define(api frontend.API) error { + expected := circuit.A + expected.AddAssign(api, circuit.B) + expected.AssertIsEqual(api, circuit.C) + return nil +} + +func TestAddAssignAffineG1(t *testing.T) { + assert := test.NewAssert(t) + aJac, bJac := distinctPointsG1(t) + var a, b, c kb8.G1Affine + a.FromJacobian(&aJac) + b.FromJacobian(&bJac) + aJac.AddAssign(&bJac) + c.FromJacobian(&aJac) + + var witness g1AddAssignAffine + witness.A.Assign(&a) + witness.B.Assign(&b) + witness.C.Assign(&c) + + assert.CheckCircuit(&g1AddAssignAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type g1DoubleAffine struct { + A G1Affine + C G1Affine `gnark:",public"` +} + +func (circuit *g1DoubleAffine) Define(api frontend.API) error { + expected := G1Affine{} + expected.Double(api, circuit.A) + expected.AssertIsEqual(api, circuit.C) + return nil +} + +func TestDoubleAffineG1(t *testing.T) { + assert := test.NewAssert(t) + aJac := randomPointG1(t) + var a, c kb8.G1Affine + a.FromJacobian(&aJac) + aJac.DoubleAssign() + c.FromJacobian(&aJac) + + var witness g1DoubleAffine + witness.A.Assign(&a) + witness.C.Assign(&c) + + assert.CheckCircuit(&g1DoubleAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type g1AddUnifiedAffine struct { + A, B G1Affine + C G1Affine `gnark:",public"` +} + +func (circuit *g1AddUnifiedAffine) Define(api frontend.API) error { + expected := circuit.A + expected.AddUnified(api, circuit.B) + expected.AssertIsEqual(api, circuit.C) + return nil +} + +func TestAddUnifiedAffineG1(t *testing.T) { + assert := test.NewAssert(t) + aJac, bJac := distinctPointsG1(t) + var a, b, c kb8.G1Affine + a.FromJacobian(&aJac) + b.FromJacobian(&bJac) + aJac.AddAssign(&bJac) + c.FromJacobian(&aJac) + + var witness g1AddUnifiedAffine + witness.A.Assign(&a) + witness.B.Assign(&b) + witness.C.Assign(&c) + + assert.CheckCircuit(&g1AddUnifiedAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +type g1DoubleAndAddAffine struct { + A, B G1Affine + C G1Affine `gnark:",public"` +} + +func (circuit *g1DoubleAndAddAffine) Define(api frontend.API) error { + expected := circuit.A + expected.DoubleAndAdd(api, &circuit.A, &circuit.B) + expected.AssertIsEqual(api, circuit.C) + return nil +} + +func TestDoubleAndAddAffineG1(t *testing.T) { + assert := test.NewAssert(t) + aJac, bJac := distinctPointsG1(t) + var a, b, c kb8.G1Affine + a.FromJacobian(&aJac) + b.FromJacobian(&bJac) + aJac.DoubleAssign().AddAssign(&bJac) + c.FromJacobian(&aJac) + + var witness g1DoubleAndAddAffine + witness.A.Assign(&a) + witness.B.Assign(&b) + witness.C.Assign(&c) + + assert.CheckCircuit(&g1DoubleAndAddAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +func randomPointG1(t *testing.T) kb8.G1Jac { + t.Helper() + _, g := kb8.Generators() + var s kb8.G1Jac + s.FromAffine(&g) + for s.Z.IsZero() { + // impossible path, keep non-zero point invariant + s.FromAffine(&g) + } + return s +} + +func distinctPointsG1(t *testing.T) (kb8.G1Jac, kb8.G1Jac) { + t.Helper() + a := randomPointG1(t) + b := a + b.DoubleAssign() + return a, b +} diff --git a/std/algebra/native/sw_kb8/hints.go b/std/algebra/native/sw_kb8/hints.go new file mode 100644 index 0000000000..50ebc4d180 --- /dev/null +++ b/std/algebra/native/sw_kb8/hints.go @@ -0,0 +1,10 @@ +package sw_kb8 + +import ( + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/std/algebra/native/fields_kb8" +) + +func GetHints() []solver.Hint { + return fields_kb8.GetHints() +} diff --git a/std/algebra/native/sw_kb8/multisethash.go b/std/algebra/native/sw_kb8/multisethash.go new file mode 100644 index 0000000000..757d882e7a --- /dev/null +++ b/std/algebra/native/sw_kb8/multisethash.go @@ -0,0 +1,65 @@ +package sw_kb8 + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" +) + +// Accumulator stores the 1-point multiset hash state. +type Accumulator struct { + curve *Curve + sum G1Affine +} + +// NewAccumulator returns a zero accumulator. +func NewAccumulator(curve *Curve) *Accumulator { + return &Accumulator{ + curve: curve, + sum: curve.Infinity(), + } +} + +// Insert maps msg and adds it to the accumulator. +func (a *Accumulator) Insert(msg frontend.Variable) error { + p, err := maptocurve_kb8.YIncrement(a.curve.api, msg) + if err != nil { + return err + } + pm := fromMapPoint(p) + a.sum.AddUnified(a.curve.api, pm) + return nil +} + +// Remove maps msg and subtracts it from the accumulator. +func (a *Accumulator) Remove(msg frontend.Variable) error { + p, err := maptocurve_kb8.YIncrement(a.curve.api, msg) + if err != nil { + return err + } + pm := fromMapPoint(p) + var neg G1Affine + neg.Neg(a.curve.api, pm) + a.sum.AddUnified(a.curve.api, neg) + return nil +} + +// Digest returns the current digest. +func (a *Accumulator) Digest() G1Affine { + return a.sum +} + +// Reset clears the accumulator. +func (a *Accumulator) Reset() { + a.sum = a.curve.Infinity() +} + +// Hash returns the multiset hash of msgs. +func (c *Curve) Hash(msgs []frontend.Variable) (G1Affine, error) { + acc := NewAccumulator(c) + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + return G1Affine{}, err + } + } + return acc.Digest(), nil +} diff --git a/std/algebra/native/sw_kb8/multisethash_test.go b/std/algebra/native/sw_kb8/multisethash_test.go new file mode 100644 index 0000000000..40a704d05a --- /dev/null +++ b/std/algebra/native/sw_kb8/multisethash_test.go @@ -0,0 +1,108 @@ +package sw_kb8 + +import ( + "testing" + + nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/internal/widecommitter" + "github.com/consensys/gnark/test" +) + +// multisetHashCircuit is the 1-point kb8 multiset-hash verification circuit. +type multisetHashCircuit struct { + Msgs [4]frontend.Variable + Digest G1Affine +} + +func (c *multisetHashCircuit) Define(api frontend.API) error { + curve, err := NewCurve(api) + if err != nil { + return err + } + digest, err := curve.Hash(c.Msgs[:]) + if err != nil { + return err + } + digest.X.AssertIsEqual(api, c.Digest.X) + digest.Y.AssertIsEqual(api, c.Digest.Y) + return nil +} + +func TestHash(t *testing.T) { + assert := test.NewAssert(t) + msgs := []uint16{7, 19, 7, 1024} + d, err := nativemsh.Hash(msgs) + assert.NoError(err) + witness := &multisetHashCircuit{ + Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, + Digest: NewG1Affine(d), + } + invalid := *witness + invalid.Digest.X.C0.B0.A0 = 42 + assert.CheckCircuit(&multisetHashCircuit{}, test.WithValidAssignment(witness), test.WithInvalidAssignment(&invalid), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +func TestHashInvalidDigest(t *testing.T) { + assert := test.NewAssert(t) + msgs := []uint16{1, 2, 3, 4} + d, err := nativemsh.Hash(msgs) + assert.NoError(err) + valid := &multisetHashCircuit{ + Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, + Digest: NewG1Affine(d), + } + invalid := *valid + invalid.Digest.Y.C1.B1.A1 = 17 + assert.CheckCircuit(&multisetHashCircuit{}, test.WithValidAssignment(valid), test.WithInvalidAssignment(&invalid), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +func BenchmarkMultisetHashCircuitSolve(b *testing.B) { + msgs := []uint16{7, 19, 7, 1024} + d, err := nativemsh.Hash(msgs) + if err != nil { + b.Fatal(err) + } + w := &multisetHashCircuit{ + Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, + Digest: NewG1Affine(d), + } + witness, err := frontend.NewWitness(w, koalabear.Modulus()) + if err != nil { + b.Fatal(err) + } + + b.Run("scs", func(b *testing.B) { + var c multisetHashCircuit + ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(scs.NewBuilder), &c) + if err != nil { + b.Fatal(err) + } + b.Log("scs nbConstraints", ccs.GetNbConstraints()) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := ccs.IsSolved(witness); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("r1cs", func(b *testing.B) { + var c multisetHashCircuit + ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(r1cs.NewBuilder), &c, frontend.WithCompressThreshold(10)) + if err != nil { + b.Fatal(err) + } + b.Log("r1cs nbConstraints", ccs.GetNbConstraints()) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := ccs.IsSolved(witness); err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/std/algebra/native/sw_kb8/types.go b/std/algebra/native/sw_kb8/types.go new file mode 100644 index 0000000000..13fed2357d --- /dev/null +++ b/std/algebra/native/sw_kb8/types.go @@ -0,0 +1,29 @@ +package sw_kb8 + +import ( + nativekb8 "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/fields_kb8" +) + +type E2 = fields_kb8.E2 +type E4 = fields_kb8.E4 +type E8 = fields_kb8.E8 + +type G1Affine struct { + X, Y E8 +} + +func NewG1Affine(v nativekb8.G1Affine) G1Affine { + return G1Affine{X: fields_kb8.NewE8(v.X), Y: fields_kb8.NewE8(v.Y)} +} + +func (p *G1Affine) Assign(v *nativekb8.G1Affine) { + p.X.Assign(&v.X) + p.Y.Assign(&v.Y) +} + +func (p *G1Affine) AssertIsEqual(api frontend.API, other G1Affine) { + p.X.AssertIsEqual(api, other.X) + p.Y.AssertIsEqual(api, other.Y) +} diff --git a/std/hints.go b/std/hints.go index 299285c07b..e2696c887e 100644 --- a/std/hints.go +++ b/std/hints.go @@ -11,7 +11,9 @@ import ( "github.com/consensys/gnark/std/algebra/emulated/sw_bw6761" "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" "github.com/consensys/gnark/std/algebra/native/fields_bls12377" + "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" "github.com/consensys/gnark/std/algebra/native/sw_bls12377" + "github.com/consensys/gnark/std/algebra/native/sw_kb8" "github.com/consensys/gnark/std/algebra/native/twistededwards" "github.com/consensys/gnark/std/conversion" "github.com/consensys/gnark/std/evmprecompiles" @@ -63,6 +65,8 @@ func registerHints() { solver.RegisterHint(sw_bw6761.GetHints()...) // native curves solver.RegisterHint(sw_bls12377.GetHints()...) + solver.RegisterHint(maptocurve_kb8.GetHints()...) + solver.RegisterHint(sw_kb8.GetHints()...) // field extensions solver.RegisterHint(fieldextension.GetHints()...) } From 434834faa0c5f67569a02705de97501d51d4f0cc Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 30 Apr 2026 16:14:55 -0400 Subject: [PATCH 02/15] perf: insert-only shifted accumulator --- std/algebra/native/sw_kb8/g1.go | 36 ++++++++++- std/algebra/native/sw_kb8/g1_test.go | 63 +++++++++++++++++++ std/algebra/native/sw_kb8/multisethash.go | 19 +----- .../native/sw_kb8/multisethash_test.go | 21 ++++++- 4 files changed, 118 insertions(+), 21 deletions(-) diff --git a/std/algebra/native/sw_kb8/g1.go b/std/algebra/native/sw_kb8/g1.go index 40dc15861b..db9d194cdb 100644 --- a/std/algebra/native/sw_kb8/g1.go +++ b/std/algebra/native/sw_kb8/g1.go @@ -16,9 +16,10 @@ type Curve struct { } var ( - curveA, curveB = func() (E8, E8) { + curveA, curveB, accumulatorOffset = func() (E8, E8, G1Affine) { a, b := kb8.CurveCoefficients() - return fields_kb8.NewE8(a), fields_kb8.NewE8(b) + _, offsetNative := kb8.Generators() + return fields_kb8.NewE8(a), fields_kb8.NewE8(b), NewG1Affine(offsetNative) }() ) @@ -151,6 +152,37 @@ func (p *G1Affine) AddUnified(api frontend.API, q G1Affine) *G1Affine { return p } +// AddBrierJoye adds q to p using the Brier-Joye/Joye unified affine formula. +// It assumes neither operand is infinity and maps opposite points to infinity. +// Doubling is handled by the same formula. +func (p *G1Affine) AddBrierJoye(api frontend.API, q G1Affine) *G1Affine { + var pxqx, pxplusqx, num, den, one, lambda, xr, yr E8 + pxqx.Mul(api, p.X, q.X) + pxplusqx.Add(api, p.X, q.X) + num.Square(api, pxplusqx) + num.Sub(api, num, pxqx) + num.Add(api, num, curveA) + den.Add(api, p.Y, q.Y) + selector := den.IsZero(api) + one.SetOne() + den.Select(api, selector, one, den) + lambda.DivUnchecked(api, num, den) + xr.Square(api, lambda) + xr.Sub(api, xr, pxplusqx) + yr.Sub(api, p.X, xr) + yr.Mul(api, lambda, yr) + yr.Sub(api, yr, p.Y) + result := G1Affine{X: xr, Y: yr} + + var inf G1Affine + inf.X.SetZero() + inf.Y.SetZero() + result.Select(api, selector, inf, result) + p.X = result.X + p.Y = result.Y + return p +} + // DoubleAndAdd computes 2*p1+p2 in affine coordinates and returns p. func (p *G1Affine) DoubleAndAdd(api frontend.API, p1, p2 *G1Affine) *G1Affine { var dx, dy, l1, x3, den2, l2, x4, y4 E8 diff --git a/std/algebra/native/sw_kb8/g1_test.go b/std/algebra/native/sw_kb8/g1_test.go index 84a33b30a4..10bbb12734 100644 --- a/std/algebra/native/sw_kb8/g1_test.go +++ b/std/algebra/native/sw_kb8/g1_test.go @@ -122,6 +122,69 @@ func TestDoubleAndAddAffineG1(t *testing.T) { assert.CheckCircuit(&g1DoubleAndAddAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) } +type g1AddBrierJoyeAffine struct { + A, B G1Affine + C G1Affine `gnark:",public"` +} + +func (circuit *g1AddBrierJoyeAffine) Define(api frontend.API) error { + expected := circuit.A + expected.AddBrierJoye(api, circuit.B) + expected.AssertIsEqual(api, circuit.C) + return nil +} + +func TestAddBrierJoyeAffineG1(t *testing.T) { + assert := test.NewAssert(t) + aJac, bJac := distinctPointsG1(t) + var a, b, c kb8.G1Affine + a.FromJacobian(&aJac) + b.FromJacobian(&bJac) + aJac.AddAssign(&bJac) + c.FromJacobian(&aJac) + + var witness g1AddBrierJoyeAffine + witness.A.Assign(&a) + witness.B.Assign(&b) + witness.C.Assign(&c) + + assert.CheckCircuit(&g1AddBrierJoyeAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +func TestAddBrierJoyeDoubleG1(t *testing.T) { + assert := test.NewAssert(t) + aJac := randomPointG1(t) + var a, c kb8.G1Affine + a.FromJacobian(&aJac) + aJac.DoubleAssign() + c.FromJacobian(&aJac) + + var witness g1AddBrierJoyeAffine + witness.A.Assign(&a) + witness.B.Assign(&a) + witness.C.Assign(&c) + + assert.CheckCircuit(&g1AddBrierJoyeAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +func TestAddBrierJoyeOppositeG1(t *testing.T) { + assert := test.NewAssert(t) + aJac := randomPointG1(t) + var a kb8.G1Affine + a.FromJacobian(&aJac) + + var negA kb8.G1Affine + negA.Neg(&a) + + var witness g1AddBrierJoyeAffine + witness.A.Assign(&a) + witness.B.Assign(&negA) + witness.C.X.SetZero() + witness.C.Y.SetZero() + + assert.CheckCircuit(&g1AddBrierJoyeAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + func randomPointG1(t *testing.T) kb8.G1Jac { t.Helper() _, g := kb8.Generators() diff --git a/std/algebra/native/sw_kb8/multisethash.go b/std/algebra/native/sw_kb8/multisethash.go index 757d882e7a..6ce3387649 100644 --- a/std/algebra/native/sw_kb8/multisethash.go +++ b/std/algebra/native/sw_kb8/multisethash.go @@ -15,7 +15,7 @@ type Accumulator struct { func NewAccumulator(curve *Curve) *Accumulator { return &Accumulator{ curve: curve, - sum: curve.Infinity(), + sum: accumulatorOffset, } } @@ -26,20 +26,7 @@ func (a *Accumulator) Insert(msg frontend.Variable) error { return err } pm := fromMapPoint(p) - a.sum.AddUnified(a.curve.api, pm) - return nil -} - -// Remove maps msg and subtracts it from the accumulator. -func (a *Accumulator) Remove(msg frontend.Variable) error { - p, err := maptocurve_kb8.YIncrement(a.curve.api, msg) - if err != nil { - return err - } - pm := fromMapPoint(p) - var neg G1Affine - neg.Neg(a.curve.api, pm) - a.sum.AddUnified(a.curve.api, neg) + a.sum.AddBrierJoye(a.curve.api, pm) return nil } @@ -50,7 +37,7 @@ func (a *Accumulator) Digest() G1Affine { // Reset clears the accumulator. func (a *Accumulator) Reset() { - a.sum = a.curve.Infinity() + a.sum = accumulatorOffset } // Hash returns the multiset hash of msgs. diff --git a/std/algebra/native/sw_kb8/multisethash_test.go b/std/algebra/native/sw_kb8/multisethash_test.go index 40a704d05a..bdae876f88 100644 --- a/std/algebra/native/sw_kb8/multisethash_test.go +++ b/std/algebra/native/sw_kb8/multisethash_test.go @@ -3,6 +3,7 @@ package sw_kb8 import ( "testing" + "github.com/consensys/gnark-crypto/ecc/kb8" nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/constraint" @@ -38,9 +39,10 @@ func TestHash(t *testing.T) { msgs := []uint16{7, 19, 7, 1024} d, err := nativemsh.Hash(msgs) assert.NoError(err) + shifted := shiftedDigest(d) witness := &multisetHashCircuit{ Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, - Digest: NewG1Affine(d), + Digest: NewG1Affine(shifted), } invalid := *witness invalid.Digest.X.C0.B0.A0 = 42 @@ -52,9 +54,10 @@ func TestHashInvalidDigest(t *testing.T) { msgs := []uint16{1, 2, 3, 4} d, err := nativemsh.Hash(msgs) assert.NoError(err) + shifted := shiftedDigest(d) valid := &multisetHashCircuit{ Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, - Digest: NewG1Affine(d), + Digest: NewG1Affine(shifted), } invalid := *valid invalid.Digest.Y.C1.B1.A1 = 17 @@ -67,9 +70,10 @@ func BenchmarkMultisetHashCircuitSolve(b *testing.B) { if err != nil { b.Fatal(err) } + shifted := shiftedDigest(d) w := &multisetHashCircuit{ Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, - Digest: NewG1Affine(d), + Digest: NewG1Affine(shifted), } witness, err := frontend.NewWitness(w, koalabear.Modulus()) if err != nil { @@ -106,3 +110,14 @@ func BenchmarkMultisetHashCircuitSolve(b *testing.B) { } }) } + +func shiftedDigest(d kb8.G1Affine) kb8.G1Affine { + _, offset := kb8.Generators() + var jd, jo kb8.G1Jac + jd.FromAffine(&d) + jo.FromAffine(&offset) + jd.AddAssign(&jo) + var shifted kb8.G1Affine + shifted.FromJacobian(&jd) + return shifted +} From c2ead1df8a87133f12e8ced0cad9248ba0f6770c Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 30 Apr 2026 19:17:47 -0400 Subject: [PATCH 03/15] test: up bench --- .../native/sw_kb8/multisethash_test.go | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/std/algebra/native/sw_kb8/multisethash_test.go b/std/algebra/native/sw_kb8/multisethash_test.go index bdae876f88..7b6661e832 100644 --- a/std/algebra/native/sw_kb8/multisethash_test.go +++ b/std/algebra/native/sw_kb8/multisethash_test.go @@ -20,6 +20,11 @@ type multisetHashCircuit struct { Digest G1Affine } +type multisetHashSingleInsertCircuit struct { + Msg frontend.Variable + Digest G1Affine +} + func (c *multisetHashCircuit) Define(api frontend.API) error { curve, err := NewCurve(api) if err != nil { @@ -34,6 +39,20 @@ func (c *multisetHashCircuit) Define(api frontend.API) error { return nil } +func (c *multisetHashSingleInsertCircuit) Define(api frontend.API) error { + curve, err := NewCurve(api) + if err != nil { + return err + } + digest, err := curve.Hash([]frontend.Variable{c.Msg}) + if err != nil { + return err + } + digest.X.AssertIsEqual(api, c.Digest.X) + digest.Y.AssertIsEqual(api, c.Digest.Y) + return nil +} + func TestHash(t *testing.T) { assert := test.NewAssert(t) msgs := []uint16{7, 19, 7, 1024} @@ -65,14 +84,14 @@ func TestHashInvalidDigest(t *testing.T) { } func BenchmarkMultisetHashCircuitSolve(b *testing.B) { - msgs := []uint16{7, 19, 7, 1024} - d, err := nativemsh.Hash(msgs) + msg := uint16(7) + d, err := nativemsh.Hash([]uint16{msg}) if err != nil { b.Fatal(err) } shifted := shiftedDigest(d) - w := &multisetHashCircuit{ - Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, + w := &multisetHashSingleInsertCircuit{ + Msg: msg, Digest: NewG1Affine(shifted), } witness, err := frontend.NewWitness(w, koalabear.Modulus()) @@ -81,7 +100,7 @@ func BenchmarkMultisetHashCircuitSolve(b *testing.B) { } b.Run("scs", func(b *testing.B) { - var c multisetHashCircuit + var c multisetHashSingleInsertCircuit ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(scs.NewBuilder), &c) if err != nil { b.Fatal(err) @@ -96,7 +115,7 @@ func BenchmarkMultisetHashCircuitSolve(b *testing.B) { }) b.Run("r1cs", func(b *testing.B) { - var c multisetHashCircuit + var c multisetHashSingleInsertCircuit ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(r1cs.NewBuilder), &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) From 8069c40ed51f64351c4ab9c9e825d62bff4ae4df Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Fri, 1 May 2026 13:19:22 -0400 Subject: [PATCH 04/15] perf: optimize fp2 mul --- std/algebra/native/fields_kb8/e2.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/std/algebra/native/fields_kb8/e2.go b/std/algebra/native/fields_kb8/e2.go index 55f8d491e8..f575092ffb 100644 --- a/std/algebra/native/fields_kb8/e2.go +++ b/std/algebra/native/fields_kb8/e2.go @@ -58,13 +58,14 @@ func (e *E2) Sub(api frontend.API, e1, e2 E2) *E2 { } func (e *E2) Mul(api frontend.API, e1, e2 E2) *E2 { - l1 := api.Add(e1.A0, e1.A1) - l2 := api.Add(e2.A0, e2.A1) - u := api.Mul(l1, l2) - ac := api.Mul(e1.A0, e2.A0) - bd := api.Mul(e1.A1, e2.A1) - e.A1 = api.Sub(u, api.Add(ac, bd)) - e.A0 = api.Add(ac, api.Mul(bd, uSquare)) + // Schoolbook multiplication: cheaper than Karatsuba in Plonk where M = A = 1 gate. + // c0 = a0*b0 + β*a1*b1, c1 = a0*b1 + a1*b0 (β = uSquare = 3, free as constant mul) + a0b0 := api.Mul(e1.A0, e2.A0) + a1b1 := api.Mul(e1.A1, e2.A1) + a0b1 := api.Mul(e1.A0, e2.A1) + a1b0 := api.Mul(e1.A1, e2.A0) + e.A0 = api.Add(a0b0, api.Mul(uSquare, a1b1)) + e.A1 = api.Add(a0b1, a1b0) return e } From aa71399fa6bf974a3ee3cf904144d3d1dc650eb7 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Fri, 1 May 2026 13:41:23 -0400 Subject: [PATCH 05/15] perf(r1cs): optimize fp2 mul --- std/algebra/native/fields_kb8/e2.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/std/algebra/native/fields_kb8/e2.go b/std/algebra/native/fields_kb8/e2.go index f575092ffb..dd702c7b3e 100644 --- a/std/algebra/native/fields_kb8/e2.go +++ b/std/algebra/native/fields_kb8/e2.go @@ -4,6 +4,7 @@ import ( "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/frontendtype" ) var uSquare = koalabear.NewElement(3) @@ -58,8 +59,31 @@ func (e *E2) Sub(api frontend.API, e1, e2 E2) *E2 { } func (e *E2) Mul(api frontend.API, e1, e2 E2) *E2 { - // Schoolbook multiplication: cheaper than Karatsuba in Plonk where M = A = 1 gate. - // c0 = a0*b0 + β*a1*b1, c1 = a0*b1 + a1*b0 (β = uSquare = 3, free as constant mul) + if ft, ok := api.Compiler().(frontendtype.FrontendTyper); ok { + switch ft.FrontendType() { + case frontendtype.R1CS: + return e.mulKaratsuba(api, e1, e2) + case frontendtype.SCS: + return e.mulSchoolbook(api, e1, e2) + } + } + return e.mulKaratsuba(api, e1, e2) +} + +// mulKaratsuba uses Karatsuba: 3M+5A = 8 gates. Cheaper in R1CS where M >> A. +func (e *E2) mulKaratsuba(api frontend.API, e1, e2 E2) *E2 { + l1 := api.Add(e1.A0, e1.A1) + l2 := api.Add(e2.A0, e2.A1) + u := api.Mul(l1, l2) + ac := api.Mul(e1.A0, e2.A0) + bd := api.Mul(e1.A1, e2.A1) + e.A1 = api.Sub(u, api.Add(ac, bd)) + e.A0 = api.Add(ac, api.Mul(bd, uSquare)) + return e +} + +// mulSchoolbook uses schoolbook: 4M+2A = 6 gates. Cheaper in Plonk where M = A = 1 gate. +func (e *E2) mulSchoolbook(api frontend.API, e1, e2 E2) *E2 { a0b0 := api.Mul(e1.A0, e2.A0) a1b1 := api.Mul(e1.A1, e2.A1) a0b1 := api.Mul(e1.A0, e2.A1) From b99327069628e28b4c5babe5404e673fe823120e Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 4 May 2026 14:26:17 -0400 Subject: [PATCH 06/15] perf: exploit y-sparsity --- .../native/maptocurve_kb8/maptocurve.go | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/std/algebra/native/maptocurve_kb8/maptocurve.go b/std/algebra/native/maptocurve_kb8/maptocurve.go index e748b9c520..a999b8bf46 100644 --- a/std/algebra/native/maptocurve_kb8/maptocurve.go +++ b/std/algebra/native/maptocurve_kb8/maptocurve.go @@ -37,17 +37,28 @@ func YIncrement(api frontend.API, msg frontend.Variable) (G1Affine, error) { return p, nil } +// assertIsOnCurve asserts y² = x³ - 3x + b for a point from the y-increment map. +// +// Optimizations over a generic on-curve check: +// - y is in the base subfield (y = (y0,0,...,0)), so y² = (y0²,0,...,0) costs +// 1 Fp mul instead of a full E8.Square (72 gates). +// - The map never produces infinity, so the isInf branch is removed (~30 gates). +// - The result is checked via direct AssertIsEqual instead of IsZero+Or (~15 gates). func assertIsOnCurve(api frontend.API, p *G1Affine) { - isInf := api.And(p.X.IsZero(api), p.Y.IsZero(api)) _, b := kb8.CurveCoefficients() - left := *new(E8).Square(api, p.Y) + + // y² — exploit that y is in the base subfield: only y.C0.B0.A0 is nonzero + var ySquared E8 + ySquared.SetZero() + ySquared.C0.B0.A0 = api.Mul(p.Y.C0.B0.A0, p.Y.C0.B0.A0) + + // x³ - 3x + b x2 := *new(E8).Square(api, p.X) - right := *new(E8).Mul(api, x2, p.X) - right.Sub(api, right, *new(E8).MulByFp(api, p.X, 3)) - right.Add(api, right, newE8(b)) - diff := *new(E8).Sub(api, left, right) - isCurve := diff.IsZero(api) - api.AssertIsEqual(api.Or(isInf, isCurve), 1) + rhs := *new(E8).Mul(api, x2, p.X) + rhs.Sub(api, rhs, *new(E8).MulByFp(api, p.X, 3)) + rhs.Add(api, rhs, newE8(b)) + + ySquared.AssertIsEqual(api, rhs) } func IsCompatible(api frontend.API) bool { From 037adedd70e13faea5811dfe511bcdd4c2309263 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 4 May 2026 15:54:19 -0400 Subject: [PATCH 07/15] perf: Cube instead of Square+Mul --- std/algebra/native/fields_kb8/e8.go | 31 +++++++++++++++++++ .../native/maptocurve_kb8/maptocurve.go | 5 ++- std/algebra/native/sw_kb8/g1.go | 3 +- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/std/algebra/native/fields_kb8/e8.go b/std/algebra/native/fields_kb8/e8.go index 5d82f9d32f..848fed3ed9 100644 --- a/std/algebra/native/fields_kb8/e8.go +++ b/std/algebra/native/fields_kb8/e8.go @@ -103,6 +103,37 @@ func (e *E8) Square(api frontend.API, x E8) *E8 { return e } +// Cube computes e = x³ directly, cheaper than Square + Mul. +// +// With x = (A, B) in E4[w]/(w²−v): +// +// x³.C0 = A·(A² + 3v·B²) +// x³.C1 = B·(3·A² + v·B²) +// +// Cost: 2 E4.Square + 2 E4.Mul + adds ≈ 104 SCS gates +// vs Square(72) + Mul(176) = 176 SCS gates. Saves 72 gates. +func (e *E8) Cube(api frontend.API, x E8) *E8 { + var a2, b2, t1, t2 E4 + + a2.Square(api, x.C0) // A² + b2.Square(api, x.C1) // B² + + // t1 = A² + 3·v·B² (v·B² = NR(B²), then scale by 3) + t1.MulByNonResidue(api, b2) + t1.MulByFp(api, t1, 3) + t1.Add(api, a2, t1) + + // t2 = 3·A² + v·B² + t2.MulByNonResidue(api, b2) + var a2x3 E4 + a2x3.MulByFp(api, a2, 3) + t2.Add(api, a2x3, t2) + + e.C0.Mul(api, x.C0, t1) // A·t1 + e.C1.Mul(api, x.C1, t2) // B·t2 + return e +} + func (e *E8) MulByFp(api frontend.API, e1 E8, c interface{}) *E8 { e.C0.MulByFp(api, e1.C0, c) e.C1.MulByFp(api, e1.C1, c) diff --git a/std/algebra/native/maptocurve_kb8/maptocurve.go b/std/algebra/native/maptocurve_kb8/maptocurve.go index a999b8bf46..68181c5225 100644 --- a/std/algebra/native/maptocurve_kb8/maptocurve.go +++ b/std/algebra/native/maptocurve_kb8/maptocurve.go @@ -52,9 +52,8 @@ func assertIsOnCurve(api frontend.API, p *G1Affine) { ySquared.SetZero() ySquared.C0.B0.A0 = api.Mul(p.Y.C0.B0.A0, p.Y.C0.B0.A0) - // x³ - 3x + b - x2 := *new(E8).Square(api, p.X) - rhs := *new(E8).Mul(api, x2, p.X) + // x³ - 3x + b — use direct Cube (104 gates) instead of Square+Mul (176 gates) + rhs := *new(E8).Cube(api, p.X) rhs.Sub(api, rhs, *new(E8).MulByFp(api, p.X, 3)) rhs.Add(api, rhs, newE8(b)) diff --git a/std/algebra/native/sw_kb8/g1.go b/std/algebra/native/sw_kb8/g1.go index db9d194cdb..d9bbee658b 100644 --- a/std/algebra/native/sw_kb8/g1.go +++ b/std/algebra/native/sw_kb8/g1.go @@ -226,8 +226,7 @@ func (c *Curve) isInfinity(p *G1Affine) frontend.Variable { func (c *Curve) AssertIsOnCurve(p *G1Affine) { isInf := c.isInfinity(p) left := *new(E8).Square(c.api, p.Y) - x2 := *new(E8).Square(c.api, p.X) - right := *new(E8).Mul(c.api, x2, p.X) + right := *new(E8).Cube(c.api, p.X) right.Sub(c.api, right, *new(E8).MulByFp(c.api, p.X, 3)) right.Add(c.api, right, curveB) diff := *new(E8).Sub(c.api, left, right) From c02f37050eb850433b5a51cd9842c7ac8026ec45 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 4 May 2026 16:03:23 -0400 Subject: [PATCH 08/15] perf: use incomplete add with offset --- std/algebra/native/sw_kb8/multisethash.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/std/algebra/native/sw_kb8/multisethash.go b/std/algebra/native/sw_kb8/multisethash.go index 6ce3387649..b2bedcd263 100644 --- a/std/algebra/native/sw_kb8/multisethash.go +++ b/std/algebra/native/sw_kb8/multisethash.go @@ -26,7 +26,15 @@ func (a *Accumulator) Insert(msg frontend.Variable) error { return err } pm := fromMapPoint(p) - a.sum.AddBrierJoye(a.curve.api, pm) + // Use AddAssign (incomplete addition) instead of AddBrierJoye (unified). + // This is safe because the fixed offset G ensures the accumulator is never + // the identity or the negation of the incoming point: + // - acc starts at G ≠ O, so acc.X ≠ 0 always (no infinity input) + // - acc = G + Σ Map(mᵢ) ≠ ±Map(m) with overwhelming probability + // (would require G = -Σ Map(mᵢ) ± Map(m), negligible over 2^248 group) + // A malicious prover hitting P = ±Q causes division by zero, which the + // constraint system rejects (unsatisfiable). + a.sum.AddAssign(a.curve.api, pm) return nil } From bcb6b85af8e9a4f05b4a66e0dbcb7897cf111b02 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 5 May 2026 12:40:55 -0400 Subject: [PATCH 09/15] perf: schoolbook square --- std/algebra/native/fields_kb8/e2.go | 25 +++++++++++++++++++- std/algebra/native/fields_kb8/e2_test.go | 19 +++++++++++++++ std/algebra/native/fields_kb8/e4.go | 30 +++++++++++++++++++++++- std/algebra/native/fields_kb8/e4_test.go | 19 +++++++++++++++ std/algebra/native/fields_kb8/e8.go | 29 ++++++++++++++++++++++- std/algebra/native/fields_kb8/e8_test.go | 19 +++++++++++++++ 6 files changed, 138 insertions(+), 3 deletions(-) diff --git a/std/algebra/native/fields_kb8/e2.go b/std/algebra/native/fields_kb8/e2.go index dd702c7b3e..b46aa441a4 100644 --- a/std/algebra/native/fields_kb8/e2.go +++ b/std/algebra/native/fields_kb8/e2.go @@ -94,7 +94,20 @@ func (e *E2) mulSchoolbook(api frontend.API, e1, e2 E2) *E2 { } func (e *E2) Square(api frontend.API, x E2) *E2 { - // Algorithm 22 from https://eprint.iacr.org/2010/354.pdf adapted to u^2 = 3. + if ft, ok := api.Compiler().(frontendtype.FrontendTyper); ok { + switch ft.FrontendType() { + case frontendtype.R1CS: + return e.squareKaratsuba(api, x) + case frontendtype.SCS: + return e.squareSchoolbook(api, x) + } + } + return e.squareKaratsuba(api, x) +} + +// squareKaratsuba uses Algorithm 22 from https://eprint.iacr.org/2010/354.pdf +// adapted to u^2 = 3: 2M + 3A + 3 const-muls. Cheaper in R1CS where M >> A. +func (e *E2) squareKaratsuba(api frontend.API, x E2) *E2 { c0 := api.Add(x.A0, x.A1) c2 := api.Mul(x.A1, uSquare) c2 = api.Add(c2, x.A0) @@ -108,6 +121,16 @@ func (e *E2) Square(api frontend.API, x E2) *E2 { return e } +// squareSchoolbook uses 3M + 1A + 2 const-muls. Cheaper in Plonk where M = A = 1 gate. +func (e *E2) squareSchoolbook(api frontend.API, x E2) *E2 { + a2 := api.Mul(x.A0, x.A0) + b2 := api.Mul(x.A1, x.A1) + ab := api.Mul(x.A0, x.A1) + e.A0 = api.Add(a2, api.Mul(b2, uSquare)) + e.A1 = api.Mul(ab, 2) + return e +} + func (e *E2) MulByFp(api frontend.API, e1 E2, c interface{}) *E2 { e.A0 = api.Mul(e1.A0, c) e.A1 = api.Mul(e1.A1, c) diff --git a/std/algebra/native/fields_kb8/e2_test.go b/std/algebra/native/fields_kb8/e2_test.go index dce64cc0dc..3e4adb27aa 100644 --- a/std/algebra/native/fields_kb8/e2_test.go +++ b/std/algebra/native/fields_kb8/e2_test.go @@ -70,3 +70,22 @@ func TestMulE2(t *testing.T) { w.C.Assign(&c) assert.CheckCircuit(&e2Mul{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) } + +type e2Square struct{ A, C E2 } + +func (c *e2Square) Define(api frontend.API) error { + var e E2 + e.Square(api, c.A) + e.AssertIsEqual(api, c.C) + return nil +} +func TestSquareE2(t *testing.T) { + assert := test.NewAssert(t) + var a, c extensions.E2 + a.SetRandom() + c.Square(&a) + var w e2Square + w.A.Assign(&a) + w.C.Assign(&c) + assert.CheckCircuit(&e2Square{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} diff --git a/std/algebra/native/fields_kb8/e4.go b/std/algebra/native/fields_kb8/e4.go index 1c72207862..af4ada1113 100644 --- a/std/algebra/native/fields_kb8/e4.go +++ b/std/algebra/native/fields_kb8/e4.go @@ -3,6 +3,7 @@ package fields_kb8 import ( "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/frontendtype" ) type E4 struct { @@ -70,7 +71,20 @@ func (e *E4) Mul(api frontend.API, e1, e2 E4) *E4 { } func (e *E4) Square(api frontend.API, x E4) *E4 { - // Quadratic-extension square over E2 with v^2 = u. + if ft, ok := api.Compiler().(frontendtype.FrontendTyper); ok { + switch ft.FrontendType() { + case frontendtype.R1CS: + return e.squareKaratsuba(api, x) + case frontendtype.SCS: + return e.squareSchoolbook(api, x) + } + } + return e.squareKaratsuba(api, x) +} + +// squareKaratsuba uses the complex method (Algo 22 over E2): 2 E2.Mul + linear ops. +// Cheaper in R1CS where the inner Mul cost (3 R1CS muls) dominates. +func (e *E4) squareKaratsuba(api frontend.API, x E4) *E4 { var c0, c2, tmp, tmpNR E2 tmp.MulByNonResidue(api, x.B1) c0.Add(api, x.B0, x.B1) @@ -86,6 +100,20 @@ func (e *E4) Square(api frontend.API, x E4) *E4 { return e } +// squareSchoolbook: x = a + bv ⇒ x² = (a² + β·b²) + 2ab·v. +// 2 E2.Sqr + 1 E2.Mul + 1 MulByNR + 1 add + 1 double. Cheaper in Plonk where +// each linear op also costs a gate. +func (e *E4) squareSchoolbook(api frontend.API, x E4) *E4 { + var a2, b2, ab, bbeta E2 + a2.Square(api, x.B0) + b2.Square(api, x.B1) + ab.Mul(api, x.B0, x.B1) + bbeta.MulByNonResidue(api, b2) + e.B0.Add(api, a2, bbeta) + e.B1.Double(api, ab) + return e +} + func (e *E4) MulByFp(api frontend.API, e1 E4, c interface{}) *E4 { e.B0.MulByFp(api, e1.B0, c) e.B1.MulByFp(api, e1.B1, c) diff --git a/std/algebra/native/fields_kb8/e4_test.go b/std/algebra/native/fields_kb8/e4_test.go index 0b5fd38760..a6b9746f4b 100644 --- a/std/algebra/native/fields_kb8/e4_test.go +++ b/std/algebra/native/fields_kb8/e4_test.go @@ -70,3 +70,22 @@ func TestMulE4(t *testing.T) { w.C.Assign(&c) assert.CheckCircuit(&e4Mul{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) } + +type e4Square struct{ A, C E4 } + +func (c *e4Square) Define(api frontend.API) error { + var e E4 + e.Square(api, c.A) + e.AssertIsEqual(api, c.C) + return nil +} +func TestSquareE4(t *testing.T) { + assert := test.NewAssert(t) + var a, c extensions.E4 + a.SetRandom() + c.Square(&a) + var w e4Square + w.A.Assign(&a) + w.C.Assign(&c) + assert.CheckCircuit(&e4Square{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} diff --git a/std/algebra/native/fields_kb8/e8.go b/std/algebra/native/fields_kb8/e8.go index 848fed3ed9..c4455b06ed 100644 --- a/std/algebra/native/fields_kb8/e8.go +++ b/std/algebra/native/fields_kb8/e8.go @@ -3,6 +3,7 @@ package fields_kb8 import ( "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/frontendtype" ) type E8 struct { @@ -87,7 +88,20 @@ func (e *E8) Mul(api frontend.API, e1, e2 E8) *E8 { } func (e *E8) Square(api frontend.API, x E8) *E8 { - // Quadratic-extension square over E4 with w^2 = v. + if ft, ok := api.Compiler().(frontendtype.FrontendTyper); ok { + switch ft.FrontendType() { + case frontendtype.R1CS: + return e.squareKaratsuba(api, x) + case frontendtype.SCS: + return e.squareSchoolbook(api, x) + } + } + return e.squareKaratsuba(api, x) +} + +// squareKaratsuba uses the complex method (Algo 22 over E4): 2 E4.Mul + linear ops. +// Cheaper in R1CS where the inner Mul cost dominates. +func (e *E8) squareKaratsuba(api frontend.API, x E8) *E8 { var c0, c2, tmp, tmpNR E4 tmp.MulByNonResidue(api, x.C1) c0.Add(api, x.C0, x.C1) @@ -103,6 +117,19 @@ func (e *E8) Square(api frontend.API, x E8) *E8 { return e } +// squareSchoolbook: x = A + Bw ⇒ x² = (A² + γ·B²) + 2AB·w. +// 2 E4.Sqr + 1 E4.Mul + 1 MulByNR + 1 add + 1 double. Cheaper in Plonk. +func (e *E8) squareSchoolbook(api frontend.API, x E8) *E8 { + var a2, b2, ab, bgamma E4 + a2.Square(api, x.C0) + b2.Square(api, x.C1) + ab.Mul(api, x.C0, x.C1) + bgamma.MulByNonResidue(api, b2) + e.C0.Add(api, a2, bgamma) + e.C1.Double(api, ab) + return e +} + // Cube computes e = x³ directly, cheaper than Square + Mul. // // With x = (A, B) in E4[w]/(w²−v): diff --git a/std/algebra/native/fields_kb8/e8_test.go b/std/algebra/native/fields_kb8/e8_test.go index e0c494bdb0..4d31a82731 100644 --- a/std/algebra/native/fields_kb8/e8_test.go +++ b/std/algebra/native/fields_kb8/e8_test.go @@ -71,6 +71,25 @@ func TestMulE8(t *testing.T) { assert.CheckCircuit(&e8Mul{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) } +type e8Square struct{ A, C E8 } + +func (c *e8Square) Define(api frontend.API) error { + var e E8 + e.Square(api, c.A) + e.AssertIsEqual(api, c.C) + return nil +} +func TestSquareE8(t *testing.T) { + assert := test.NewAssert(t) + var a, c extensions.E8 + a.SetRandom() + c.Square(&a) + var w e8Square + w.A.Assign(&a) + w.C.Assign(&c) + assert.CheckCircuit(&e8Square{}, test.WithValidAssignment(&w), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + type e8Inv struct{ A, C E8 } func (c *e8Inv) Define(api frontend.API) error { From d78003c0fec377fb5670a9d792eb1778bca173e3 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 18 May 2026 16:52:25 -0400 Subject: [PATCH 10/15] feat: add vector/pq multiset hash --- .../native/maptocurve_kb8/hints_linear.go | 44 +++++ .../native/maptocurve_kb8/hints_poseidon2.go | 51 +++++ .../maptocurve_vector_linear.go | 87 +++++++++ .../maptocurve_vector_poseidon2.go | 129 ++++++++++++ .../sw_kb8/vector_multisethash_linear.go | 64 ++++++ .../sw_kb8/vector_multisethash_linear_test.go | 176 +++++++++++++++++ .../sw_kb8/vector_multisethash_poseidon2.go | 71 +++++++ .../vector_multisethash_poseidon2_test.go | 184 ++++++++++++++++++ std/permutation/poseidon2/poseidon2.go | 45 +++++ .../poseidon2/poseidon2_koalabear.go | 72 +++++++ .../poseidon2/poseidon2_koalabear_test.go | 63 ++++++ 11 files changed, 986 insertions(+) create mode 100644 std/algebra/native/maptocurve_kb8/hints_linear.go create mode 100644 std/algebra/native/maptocurve_kb8/hints_poseidon2.go create mode 100644 std/algebra/native/maptocurve_kb8/maptocurve_vector_linear.go create mode 100644 std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go create mode 100644 std/algebra/native/sw_kb8/vector_multisethash_linear.go create mode 100644 std/algebra/native/sw_kb8/vector_multisethash_linear_test.go create mode 100644 std/algebra/native/sw_kb8/vector_multisethash_poseidon2.go create mode 100644 std/algebra/native/sw_kb8/vector_multisethash_poseidon2_test.go create mode 100644 std/permutation/poseidon2/poseidon2_koalabear.go create mode 100644 std/permutation/poseidon2/poseidon2_koalabear_test.go diff --git a/std/algebra/native/maptocurve_kb8/hints_linear.go b/std/algebra/native/maptocurve_kb8/hints_linear.go new file mode 100644 index 0000000000..44720174b9 --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/hints_linear.go @@ -0,0 +1,44 @@ +package maptocurve_kb8 + +import ( + "fmt" + "math/big" + + multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark/constraint/solver" +) + +func init() { + solver.RegisterHint(yIncrementLinearHint) +} + +// yIncrementLinearHint maps msg to LinearN points natively (using gnark-crypto's +// MapLinear) and writes (k_i, x_i.coeffs[8]) for each coordinate, in order. +// 9 outputs per coordinate * LinearN coordinates. +func yIncrementLinearHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { + if len(inputs) != 1 { + return fmt.Errorf("yIncrementLinearHint: expected 1 input, got %d", len(inputs)) + } + const coeffsPerCoord = 9 + if len(outputs) != LinearN*coeffsPerCoord { + return fmt.Errorf("yIncrementLinearHint: expected %d outputs, got %d", LinearN*coeffsPerCoord, len(outputs)) + } + if !inputs[0].IsUint64() { + return fmt.Errorf("yIncrementLinearHint: input does not fit in uint64") + } + msg := inputs[0].Uint64() + if msg >= LinearM { + return fmt.Errorf("yIncrementLinearHint: input %d exceeds LinearM = %d", msg, LinearM) + } + + pts, ks, err := multisethash.MapLinear(uint32(msg)) + if err != nil { + return err + } + for i := 0; i < LinearN; i++ { + base := outputs[i*coeffsPerCoord:] + base[0].SetUint64(uint64(ks[i])) + getNativeE8(&pts[i].X, base[1:coeffsPerCoord]) + } + return nil +} diff --git a/std/algebra/native/maptocurve_kb8/hints_poseidon2.go b/std/algebra/native/maptocurve_kb8/hints_poseidon2.go new file mode 100644 index 0000000000..ec677857ac --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/hints_poseidon2.go @@ -0,0 +1,51 @@ +package maptocurve_kb8 + +import ( + "fmt" + "math/big" + + multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark/constraint/solver" +) + +func init() { + solver.RegisterHint(yIncrementPoseidon2Hint) +} + +// yIncrementPoseidon2Hint, given the PqN squeezed koalabear elements (already +// computed in-circuit by the Poseidon2 sponge), produces per-coordinate +// (q, s, k, x_coeffs[8]) where: +// - q*B + s = squeezed[i] with s < B = ⌊p/(2T)⌋ +// - k < PqT and y = PqT*s + k yields a valid kb8 point with abscissa x. +// +// The cubic solve runs natively via gnark-crypto's MapAtSlot helper. +func yIncrementPoseidon2Hint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { + if len(inputs) != PqN { + return fmt.Errorf("yIncrementPoseidon2Hint: expected %d inputs, got %d", PqN, len(inputs)) + } + if len(outputs) != PqN*pqOutputsPerCoord { + return fmt.Errorf("yIncrementPoseidon2Hint: expected %d outputs, got %d", PqN*pqOutputsPerCoord, len(outputs)) + } + + bound := multisethash.PqReducerBound() + var q, s big.Int + for i := 0; i < PqN; i++ { + u := new(big.Int).Set(inputs[i]) + q.DivMod(u, bound, &s) + if !s.IsUint64() { + return fmt.Errorf("yIncrementPoseidon2Hint: slot for coord %d does not fit in uint64", i) + } + + pt, k, err := multisethash.MapAtSlot(s.Uint64()) + if err != nil { + return err + } + + base := outputs[i*pqOutputsPerCoord:] + base[0].Set(&q) + base[1].Set(&s) + base[2].SetUint64(uint64(k)) + getNativeE8(&pt.X, base[3:pqOutputsPerCoord]) + } + return nil +} diff --git a/std/algebra/native/maptocurve_kb8/maptocurve_vector_linear.go b/std/algebra/native/maptocurve_kb8/maptocurve_vector_linear.go new file mode 100644 index 0000000000..87d73deba9 --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/maptocurve_vector_linear.go @@ -0,0 +1,87 @@ +package maptocurve_kb8 + +import ( + "errors" + + "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/rangecheck" +) + +// Linear-separator vector ECMSH parameters (paper §4, App. B "T=128" row). +// These must match the native side (ecc/kb8/multiset-hash/vector_multiset_hash_linear.go). +const ( + LinearN = 23 + LinearT = 128 + LinearM = 1 << 18 +) + +// MapLinear maps msg to LinearN points on kb8 using the linear domain +// separator y_i(msg, k_i) = LinearT*(msg + i*LinearM) + k_i. +// +// The expensive cubic solve for each coordinate is performed outside the +// circuit by yIncrementLinearHint; the circuit only enforces: +// - msg < LinearM (binary decomposition) +// - k_i < LinearT (range check) +// - y_i = LinearT*(msg + i*LinearM) + k_i in the base subfield +// - y_i² = x_i³ - 3 x_i + b in Fp^8 +// +// Inverse-freeness is structural: LinearN*LinearM*LinearT = 23*2^18*128 < p/2. +func MapLinear(api frontend.API, msg frontend.Variable) ([LinearN]G1Affine, error) { + var pts [LinearN]G1Affine + + if !IsCompatible(api) { + return pts, errors.New("expected KoalaBear native field for kb8 linear map-to-curve") + } + + // msg < 2^18 = LinearM + _ = api.ToBinary(msg, 18) + + const coeffsPerCoord = 9 // 1 tweak k + 8 E8 coefficients of x + out, err := api.Compiler().NewHint(yIncrementLinearHint, LinearN*coeffsPerCoord, msg) + if err != nil { + return pts, err + } + + _, b := kb8.CurveCoefficients() + bE8 := newE8(b) + + rc := rangecheck.New(api) + for i := 0; i < LinearN; i++ { + base := out[i*coeffsPerCoord:] + k := base[0] + x := fromCoeffs(base[1:coeffsPerCoord]) + + // k_i < LinearT = 128 ⇒ 7 bits + rc.Check(k, 7) + + // baseY_i = LinearT * (msg + i*LinearM). The (i*LinearM) term is a + // compile-time constant, so the api.Add folds into a linear combination. + baseY := api.Mul(LinearT, api.Add(msg, i*LinearM)) + + var y fields_kb8.E8 + y.SetZero() + y.C0.B0.A0 = api.Add(baseY, k) + p := G1Affine{X: x, Y: y} + + assertIsOnCurveWithB(api, &p, bE8) + pts[i] = p + } + return pts, nil +} + +// assertIsOnCurveWithB is the per-coordinate version of assertIsOnCurve that +// takes the precomputed b ∈ Fp^8 to avoid recomputing CurveCoefficients in the +// inner loop. Behaviour matches assertIsOnCurve in maptocurve.go. +func assertIsOnCurveWithB(api frontend.API, p *G1Affine, bE8 fields_kb8.E8) { + var ySquared fields_kb8.E8 + ySquared.SetZero() + ySquared.C0.B0.A0 = api.Mul(p.Y.C0.B0.A0, p.Y.C0.B0.A0) + + rhs := *new(fields_kb8.E8).Cube(api, p.X) + rhs.Sub(api, rhs, *new(fields_kb8.E8).MulByFp(api, p.X, 3)) + rhs.Add(api, rhs, bE8) + + ySquared.AssertIsEqual(api, rhs) +} diff --git a/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go b/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go new file mode 100644 index 0000000000..2edc21113e --- /dev/null +++ b/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go @@ -0,0 +1,129 @@ +package maptocurve_kb8 + +import ( + "encoding/binary" + "errors" + + "github.com/consensys/gnark-crypto/ecc/kb8" + multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/permutation/poseidon2" + "github.com/consensys/gnark/std/rangecheck" +) + +// Poseidon2-sponge vector ECMSH parameters (paper §4.3 "preferred" derivation). +// These must match the native side +// (ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go). +const ( + PqN = 23 + PqT = 256 + PqWidth = 16 + PqSqueezeRate = 8 + PqPermutations = 3 // ceil(PqN / PqSqueezeRate) + pqRangeS = 23 // ⌊p/(2T)⌋ = 4_161_536 < 2^23 (with T=256) + pqRangeQ = 9 // q ≤ ⌊(p-1)/B⌋ = 2T-1 = 511 < 2^9 + pqRangeK = 8 // k < T = 256 + pqOutputsPerCoord = 11 // q, s, k + 8 E8 coefficients of x +) + +// MapPoseidon2 maps a 64-bit message (split into a low and a high 32-bit half +// to fit into the koalabear field) to PqN points on kb8 using a width-16 +// Poseidon2 sponge with rate PqSqueezeRate. +// +// Both halves are expected to be ≤ 2^32 − 1. The function constrains each +// half to fit in 32 bits. +// +// The expensive cubic solve per coordinate is performed outside the circuit by +// yIncrementPoseidon2Hint; the circuit only enforces: +// - msgLow, msgHigh < 2^32 +// - Poseidon2(state) computed in-circuit matches the squeezed slots fed to +// the range-reduction +// - For each i: squeezed[i] = q_i * B + s_i with s_i < B, q_i < 2T +// - k_i < T +// - y_i = T * s_i + k_i in the base subfield +// - y_i² = x_i³ - 3 x_i + b in Fp^8 +// +// Inverse-freeness is structural: s_i < ⌊p/(2T)⌋ ⇒ y_i < p/2. +func MapPoseidon2(api frontend.API, msgLow, msgHigh frontend.Variable) ([PqN]G1Affine, error) { + var pts [PqN]G1Affine + + if !IsCompatible(api) { + return pts, errors.New("expected KoalaBear native field for kb8 Poseidon2 map-to-curve") + } + + // Constrain msgLow, msgHigh < 2^32. + _ = api.ToBinary(msgLow, 32) + _ = api.ToBinary(msgHigh, 32) + + // Build sponge state and absorb (domainTag, msgLow, msgHigh) into the + // rate slots. The 8-byte tag is split into two big-endian uint32 halves to + // match the native packing in vector_multiset_hash_poseidon2.go. + tag := multisethash.PqDomainTag() + tag0 := binary.BigEndian.Uint32(tag[0:4]) + tag1 := binary.BigEndian.Uint32(tag[4:8]) + state := make([]frontend.Variable, PqWidth) + state[0] = tag0 + state[1] = tag1 + state[2] = msgLow + state[3] = msgHigh + for i := 4; i < PqWidth; i++ { + state[i] = frontend.Variable(0) + } + + perm, err := poseidon2.NewPoseidon2FromParameters(api, PqWidth, 6, 21) + if err != nil { + return pts, err + } + + // 3 squeeze permutations, 8 elements each → 24 squeezed (23 used). + squeezed := make([]frontend.Variable, PqPermutations*PqSqueezeRate) + for p := 0; p < PqPermutations; p++ { + if err := perm.Permutation(state); err != nil { + return pts, err + } + copy(squeezed[p*PqSqueezeRate:(p+1)*PqSqueezeRate], state[:PqSqueezeRate]) + } + + // Hint inputs: the 23 squeezed values used. The hint computes (q, s, k, x) + // per coordinate so the in-circuit code only verifies the relations. + hintInputs := make([]frontend.Variable, PqN) + copy(hintInputs, squeezed[:PqN]) + out, err := api.Compiler().NewHint(yIncrementPoseidon2Hint, PqN*pqOutputsPerCoord, hintInputs...) + if err != nil { + return pts, err + } + + _, b := kb8.CurveCoefficients() + bE8 := newE8(b) + bound := multisethash.PqReducerBound() + + rc := rangecheck.New(api) + for i := 0; i < PqN; i++ { + base := out[i*pqOutputsPerCoord:] + q := base[0] + s := base[1] + k := base[2] + x := fromCoeffs(base[3:pqOutputsPerCoord]) + + // squeezed[i] = q * B + s + api.AssertIsEqual(squeezed[i], api.Add(api.Mul(q, bound), s)) + + // Range checks. (We range-check s to one less bit than the bound width + // since s < B = 4_161_536 ⇒ s ≤ 4_161_535 < 2^pqRangeS.) + rc.Check(s, pqRangeS) + rc.Check(q, pqRangeQ) + rc.Check(k, pqRangeK) + + // baseY = T * s; y = (T*s + k, 0, ..., 0) in E8. + baseY := api.Mul(PqT, s) + var y fields_kb8.E8 + y.SetZero() + y.C0.B0.A0 = api.Add(baseY, k) + pt := G1Affine{X: x, Y: y} + + assertIsOnCurveWithB(api, &pt, bE8) + pts[i] = pt + } + return pts, nil +} diff --git a/std/algebra/native/sw_kb8/vector_multisethash_linear.go b/std/algebra/native/sw_kb8/vector_multisethash_linear.go new file mode 100644 index 0000000000..d480710b8c --- /dev/null +++ b/std/algebra/native/sw_kb8/vector_multisethash_linear.go @@ -0,0 +1,64 @@ +package sw_kb8 + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" +) + +// LinearAccumulator stores the N-coordinate linear-separator vector ECMSH state. +// Each coordinate accumulator starts at the fixed offset point G and uses the +// same incomplete-addition safety argument as the one-point Accumulator: with +// G ≠ O, the per-coordinate sum is never the identity, and a malicious prover +// hitting acc_i = ±Map_i(m) succeeds with negligible probability over the 2^248 +// group (and produces an unsatisfiable division by zero otherwise). +type LinearAccumulator struct { + curve *Curve + sums [maptocurve_kb8.LinearN]G1Affine +} + +// NewLinearAccumulator returns a zero linear accumulator. Each coordinate is +// initialized to the fixed offset generator. +func NewLinearAccumulator(curve *Curve) *LinearAccumulator { + a := &LinearAccumulator{curve: curve} + for i := range a.sums { + a.sums[i] = accumulatorOffset + } + return a +} + +// Insert maps msg via the linear separator and adds each of the N mapped +// points to the matching accumulator coordinate. +func (a *LinearAccumulator) Insert(msg frontend.Variable) error { + pts, err := maptocurve_kb8.MapLinear(a.curve.api, msg) + if err != nil { + return err + } + for i := range a.sums { + pm := fromMapPoint(pts[i]) + a.sums[i].AddAssign(a.curve.api, pm) + } + return nil +} + +// Digest returns the current vector of accumulator points. +func (a *LinearAccumulator) Digest() [maptocurve_kb8.LinearN]G1Affine { + return a.sums +} + +// Reset clears the accumulator back to (offset, offset, ..., offset). +func (a *LinearAccumulator) Reset() { + for i := range a.sums { + a.sums[i] = accumulatorOffset + } +} + +// HashLinear returns the linear-separator vector multiset hash of msgs. +func (c *Curve) HashLinear(msgs []frontend.Variable) ([maptocurve_kb8.LinearN]G1Affine, error) { + acc := NewLinearAccumulator(c) + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + return [maptocurve_kb8.LinearN]G1Affine{}, err + } + } + return acc.Digest(), nil +} diff --git a/std/algebra/native/sw_kb8/vector_multisethash_linear_test.go b/std/algebra/native/sw_kb8/vector_multisethash_linear_test.go new file mode 100644 index 0000000000..db3802760b --- /dev/null +++ b/std/algebra/native/sw_kb8/vector_multisethash_linear_test.go @@ -0,0 +1,176 @@ +package sw_kb8 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/ecc/kb8" + nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/internal/widecommitter" + "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/test" +) + +// linearHashCircuit verifies LinearAccumulator over a small batch of inserts. +type linearHashCircuit struct { + Msgs [4]frontend.Variable + Digest [maptocurve_kb8.LinearN]G1Affine +} + +func (c *linearHashCircuit) Define(api frontend.API) error { + curve, err := NewCurve(api) + if err != nil { + return err + } + digest, err := curve.HashLinear(c.Msgs[:]) + if err != nil { + return err + } + for i := range digest { + digest[i].X.AssertIsEqual(api, c.Digest[i].X) + digest[i].Y.AssertIsEqual(api, c.Digest[i].Y) + } + return nil +} + +// linearSingleInsertCircuit measures the per-Insert constraint cost. +type linearSingleInsertCircuit struct { + Msg frontend.Variable + Digest [maptocurve_kb8.LinearN]G1Affine +} + +func (c *linearSingleInsertCircuit) Define(api frontend.API) error { + curve, err := NewCurve(api) + if err != nil { + return err + } + digest, err := curve.HashLinear([]frontend.Variable{c.Msg}) + if err != nil { + return err + } + for i := range digest { + digest[i].X.AssertIsEqual(api, c.Digest[i].X) + digest[i].Y.AssertIsEqual(api, c.Digest[i].Y) + } + return nil +} + +// shiftedLinearDigest matches the per-coordinate offset added by the in-circuit +// LinearAccumulator (each coordinate starts at the generator G). The native +// HashLinear returns un-shifted sums starting at infinity, so we add G to each +// coordinate to bring them in sync with what the circuit accumulator computes. +func shiftedLinearDigest(d [maptocurve_kb8.LinearN]kb8.G1Affine) [maptocurve_kb8.LinearN]kb8.G1Affine { + _, offset := kb8.Generators() + var out [maptocurve_kb8.LinearN]kb8.G1Affine + for i := range d { + var jd, jo kb8.G1Jac + jd.FromAffine(&d[i]) + jo.FromAffine(&offset) + jd.AddAssign(&jo) + out[i].FromJacobian(&jd) + } + return out +} + +func newLinearWitnessDigest(d [maptocurve_kb8.LinearN]kb8.G1Affine) [maptocurve_kb8.LinearN]G1Affine { + var out [maptocurve_kb8.LinearN]G1Affine + for i := range d { + out[i] = NewG1Affine(d[i]) + } + return out +} + +func TestLinearHash(t *testing.T) { + assert := test.NewAssert(t) + msgs := []uint32{7, 19, 7, 1024} + d, err := nativemsh.HashLinear(msgs) + assert.NoError(err) + shifted := shiftedLinearDigest(d) + witness := &linearHashCircuit{ + Msgs: [4]frontend.Variable{msgs[0], msgs[1], msgs[2], msgs[3]}, + Digest: newLinearWitnessDigest(shifted), + } + invalid := *witness + invalid.Digest[0].X.C0.B0.A0 = 42 + assert.CheckCircuit(&linearHashCircuit{}, test.WithValidAssignment(witness), test.WithInvalidAssignment(&invalid), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) +} + +func TestLinearHashHomomorphic(t *testing.T) { + // Hash(A ∪ B) == Hash(A) + Hash(B) componentwise. We can't run this purely + // in the native code (since the circuit accumulator shifts by G per + // coordinate, the linearity has to be tested at the un-shifted native + // level). Just verify the native side here; the in-circuit additivity is + // guaranteed by Insert calling AddAssign per coordinate. + a := []uint32{3, 41, 197} + b := []uint32{2, 99, 65535, 7} + full, err := nativemsh.HashLinear(append(append([]uint32{}, a...), b...)) + if err != nil { + t.Fatal(err) + } + dA, err := nativemsh.HashLinear(a) + if err != nil { + t.Fatal(err) + } + dB, err := nativemsh.HashLinear(b) + if err != nil { + t.Fatal(err) + } + for i := range full { + var sum kb8.G1Affine + sum.Add(&dA[i], &dB[i]) + if !sum.Equal(&full[i]) { + t.Fatalf("native HashLinear is not additive at coord %d", i) + } + } +} + +func BenchmarkLinearMultisetHashCircuitSolve(b *testing.B) { + msg := uint32(7) + d, err := nativemsh.HashLinear([]uint32{msg}) + if err != nil { + b.Fatal(err) + } + shifted := shiftedLinearDigest(d) + w := &linearSingleInsertCircuit{ + Msg: msg, + Digest: newLinearWitnessDigest(shifted), + } + witness, err := frontend.NewWitness(w, koalabear.Modulus()) + if err != nil { + b.Fatal(err) + } + + b.Run("scs", func(b *testing.B) { + var c linearSingleInsertCircuit + ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(scs.NewBuilder), &c) + if err != nil { + b.Fatal(err) + } + b.Log("scs nbConstraints", ccs.GetNbConstraints()) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := ccs.IsSolved(witness); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("r1cs", func(b *testing.B) { + var c linearSingleInsertCircuit + ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(r1cs.NewBuilder), &c, frontend.WithCompressThreshold(10)) + if err != nil { + b.Fatal(err) + } + b.Log("r1cs nbConstraints", ccs.GetNbConstraints()) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := ccs.IsSolved(witness); err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/std/algebra/native/sw_kb8/vector_multisethash_poseidon2.go b/std/algebra/native/sw_kb8/vector_multisethash_poseidon2.go new file mode 100644 index 0000000000..7cd195e00e --- /dev/null +++ b/std/algebra/native/sw_kb8/vector_multisethash_poseidon2.go @@ -0,0 +1,71 @@ +package sw_kb8 + +import ( + "errors" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" +) + +var errPoseidon2MismatchedHalves = errors.New("kb8 Poseidon2 multiset hash: msgsLow and msgsHigh must have the same length") + +// Poseidon2Accumulator stores the N-coordinate Poseidon2-sponge vector ECMSH +// state. Each coordinate accumulator starts at the fixed offset generator, +// preserving the same incomplete-addition safety argument as the one-point +// Accumulator and the LinearAccumulator. +type Poseidon2Accumulator struct { + curve *Curve + sums [maptocurve_kb8.PqN]G1Affine +} + +// NewPoseidon2Accumulator returns a zero Poseidon2 accumulator. Each coordinate +// is initialized to the fixed offset generator. +func NewPoseidon2Accumulator(curve *Curve) *Poseidon2Accumulator { + a := &Poseidon2Accumulator{curve: curve} + for i := range a.sums { + a.sums[i] = accumulatorOffset + } + return a +} + +// Insert maps a 64-bit message — supplied as two 32-bit halves (msgLow, +// msgHigh) — via the Poseidon2 sponge separator and adds each of the PqN +// mapped points to the matching accumulator coordinate. +func (a *Poseidon2Accumulator) Insert(msgLow, msgHigh frontend.Variable) error { + pts, err := maptocurve_kb8.MapPoseidon2(a.curve.api, msgLow, msgHigh) + if err != nil { + return err + } + for i := range a.sums { + pm := fromMapPoint(pts[i]) + a.sums[i].AddAssign(a.curve.api, pm) + } + return nil +} + +// Digest returns the current vector of accumulator points. +func (a *Poseidon2Accumulator) Digest() [maptocurve_kb8.PqN]G1Affine { + return a.sums +} + +// Reset clears the accumulator back to the per-coordinate offset. +func (a *Poseidon2Accumulator) Reset() { + for i := range a.sums { + a.sums[i] = accumulatorOffset + } +} + +// HashPoseidon2 returns the Poseidon2-sponge vector multiset hash of msgs. +// Each message is supplied as (low, high) 32-bit halves of a 64-bit value. +func (c *Curve) HashPoseidon2(msgsLow, msgsHigh []frontend.Variable) ([maptocurve_kb8.PqN]G1Affine, error) { + if len(msgsLow) != len(msgsHigh) { + return [maptocurve_kb8.PqN]G1Affine{}, errPoseidon2MismatchedHalves + } + acc := NewPoseidon2Accumulator(c) + for i := range msgsLow { + if err := acc.Insert(msgsLow[i], msgsHigh[i]); err != nil { + return [maptocurve_kb8.PqN]G1Affine{}, err + } + } + return acc.Digest(), nil +} diff --git a/std/algebra/native/sw_kb8/vector_multisethash_poseidon2_test.go b/std/algebra/native/sw_kb8/vector_multisethash_poseidon2_test.go new file mode 100644 index 0000000000..38170bfada --- /dev/null +++ b/std/algebra/native/sw_kb8/vector_multisethash_poseidon2_test.go @@ -0,0 +1,184 @@ +package sw_kb8 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/ecc/kb8" + nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/internal/widecommitter" + "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/test" +) + +// poseidon2HashCircuit verifies Poseidon2Accumulator over a small batch. +type poseidon2HashCircuit struct { + MsgsLow [4]frontend.Variable + MsgsHigh [4]frontend.Variable + Digest [maptocurve_kb8.PqN]G1Affine +} + +func (c *poseidon2HashCircuit) Define(api frontend.API) error { + curve, err := NewCurve(api) + if err != nil { + return err + } + digest, err := curve.HashPoseidon2(c.MsgsLow[:], c.MsgsHigh[:]) + if err != nil { + return err + } + for i := range digest { + digest[i].X.AssertIsEqual(api, c.Digest[i].X) + digest[i].Y.AssertIsEqual(api, c.Digest[i].Y) + } + return nil +} + +type poseidon2SingleInsertCircuit struct { + MsgLow frontend.Variable + MsgHigh frontend.Variable + Digest [maptocurve_kb8.PqN]G1Affine +} + +func (c *poseidon2SingleInsertCircuit) Define(api frontend.API) error { + curve, err := NewCurve(api) + if err != nil { + return err + } + digest, err := curve.HashPoseidon2([]frontend.Variable{c.MsgLow}, []frontend.Variable{c.MsgHigh}) + if err != nil { + return err + } + for i := range digest { + digest[i].X.AssertIsEqual(api, c.Digest[i].X) + digest[i].Y.AssertIsEqual(api, c.Digest[i].Y) + } + return nil +} + +func shiftedPoseidon2Digest(d [maptocurve_kb8.PqN]kb8.G1Affine) [maptocurve_kb8.PqN]kb8.G1Affine { + _, offset := kb8.Generators() + var out [maptocurve_kb8.PqN]kb8.G1Affine + for i := range d { + var jd, jo kb8.G1Jac + jd.FromAffine(&d[i]) + jo.FromAffine(&offset) + jd.AddAssign(&jo) + out[i].FromJacobian(&jd) + } + return out +} + +func newPoseidon2WitnessDigest(d [maptocurve_kb8.PqN]kb8.G1Affine) [maptocurve_kb8.PqN]G1Affine { + var out [maptocurve_kb8.PqN]G1Affine + for i := range d { + out[i] = NewG1Affine(d[i]) + } + return out +} + +func splitMsg(msg uint64) (low, high uint32) { + return uint32(msg & 0xFFFFFFFF), uint32(msg >> 32) +} + +func TestPoseidon2Hash(t *testing.T) { + assert := test.NewAssert(t) + msgs := []uint64{7, 19, 7, 1024} + d, err := nativemsh.HashPoseidon2(msgs) + assert.NoError(err) + shifted := shiftedPoseidon2Digest(d) + + var w poseidon2HashCircuit + for i, m := range msgs { + low, high := splitMsg(m) + w.MsgsLow[i] = low + w.MsgsHigh[i] = high + } + w.Digest = newPoseidon2WitnessDigest(shifted) + + invalid := w + invalid.Digest[0].X.C0.B0.A0 = 42 + + assert.CheckCircuit(&poseidon2HashCircuit{}, + test.WithValidAssignment(&w), + test.WithInvalidAssignment(&invalid), + test.WithoutCurveChecks(), + test.WithSmallfieldCheck()) +} + +func TestPoseidon2HashHomomorphic(t *testing.T) { + a := []uint64{3, 41, 197} + b := []uint64{2, 99, 65535, 7} + full, err := nativemsh.HashPoseidon2(append(append([]uint64{}, a...), b...)) + if err != nil { + t.Fatal(err) + } + dA, err := nativemsh.HashPoseidon2(a) + if err != nil { + t.Fatal(err) + } + dB, err := nativemsh.HashPoseidon2(b) + if err != nil { + t.Fatal(err) + } + for i := range full { + var sum kb8.G1Affine + sum.Add(&dA[i], &dB[i]) + if !sum.Equal(&full[i]) { + t.Fatalf("native HashPoseidon2 is not additive at coord %d", i) + } + } +} + +func BenchmarkPoseidon2MultisetHashCircuitSolve(b *testing.B) { + msg := uint64(7) + d, err := nativemsh.HashPoseidon2([]uint64{msg}) + if err != nil { + b.Fatal(err) + } + shifted := shiftedPoseidon2Digest(d) + low, high := splitMsg(msg) + w := &poseidon2SingleInsertCircuit{ + MsgLow: low, + MsgHigh: high, + Digest: newPoseidon2WitnessDigest(shifted), + } + witness, err := frontend.NewWitness(w, koalabear.Modulus()) + if err != nil { + b.Fatal(err) + } + + b.Run("scs", func(b *testing.B) { + var c poseidon2SingleInsertCircuit + ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(scs.NewBuilder), &c) + if err != nil { + b.Fatal(err) + } + b.Log("scs nbConstraints", ccs.GetNbConstraints()) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := ccs.IsSolved(witness); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("r1cs", func(b *testing.B) { + var c poseidon2SingleInsertCircuit + ccs, err := frontend.CompileGeneric[constraint.U32](koalabear.Modulus(), widecommitter.From(r1cs.NewBuilder), &c, frontend.WithCompressThreshold(10)) + if err != nil { + b.Fatal(err) + } + b.Log("r1cs nbConstraints", ccs.GetNbConstraints()) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err := ccs.IsSolved(witness); err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/std/permutation/poseidon2/poseidon2.go b/std/permutation/poseidon2/poseidon2.go index 06e831d6ee..fb79925159 100644 --- a/std/permutation/poseidon2/poseidon2.go +++ b/std/permutation/poseidon2/poseidon2.go @@ -43,6 +43,12 @@ type Parameters struct { // For width 2 and 3 the internal matrix is hardcoded and this field is unused. // See https://eprint.iacr.org/2023/323.pdf page 15. DiagM1 []big.Int + + // useKoalaBearM4 selects the Plonky3 circulant M4 = circ(2,3,1,1) used by + // the koalabear native Poseidon2 in gnark-crypto, instead of the default + // pairing-curve M4 = circ(5,7,1,3) / (1,3,5,7) etc. Internal flag set by + // the koalabear constructor. + useKoalaBearM4 bool } func GetDefaultParameters(curve ecc.ID) (Parameters, error) { @@ -140,6 +146,15 @@ func NewPoseidon2(api frontend.API) (*Permutation, error) { // is deterministic and depends on the curve ID. See the corresponding NewParameters // function in the gnark-crypto library poseidon2 packages for more details. func NewPoseidon2FromParameters(api frontend.API, width, nbFullRounds, nbPartialRounds int) (*Permutation, error) { + // koalabear isn't a curve, so FieldToCurve would return UNKNOWN; intercept + // it here so the koalabear-specific round-key derivation runs. + if isKoalaBearField(api.Compiler().Field()) { + params, err := koalaBearParameters(width, nbFullRounds, nbPartialRounds) + if err != nil { + return nil, err + } + return &Permutation{api: api, params: params}, nil + } params := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} switch utils.FieldToCurve(api.Compiler().Field()) { // TODO: assumes pairing based builder, reconsider when supporting other backends case ecc.BN254: @@ -233,6 +248,10 @@ func (h *Permutation) sBox(index int, input []frontend.Variable) { // on chunks of 4 elements on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain func (h *Permutation) matMulM4InPlace(s []frontend.Variable) { + if h.params.useKoalaBearM4 { + h.matMulM4KoalaBearInPlace(s) + return + } c := len(s) / 4 for i := 0; i < c; i++ { t0 := h.api.Add(s[4*i], s[4*i+1]) // s0+s1 @@ -254,6 +273,32 @@ func (h *Permutation) matMulM4InPlace(s []frontend.Variable) { } } +// matMulM4KoalaBearInPlace multiplies each 4-element chunk by the circulant +// M4 = circ(2,3,1,1) used by gnark-crypto's koalabear Poseidon2 (Plonky3-style). +// Output row 0 = (2,3,1,1)·s, row 1 = (1,2,3,1)·s, row 2 = (1,1,2,3)·s, +// row 3 = (3,1,1,2)·s. Addition chain mirrors +// field/koalabear/poseidon2/poseidon2.go:176-191. +func (h *Permutation) matMulM4KoalaBearInPlace(s []frontend.Variable) { + c := len(s) / 4 + for i := 0; i < c; i++ { + t01 := h.api.Add(s[4*i], s[4*i+1]) + t23 := h.api.Add(s[4*i+2], s[4*i+3]) + t0123 := h.api.Add(t01, t23) + t01123 := h.api.Add(t0123, s[4*i+1]) + t01233 := h.api.Add(t0123, s[4*i+3]) + // The order matches the native one — assign indices 3 and 1 before 0 and 2, + // since the native code overwrites in place. + out3 := h.api.Add(h.api.Mul(s[4*i], 2), t01233) + out1 := h.api.Add(h.api.Mul(s[4*i+2], 2), t01123) + out0 := h.api.Add(t01, t01123) + out2 := h.api.Add(t23, t01233) + s[4*i] = out0 + s[4*i+1] = out1 + s[4*i+2] = out2 + s[4*i+3] = out3 + } +} + // when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) // see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 // diff --git a/std/permutation/poseidon2/poseidon2_koalabear.go b/std/permutation/poseidon2/poseidon2_koalabear.go new file mode 100644 index 0000000000..5c499dbbcc --- /dev/null +++ b/std/permutation/poseidon2/poseidon2_koalabear.go @@ -0,0 +1,72 @@ +package poseidon2 + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/field/koalabear" + kbposeidon2 "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" +) + +// diag16KoalaBearMinus1 holds the entries the in-circuit matMulInternalInPlace +// multiplies by per coordinate (state[i] = state[i] * DiagM1[i] + Σstate). +// For koalabear they match the unexported diag16 array in gnark-crypto +// (field/koalabear/poseidon2/hash.go:48-69) directly; the "M1" naming is a +// gnark convention shared with bn254 and is unrelated to a "-1" shift. +// +// p = 2^31 - 2^24 + 1 = 2_130_706_433. +var diag16KoalaBearMinus1 = [16]uint64{ + 2130706431, // -2 mod p + 1, // 1 + 2, // 2 + 1065353217, // 1/2 mod p + 3, // 3 + 4, // 4 + 1065353216, // -1/2 mod p + 2130706430, // -3 mod p + 2130706429, // -4 mod p + 2122383361, // 1/2^8 mod p + 1864368129, // 1/8 mod p + 2130706306, // 1/2^24 mod p + 8323072, // -1/2^8 mod p + 266338304, // -1/8 mod p + 133169152, // -1/16 mod p + 127, // -1/2^24 mod p +} + +// isKoalaBearField reports whether the api compiles over the koalabear native +// field. +func isKoalaBearField(field *big.Int) bool { + return field.Cmp(koalabear.Modulus()) == 0 +} + +// koalaBearParameters builds in-circuit Parameters from gnark-crypto's native +// koalabear poseidon2 parameters. Round keys come from the same deterministic +// seed-based derivation used natively, so circuit and native permutations +// produce identical outputs for the same input. +func koalaBearParameters(width, nbFullRounds, nbPartialRounds int) (Parameters, error) { + native := kbposeidon2.NewParameters(width, nbFullRounds, nbPartialRounds) + params := Parameters{ + Width: native.Width, + DegreeSBox: kbposeidon2.DegreeSBox(), + NbFullRounds: native.NbFullRounds, + NbPartialRounds: native.NbPartialRounds, + RoundKeys: make([][]big.Int, len(native.RoundKeys)), + } + for i := range params.RoundKeys { + params.RoundKeys[i] = make([]big.Int, len(native.RoundKeys[i])) + for j := range params.RoundKeys[i] { + native.RoundKeys[i][j].BigInt(¶ms.RoundKeys[i][j]) + } + } + + if width != 16 { + return Parameters{}, fmt.Errorf("koalabear poseidon2: in-circuit width %d not yet supported (only 16)", width) + } + params.DiagM1 = make([]big.Int, 16) + for i, v := range diag16KoalaBearMinus1 { + params.DiagM1[i].SetUint64(v) + } + params.useKoalaBearM4 = true + return params, nil +} diff --git a/std/permutation/poseidon2/poseidon2_koalabear_test.go b/std/permutation/poseidon2/poseidon2_koalabear_test.go new file mode 100644 index 0000000000..76cd5c5374 --- /dev/null +++ b/std/permutation/poseidon2/poseidon2_koalabear_test.go @@ -0,0 +1,63 @@ +package poseidon2 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/koalabear" + kbposeidon2 "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" +) + +const koalaBearWidth = 16 + +// poseidon2KoalaBearCircuit applies the koalabear Poseidon2 permutation and +// asserts the result equals a precomputed native output. +type poseidon2KoalaBearCircuit struct { + Input [koalaBearWidth]frontend.Variable + Output [koalaBearWidth]frontend.Variable `gnark:",public"` +} + +func (c *poseidon2KoalaBearCircuit) Define(api frontend.API) error { + h, err := NewPoseidon2FromParameters(api, koalaBearWidth, 6, 21) + if err != nil { + return err + } + state := make([]frontend.Variable, koalaBearWidth) + for i := range c.Input { + state[i] = c.Input[i] + } + if err := h.Permutation(state); err != nil { + return err + } + for i := range c.Output { + api.AssertIsEqual(c.Output[i], state[i]) + } + return nil +} + +func TestPoseidon2KoalaBearMatchesNative(t *testing.T) { + assert := test.NewAssert(t) + + // Build a deterministic non-trivial input. + var in [koalaBearWidth]koalabear.Element + for i := range in { + in[i].SetUint64(uint64(i)*1234567 + 7) + } + + native := kbposeidon2.NewPermutation(koalaBearWidth, 6, 21) + state := in + if err := native.Permutation(state[:]); err != nil { + t.Fatal(err) + } + + var witness poseidon2KoalaBearCircuit + for i := range in { + witness.Input[i] = in[i].String() + witness.Output[i] = state[i].String() + } + assert.CheckCircuit(&poseidon2KoalaBearCircuit{}, + test.WithValidAssignment(&witness), + test.WithoutCurveChecks(), + test.WithSmallfieldCheck()) +} From f6f5beddca635c04a7b09c3ae46af30700468a75 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 26 May 2026 14:14:05 -0400 Subject: [PATCH 11/15] refactor: consolidate hints --- std/algebra/native/maptocurve_kb8/hints.go | 2 +- std/algebra/native/maptocurve_kb8/hints_linear.go | 5 ----- std/algebra/native/maptocurve_kb8/hints_poseidon2.go | 5 ----- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/std/algebra/native/maptocurve_kb8/hints.go b/std/algebra/native/maptocurve_kb8/hints.go index 2308a80cd1..61a121849d 100644 --- a/std/algebra/native/maptocurve_kb8/hints.go +++ b/std/algebra/native/maptocurve_kb8/hints.go @@ -15,7 +15,7 @@ func init() { // GetHints returns all hint functions used in the package. func GetHints() []solver.Hint { - return []solver.Hint{yIncrementHint} + return []solver.Hint{yIncrementHint, yIncrementLinearHint, yIncrementPoseidon2Hint} } func yIncrementHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { diff --git a/std/algebra/native/maptocurve_kb8/hints_linear.go b/std/algebra/native/maptocurve_kb8/hints_linear.go index 44720174b9..fe2d8409ec 100644 --- a/std/algebra/native/maptocurve_kb8/hints_linear.go +++ b/std/algebra/native/maptocurve_kb8/hints_linear.go @@ -5,13 +5,8 @@ import ( "math/big" multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" - "github.com/consensys/gnark/constraint/solver" ) -func init() { - solver.RegisterHint(yIncrementLinearHint) -} - // yIncrementLinearHint maps msg to LinearN points natively (using gnark-crypto's // MapLinear) and writes (k_i, x_i.coeffs[8]) for each coordinate, in order. // 9 outputs per coordinate * LinearN coordinates. diff --git a/std/algebra/native/maptocurve_kb8/hints_poseidon2.go b/std/algebra/native/maptocurve_kb8/hints_poseidon2.go index ec677857ac..710bd1e3cc 100644 --- a/std/algebra/native/maptocurve_kb8/hints_poseidon2.go +++ b/std/algebra/native/maptocurve_kb8/hints_poseidon2.go @@ -5,13 +5,8 @@ import ( "math/big" multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" - "github.com/consensys/gnark/constraint/solver" ) -func init() { - solver.RegisterHint(yIncrementPoseidon2Hint) -} - // yIncrementPoseidon2Hint, given the PqN squeezed koalabear elements (already // computed in-circuit by the Poseidon2 sponge), produces per-coordinate // (q, s, k, x_coeffs[8]) where: From a4313f7c41d7caea3258f6ab1c07a7556927f4a0 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 26 May 2026 14:58:31 -0400 Subject: [PATCH 12/15] fix: range check bound --- .../maptocurve_kb8/maptocurve_vector_poseidon2.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go b/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go index 2edc21113e..83bbf93a34 100644 --- a/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go +++ b/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go @@ -3,6 +3,7 @@ package maptocurve_kb8 import ( "encoding/binary" "errors" + "math/big" "github.com/consensys/gnark-crypto/ecc/kb8" multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" @@ -21,7 +22,7 @@ const ( PqWidth = 16 PqSqueezeRate = 8 PqPermutations = 3 // ceil(PqN / PqSqueezeRate) - pqRangeS = 23 // ⌊p/(2T)⌋ = 4_161_536 < 2^23 (with T=256) + pqRangeS = 22 // B = ⌊p/(2T)⌋ = 2^22 - 2^15 < 2^22 (with T=256) pqRangeQ = 9 // q ≤ ⌊(p-1)/B⌋ = 2T-1 = 511 < 2^9 pqRangeK = 8 // k < T = 256 pqOutputsPerCoord = 11 // q, s, k + 8 E8 coefficients of x @@ -97,6 +98,7 @@ func MapPoseidon2(api frontend.API, msgLow, msgHigh frontend.Variable) ([PqN]G1A _, b := kb8.CurveCoefficients() bE8 := newE8(b) bound := multisethash.PqReducerBound() + boundMinusOne := new(big.Int).Sub(bound, big.NewInt(1)) rc := rangecheck.New(api) for i := 0; i < PqN; i++ { @@ -109,9 +111,12 @@ func MapPoseidon2(api frontend.API, msgLow, msgHigh frontend.Variable) ([PqN]G1A // squeezed[i] = q * B + s api.AssertIsEqual(squeezed[i], api.Add(api.Mul(q, bound), s)) - // Range checks. (We range-check s to one less bit than the bound width - // since s < B = 4_161_536 ⇒ s ≤ 4_161_535 < 2^pqRangeS.) + // Enforce s ∈ [0, B-1] exactly: two pqRangeS-bit checks on s and B-1-s. + // A single 2^pqRangeS-bit check would let the prover wrap modulo p and + // pick s ∈ [B, 2^pqRangeS), violating the inverse-freeness invariant + // y = T*s + k < p/2. rc.Check(s, pqRangeS) + rc.Check(api.Sub(boundMinusOne, s), pqRangeS) rc.Check(q, pqRangeQ) rc.Check(k, pqRangeK) From 85fc74536fb1f6a0655fc56e90a1bba177a5a101 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 26 May 2026 15:23:56 -0400 Subject: [PATCH 13/15] refactor: rename kb8 to octobear --- .golangci.yml | 3 + go.mod | 12 +- go.sum | 24 +-- internal/smallfields/tinyfield/element.go | 34 +++- .../smallfields/tinyfield/element_test.go | 172 +++++++++++++----- internal/smallfields/tinyfield/vector.go | 77 ++------ internal/smallfields/tinyfield/vector_test.go | 26 +-- .../{fields_kb8 => fields_octobear}/doc.go | 4 +- .../{fields_kb8 => fields_octobear}/e2.go | 2 +- .../e2_test.go | 2 +- .../{fields_kb8 => fields_octobear}/e4.go | 2 +- .../e4_test.go | 2 +- .../{fields_kb8 => fields_octobear}/e8.go | 2 +- .../e8_test.go | 2 +- .../{fields_kb8 => fields_octobear}/hints.go | 2 +- std/algebra/native/maptocurve_kb8/doc.go | 3 - std/algebra/native/maptocurve_octobear/doc.go | 3 + .../hints.go | 4 +- .../hints_linear.go | 4 +- .../hints_poseidon2.go | 6 +- .../maptocurve.go | 14 +- .../maptocurve_test.go | 4 +- .../maptocurve_vector_linear.go | 24 +-- .../maptocurve_vector_poseidon2.go | 18 +- .../types.go | 12 +- std/algebra/native/sw_kb8/doc.go | 7 - std/algebra/native/sw_kb8/hints.go | 10 - std/algebra/native/sw_kb8/types.go | 29 --- std/algebra/native/sw_octobear/doc.go | 7 + .../native/{sw_kb8 => sw_octobear}/g1.go | 34 ++-- .../native/{sw_kb8 => sw_octobear}/g1_test.go | 28 +-- std/algebra/native/sw_octobear/hints.go | 10 + .../{sw_kb8 => sw_octobear}/multisethash.go | 6 +- .../multisethash_test.go | 16 +- std/algebra/native/sw_octobear/types.go | 29 +++ .../vector_multisethash_linear.go | 14 +- .../vector_multisethash_linear_test.go | 26 +-- .../vector_multisethash_poseidon2.go | 18 +- .../vector_multisethash_poseidon2_test.go | 26 +-- std/hints.go | 8 +- .../fieldextension/koalabear_ext_test.go | 2 +- .../poseidon2/poseidon2_koalabear_test.go | 4 +- 42 files changed, 395 insertions(+), 337 deletions(-) rename std/algebra/native/{fields_kb8 => fields_octobear}/doc.go (51%) rename std/algebra/native/{fields_kb8 => fields_octobear}/e2.go (99%) rename std/algebra/native/{fields_kb8 => fields_octobear}/e2_test.go (98%) rename std/algebra/native/{fields_kb8 => fields_octobear}/e4.go (99%) rename std/algebra/native/{fields_kb8 => fields_octobear}/e4_test.go (98%) rename std/algebra/native/{fields_kb8 => fields_octobear}/e8.go (99%) rename std/algebra/native/{fields_kb8 => fields_octobear}/e8_test.go (99%) rename std/algebra/native/{fields_kb8 => fields_octobear}/hints.go (98%) delete mode 100644 std/algebra/native/maptocurve_kb8/doc.go create mode 100644 std/algebra/native/maptocurve_octobear/doc.go rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/hints.go (92%) rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/hints_linear.go (91%) rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/hints_poseidon2.go (87%) rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/maptocurve.go (84%) rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/maptocurve_test.go (87%) rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/maptocurve_vector_linear.go (76%) rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/maptocurve_vector_poseidon2.go (88%) rename std/algebra/native/{maptocurve_kb8 => maptocurve_octobear}/types.go (65%) delete mode 100644 std/algebra/native/sw_kb8/doc.go delete mode 100644 std/algebra/native/sw_kb8/hints.go delete mode 100644 std/algebra/native/sw_kb8/types.go create mode 100644 std/algebra/native/sw_octobear/doc.go rename std/algebra/native/{sw_kb8 => sw_octobear}/g1.go (85%) rename std/algebra/native/{sw_kb8 => sw_octobear}/g1_test.go (90%) create mode 100644 std/algebra/native/sw_octobear/hints.go rename std/algebra/native/{sw_kb8 => sw_octobear}/multisethash.go (91%) rename std/algebra/native/{sw_kb8 => sw_octobear}/multisethash_test.go (90%) create mode 100644 std/algebra/native/sw_octobear/types.go rename std/algebra/native/{sw_kb8 => sw_octobear}/vector_multisethash_linear.go (82%) rename std/algebra/native/{sw_kb8 => sw_octobear}/vector_multisethash_linear_test.go (85%) rename std/algebra/native/{sw_kb8 => sw_octobear}/vector_multisethash_poseidon2.go (75%) rename std/algebra/native/{sw_kb8 => sw_octobear}/vector_multisethash_poseidon2_test.go (85%) diff --git a/.golangci.yml b/.golangci.yml index 7940a03693..74ef1ff18d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,6 +20,9 @@ linters: excludes: - G115 - G602 + misspell: + ignore-rules: + - octobear exclusions: generated: disable presets: diff --git a/go.mod b/go.mod index 32dfbac22c..7dc3e13e8e 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.20.1 + github.com/consensys/gnark-crypto v0.20.2-0.20260521220852-8d7eba492bae github.com/fxamacker/cbor/v2 v2.9.0 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef @@ -18,7 +18,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.48.0 - golang.org/x/sync v0.19.0 + golang.org/x/sync v0.20.0 ) require ( @@ -33,10 +33,10 @@ require ( github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/tools v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index d3fdb86fca..be9de8da88 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.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg= -github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= +github.com/consensys/gnark-crypto v0.20.2-0.20260521220852-8d7eba492bae h1:o3yoQFcDyfXLKFHsoNOzD7BFuojrU3IbB5EjySG53W8= +github.com/consensys/gnark-crypto v0.20.2-0.20260521220852-8d7eba492bae/go.mod h1:NzeBHSZ49bIM7RtrNTYYR2kymTqwvI/A4eTgQlyQc+Q= github.com/consensys/gnark-solidity-checker v0.2.0 h1:i5iUEzNOkUvpaKm23UEe0wajBMwj7NzyT4EI0T2N8WQ= github.com/consensys/gnark-solidity-checker v0.2.0/go.mod h1:cEvl4g5AH+L4qGQLDOVZjqvn5IKZIAZdhSi8zAM6BiY= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -358,8 +358,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -424,8 +424,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -476,10 +476,10 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -552,8 +552,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/smallfields/tinyfield/element.go b/internal/smallfields/tinyfield/element.go index c1f38612eb..5f18dbb8ab 100644 --- a/internal/smallfields/tinyfield/element.go +++ b/internal/smallfields/tinyfield/element.go @@ -121,7 +121,7 @@ func (z *Element) Set(x *Element) *Element { // *big.Int // big.Int // []byte -func (z *Element) SetInterface(i1 interface{}) (*Element, error) { +func (z *Element) SetInterface(i1 any) (*Element, error) { if i1 == nil { return nil, errors.New("can't set tinyfield.Element with ") } @@ -417,7 +417,7 @@ func BatchInvert(a []Element) []Element { zeroes := bitset.New(uint(len(a))) accumulator := One() - for i := 0; i < len(a); i++ { + for i := range len(a) { if a[i].IsZero() { zeroes.Set(uint(i)) continue @@ -469,7 +469,7 @@ func Hash(msg, dst []byte, count int) ([]Element, error) { vv := pool.BigInt.Get() res := make([]Element, count) - for i := 0; i < count; i++ { + for i := range count { vv.SetBytes(pseudoRandomBytes[i*L : (i+1)*L]) res[i].SetBigInt(vv) } @@ -498,7 +498,6 @@ func (z *Element) Exp(x Element, k *big.Int) *Element { defer pool.BigInt.Put(e) e.Neg(k) } - z.Set(&x) for i := e.BitLen() - 2; i >= 0; i-- { @@ -700,7 +699,7 @@ func (z *Element) SetBigInt(v *big.Int) *Element { func (z *Element) setBigInt(v *big.Int) *Element { vBits := v.Bits() // we assume v < q, so even if big.Int words are on 64bits, we can safely cast them to 32bits - for i := 0; i < len(vBits); i++ { + for i := range len(vBits) { z[i] = uint32(vBits[i]) } @@ -907,6 +906,31 @@ func (z *Element) Sqrt(x *Element) *Element { return nil } +var _bCbrtExponentElement *big.Int + +func init() { + _bCbrtExponentElement, _ = new(big.Int).SetString("1f", 16) +} + +// Cbrt z = ∛x (mod q) +// if the cube root doesn't exist (x is not a cube mod q) +// Cbrt leaves z unchanged and returns nil +func (z *Element) Cbrt(x *Element) *Element { + // q ≡ 2 (mod 3) + // using z = x^((2q-1)/3) (mod q) + z.Exp(*x, _bCbrtExponentElement) + // as we use x^((2q-1)/3), there is no check to do: every element has a unique cube root + return z +} + +// Cube sets z to x^3 and returns z +func (z *Element) Cube(x *Element) *Element { + var t Element + t.Square(x).Mul(&t, x) + z.Set(&t) + return z +} + // Inverse z = x⁻¹ (mod q) // // if x == 0, sets and returns z = x diff --git a/internal/smallfields/tinyfield/element_test.go b/internal/smallfields/tinyfield/element_test.go index fa1aeec332..ac777e36cc 100644 --- a/internal/smallfields/tinyfield/element_test.go +++ b/internal/smallfields/tinyfield/element_test.go @@ -34,7 +34,7 @@ func BenchmarkElementSelect(b *testing.B) { y.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := range b.N { benchResElement.Select(i%3, &x, &y) } } @@ -44,7 +44,7 @@ func BenchmarkElementSetRandom(b *testing.B) { x.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { x.MustSetRandom() } } @@ -55,7 +55,7 @@ func BenchmarkElementSetBytes(b *testing.B) { bb := x.Bytes() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.SetBytes(bb[:]) } @@ -65,21 +65,21 @@ func BenchmarkElementMulByConstants(b *testing.B) { b.Run("mulBy3", func(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { MulBy3(&benchResElement) } }) b.Run("mulBy5", func(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { MulBy5(&benchResElement) } }) b.Run("mulBy13", func(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { MulBy13(&benchResElement) } }) @@ -91,7 +91,7 @@ func BenchmarkElementInverse(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Inverse(&x) } @@ -102,7 +102,7 @@ func BenchmarkElementButterfly(b *testing.B) { x.MustSetRandom() benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { Butterfly(&x, &benchResElement) } } @@ -113,7 +113,7 @@ func BenchmarkElementExp(b *testing.B) { benchResElement.MustSetRandom() b1, _ := rand.Int(rand.Reader, Modulus()) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Exp(x, b1) } } @@ -121,7 +121,7 @@ func BenchmarkElementExp(b *testing.B) { func BenchmarkElementDouble(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Double(&benchResElement) } } @@ -131,7 +131,7 @@ func BenchmarkElementAdd(b *testing.B) { x.MustSetRandom() benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Add(&x, &benchResElement) } } @@ -141,7 +141,7 @@ func BenchmarkElementSub(b *testing.B) { x.MustSetRandom() benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Sub(&x, &benchResElement) } } @@ -149,7 +149,7 @@ func BenchmarkElementSub(b *testing.B) { func BenchmarkElementNeg(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Neg(&benchResElement) } } @@ -159,7 +159,7 @@ func BenchmarkElementDiv(b *testing.B) { x.MustSetRandom() benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Div(&x, &benchResElement) } } @@ -167,7 +167,7 @@ func BenchmarkElementDiv(b *testing.B) { func BenchmarkElementFromMont(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.fromMont() } } @@ -175,7 +175,7 @@ func BenchmarkElementFromMont(b *testing.B) { func BenchmarkElementSquare(b *testing.B) { benchResElement.MustSetRandom() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Square(&benchResElement) } } @@ -185,18 +185,27 @@ func BenchmarkElementSqrt(b *testing.B) { a.MustSetRandom() a.Square(&a) b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Sqrt(&a) } } +func BenchmarkElementCbrt(b *testing.B) { + var a Element + a.SetUint64(8) + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchResElement.Cbrt(&a) + } +} + func BenchmarkElementMul(b *testing.B) { x := Element{ 25, } benchResElement.SetOne() b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Mul(&benchResElement, &x) } } @@ -208,7 +217,7 @@ func BenchmarkElementCmp(b *testing.B) { benchResElement = x benchResElement[0] = 0 b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { benchResElement.Cmp(&x) } } @@ -1241,15 +1250,12 @@ func TestElementSquare(t *testing.T) { func(a testPairElement) bool { var c Element c.Square(&a.element) - var d, e big.Int d.Mul(&a.bigint, &a.bigint).Mod(&d, Modulus()) - return c.BigInt(&e).Cmp(&d) == 0 }, genA, )) - properties.Property("Square: operation result must be smaller than modulus", prop.ForAll( func(a testPairElement) bool { var c Element @@ -1270,7 +1276,6 @@ func TestElementSquare(t *testing.T) { a.BigInt(&aBig) var c Element c.Square(&a) - var d, e big.Int d.Mul(&aBig, &aBig).Mod(&d, Modulus()) @@ -1314,15 +1319,12 @@ func TestElementInverse(t *testing.T) { func(a testPairElement) bool { var c Element c.Inverse(&a.element) - var d, e big.Int d.ModInverse(&a.bigint, Modulus()) - return c.BigInt(&e).Cmp(&d) == 0 }, genA, )) - properties.Property("Inverse: operation result must be smaller than modulus", prop.ForAll( func(a testPairElement) bool { var c Element @@ -1343,7 +1345,6 @@ func TestElementInverse(t *testing.T) { a.BigInt(&aBig) var c Element c.Inverse(&a) - var d, e big.Int d.ModInverse(&aBig, Modulus()) @@ -1387,15 +1388,12 @@ func TestElementSqrt(t *testing.T) { func(a testPairElement) bool { var c Element c.Sqrt(&a.element) - var d, e big.Int d.ModSqrt(&a.bigint, Modulus()) - return c.BigInt(&e).Cmp(&d) == 0 }, genA, )) - properties.Property("Sqrt: operation result must be smaller than modulus", prop.ForAll( func(a testPairElement) bool { var c Element @@ -1416,7 +1414,6 @@ func TestElementSqrt(t *testing.T) { a.BigInt(&aBig) var c Element c.Sqrt(&a) - var d, e big.Int d.ModSqrt(&aBig, Modulus()) @@ -1431,6 +1428,103 @@ func TestElementSqrt(t *testing.T) { } +func TestElementCbrt(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + + properties.Property("Cbrt: having the receiver as operand should output the same result", prop.ForAll( + func(a testPairElement) bool { + + b := a.element + + b.Cbrt(&a.element) + a.element.Cbrt(&a.element) + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("Cbrt: operation result must match big.Int result", prop.ForAll( + func(a testPairElement) bool { + // verify that c^3 == a (since there's no big.Int.ModCbrt) + // Cbrt returns nil if the element is not a cubic residue + var c Element + result := c.Cbrt(&a.element) + if result == nil { + // a is not a cubic residue, this is valid + return true + } + var cube, e big.Int + c.BigInt(&e) + cube.Exp(&e, big.NewInt(3), Modulus()) + return cube.Cmp(&a.bigint) == 0 + }, + genA, + )) + properties.Property("Cbrt: cubic residues must always have a cube root", prop.ForAll( + func(a testPairElement) bool { + // b = a³ is guaranteed to be a cubic residue + var b, c Element + b.Square(&a.element).Mul(&b, &a.element) + if c.Cbrt(&b) == nil { + return false + } + var check Element + check.Square(&c).Mul(&check, &c) + return check.Equal(&b) + }, + genA, + )) + + properties.Property("Cbrt: operation result must be smaller than modulus", prop.ForAll( + func(a testPairElement) bool { + var c Element + c.Cbrt(&a.element) + return c.smallerThanModulus() + }, + genA, + )) + + specialValueTest := func() { + // test special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + a := testValues[i] + var aBig big.Int + a.BigInt(&aBig) + var c Element + // verify that c^3 == a (since there's no big.Int.ModCbrt) + // Cbrt returns nil if the element is not a cubic residue + result := c.Cbrt(&a) + if result == nil { + // a is not a cubic residue, this is valid, continue + continue + } + var cube, e big.Int + c.BigInt(&e) + cube.Exp(&e, big.NewInt(3), Modulus()) + if cube.Cmp(&aBig) != 0 { + t.Fatal("Cbrt failed for special value") + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + func TestElementDouble(t *testing.T) { t.Parallel() parameters := gopter.DefaultTestParameters() @@ -1460,15 +1554,12 @@ func TestElementDouble(t *testing.T) { func(a testPairElement) bool { var c Element c.Double(&a.element) - var d, e big.Int d.Lsh(&a.bigint, 1).Mod(&d, Modulus()) - return c.BigInt(&e).Cmp(&d) == 0 }, genA, )) - properties.Property("Double: operation result must be smaller than modulus", prop.ForAll( func(a testPairElement) bool { var c Element @@ -1489,7 +1580,6 @@ func TestElementDouble(t *testing.T) { a.BigInt(&aBig) var c Element c.Double(&a) - var d, e big.Int d.Lsh(&aBig, 1).Mod(&d, Modulus()) @@ -1533,15 +1623,12 @@ func TestElementNeg(t *testing.T) { func(a testPairElement) bool { var c Element c.Neg(&a.element) - var d, e big.Int d.Neg(&a.bigint).Mod(&d, Modulus()) - return c.BigInt(&e).Cmp(&d) == 0 }, genA, )) - properties.Property("Neg: operation result must be smaller than modulus", prop.ForAll( func(a testPairElement) bool { var c Element @@ -1562,7 +1649,6 @@ func TestElementNeg(t *testing.T) { a.BigInt(&aBig) var c Element c.Neg(&a) - var d, e big.Int d.Neg(&aBig).Mod(&d, Modulus()) @@ -1957,7 +2043,7 @@ func TestElementBatchInvert(t *testing.T) { for _, t := range tData { a := make([]Element, len(t)) - for i := 0; i < len(a); i++ { + for i := range len(a) { a[i].SetInt64(t[i]) } @@ -1965,7 +2051,7 @@ func TestElementBatchInvert(t *testing.T) { assert.True(len(aInv) == len(a)) - for i := 0; i < len(a); i++ { + for i := range len(a) { if a[i].IsZero() { assert.True(aInv[i].IsZero(), "0⁻¹ != 0") } else { @@ -2002,7 +2088,7 @@ func TestElementBatchInvert(t *testing.T) { assert.True(len(aInv) == len(a)) - for i := 0; i < len(a); i++ { + for i := range len(a) { if a[i].IsZero() { if !aInv[i].IsZero() { return false @@ -2123,7 +2209,7 @@ func TestElementMul2ExpNegN(t *testing.T) { var b, e, two Element var c [33]Element two.SetUint64(2) - for n := 0; n < 33; n++ { + for n := range 33 { e.Exp(two, big.NewInt(int64(n))).Inverse(&e) b.Mul(&a.element, &e) c[n].Mul2ExpNegN(&a.element, uint32(n)) diff --git a/internal/smallfields/tinyfield/vector.go b/internal/smallfields/tinyfield/vector.go index 29a1f64672..dabdddc764 100644 --- a/internal/smallfields/tinyfield/vector.go +++ b/internal/smallfields/tinyfield/vector.go @@ -12,12 +12,12 @@ import ( "fmt" "io" "math/bits" - "runtime" "slices" "strings" - "sync" "sync/atomic" "unsafe" + + "github.com/consensys/gnark-crypto/parallel" ) // Vector represents a slice of Element. @@ -59,7 +59,7 @@ func (vector *Vector) WriteTo(w io.Writer) (int64, error) { n := int64(4) var buf [Bytes]byte - for i := 0; i < len(*vector); i++ { + for i := range len(*vector) { BigEndian.PutElement(&buf, (*vector)[i]) m, err := w.Write(buf[:]) n += int64(m) @@ -147,7 +147,7 @@ func (vector *Vector) AsyncReadFrom(r io.Reader) (int64, error, chan error) { // go func() { var cptErrors uint64 // process the elements in parallel - execute(int(headerSliceLen), func(start, end int) { + parallel.Execute(int(headerSliceLen), func(start, end int) { var z Element for i := start; i < end; i++ { @@ -217,7 +217,7 @@ func (vector *Vector) ReadFrom(r io.Reader) (int64, error) { *vector = []Element{} } - for i := uint64(0); i < headerSliceLen; i++ { + for i := range headerSliceLen { read, err := io.ReadFull(r, buf[:]) totalRead += int64(read) if errors.Is(err, io.ErrUnexpectedEOF) { @@ -243,7 +243,7 @@ func (vector *Vector) ReadFrom(r io.Reader) (int64, error) { func (vector Vector) String() string { var sbb strings.Builder sbb.WriteByte('[') - for i := 0; i < len(vector); i++ { + for i := range len(vector) { sbb.WriteString(vector[i].String()) if i != len(vector)-1 { sbb.WriteByte(',') @@ -341,7 +341,7 @@ func addVecGeneric(res, a, b Vector) { if len(a) != len(b) || len(a) != len(res) { panic("vector.Add: vectors don't have the same length") } - for i := 0; i < len(a); i++ { + for i := range len(a) { res[i].Add(&a[i], &b[i]) } } @@ -350,7 +350,7 @@ func subVecGeneric(res, a, b Vector) { if len(a) != len(b) || len(a) != len(res) { panic("vector.Sub: vectors don't have the same length") } - for i := 0; i < len(a); i++ { + for i := range len(a) { res[i].Sub(&a[i], &b[i]) } } @@ -359,13 +359,13 @@ func scalarMulVecGeneric(res, a Vector, b *Element) { if len(a) != len(res) { panic("vector.ScalarMul: vectors don't have the same length") } - for i := 0; i < len(a); i++ { + for i := range len(a) { res[i].Mul(&a[i], b) } } func sumVecGeneric(res *Element, a Vector) { - for i := 0; i < len(a); i++ { + for i := range len(a) { res.Add(res, &a[i]) } } @@ -375,7 +375,7 @@ func innerProductVecGeneric(res *Element, a, b Vector) { panic("vector.InnerProduct: vectors don't have the same length") } var tmp Element - for i := 0; i < len(a); i++ { + for i := range len(a) { tmp.Mul(&a[i], &b[i]) res.Add(res, &tmp) } @@ -385,60 +385,7 @@ func mulVecGeneric(res, a, b Vector) { if len(a) != len(b) || len(a) != len(res) { panic("vector.Mul: vectors don't have the same length") } - for i := 0; i < len(a); i++ { + for i := range len(a) { res[i].Mul(&a[i], &b[i]) } } - -// TODO @gbotrel make a public package out of that. -// execute executes the work function in parallel. -// this is copy paste from internal/parallel/parallel.go -// as we don't want to generate code importing internal/ -func execute(nbIterations int, work func(int, int), maxCpus ...int) { - - nbTasks := runtime.NumCPU() - if len(maxCpus) == 1 { - nbTasks = maxCpus[0] - if nbTasks < 1 { - nbTasks = 1 - } else if nbTasks > 512 { - nbTasks = 512 - } - } - - if nbTasks == 1 { - // no go routines - work(0, nbIterations) - return - } - - nbIterationsPerCpus := nbIterations / nbTasks - - // more CPUs than tasks: a CPU will work on exactly one iteration - if nbIterationsPerCpus < 1 { - nbIterationsPerCpus = 1 - nbTasks = nbIterations - } - - var wg sync.WaitGroup - - extraTasks := nbIterations - (nbTasks * nbIterationsPerCpus) - extraTasksOffset := 0 - - for i := 0; i < nbTasks; i++ { - wg.Add(1) - _start := i*nbIterationsPerCpus + extraTasksOffset - _end := _start + nbIterationsPerCpus - if extraTasks > 0 { - _end++ - extraTasks-- - extraTasksOffset++ - } - go func() { - work(_start, _end) - wg.Done() - }() - } - - wg.Wait() -} diff --git a/internal/smallfields/tinyfield/vector_test.go b/internal/smallfields/tinyfield/vector_test.go index 46df33275c..14116c2107 100644 --- a/internal/smallfields/tinyfield/vector_test.go +++ b/internal/smallfields/tinyfield/vector_test.go @@ -183,7 +183,7 @@ func TestVectorOps(t *testing.T) { c := make(Vector, len(a)) c.Add(a, b) - for i := 0; i < len(a); i++ { + for i := range len(a) { var tmp Element tmp.Add(&a[i], &b[i]) if !tmp.Equal(&c[i]) { @@ -197,7 +197,7 @@ func TestVectorOps(t *testing.T) { c := make(Vector, len(a)) c.Sub(a, b) - for i := 0; i < len(a); i++ { + for i := range len(a) { var tmp Element tmp.Sub(&a[i], &b[i]) if !tmp.Equal(&c[i]) { @@ -211,7 +211,7 @@ func TestVectorOps(t *testing.T) { c := make(Vector, len(a)) c.ScalarMul(a, &b) - for i := 0; i < len(a); i++ { + for i := range len(a) { var tmp Element tmp.Mul(&a[i], &b) if !tmp.Equal(&c[i]) { @@ -224,7 +224,7 @@ func TestVectorOps(t *testing.T) { sumVector := func(a Vector) bool { var sum Element computed := a.Sum() - for i := 0; i < len(a); i++ { + for i := range len(a) { sum.Add(&sum, &a[i]) } @@ -234,7 +234,7 @@ func TestVectorOps(t *testing.T) { innerProductVector := func(a, b Vector) bool { computed := a.InnerProduct(b) var innerProduct Element - for i := 0; i < len(a); i++ { + for i := range len(a) { var tmp Element tmp.Mul(&a[i], &b[i]) innerProduct.Add(&innerProduct, &tmp) @@ -249,7 +249,7 @@ func TestVectorOps(t *testing.T) { b[0].SetUint64(0x42) c.Mul(a, b) - for i := 0; i < len(a); i++ { + for i := range len(a) { var tmp Element tmp.Mul(&a[i], &b[i]) if !tmp.Equal(&c[i]) { @@ -335,7 +335,7 @@ func BenchmarkVectorOps(b *testing.B) { _b := b1[:n] _c := c1[:n] b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _c.Add(_a, _b) } }) @@ -345,7 +345,7 @@ func BenchmarkVectorOps(b *testing.B) { _b := b1[:n] _c := c1[:n] b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _c.Sub(_a, _b) } }) @@ -354,7 +354,7 @@ func BenchmarkVectorOps(b *testing.B) { _a := a1[:n] _c := c1[:n] b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _c.ScalarMul(_a, &mixer) } }) @@ -362,7 +362,7 @@ func BenchmarkVectorOps(b *testing.B) { b.Run(fmt.Sprintf("sum %d", n), func(b *testing.B) { _a := a1[:n] b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _ = _a.Sum() } }) @@ -371,7 +371,7 @@ func BenchmarkVectorOps(b *testing.B) { _a := a1[:n] _b := b1[:n] b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _ = _a.InnerProduct(_b) } }) @@ -381,7 +381,7 @@ func BenchmarkVectorOps(b *testing.B) { _b := b1[:n] _c := c1[:n] b.ResetTimer() - for i := 0; i < b.N; i++ { + for range b.N { _c.Mul(_a, _b) } }) @@ -403,7 +403,7 @@ func genMaxVector(size int) gopter.Gen { qMinusOne := qElement qMinusOne[0]-- - for i := 0; i < size; i++ { + for i := range size { g[i] = qMinusOne } genResult := gopter.NewGenResult(g, gopter.NoShrinker) diff --git a/std/algebra/native/fields_kb8/doc.go b/std/algebra/native/fields_octobear/doc.go similarity index 51% rename from std/algebra/native/fields_kb8/doc.go rename to std/algebra/native/fields_octobear/doc.go index c3ad405595..308e546003 100644 --- a/std/algebra/native/fields_kb8/doc.go +++ b/std/algebra/native/fields_octobear/doc.go @@ -1,5 +1,5 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Package fields_kb8 implements KoalaBear-native Fp^8 arithmetic for kb8-based gadgets. -package fields_kb8 +// Package fields_octobear implements KoalaBear-native Fp^8 arithmetic for octobear-based gadgets. +package fields_octobear diff --git a/std/algebra/native/fields_kb8/e2.go b/std/algebra/native/fields_octobear/e2.go similarity index 99% rename from std/algebra/native/fields_kb8/e2.go rename to std/algebra/native/fields_octobear/e2.go index b46aa441a4..958f750664 100644 --- a/std/algebra/native/fields_kb8/e2.go +++ b/std/algebra/native/fields_octobear/e2.go @@ -1,4 +1,4 @@ -package fields_kb8 +package fields_octobear import ( "github.com/consensys/gnark-crypto/field/koalabear" diff --git a/std/algebra/native/fields_kb8/e2_test.go b/std/algebra/native/fields_octobear/e2_test.go similarity index 98% rename from std/algebra/native/fields_kb8/e2_test.go rename to std/algebra/native/fields_octobear/e2_test.go index 3e4adb27aa..e04227a0e3 100644 --- a/std/algebra/native/fields_kb8/e2_test.go +++ b/std/algebra/native/fields_octobear/e2_test.go @@ -1,4 +1,4 @@ -package fields_kb8 +package fields_octobear import ( "testing" diff --git a/std/algebra/native/fields_kb8/e4.go b/std/algebra/native/fields_octobear/e4.go similarity index 99% rename from std/algebra/native/fields_kb8/e4.go rename to std/algebra/native/fields_octobear/e4.go index af4ada1113..a3b08cd41d 100644 --- a/std/algebra/native/fields_kb8/e4.go +++ b/std/algebra/native/fields_octobear/e4.go @@ -1,4 +1,4 @@ -package fields_kb8 +package fields_octobear import ( "github.com/consensys/gnark-crypto/field/koalabear/extensions" diff --git a/std/algebra/native/fields_kb8/e4_test.go b/std/algebra/native/fields_octobear/e4_test.go similarity index 98% rename from std/algebra/native/fields_kb8/e4_test.go rename to std/algebra/native/fields_octobear/e4_test.go index a6b9746f4b..14dca10302 100644 --- a/std/algebra/native/fields_kb8/e4_test.go +++ b/std/algebra/native/fields_octobear/e4_test.go @@ -1,4 +1,4 @@ -package fields_kb8 +package fields_octobear import ( "testing" diff --git a/std/algebra/native/fields_kb8/e8.go b/std/algebra/native/fields_octobear/e8.go similarity index 99% rename from std/algebra/native/fields_kb8/e8.go rename to std/algebra/native/fields_octobear/e8.go index c4455b06ed..768d1fe864 100644 --- a/std/algebra/native/fields_kb8/e8.go +++ b/std/algebra/native/fields_octobear/e8.go @@ -1,4 +1,4 @@ -package fields_kb8 +package fields_octobear import ( "github.com/consensys/gnark-crypto/field/koalabear/extensions" diff --git a/std/algebra/native/fields_kb8/e8_test.go b/std/algebra/native/fields_octobear/e8_test.go similarity index 99% rename from std/algebra/native/fields_kb8/e8_test.go rename to std/algebra/native/fields_octobear/e8_test.go index 4d31a82731..713250d4fb 100644 --- a/std/algebra/native/fields_kb8/e8_test.go +++ b/std/algebra/native/fields_octobear/e8_test.go @@ -1,4 +1,4 @@ -package fields_kb8 +package fields_octobear import ( "testing" diff --git a/std/algebra/native/fields_kb8/hints.go b/std/algebra/native/fields_octobear/hints.go similarity index 98% rename from std/algebra/native/fields_kb8/hints.go rename to std/algebra/native/fields_octobear/hints.go index 1929c695b6..9049028398 100644 --- a/std/algebra/native/fields_kb8/hints.go +++ b/std/algebra/native/fields_octobear/hints.go @@ -1,4 +1,4 @@ -package fields_kb8 +package fields_octobear import ( "fmt" diff --git a/std/algebra/native/maptocurve_kb8/doc.go b/std/algebra/native/maptocurve_kb8/doc.go deleted file mode 100644 index c31fae831b..0000000000 --- a/std/algebra/native/maptocurve_kb8/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package maptocurve_kb8 implements the y-increment map-to-curve gadget for -// the kb8 curve over the KoalaBear field. -package maptocurve_kb8 diff --git a/std/algebra/native/maptocurve_octobear/doc.go b/std/algebra/native/maptocurve_octobear/doc.go new file mode 100644 index 0000000000..135bfc6301 --- /dev/null +++ b/std/algebra/native/maptocurve_octobear/doc.go @@ -0,0 +1,3 @@ +// Package maptocurve_octobear implements the y-increment map-to-curve gadget for +// the octobear curve over the KoalaBear field. +package maptocurve_octobear diff --git a/std/algebra/native/maptocurve_kb8/hints.go b/std/algebra/native/maptocurve_octobear/hints.go similarity index 92% rename from std/algebra/native/maptocurve_kb8/hints.go rename to std/algebra/native/maptocurve_octobear/hints.go index 61a121849d..4972b4eb6c 100644 --- a/std/algebra/native/maptocurve_kb8/hints.go +++ b/std/algebra/native/maptocurve_octobear/hints.go @@ -1,10 +1,10 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "fmt" "math/big" - multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + multisethash "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark/constraint/solver" ) diff --git a/std/algebra/native/maptocurve_kb8/hints_linear.go b/std/algebra/native/maptocurve_octobear/hints_linear.go similarity index 91% rename from std/algebra/native/maptocurve_kb8/hints_linear.go rename to std/algebra/native/maptocurve_octobear/hints_linear.go index fe2d8409ec..019ed3cdfd 100644 --- a/std/algebra/native/maptocurve_kb8/hints_linear.go +++ b/std/algebra/native/maptocurve_octobear/hints_linear.go @@ -1,10 +1,10 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "fmt" "math/big" - multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + multisethash "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" ) // yIncrementLinearHint maps msg to LinearN points natively (using gnark-crypto's diff --git a/std/algebra/native/maptocurve_kb8/hints_poseidon2.go b/std/algebra/native/maptocurve_octobear/hints_poseidon2.go similarity index 87% rename from std/algebra/native/maptocurve_kb8/hints_poseidon2.go rename to std/algebra/native/maptocurve_octobear/hints_poseidon2.go index 710bd1e3cc..50514ece72 100644 --- a/std/algebra/native/maptocurve_kb8/hints_poseidon2.go +++ b/std/algebra/native/maptocurve_octobear/hints_poseidon2.go @@ -1,17 +1,17 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "fmt" "math/big" - multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + multisethash "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" ) // yIncrementPoseidon2Hint, given the PqN squeezed koalabear elements (already // computed in-circuit by the Poseidon2 sponge), produces per-coordinate // (q, s, k, x_coeffs[8]) where: // - q*B + s = squeezed[i] with s < B = ⌊p/(2T)⌋ -// - k < PqT and y = PqT*s + k yields a valid kb8 point with abscissa x. +// - k < PqT and y = PqT*s + k yields a valid octobear point with abscissa x. // // The cubic solve runs natively via gnark-crypto's MapAtSlot helper. func yIncrementPoseidon2Hint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { diff --git a/std/algebra/native/maptocurve_kb8/maptocurve.go b/std/algebra/native/maptocurve_octobear/maptocurve.go similarity index 84% rename from std/algebra/native/maptocurve_kb8/maptocurve.go rename to std/algebra/native/maptocurve_octobear/maptocurve.go index 68181c5225..7beac4232b 100644 --- a/std/algebra/native/maptocurve_kb8/maptocurve.go +++ b/std/algebra/native/maptocurve_octobear/maptocurve.go @@ -1,21 +1,21 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "errors" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" kbfp "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/algebra/native/fields_octobear" "github.com/consensys/gnark/std/rangecheck" ) const T = 256 -// YIncrement maps msg to a point on kb8 with y = msg*256 + k. +// YIncrement maps msg to a point on octobear with y = msg*256 + k. func YIncrement(api frontend.API, msg frontend.Variable) (G1Affine, error) { if !IsCompatible(api) { - return G1Affine{}, errors.New("expected KoalaBear native field for kb8 map-to-curve") + return G1Affine{}, errors.New("expected KoalaBear native field for octobear map-to-curve") } _ = api.ToBinary(msg, 16) @@ -27,7 +27,7 @@ func YIncrement(api frontend.API, msg frontend.Variable) (G1Affine, error) { rangecheck.New(api).Check(k, 8) x := fromCoeffs(res[1:]) - var y fields_kb8.E8 + var y fields_octobear.E8 y0 := api.Add(api.Mul(msg, T), k) y.SetZero() y.C0.B0.A0 = y0 @@ -45,7 +45,7 @@ func YIncrement(api frontend.API, msg frontend.Variable) (G1Affine, error) { // - The map never produces infinity, so the isInf branch is removed (~30 gates). // - The result is checked via direct AssertIsEqual instead of IsZero+Or (~15 gates). func assertIsOnCurve(api frontend.API, p *G1Affine) { - _, b := kb8.CurveCoefficients() + _, b := octobear.CurveCoefficients() // y² — exploit that y is in the base subfield: only y.C0.B0.A0 is nonzero var ySquared E8 diff --git a/std/algebra/native/maptocurve_kb8/maptocurve_test.go b/std/algebra/native/maptocurve_octobear/maptocurve_test.go similarity index 87% rename from std/algebra/native/maptocurve_kb8/maptocurve_test.go rename to std/algebra/native/maptocurve_octobear/maptocurve_test.go index ee36507693..a3a8cbe4ff 100644 --- a/std/algebra/native/maptocurve_kb8/maptocurve_test.go +++ b/std/algebra/native/maptocurve_octobear/maptocurve_test.go @@ -1,9 +1,9 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "testing" - nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + nativemsh "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/test" ) diff --git a/std/algebra/native/maptocurve_kb8/maptocurve_vector_linear.go b/std/algebra/native/maptocurve_octobear/maptocurve_vector_linear.go similarity index 76% rename from std/algebra/native/maptocurve_kb8/maptocurve_vector_linear.go rename to std/algebra/native/maptocurve_octobear/maptocurve_vector_linear.go index 87d73deba9..30b663e0fb 100644 --- a/std/algebra/native/maptocurve_kb8/maptocurve_vector_linear.go +++ b/std/algebra/native/maptocurve_octobear/maptocurve_vector_linear.go @@ -1,23 +1,23 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "errors" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/algebra/native/fields_octobear" "github.com/consensys/gnark/std/rangecheck" ) // Linear-separator vector ECMSH parameters (paper §4, App. B "T=128" row). -// These must match the native side (ecc/kb8/multiset-hash/vector_multiset_hash_linear.go). +// These must match the native side (ecc/octobear/multiset-hash/vector_multiset_hash_linear.go). const ( LinearN = 23 LinearT = 128 LinearM = 1 << 18 ) -// MapLinear maps msg to LinearN points on kb8 using the linear domain +// MapLinear maps msg to LinearN points on octobear using the linear domain // separator y_i(msg, k_i) = LinearT*(msg + i*LinearM) + k_i. // // The expensive cubic solve for each coordinate is performed outside the @@ -32,7 +32,7 @@ func MapLinear(api frontend.API, msg frontend.Variable) ([LinearN]G1Affine, erro var pts [LinearN]G1Affine if !IsCompatible(api) { - return pts, errors.New("expected KoalaBear native field for kb8 linear map-to-curve") + return pts, errors.New("expected KoalaBear native field for octobear linear map-to-curve") } // msg < 2^18 = LinearM @@ -44,7 +44,7 @@ func MapLinear(api frontend.API, msg frontend.Variable) ([LinearN]G1Affine, erro return pts, err } - _, b := kb8.CurveCoefficients() + _, b := octobear.CurveCoefficients() bE8 := newE8(b) rc := rangecheck.New(api) @@ -60,7 +60,7 @@ func MapLinear(api frontend.API, msg frontend.Variable) ([LinearN]G1Affine, erro // compile-time constant, so the api.Add folds into a linear combination. baseY := api.Mul(LinearT, api.Add(msg, i*LinearM)) - var y fields_kb8.E8 + var y fields_octobear.E8 y.SetZero() y.C0.B0.A0 = api.Add(baseY, k) p := G1Affine{X: x, Y: y} @@ -74,13 +74,13 @@ func MapLinear(api frontend.API, msg frontend.Variable) ([LinearN]G1Affine, erro // assertIsOnCurveWithB is the per-coordinate version of assertIsOnCurve that // takes the precomputed b ∈ Fp^8 to avoid recomputing CurveCoefficients in the // inner loop. Behaviour matches assertIsOnCurve in maptocurve.go. -func assertIsOnCurveWithB(api frontend.API, p *G1Affine, bE8 fields_kb8.E8) { - var ySquared fields_kb8.E8 +func assertIsOnCurveWithB(api frontend.API, p *G1Affine, bE8 fields_octobear.E8) { + var ySquared fields_octobear.E8 ySquared.SetZero() ySquared.C0.B0.A0 = api.Mul(p.Y.C0.B0.A0, p.Y.C0.B0.A0) - rhs := *new(fields_kb8.E8).Cube(api, p.X) - rhs.Sub(api, rhs, *new(fields_kb8.E8).MulByFp(api, p.X, 3)) + rhs := *new(fields_octobear.E8).Cube(api, p.X) + rhs.Sub(api, rhs, *new(fields_octobear.E8).MulByFp(api, p.X, 3)) rhs.Add(api, rhs, bE8) ySquared.AssertIsEqual(api, rhs) diff --git a/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go b/std/algebra/native/maptocurve_octobear/maptocurve_vector_poseidon2.go similarity index 88% rename from std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go rename to std/algebra/native/maptocurve_octobear/maptocurve_vector_poseidon2.go index 83bbf93a34..0f18078ce3 100644 --- a/std/algebra/native/maptocurve_kb8/maptocurve_vector_poseidon2.go +++ b/std/algebra/native/maptocurve_octobear/maptocurve_vector_poseidon2.go @@ -1,21 +1,21 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "encoding/binary" "errors" "math/big" - "github.com/consensys/gnark-crypto/ecc/kb8" - multisethash "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/ecc/octobear" + multisethash "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/algebra/native/fields_octobear" "github.com/consensys/gnark/std/permutation/poseidon2" "github.com/consensys/gnark/std/rangecheck" ) // Poseidon2-sponge vector ECMSH parameters (paper §4.3 "preferred" derivation). // These must match the native side -// (ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go). +// (ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go). const ( PqN = 23 PqT = 256 @@ -29,7 +29,7 @@ const ( ) // MapPoseidon2 maps a 64-bit message (split into a low and a high 32-bit half -// to fit into the koalabear field) to PqN points on kb8 using a width-16 +// to fit into the koalabear field) to PqN points on octobear using a width-16 // Poseidon2 sponge with rate PqSqueezeRate. // // Both halves are expected to be ≤ 2^32 − 1. The function constrains each @@ -50,7 +50,7 @@ func MapPoseidon2(api frontend.API, msgLow, msgHigh frontend.Variable) ([PqN]G1A var pts [PqN]G1Affine if !IsCompatible(api) { - return pts, errors.New("expected KoalaBear native field for kb8 Poseidon2 map-to-curve") + return pts, errors.New("expected KoalaBear native field for octobear Poseidon2 map-to-curve") } // Constrain msgLow, msgHigh < 2^32. @@ -95,7 +95,7 @@ func MapPoseidon2(api frontend.API, msgLow, msgHigh frontend.Variable) ([PqN]G1A return pts, err } - _, b := kb8.CurveCoefficients() + _, b := octobear.CurveCoefficients() bE8 := newE8(b) bound := multisethash.PqReducerBound() boundMinusOne := new(big.Int).Sub(bound, big.NewInt(1)) @@ -122,7 +122,7 @@ func MapPoseidon2(api frontend.API, msgLow, msgHigh frontend.Variable) ([PqN]G1A // baseY = T * s; y = (T*s + k, 0, ..., 0) in E8. baseY := api.Mul(PqT, s) - var y fields_kb8.E8 + var y fields_octobear.E8 y.SetZero() y.C0.B0.A0 = api.Add(baseY, k) pt := G1Affine{X: x, Y: y} diff --git a/std/algebra/native/maptocurve_kb8/types.go b/std/algebra/native/maptocurve_octobear/types.go similarity index 65% rename from std/algebra/native/maptocurve_kb8/types.go rename to std/algebra/native/maptocurve_octobear/types.go index 0a245457a8..f801dd9a6b 100644 --- a/std/algebra/native/maptocurve_kb8/types.go +++ b/std/algebra/native/maptocurve_octobear/types.go @@ -1,21 +1,21 @@ -package maptocurve_kb8 +package maptocurve_octobear import ( "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/fields_kb8" + "github.com/consensys/gnark/std/algebra/native/fields_octobear" ) -type E2 = fields_kb8.E2 -type E4 = fields_kb8.E4 -type E8 = fields_kb8.E8 +type E2 = fields_octobear.E2 +type E4 = fields_octobear.E4 +type E8 = fields_octobear.E8 type G1Affine struct { X, Y E8 } func newE8(v extensions.E8) E8 { - return fields_kb8.NewE8(v) + return fields_octobear.NewE8(v) } func fromCoeffs(v []frontend.Variable) E8 { diff --git a/std/algebra/native/sw_kb8/doc.go b/std/algebra/native/sw_kb8/doc.go deleted file mode 100644 index c06f339a20..0000000000 --- a/std/algebra/native/sw_kb8/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2020-2026 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Package sw_kb8 provides native KoalaBear-field circuit gadgets for the kb8 -// elliptic curve and its 1-point multiset-hash construction. The y-increment -// map-to-curve gadget lives in package maptocurve_kb8. -package sw_kb8 diff --git a/std/algebra/native/sw_kb8/hints.go b/std/algebra/native/sw_kb8/hints.go deleted file mode 100644 index 50ebc4d180..0000000000 --- a/std/algebra/native/sw_kb8/hints.go +++ /dev/null @@ -1,10 +0,0 @@ -package sw_kb8 - -import ( - "github.com/consensys/gnark/constraint/solver" - "github.com/consensys/gnark/std/algebra/native/fields_kb8" -) - -func GetHints() []solver.Hint { - return fields_kb8.GetHints() -} diff --git a/std/algebra/native/sw_kb8/types.go b/std/algebra/native/sw_kb8/types.go deleted file mode 100644 index 13fed2357d..0000000000 --- a/std/algebra/native/sw_kb8/types.go +++ /dev/null @@ -1,29 +0,0 @@ -package sw_kb8 - -import ( - nativekb8 "github.com/consensys/gnark-crypto/ecc/kb8" - "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/fields_kb8" -) - -type E2 = fields_kb8.E2 -type E4 = fields_kb8.E4 -type E8 = fields_kb8.E8 - -type G1Affine struct { - X, Y E8 -} - -func NewG1Affine(v nativekb8.G1Affine) G1Affine { - return G1Affine{X: fields_kb8.NewE8(v.X), Y: fields_kb8.NewE8(v.Y)} -} - -func (p *G1Affine) Assign(v *nativekb8.G1Affine) { - p.X.Assign(&v.X) - p.Y.Assign(&v.Y) -} - -func (p *G1Affine) AssertIsEqual(api frontend.API, other G1Affine) { - p.X.AssertIsEqual(api, other.X) - p.Y.AssertIsEqual(api, other.Y) -} diff --git a/std/algebra/native/sw_octobear/doc.go b/std/algebra/native/sw_octobear/doc.go new file mode 100644 index 0000000000..cc79d4cbdf --- /dev/null +++ b/std/algebra/native/sw_octobear/doc.go @@ -0,0 +1,7 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Package sw_octobear provides native KoalaBear-field circuit gadgets for the +// octobear elliptic curve and its 1-point multiset-hash construction. The +// y-increment map-to-curve gadget lives in package maptocurve_octobear. +package sw_octobear diff --git a/std/algebra/native/sw_kb8/g1.go b/std/algebra/native/sw_octobear/g1.go similarity index 85% rename from std/algebra/native/sw_kb8/g1.go rename to std/algebra/native/sw_octobear/g1.go index d9bbee658b..ffbc4c321d 100644 --- a/std/algebra/native/sw_kb8/g1.go +++ b/std/algebra/native/sw_octobear/g1.go @@ -1,57 +1,57 @@ -package sw_kb8 +package sw_octobear import ( "errors" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" kbfp "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/fields_kb8" - "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/std/algebra/native/fields_octobear" + "github.com/consensys/gnark/std/algebra/native/maptocurve_octobear" ) -// Curve exposes kb8 point operations in circuits over the KoalaBear field. +// Curve exposes octobear point operations in circuits over the KoalaBear field. type Curve struct { api frontend.API } var ( curveA, curveB, accumulatorOffset = func() (E8, E8, G1Affine) { - a, b := kb8.CurveCoefficients() - _, offsetNative := kb8.Generators() - return fields_kb8.NewE8(a), fields_kb8.NewE8(b), NewG1Affine(offsetNative) + a, b := octobear.CurveCoefficients() + _, offsetNative := octobear.Generators() + return fields_octobear.NewE8(a), fields_octobear.NewE8(b), NewG1Affine(offsetNative) }() ) -func fromMapE2(v maptocurve_kb8.E2) E2 { - return E2{A0: v.A0, A1: v.A1} +func fromMapE2(v maptocurve_octobear.E2) E2 { + return E2(v) } -func fromMapE4(v maptocurve_kb8.E4) E4 { +func fromMapE4(v maptocurve_octobear.E4) E4 { return E4{ B0: fromMapE2(v.B0), B1: fromMapE2(v.B1), } } -func fromMapE8(v maptocurve_kb8.E8) E8 { +func fromMapE8(v maptocurve_octobear.E8) E8 { return E8{ C0: fromMapE4(v.C0), C1: fromMapE4(v.C1), } } -func fromMapPoint(v maptocurve_kb8.G1Affine) G1Affine { +func fromMapPoint(v maptocurve_octobear.G1Affine) G1Affine { return G1Affine{ X: fromMapE8(v.X), Y: fromMapE8(v.Y), } } -// NewCurve initializes a new kb8 curve gadget. +// NewCurve initializes a new octobear curve gadget. func NewCurve(api frontend.API) (*Curve, error) { if api.Compiler().Field().Cmp(kbfp.Modulus()) != 0 { - return nil, errors.New("expected KoalaBear native field for kb8 operations") + return nil, errors.New("expected KoalaBear native field for octobear operations") } return &Curve{api: api}, nil } @@ -222,7 +222,7 @@ func (c *Curve) isInfinity(p *G1Affine) frontend.Variable { return c.api.And(p.X.IsZero(c.api), p.Y.IsZero(c.api)) } -// AssertIsOnCurve asserts that p is infinity or lies on kb8. +// AssertIsOnCurve asserts that p is infinity or lies on octobear. func (c *Curve) AssertIsOnCurve(p *G1Affine) { isInf := c.isInfinity(p) left := *new(E8).Square(c.api, p.Y) @@ -234,7 +234,7 @@ func (c *Curve) AssertIsOnCurve(p *G1Affine) { c.api.AssertIsEqual(c.api.Or(isInf, isCurve), 1) } -// AssertIsInSubGroup asserts subgroup membership. kb8 has prime order, so this +// AssertIsInSubGroup asserts subgroup membership. octobear has prime order, so this // is equivalent to the on-curve check. func (c *Curve) AssertIsInSubGroup(p *G1Affine) { c.AssertIsOnCurve(p) diff --git a/std/algebra/native/sw_kb8/g1_test.go b/std/algebra/native/sw_octobear/g1_test.go similarity index 90% rename from std/algebra/native/sw_kb8/g1_test.go rename to std/algebra/native/sw_octobear/g1_test.go index 10bbb12734..110609d2a3 100644 --- a/std/algebra/native/sw_kb8/g1_test.go +++ b/std/algebra/native/sw_octobear/g1_test.go @@ -1,9 +1,9 @@ -package sw_kb8 +package sw_octobear import ( "testing" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/test" ) @@ -23,7 +23,7 @@ func (circuit *g1AddAssignAffine) Define(api frontend.API) error { func TestAddAssignAffineG1(t *testing.T) { assert := test.NewAssert(t) aJac, bJac := distinctPointsG1(t) - var a, b, c kb8.G1Affine + var a, b, c octobear.G1Affine a.FromJacobian(&aJac) b.FromJacobian(&bJac) aJac.AddAssign(&bJac) @@ -52,7 +52,7 @@ func (circuit *g1DoubleAffine) Define(api frontend.API) error { func TestDoubleAffineG1(t *testing.T) { assert := test.NewAssert(t) aJac := randomPointG1(t) - var a, c kb8.G1Affine + var a, c octobear.G1Affine a.FromJacobian(&aJac) aJac.DoubleAssign() c.FromJacobian(&aJac) @@ -79,7 +79,7 @@ func (circuit *g1AddUnifiedAffine) Define(api frontend.API) error { func TestAddUnifiedAffineG1(t *testing.T) { assert := test.NewAssert(t) aJac, bJac := distinctPointsG1(t) - var a, b, c kb8.G1Affine + var a, b, c octobear.G1Affine a.FromJacobian(&aJac) b.FromJacobian(&bJac) aJac.AddAssign(&bJac) @@ -108,7 +108,7 @@ func (circuit *g1DoubleAndAddAffine) Define(api frontend.API) error { func TestDoubleAndAddAffineG1(t *testing.T) { assert := test.NewAssert(t) aJac, bJac := distinctPointsG1(t) - var a, b, c kb8.G1Affine + var a, b, c octobear.G1Affine a.FromJacobian(&aJac) b.FromJacobian(&bJac) aJac.DoubleAssign().AddAssign(&bJac) @@ -137,7 +137,7 @@ func (circuit *g1AddBrierJoyeAffine) Define(api frontend.API) error { func TestAddBrierJoyeAffineG1(t *testing.T) { assert := test.NewAssert(t) aJac, bJac := distinctPointsG1(t) - var a, b, c kb8.G1Affine + var a, b, c octobear.G1Affine a.FromJacobian(&aJac) b.FromJacobian(&bJac) aJac.AddAssign(&bJac) @@ -154,7 +154,7 @@ func TestAddBrierJoyeAffineG1(t *testing.T) { func TestAddBrierJoyeDoubleG1(t *testing.T) { assert := test.NewAssert(t) aJac := randomPointG1(t) - var a, c kb8.G1Affine + var a, c octobear.G1Affine a.FromJacobian(&aJac) aJac.DoubleAssign() c.FromJacobian(&aJac) @@ -170,10 +170,10 @@ func TestAddBrierJoyeDoubleG1(t *testing.T) { func TestAddBrierJoyeOppositeG1(t *testing.T) { assert := test.NewAssert(t) aJac := randomPointG1(t) - var a kb8.G1Affine + var a octobear.G1Affine a.FromJacobian(&aJac) - var negA kb8.G1Affine + var negA octobear.G1Affine negA.Neg(&a) var witness g1AddBrierJoyeAffine @@ -185,10 +185,10 @@ func TestAddBrierJoyeOppositeG1(t *testing.T) { assert.CheckCircuit(&g1AddBrierJoyeAffine{}, test.WithValidAssignment(&witness), test.WithoutCurveChecks(), test.WithSmallfieldCheck()) } -func randomPointG1(t *testing.T) kb8.G1Jac { +func randomPointG1(t *testing.T) octobear.G1Jac { t.Helper() - _, g := kb8.Generators() - var s kb8.G1Jac + _, g := octobear.Generators() + var s octobear.G1Jac s.FromAffine(&g) for s.Z.IsZero() { // impossible path, keep non-zero point invariant @@ -197,7 +197,7 @@ func randomPointG1(t *testing.T) kb8.G1Jac { return s } -func distinctPointsG1(t *testing.T) (kb8.G1Jac, kb8.G1Jac) { +func distinctPointsG1(t *testing.T) (octobear.G1Jac, octobear.G1Jac) { t.Helper() a := randomPointG1(t) b := a diff --git a/std/algebra/native/sw_octobear/hints.go b/std/algebra/native/sw_octobear/hints.go new file mode 100644 index 0000000000..b0524d6d80 --- /dev/null +++ b/std/algebra/native/sw_octobear/hints.go @@ -0,0 +1,10 @@ +package sw_octobear + +import ( + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/std/algebra/native/fields_octobear" +) + +func GetHints() []solver.Hint { + return fields_octobear.GetHints() +} diff --git a/std/algebra/native/sw_kb8/multisethash.go b/std/algebra/native/sw_octobear/multisethash.go similarity index 91% rename from std/algebra/native/sw_kb8/multisethash.go rename to std/algebra/native/sw_octobear/multisethash.go index b2bedcd263..68591c8d84 100644 --- a/std/algebra/native/sw_kb8/multisethash.go +++ b/std/algebra/native/sw_octobear/multisethash.go @@ -1,8 +1,8 @@ -package sw_kb8 +package sw_octobear import ( "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/std/algebra/native/maptocurve_octobear" ) // Accumulator stores the 1-point multiset hash state. @@ -21,7 +21,7 @@ func NewAccumulator(curve *Curve) *Accumulator { // Insert maps msg and adds it to the accumulator. func (a *Accumulator) Insert(msg frontend.Variable) error { - p, err := maptocurve_kb8.YIncrement(a.curve.api, msg) + p, err := maptocurve_octobear.YIncrement(a.curve.api, msg) if err != nil { return err } diff --git a/std/algebra/native/sw_kb8/multisethash_test.go b/std/algebra/native/sw_octobear/multisethash_test.go similarity index 90% rename from std/algebra/native/sw_kb8/multisethash_test.go rename to std/algebra/native/sw_octobear/multisethash_test.go index 7b6661e832..d136cbc3ee 100644 --- a/std/algebra/native/sw_kb8/multisethash_test.go +++ b/std/algebra/native/sw_octobear/multisethash_test.go @@ -1,10 +1,10 @@ -package sw_kb8 +package sw_octobear import ( "testing" - "github.com/consensys/gnark-crypto/ecc/kb8" - nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/ecc/octobear" + nativemsh "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" @@ -14,7 +14,7 @@ import ( "github.com/consensys/gnark/test" ) -// multisetHashCircuit is the 1-point kb8 multiset-hash verification circuit. +// multisetHashCircuit is the 1-point octobear multiset-hash verification circuit. type multisetHashCircuit struct { Msgs [4]frontend.Variable Digest G1Affine @@ -130,13 +130,13 @@ func BenchmarkMultisetHashCircuitSolve(b *testing.B) { }) } -func shiftedDigest(d kb8.G1Affine) kb8.G1Affine { - _, offset := kb8.Generators() - var jd, jo kb8.G1Jac +func shiftedDigest(d octobear.G1Affine) octobear.G1Affine { + _, offset := octobear.Generators() + var jd, jo octobear.G1Jac jd.FromAffine(&d) jo.FromAffine(&offset) jd.AddAssign(&jo) - var shifted kb8.G1Affine + var shifted octobear.G1Affine shifted.FromJacobian(&jd) return shifted } diff --git a/std/algebra/native/sw_octobear/types.go b/std/algebra/native/sw_octobear/types.go new file mode 100644 index 0000000000..de225bbf75 --- /dev/null +++ b/std/algebra/native/sw_octobear/types.go @@ -0,0 +1,29 @@ +package sw_octobear + +import ( + nativeoctobear "github.com/consensys/gnark-crypto/ecc/octobear" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/native/fields_octobear" +) + +type E2 = fields_octobear.E2 +type E4 = fields_octobear.E4 +type E8 = fields_octobear.E8 + +type G1Affine struct { + X, Y E8 +} + +func NewG1Affine(v nativeoctobear.G1Affine) G1Affine { + return G1Affine{X: fields_octobear.NewE8(v.X), Y: fields_octobear.NewE8(v.Y)} +} + +func (p *G1Affine) Assign(v *nativeoctobear.G1Affine) { + p.X.Assign(&v.X) + p.Y.Assign(&v.Y) +} + +func (p *G1Affine) AssertIsEqual(api frontend.API, other G1Affine) { + p.X.AssertIsEqual(api, other.X) + p.Y.AssertIsEqual(api, other.Y) +} diff --git a/std/algebra/native/sw_kb8/vector_multisethash_linear.go b/std/algebra/native/sw_octobear/vector_multisethash_linear.go similarity index 82% rename from std/algebra/native/sw_kb8/vector_multisethash_linear.go rename to std/algebra/native/sw_octobear/vector_multisethash_linear.go index d480710b8c..d9097865d8 100644 --- a/std/algebra/native/sw_kb8/vector_multisethash_linear.go +++ b/std/algebra/native/sw_octobear/vector_multisethash_linear.go @@ -1,8 +1,8 @@ -package sw_kb8 +package sw_octobear import ( "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/std/algebra/native/maptocurve_octobear" ) // LinearAccumulator stores the N-coordinate linear-separator vector ECMSH state. @@ -13,7 +13,7 @@ import ( // group (and produces an unsatisfiable division by zero otherwise). type LinearAccumulator struct { curve *Curve - sums [maptocurve_kb8.LinearN]G1Affine + sums [maptocurve_octobear.LinearN]G1Affine } // NewLinearAccumulator returns a zero linear accumulator. Each coordinate is @@ -29,7 +29,7 @@ func NewLinearAccumulator(curve *Curve) *LinearAccumulator { // Insert maps msg via the linear separator and adds each of the N mapped // points to the matching accumulator coordinate. func (a *LinearAccumulator) Insert(msg frontend.Variable) error { - pts, err := maptocurve_kb8.MapLinear(a.curve.api, msg) + pts, err := maptocurve_octobear.MapLinear(a.curve.api, msg) if err != nil { return err } @@ -41,7 +41,7 @@ func (a *LinearAccumulator) Insert(msg frontend.Variable) error { } // Digest returns the current vector of accumulator points. -func (a *LinearAccumulator) Digest() [maptocurve_kb8.LinearN]G1Affine { +func (a *LinearAccumulator) Digest() [maptocurve_octobear.LinearN]G1Affine { return a.sums } @@ -53,11 +53,11 @@ func (a *LinearAccumulator) Reset() { } // HashLinear returns the linear-separator vector multiset hash of msgs. -func (c *Curve) HashLinear(msgs []frontend.Variable) ([maptocurve_kb8.LinearN]G1Affine, error) { +func (c *Curve) HashLinear(msgs []frontend.Variable) ([maptocurve_octobear.LinearN]G1Affine, error) { acc := NewLinearAccumulator(c) for _, msg := range msgs { if err := acc.Insert(msg); err != nil { - return [maptocurve_kb8.LinearN]G1Affine{}, err + return [maptocurve_octobear.LinearN]G1Affine{}, err } } return acc.Digest(), nil diff --git a/std/algebra/native/sw_kb8/vector_multisethash_linear_test.go b/std/algebra/native/sw_octobear/vector_multisethash_linear_test.go similarity index 85% rename from std/algebra/native/sw_kb8/vector_multisethash_linear_test.go rename to std/algebra/native/sw_octobear/vector_multisethash_linear_test.go index db3802760b..0b0e67bd5e 100644 --- a/std/algebra/native/sw_kb8/vector_multisethash_linear_test.go +++ b/std/algebra/native/sw_octobear/vector_multisethash_linear_test.go @@ -1,24 +1,24 @@ -package sw_kb8 +package sw_octobear import ( "testing" - "github.com/consensys/gnark-crypto/ecc/kb8" - nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/ecc/octobear" + nativemsh "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/widecommitter" - "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/std/algebra/native/maptocurve_octobear" "github.com/consensys/gnark/test" ) // linearHashCircuit verifies LinearAccumulator over a small batch of inserts. type linearHashCircuit struct { Msgs [4]frontend.Variable - Digest [maptocurve_kb8.LinearN]G1Affine + Digest [maptocurve_octobear.LinearN]G1Affine } func (c *linearHashCircuit) Define(api frontend.API) error { @@ -40,7 +40,7 @@ func (c *linearHashCircuit) Define(api frontend.API) error { // linearSingleInsertCircuit measures the per-Insert constraint cost. type linearSingleInsertCircuit struct { Msg frontend.Variable - Digest [maptocurve_kb8.LinearN]G1Affine + Digest [maptocurve_octobear.LinearN]G1Affine } func (c *linearSingleInsertCircuit) Define(api frontend.API) error { @@ -63,11 +63,11 @@ func (c *linearSingleInsertCircuit) Define(api frontend.API) error { // LinearAccumulator (each coordinate starts at the generator G). The native // HashLinear returns un-shifted sums starting at infinity, so we add G to each // coordinate to bring them in sync with what the circuit accumulator computes. -func shiftedLinearDigest(d [maptocurve_kb8.LinearN]kb8.G1Affine) [maptocurve_kb8.LinearN]kb8.G1Affine { - _, offset := kb8.Generators() - var out [maptocurve_kb8.LinearN]kb8.G1Affine +func shiftedLinearDigest(d [maptocurve_octobear.LinearN]octobear.G1Affine) [maptocurve_octobear.LinearN]octobear.G1Affine { + _, offset := octobear.Generators() + var out [maptocurve_octobear.LinearN]octobear.G1Affine for i := range d { - var jd, jo kb8.G1Jac + var jd, jo octobear.G1Jac jd.FromAffine(&d[i]) jo.FromAffine(&offset) jd.AddAssign(&jo) @@ -76,8 +76,8 @@ func shiftedLinearDigest(d [maptocurve_kb8.LinearN]kb8.G1Affine) [maptocurve_kb8 return out } -func newLinearWitnessDigest(d [maptocurve_kb8.LinearN]kb8.G1Affine) [maptocurve_kb8.LinearN]G1Affine { - var out [maptocurve_kb8.LinearN]G1Affine +func newLinearWitnessDigest(d [maptocurve_octobear.LinearN]octobear.G1Affine) [maptocurve_octobear.LinearN]G1Affine { + var out [maptocurve_octobear.LinearN]G1Affine for i := range d { out[i] = NewG1Affine(d[i]) } @@ -120,7 +120,7 @@ func TestLinearHashHomomorphic(t *testing.T) { t.Fatal(err) } for i := range full { - var sum kb8.G1Affine + var sum octobear.G1Affine sum.Add(&dA[i], &dB[i]) if !sum.Equal(&full[i]) { t.Fatalf("native HashLinear is not additive at coord %d", i) diff --git a/std/algebra/native/sw_kb8/vector_multisethash_poseidon2.go b/std/algebra/native/sw_octobear/vector_multisethash_poseidon2.go similarity index 75% rename from std/algebra/native/sw_kb8/vector_multisethash_poseidon2.go rename to std/algebra/native/sw_octobear/vector_multisethash_poseidon2.go index 7cd195e00e..b9f690ee57 100644 --- a/std/algebra/native/sw_kb8/vector_multisethash_poseidon2.go +++ b/std/algebra/native/sw_octobear/vector_multisethash_poseidon2.go @@ -1,13 +1,13 @@ -package sw_kb8 +package sw_octobear import ( "errors" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/std/algebra/native/maptocurve_octobear" ) -var errPoseidon2MismatchedHalves = errors.New("kb8 Poseidon2 multiset hash: msgsLow and msgsHigh must have the same length") +var errPoseidon2MismatchedHalves = errors.New("octobear Poseidon2 multiset hash: msgsLow and msgsHigh must have the same length") // Poseidon2Accumulator stores the N-coordinate Poseidon2-sponge vector ECMSH // state. Each coordinate accumulator starts at the fixed offset generator, @@ -15,7 +15,7 @@ var errPoseidon2MismatchedHalves = errors.New("kb8 Poseidon2 multiset hash: msgs // Accumulator and the LinearAccumulator. type Poseidon2Accumulator struct { curve *Curve - sums [maptocurve_kb8.PqN]G1Affine + sums [maptocurve_octobear.PqN]G1Affine } // NewPoseidon2Accumulator returns a zero Poseidon2 accumulator. Each coordinate @@ -32,7 +32,7 @@ func NewPoseidon2Accumulator(curve *Curve) *Poseidon2Accumulator { // msgHigh) — via the Poseidon2 sponge separator and adds each of the PqN // mapped points to the matching accumulator coordinate. func (a *Poseidon2Accumulator) Insert(msgLow, msgHigh frontend.Variable) error { - pts, err := maptocurve_kb8.MapPoseidon2(a.curve.api, msgLow, msgHigh) + pts, err := maptocurve_octobear.MapPoseidon2(a.curve.api, msgLow, msgHigh) if err != nil { return err } @@ -44,7 +44,7 @@ func (a *Poseidon2Accumulator) Insert(msgLow, msgHigh frontend.Variable) error { } // Digest returns the current vector of accumulator points. -func (a *Poseidon2Accumulator) Digest() [maptocurve_kb8.PqN]G1Affine { +func (a *Poseidon2Accumulator) Digest() [maptocurve_octobear.PqN]G1Affine { return a.sums } @@ -57,14 +57,14 @@ func (a *Poseidon2Accumulator) Reset() { // HashPoseidon2 returns the Poseidon2-sponge vector multiset hash of msgs. // Each message is supplied as (low, high) 32-bit halves of a 64-bit value. -func (c *Curve) HashPoseidon2(msgsLow, msgsHigh []frontend.Variable) ([maptocurve_kb8.PqN]G1Affine, error) { +func (c *Curve) HashPoseidon2(msgsLow, msgsHigh []frontend.Variable) ([maptocurve_octobear.PqN]G1Affine, error) { if len(msgsLow) != len(msgsHigh) { - return [maptocurve_kb8.PqN]G1Affine{}, errPoseidon2MismatchedHalves + return [maptocurve_octobear.PqN]G1Affine{}, errPoseidon2MismatchedHalves } acc := NewPoseidon2Accumulator(c) for i := range msgsLow { if err := acc.Insert(msgsLow[i], msgsHigh[i]); err != nil { - return [maptocurve_kb8.PqN]G1Affine{}, err + return [maptocurve_octobear.PqN]G1Affine{}, err } } return acc.Digest(), nil diff --git a/std/algebra/native/sw_kb8/vector_multisethash_poseidon2_test.go b/std/algebra/native/sw_octobear/vector_multisethash_poseidon2_test.go similarity index 85% rename from std/algebra/native/sw_kb8/vector_multisethash_poseidon2_test.go rename to std/algebra/native/sw_octobear/vector_multisethash_poseidon2_test.go index 38170bfada..2337e6fb5f 100644 --- a/std/algebra/native/sw_kb8/vector_multisethash_poseidon2_test.go +++ b/std/algebra/native/sw_octobear/vector_multisethash_poseidon2_test.go @@ -1,17 +1,17 @@ -package sw_kb8 +package sw_octobear import ( "testing" - "github.com/consensys/gnark-crypto/ecc/kb8" - nativemsh "github.com/consensys/gnark-crypto/ecc/kb8/multiset-hash" + "github.com/consensys/gnark-crypto/ecc/octobear" + nativemsh "github.com/consensys/gnark-crypto/ecc/octobear/multiset-hash" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/widecommitter" - "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/std/algebra/native/maptocurve_octobear" "github.com/consensys/gnark/test" ) @@ -19,7 +19,7 @@ import ( type poseidon2HashCircuit struct { MsgsLow [4]frontend.Variable MsgsHigh [4]frontend.Variable - Digest [maptocurve_kb8.PqN]G1Affine + Digest [maptocurve_octobear.PqN]G1Affine } func (c *poseidon2HashCircuit) Define(api frontend.API) error { @@ -41,7 +41,7 @@ func (c *poseidon2HashCircuit) Define(api frontend.API) error { type poseidon2SingleInsertCircuit struct { MsgLow frontend.Variable MsgHigh frontend.Variable - Digest [maptocurve_kb8.PqN]G1Affine + Digest [maptocurve_octobear.PqN]G1Affine } func (c *poseidon2SingleInsertCircuit) Define(api frontend.API) error { @@ -60,11 +60,11 @@ func (c *poseidon2SingleInsertCircuit) Define(api frontend.API) error { return nil } -func shiftedPoseidon2Digest(d [maptocurve_kb8.PqN]kb8.G1Affine) [maptocurve_kb8.PqN]kb8.G1Affine { - _, offset := kb8.Generators() - var out [maptocurve_kb8.PqN]kb8.G1Affine +func shiftedPoseidon2Digest(d [maptocurve_octobear.PqN]octobear.G1Affine) [maptocurve_octobear.PqN]octobear.G1Affine { + _, offset := octobear.Generators() + var out [maptocurve_octobear.PqN]octobear.G1Affine for i := range d { - var jd, jo kb8.G1Jac + var jd, jo octobear.G1Jac jd.FromAffine(&d[i]) jo.FromAffine(&offset) jd.AddAssign(&jo) @@ -73,8 +73,8 @@ func shiftedPoseidon2Digest(d [maptocurve_kb8.PqN]kb8.G1Affine) [maptocurve_kb8. return out } -func newPoseidon2WitnessDigest(d [maptocurve_kb8.PqN]kb8.G1Affine) [maptocurve_kb8.PqN]G1Affine { - var out [maptocurve_kb8.PqN]G1Affine +func newPoseidon2WitnessDigest(d [maptocurve_octobear.PqN]octobear.G1Affine) [maptocurve_octobear.PqN]G1Affine { + var out [maptocurve_octobear.PqN]G1Affine for i := range d { out[i] = NewG1Affine(d[i]) } @@ -126,7 +126,7 @@ func TestPoseidon2HashHomomorphic(t *testing.T) { t.Fatal(err) } for i := range full { - var sum kb8.G1Affine + var sum octobear.G1Affine sum.Add(&dA[i], &dB[i]) if !sum.Equal(&full[i]) { t.Fatalf("native HashPoseidon2 is not additive at coord %d", i) diff --git a/std/hints.go b/std/hints.go index e2696c887e..0783e69293 100644 --- a/std/hints.go +++ b/std/hints.go @@ -11,9 +11,9 @@ import ( "github.com/consensys/gnark/std/algebra/emulated/sw_bw6761" "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" "github.com/consensys/gnark/std/algebra/native/fields_bls12377" - "github.com/consensys/gnark/std/algebra/native/maptocurve_kb8" + "github.com/consensys/gnark/std/algebra/native/maptocurve_octobear" "github.com/consensys/gnark/std/algebra/native/sw_bls12377" - "github.com/consensys/gnark/std/algebra/native/sw_kb8" + "github.com/consensys/gnark/std/algebra/native/sw_octobear" "github.com/consensys/gnark/std/algebra/native/twistededwards" "github.com/consensys/gnark/std/conversion" "github.com/consensys/gnark/std/evmprecompiles" @@ -65,8 +65,8 @@ func registerHints() { solver.RegisterHint(sw_bw6761.GetHints()...) // native curves solver.RegisterHint(sw_bls12377.GetHints()...) - solver.RegisterHint(maptocurve_kb8.GetHints()...) - solver.RegisterHint(sw_kb8.GetHints()...) + solver.RegisterHint(maptocurve_octobear.GetHints()...) + solver.RegisterHint(sw_octobear.GetHints()...) // field extensions solver.RegisterHint(fieldextension.GetHints()...) } diff --git a/std/internal/fieldextension/koalabear_ext_test.go b/std/internal/fieldextension/koalabear_ext_test.go index a700e80cb8..a50f6fd726 100644 --- a/std/internal/fieldextension/koalabear_ext_test.go +++ b/std/internal/fieldextension/koalabear_ext_test.go @@ -158,7 +158,7 @@ func TestKoalabearExt2MulByNonResidue(t *testing.T) { assert := test.NewAssert(t) var a, c extensions.E2 a.MustSetRandom() - c.MulByNonResidue(&a) + c.MulByQuadraticNonResidue(&a) assert.CheckCircuit( &kbE2MulByNonResidueTestCircuit{}, diff --git a/std/permutation/poseidon2/poseidon2_koalabear_test.go b/std/permutation/poseidon2/poseidon2_koalabear_test.go index 76cd5c5374..a0ba5e61b6 100644 --- a/std/permutation/poseidon2/poseidon2_koalabear_test.go +++ b/std/permutation/poseidon2/poseidon2_koalabear_test.go @@ -24,9 +24,7 @@ func (c *poseidon2KoalaBearCircuit) Define(api frontend.API) error { return err } state := make([]frontend.Variable, koalaBearWidth) - for i := range c.Input { - state[i] = c.Input[i] - } + copy(state, c.Input[:]) if err := h.Permutation(state); err != nil { return err } From 37019c26c2a6fb757edaae6f492eb6102a1a798a Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 1 Jun 2026 15:42:33 -0400 Subject: [PATCH 14/15] fix: apply copilot suggestion --- go.mod | 2 +- go.sum | 4 ++-- internal/smallfields/tinyfield/element.go | 4 ++-- internal/smallfields/tinyfield/element_test.go | 18 +++++------------- .../native/maptocurve_octobear/maptocurve.go | 4 +++- .../native/maptocurve_octobear/types.go | 14 ++++++++++++++ std/algebra/native/sw_octobear/g1.go | 4 ++++ 7 files changed, 31 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 7dc3e13e8e..bde5a483ea 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.20.2-0.20260521220852-8d7eba492bae + github.com/consensys/gnark-crypto v0.20.2-0.20260601192128-f6b0b478eda6 github.com/fxamacker/cbor/v2 v2.9.0 github.com/google/go-cmp v0.7.0 github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef diff --git a/go.sum b/go.sum index be9de8da88..6b6569e21c 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.20.2-0.20260521220852-8d7eba492bae h1:o3yoQFcDyfXLKFHsoNOzD7BFuojrU3IbB5EjySG53W8= -github.com/consensys/gnark-crypto v0.20.2-0.20260521220852-8d7eba492bae/go.mod h1:NzeBHSZ49bIM7RtrNTYYR2kymTqwvI/A4eTgQlyQc+Q= +github.com/consensys/gnark-crypto v0.20.2-0.20260601192128-f6b0b478eda6 h1:ZGbAbBSo9A4ST6nZoExk9e/yoFPsPKjiFGKkaWuAGWg= +github.com/consensys/gnark-crypto v0.20.2-0.20260601192128-f6b0b478eda6/go.mod h1:NzeBHSZ49bIM7RtrNTYYR2kymTqwvI/A4eTgQlyQc+Q= github.com/consensys/gnark-solidity-checker v0.2.0 h1:i5iUEzNOkUvpaKm23UEe0wajBMwj7NzyT4EI0T2N8WQ= github.com/consensys/gnark-solidity-checker v0.2.0/go.mod h1:cEvl4g5AH+L4qGQLDOVZjqvn5IKZIAZdhSi8zAM6BiY= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= diff --git a/internal/smallfields/tinyfield/element.go b/internal/smallfields/tinyfield/element.go index 5f18dbb8ab..4acde5b70c 100644 --- a/internal/smallfields/tinyfield/element.go +++ b/internal/smallfields/tinyfield/element.go @@ -913,8 +913,8 @@ func init() { } // Cbrt z = ∛x (mod q) -// if the cube root doesn't exist (x is not a cube mod q) -// Cbrt leaves z unchanged and returns nil +// Since q ≡ 2 (mod 3), cubing is a bijection on Fq, so every input has a unique +// cube root and Cbrt always returns z. func (z *Element) Cbrt(x *Element) *Element { // q ≡ 2 (mod 3) // using z = x^((2q-1)/3) (mod q) diff --git a/internal/smallfields/tinyfield/element_test.go b/internal/smallfields/tinyfield/element_test.go index ac777e36cc..e6b2057f18 100644 --- a/internal/smallfields/tinyfield/element_test.go +++ b/internal/smallfields/tinyfield/element_test.go @@ -1458,11 +1458,8 @@ func TestElementCbrt(t *testing.T) { // verify that c^3 == a (since there's no big.Int.ModCbrt) // Cbrt returns nil if the element is not a cubic residue var c Element - result := c.Cbrt(&a.element) - if result == nil { - // a is not a cubic residue, this is valid - return true - } + // q ≡ 2 (mod 3): every element has a unique cube root, Cbrt never returns nil. + c.Cbrt(&a.element) var cube, e big.Int c.BigInt(&e) cube.Exp(&e, big.NewInt(3), Modulus()) @@ -1475,9 +1472,7 @@ func TestElementCbrt(t *testing.T) { // b = a³ is guaranteed to be a cubic residue var b, c Element b.Square(&a.element).Mul(&b, &a.element) - if c.Cbrt(&b) == nil { - return false - } + c.Cbrt(&b) var check Element check.Square(&c).Mul(&check, &c) return check.Equal(&b) @@ -1506,11 +1501,8 @@ func TestElementCbrt(t *testing.T) { var c Element // verify that c^3 == a (since there's no big.Int.ModCbrt) // Cbrt returns nil if the element is not a cubic residue - result := c.Cbrt(&a) - if result == nil { - // a is not a cubic residue, this is valid, continue - continue - } + // q ≡ 2 (mod 3): every element has a unique cube root, Cbrt never returns nil. + c.Cbrt(&a) var cube, e big.Int c.BigInt(&e) cube.Exp(&e, big.NewInt(3), Modulus()) diff --git a/std/algebra/native/maptocurve_octobear/maptocurve.go b/std/algebra/native/maptocurve_octobear/maptocurve.go index 7beac4232b..987d9626ec 100644 --- a/std/algebra/native/maptocurve_octobear/maptocurve.go +++ b/std/algebra/native/maptocurve_octobear/maptocurve.go @@ -37,7 +37,9 @@ func YIncrement(api frontend.API, msg frontend.Variable) (G1Affine, error) { return p, nil } -// assertIsOnCurve asserts y² = x³ - 3x + b for a point from the y-increment map. +// assertIsOnCurve asserts y² = x³ + a·x + b for a point from the y-increment map. +// a = -3 is enforced at package init (see types.go) so the formula uses +// `Sub(rhs, 3·x)` rather than a full E8 mul by curveA, saving constraints. // // Optimizations over a generic on-curve check: // - y is in the base subfield (y = (y0,0,...,0)), so y² = (y0²,0,...,0) costs diff --git a/std/algebra/native/maptocurve_octobear/types.go b/std/algebra/native/maptocurve_octobear/types.go index f801dd9a6b..5b6328666a 100644 --- a/std/algebra/native/maptocurve_octobear/types.go +++ b/std/algebra/native/maptocurve_octobear/types.go @@ -1,6 +1,7 @@ package maptocurve_octobear import ( + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/algebra/native/fields_octobear" @@ -18,6 +19,19 @@ func newE8(v extensions.E8) E8 { return fields_octobear.NewE8(v) } +// The on-curve checks in this package (and in sw_octobear, which imports it) +// hardcode a = -3 as `MulByFp(p.X, 3)` for constraint-count reasons. Guard +// against silent drift if the underlying curve parameter ever changes. +func init() { + a, _ := octobear.CurveCoefficients() + var minus3 extensions.E8 + minus3.C0.B0.A0.SetUint64(3) + minus3.Neg(&minus3) + if !a.Equal(&minus3) { + panic("maptocurve_octobear: octobear curve coefficient a != -3; on-curve formulas need updating") + } +} + func fromCoeffs(v []frontend.Variable) E8 { return E8{ C0: E4{ diff --git a/std/algebra/native/sw_octobear/g1.go b/std/algebra/native/sw_octobear/g1.go index ffbc4c321d..50e883374e 100644 --- a/std/algebra/native/sw_octobear/g1.go +++ b/std/algebra/native/sw_octobear/g1.go @@ -223,6 +223,10 @@ func (c *Curve) isInfinity(p *G1Affine) frontend.Variable { } // AssertIsOnCurve asserts that p is infinity or lies on octobear. +// +// Implements y² = x³ + a·x + b with a = -3 hardcoded. The a = -3 assumption +// is enforced at package init in maptocurve_octobear (imported transitively), +// so any future change to octobear.CurveCoefficients() will panic at load. func (c *Curve) AssertIsOnCurve(p *G1Affine) { isInf := c.isInfinity(p) left := *new(E8).Square(c.api, p.Y) From 0a7b5108aad497b7e4a6efcf150a883f63ae7101 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 1 Jun 2026 16:19:53 -0400 Subject: [PATCH 15/15] fix: mirror gnark-crypto --- .../maptocurve_vector_poseidon2.go | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/std/algebra/native/maptocurve_octobear/maptocurve_vector_poseidon2.go b/std/algebra/native/maptocurve_octobear/maptocurve_vector_poseidon2.go index 0f18078ce3..6370af18fb 100644 --- a/std/algebra/native/maptocurve_octobear/maptocurve_vector_poseidon2.go +++ b/std/algebra/native/maptocurve_octobear/maptocurve_vector_poseidon2.go @@ -53,22 +53,33 @@ func MapPoseidon2(api frontend.API, msgLow, msgHigh frontend.Variable) ([PqN]G1A return pts, errors.New("expected KoalaBear native field for octobear Poseidon2 map-to-curve") } - // Constrain msgLow, msgHigh < 2^32. - _ = api.ToBinary(msgLow, 32) - _ = api.ToBinary(msgHigh, 32) - - // Build sponge state and absorb (domainTag, msgLow, msgHigh) into the - // rate slots. The 8-byte tag is split into two big-endian uint32 halves to - // match the native packing in vector_multiset_hash_poseidon2.go. + // Constrain msgLow, msgHigh < 2^32 and reuse the bit decomposition to + // extract the four 16-bit chunks the native side absorbs. + lowBits := api.ToBinary(msgLow, 32) + highBits := api.ToBinary(msgHigh, 32) + msgLowLo := api.FromBinary(lowBits[0:16]...) + msgLowHi := api.FromBinary(lowBits[16:32]...) + msgHighLo := api.FromBinary(highBits[0:16]...) + msgHighHi := api.FromBinary(highBits[16:32]...) + + // Build sponge state and absorb (domainTag, msg) into the rate slots. + // The 8-byte tag occupies state[0..1] as two big-endian uint32 halves. + // The 64-bit message is split into four 16-bit big-endian chunks across + // state[2..5] — each chunk < 2^16 < p, so the encoding is injective for + // the full uint64 domain. Must match the native packing in + // vector_multiset_hash_poseidon2.go (a 32-bit-half encoding would collide + // because koalabear p = 2^31 - 2^24 + 1 < 2^32). tag := multisethash.PqDomainTag() tag0 := binary.BigEndian.Uint32(tag[0:4]) tag1 := binary.BigEndian.Uint32(tag[4:8]) state := make([]frontend.Variable, PqWidth) state[0] = tag0 state[1] = tag1 - state[2] = msgLow - state[3] = msgHigh - for i := 4; i < PqWidth; i++ { + state[2] = msgHighHi + state[3] = msgHighLo + state[4] = msgLowHi + state[5] = msgLowLo + for i := 6; i < PqWidth; i++ { state[i] = frontend.Variable(0) }