From 9644c2e4bd1067b15c2e6c20dfcd872e336f6a73 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 14 Apr 2026 15:58:34 -0400 Subject: [PATCH 01/33] feat: add kb8 curve --- ecc/bls12-377/g1.go | 2 +- ecc/bls12-377/g2.go | 2 +- ecc/bls12-377/marshal.go | 53 - ecc/bls12-377/marshal_test.go | 12 +- ecc/bls12-381/g1.go | 2 +- ecc/bls12-381/g2.go | 2 +- ecc/bls12-381/marshal.go | 53 - ecc/bls12-381/marshal_test.go | 12 +- ecc/bls24-315/g1.go | 2 +- ecc/bls24-315/g2.go | 2 +- ecc/bls24-315/marshal.go | 53 - ecc/bls24-315/marshal_test.go | 2 - ecc/bls24-317/g1.go | 2 +- ecc/bls24-317/g2.go | 2 +- ecc/bls24-317/marshal.go | 53 - ecc/bls24-317/marshal_test.go | 2 - ecc/bn254/g1.go | 2 +- ecc/bn254/g2.go | 2 +- ecc/bn254/marshal.go | 53 - ecc/bn254/marshal_test.go | 12 +- ecc/bw6-633/g1.go | 2 +- ecc/bw6-633/g2.go | 2 +- ecc/bw6-633/marshal.go | 53 - ecc/bw6-633/marshal_test.go | 2 - ecc/bw6-761/g1.go | 2 +- ecc/bw6-761/g2.go | 2 +- ecc/bw6-761/marshal.go | 53 - ecc/bw6-761/marshal_test.go | 2 - ecc/ecc.go | 3 +- ecc/ecc_field.go | 7 + ecc/grumpkin/g1.go | 2 +- ecc/grumpkin/marshal.go | 33 +- ecc/grumpkin/marshal_test.go | 59 +- ecc/kb8/fp/fp.go | 42 + ecc/kb8/fr/arith.go | 49 + ecc/kb8/fr/doc.go | 46 + ecc/kb8/fr/element.go | 1849 ++++++++++ ecc/kb8/fr/element_amd64.go | 62 + ecc/kb8/fr/element_amd64.s | 10 + ecc/kb8/fr/element_arm64.go | 70 + ecc/kb8/fr/element_arm64.s | 10 + ecc/kb8/fr/element_exp.go | 807 +++++ ecc/kb8/fr/element_purego.go | 391 +++ ecc/kb8/fr/element_test.go | 2982 +++++++++++++++++ ecc/kb8/fr/vector.go | 394 +++ ecc/kb8/fr/vector_amd64.go | 164 + ecc/kb8/fr/vector_purego.go | 45 + ecc/kb8/fr/vector_test.go | 926 +++++ ecc/kb8/g1.go | 1347 ++++++++ ecc/kb8/g1_test.go | 828 +++++ ecc/kb8/internal/fptower/fptower.go | 19 + ecc/kb8/kb8.go | 82 + ecc/kb8/marshal.go | 869 +++++ ecc/kb8/marshal_test.go | 372 ++ ecc/kb8/multiexp.go | 532 +++ ecc/kb8/multiexp_affine.go | 369 ++ ecc/kb8/multiexp_jacobian.go | 97 + ecc/kb8/multiexp_test.go | 442 +++ ecc/secp256k1/g1.go | 2 +- ecc/secp256k1/marshal.go | 870 ++++- ecc/secp256k1/marshal_test.go | 315 +- field/koalabear/extensions/e8.go | 311 ++ field/koalabear/extensions/e8_test.go | 175 + ...db6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 | Bin 0 -> 2240 bytes ...352d70ba47f60b8efb355dedc0d683f9b76c0dc824 | Bin 0 -> 2266 bytes ...26a5ae1748fec171df66abbdb81ad07f36ed81b904 | Bin 0 -> 2237 bytes internal/generator/config/curve.go | 4 + internal/generator/config/kb8.go | 22 + internal/generator/ecc/generate.go | 10 +- .../generator/ecc/template/marshal.go.tmpl | 367 +- .../generator/ecc/template/multiexp.go.tmpl | 2 + .../ecc/template/multiexp_affine.go.tmpl | 4 +- .../ecc/template/multiexp_jacobian.go.tmpl | 2 +- internal/generator/ecc/template/point.go.tmpl | 88 +- .../ecc/template/tests/marshal.go.tmpl | 84 +- .../ecc/template/tests/multiexp.go.tmpl | 2 + .../ecc/template/tests/point.go.tmpl | 10 +- .../generator/field/generator_extensions.go | 11 + .../field/template/extensions/e8.go.tmpl | 304 ++ .../field/template/extensions/e8_test.go.tmpl | 168 + internal/generator/main.go | 12 +- 81 files changed, 15636 insertions(+), 478 deletions(-) create mode 100644 ecc/kb8/fp/fp.go create mode 100644 ecc/kb8/fr/arith.go create mode 100644 ecc/kb8/fr/doc.go create mode 100644 ecc/kb8/fr/element.go create mode 100644 ecc/kb8/fr/element_amd64.go create mode 100644 ecc/kb8/fr/element_amd64.s create mode 100644 ecc/kb8/fr/element_arm64.go create mode 100644 ecc/kb8/fr/element_arm64.s create mode 100644 ecc/kb8/fr/element_exp.go create mode 100644 ecc/kb8/fr/element_purego.go create mode 100644 ecc/kb8/fr/element_test.go create mode 100644 ecc/kb8/fr/vector.go create mode 100644 ecc/kb8/fr/vector_amd64.go create mode 100644 ecc/kb8/fr/vector_purego.go create mode 100644 ecc/kb8/fr/vector_test.go create mode 100644 ecc/kb8/g1.go create mode 100644 ecc/kb8/g1_test.go create mode 100644 ecc/kb8/internal/fptower/fptower.go create mode 100644 ecc/kb8/kb8.go create mode 100644 ecc/kb8/marshal.go create mode 100644 ecc/kb8/marshal_test.go create mode 100644 ecc/kb8/multiexp.go create mode 100644 ecc/kb8/multiexp_affine.go create mode 100644 ecc/kb8/multiexp_jacobian.go create mode 100644 ecc/kb8/multiexp_test.go create mode 100644 field/koalabear/extensions/e8.go create mode 100644 field/koalabear/extensions/e8_test.go create mode 100644 internal/generator/addchain/2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 create mode 100644 internal/generator/addchain/78372234161552207af9352d70ba47f60b8efb355dedc0d683f9b76c0dc824 create mode 100644 internal/generator/addchain/f06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b904 create mode 100644 internal/generator/config/kb8.go create mode 100644 internal/generator/field/template/extensions/e8.go.tmpl create mode 100644 internal/generator/field/template/extensions/e8_test.go.tmpl diff --git a/ecc/bls12-377/g1.go b/ecc/bls12-377/g1.go index 5a2d550bec..de321721a4 100644 --- a/ecc/bls12-377/g1.go +++ b/ecc/bls12-377/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls12-377/g2.go b/ecc/bls12-377/g2.go index 16552bb4eb..264ea5c139 100644 --- a/ecc/bls12-377/g2.go +++ b/ecc/bls12-377/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fptower.E2 + var left, right, tmp fptower.E2 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls12-377/marshal.go b/ecc/bls12-377/marshal.go index 17c2911c6e..c089220ed8 100644 --- a/ecc/bls12-377/marshal.go +++ b/ecc/bls12-377/marshal.go @@ -89,7 +89,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG2AffineUncompressed]byte var read int var sliceLen uint32 @@ -514,11 +513,6 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -577,27 +571,6 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range len(t) { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encode(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineCompressed]byte - for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -655,11 +628,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -718,27 +686,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range len(t) { - buf = t[i].RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encodeRaw(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineUncompressed]byte - for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) diff --git a/ecc/bls12-377/marshal_test.go b/ecc/bls12-377/marshal_test.go index 389841372a..4fc2420da9 100644 --- a/ecc/bls12-377/marshal_test.go +++ b/ecc/bls12-377/marshal_test.go @@ -105,7 +105,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -201,7 +200,6 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - { b := g2Inf.Bytes() if !isCompressed(b[0]) { @@ -490,6 +488,16 @@ func GenE2() gopter.Gen { }) } +// GenE4 generates an fptower.E4 elmt +func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) +} + // GenE6 generates an fptower.E6 elmt func GenE6() gopter.Gen { return gopter.CombineGens( diff --git a/ecc/bls12-381/g1.go b/ecc/bls12-381/g1.go index 4c5bb4dd60..e17fec1c09 100644 --- a/ecc/bls12-381/g1.go +++ b/ecc/bls12-381/g1.go @@ -198,7 +198,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls12-381/g2.go b/ecc/bls12-381/g2.go index f3b048ffaa..a3e036668a 100644 --- a/ecc/bls12-381/g2.go +++ b/ecc/bls12-381/g2.go @@ -204,7 +204,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fptower.E2 + var left, right, tmp fptower.E2 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls12-381/marshal.go b/ecc/bls12-381/marshal.go index f46e903fc8..fc71a27cba 100644 --- a/ecc/bls12-381/marshal.go +++ b/ecc/bls12-381/marshal.go @@ -89,7 +89,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG2AffineUncompressed]byte var read int var sliceLen uint32 @@ -514,11 +513,6 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -577,27 +571,6 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range len(t) { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encode(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineCompressed]byte - for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -655,11 +628,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -718,27 +686,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range len(t) { - buf = t[i].RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encodeRaw(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineUncompressed]byte - for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) diff --git a/ecc/bls12-381/marshal_test.go b/ecc/bls12-381/marshal_test.go index 596145436e..649d3059c4 100644 --- a/ecc/bls12-381/marshal_test.go +++ b/ecc/bls12-381/marshal_test.go @@ -105,7 +105,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -201,7 +200,6 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - { b := g2Inf.Bytes() if !isCompressed(b[0]) { @@ -490,6 +488,16 @@ func GenE2() gopter.Gen { }) } +// GenE4 generates an fptower.E4 elmt +func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) +} + // GenE6 generates an fptower.E6 elmt func GenE6() gopter.Gen { return gopter.CombineGens( diff --git a/ecc/bls24-315/g1.go b/ecc/bls24-315/g1.go index 5872bf92b5..840d51c5f8 100644 --- a/ecc/bls24-315/g1.go +++ b/ecc/bls24-315/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls24-315/g2.go b/ecc/bls24-315/g2.go index 06e71b8b09..304e1f8edc 100644 --- a/ecc/bls24-315/g2.go +++ b/ecc/bls24-315/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fptower.E4 + var left, right, tmp fptower.E4 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls24-315/marshal.go b/ecc/bls24-315/marshal.go index e71cf75362..caae4d7bc6 100644 --- a/ecc/bls24-315/marshal.go +++ b/ecc/bls24-315/marshal.go @@ -89,7 +89,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG2AffineUncompressed]byte var read int var sliceLen uint32 @@ -514,11 +513,6 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -577,27 +571,6 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range len(t) { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encode(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineCompressed]byte - for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -655,11 +628,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -718,27 +686,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range len(t) { - buf = t[i].RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encodeRaw(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineUncompressed]byte - for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) diff --git a/ecc/bls24-315/marshal_test.go b/ecc/bls24-315/marshal_test.go index a6e8abb3b6..c32faacf08 100644 --- a/ecc/bls24-315/marshal_test.go +++ b/ecc/bls24-315/marshal_test.go @@ -105,7 +105,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -201,7 +200,6 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - { b := g2Inf.Bytes() if !isCompressed(b[0]) { diff --git a/ecc/bls24-317/g1.go b/ecc/bls24-317/g1.go index e7af58ba0d..e7991ade97 100644 --- a/ecc/bls24-317/g1.go +++ b/ecc/bls24-317/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls24-317/g2.go b/ecc/bls24-317/g2.go index a2565b729f..c349583df8 100644 --- a/ecc/bls24-317/g2.go +++ b/ecc/bls24-317/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fptower.E4 + var left, right, tmp fptower.E4 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls24-317/marshal.go b/ecc/bls24-317/marshal.go index 9e6a191e67..95fb4c7841 100644 --- a/ecc/bls24-317/marshal.go +++ b/ecc/bls24-317/marshal.go @@ -89,7 +89,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG2AffineUncompressed]byte var read int var sliceLen uint32 @@ -514,11 +513,6 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -577,27 +571,6 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range len(t) { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encode(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineCompressed]byte - for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -655,11 +628,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -718,27 +686,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range len(t) { - buf = t[i].RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encodeRaw(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineUncompressed]byte - for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) diff --git a/ecc/bls24-317/marshal_test.go b/ecc/bls24-317/marshal_test.go index 99c84963a9..218e6bda01 100644 --- a/ecc/bls24-317/marshal_test.go +++ b/ecc/bls24-317/marshal_test.go @@ -105,7 +105,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -201,7 +200,6 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - { b := g2Inf.Bytes() if !isCompressed(b[0]) { diff --git a/ecc/bn254/g1.go b/ecc/bn254/g1.go index b8475c4dac..5d3036a900 100644 --- a/ecc/bn254/g1.go +++ b/ecc/bn254/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bn254/g2.go b/ecc/bn254/g2.go index badc36bf4f..65170bf8d4 100644 --- a/ecc/bn254/g2.go +++ b/ecc/bn254/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fptower.E2 + var left, right, tmp fptower.E2 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bn254/marshal.go b/ecc/bn254/marshal.go index 0e75008bf5..864f834a5c 100644 --- a/ecc/bn254/marshal.go +++ b/ecc/bn254/marshal.go @@ -84,7 +84,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG2AffineUncompressed]byte var read int var sliceLen uint32 @@ -479,11 +478,6 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -542,27 +536,6 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range len(t) { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encode(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineCompressed]byte - for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -620,11 +593,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -683,27 +651,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range len(t) { - buf = t[i].RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encodeRaw(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineUncompressed]byte - for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) diff --git a/ecc/bn254/marshal_test.go b/ecc/bn254/marshal_test.go index faa3998c5b..17f011f793 100644 --- a/ecc/bn254/marshal_test.go +++ b/ecc/bn254/marshal_test.go @@ -105,7 +105,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -201,7 +200,6 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - { b := g2Inf.Bytes() if !isCompressed(b[0]) { @@ -450,6 +448,16 @@ func GenE2() gopter.Gen { }) } +// GenE4 generates an fptower.E4 elmt +func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) +} + // GenE6 generates an fptower.E6 elmt func GenE6() gopter.Gen { return gopter.CombineGens( diff --git a/ecc/bw6-633/g1.go b/ecc/bw6-633/g1.go index 97fb84c9f9..57651102d7 100644 --- a/ecc/bw6-633/g1.go +++ b/ecc/bw6-633/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bw6-633/g2.go b/ecc/bw6-633/g2.go index d3504e15e3..5319f4c82f 100644 --- a/ecc/bw6-633/g2.go +++ b/ecc/bw6-633/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bw6-633/marshal.go b/ecc/bw6-633/marshal.go index 0b64e7dfbb..eb4f6ae4f8 100644 --- a/ecc/bw6-633/marshal.go +++ b/ecc/bw6-633/marshal.go @@ -89,7 +89,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG2AffineUncompressed]byte var read int var sliceLen uint32 @@ -514,11 +513,6 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -577,27 +571,6 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range len(t) { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encode(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineCompressed]byte - for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -655,11 +628,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -718,27 +686,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range len(t) { - buf = t[i].RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encodeRaw(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineUncompressed]byte - for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) diff --git a/ecc/bw6-633/marshal_test.go b/ecc/bw6-633/marshal_test.go index b84477dda9..2d40e85d9e 100644 --- a/ecc/bw6-633/marshal_test.go +++ b/ecc/bw6-633/marshal_test.go @@ -105,7 +105,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -201,7 +200,6 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - { b := g2Inf.Bytes() if !isCompressed(b[0]) { diff --git a/ecc/bw6-761/g1.go b/ecc/bw6-761/g1.go index 3b46dbf5da..42ec140dd2 100644 --- a/ecc/bw6-761/g1.go +++ b/ecc/bw6-761/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bw6-761/g2.go b/ecc/bw6-761/g2.go index 48d19303bf..8f6357fbb2 100644 --- a/ecc/bw6-761/g2.go +++ b/ecc/bw6-761/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bw6-761/marshal.go b/ecc/bw6-761/marshal.go index ef94f98879..5c8e55e503 100644 --- a/ecc/bw6-761/marshal.go +++ b/ecc/bw6-761/marshal.go @@ -89,7 +89,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG2AffineUncompressed]byte var read int var sliceLen uint32 @@ -514,11 +513,6 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -577,27 +571,6 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range len(t) { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encode(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineCompressed]byte - for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -655,11 +628,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - case *G2Affine: - buf := t.RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -718,27 +686,6 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range len(t) { - buf = t[i].RawBytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } - } - return nil - case *[]G2Affine: - return enc.encodeRaw(*t) - case []G2Affine: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { - return - } - enc.n += 4 - - var buf [SizeOfG2AffineUncompressed]byte - for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) diff --git a/ecc/bw6-761/marshal_test.go b/ecc/bw6-761/marshal_test.go index 4ffc2832b0..35e39dd869 100644 --- a/ecc/bw6-761/marshal_test.go +++ b/ecc/bw6-761/marshal_test.go @@ -105,7 +105,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -201,7 +200,6 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - { b := g2Inf.Bytes() if !isCompressed(b[0]) { diff --git a/ecc/ecc.go b/ecc/ecc.go index 3fe3bd314c..5b59f46a21 100644 --- a/ecc/ecc.go +++ b/ecc/ecc.go @@ -1,7 +1,7 @@ // Copyright 2020-2025 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Package ecc provides bls12-381, bls12-377, bn254, bw6-761, bls24-315, bls24-317, bw6-633, secp256k1, secp256r1, grumpkin and stark-curve elliptic curves implementations. +// Package ecc provides bls12-381, bls12-377, bn254, bw6-761, bls24-315, bls24-317, bw6-633, secp256k1, secp256r1, grumpkin, stark-curve and kb8 elliptic curves implementations. // // Also // @@ -32,6 +32,7 @@ const ( SECP256K1 GRUMPKIN SECP256R1 + KB8 ) // MultiExpConfig enables to set optional configuration attribute to a call to MultiExp diff --git a/ecc/ecc_field.go b/ecc/ecc_field.go index bf774fd4c1..a3bf8e85e0 100644 --- a/ecc/ecc_field.go +++ b/ecc/ecc_field.go @@ -20,6 +20,7 @@ var mID = map[string]ID{ "bw6_633": BW6_633, "bw6_761": BW6_761, "grumpkin": GRUMPKIN, + "kb8": KB8, "secp256k1": SECP256K1, "secp256r1": SECP256R1, "stark_curve": STARK_CURVE, @@ -45,6 +46,8 @@ func (id ID) ScalarField() *big.Int { f.SetString("258664426012969094010652733694893533536393512754914660539884262666720468348340822774968888139573360124440321458177", 10) case GRUMPKIN: f.SetString("21888242871839275222246405745257275088696311157297823662689037894645226208583", 10) + case KB8: + f.SetString("424804331891979973455971894938199991839487883914575852667663156896715214921", 10) case SECP256K1: f.SetString("115792089237316195423570985008687907852837564279074904382605163141518161494337", 10) case SECP256R1: @@ -77,6 +80,8 @@ func (id ID) BaseField() *big.Int { f.SetString("6891450384315732539396789682275657542479668912536150109513790160209623422243491736087683183289411687640864567753786613451161759120554247759349511699125301598951605099378508850372543631423596795951899700429969112842764913119068299", 10) case GRUMPKIN: f.SetString("21888242871839275222246405745257275088548364400416034343698204186575808495617", 10) + case KB8: + f.SetString("2130706433", 10) case SECP256K1: f.SetString("115792089237316195423570985008687907853269984665640564039457584007908834671663", 10) case SECP256R1: @@ -108,6 +113,8 @@ func (id ID) String() string { return "bw6_761" case GRUMPKIN: return "grumpkin" + case KB8: + return "kb8" case SECP256K1: return "secp256k1" case SECP256R1: diff --git a/ecc/grumpkin/g1.go b/ecc/grumpkin/g1.go index f81420f0e6..688302f4ec 100644 --- a/ecc/grumpkin/g1.go +++ b/ecc/grumpkin/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/grumpkin/marshal.go b/ecc/grumpkin/marshal.go index 8f17c4a22a..54767a4ce5 100644 --- a/ecc/grumpkin/marshal.go +++ b/ecc/grumpkin/marshal.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT @@ -17,7 +17,7 @@ import ( "github.com/consensys/gnark-crypto/parallel" ) -// To encode G1Affine points, we mask the most significant bits with these bits to specify without ambiguity +// To encode G1Affine and G2Affine points, we mask the most significant bits with these bits to specify without ambiguity // metadata needed for point (de)compression // we have less than 3 bits available on the msw, so we can't follow BLS12-381 style encoding. // the difference is the case where a point is infinity and uncompressed is not flagged @@ -34,21 +34,21 @@ var ( ErrInvalidEncoding = errors.New("invalid point encoding") ) -// Encoder writes bn254 object values to an output stream +// Encoder writes grumpkin object values to an output stream type Encoder struct { w io.Writer n int64 // written bytes raw bool // raw vs compressed encoding } -// Decoder reads bn254 object values from an inbound stream +// Decoder reads grumpkin object values from an inbound stream type Decoder struct { r io.Reader n int64 // read bytes subGroupCheck bool // default to true } -// NewDecoder returns a binary decoder supporting curve bn254 objects in both +// NewDecoder returns a binary decoder supporting curve grumpkin objects in both // compressed and uncompressed (raw) forms func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { d := &Decoder{r: r, subGroupCheck: true} @@ -61,11 +61,11 @@ func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { } // Decode reads the binary encoding of v from the stream -// type must be *uint64, *fr.Element, *fp.Element, *G1Affine or *[]G1Affine +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, *[]G1Affine or *[]G2Affine func (dec *Decoder) Decode(v any) (err error) { rv := reflect.ValueOf(v) - if v == nil || rv.Kind() != reflect.Pointer || rv.IsNil() || !rv.Elem().CanSet() { - return errors.New("bn254 decoder: unsupported type, need pointer") + if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { + return errors.New("grumpkin decoder: unsupported type, need pointer") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -80,7 +80,6 @@ func (dec *Decoder) Decode(v any) (err error) { dec.n += read64 return } - var buf [SizeOfG1AffineUncompressed]byte var read int var sliceLen uint32 @@ -201,7 +200,7 @@ func (dec *Decoder) Decode(v any) (err error) { *t = make([]G1Affine, sliceLen) } compressed := make([]bool, sliceLen) - for i := 0; i < len(*t); i++ { + for i := range len(*t) { // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) @@ -254,7 +253,7 @@ func (dec *Decoder) Decode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New("bn254 encoder: unsupported type") + return errors.New("grumpkin encoder: unsupported type") } err = binary.Read(dec.r, binary.BigEndian, t) if err == nil { @@ -298,7 +297,7 @@ func isCompressed(msb byte) bool { return mData != mUncompressed } -// NewEncoder returns a binary encoder supporting curve bn254 objects +// NewEncoder returns a binary encoder supporting curve grumpkin objects func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { // default settings enc := &Encoder{ @@ -316,7 +315,7 @@ func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { } // Encode writes the binary encoding of v to the stream -// type must be uint64, *fr.Element, *fp.Element, *G1Affine, []G1Affine or *[]G1Affine +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, []G1Affine, []G2Affine, *[]G1Affine or *[]G2Affine func (enc *Encoder) Encode(v any) (err error) { if enc.raw { return enc.encodeRaw(v) @@ -360,7 +359,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) - if v == nil || (rv.Kind() == reflect.Pointer && rv.IsNil()) { + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { return errors.New(" encoder: can't encode ") } @@ -453,7 +452,7 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range t { + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) enc.n += int64(written) @@ -475,7 +474,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) - if v == nil || (rv.Kind() == reflect.Pointer && rv.IsNil()) { + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { return errors.New(" encoder: can't encode ") } @@ -568,7 +567,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range t { + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) enc.n += int64(written) diff --git a/ecc/grumpkin/marshal_test.go b/ecc/grumpkin/marshal_test.go index 9c783967e6..4af4d5559a 100644 --- a/ecc/grumpkin/marshal_test.go +++ b/ecc/grumpkin/marshal_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. // Code generated by consensys/gnark-crypto DO NOT EDIT @@ -61,9 +61,10 @@ func TestEncoder(t *testing.T) { inN = make([][][]fr.Element, 4) for i := range 4 { inN[i] = make([][]fr.Element, i+2) - for j := 0; j < i+2; j++ { - inN[i][j] = make([]fr.Element, j+3) - fr.Vector(inN[i][j]).MustSetRandom() + for j := range i + 2 { + inNIJ := make(fr.Vector, j+3) + inNIJ.MustSetRandom() + inN[i][j] = inNIJ } } @@ -97,7 +98,6 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -116,7 +116,10 @@ func TestEncoder(t *testing.T) { if !inD.Equal(&outD) || !inE.Equal(&outE) { t.Fatal("decode(encode(G1Affine) failed") } - for i := 0; i < len(inG); i++ { + if len(inG) != len(outG) { + t.Fatal("decode(encode(slice(points))) failed") + } + for i := range len(inG) { if !inG[i].Equal(&outG[i]) { t.Fatal("decode(encode(slice(points))) failed") } @@ -124,7 +127,7 @@ func TestEncoder(t *testing.T) { if (len(inI) != len(outI)) || (len(inJ) != len(outJ)) { t.Fatal("decode(encode(slice(elements))) failed") } - for i := 0; i < len(inI); i++ { + for i := range len(inI) { if !inI[i].Equal(&outI[i]) { t.Fatal("decode(encode(slice(elements))) failed") } @@ -155,7 +158,6 @@ func TestEncoder(t *testing.T) { func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine - g1 = g1GenAff { @@ -303,6 +305,47 @@ func GenFp() gopter.Gen { } } +// GenE2 generates an fptower.E2 elmt +func GenE2() gopter.Gen { + return gopter.CombineGens( + GenFp(), + GenFp(), + ).Map(func(values []any) fptower.E2 { + return fptower.E2{A0: values[0].(fp.Element), A1: values[1].(fp.Element)} + }) +} + +// GenE4 generates an fptower.E4 elmt +func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) +} + +// GenE6 generates an fptower.E6 elmt +func GenE6() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E6 { + return fptower.E6{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2), B2: values[2].(fptower.E2)} + }) +} + +// GenE12 generates an fptower.E6 elmt +func GenE12() gopter.Gen { + return gopter.CombineGens( + GenE6(), + GenE6(), + ).Map(func(values []any) fptower.E12 { + return fptower.E12{C0: values[0].(fptower.E6), C1: values[1].(fptower.E6)} + }) +} + // GenBigInt generates a big.Int func GenBigInt() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { diff --git a/ecc/kb8/fp/fp.go b/ecc/kb8/fp/fp.go new file mode 100644 index 0000000000..d07d58d74d --- /dev/null +++ b/ecc/kb8/fp/fp.go @@ -0,0 +1,42 @@ +package fp + +import ( + "math/big" + + kb "github.com/consensys/gnark-crypto/field/koalabear" +) + +type Element = kb.Element +type Vector = kb.Vector + +const ( + Bits = kb.Bits + Bytes = kb.Bytes + Limbs = kb.Limbs +) + +var BigEndian = kb.BigEndian + +func Modulus() *big.Int { + return kb.Modulus() +} + +func One() Element { + return kb.One() +} + +func BatchInvert(a []Element) []Element { + return kb.BatchInvert(a) +} + +func Generator(m uint64) (Element, error) { + return kb.Generator(m) +} + +func Butterfly(a, b *Element) { + kb.Butterfly(a, b) +} + +func MulBy3(x *Element) { + kb.MulBy3(x) +} diff --git a/ecc/kb8/fr/arith.go b/ecc/kb8/fr/arith.go new file mode 100644 index 0000000000..9aa0f3c4db --- /dev/null +++ b/ecc/kb8/fr/arith.go @@ -0,0 +1,49 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + "math/bits" +) + +// madd0 hi = a*b + c (discards lo bits) +func madd0(a, b, c uint64) (hi uint64) { + var carry, lo uint64 + hi, lo = bits.Mul64(a, b) + _, carry = bits.Add64(lo, c, 0) + hi, _ = bits.Add64(hi, 0, carry) + return +} + +// madd1 hi, lo = a*b + c +func madd1(a, b, c uint64) (hi uint64, lo uint64) { + var carry uint64 + hi, lo = bits.Mul64(a, b) + lo, carry = bits.Add64(lo, c, 0) + hi, _ = bits.Add64(hi, 0, carry) + return +} + +// madd2 hi, lo = a*b + c + d +func madd2(a, b, c, d uint64) (hi uint64, lo uint64) { + var carry uint64 + hi, lo = bits.Mul64(a, b) + c, carry = bits.Add64(c, d, 0) + hi, _ = bits.Add64(hi, 0, carry) + lo, carry = bits.Add64(lo, c, 0) + hi, _ = bits.Add64(hi, 0, carry) + return +} + +func madd3(a, b, c, d, e uint64) (hi uint64, lo uint64) { + var carry uint64 + hi, lo = bits.Mul64(a, b) + c, carry = bits.Add64(c, d, 0) + hi, _ = bits.Add64(hi, 0, carry) + lo, carry = bits.Add64(lo, c, 0) + hi, _ = bits.Add64(hi, e, carry) + return +} diff --git a/ecc/kb8/fr/doc.go b/ecc/kb8/fr/doc.go new file mode 100644 index 0000000000..0632b23e19 --- /dev/null +++ b/ecc/kb8/fr/doc.go @@ -0,0 +1,46 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +// Package fr contains field arithmetic operations for modulus = 0xf06e44...1b9049. +// +// The API is similar to math/big (big.Int), but the operations are significantly faster (up to 20x). +// +// Additionally fr.Vector offers an API to manipulate []Element using AVX512 instructions if available. +// +// The modulus is hardcoded in all the operations. +// +// Field elements are represented as an array, and assumed to be in Montgomery form in all methods: +// +// type Element [4]uint64 +// +// # Usage +// +// Example API signature: +// +// // Mul z = x * y (mod q) +// func (z *Element) Mul(x, y *Element) *Element +// +// and can be used like so: +// +// var a, b Element +// a.SetUint64(2) +// b.SetString("984896738") +// a.Mul(a, b) +// a.Sub(a, a) +// .Add(a, b) +// .Inv(a) +// b.Exp(b, new(big.Int).SetUint64(42)) +// +// Modulus q = +// +// q[base10] = 424804331891979973455971894938199991839487883914575852667663156896715214921 +// q[base16] = 0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b9049 +// +// # Warning +// +// There is no security guarantees such as constant time implementation or side-channel attack resistance. +// This code is provided as-is. Partially audited, see https://github.com/Consensys/gnark/tree/master/audits +// for more details. +package fr diff --git a/ecc/kb8/fr/element.go b/ecc/kb8/fr/element.go new file mode 100644 index 0000000000..4202285917 --- /dev/null +++ b/ecc/kb8/fr/element.go @@ -0,0 +1,1849 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + "crypto/rand" + "encoding/binary" + "errors" + "io" + "math/big" + "math/bits" + "reflect" + "strconv" + "strings" + + "github.com/bits-and-blooms/bitset" + "github.com/consensys/gnark-crypto/field/hash" + "github.com/consensys/gnark-crypto/field/pool" +) + +// Element represents a field element stored on 4 words (uint64) +// +// Element are assumed to be in Montgomery form in all methods. +// +// Modulus q = +// +// q[base10] = 424804331891979973455971894938199991839487883914575852667663156896715214921 +// q[base16] = 0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b9049 +// +// # Warning +// +// This code has not been audited and is provided as-is. In particular, there is no security guarantees such as constant time implementation or side-channel attack resistance. +type Element [4]uint64 + +const ( + Limbs = 4 // number of 64 bits words needed to represent a Element + Bits = 248 // number of bits needed to represent a Element + Bytes = 32 // number of bytes needed to represent a Element +) + +// Field modulus q +const ( + q0 = 12468201750796144713 + q1 = 17012099061605194625 + q2 = 4680913926326678671 + q3 = 67675234495113892 +) + +var qElement = Element{ + q0, + q1, + q2, + q3, +} + +var _modulus big.Int // q stored as big.Int + +// Modulus returns q as a big.Int +// +// q[base10] = 424804331891979973455971894938199991839487883914575852667663156896715214921 +// q[base16] = 0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b9049 +func Modulus() *big.Int { + return new(big.Int).Set(&_modulus) +} + +// q + r'.r = 1, i.e., qInvNeg = - q⁻¹ mod r +// used for Montgomery reduction +const qInvNeg = 15380680873399721479 + +func init() { + _modulus.SetString("f06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b9049", 16) +} + +// NewElement returns a new Element from a uint64 value +// +// it is equivalent to +// +// var v Element +// v.SetUint64(...) +func NewElement(v uint64) Element { + z := Element{v} + z.Mul(&z, &rSquare) + return z +} + +// SetUint64 sets z to v and returns z +func (z *Element) SetUint64(v uint64) *Element { + // sets z LSB to v (non-Montgomery form) and convert z to Montgomery form + *z = Element{v} + return z.Mul(z, &rSquare) // z.toMont() +} + +// SetInt64 sets z to v and returns z +func (z *Element) SetInt64(v int64) *Element { + + // absolute value of v + m := v >> 63 + z.SetUint64(uint64((v ^ m) - m)) + + if m != 0 { + // v is negative + z.Neg(z) + } + + return z +} + +// Set z = x and returns z +func (z *Element) Set(x *Element) *Element { + z[0] = x[0] + z[1] = x[1] + z[2] = x[2] + z[3] = x[3] + return z +} + +// SetInterface converts provided interface into Element +// returns an error if provided type is not supported. +// supported types: +// +// Element +// *Element +// uint64 +// int +// string (see SetString for valid formats) +// *big.Int +// big.Int +// []byte +func (z *Element) SetInterface(i1 any) (*Element, error) { + if i1 == nil { + return nil, errors.New("can't set fr.Element with ") + } + + switch c1 := i1.(type) { + case Element: + return z.Set(&c1), nil + case *Element: + if c1 == nil { + return nil, errors.New("can't set fr.Element with ") + } + return z.Set(c1), nil + case uint8: + return z.SetUint64(uint64(c1)), nil + case uint16: + return z.SetUint64(uint64(c1)), nil + case uint32: + return z.SetUint64(uint64(c1)), nil + case uint: + return z.SetUint64(uint64(c1)), nil + case uint64: + return z.SetUint64(c1), nil + case int8: + return z.SetInt64(int64(c1)), nil + case int16: + return z.SetInt64(int64(c1)), nil + case int32: + return z.SetInt64(int64(c1)), nil + case int64: + return z.SetInt64(c1), nil + case int: + return z.SetInt64(int64(c1)), nil + case string: + return z.SetString(c1) + case *big.Int: + if c1 == nil { + return nil, errors.New("can't set fr.Element with ") + } + return z.SetBigInt(c1), nil + case big.Int: + return z.SetBigInt(&c1), nil + case []byte: + return z.SetBytes(c1), nil + default: + return nil, errors.New("can't set fr.Element from type " + reflect.TypeOf(i1).String()) + } +} + +// SetZero z = 0 +func (z *Element) SetZero() *Element { + z[0] = 0 + z[1] = 0 + z[2] = 0 + z[3] = 0 + return z +} + +// SetOne z = 1 (in Montgomery form) +func (z *Element) SetOne() *Element { + z[0] = 2850033346006135408 + z[1] = 2841817744484517432 + z[2] = 18063497198812014357 + z[3] = 39080291038572922 + return z +} + +// Div z = x*y⁻¹ (mod q) +func (z *Element) Div(x, y *Element) *Element { + var yInv Element + yInv.Inverse(y) + z.Mul(x, &yInv) + return z +} + +// Equal returns z == x; constant-time +func (z *Element) Equal(x *Element) bool { + return z.NotEqual(x) == 0 +} + +// NotEqual returns 0 if and only if z == x; constant-time +func (z *Element) NotEqual(x *Element) uint64 { + return (z[3] ^ x[3]) | (z[2] ^ x[2]) | (z[1] ^ x[1]) | (z[0] ^ x[0]) +} + +// IsZero returns z == 0 +func (z *Element) IsZero() bool { + return (z[3] | z[2] | z[1] | z[0]) == 0 +} + +// IsOne returns z == 1 +func (z *Element) IsOne() bool { + return ((z[3] ^ 39080291038572922) | (z[2] ^ 18063497198812014357) | (z[1] ^ 2841817744484517432) | (z[0] ^ 2850033346006135408)) == 0 +} + +// IsUint64 reports whether z can be represented as an uint64. +func (z *Element) IsUint64() bool { + zz := *z + zz.fromMont() + return zz.FitsOnOneWord() +} + +// Uint64 returns the uint64 representation of x. If x cannot be represented in a uint64, the result is undefined. +func (z *Element) Uint64() uint64 { + return z.Bits()[0] +} + +// FitsOnOneWord reports whether z words (except the least significant word) are 0 +// +// It is the responsibility of the caller to convert from Montgomery to Regular form if needed. +func (z *Element) FitsOnOneWord() bool { + return (z[3] | z[2] | z[1]) == 0 +} + +// Cmp compares (lexicographic order) z and x and returns: +// +// -1 if z < x +// 0 if z == x +// +1 if z > x +func (z *Element) Cmp(x *Element) int { + _z := z.Bits() + _x := x.Bits() + if _z[3] > _x[3] { + return 1 + } else if _z[3] < _x[3] { + return -1 + } + if _z[2] > _x[2] { + return 1 + } else if _z[2] < _x[2] { + return -1 + } + if _z[1] > _x[1] { + return 1 + } else if _z[1] < _x[1] { + return -1 + } + if _z[0] > _x[0] { + return 1 + } else if _z[0] < _x[0] { + return -1 + } + return 0 +} + +// LexicographicallyLargest returns true if this element is strictly lexicographically +// larger than its negation, false otherwise +func (z *Element) LexicographicallyLargest() bool { + // adapted from github.com/zkcrypto/bls12_381 + // we check if the element is larger than (q-1) / 2 + // if z - (((q -1) / 2) + 1) have no underflow, then z > (q-1) / 2 + + _z := z.Bits() + + var b uint64 + _, b = bits.Sub64(_z[0], 15457472912252848165, 0) + _, b = bits.Sub64(_z[1], 17729421567657373120, b) + _, b = bits.Sub64(_z[2], 2340456963163339335, b) + _, b = bits.Sub64(_z[3], 33837617247556946, b) + + return b == 0 +} + +// SetRandom sets z to a uniform random value in [0, q). +// +// This might error only if reading from crypto/rand.Reader errors, +// in which case, value of z is undefined. +func (z *Element) SetRandom() (*Element, error) { + // this code is generated for all modulus + // and derived from go/src/crypto/rand/util.go + + // l is number of limbs * 8; the number of bytes needed to reconstruct 4 uint64 + const l = 32 + + // bitLen is the maximum bit length needed to encode a value < q. + const bitLen = 248 + + // k is the maximum byte length needed to encode a value < q. + const k = (bitLen + 7) / 8 + + // b is the number of bits in the most significant byte of q-1. + b := uint(bitLen % 8) + if b == 0 { + b = 8 + } + + var bytes [l]byte + + for { + // note that bytes[k:l] is always 0 + if _, err := io.ReadFull(rand.Reader, bytes[:k]); err != nil { + return nil, err + } + + // Clear unused bits in in the most significant byte to increase probability + // that the candidate is < q. + bytes[k-1] &= uint8(int(1<> 1 + z[0] = z[0]>>1 | z[1]<<63 + z[1] = z[1]>>1 | z[2]<<63 + z[2] = z[2]>>1 | z[3]<<63 + z[3] >>= 1 + +} + +// fromMont converts z in place (i.e. mutates) from Montgomery to regular representation +// sets and returns z = z * 1 +func (z *Element) fromMont() *Element { + fromMont(z) + return z +} + +// Add z = x + y (mod q) +func (z *Element) Add(x, y *Element) *Element { + + var carry uint64 + z[0], carry = bits.Add64(x[0], y[0], 0) + z[1], carry = bits.Add64(x[1], y[1], carry) + z[2], carry = bits.Add64(x[2], y[2], carry) + z[3], _ = bits.Add64(x[3], y[3], carry) + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } + return z +} + +// Double z = x + x (mod q), aka Lsh 1 +func (z *Element) Double(x *Element) *Element { + + var carry uint64 + z[0], carry = bits.Add64(x[0], x[0], 0) + z[1], carry = bits.Add64(x[1], x[1], carry) + z[2], carry = bits.Add64(x[2], x[2], carry) + z[3], _ = bits.Add64(x[3], x[3], carry) + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } + return z +} + +// Sub z = x - y (mod q) +func (z *Element) Sub(x, y *Element) *Element { + var b uint64 + z[0], b = bits.Sub64(x[0], y[0], 0) + z[1], b = bits.Sub64(x[1], y[1], b) + z[2], b = bits.Sub64(x[2], y[2], b) + z[3], b = bits.Sub64(x[3], y[3], b) + if b != 0 { + var c uint64 + z[0], c = bits.Add64(z[0], q0, 0) + z[1], c = bits.Add64(z[1], q1, c) + z[2], c = bits.Add64(z[2], q2, c) + z[3], _ = bits.Add64(z[3], q3, c) + } + return z +} + +// Neg z = q - x +func (z *Element) Neg(x *Element) *Element { + if x.IsZero() { + z.SetZero() + return z + } + var borrow uint64 + z[0], borrow = bits.Sub64(q0, x[0], 0) + z[1], borrow = bits.Sub64(q1, x[1], borrow) + z[2], borrow = bits.Sub64(q2, x[2], borrow) + z[3], _ = bits.Sub64(q3, x[3], borrow) + return z +} + +// Select is a constant-time conditional move. +// If c=0, z = x0. Else z = x1 +func (z *Element) Select(c int, x0 *Element, x1 *Element) *Element { + cC := uint64((int64(c) | -int64(c)) >> 63) // "canonicized" into: 0 if c=0, -1 otherwise + z[0] = x0[0] ^ cC&(x0[0]^x1[0]) + z[1] = x0[1] ^ cC&(x0[1]^x1[1]) + z[2] = x0[2] ^ cC&(x0[2]^x1[2]) + z[3] = x0[3] ^ cC&(x0[3]^x1[3]) + return z +} + +// _mulGeneric is unoptimized textbook CIOS +// it is a fallback solution on x86 when ADX instruction set is not available +// and is used for testing purposes. +func _mulGeneric(z, x, y *Element) { + + // Algorithm 2 of "Faster Montgomery Multiplication and Multi-Scalar-Multiplication for SNARKS" + // by Y. El Housni and G. Botrel https://doi.org/10.46586/tches.v2023.i3.504-521 + + var t [5]uint64 + var D uint64 + var m, C uint64 + // ----------------------------------- + // First loop + + C, t[0] = bits.Mul64(y[0], x[0]) + C, t[1] = madd1(y[0], x[1], C) + C, t[2] = madd1(y[0], x[2], C) + C, t[3] = madd1(y[0], x[3], C) + + t[4], D = bits.Add64(t[4], C, 0) + + // m = t[0]n'[0] mod W + m = t[0] * qInvNeg + + // ----------------------------------- + // Second loop + C = madd0(m, q0, t[0]) + C, t[0] = madd2(m, q1, t[1], C) + C, t[1] = madd2(m, q2, t[2], C) + C, t[2] = madd2(m, q3, t[3], C) + + t[3], C = bits.Add64(t[4], C, 0) + t[4], _ = bits.Add64(0, D, C) + // ----------------------------------- + // First loop + + C, t[0] = madd1(y[1], x[0], t[0]) + C, t[1] = madd2(y[1], x[1], t[1], C) + C, t[2] = madd2(y[1], x[2], t[2], C) + C, t[3] = madd2(y[1], x[3], t[3], C) + + t[4], D = bits.Add64(t[4], C, 0) + + // m = t[0]n'[0] mod W + m = t[0] * qInvNeg + + // ----------------------------------- + // Second loop + C = madd0(m, q0, t[0]) + C, t[0] = madd2(m, q1, t[1], C) + C, t[1] = madd2(m, q2, t[2], C) + C, t[2] = madd2(m, q3, t[3], C) + + t[3], C = bits.Add64(t[4], C, 0) + t[4], _ = bits.Add64(0, D, C) + // ----------------------------------- + // First loop + + C, t[0] = madd1(y[2], x[0], t[0]) + C, t[1] = madd2(y[2], x[1], t[1], C) + C, t[2] = madd2(y[2], x[2], t[2], C) + C, t[3] = madd2(y[2], x[3], t[3], C) + + t[4], D = bits.Add64(t[4], C, 0) + + // m = t[0]n'[0] mod W + m = t[0] * qInvNeg + + // ----------------------------------- + // Second loop + C = madd0(m, q0, t[0]) + C, t[0] = madd2(m, q1, t[1], C) + C, t[1] = madd2(m, q2, t[2], C) + C, t[2] = madd2(m, q3, t[3], C) + + t[3], C = bits.Add64(t[4], C, 0) + t[4], _ = bits.Add64(0, D, C) + // ----------------------------------- + // First loop + + C, t[0] = madd1(y[3], x[0], t[0]) + C, t[1] = madd2(y[3], x[1], t[1], C) + C, t[2] = madd2(y[3], x[2], t[2], C) + C, t[3] = madd2(y[3], x[3], t[3], C) + + t[4], D = bits.Add64(t[4], C, 0) + + // m = t[0]n'[0] mod W + m = t[0] * qInvNeg + + // ----------------------------------- + // Second loop + C = madd0(m, q0, t[0]) + C, t[0] = madd2(m, q1, t[1], C) + C, t[1] = madd2(m, q2, t[2], C) + C, t[2] = madd2(m, q3, t[3], C) + + t[3], C = bits.Add64(t[4], C, 0) + t[4], _ = bits.Add64(0, D, C) + + if t[4] != 0 { + // we need to reduce, we have a result on 5 words + var b uint64 + z[0], b = bits.Sub64(t[0], q0, 0) + z[1], b = bits.Sub64(t[1], q1, b) + z[2], b = bits.Sub64(t[2], q2, b) + z[3], _ = bits.Sub64(t[3], q3, b) + return + } + + // copy t into z + z[0] = t[0] + z[1] = t[1] + z[2] = t[2] + z[3] = t[3] + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } +} + +func _fromMontGeneric(z *Element) { + // the following lines implement z = z * 1 + // with a modified CIOS montgomery multiplication + // see Mul for algorithm documentation + { + // m = z[0]n'[0] mod W + m := z[0] * qInvNeg + C := madd0(m, q0, z[0]) + C, z[0] = madd2(m, q1, z[1], C) + C, z[1] = madd2(m, q2, z[2], C) + C, z[2] = madd2(m, q3, z[3], C) + z[3] = C + } + { + // m = z[0]n'[0] mod W + m := z[0] * qInvNeg + C := madd0(m, q0, z[0]) + C, z[0] = madd2(m, q1, z[1], C) + C, z[1] = madd2(m, q2, z[2], C) + C, z[2] = madd2(m, q3, z[3], C) + z[3] = C + } + { + // m = z[0]n'[0] mod W + m := z[0] * qInvNeg + C := madd0(m, q0, z[0]) + C, z[0] = madd2(m, q1, z[1], C) + C, z[1] = madd2(m, q2, z[2], C) + C, z[2] = madd2(m, q3, z[3], C) + z[3] = C + } + { + // m = z[0]n'[0] mod W + m := z[0] * qInvNeg + C := madd0(m, q0, z[0]) + C, z[0] = madd2(m, q1, z[1], C) + C, z[1] = madd2(m, q2, z[2], C) + C, z[2] = madd2(m, q3, z[3], C) + z[3] = C + } + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } +} + +func _reduceGeneric(z *Element) { + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } +} + +// BatchInvert returns a new slice with every element inverted. +// Uses Montgomery batch inversion trick +func BatchInvert(a []Element) []Element { + res := make([]Element, len(a)) + if len(a) == 0 { + return res + } + + zeroes := bitset.New(uint(len(a))) + accumulator := One() + + for i := range len(a) { + if a[i].IsZero() { + zeroes.Set(uint(i)) + continue + } + res[i] = accumulator + accumulator.Mul(&accumulator, &a[i]) + } + + accumulator.Inverse(&accumulator) + + for i := len(a) - 1; i >= 0; i-- { + if zeroes.Test(uint(i)) { + continue + } + res[i].Mul(&res[i], &accumulator) + accumulator.Mul(&accumulator, &a[i]) + } + + return res +} + +func _butterflyGeneric(a, b *Element) { + t := *a + a.Add(a, b) + b.Sub(&t, b) +} + +// BitLen returns the minimum number of bits needed to represent z +// returns 0 if z == 0 +func (z *Element) BitLen() int { + if z[3] != 0 { + return 192 + bits.Len64(z[3]) + } + if z[2] != 0 { + return 128 + bits.Len64(z[2]) + } + if z[1] != 0 { + return 64 + bits.Len64(z[1]) + } + return bits.Len64(z[0]) +} + +// Hash msg to count prime field elements. +// https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-06#section-5.2 +func Hash(msg, dst []byte, count int) ([]Element, error) { + // 128 bits of security + // L = ceil((ceil(log2(p)) + k) / 8), where k is the security parameter = 128 + const Bytes = 1 + (Bits-1)/8 + const L = 16 + Bytes + + lenInBytes := count * L + pseudoRandomBytes, err := hash.ExpandMsgXmd(msg, dst, lenInBytes) + if err != nil { + return nil, err + } + + // get temporary big int from the pool + vv := pool.BigInt.Get() + + res := make([]Element, count) + for i := range count { + vv.SetBytes(pseudoRandomBytes[i*L : (i+1)*L]) + res[i].SetBigInt(vv) + } + + // release object into pool + pool.BigInt.Put(vv) + + return res, nil +} + +// Exp z = xᵏ (mod q) +func (z *Element) Exp(x Element, k *big.Int) *Element { + if k.IsUint64() && k.Uint64() == 0 { + return z.SetOne() + } + + e := k + if k.Sign() == -1 { + // negative k, we invert + // if k < 0: xᵏ (mod q) == (x⁻¹)ᵏ (mod q) + x.Inverse(&x) + + // we negate k in a temp big.Int since + // Int.Bit(_) of k and -k is different + e = pool.BigInt.Get() + defer pool.BigInt.Put(e) + e.Neg(k) + } + + z.Set(&x) + + for i := e.BitLen() - 2; i >= 0; i-- { + z.Square(z) + if e.Bit(i) == 1 { + z.Mul(z, &x) + } + } + + return z +} + +// rSquare where r is the Montgommery constant +// see section 2.3.2 of Tolga Acar's thesis +// https://www.microsoft.com/en-us/research/wp-content/uploads/1998/06/97Acar.pdf +var rSquare = Element{ + 12503850779977665488, + 4231521375444390721, + 12923916254582618147, + 4786565839245165, +} + +// toMont converts z to Montgomery form +// sets and returns z = z * r² +func (z *Element) toMont() *Element { + return z.Mul(z, &rSquare) +} + +// String returns the decimal representation of z as generated by +// z.Text(10). +func (z *Element) String() string { + return z.Text(10) +} + +// toBigInt returns z as a big.Int in Montgomery form +func (z *Element) toBigInt(res *big.Int) *big.Int { + var b [Bytes]byte + binary.BigEndian.PutUint64(b[24:32], z[0]) + binary.BigEndian.PutUint64(b[16:24], z[1]) + binary.BigEndian.PutUint64(b[8:16], z[2]) + binary.BigEndian.PutUint64(b[0:8], z[3]) + + return res.SetBytes(b[:]) +} + +// Text returns the string representation of z in the given base. +// Base must be between 2 and 36, inclusive. The result uses the +// lower-case letters 'a' to 'z' for digit values 10 to 35. +// No prefix (such as "0x") is added to the string. If z is a nil +// pointer it returns "". +// If base == 10 and -z fits in a uint16 prefix "-" is added to the string. +func (z *Element) Text(base int) string { + if base < 2 || base > 36 { + panic("invalid base") + } + if z == nil { + return "" + } + + const maxUint16 = 65535 + if base == 10 { + var zzNeg Element + zzNeg.Neg(z) + zzNeg.fromMont() + if zzNeg.FitsOnOneWord() && zzNeg[0] <= maxUint16 && zzNeg[0] != 0 { + return "-" + strconv.FormatUint(zzNeg[0], base) + } + } + zz := *z + zz.fromMont() + if zz.FitsOnOneWord() { + return strconv.FormatUint(zz[0], base) + } + vv := pool.BigInt.Get() + r := zz.toBigInt(vv).Text(base) + pool.BigInt.Put(vv) + return r +} + +// BigInt sets and return z as a *big.Int +func (z *Element) BigInt(res *big.Int) *big.Int { + _z := *z + _z.fromMont() + return _z.toBigInt(res) +} + +// ToBigIntRegular returns z as a big.Int in regular form +// +// Deprecated: use BigInt(*big.Int) instead +func (z Element) ToBigIntRegular(res *big.Int) *big.Int { + z.fromMont() + return z.toBigInt(res) +} + +// Bits provides access to z by returning its value as a little-endian [4]uint64 array. +// Bits is intended to support implementation of missing low-level Element +// functionality outside this package; it should be avoided otherwise. +func (z *Element) Bits() [4]uint64 { + _z := *z + fromMont(&_z) + return _z +} + +// Bytes returns the value of z as a big-endian byte array +func (z *Element) Bytes() (res [Bytes]byte) { + BigEndian.PutElement(&res, *z) + return +} + +// Marshal returns the value of z as a big-endian byte slice +func (z *Element) Marshal() []byte { + b := z.Bytes() + return b[:] +} + +// Unmarshal is an alias for SetBytes, it sets z to the value of e. +func (z *Element) Unmarshal(e []byte) { + z.SetBytes(e) +} + +// SetBytes interprets e as the bytes of a big-endian unsigned integer, +// sets z to that value, and returns z. +func (z *Element) SetBytes(e []byte) *Element { + if len(e) == Bytes { + // fast path + v, err := BigEndian.Element((*[Bytes]byte)(e)) + if err == nil { + *z = v + return z + } + } + + // slow path. + // get a big int from our pool + vv := pool.BigInt.Get() + vv.SetBytes(e) + + // set big int + z.SetBigInt(vv) + + // put temporary object back in pool + pool.BigInt.Put(vv) + + return z +} + +// SetBytesCanonical interprets e as the bytes of a big-endian 32-byte integer. +// If e is not a 32-byte slice or encodes a value higher than q, +// SetBytesCanonical returns an error. +func (z *Element) SetBytesCanonical(e []byte) error { + if len(e) != Bytes { + return errors.New("invalid fr.Element encoding") + } + v, err := BigEndian.Element((*[Bytes]byte)(e)) + if err != nil { + return err + } + *z = v + return nil +} + +// SetBigInt sets z to v and returns z +func (z *Element) SetBigInt(v *big.Int) *Element { + z.SetZero() + + var zero big.Int + + // fast path + c := v.Cmp(&_modulus) + if c == 0 { + // v == 0 + return z + } else if c != 1 && v.Cmp(&zero) != -1 { + // 0 <= v < q + return z.setBigInt(v) + } + + // get temporary big int from the pool + vv := pool.BigInt.Get() + + // copy input + modular reduction + vv.Mod(v, &_modulus) + + // set big int byte value + z.setBigInt(vv) + + // release object into pool + pool.BigInt.Put(vv) + return z +} + +// setBigInt assumes 0 ⩽ v < q +func (z *Element) setBigInt(v *big.Int) *Element { + vBits := v.Bits() + + if bits.UintSize == 64 { + for i := range len(vBits) { + z[i] = uint64(vBits[i]) + } + } else { + for i := range len(vBits) { + if i%2 == 0 { + z[i/2] = uint64(vBits[i]) + } else { + z[i/2] |= uint64(vBits[i]) << 32 + } + } + } + + return z.toMont() +} + +// SetString creates a big.Int with number and calls SetBigInt on z +// +// The number prefix determines the actual base: A prefix of +// ”0b” or ”0B” selects base 2, ”0”, ”0o” or ”0O” selects base 8, +// and ”0x” or ”0X” selects base 16. Otherwise, the selected base is 10 +// and no prefix is accepted. +// +// For base 16, lower and upper case letters are considered the same: +// The letters 'a' to 'f' and 'A' to 'F' represent digit values 10 to 15. +// +// An underscore character ”_” may appear between a base +// prefix and an adjacent digit, and between successive digits; such +// underscores do not change the value of the number. +// Incorrect placement of underscores is reported as a panic if there +// are no other errors. +// +// If the number is invalid this method leaves z unchanged and returns nil, error. +func (z *Element) SetString(number string) (*Element, error) { + // get temporary big int from the pool + vv := pool.BigInt.Get() + + if _, ok := vv.SetString(number, 0); !ok { + return nil, errors.New("Element.SetString failed -> can't parse number into a big.Int " + number) + } + + z.SetBigInt(vv) + + // release object into pool + pool.BigInt.Put(vv) + + return z, nil +} + +// MarshalJSON returns json encoding of z (z.Text(10)) +// If z == nil, returns null +func (z *Element) MarshalJSON() ([]byte, error) { + if z == nil { + return []byte("null"), nil + } + const maxSafeBound = 15 // we encode it as number if it's small + s := z.Text(10) + if len(s) <= maxSafeBound { + return []byte(s), nil + } + var sbb strings.Builder + sbb.WriteByte('"') + sbb.WriteString(s) + sbb.WriteByte('"') + return []byte(sbb.String()), nil +} + +// UnmarshalJSON accepts numbers and strings as input +// See Element.SetString for valid prefixes (0x, 0b, ...) +func (z *Element) UnmarshalJSON(data []byte) error { + s := string(data) + if len(s) > Bits*3 { + return errors.New("value too large (max = Element.Bits * 3)") + } + + // we accept numbers and strings, remove leading and trailing quotes if any + if len(s) > 0 && s[0] == '"' { + s = s[1:] + } + if len(s) > 0 && s[len(s)-1] == '"' { + s = s[:len(s)-1] + } + + // get temporary big int from the pool + vv := pool.BigInt.Get() + + if _, ok := vv.SetString(s, 0); !ok { + return errors.New("can't parse into a big.Int: " + s) + } + + z.SetBigInt(vv) + + // release object into pool + pool.BigInt.Put(vv) + return nil +} + +// A ByteOrder specifies how to convert byte slices into a Element +type ByteOrder interface { + Element(*[Bytes]byte) (Element, error) + PutElement(*[Bytes]byte, Element) + String() string +} + +var errInvalidEncoding = errors.New("invalid fr.Element encoding") + +// BigEndian is the big-endian implementation of ByteOrder and AppendByteOrder. +var BigEndian bigEndian + +type bigEndian struct{} + +// Element interpret b is a big-endian 32-byte slice. +// If b encodes a value higher than q, Element returns error. +func (bigEndian) Element(b *[Bytes]byte) (Element, error) { + var z Element + z[0] = binary.BigEndian.Uint64((*b)[24:32]) + z[1] = binary.BigEndian.Uint64((*b)[16:24]) + z[2] = binary.BigEndian.Uint64((*b)[8:16]) + z[3] = binary.BigEndian.Uint64((*b)[0:8]) + + if !z.smallerThanModulus() { + return Element{}, errInvalidEncoding + } + + z.toMont() + return z, nil +} + +func (bigEndian) PutElement(b *[Bytes]byte, e Element) { + e.fromMont() + binary.BigEndian.PutUint64((*b)[24:32], e[0]) + binary.BigEndian.PutUint64((*b)[16:24], e[1]) + binary.BigEndian.PutUint64((*b)[8:16], e[2]) + binary.BigEndian.PutUint64((*b)[0:8], e[3]) +} + +func (bigEndian) String() string { return "BigEndian" } + +// LittleEndian is the little-endian implementation of ByteOrder and AppendByteOrder. +var LittleEndian littleEndian + +type littleEndian struct{} + +func (littleEndian) Element(b *[Bytes]byte) (Element, error) { + var z Element + z[0] = binary.LittleEndian.Uint64((*b)[0:8]) + z[1] = binary.LittleEndian.Uint64((*b)[8:16]) + z[2] = binary.LittleEndian.Uint64((*b)[16:24]) + z[3] = binary.LittleEndian.Uint64((*b)[24:32]) + + if !z.smallerThanModulus() { + return Element{}, errInvalidEncoding + } + + z.toMont() + return z, nil +} + +func (littleEndian) PutElement(b *[Bytes]byte, e Element) { + e.fromMont() + binary.LittleEndian.PutUint64((*b)[0:8], e[0]) + binary.LittleEndian.PutUint64((*b)[8:16], e[1]) + binary.LittleEndian.PutUint64((*b)[16:24], e[2]) + binary.LittleEndian.PutUint64((*b)[24:32], e[3]) +} + +func (littleEndian) String() string { return "LittleEndian" } + +// Legendre returns the Legendre symbol of z (either +1, -1, or 0.) +func (z *Element) Legendre() int { + + // Adapts "Optimized Binary GCD for Modular Inversion" + // https://github.com/pornin/bingcd/blob/main/doc/bingcd.pdf + // For a faithful implementation of Pornin20 see [Inverse]. + + // We don't need to account for z being in Montgomery form. + // (xR|q) = (x|q)(R|q). R is a square (an even power of 2), so (R|q) = 1. + a := *z + b := Element{ + q0, + q1, + q2, + q3, + } // b := q + + // Update factors: we get [a; b] ← [f₀ g₀; f₁ g₁] [a; b] + // cᵢ = fᵢ + 2³¹ - 1 + 2³² * (gᵢ + 2³¹ - 1) + var c0, c1 int64 + + var s Element + + l := 1 // loop invariant: (x|q) = (a|b) . l + // This means that every time a and b are updated into a' and b', + // l is updated into l' = (x|q)(a'|b')=(x|q)(a|b)(a|b)(a'|b') = l (a|b)(a'|b') + // During the algorithm's run, there is no guarantee that b remains prime, or even positive. + // Therefore, we use the properties of the Kronecker symbol, a generalization of the Legendre symbol to all integers. + + for !a.IsZero() { + n := max(a.BitLen(), b.BitLen()) + aApprox, bApprox := approximateForLegendre(&a, n), approximateForLegendre(&b, n) + + // f₀, g₀, f₁, g₁ = 1, 0, 0, 1 + c0, c1 = updateFactorIdentityMatrixRow0, updateFactorIdentityMatrixRow1 + + const nbIterations = k - 2 + // running fewer iterations because we need access to 3 low bits from b, rather than 1 in the inversion algorithm + for range nbIterations { + + if aApprox&1 == 0 { + aApprox /= 2 + + // update the Kronecker symbol + // + // (a/2 | b) (2|b) = (a|b) + // + // b is either odd or zero, the latter case implying a non-trivial GCD and an ultimate result of 0, + // regardless of what value l holds. + // So in updating l, we may assume that b is odd. + // Since a is even, we only need to correctly compute l if b is odd. + // if b is also even, the non-trivial GCD will result in the function returning 0 anyway. + // so we may here assume b is odd. + // (2|b) = 1 if b ≡ 1 or 7 (mod 8), and -1 if b ≡ 3 or 5 (mod 8) + if bMod8 := bApprox & 7; bMod8 == 3 || bMod8 == 5 { + l = -l + } + + } else { + s, borrow := bits.Sub64(aApprox, bApprox, 0) + if borrow == 1 { + // Compute (b-a|a) + // (x-y|z) = (x|z) unless z < 0 and sign(x-y) ≠ sign(x) + // Pornin20 asserts that at least one of a and b is non-negative. + // If a is non-negative, we immediately get (b-a|a) = (b|a) + // If a is negative, b-a > b. But b is already non-negative, so the b-a and b have the same sign. + // Thus in that case also (b-a|a) = (b|a) + // Since not both a and b are negative, we get a quadratic reciprocity law + // like that of the Legendre symbol: (b|a) = (a|b), unless a, b ≡ 3 (mod 4), in which case (b|a) = -(a|b) + if bApprox&3 == 3 && aApprox&3 == 3 { + l = -l + } + + s = bApprox - aApprox + bApprox = aApprox + c0, c1 = c1, c0 + } + + aApprox = s / 2 + c0 = c0 - c1 + + // update l to reflect halving a, just like in the case where a is even + if bMod8 := bApprox & 7; bMod8 == 3 || bMod8 == 5 { + l = -l + } + } + + c1 *= 2 + } + + s = a + + var g0 int64 + // from this point on c0 aliases for f0 + c0, g0 = updateFactorsDecompose(c0) + aHi := a.linearCombNonModular(&s, c0, &b, g0) + if aHi&signBitSelector != 0 { + // if aHi < 0 + aHi = negL(&a, aHi) + // Since a is negative, b is not and hence b ≠ -1 + // So we get (-a|b)=(-1|b)(a|b) + // b is odd so we get (-1|b) = 1 if b ≡ 1 (mod 4) and -1 otherwise. + if bApprox&3 == 3 { // we still have two valid lower bits for b + l = -l + } + } + // right-shift a by k-2 bits + a[0] = (a[0] >> nbIterations) | ((a[1]) << (2*k - nbIterations)) + a[1] = (a[1] >> nbIterations) | ((a[2]) << (2*k - nbIterations)) + a[2] = (a[2] >> nbIterations) | ((a[3]) << (2*k - nbIterations)) + a[3] = (a[3] >> nbIterations) | (aHi << (2*k - nbIterations)) + + var f1 int64 + // from this point on c1 aliases for g0 + f1, c1 = updateFactorsDecompose(c1) + bHi := b.linearCombNonModular(&s, f1, &b, c1) + if bHi&signBitSelector != 0 { + // if bHi < 0 + bHi = negL(&b, bHi) + // no need to update l, since we know a ≥ 0 + // (a|-1) = 1 if a ≥ 0 + } + // right-shift b by k-2 bits + b[0] = (b[0] >> nbIterations) | ((b[1]) << (2*k - nbIterations)) + b[1] = (b[1] >> nbIterations) | ((b[2]) << (2*k - nbIterations)) + b[2] = (b[2] >> nbIterations) | ((b[3]) << (2*k - nbIterations)) + b[3] = (b[3] >> nbIterations) | (bHi << (2*k - nbIterations)) + } + + if b[0] == 1 && (b[1]|b[2]|b[3]) == 0 { + return l // (0|1) = 1 + } else { + return 0 // if b ≠ 1, then (z,q) ≠ 0 ⇒ (z|q) = 0 + } +} + +// approximate a big number x into a single 64 bit word using its uppermost and lowermost bits. +// If x fits in a word as is, no approximation necessary. +// This differs from the standard approximate function in that in the Legendre symbol computation +// we need to access the 3 low bits of b, rather than just one. So lo ≥ n+2 where n is the number of inner iterations. +// The requirement on the high bits is unchanged, hi ≥ n+1. +// Thus we hit a maximum of hi = lo = k and n = k-2 as opposed to n = lo = k-1 and hi = k+1 in the standard approximate function. +// Since we are doing fewer iterations than in the inversion algorithm, all the arguments on bounds for update factors remain valid. +func approximateForLegendre(x *Element, nBits int) uint64 { + + if nBits <= 64 { + return x[0] + } + + const mask = (uint64(1) << k) - 1 // k ones + lo := mask & x[0] + + hiWordIndex := (nBits - 1) / 64 + + hiWordBitsAvailable := nBits - hiWordIndex*64 + hiWordBitsUsed := min(hiWordBitsAvailable, k) + + mask_ := uint64(^((1 << (hiWordBitsAvailable - hiWordBitsUsed)) - 1)) + hi := (x[hiWordIndex] & mask_) << (64 - hiWordBitsAvailable) + + mask_ = ^(1<<(k+hiWordBitsUsed) - 1) + mid := (mask_ & x[hiWordIndex-1]) >> hiWordBitsUsed + + return lo | mid | hi +} + +// Sqrt z = √x (mod q) +// if the square root doesn't exist (x is not a square mod q) +// Sqrt leaves z unchanged and returns nil +func (z *Element) Sqrt(x *Element) *Element { + // q ≡ 1 (mod 4) + // see modSqrtTonelliShanks in math/big/int.go + // using https://www.maa.org/sites/default/files/pdf/upload_library/22/Polya/07468342.di020786.02p0470a.pdf + + var y, b, t, w Element + // w = x^((s-1)/2)) + w.ExpBySqrtExp(*x) + + // y = x^((s+1)/2)) = w * x + y.Mul(x, &w) + + // b = xˢ = w * w * x = y * x + b.Mul(&w, &y) + + // g = nonResidue ^ s + var g = Element{ + 4018409873578636989, + 10887900054184212976, + 2544652568513935695, + 49070325971381582, + } + r := uint64(3) + + // compute legendre symbol + // t = x^((q-1)/2) = r-1 squaring of xˢ + t = b + for i := uint64(0); i < r-1; i++ { + t.Square(&t) + } + if t.IsZero() { + return z.SetZero() + } + if !t.IsOne() { + // t != 1, we don't have a square root + return nil + } + for { + var m uint64 + t = b + + // for t != 1 + for !t.IsOne() { + t.Square(&t) + m++ + } + + if m == 0 { + return z.Set(&y) + } + // t = g^(2^(r-m-1)) (mod q) + ge := int(r - m - 1) + t = g + for ge > 0 { + t.Square(&t) + ge-- + } + + g.Square(&t) + y.Mul(&y, &t) + b.Mul(&b, &g) + r = m + } +} + +// 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 ≡ 1 (mod 3) + // Reference: Lemma 3 of https://eprint.iacr.org/2021/1446.pdf + // e ≥ 3: Tonelli-Shanks variant + var y, t, w, c Element + // s ≡ 2 (mod 3), using (s+1)/3 + // y = x^((s+1)/3) is the initial candidate + y.ExpByCbrts1o3(*x) + + // t = y^3 * x^{-1} = x^{s+1} * x^{-1} = x^s + c.Cube(&y) // c = y^3 = x^{s+1} + t.Inverse(x).Mul(&t, &c) // t = x^s + + // γ = nonCubicResidue ^ s (a primitive 3^e root of unity) + var g = Element{ + 4092408165435579577, + 10162551647454513167, + 8295980666640042594, + 41673137058195811, + } + r := uint64(3) + + // Check if x is a cubic residue: x^((q-1)/3) should be 1 + // This equals t^(3^(e-1)) + var check Element + check = t + for i := uint64(0); i < r-1; i++ { + check.Cube(&check) + } + if !check.IsOne() { + // x is not a cubic residue + return nil + } + + // Main loop: adjust y until y^3 = x + for { + var m uint64 + check = t + + // Find smallest m ≥ 0 such that t^{3^m} = 1 + for !check.IsOne() { + check.Cube(&check) + m++ + } + + if m == 0 { + // t = 1, so y^3 = x + return z.Set(&y) + } + + // Compute δ = g^{3^{r-m}} (a primitive 3^m-th root of unity) + ge := int(r - m) + w = g + for ge > 0 { + w.Cube(&w) + ge-- + } + + // Compute cube root of δ: c = g^{3^{r-m-1}} + // Note: c^3 = g^{3^{r-m}} = w = δ + ge = int(r - m - 1) + c = g + for ge > 0 { + c.Cube(&c) + ge-- + } + + // Find k ∈ {1, 2} such that (t * δ^k)^{3^{m-1}} = 1 + // We test k = 1 first + var tw Element + tw.Mul(&t, &w) + check = tw + for i := uint64(0); i < m-1; i++ { + check.Cube(&check) + } + + if check.IsOne() { + // k = 1: t_new = t * δ, y_new = y * (cube root of δ) = y * c + t = tw + y.Mul(&y, &c) + } else { + // k = 2: t_new = t * δ^2, y_new = y * (cube root of δ^2) = y * c^2 + t.Mul(&tw, &w) + y.Mul(&y, &c).Mul(&y, &c) + } + + // Update g to be w (primitive 3^m root of unity) for next iteration + g = w + r = m + } +} + +// 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 +} + +const ( + k = 32 // word size / 2 + signBitSelector = uint64(1) << 63 + approxLowBitsN = k - 1 + approxHighBitsN = k + 1 +) + +const ( + inversionCorrectionFactorWord0 = 8013490130535197113 + inversionCorrectionFactorWord1 = 341597987825497263 + inversionCorrectionFactorWord2 = 12045570510022576392 + inversionCorrectionFactorWord3 = 38390577882398742 + invIterationsN = 16 +) + +// Inverse z = x⁻¹ (mod q) +// +// if x == 0, sets and returns z = x +func (z *Element) Inverse(x *Element) *Element { + // Implements "Optimized Binary GCD for Modular Inversion" + // https://github.com/pornin/bingcd/blob/main/doc/bingcd.pdf + + a := *x + b := Element{ + q0, + q1, + q2, + q3, + } // b := q + + u := Element{1} + + // Update factors: we get [u; v] ← [f₀ g₀; f₁ g₁] [u; v] + // cᵢ = fᵢ + 2³¹ - 1 + 2³² * (gᵢ + 2³¹ - 1) + var c0, c1 int64 + + // Saved update factors to reduce the number of field multiplications + var pf0, pf1, pg0, pg1 int64 + + var i uint + + var v, s Element + + // Since u,v are updated every other iteration, we must make sure we terminate after evenly many iterations + // This also lets us get away with half as many updates to u,v + // To make this constant-time-ish, replace the condition with i < invIterationsN + for i = 0; i&1 == 1 || !a.IsZero(); i++ { + n := max(a.BitLen(), b.BitLen()) + aApprox, bApprox := approximate(&a, n), approximate(&b, n) + + // f₀, g₀, f₁, g₁ = 1, 0, 0, 1 + c0, c1 = updateFactorIdentityMatrixRow0, updateFactorIdentityMatrixRow1 + + for range approxLowBitsN { + + // -2ʲ < f₀, f₁ ≤ 2ʲ + // |f₀| + |f₁| < 2ʲ⁺¹ + + if aApprox&1 == 0 { + aApprox /= 2 + } else { + s, borrow := bits.Sub64(aApprox, bApprox, 0) + if borrow == 1 { + s = bApprox - aApprox + bApprox = aApprox + c0, c1 = c1, c0 + // invariants unchanged + } + + aApprox = s / 2 + c0 = c0 - c1 + + // Now |f₀| < 2ʲ⁺¹ ≤ 2ʲ⁺¹ (only the weaker inequality is needed, strictly speaking) + // Started with f₀ > -2ʲ and f₁ ≤ 2ʲ, so f₀ - f₁ > -2ʲ⁺¹ + // Invariants unchanged for f₁ + } + + c1 *= 2 + // -2ʲ⁺¹ < f₁ ≤ 2ʲ⁺¹ + // So now |f₀| + |f₁| < 2ʲ⁺² + } + + s = a + + var g0 int64 + // from this point on c0 aliases for f0 + c0, g0 = updateFactorsDecompose(c0) + aHi := a.linearCombNonModular(&s, c0, &b, g0) + if aHi&signBitSelector != 0 { + // if aHi < 0 + c0, g0 = -c0, -g0 + aHi = negL(&a, aHi) + } + // right-shift a by k-1 bits + a[0] = (a[0] >> approxLowBitsN) | ((a[1]) << approxHighBitsN) + a[1] = (a[1] >> approxLowBitsN) | ((a[2]) << approxHighBitsN) + a[2] = (a[2] >> approxLowBitsN) | ((a[3]) << approxHighBitsN) + a[3] = (a[3] >> approxLowBitsN) | (aHi << approxHighBitsN) + + var f1 int64 + // from this point on c1 aliases for g0 + f1, c1 = updateFactorsDecompose(c1) + bHi := b.linearCombNonModular(&s, f1, &b, c1) + if bHi&signBitSelector != 0 { + // if bHi < 0 + f1, c1 = -f1, -c1 + bHi = negL(&b, bHi) + } + // right-shift b by k-1 bits + b[0] = (b[0] >> approxLowBitsN) | ((b[1]) << approxHighBitsN) + b[1] = (b[1] >> approxLowBitsN) | ((b[2]) << approxHighBitsN) + b[2] = (b[2] >> approxLowBitsN) | ((b[3]) << approxHighBitsN) + b[3] = (b[3] >> approxLowBitsN) | (bHi << approxHighBitsN) + + if i&1 == 1 { + // Combine current update factors with previously stored ones + // [F₀, G₀; F₁, G₁] ← [f₀, g₀; f₁, g₁] [pf₀, pg₀; pf₁, pg₁], with capital letters denoting new combined values + // We get |F₀| = | f₀pf₀ + g₀pf₁ | ≤ |f₀pf₀| + |g₀pf₁| = |f₀| |pf₀| + |g₀| |pf₁| ≤ 2ᵏ⁻¹|pf₀| + 2ᵏ⁻¹|pf₁| + // = 2ᵏ⁻¹ (|pf₀| + |pf₁|) < 2ᵏ⁻¹ 2ᵏ = 2²ᵏ⁻¹ + // So |F₀| < 2²ᵏ⁻¹ meaning it fits in a 2k-bit signed register + + // c₀ aliases f₀, c₁ aliases g₁ + c0, g0, f1, c1 = c0*pf0+g0*pf1, + c0*pg0+g0*pg1, + f1*pf0+c1*pf1, + f1*pg0+c1*pg1 + + s = u + + // 0 ≤ u, v < 2²⁵⁵ + // |F₀|, |G₀| < 2⁶³ + u.linearComb(&u, c0, &v, g0) + // |F₁|, |G₁| < 2⁶³ + v.linearComb(&s, f1, &v, c1) + + } else { + // Save update factors + pf0, pg0, pf1, pg1 = c0, g0, f1, c1 + } + } + + // For every iteration that we miss, v is not being multiplied by 2ᵏ⁻² + const pSq uint64 = 1 << (2 * (k - 1)) + a = Element{pSq} + // If the function is constant-time ish, this loop will not run (no need to take it out explicitly) + for ; i < invIterationsN; i += 2 { + // could optimize further with mul by word routine or by pre-computing a table since with k=26, + // we would multiply by pSq up to 13times; + // on x86, the assembly routine outperforms generic code for mul by word + // on arm64, we may loose up to ~5% for 6 limbs + v.Mul(&v, &a) + } + + u.Set(x) // for correctness check + + z.Mul(&v, &Element{ + inversionCorrectionFactorWord0, + inversionCorrectionFactorWord1, + inversionCorrectionFactorWord2, + inversionCorrectionFactorWord3, + }) + + // correctness check + v.Mul(&u, z) + if !v.IsOne() && !u.IsZero() { + return z.inverseExp(u) + } + + return z +} + +// inverseExp computes z = x⁻¹ (mod q) = x**(q-2) (mod q) +func (z *Element) inverseExp(x Element) *Element { + // e == q-2 + e := Modulus() + e.Sub(e, big.NewInt(2)) + + z.Set(&x) + + for i := e.BitLen() - 2; i >= 0; i-- { + z.Square(z) + if e.Bit(i) == 1 { + z.Mul(z, &x) + } + } + + return z +} + +// approximate a big number x into a single 64 bit word using its uppermost and lowermost bits +// if x fits in a word as is, no approximation necessary +func approximate(x *Element, nBits int) uint64 { + + if nBits <= 64 { + return x[0] + } + + const mask = (uint64(1) << approxLowBitsN) - 1 // k-1 ones + lo := mask & x[0] + + hiWordIndex := (nBits - 1) / 64 + + hiWordBitsAvailable := nBits - hiWordIndex*64 + hiWordBitsUsed := min(hiWordBitsAvailable, approxHighBitsN) + + mask_ := uint64(^((1 << (hiWordBitsAvailable - hiWordBitsUsed)) - 1)) + hi := (x[hiWordIndex] & mask_) << (64 - hiWordBitsAvailable) + + mask_ = ^(1<<(approxLowBitsN+hiWordBitsUsed) - 1) + mid := (mask_ & x[hiWordIndex-1]) >> hiWordBitsUsed + + return lo | mid | hi +} + +// linearComb z = xC * x + yC * y; +// 0 ≤ x, y < 2²⁴⁸ +// |xC|, |yC| < 2⁶³ +func (z *Element) linearComb(x *Element, xC int64, y *Element, yC int64) { + // | (hi, z) | < 2 * 2⁶³ * 2²⁴⁸ = 2³¹² + // therefore | hi | < 2⁵⁶ ≤ 2⁶³ + hi := z.linearCombNonModular(x, xC, y, yC) + z.montReduceSigned(z, hi) +} + +// montReduceSigned z = (xHi * r + x) * r⁻¹ using the SOS algorithm +// Requires |xHi| < 2⁶³. Most significant bit of xHi is the sign bit. +func (z *Element) montReduceSigned(x *Element, xHi uint64) { + const signBitRemover = ^signBitSelector + mustNeg := xHi&signBitSelector != 0 + // the SOS implementation requires that most significant bit is 0 + // Let X be xHi*r + x + // If X is negative we would have initially stored it as 2⁶⁴ r + X (à la 2's complement) + xHi &= signBitRemover + // with this a negative X is now represented as 2⁶³ r + X + + var t [2*Limbs - 1]uint64 + var C uint64 + + m := x[0] * qInvNeg + + C = madd0(m, q0, x[0]) + C, t[1] = madd2(m, q1, x[1], C) + C, t[2] = madd2(m, q2, x[2], C) + C, t[3] = madd2(m, q3, x[3], C) + + // m * qElement[3] ≤ (2⁶⁴ - 1) * (2⁶³ - 1) = 2¹²⁷ - 2⁶⁴ - 2⁶³ + 1 + // x[3] + C ≤ 2*(2⁶⁴ - 1) = 2⁶⁵ - 2 + // On LHS, (C, t[3]) ≤ 2¹²⁷ - 2⁶⁴ - 2⁶³ + 1 + 2⁶⁵ - 2 = 2¹²⁷ + 2⁶³ - 1 + // So on LHS, C ≤ 2⁶³ + t[4] = xHi + C + // xHi + C < 2⁶³ + 2⁶³ = 2⁶⁴ + + // + { + const i = 1 + m = t[i] * qInvNeg + + C = madd0(m, q0, t[i+0]) + C, t[i+1] = madd2(m, q1, t[i+1], C) + C, t[i+2] = madd2(m, q2, t[i+2], C) + C, t[i+3] = madd2(m, q3, t[i+3], C) + + t[i+Limbs] += C + } + { + const i = 2 + m = t[i] * qInvNeg + + C = madd0(m, q0, t[i+0]) + C, t[i+1] = madd2(m, q1, t[i+1], C) + C, t[i+2] = madd2(m, q2, t[i+2], C) + C, t[i+3] = madd2(m, q3, t[i+3], C) + + t[i+Limbs] += C + } + { + const i = 3 + m := t[i] * qInvNeg + + C = madd0(m, q0, t[i+0]) + C, z[0] = madd2(m, q1, t[i+1], C) + C, z[1] = madd2(m, q2, t[i+2], C) + z[3], z[2] = madd2(m, q3, t[i+3], C) + } + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } + // + + if mustNeg { + // We have computed ( 2⁶³ r + X ) r⁻¹ = 2⁶³ + X r⁻¹ instead + var b uint64 + z[0], b = bits.Sub64(z[0], signBitSelector, 0) + z[1], b = bits.Sub64(z[1], 0, b) + z[2], b = bits.Sub64(z[2], 0, b) + z[3], b = bits.Sub64(z[3], 0, b) + + // Occurs iff x == 0 && xHi < 0, i.e. X = rX' for -2⁶³ ≤ X' < 0 + + if b != 0 { + // z[3] = -1 + // negative: add q + const neg1 = 0xFFFFFFFFFFFFFFFF + + var carry uint64 + + z[0], carry = bits.Add64(z[0], q0, 0) + z[1], carry = bits.Add64(z[1], q1, carry) + z[2], carry = bits.Add64(z[2], q2, carry) + z[3], _ = bits.Add64(neg1, q3, carry) + } + } +} + +const ( + updateFactorsConversionBias int64 = 0x7fffffff7fffffff // (2³¹ - 1)(2³² + 1) + updateFactorIdentityMatrixRow0 = 1 + updateFactorIdentityMatrixRow1 = 1 << 32 +) + +func updateFactorsDecompose(c int64) (int64, int64) { + c += updateFactorsConversionBias + const low32BitsFilter int64 = 0xFFFFFFFF + f := c&low32BitsFilter - 0x7FFFFFFF + g := c>>32&low32BitsFilter - 0x7FFFFFFF + return f, g +} + +// negL negates in place [x | xHi] and return the new most significant word xHi +func negL(x *Element, xHi uint64) uint64 { + var b uint64 + + x[0], b = bits.Sub64(0, x[0], 0) + x[1], b = bits.Sub64(0, x[1], b) + x[2], b = bits.Sub64(0, x[2], b) + x[3], b = bits.Sub64(0, x[3], b) + xHi, _ = bits.Sub64(0, xHi, b) + + return xHi +} + +// mulWNonModular multiplies by one word in non-montgomery, without reducing +func (z *Element) mulWNonModular(x *Element, y int64) uint64 { + + // w := abs(y) + m := y >> 63 + w := uint64((y ^ m) - m) + + var c uint64 + c, z[0] = bits.Mul64(x[0], w) + c, z[1] = madd1(x[1], w, c) + c, z[2] = madd1(x[2], w, c) + c, z[3] = madd1(x[3], w, c) + + if y < 0 { + c = negL(z, c) + } + + return c +} + +// linearCombNonModular computes a linear combination without modular reduction +func (z *Element) linearCombNonModular(x *Element, xC int64, y *Element, yC int64) uint64 { + var yTimes Element + + yHi := yTimes.mulWNonModular(y, yC) + xHi := z.mulWNonModular(x, xC) + + var carry uint64 + z[0], carry = bits.Add64(z[0], yTimes[0], 0) + z[1], carry = bits.Add64(z[1], yTimes[1], carry) + z[2], carry = bits.Add64(z[2], yTimes[2], carry) + z[3], carry = bits.Add64(z[3], yTimes[3], carry) + + yHi, _ = bits.Add64(xHi, yHi, carry) + + return yHi +} diff --git a/ecc/kb8/fr/element_amd64.go b/ecc/kb8/fr/element_amd64.go new file mode 100644 index 0000000000..c2f1e3691d --- /dev/null +++ b/ecc/kb8/fr/element_amd64.go @@ -0,0 +1,62 @@ +//go:build !purego + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + _ "github.com/consensys/gnark-crypto/field/asm/element_4w" + "github.com/consensys/gnark-crypto/utils/cpu" +) + +var supportAdx = cpu.SupportADX + +//go:noescape +func MulBy3(x *Element) + +//go:noescape +func MulBy5(x *Element) + +//go:noescape +func MulBy13(x *Element) + +//go:noescape +func mul(res, x, y *Element) + +//go:noescape +func fromMont(res *Element) + +//go:noescape +func reduce(res *Element) + +// Butterfly sets +// +// a = a + b (mod q) +// b = a - b (mod q) +// +//go:noescape +func Butterfly(a, b *Element) + +// Mul z = x * y (mod q) +// +// x and y must be less than q +func (z *Element) Mul(x, y *Element) *Element { + + // Algorithm 2 of "Faster Montgomery Multiplication and Multi-Scalar-Multiplication for SNARKS" + // by Y. El Housni and G. Botrel https://doi.org/10.46586/tches.v2023.i3.504-521 + + mul(z, x, y) + return z +} + +// Square z = x * x (mod q) +// +// x must be less than q +func (z *Element) Square(x *Element) *Element { + // see Mul for doc. + mul(z, x, x) + return z +} diff --git a/ecc/kb8/fr/element_amd64.s b/ecc/kb8/fr/element_amd64.s new file mode 100644 index 0000000000..7c1e38abd2 --- /dev/null +++ b/ecc/kb8/fr/element_amd64.s @@ -0,0 +1,10 @@ +//go:build !purego + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +// We include the hash to force the Go compiler to recompile: 4712025900192836046 +#include "../../../field/asm/element_4w/element_4w_amd64.s" + diff --git a/ecc/kb8/fr/element_arm64.go b/ecc/kb8/fr/element_arm64.go new file mode 100644 index 0000000000..d4417ed826 --- /dev/null +++ b/ecc/kb8/fr/element_arm64.go @@ -0,0 +1,70 @@ +//go:build !purego + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + _ "github.com/consensys/gnark-crypto/field/asm/element_4w" +) + +// Butterfly sets +// +// a = a + b (mod q) +// b = a - b (mod q) +// +//go:noescape +func Butterfly(a, b *Element) + +//go:noescape +func mul(res, x, y *Element) + +// Mul z = x * y (mod q) +// +// x and y must be less than q +func (z *Element) Mul(x, y *Element) *Element { + mul(z, x, y) + return z +} + +// Square z = x * x (mod q) +// +// x must be less than q +func (z *Element) Square(x *Element) *Element { + // see Mul for doc. + mul(z, x, x) + return z +} + +// MulBy3 x *= 3 (mod q) +func MulBy3(x *Element) { + _x := *x + x.Double(x).Add(x, &_x) +} + +// MulBy5 x *= 5 (mod q) +func MulBy5(x *Element) { + _x := *x + x.Double(x).Double(x).Add(x, &_x) +} + +// MulBy13 x *= 13 (mod q) +func MulBy13(x *Element) { + var y = Element{ + 5113253463635402161, + 10092657615610122318, + 17591625363173919779, + 34317142035650752, + } + x.Mul(x, &y) +} + +func fromMont(z *Element) { + _fromMontGeneric(z) +} + +//go:noescape +func reduce(res *Element) diff --git a/ecc/kb8/fr/element_arm64.s b/ecc/kb8/fr/element_arm64.s new file mode 100644 index 0000000000..2cbfacdc7a --- /dev/null +++ b/ecc/kb8/fr/element_arm64.s @@ -0,0 +1,10 @@ +//go:build !purego + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +// We include the hash to force the Go compiler to recompile: 1501560133179981797 +#include "../../../field/asm/element_4w/element_4w_arm64.s" + diff --git a/ecc/kb8/fr/element_exp.go b/ecc/kb8/fr/element_exp.go new file mode 100644 index 0000000000..bcc5bc6d47 --- /dev/null +++ b/ecc/kb8/fr/element_exp.go @@ -0,0 +1,807 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +// ExpBySqrtExp is equivalent to z.Exp(x, f06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b904). +// It raises x to the (p-2^s-1)/2^(s+1) power using a shorter addition chain, +// where s the 2-adic valuation of p-1. +// +// uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain +func (z *Element) ExpBySqrtExp(x Element) *Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _100 = 1 + _11 + // _101 = 1 + _100 + // _1001 = _100 + _101 + // _1011 = _10 + _1001 + // _1101 = _10 + _1011 + // _10001 = _100 + _1101 + // _10011 = _10 + _10001 + // _10101 = _10 + _10011 + // _10111 = _10 + _10101 + // _11011 = _100 + _10111 + // _11110 = _11 + _11011 + // _11111 = 1 + _11110 + // _101001 = _1011 + _11110 + // _110101 = _10111 + _11110 + // _110111 = _10 + _110101 + // _111011 = _100 + _110111 + // _111101 = _10 + _111011 + // _1111000 = _111011 + _111101 + // _1111010 = _10 + _1111000 + // _1111111 = _101 + _1111010 + // i46 = ((_1111000 << 8 + _110111) << 7 + _10001) << 7 + // i67 = ((_1101 + i46) << 9 + _1011) << 9 + _10101 + // i92 = ((i67 << 7 + _101001) << 4 + 1) << 12 + // i108 = ((_111101 + i92) << 6 + _11111) << 7 + _10011 + // i126 = ((i108 << 7 + _101001) << 7 + _110101) << 2 + // i143 = ((_11 + i126) << 9 + _10111) << 5 + _1001 + // i168 = ((i143 << 10 + _1111111) << 3 + _11) << 10 + // i186 = ((_10111 + i168) << 9 + _111011) << 6 + _111011 + // i209 = ((i186 << 8 + _110101) << 6 + _10111) << 7 + // i229 = ((_111101 + i209) << 5 + _10111) << 12 + _110101 + // i253 = ((i229 << 3 + _101) << 12 + _1111111) << 7 + // i266 = ((_11011 + i253) << 7 + _111011) << 3 + _11 + // i289 = ((i266 << 12 + _110111) << 3 + 1) << 6 + // return (1 + i289) << 2 + // + // Operations: 238 squares 54 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16 Element + + // Step 1: t3 = x^0x2 + t3.Square(&x) + + // Step 2: t0 = x^0x3 + t0.Mul(&x, &t3) + + // Step 3: t1 = x^0x4 + t1.Mul(&x, &t0) + + // Step 4: t4 = x^0x5 + t4.Mul(&x, &t1) + + // Step 5: t8 = x^0x9 + t8.Mul(&t1, &t4) + + // Step 6: t13 = x^0xb + t13.Mul(&t3, &t8) + + // Step 7: t14 = x^0xd + t14.Mul(&t3, &t13) + + // Step 8: t15 = x^0x11 + t15.Mul(&t1, &t14) + + // Step 9: t10 = x^0x13 + t10.Mul(&t3, &t15) + + // Step 10: t12 = x^0x15 + t12.Mul(&t3, &t10) + + // Step 11: t6 = x^0x17 + t6.Mul(&t3, &t12) + + // Step 12: t2 = x^0x1b + t2.Mul(&t1, &t6) + + // Step 13: z = x^0x1e + z.Mul(&t0, &t2) + + // Step 14: t11 = x^0x1f + t11.Mul(&x, z) + + // Step 15: t9 = x^0x29 + t9.Mul(&t13, z) + + // Step 16: t5 = x^0x35 + t5.Mul(&t6, z) + + // Step 17: z = x^0x37 + z.Mul(&t3, &t5) + + // Step 18: t1 = x^0x3b + t1.Mul(&t1, z) + + // Step 19: t7 = x^0x3d + t7.Mul(&t3, &t1) + + // Step 20: t16 = x^0x78 + t16.Mul(&t1, &t7) + + // Step 21: t3 = x^0x7a + t3.Mul(&t3, &t16) + + // Step 22: t3 = x^0x7f + t3.Mul(&t4, &t3) + + // Step 30: t16 = x^0x7800 + for range 8 { + t16.Square(&t16) + } + + // Step 31: t16 = x^0x7837 + t16.Mul(z, &t16) + + // Step 38: t16 = x^0x3c1b80 + for range 7 { + t16.Square(&t16) + } + + // Step 39: t15 = x^0x3c1b91 + t15.Mul(&t15, &t16) + + // Step 46: t15 = x^0x1e0dc880 + for range 7 { + t15.Square(&t15) + } + + // Step 47: t14 = x^0x1e0dc88d + t14.Mul(&t14, &t15) + + // Step 56: t14 = x^0x3c1b911a00 + for range 9 { + t14.Square(&t14) + } + + // Step 57: t13 = x^0x3c1b911a0b + t13.Mul(&t13, &t14) + + // Step 66: t13 = x^0x783722341600 + for range 9 { + t13.Square(&t13) + } + + // Step 67: t12 = x^0x783722341615 + t12.Mul(&t12, &t13) + + // Step 74: t12 = x^0x3c1b911a0b0a80 + for range 7 { + t12.Square(&t12) + } + + // Step 75: t12 = x^0x3c1b911a0b0aa9 + t12.Mul(&t9, &t12) + + // Step 79: t12 = x^0x3c1b911a0b0aa90 + for range 4 { + t12.Square(&t12) + } + + // Step 80: t12 = x^0x3c1b911a0b0aa91 + t12.Mul(&x, &t12) + + // Step 92: t12 = x^0x3c1b911a0b0aa91000 + for range 12 { + t12.Square(&t12) + } + + // Step 93: t12 = x^0x3c1b911a0b0aa9103d + t12.Mul(&t7, &t12) + + // Step 99: t12 = x^0xf06e44682c2aa440f40 + for range 6 { + t12.Square(&t12) + } + + // Step 100: t11 = x^0xf06e44682c2aa440f5f + t11.Mul(&t11, &t12) + + // Step 107: t11 = x^0x78372234161552207af80 + for range 7 { + t11.Square(&t11) + } + + // Step 108: t10 = x^0x78372234161552207af93 + t10.Mul(&t10, &t11) + + // Step 115: t10 = x^0x3c1b911a0b0aa9103d7c980 + for range 7 { + t10.Square(&t10) + } + + // Step 116: t9 = x^0x3c1b911a0b0aa9103d7c9a9 + t9.Mul(&t9, &t10) + + // Step 123: t9 = x^0x1e0dc88d058554881ebe4d480 + for range 7 { + t9.Square(&t9) + } + + // Step 124: t9 = x^0x1e0dc88d058554881ebe4d4b5 + t9.Mul(&t5, &t9) + + // Step 126: t9 = x^0x78372234161552207af9352d4 + for range 2 { + t9.Square(&t9) + } + + // Step 127: t9 = x^0x78372234161552207af9352d7 + t9.Mul(&t0, &t9) + + // Step 136: t9 = x^0xf06e44682c2aa440f5f26a5ae00 + for range 9 { + t9.Square(&t9) + } + + // Step 137: t9 = x^0xf06e44682c2aa440f5f26a5ae17 + t9.Mul(&t6, &t9) + + // Step 142: t9 = x^0x1e0dc88d058554881ebe4d4b5c2e0 + for range 5 { + t9.Square(&t9) + } + + // Step 143: t8 = x^0x1e0dc88d058554881ebe4d4b5c2e9 + t8.Mul(&t8, &t9) + + // Step 153: t8 = x^0x78372234161552207af9352d70ba400 + for range 10 { + t8.Square(&t8) + } + + // Step 154: t8 = x^0x78372234161552207af9352d70ba47f + t8.Mul(&t3, &t8) + + // Step 157: t8 = x^0x3c1b911a0b0aa9103d7c9a96b85d23f8 + for range 3 { + t8.Square(&t8) + } + + // Step 158: t8 = x^0x3c1b911a0b0aa9103d7c9a96b85d23fb + t8.Mul(&t0, &t8) + + // Step 168: t8 = x^0xf06e44682c2aa440f5f26a5ae1748fec00 + for range 10 { + t8.Square(&t8) + } + + // Step 169: t8 = x^0xf06e44682c2aa440f5f26a5ae1748fec17 + t8.Mul(&t6, &t8) + + // Step 178: t8 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e00 + for range 9 { + t8.Square(&t8) + } + + // Step 179: t8 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3b + t8.Mul(&t1, &t8) + + // Step 185: t8 = x^0x78372234161552207af9352d70ba47f60b8ec0 + for range 6 { + t8.Square(&t8) + } + + // Step 186: t8 = x^0x78372234161552207af9352d70ba47f60b8efb + t8.Mul(&t1, &t8) + + // Step 194: t8 = x^0x78372234161552207af9352d70ba47f60b8efb00 + for range 8 { + t8.Square(&t8) + } + + // Step 195: t8 = x^0x78372234161552207af9352d70ba47f60b8efb35 + t8.Mul(&t5, &t8) + + // Step 201: t8 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd40 + for range 6 { + t8.Square(&t8) + } + + // Step 202: t8 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd57 + t8.Mul(&t6, &t8) + + // Step 209: t8 = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66ab80 + for range 7 { + t8.Square(&t8) + } + + // Step 210: t7 = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbd + t7.Mul(&t7, &t8) + + // Step 215: t7 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577a0 + for range 5 { + t7.Square(&t7) + } + + // Step 216: t6 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577b7 + t6.Mul(&t6, &t7) + + // Step 228: t6 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577b7000 + for range 12 { + t6.Square(&t6) + } + + // Step 229: t5 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577b7035 + t5.Mul(&t5, &t6) + + // Step 232: t5 = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81a8 + for range 3 { + t5.Square(&t5) + } + + // Step 233: t4 = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad + t4.Mul(&t4, &t5) + + // Step 245: t4 = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad000 + for range 12 { + t4.Square(&t4) + } + + // Step 246: t3 = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f + t3.Mul(&t3, &t4) + + // Step 253: t3 = x^0x78372234161552207af9352d70ba47f60b8efb355dedc0d683f80 + for range 7 { + t3.Square(&t3) + } + + // Step 254: t2 = x^0x78372234161552207af9352d70ba47f60b8efb355dedc0d683f9b + t2.Mul(&t2, &t3) + + // Step 261: t2 = x^0x3c1b911a0b0aa9103d7c9a96b85d23fb05c77d9aaef6e06b41fcd80 + for range 7 { + t2.Square(&t2) + } + + // Step 262: t1 = x^0x3c1b911a0b0aa9103d7c9a96b85d23fb05c77d9aaef6e06b41fcdbb + t1.Mul(&t1, &t2) + + // Step 265: t1 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577b7035a0fe6dd8 + for range 3 { + t1.Square(&t1) + } + + // Step 266: t0 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577b7035a0fe6ddb + t0.Mul(&t0, &t1) + + // Step 278: t0 = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577b7035a0fe6ddb000 + for range 12 { + t0.Square(&t0) + } + + // Step 279: z = x^0x1e0dc88d058554881ebe4d4b5c2e91fd82e3becd577b7035a0fe6ddb037 + z.Mul(z, &t0) + + // Step 282: z = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b8 + for range 3 { + z.Square(z) + } + + // Step 283: z = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b9 + z.Mul(&x, z) + + // Step 289: z = x^0x3c1b911a0b0aa9103d7c9a96b85d23fb05c77d9aaef6e06b41fcdbb606e40 + for range 6 { + z.Square(z) + } + + // Step 290: z = x^0x3c1b911a0b0aa9103d7c9a96b85d23fb05c77d9aaef6e06b41fcdbb606e41 + z.Mul(&x, z) + + // Step 292: z = x^0xf06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b904 + for range 2 { + z.Square(z) + } + + return z +} + +// ExpByCbrts1o3 is equivalent to z.Exp(x, 2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73). +// It raises x to the (s±1)/3 power using a shorter addition chain, +// where s is such that q-1 = 3^e * s with gcd(s, 3) = 1. +// +// uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain +func (z *Element) ExpByCbrts1o3(x Element) *Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _100 = 1 + _11 + // _111 = _11 + _100 + // _1001 = _10 + _111 + // _1101 = _100 + _1001 + // _1111 = _10 + _1101 + // _10001 = _10 + _1111 + // _10011 = _10 + _10001 + // _10101 = _10 + _10011 + // _10111 = _10 + _10101 + // _11001 = _10 + _10111 + // _11011 = _10 + _11001 + // _11101 = _10 + _11011 + // _11111 = _10 + _11101 + // _101110 = _1111 + _11111 + // _101111 = 1 + _101110 + // i35 = (2*(_101111 << 6 + _11111) + 1) << 9 + // i48 = ((_10011 + i35) << 4 + _1101) << 6 + _10101 + // i71 = ((i48 << 5 + _1111) << 10 + _11101) << 6 + // i88 = ((_11111 + i71) << 6 + _1111) << 8 + _1001 + // i108 = ((i88 << 7 + _11101) << 5 + _11011) << 6 + // i121 = ((_11011 + i108) << 5 + _11) << 5 + _11 + // i146 = ((i121 << 8 + _11101) << 10 + _10011) << 5 + // i161 = ((_11011 + i146) << 7 + _11111) << 5 + _111 + // i186 = ((i161 << 14 + _10111) << 5 + _10101) << 4 + // i201 = ((_1001 + i186) << 7 + _10011) << 5 + _10001 + // i218 = ((i201 << 6 + _10111) << 4 + _1001) << 5 + // i233 = ((_1111 + i218) << 6 + _11111) << 6 + _10111 + // i250 = ((i233 << 5 + _10111) << 6 + _10011) << 4 + // i266 = ((_111 + i250) << 8 + _10101) << 5 + _10101 + // i284 = ((i266 << 6 + _10101) << 5 + _10101) << 5 + // return 2*((_10001 + i284) << 5 + _11001) + 1 + // + // Operations: 237 squares 56 multiplies + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12 Element + + // Step 1: t5 = x^0x2 + t5.Square(&x) + + // Step 2: t10 = x^0x3 + t10.Mul(&x, &t5) + + // Step 3: z = x^0x4 + z.Mul(&x, &t10) + + // Step 4: t2 = x^0x7 + t2.Mul(&t10, z) + + // Step 5: t7 = x^0x9 + t7.Mul(&t5, &t2) + + // Step 6: t11 = x^0xd + t11.Mul(z, &t7) + + // Step 7: t6 = x^0xf + t6.Mul(&t5, &t11) + + // Step 8: t0 = x^0x11 + t0.Mul(&t5, &t6) + + // Step 9: t3 = x^0x13 + t3.Mul(&t5, &t0) + + // Step 10: t1 = x^0x15 + t1.Mul(&t5, &t3) + + // Step 11: t4 = x^0x17 + t4.Mul(&t5, &t1) + + // Step 12: z = x^0x19 + z.Mul(&t5, &t4) + + // Step 13: t8 = x^0x1b + t8.Mul(&t5, z) + + // Step 14: t9 = x^0x1d + t9.Mul(&t5, &t8) + + // Step 15: t5 = x^0x1f + t5.Mul(&t5, &t9) + + // Step 16: t12 = x^0x2e + t12.Mul(&t6, &t5) + + // Step 17: t12 = x^0x2f + t12.Mul(&x, &t12) + + // Step 23: t12 = x^0xbc0 + for range 6 { + t12.Square(&t12) + } + + // Step 24: t12 = x^0xbdf + t12.Mul(&t5, &t12) + + // Step 25: t12 = x^0x17be + t12.Square(&t12) + + // Step 26: t12 = x^0x17bf + t12.Mul(&x, &t12) + + // Step 35: t12 = x^0x2f7e00 + for range 9 { + t12.Square(&t12) + } + + // Step 36: t12 = x^0x2f7e13 + t12.Mul(&t3, &t12) + + // Step 40: t12 = x^0x2f7e130 + for range 4 { + t12.Square(&t12) + } + + // Step 41: t11 = x^0x2f7e13d + t11.Mul(&t11, &t12) + + // Step 47: t11 = x^0xbdf84f40 + for range 6 { + t11.Square(&t11) + } + + // Step 48: t11 = x^0xbdf84f55 + t11.Mul(&t1, &t11) + + // Step 53: t11 = x^0x17bf09eaa0 + for range 5 { + t11.Square(&t11) + } + + // Step 54: t11 = x^0x17bf09eaaf + t11.Mul(&t6, &t11) + + // Step 64: t11 = x^0x5efc27aabc00 + for range 10 { + t11.Square(&t11) + } + + // Step 65: t11 = x^0x5efc27aabc1d + t11.Mul(&t9, &t11) + + // Step 71: t11 = x^0x17bf09eaaf0740 + for range 6 { + t11.Square(&t11) + } + + // Step 72: t11 = x^0x17bf09eaaf075f + t11.Mul(&t5, &t11) + + // Step 78: t11 = x^0x5efc27aabc1d7c0 + for range 6 { + t11.Square(&t11) + } + + // Step 79: t11 = x^0x5efc27aabc1d7cf + t11.Mul(&t6, &t11) + + // Step 87: t11 = x^0x5efc27aabc1d7cf00 + for range 8 { + t11.Square(&t11) + } + + // Step 88: t11 = x^0x5efc27aabc1d7cf09 + t11.Mul(&t7, &t11) + + // Step 95: t11 = x^0x2f7e13d55e0ebe78480 + for range 7 { + t11.Square(&t11) + } + + // Step 96: t11 = x^0x2f7e13d55e0ebe7849d + t11.Mul(&t9, &t11) + + // Step 101: t11 = x^0x5efc27aabc1d7cf093a0 + for range 5 { + t11.Square(&t11) + } + + // Step 102: t11 = x^0x5efc27aabc1d7cf093bb + t11.Mul(&t8, &t11) + + // Step 108: t11 = x^0x17bf09eaaf075f3c24eec0 + for range 6 { + t11.Square(&t11) + } + + // Step 109: t11 = x^0x17bf09eaaf075f3c24eedb + t11.Mul(&t8, &t11) + + // Step 114: t11 = x^0x2f7e13d55e0ebe7849ddb60 + for range 5 { + t11.Square(&t11) + } + + // Step 115: t11 = x^0x2f7e13d55e0ebe7849ddb63 + t11.Mul(&t10, &t11) + + // Step 120: t11 = x^0x5efc27aabc1d7cf093bb6c60 + for range 5 { + t11.Square(&t11) + } + + // Step 121: t10 = x^0x5efc27aabc1d7cf093bb6c63 + t10.Mul(&t10, &t11) + + // Step 129: t10 = x^0x5efc27aabc1d7cf093bb6c6300 + for range 8 { + t10.Square(&t10) + } + + // Step 130: t9 = x^0x5efc27aabc1d7cf093bb6c631d + t9.Mul(&t9, &t10) + + // Step 140: t9 = x^0x17bf09eaaf075f3c24eedb18c7400 + for range 10 { + t9.Square(&t9) + } + + // Step 141: t9 = x^0x17bf09eaaf075f3c24eedb18c7413 + t9.Mul(&t3, &t9) + + // Step 146: t9 = x^0x2f7e13d55e0ebe7849ddb6318e8260 + for range 5 { + t9.Square(&t9) + } + + // Step 147: t8 = x^0x2f7e13d55e0ebe7849ddb6318e827b + t8.Mul(&t8, &t9) + + // Step 154: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d80 + for range 7 { + t8.Square(&t8) + } + + // Step 155: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d9f + t8.Mul(&t5, &t8) + + // Step 160: t8 = x^0x2f7e13d55e0ebe7849ddb6318e827b3e0 + for range 5 { + t8.Square(&t8) + } + + // Step 161: t8 = x^0x2f7e13d55e0ebe7849ddb6318e827b3e7 + t8.Mul(&t2, &t8) + + // Step 175: t8 = x^0xbdf84f55783af9e12776d8c63a09ecf9c000 + for range 14 { + t8.Square(&t8) + } + + // Step 176: t8 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017 + t8.Mul(&t4, &t8) + + // Step 181: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802e0 + for range 5 { + t8.Square(&t8) + } + + // Step 182: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f5 + t8.Mul(&t1, &t8) + + // Step 186: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f50 + for range 4 { + t8.Square(&t8) + } + + // Step 187: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f59 + t8.Mul(&t7, &t8) + + // Step 194: t8 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac80 + for range 7 { + t8.Square(&t8) + } + + // Step 195: t8 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac93 + t8.Mul(&t3, &t8) + + // Step 200: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f59260 + for range 5 { + t8.Square(&t8) + } + + // Step 201: t8 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f59271 + t8.Mul(&t0, &t8) + + // Step 207: t8 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c40 + for range 6 { + t8.Square(&t8) + } + + // Step 208: t8 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c57 + t8.Mul(&t4, &t8) + + // Step 212: t8 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c570 + for range 4 { + t8.Square(&t8) + } + + // Step 213: t7 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c579 + t7.Mul(&t7, &t8) + + // Step 218: t7 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af20 + for range 5 { + t7.Square(&t7) + } + + // Step 219: t6 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af2f + t6.Mul(&t6, &t7) + + // Step 225: t6 = x^0x2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbc0 + for range 6 { + t6.Square(&t6) + } + + // Step 226: t5 = x^0x2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf + t5.Mul(&t5, &t6) + + // Step 232: t5 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af2f7c0 + for range 6 { + t5.Square(&t5) + } + + // Step 233: t5 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af2f7d7 + t5.Mul(&t4, &t5) + + // Step 238: t5 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f592715e5efae0 + for range 5 { + t5.Square(&t5) + } + + // Step 239: t4 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f592715e5efaf7 + t4.Mul(&t4, &t5) + + // Step 245: t4 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdc0 + for range 6 { + t4.Square(&t4) + } + + // Step 246: t3 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdd3 + t3.Mul(&t3, &t4) + + // Step 250: t3 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdd30 + for range 4 { + t3.Square(&t3) + } + + // Step 251: t2 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdd37 + t2.Mul(&t2, &t3) + + // Step 259: t2 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdd3700 + for range 8 { + t2.Square(&t2) + } + + // Step 260: t2 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdd3715 + t2.Mul(&t1, &t2) + + // Step 265: t2 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af2f7d7ba6e2a0 + for range 5 { + t2.Square(&t2) + } + + // Step 266: t2 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af2f7d7ba6e2b5 + t2.Mul(&t1, &t2) + + // Step 272: t2 = x^0x2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad40 + for range 6 { + t2.Square(&t2) + } + + // Step 273: t2 = x^0x2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55 + t2.Mul(&t1, &t2) + + // Step 278: t2 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdd3715aaa0 + for range 5 { + t2.Square(&t2) + } + + // Step 279: t1 = x^0x5efc27aabc1d7cf093bb6c631d04f67ce00bd649c5797bebdd3715aab5 + t1.Mul(&t1, &t2) + + // Step 284: t1 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af2f7d7ba6e2b556a0 + for range 5 { + t1.Square(&t1) + } + + // Step 285: t0 = x^0xbdf84f55783af9e12776d8c63a09ecf9c017ac938af2f7d7ba6e2b556b1 + t0.Mul(&t0, &t1) + + // Step 290: t0 = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f592715e5efaf74dc56aad620 + for range 5 { + t0.Square(&t0) + } + + // Step 291: z = x^0x17bf09eaaf075f3c24eedb18c7413d9f3802f592715e5efaf74dc56aad639 + z.Mul(z, &t0) + + // Step 292: z = x^0x2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac72 + z.Square(z) + + // Step 293: z = x^0x2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 + z.Mul(&x, z) + + return z +} diff --git a/ecc/kb8/fr/element_purego.go b/ecc/kb8/fr/element_purego.go new file mode 100644 index 0000000000..799ff4497f --- /dev/null +++ b/ecc/kb8/fr/element_purego.go @@ -0,0 +1,391 @@ +//go:build purego || (!amd64 && !arm64) + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import "math/bits" + +// MulBy3 x *= 3 (mod q) +func MulBy3(x *Element) { + _x := *x + x.Double(x).Add(x, &_x) +} + +// MulBy5 x *= 5 (mod q) +func MulBy5(x *Element) { + _x := *x + x.Double(x).Double(x).Add(x, &_x) +} + +// MulBy13 x *= 13 (mod q) +func MulBy13(x *Element) { + var y = Element{ + 5113253463635402161, + 10092657615610122318, + 17591625363173919779, + 34317142035650752, + } + x.Mul(x, &y) +} + +func fromMont(z *Element) { + _fromMontGeneric(z) +} + +func reduce(z *Element) { + _reduceGeneric(z) +} + +// Mul z = x * y (mod q) +// +// x and y must be less than q +func (z *Element) Mul(x, y *Element) *Element { + + // Algorithm 2 of "Faster Montgomery Multiplication and Multi-Scalar-Multiplication for SNARKS" + // by Y. El Housni and G. Botrel https://doi.org/10.46586/tches.v2023.i3.504-521 + + var t0, t1, t2, t3 uint64 + var u0, u1, u2, u3 uint64 + { + var c0, c1, c2 uint64 + v := x[0] + u0, t0 = bits.Mul64(v, y[0]) + u1, t1 = bits.Mul64(v, y[1]) + u2, t2 = bits.Mul64(v, y[2]) + u3, t3 = bits.Mul64(v, y[3]) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, 0, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + { + var c0, c1, c2 uint64 + v := x[1] + u0, c1 = bits.Mul64(v, y[0]) + t0, c0 = bits.Add64(c1, t0, 0) + u1, c1 = bits.Mul64(v, y[1]) + t1, c0 = bits.Add64(c1, t1, c0) + u2, c1 = bits.Mul64(v, y[2]) + t2, c0 = bits.Add64(c1, t2, c0) + u3, c1 = bits.Mul64(v, y[3]) + t3, c0 = bits.Add64(c1, t3, c0) + + c2, _ = bits.Add64(0, 0, c0) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, c2, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + { + var c0, c1, c2 uint64 + v := x[2] + u0, c1 = bits.Mul64(v, y[0]) + t0, c0 = bits.Add64(c1, t0, 0) + u1, c1 = bits.Mul64(v, y[1]) + t1, c0 = bits.Add64(c1, t1, c0) + u2, c1 = bits.Mul64(v, y[2]) + t2, c0 = bits.Add64(c1, t2, c0) + u3, c1 = bits.Mul64(v, y[3]) + t3, c0 = bits.Add64(c1, t3, c0) + + c2, _ = bits.Add64(0, 0, c0) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, c2, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + { + var c0, c1, c2 uint64 + v := x[3] + u0, c1 = bits.Mul64(v, y[0]) + t0, c0 = bits.Add64(c1, t0, 0) + u1, c1 = bits.Mul64(v, y[1]) + t1, c0 = bits.Add64(c1, t1, c0) + u2, c1 = bits.Mul64(v, y[2]) + t2, c0 = bits.Add64(c1, t2, c0) + u3, c1 = bits.Mul64(v, y[3]) + t3, c0 = bits.Add64(c1, t3, c0) + + c2, _ = bits.Add64(0, 0, c0) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, c2, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + z[0] = t0 + z[1] = t1 + z[2] = t2 + z[3] = t3 + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } + return z +} + +// Square z = x * x (mod q) +// +// x must be less than q +func (z *Element) Square(x *Element) *Element { + // see Mul for algorithm documentation + + var t0, t1, t2, t3 uint64 + var u0, u1, u2, u3 uint64 + { + var c0, c1, c2 uint64 + v := x[0] + u0, t0 = bits.Mul64(v, x[0]) + u1, t1 = bits.Mul64(v, x[1]) + u2, t2 = bits.Mul64(v, x[2]) + u3, t3 = bits.Mul64(v, x[3]) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, 0, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + { + var c0, c1, c2 uint64 + v := x[1] + u0, c1 = bits.Mul64(v, x[0]) + t0, c0 = bits.Add64(c1, t0, 0) + u1, c1 = bits.Mul64(v, x[1]) + t1, c0 = bits.Add64(c1, t1, c0) + u2, c1 = bits.Mul64(v, x[2]) + t2, c0 = bits.Add64(c1, t2, c0) + u3, c1 = bits.Mul64(v, x[3]) + t3, c0 = bits.Add64(c1, t3, c0) + + c2, _ = bits.Add64(0, 0, c0) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, c2, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + { + var c0, c1, c2 uint64 + v := x[2] + u0, c1 = bits.Mul64(v, x[0]) + t0, c0 = bits.Add64(c1, t0, 0) + u1, c1 = bits.Mul64(v, x[1]) + t1, c0 = bits.Add64(c1, t1, c0) + u2, c1 = bits.Mul64(v, x[2]) + t2, c0 = bits.Add64(c1, t2, c0) + u3, c1 = bits.Mul64(v, x[3]) + t3, c0 = bits.Add64(c1, t3, c0) + + c2, _ = bits.Add64(0, 0, c0) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, c2, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + { + var c0, c1, c2 uint64 + v := x[3] + u0, c1 = bits.Mul64(v, x[0]) + t0, c0 = bits.Add64(c1, t0, 0) + u1, c1 = bits.Mul64(v, x[1]) + t1, c0 = bits.Add64(c1, t1, c0) + u2, c1 = bits.Mul64(v, x[2]) + t2, c0 = bits.Add64(c1, t2, c0) + u3, c1 = bits.Mul64(v, x[3]) + t3, c0 = bits.Add64(c1, t3, c0) + + c2, _ = bits.Add64(0, 0, c0) + t1, c0 = bits.Add64(u0, t1, 0) + t2, c0 = bits.Add64(u1, t2, c0) + t3, c0 = bits.Add64(u2, t3, c0) + c2, _ = bits.Add64(u3, c2, c0) + + m := qInvNeg * t0 + + u0, c1 = bits.Mul64(m, q0) + _, c0 = bits.Add64(t0, c1, 0) + u1, c1 = bits.Mul64(m, q1) + t0, c0 = bits.Add64(t1, c1, c0) + u2, c1 = bits.Mul64(m, q2) + t1, c0 = bits.Add64(t2, c1, c0) + u3, c1 = bits.Mul64(m, q3) + + t2, c0 = bits.Add64(0, c1, c0) + u3, _ = bits.Add64(u3, 0, c0) + t0, c0 = bits.Add64(u0, t0, 0) + t1, c0 = bits.Add64(u1, t1, c0) + t2, c0 = bits.Add64(u2, t2, c0) + c2, _ = bits.Add64(c2, 0, c0) + t2, c0 = bits.Add64(t3, t2, 0) + t3, _ = bits.Add64(u3, c2, c0) + + } + z[0] = t0 + z[1] = t1 + z[2] = t2 + z[3] = t3 + + // if z ⩾ q → z -= q + if !z.smallerThanModulus() { + var b uint64 + z[0], b = bits.Sub64(z[0], q0, 0) + z[1], b = bits.Sub64(z[1], q1, b) + z[2], b = bits.Sub64(z[2], q2, b) + z[3], _ = bits.Sub64(z[3], q3, b) + } + return z +} + +// Butterfly sets +// +// a = a + b (mod q) +// b = a - b (mod q) +func Butterfly(a, b *Element) { + _butterflyGeneric(a, b) +} diff --git a/ecc/kb8/fr/element_test.go b/ecc/kb8/fr/element_test.go new file mode 100644 index 0000000000..52f4ce3459 --- /dev/null +++ b/ecc/kb8/fr/element_test.go @@ -0,0 +1,2982 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "math/big" + "math/bits" + + mrand "math/rand" + + "testing" + + "github.com/leanovate/gopter" + ggen "github.com/leanovate/gopter/gen" + "github.com/leanovate/gopter/prop" + + "github.com/stretchr/testify/require" +) + +// ------------------------------------------------------------------------------------------------- +// benchmarks +// most benchmarks are rudimentary and should sample a large number of random inputs +// or be run multiple times to ensure it didn't measure the fastest path of the function + +var benchResElement Element + +func BenchmarkElementSelect(b *testing.B) { + var x, y Element + x.MustSetRandom() + y.MustSetRandom() + + b.ResetTimer() + for i := range b.N { + benchResElement.Select(i%3, &x, &y) + } +} + +func BenchmarkElementSetRandom(b *testing.B) { + var x Element + x.MustSetRandom() + + b.ResetTimer() + for range b.N { + x.MustSetRandom() + } +} + +func BenchmarkElementSetBytes(b *testing.B) { + var x Element + x.MustSetRandom() + bb := x.Bytes() + b.ResetTimer() + + for range b.N { + benchResElement.SetBytes(bb[:]) + } + +} + +func BenchmarkElementMulByConstants(b *testing.B) { + b.Run("mulBy3", func(b *testing.B) { + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + MulBy3(&benchResElement) + } + }) + b.Run("mulBy5", func(b *testing.B) { + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + MulBy5(&benchResElement) + } + }) + b.Run("mulBy13", func(b *testing.B) { + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + MulBy13(&benchResElement) + } + }) +} + +func BenchmarkElementInverse(b *testing.B) { + var x Element + x.MustSetRandom() + benchResElement.MustSetRandom() + b.ResetTimer() + + for range b.N { + benchResElement.Inverse(&x) + } + +} + +func BenchmarkElementButterfly(b *testing.B) { + var x Element + x.MustSetRandom() + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + Butterfly(&x, &benchResElement) + } +} + +func BenchmarkElementExp(b *testing.B) { + var x Element + x.MustSetRandom() + benchResElement.MustSetRandom() + b1, _ := rand.Int(rand.Reader, Modulus()) + b.ResetTimer() + for range b.N { + benchResElement.Exp(x, b1) + } +} + +func BenchmarkElementDouble(b *testing.B) { + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + benchResElement.Double(&benchResElement) + } +} + +func BenchmarkElementAdd(b *testing.B) { + var x Element + x.MustSetRandom() + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + benchResElement.Add(&x, &benchResElement) + } +} + +func BenchmarkElementSub(b *testing.B) { + var x Element + x.MustSetRandom() + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + benchResElement.Sub(&x, &benchResElement) + } +} + +func BenchmarkElementNeg(b *testing.B) { + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + benchResElement.Neg(&benchResElement) + } +} + +func BenchmarkElementDiv(b *testing.B) { + var x Element + x.MustSetRandom() + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + benchResElement.Div(&x, &benchResElement) + } +} + +func BenchmarkElementFromMont(b *testing.B) { + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + benchResElement.fromMont() + } +} + +func BenchmarkElementSquare(b *testing.B) { + benchResElement.MustSetRandom() + b.ResetTimer() + for range b.N { + benchResElement.Square(&benchResElement) + } +} + +func BenchmarkElementSqrt(b *testing.B) { + var a Element + a.MustSetRandom() + a.Square(&a) + b.ResetTimer() + 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{ + 12503850779977665488, + 4231521375444390721, + 12923916254582618147, + 4786565839245165, + } + benchResElement.SetOne() + b.ResetTimer() + for range b.N { + benchResElement.Mul(&benchResElement, &x) + } +} + +func BenchmarkElementCmp(b *testing.B) { + x := Element{ + 12503850779977665488, + 4231521375444390721, + 12923916254582618147, + 4786565839245165, + } + benchResElement = x + benchResElement[0] = 0 + b.ResetTimer() + for range b.N { + benchResElement.Cmp(&x) + } +} + +func TestElementCmp(t *testing.T) { + var x, y Element + + if x.Cmp(&y) != 0 { + t.Fatal("x == y") + } + + one := One() + y.Sub(&y, &one) + + if x.Cmp(&y) != -1 { + t.Fatal("x < y") + } + if y.Cmp(&x) != 1 { + t.Fatal("x < y") + } + + x = y + if x.Cmp(&y) != 0 { + t.Fatal("x == y") + } + + x.Sub(&x, &one) + if x.Cmp(&y) != -1 { + t.Fatal("x < y") + } + if y.Cmp(&x) != 1 { + t.Fatal("x < y") + } +} +func TestElementIsRandom(t *testing.T) { + for range 50 { + var x, y Element + x.MustSetRandom() + y.MustSetRandom() + if x.Equal(&y) { + t.Fatal("2 random numbers are unlikely to be equal") + } + } +} + +func TestElementIsUint64(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + properties.Property("reduce should output a result smaller than modulus", prop.ForAll( + func(v uint64) bool { + var e Element + e.SetUint64(v) + + if !e.IsUint64() { + return false + } + + return e.Uint64() == v + }, + ggen.UInt64(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementNegZero(t *testing.T) { + var a, b Element + b.SetZero() + for a.IsZero() { + a.MustSetRandom() + } + a.Neg(&b) + if !a.IsZero() { + t.Fatal("neg(0) != 0") + } +} + +// ------------------------------------------------------------------------------------------------- +// Gopter tests +// most of them are generated with a template + +const ( + nbFuzzShort = 200 + nbFuzz = 1000 +) + +// special values to be used in tests +var staticTestValues []Element + +func init() { + staticTestValues = append(staticTestValues, Element{}) // zero + staticTestValues = append(staticTestValues, One()) // one + staticTestValues = append(staticTestValues, rSquare) // r² + var e, one Element + one.SetOne() + e.Sub(&qElement, &one) + staticTestValues = append(staticTestValues, e) // q - 1 + e.Double(&one) + staticTestValues = append(staticTestValues, e) // 2 + + { + a := qElement + a[0]-- + staticTestValues = append(staticTestValues, a) + } + staticTestValues = append(staticTestValues, Element{0}) + staticTestValues = append(staticTestValues, Element{0, 0}) + staticTestValues = append(staticTestValues, Element{1}) + staticTestValues = append(staticTestValues, Element{0, 1}) + staticTestValues = append(staticTestValues, Element{2}) + staticTestValues = append(staticTestValues, Element{0, 2}) + + { + a := qElement + a[3]-- + staticTestValues = append(staticTestValues, a) + } + { + a := qElement + a[3]-- + a[0]++ + staticTestValues = append(staticTestValues, a) + } + + { + a := qElement + a[3] = 0 + staticTestValues = append(staticTestValues, a) + } + +} + +func TestElementReduce(t *testing.T) { + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + s := testValues[i] + expected := s + reduce(&s) + _reduceGeneric(&expected) + if !s.Equal(&expected) { + t.Fatal("reduce failed: asm and generic impl don't match") + } + } + + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := genFull() + + properties.Property("reduce should output a result smaller than modulus", prop.ForAll( + func(a Element) bool { + b := a + reduce(&a) + _reduceGeneric(&b) + return a.smallerThanModulus() && a.Equal(&b) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + +} + +func TestElementEqual(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + genB := gen() + + properties.Property("x.Equal(&y) iff x == y; likely false for random pairs", prop.ForAll( + func(a testPairElement, b testPairElement) bool { + return a.element.Equal(&b.element) == (a.element == b.element) + }, + genA, + genB, + )) + + properties.Property("x.Equal(&y) if x == y", prop.ForAll( + func(a testPairElement) bool { + b := a.element + return a.element.Equal(&b) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementBytes(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("SetBytes(Bytes()) should stay constant", prop.ForAll( + func(a testPairElement) bool { + var b Element + bytes := a.element.Bytes() + b.SetBytes(bytes[:]) + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("SetBytesCanonical(Bytes()) should stay constant", prop.ForAll( + func(a testPairElement) bool { + var b Element + bytes := a.element.Bytes() + if err := b.SetBytesCanonical(bytes[:]); err != nil { + t.Error(err) + return false + } + return a.element.Equal(&b) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementInverseExp(t *testing.T) { + // inverse must be equal to exp^-2 + exp := Modulus() + exp.Sub(exp, new(big.Int).SetUint64(2)) + + invMatchExp := func(a testPairElement) bool { + var b Element + b.Set(&a.element) + a.element.Inverse(&a.element) + b.Exp(b, exp) + + return a.element.Equal(&b) + } + + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + properties := gopter.NewProperties(parameters) + genA := gen() + properties.Property("inv == exp^-2", prop.ForAll(invMatchExp, genA)) + properties.TestingRun(t, gopter.ConsoleReporter(false)) + + parameters.MinSuccessfulTests = 1 + properties = gopter.NewProperties(parameters) + properties.Property("inv(0) == 0", prop.ForAll(invMatchExp, ggen.OneConstOf(testPairElement{}))) + properties.TestingRun(t, gopter.ConsoleReporter(false)) + +} + +func mulByConstant(z *Element, c uint8) { + var y Element + y.SetUint64(uint64(c)) + z.Mul(z, &y) +} + +func TestElementMulByConstants(t *testing.T) { + + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + + implemented := []uint8{0, 1, 2, 3, 5, 13} + properties.Property("mulByConstant", prop.ForAll( + func(a testPairElement) bool { + for _, c := range implemented { + var constant Element + constant.SetUint64(uint64(c)) + + b := a.element + b.Mul(&b, &constant) + + aa := a.element + mulByConstant(&aa, c) + + if !aa.Equal(&b) { + return false + } + } + + return true + }, + genA, + )) + + properties.Property("MulBy3(x) == Mul(x, 3)", prop.ForAll( + func(a testPairElement) bool { + var constant Element + constant.SetUint64(3) + + b := a.element + b.Mul(&b, &constant) + + MulBy3(&a.element) + + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("MulBy5(x) == Mul(x, 5)", prop.ForAll( + func(a testPairElement) bool { + var constant Element + constant.SetUint64(5) + + b := a.element + b.Mul(&b, &constant) + + MulBy5(&a.element) + + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("MulBy13(x) == Mul(x, 13)", prop.ForAll( + func(a testPairElement) bool { + var constant Element + constant.SetUint64(13) + + b := a.element + b.Mul(&b, &constant) + + MulBy13(&a.element) + + return a.element.Equal(&b) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + +} + +func TestElementLegendre(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("legendre should output same result than big.Int.Jacobi", prop.ForAll( + func(a testPairElement) bool { + return a.element.Legendre() == big.Jacobi(&a.bigint, Modulus()) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + + require.Equal(t, 0, new(Element).Legendre(), "(0|q) must be zero") +} + +func TestElementBitLen(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("BitLen should output same result than big.Int.BitLen", prop.ForAll( + func(a testPairElement) bool { + return a.element.fromMont().BitLen() == a.bigint.BitLen() + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementButterflies(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("butterfly0 == a -b; a +b", prop.ForAll( + func(a, b testPairElement) bool { + a0, b0 := a.element, b.element + + _butterflyGeneric(&a.element, &b.element) + Butterfly(&a0, &b0) + + return a.element.Equal(&a0) && b.element.Equal(&b0) + }, + genA, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + +} + +func TestElementLexicographicallyLargest(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("element.Cmp should match LexicographicallyLargest output", prop.ForAll( + func(a testPairElement) bool { + var negA Element + negA.Neg(&a.element) + + cmpResult := a.element.Cmp(&negA) + lResult := a.element.LexicographicallyLargest() + + if lResult && cmpResult == 1 { + return true + } + if !lResult && cmpResult != 1 { + return true + } + return false + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + +} + +func TestElementAdd(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + genB := gen() + + properties.Property("Add: having the receiver as operand should output the same result", prop.ForAll( + func(a, b testPairElement) bool { + var c, d Element + d.Set(&a.element) + + c.Add(&a.element, &b.element) + a.element.Add(&a.element, &b.element) + b.element.Add(&d, &b.element) + + return a.element.Equal(&b.element) && a.element.Equal(&c) && b.element.Equal(&c) + }, + genA, + genB, + )) + + properties.Property("Add: operation result must match big.Int result", prop.ForAll( + func(a, b testPairElement) bool { + { + var c Element + + c.Add(&a.element, &b.element) + + var d, e big.Int + d.Add(&a.bigint, &b.bigint).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + + // fixed elements + // a is random + // r takes special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + r := testValues[i] + var d, e, rb big.Int + r.BigInt(&rb) + + var c Element + c.Add(&a.element, &r) + d.Add(&a.bigint, &rb).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + return true + }, + genA, + genB, + )) + + properties.Property("Add: operation result must be smaller than modulus", prop.ForAll( + func(a, b testPairElement) bool { + var c Element + + c.Add(&a.element, &b.element) + + return c.smallerThanModulus() + }, + genA, + genB, + )) + + specialValueTest := func() { + // test special values against special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + a := testValues[i] + var aBig big.Int + a.BigInt(&aBig) + for j := range testValues { + b := testValues[j] + var bBig, d, e big.Int + b.BigInt(&bBig) + + var c Element + c.Add(&a, &b) + d.Add(&aBig, &bBig).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Add failed special test values") + } + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementSub(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + genB := gen() + + properties.Property("Sub: having the receiver as operand should output the same result", prop.ForAll( + func(a, b testPairElement) bool { + var c, d Element + d.Set(&a.element) + + c.Sub(&a.element, &b.element) + a.element.Sub(&a.element, &b.element) + b.element.Sub(&d, &b.element) + + return a.element.Equal(&b.element) && a.element.Equal(&c) && b.element.Equal(&c) + }, + genA, + genB, + )) + + properties.Property("Sub: operation result must match big.Int result", prop.ForAll( + func(a, b testPairElement) bool { + { + var c Element + + c.Sub(&a.element, &b.element) + + var d, e big.Int + d.Sub(&a.bigint, &b.bigint).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + + // fixed elements + // a is random + // r takes special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + r := testValues[i] + var d, e, rb big.Int + r.BigInt(&rb) + + var c Element + c.Sub(&a.element, &r) + d.Sub(&a.bigint, &rb).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + return true + }, + genA, + genB, + )) + + properties.Property("Sub: operation result must be smaller than modulus", prop.ForAll( + func(a, b testPairElement) bool { + var c Element + + c.Sub(&a.element, &b.element) + + return c.smallerThanModulus() + }, + genA, + genB, + )) + + specialValueTest := func() { + // test special values against special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + a := testValues[i] + var aBig big.Int + a.BigInt(&aBig) + for j := range testValues { + b := testValues[j] + var bBig, d, e big.Int + b.BigInt(&bBig) + + var c Element + c.Sub(&a, &b) + d.Sub(&aBig, &bBig).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Sub failed special test values") + } + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementMul(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + genB := gen() + + properties.Property("Mul: having the receiver as operand should output the same result", prop.ForAll( + func(a, b testPairElement) bool { + var c, d Element + d.Set(&a.element) + + c.Mul(&a.element, &b.element) + a.element.Mul(&a.element, &b.element) + b.element.Mul(&d, &b.element) + + return a.element.Equal(&b.element) && a.element.Equal(&c) && b.element.Equal(&c) + }, + genA, + genB, + )) + + properties.Property("Mul: operation result must match big.Int result", prop.ForAll( + func(a, b testPairElement) bool { + { + var c Element + + c.Mul(&a.element, &b.element) + + var d, e big.Int + d.Mul(&a.bigint, &b.bigint).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + + // fixed elements + // a is random + // r takes special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + r := testValues[i] + var d, e, rb big.Int + r.BigInt(&rb) + + var c Element + c.Mul(&a.element, &r) + d.Mul(&a.bigint, &rb).Mod(&d, Modulus()) + + // checking generic impl against asm path + var cGeneric Element + _mulGeneric(&cGeneric, &a.element, &r) + if !cGeneric.Equal(&c) { + // need to give context to failing error. + return false + } + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + return true + }, + genA, + genB, + )) + + properties.Property("Mul: operation result must be smaller than modulus", prop.ForAll( + func(a, b testPairElement) bool { + var c Element + + c.Mul(&a.element, &b.element) + + return c.smallerThanModulus() + }, + genA, + genB, + )) + + properties.Property("Mul: assembly implementation must be consistent with generic one", prop.ForAll( + func(a, b testPairElement) bool { + var c, d Element + c.Mul(&a.element, &b.element) + _mulGeneric(&d, &a.element, &b.element) + return c.Equal(&d) + }, + genA, + genB, + )) + + specialValueTest := func() { + // test special values against special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + a := testValues[i] + var aBig big.Int + a.BigInt(&aBig) + for j := range testValues { + b := testValues[j] + var bBig, d, e big.Int + b.BigInt(&bBig) + + var c Element + c.Mul(&a, &b) + d.Mul(&aBig, &bBig).Mod(&d, Modulus()) + + // checking asm against generic impl + var cGeneric Element + _mulGeneric(&cGeneric, &a, &b) + if !cGeneric.Equal(&c) { + t.Fatal("Mul failed special test values: asm and generic impl don't match") + } + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Mul failed special test values") + } + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementDiv(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + genB := gen() + + properties.Property("Div: having the receiver as operand should output the same result", prop.ForAll( + func(a, b testPairElement) bool { + var c, d Element + d.Set(&a.element) + + c.Div(&a.element, &b.element) + a.element.Div(&a.element, &b.element) + b.element.Div(&d, &b.element) + + return a.element.Equal(&b.element) && a.element.Equal(&c) && b.element.Equal(&c) + }, + genA, + genB, + )) + + properties.Property("Div: operation result must match big.Int result", prop.ForAll( + func(a, b testPairElement) bool { + { + var c Element + + c.Div(&a.element, &b.element) + + var d, e big.Int + d.ModInverse(&b.bigint, Modulus()) + d.Mul(&d, &a.bigint).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + + // fixed elements + // a is random + // r takes special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + r := testValues[i] + var d, e, rb big.Int + r.BigInt(&rb) + + var c Element + c.Div(&a.element, &r) + d.ModInverse(&rb, Modulus()) + d.Mul(&d, &a.bigint).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + return true + }, + genA, + genB, + )) + + properties.Property("Div: operation result must be smaller than modulus", prop.ForAll( + func(a, b testPairElement) bool { + var c Element + + c.Div(&a.element, &b.element) + + return c.smallerThanModulus() + }, + genA, + genB, + )) + + specialValueTest := func() { + // test special values against special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + a := testValues[i] + var aBig big.Int + a.BigInt(&aBig) + for j := range testValues { + b := testValues[j] + var bBig, d, e big.Int + b.BigInt(&bBig) + + var c Element + c.Div(&a, &b) + d.ModInverse(&bBig, Modulus()) + d.Mul(&d, &aBig).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Div failed special test values") + } + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementExp(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + genB := gen() + + properties.Property("Exp: having the receiver as operand should output the same result", prop.ForAll( + func(a, b testPairElement) bool { + var c, d Element + d.Set(&a.element) + + c.Exp(a.element, &b.bigint) + a.element.Exp(a.element, &b.bigint) + b.element.Exp(d, &b.bigint) + + return a.element.Equal(&b.element) && a.element.Equal(&c) && b.element.Equal(&c) + }, + genA, + genB, + )) + + properties.Property("Exp: operation result must match big.Int result", prop.ForAll( + func(a, b testPairElement) bool { + { + var c Element + + c.Exp(a.element, &b.bigint) + + var d, e big.Int + d.Exp(&a.bigint, &b.bigint, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + + // fixed elements + // a is random + // r takes special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + r := testValues[i] + var d, e, rb big.Int + r.BigInt(&rb) + + var c Element + c.Exp(a.element, &rb) + d.Exp(&a.bigint, &rb, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + return false + } + } + return true + }, + genA, + genB, + )) + + properties.Property("Exp: operation result must be smaller than modulus", prop.ForAll( + func(a, b testPairElement) bool { + var c Element + + c.Exp(a.element, &b.bigint) + + return c.smallerThanModulus() + }, + genA, + genB, + )) + + specialValueTest := func() { + // test special values against special values + testValues := make([]Element, len(staticTestValues)) + copy(testValues, staticTestValues) + + for i := range testValues { + a := testValues[i] + var aBig big.Int + a.BigInt(&aBig) + for j := range testValues { + b := testValues[j] + var bBig, d, e big.Int + b.BigInt(&bBig) + + var c Element + c.Exp(a, &bBig) + d.Exp(&aBig, &bBig, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Exp failed special test values") + } + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementSquare(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("Square: having the receiver as operand should output the same result", prop.ForAll( + func(a testPairElement) bool { + + var b Element + + b.Square(&a.element) + a.element.Square(&a.element) + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("Square: operation result must match big.Int result", prop.ForAll( + 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 + c.Square(&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 + c.Square(&a) + var d, e big.Int + d.Mul(&aBig, &aBig).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Square failed special test values") + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementInverse(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("Inverse: having the receiver as operand should output the same result", prop.ForAll( + func(a testPairElement) bool { + + var b Element + + b.Inverse(&a.element) + a.element.Inverse(&a.element) + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("Inverse: operation result must match big.Int result", prop.ForAll( + 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 + c.Inverse(&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 + c.Inverse(&a) + var d, e big.Int + d.ModInverse(&aBig, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Inverse failed special test values") + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementSqrt(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("Sqrt: having the receiver as operand should output the same result", prop.ForAll( + func(a testPairElement) bool { + + b := a.element + + b.Sqrt(&a.element) + a.element.Sqrt(&a.element) + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("Sqrt: operation result must match big.Int result", prop.ForAll( + 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 + c.Sqrt(&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 + c.Sqrt(&a) + var d, e big.Int + d.ModSqrt(&aBig, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Sqrt failed special test values") + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +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() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + + properties.Property("Double: having the receiver as operand should output the same result", prop.ForAll( + func(a testPairElement) bool { + + var b Element + + b.Double(&a.element) + a.element.Double(&a.element) + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("Double: operation result must match big.Int result", prop.ForAll( + 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 + c.Double(&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 + c.Double(&a) + var d, e big.Int + d.Lsh(&aBig, 1).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Double failed special test values") + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementNeg(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("Neg: having the receiver as operand should output the same result", prop.ForAll( + func(a testPairElement) bool { + + var b Element + + b.Neg(&a.element) + a.element.Neg(&a.element) + return a.element.Equal(&b) + }, + genA, + )) + + properties.Property("Neg: operation result must match big.Int result", prop.ForAll( + 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 + c.Neg(&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 + c.Neg(&a) + var d, e big.Int + d.Neg(&aBig).Mod(&d, Modulus()) + + if c.BigInt(&e).Cmp(&d) != 0 { + t.Fatal("Neg failed special test values") + } + } + } + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + specialValueTest() + +} + +func TestElementFixedExp(t *testing.T) { + + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + var _bSqrtExponentElement *big.Int + const sqrtExponentElement = "f06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b904" + _bSqrtExponentElement, _ = new(big.Int).SetString(sqrtExponentElement, 16) + + genA := gen() + + properties.Property(fmt.Sprintf("ExpBySqrtExp must match Exp(%s)", sqrtExponentElement), prop.ForAll( + func(a testPairElement) bool { + c := a.element + d := a.element + c.ExpBySqrtExp(c) + d.Exp(d, _bSqrtExponentElement) + return c.Equal(&d) + }, + genA, + )) + var _bCbrtExponentElement *big.Int + _bCbrtExponentElement, _ = new(big.Int).SetString("2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73", 16) + + properties.Property("ExpByCbrts1o3 must match Exp", prop.ForAll( + func(a testPairElement) bool { + c := a.element + d := a.element + c.ExpByCbrts1o3(c) + d.Exp(d, _bCbrtExponentElement) + return c.Equal(&d) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementHalve(t *testing.T) { + + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + var twoInv Element + twoInv.SetUint64(2) + twoInv.Inverse(&twoInv) + + properties.Property("z.Halve must match z / 2", prop.ForAll( + func(a testPairElement) bool { + c := a.element + d := a.element + c.Halve() + d.Mul(&d, &twoInv) + return c.Equal(&d) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func combineSelectionArguments(c int64, z int8) int { + if z%3 == 0 { + return 0 + } + return int(c) +} + +func TestElementSelect(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := genFull() + genB := genFull() + genC := ggen.Int64() //the condition + genZ := ggen.Int8() //to make zeros artificially more likely + + properties.Property("Select: must select correctly", prop.ForAll( + func(a, b Element, cond int64, z int8) bool { + condC := combineSelectionArguments(cond, z) + + var c Element + c.Select(condC, &a, &b) + + if condC == 0 { + return c.Equal(&a) + } + return c.Equal(&b) + }, + genA, + genB, + genC, + genZ, + )) + + properties.Property("Select: having the receiver as operand should output the same result", prop.ForAll( + func(a, b Element, cond int64, z int8) bool { + condC := combineSelectionArguments(cond, z) + + var c, d Element + d.Set(&a) + c.Select(condC, &a, &b) + a.Select(condC, &a, &b) + b.Select(condC, &d, &b) + return a.Equal(&b) && a.Equal(&c) && b.Equal(&c) + }, + genA, + genB, + genC, + genZ, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementSetInt64(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("z.SetInt64 must match z.SetString", prop.ForAll( + func(a testPairElement, v int64) bool { + c := a.element + d := a.element + + c.SetInt64(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, ggen.Int64(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementSetInterface(t *testing.T) { + + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + genInt := ggen.Int + genInt8 := ggen.Int8 + genInt16 := ggen.Int16 + genInt32 := ggen.Int32 + genInt64 := ggen.Int64 + + genUint := ggen.UInt + genUint8 := ggen.UInt8 + genUint16 := ggen.UInt16 + genUint32 := ggen.UInt32 + genUint64 := ggen.UInt64 + + properties.Property("z.SetInterface must match z.SetString with int8", prop.ForAll( + func(a testPairElement, v int8) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genInt8(), + )) + + properties.Property("z.SetInterface must match z.SetString with int16", prop.ForAll( + func(a testPairElement, v int16) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genInt16(), + )) + + properties.Property("z.SetInterface must match z.SetString with int32", prop.ForAll( + func(a testPairElement, v int32) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genInt32(), + )) + + properties.Property("z.SetInterface must match z.SetString with int64", prop.ForAll( + func(a testPairElement, v int64) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genInt64(), + )) + + properties.Property("z.SetInterface must match z.SetString with int", prop.ForAll( + func(a testPairElement, v int) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genInt(), + )) + + properties.Property("z.SetInterface must match z.SetString with uint8", prop.ForAll( + func(a testPairElement, v uint8) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genUint8(), + )) + + properties.Property("z.SetInterface must match z.SetString with uint16", prop.ForAll( + func(a testPairElement, v uint16) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genUint16(), + )) + + properties.Property("z.SetInterface must match z.SetString with uint32", prop.ForAll( + func(a testPairElement, v uint32) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genUint32(), + )) + + properties.Property("z.SetInterface must match z.SetString with uint64", prop.ForAll( + func(a testPairElement, v uint64) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genUint64(), + )) + + properties.Property("z.SetInterface must match z.SetString with uint", prop.ForAll( + func(a testPairElement, v uint) bool { + c := a.element + d := a.element + + c.SetInterface(v) + d.SetString(fmt.Sprintf("%v", v)) + + return c.Equal(&d) + }, + genA, genUint(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) + + { + assert := require.New(t) + var e Element + r, err := e.SetInterface(nil) + assert.Nil(r) + assert.Error(err) + + var ptE *Element + var ptB *big.Int + + r, err = e.SetInterface(ptE) + assert.Nil(r) + assert.Error(err) + ptE = new(Element).SetOne() + r, err = e.SetInterface(ptE) + assert.NoError(err) + assert.True(r.IsOne()) + + r, err = e.SetInterface(ptB) + assert.Nil(r) + assert.Error(err) + + } +} + +func TestElementNegativeExp(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("x⁻ᵏ == 1/xᵏ", prop.ForAll( + func(a, b testPairElement) bool { + + var nb, d, e big.Int + nb.Neg(&b.bigint) + + var c Element + c.Exp(a.element, &nb) + + d.Exp(&a.bigint, &nb, Modulus()) + + return c.BigInt(&e).Cmp(&d) == 0 + }, + genA, genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementNewElement(t *testing.T) { + assert := require.New(t) + + t.Parallel() + + e := NewElement(1) + assert.True(e.IsOne()) + + e = NewElement(0) + assert.True(e.IsZero()) +} + +func TestElementBatchInvert(t *testing.T) { + assert := require.New(t) + + t.Parallel() + + // ensure batchInvert([x]) == invert(x) + for i := int64(-1); i <= 2; i++ { + var e, eInv Element + e.SetInt64(i) + eInv.Inverse(&e) + + a := []Element{e} + aInv := BatchInvert(a) + + assert.True(aInv[0].Equal(&eInv), "batchInvert != invert") + + } + + // test x * x⁻¹ == 1 + tData := [][]int64{ + {-1, 1, 2, 3}, + {0, -1, 1, 2, 3, 0}, + {0, -1, 1, 0, 2, 3, 0}, + {-1, 1, 0, 2, 3}, + {0, 0, 1}, + {1, 0, 0}, + {0, 0, 0}, + } + + for _, t := range tData { + a := make([]Element, len(t)) + for i := range len(a) { + a[i].SetInt64(t[i]) + } + + aInv := BatchInvert(a) + + assert.True(len(aInv) == len(a)) + + for i := range len(a) { + if a[i].IsZero() { + assert.True(aInv[i].IsZero(), "0⁻¹ != 0") + } else { + assert.True(a[i].Mul(&a[i], &aInv[i]).IsOne(), "x * x⁻¹ != 1") + } + } + } + + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + + properties.Property("batchInvert --> x * x⁻¹ == 1", prop.ForAll( + func(tp testPairElement, r uint8) bool { + + a := make([]Element, r) + if r != 0 { + a[0] = tp.element + + } + one := One() + for i := 1; i < len(a); i++ { + a[i].Add(&a[i-1], &one) + } + + aInv := BatchInvert(a) + + assert.True(len(aInv) == len(a)) + + for i := range len(a) { + if a[i].IsZero() { + if !aInv[i].IsZero() { + return false + } + } else { + if !a[i].Mul(&a[i], &aInv[i]).IsOne() { + return false + } + } + } + return true + }, + genA, ggen.UInt8(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementFromMont(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("Assembly implementation must be consistent with generic one", prop.ForAll( + func(a testPairElement) bool { + c := a.element + d := a.element + c.fromMont() + _fromMontGeneric(&d) + return c.Equal(&d) + }, + genA, + )) + + properties.Property("x.fromMont().toMont() == x", prop.ForAll( + func(a testPairElement) bool { + c := a.element + c.fromMont().toMont() + return c.Equal(&a.element) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementJSON(t *testing.T) { + assert := require.New(t) + + type S struct { + A Element + B [3]Element + C *Element + D *Element + } + + // encode to JSON + var s S + s.A.SetString("-1") + s.B[2].SetUint64(42) + s.D = new(Element).SetUint64(8000) + + encoded, err := json.Marshal(&s) + assert.NoError(err) + // we may need to adjust "42" and "8000" values for some moduli; see Text() method for more details. + formatValue := func(v int64) string { + var a big.Int + a.SetInt64(v) + a.Mod(&a, Modulus()) + const maxUint16 = 65535 + var aNeg big.Int + aNeg.Neg(&a).Mod(&aNeg, Modulus()) + if aNeg.Uint64() != 0 && aNeg.Uint64() <= maxUint16 { + return "-" + aNeg.Text(10) + } + return a.Text(10) + } + expected := fmt.Sprintf("{\"A\":%s,\"B\":[0,0,%s],\"C\":null,\"D\":%s}", formatValue(-1), formatValue(42), formatValue(8000)) + assert.Equal(expected, string(encoded)) + + // decode valid + var decoded S + err = json.Unmarshal([]byte(expected), &decoded) + assert.NoError(err) + + assert.Equal(s, decoded, "element -> json -> element round trip failed") + + // decode hex and string values + withHexValues := "{\"A\":\"-1\",\"B\":[0,\"0x00000\",\"0x2A\"],\"C\":null,\"D\":\"8000\"}" + + var decodedS S + err = json.Unmarshal([]byte(withHexValues), &decodedS) + assert.NoError(err) + + assert.Equal(s, decodedS, " json with strings -> element failed") + +} + +type testPairElement struct { + element Element + bigint big.Int +} + +func gen() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var g testPairElement + + g.element = Element{ + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + } + if qElement[3] != ^uint64(0) { + g.element[3] %= (qElement[3] + 1) + } + + for !g.element.smallerThanModulus() { + g.element = Element{ + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + } + if qElement[3] != ^uint64(0) { + g.element[3] %= (qElement[3] + 1) + } + } + + g.element.BigInt(&g.bigint) + genResult := gopter.NewGenResult(g, gopter.NoShrinker) + return genResult + } +} + +func genRandomFq(genParams *gopter.GenParameters) Element { + var g Element + + g = Element{ + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + } + + if qElement[3] != ^uint64(0) { + g[3] %= (qElement[3] + 1) + } + + for !g.smallerThanModulus() { + g = Element{ + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + } + if qElement[3] != ^uint64(0) { + g[3] %= (qElement[3] + 1) + } + } + + return g +} + +func genFull() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + a := genRandomFq(genParams) + + var carry uint64 + a[0], carry = bits.Add64(a[0], qElement[0], carry) + a[1], carry = bits.Add64(a[1], qElement[1], carry) + a[2], carry = bits.Add64(a[2], qElement[2], carry) + a[3], _ = bits.Add64(a[3], qElement[3], carry) + + genResult := gopter.NewGenResult(a, gopter.NoShrinker) + return genResult + } +} + +func genElement() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + a := genRandomFq(genParams) + genResult := gopter.NewGenResult(a, gopter.NoShrinker) + return genResult + } +} + +func (z *Element) matchVeryBigInt(aHi uint64, aInt *big.Int) error { + var modulus big.Int + var aIntMod big.Int + modulus.SetInt64(1) + modulus.Lsh(&modulus, (Limbs+1)*64) + aIntMod.Mod(aInt, &modulus) + + slice := append(z[:], aHi) + + return bigIntMatchUint64Slice(&aIntMod, slice) +} + +// TODO: Phase out in favor of property based testing +func (z *Element) assertMatchVeryBigInt(t *testing.T, aHi uint64, aInt *big.Int) { + + if err := z.matchVeryBigInt(aHi, aInt); err != nil { + t.Error(err) + } +} + +// bigIntMatchUint64Slice is a test helper to match big.Int words against a uint64 slice +func bigIntMatchUint64Slice(aInt *big.Int, a []uint64) error { + + words := aInt.Bits() + + const steps = 64 / bits.UintSize + const filter uint64 = 0xFFFFFFFFFFFFFFFF >> (64 - bits.UintSize) + for i := range len(a) * steps { + + var wI big.Word + + if i < len(words) { + wI = words[i] + } + + aI := a[i/steps] >> ((i * bits.UintSize) % 64) + aI &= filter + + if uint64(wI) != aI { + return fmt.Errorf("bignum mismatch: disagreement on word %d: %x ≠ %x; %d ≠ %d", i, uint64(wI), aI, uint64(wI), aI) + } + } + + return nil +} + +func TestElementInversionApproximation(t *testing.T) { + var x Element + for range 1000 { + x.MustSetRandom() + + // Normally small elements are unlikely. Here we give them a higher chance + xZeros := mrand.Int() % Limbs //#nosec G404 weak rng is fine here + for j := 1; j < xZeros; j++ { + x[Limbs-j] = 0 + } + + a := approximate(&x, x.BitLen()) + aRef := approximateRef(&x) + + if a != aRef { + t.Error("Approximation mismatch") + } + } +} + +func TestElementInversionCorrectionFactorFormula(t *testing.T) { + const kLimbs = k * Limbs + const power = kLimbs*6 + invIterationsN*(kLimbs-k+1) + factorInt := big.NewInt(1) + factorInt.Lsh(factorInt, power) + factorInt.Mod(factorInt, Modulus()) + + var refFactorInt big.Int + inversionCorrectionFactor := Element{ + inversionCorrectionFactorWord0, + inversionCorrectionFactorWord1, + inversionCorrectionFactorWord2, + inversionCorrectionFactorWord3, + } + inversionCorrectionFactor.toBigInt(&refFactorInt) + + if refFactorInt.Cmp(factorInt) != 0 { + t.Error("mismatch") + } +} + +func TestElementLinearComb(t *testing.T) { + var x Element + var y Element + + for range 1000 { + x.MustSetRandom() + y.MustSetRandom() + testLinearComb(t, &x, mrand.Int63(), &y, mrand.Int63()) //#nosec G404 weak rng is fine here + } +} + +// Probably unnecessary post-dev. In case the output of inv is wrong, this checks whether it's only off by a constant factor. +func TestElementInversionCorrectionFactor(t *testing.T) { + + // (1/x)/inv(x) = (1/1)/inv(1) ⇔ inv(1) = x inv(x) + + var one Element + var oneInv Element + one.SetOne() + oneInv.Inverse(&one) + + for range 100 { + var x Element + var xInv Element + x.MustSetRandom() + xInv.Inverse(&x) + + x.Mul(&x, &xInv) + if !x.Equal(&oneInv) { + t.Error("Correction factor is inconsistent") + } + } + + if !oneInv.Equal(&one) { + var i big.Int + oneInv.BigInt(&i) // no montgomery + i.ModInverse(&i, Modulus()) + var fac Element + fac.setBigInt(&i) // back to montgomery + + var facTimesFac Element + facTimesFac.Mul(&fac, &Element{ + inversionCorrectionFactorWord0, + inversionCorrectionFactorWord1, + inversionCorrectionFactorWord2, + inversionCorrectionFactorWord3, + }) + + t.Error("Correction factor is consistently off by", fac, "Should be", facTimesFac) + } +} + +func TestElementBigNumNeg(t *testing.T) { + var a Element + aHi := negL(&a, 0) + if !a.IsZero() || aHi != 0 { + t.Error("-0 != 0") + } +} + +func TestElementBigNumWMul(t *testing.T) { + var x Element + + for range 1000 { + x.MustSetRandom() + w := mrand.Int63() //#nosec G404 weak rng is fine here + testBigNumWMul(t, &x, w) + } +} + +func TestElementVeryBigIntConversion(t *testing.T) { + xHi := mrand.Uint64() //#nosec G404 weak rng is fine here + var x Element + x.MustSetRandom() + var xInt big.Int + x.toVeryBigIntSigned(&xInt, xHi) + x.assertMatchVeryBigInt(t, xHi, &xInt) +} + +type veryBigInt struct { + asInt big.Int + low Element + hi uint64 +} + +// genVeryBigIntSigned if sign == 0, no sign is forced +func genVeryBigIntSigned(sign int) gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var g veryBigInt + + g.low = Element{ + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + } + + g.hi = genParams.NextUint64() + + if sign < 0 { + g.hi |= signBitSelector + } else if sign > 0 { + g.hi &= ^signBitSelector + } + + g.low.toVeryBigIntSigned(&g.asInt, g.hi) + + genResult := gopter.NewGenResult(g, gopter.NoShrinker) + return genResult + } +} + +func TestElementMontReduce(t *testing.T) { + + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + gen := genVeryBigIntSigned(0) + + properties.Property("Montgomery reduction is correct", prop.ForAll( + func(g veryBigInt) bool { + var res Element + var resInt big.Int + + montReduce(&resInt, &g.asInt) + res.montReduceSigned(&g.low, g.hi) + + return res.matchVeryBigInt(0, &resInt) == nil + }, + gen, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElementMontReduceMultipleOfR(t *testing.T) { + + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + gen := ggen.UInt64() + + properties.Property("Montgomery reduction is correct", prop.ForAll( + func(hi uint64) bool { + var zero, res Element + var asInt, resInt big.Int + + zero.toVeryBigIntSigned(&asInt, hi) + + montReduce(&resInt, &asInt) + res.montReduceSigned(&zero, hi) + + return res.matchVeryBigInt(0, &resInt) == nil + }, + gen, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestElement0Inverse(t *testing.T) { + var x Element + x.Inverse(&x) + if !x.IsZero() { + t.Fail() + } +} + +// TODO: Tests like this (update factor related) are common to all fields. Move them to somewhere non-autogen +func TestUpdateFactorSubtraction(t *testing.T) { + for i := range 1000 { + + f0, g0 := randomizeUpdateFactors() + f1, g1 := randomizeUpdateFactors() + + for f0-f1 > 1<<31 || f0-f1 <= -1<<31 { + f1 /= 2 + } + + for g0-g1 > 1<<31 || g0-g1 <= -1<<31 { + g1 /= 2 + } + + c0 := updateFactorsCompose(f0, g0) + c1 := updateFactorsCompose(f1, g1) + + cRes := c0 - c1 + fRes, gRes := updateFactorsDecompose(cRes) + + if fRes != f0-f1 || gRes != g0-g1 { + t.Error(i) + } + } +} + +func TestUpdateFactorsDouble(t *testing.T) { + for i := range 1000 { + f, g := randomizeUpdateFactors() + + if f > 1<<30 || f < (-1<<31+1)/2 { + f /= 2 + if g <= 1<<29 && g >= (-1<<31+1)/4 { + g *= 2 //g was kept small on f's account. Now that we're halving f, we can double g + } + } + + if g > 1<<30 || g < (-1<<31+1)/2 { + g /= 2 + + if f <= 1<<29 && f >= (-1<<31+1)/4 { + f *= 2 //f was kept small on g's account. Now that we're halving g, we can double f + } + } + + c := updateFactorsCompose(f, g) + cD := c * 2 + fD, gD := updateFactorsDecompose(cD) + + if fD != 2*f || gD != 2*g { + t.Error(i) + } + } +} + +func TestUpdateFactorsNeg(t *testing.T) { + var fMistake bool + for i := 0; i < 1000; i++ { + f, g := randomizeUpdateFactors() + + if f == 0x80000000 || g == 0x80000000 { + // Update factors this large can only have been obtained after 31 iterations and will therefore never be negated + // We don't have capacity to store -2³¹ + // Repeat this iteration + i-- + continue + } + + c := updateFactorsCompose(f, g) + nc := -c + nf, ng := updateFactorsDecompose(nc) + fMistake = fMistake || nf != -f + if nf != -f || ng != -g { + t.Errorf("Mismatch iteration #%d:\n%d, %d ->\n %d -> %d ->\n %d, %d\n Inputs in hex: %X, %X", + i, f, g, c, nc, nf, ng, f, g) + } + } + if fMistake { + t.Error("Mistake with f detected") + } else { + t.Log("All good with f") + } +} + +func TestUpdateFactorsNeg0(t *testing.T) { + c := updateFactorsCompose(0, 0) + t.Logf("c(0,0) = %X", c) + cn := -c + + if c != cn { + t.Error("Negation of zero update factors should yield the same result.") + } +} + +func TestUpdateFactorDecomposition(t *testing.T) { + var negSeen bool + + for range 1000 { + + f, g := randomizeUpdateFactors() + + if f <= -(1<<31) || f > 1<<31 { + t.Fatal("f out of range") + } + + negSeen = negSeen || f < 0 + + c := updateFactorsCompose(f, g) + + fBack, gBack := updateFactorsDecompose(c) + + if f != fBack || g != gBack { + t.Errorf("(%d, %d) -> %d -> (%d, %d)\n", f, g, c, fBack, gBack) + } + } + + if !negSeen { + t.Fatal("No negative f factors") + } +} + +func TestUpdateFactorInitialValues(t *testing.T) { + + f0, g0 := updateFactorsDecompose(updateFactorIdentityMatrixRow0) + f1, g1 := updateFactorsDecompose(updateFactorIdentityMatrixRow1) + + if f0 != 1 || g0 != 0 || f1 != 0 || g1 != 1 { + t.Error("Update factor initial value constants are incorrect") + } +} + +func TestUpdateFactorsRandomization(t *testing.T) { + var maxLen int + + //t.Log("|f| + |g| is not to exceed", 1 << 31) + for i := range 1000 { + f, g := randomizeUpdateFactors() + lf, lg := abs64T32(f), abs64T32(g) + absSum := lf + lg + if absSum >= 1<<31 { + + if absSum == 1<<31 { + maxLen++ + } else { + t.Error(i, "Sum of absolute values too large, f =", f, ",g =", g, ",|f| + |g| =", absSum) + } + } + } + + if maxLen == 0 { + t.Error("max len not observed") + } else { + t.Log(maxLen, "maxLens observed") + } +} + +func randomizeUpdateFactor(absLimit uint32) int64 { + const maxSizeLikelihood = 10 + maxSize := mrand.Intn(maxSizeLikelihood) //#nosec G404 weak rng is fine here + + absLimit64 := int64(absLimit) + var f int64 + switch maxSize { + case 0: + f = absLimit64 + case 1: + f = -absLimit64 + default: + f = int64(mrand.Uint64()%(2*uint64(absLimit64)+1)) - absLimit64 //#nosec G404 weak rng is fine here + } + + if f > 1<<31 { + return 1 << 31 + } else if f < -1<<31+1 { + return -1<<31 + 1 + } + + return f +} + +func abs64T32(f int64) uint32 { + if f >= 1<<32 || f < -1<<32 { + panic("f out of range") + } + + if f < 0 { + return uint32(-f) + } + return uint32(f) +} + +func randomizeUpdateFactors() (int64, int64) { + var f [2]int64 + b := mrand.Int() % 2 //#nosec G404 weak rng is fine here + + f[b] = randomizeUpdateFactor(1 << 31) + + //As per the paper, |f| + |g| \le 2³¹. + f[1-b] = randomizeUpdateFactor(1<<31 - abs64T32(f[b])) + + //Patching another edge case + if f[0]+f[1] == -1<<31 { + b = mrand.Int() % 2 //#nosec G404 weak rng is fine here + f[b]++ + } + + return f[0], f[1] +} + +func testLinearComb(t *testing.T, x *Element, xC int64, y *Element, yC int64) { + + var p1 big.Int + x.toBigInt(&p1) + p1.Mul(&p1, big.NewInt(xC)) + + var p2 big.Int + y.toBigInt(&p2) + p2.Mul(&p2, big.NewInt(yC)) + + p1.Add(&p1, &p2) + p1.Mod(&p1, Modulus()) + montReduce(&p1, &p1) + + var z Element + z.linearComb(x, xC, y, yC) + z.assertMatchVeryBigInt(t, 0, &p1) +} + +func testBigNumWMul(t *testing.T, a *Element, c int64) { + var aHi uint64 + var aTimes Element + aHi = aTimes.mulWNonModular(a, c) + + assertMulProduct(t, a, c, &aTimes, aHi) +} + +func updateFactorsCompose(f int64, g int64) int64 { + return f + g<<32 +} + +var rInv big.Int + +func montReduce(res *big.Int, x *big.Int) { + if rInv.BitLen() == 0 { // initialization + rInv.SetUint64(1) + rInv.Lsh(&rInv, Limbs*64) + rInv.ModInverse(&rInv, Modulus()) + } + res.Mul(x, &rInv) + res.Mod(res, Modulus()) +} + +func (z *Element) toVeryBigIntUnsigned(i *big.Int, xHi uint64) { + z.toBigInt(i) + var upperWord big.Int + upperWord.SetUint64(xHi) + upperWord.Lsh(&upperWord, Limbs*64) + i.Add(&upperWord, i) +} + +func (z *Element) toVeryBigIntSigned(i *big.Int, xHi uint64) { + z.toVeryBigIntUnsigned(i, xHi) + if signBitSelector&xHi != 0 { + twosCompModulus := big.NewInt(1) + twosCompModulus.Lsh(twosCompModulus, (Limbs+1)*64) + i.Sub(i, twosCompModulus) + } +} + +func assertMulProduct(t *testing.T, x *Element, c int64, result *Element, resultHi uint64) big.Int { + var xInt big.Int + x.toBigInt(&xInt) + + xInt.Mul(&xInt, big.NewInt(c)) + + result.assertMatchVeryBigInt(t, resultHi, &xInt) + return xInt +} + +func approximateRef(x *Element) uint64 { + + var asInt big.Int + x.toBigInt(&asInt) + n := x.BitLen() + + if n <= 64 { + return asInt.Uint64() + } + + modulus := big.NewInt(1 << 31) + var lo big.Int + lo.Mod(&asInt, modulus) + + modulus.Lsh(modulus, uint(n-64)) + var hi big.Int + hi.Div(&asInt, modulus) + hi.Lsh(&hi, 31) + + hi.Add(&hi, &lo) + return hi.Uint64() +} diff --git a/ecc/kb8/fr/vector.go b/ecc/kb8/fr/vector.go new file mode 100644 index 0000000000..c920dc1e17 --- /dev/null +++ b/ecc/kb8/fr/vector.go @@ -0,0 +1,394 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "math/bits" + "slices" + "strings" + "sync/atomic" + "unsafe" + + "github.com/consensys/gnark-crypto/parallel" +) + +// Vector represents a slice of Element. +// +// It implements the following interfaces: +// - Stringer +// - io.WriterTo +// - io.ReaderFrom +// - encoding.BinaryMarshaler +// - encoding.BinaryUnmarshaler +// - sort.Interface +type Vector []Element + +// MarshalBinary implements encoding.BinaryMarshaler +func (vector *Vector) MarshalBinary() (data []byte, err error) { + var buf bytes.Buffer + + if _, err = vector.WriteTo(&buf); err != nil { + return + } + return buf.Bytes(), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (vector *Vector) UnmarshalBinary(data []byte) error { + r := bytes.NewReader(data) + _, err := vector.ReadFrom(r) + return err +} + +// WriteTo implements io.WriterTo and writes a vector of big endian encoded Element. +// Length of the vector is encoded as a uint32 on the first 4 bytes. +func (vector *Vector) WriteTo(w io.Writer) (int64, error) { + // encode slice length + if err := binary.Write(w, binary.BigEndian, uint32(len(*vector))); err != nil { + return 0, err + } + + n := int64(4) + + var buf [Bytes]byte + for i := range len(*vector) { + BigEndian.PutElement(&buf, (*vector)[i]) + m, err := w.Write(buf[:]) + n += int64(m) + if err != nil { + return n, err + } + } + return n, nil +} + +// AsyncReadFrom implements an asynchronous version of [Vector.ReadFrom]. It +// reads the reader r in full and then performs the validation and conversion to +// Montgomery form separately in a goroutine. Any error encountered during +// reading is returned directly, while errors encountered during +// validation/conversion are sent on the returned channel. Thus the caller must +// wait on the channel to ensure the vector is ready to use. The method +// additionally returns the number of bytes read from r. +// +// The errors during reading can be: +// - an error while reading from r; +// - not enough bytes in r to read the full vector indicated by header. +// +// The reader can contain more bytes than needed to decode the vector, in which +// case the extra bytes are ignored. In that case the reader is not seeked nor +// read further. +// +// The method allocates sufficiently large slice to store the vector. If the +// current slice fits the vector, it is reused, otherwise the slice is grown to +// fit the vector. +// +// The serialized encoding is as follows: +// - first 4 bytes: length of the vector as a big-endian uint32 +// - for each element of the vector, [Bytes] bytes representing the element in +// big-endian encoding. +func (vector *Vector) AsyncReadFrom(r io.Reader) (int64, error, chan error) { // nolint ST1008 + chErr := make(chan error, 1) + var buf [Bytes]byte + if read, err := io.ReadFull(r, buf[:4]); err != nil { + close(chErr) + return int64(read), err, chErr + } + headerSliceLen := uint64(binary.BigEndian.Uint32(buf[:4])) + + // to avoid allocating too large slice when the header is tampered, we limit + // the maximum allocation. We set the target to 4GB. This incurs a performance + // hit when reading very large slices, but protects against OOM. + targetSize := uint64(1 << 32) // 4GB + if bits.UintSize == 32 { + // reduce target size to 1GB on 32 bits architectures + targetSize = uint64(1 << 30) // 1GB + } + maxAllocateSliceLength := targetSize / uint64(Bytes) + + totalRead := int64(4) + *vector = (*vector)[:0] + if headerSliceLen == 0 { + // if the vector was nil previously even by reslicing we have a nil vector. + // but we want to have an empty slice to indicate that the vector has zero length. + if *vector == nil { + *vector = []Element{} + } + // we return already here to avoid launching a goroutine doing nothing below + close(chErr) + return totalRead, nil, chErr + } + + for i := uint64(0); i < headerSliceLen; i += maxAllocateSliceLength { + if len(*vector) <= int(i) { + (*vector) = append(*vector, make([]Element, int(min(headerSliceLen-i, maxAllocateSliceLength)))...) + } + bSlice := unsafe.Slice((*byte)(unsafe.Pointer(&(*vector)[i])), int(min(headerSliceLen-i, maxAllocateSliceLength))*Bytes) + read, err := io.ReadFull(r, bSlice) + totalRead += int64(read) + if errors.Is(err, io.ErrUnexpectedEOF) { + close(chErr) + return totalRead, fmt.Errorf("less data than expected: read %d elements, expected %d", i+uint64(read)/Bytes, headerSliceLen), chErr + } + if err != nil { + close(chErr) + return totalRead, err, chErr + } + } + + bSlice := unsafe.Slice((*byte)(unsafe.Pointer(&(*vector)[0])), int(headerSliceLen)*Bytes) + go func() { + var cptErrors uint64 + // process the elements in parallel + parallel.Execute(int(headerSliceLen), func(start, end int) { + + var z Element + for i := start; i < end; i++ { + // we have to set vector[i] + bstart := i * Bytes + bend := bstart + Bytes + b := bSlice[bstart:bend] + z[0] = binary.BigEndian.Uint64(b[24:32]) + z[1] = binary.BigEndian.Uint64(b[16:24]) + z[2] = binary.BigEndian.Uint64(b[8:16]) + z[3] = binary.BigEndian.Uint64(b[0:8]) + + if !z.smallerThanModulus() { + atomic.AddUint64(&cptErrors, 1) + return + } + z.toMont() + (*vector)[i] = z + } + }) + + if cptErrors > 0 { + chErr <- fmt.Errorf("async read: %d elements failed validation", cptErrors) + } + close(chErr) + }() + return totalRead, nil, chErr +} + +// ReadFrom reads the vector from the reader r. It returns the number of bytes +// read and an error, if any. The errors can be: +// - an error while reading from r; +// - not enough bytes in r to read the full vector indicated by header; +// - when decoding the bytes into elements. +// +// The reader can contain more bytes than needed to decode the vector, in which case +// the extra bytes are ignored. In that case the reader is not seeked nor read further. +// +// The method allocates sufficiently large slice to store the vector. If the current slice fits +// the vector, it is reused, otherwise the slice is grown to fit the vector. +// +// The serialized encoding is as follows: +// - first 4 bytes: length of the vector as a big-endian uint32 +// - for each element of the vector, [Bytes] bytes representing the element in big-endian encoding. +// +// The method implements [io.ReaderFrom] interface. +func (vector *Vector) ReadFrom(r io.Reader) (int64, error) { + var buf [Bytes]byte + if read, err := io.ReadFull(r, buf[:4]); err != nil { + return int64(read), err + } + headerSliceLen := uint64(binary.BigEndian.Uint32(buf[:4])) + + // to avoid allocating too large slice when the header is tampered, we limit + // the maximum allocation. We set the target to 4GB. This incurs a performance + // hit when reading very large slices, but protects against OOM. + targetSize := uint64(1 << 32) // 4GB + if bits.UintSize == 32 { + // reduce target size to 1GB on 32 bits architectures + targetSize = uint64(1 << 30) // 1GB + } + maxAllocateSliceLength := targetSize / uint64(Bytes) + + totalRead := int64(4) // include already the header length + *vector = (*vector)[:0] + // if the vector was nil previously even by reslicing we have a nil vector. But we want + // to have an empty slice to indicate that the vector has zero length. When headerSliceLen == 0 + // we handle this edge case after reading the header as the loop body below is skipped. + if headerSliceLen == 0 && *vector == nil { + *vector = []Element{} + } + + for i := range headerSliceLen { + read, err := io.ReadFull(r, buf[:]) + totalRead += int64(read) + if errors.Is(err, io.ErrUnexpectedEOF) { + return totalRead, fmt.Errorf("less data than expected: read %d elements, expected %d", i, headerSliceLen) + } + if err != nil { + return totalRead, fmt.Errorf("error reading element %d: %w", i, err) + } + if uint64(cap(*vector)) <= i { + (*vector) = slices.Grow(*vector, int(min(headerSliceLen-i, maxAllocateSliceLength))) + } + el, err := BigEndian.Element(&buf) + if err != nil { + return totalRead, fmt.Errorf("error decoding element %d: %w", i, err) + } + *vector = append(*vector, el) + } + + return totalRead, nil +} + +// String implements fmt.Stringer interface +func (vector Vector) String() string { + var sbb strings.Builder + sbb.WriteByte('[') + for i := range len(vector) { + sbb.WriteString(vector[i].String()) + if i != len(vector)-1 { + sbb.WriteByte(',') + } + } + sbb.WriteByte(']') + return sbb.String() +} + +// Len is the number of elements in the collection. +func (vector Vector) Len() int { + return len(vector) +} + +// Less reports whether the element with +// index i should sort before the element with index j. +func (vector Vector) Less(i, j int) bool { + return vector[i].Cmp(&vector[j]) == -1 +} + +// Swap swaps the elements with indexes i and j. +func (vector Vector) Swap(i, j int) { + vector[i], vector[j] = vector[j], vector[i] +} + +// SetRandom sets the elements in vector to independent uniform random values in [0, q). +// +// This might error only if reading from crypto/rand.Reader errors, +// in which case the values in vector are undefined. +func (vector Vector) SetRandom() error { + for i := range vector { + if _, err := vector[i].SetRandom(); err != nil { + return err + } + } + return nil +} + +// Exp sets vector[i] = a[i]ᵏ for all i +func (vector Vector) Exp(a Vector, k int64) { + N := len(a) + if N != len(vector) { + panic("vector.Exp: vectors don't have the same length") + } + if k == 0 { + for i := range vector { + vector[i].SetOne() + } + return + } + base := a + exp := k + if k < 0 { + // call batch inverse + base = BatchInvert(a) + exp = -k // if k == math.MinInt64, -k overflows, but uint64(-k) is correct + } else if N > 0 { + // ensure that vector and a are not the same slice; else we need to copy a into base + v0 := &vector[0] // #nosec G602 we check that N > 0 above + a0 := &a[0] // #nosec G602 we check that N > 0 above + if v0 == a0 { + base = make(Vector, N) + copy(base, a) + } + } + + copy(vector, base) + + // Use bits.Len64 to iterate only over significant bits + for i := bits.Len64(uint64(exp)) - 2; i >= 0; i-- { + vector.Mul(vector, vector) + if (uint64(exp)>>uint(i))&1 != 0 { + vector.Mul(vector, base) + } + } +} + +// MustSetRandom sets the elements in vector to independent uniform random values in [0, q). +// +// It panics if reading from crypto/rand.Reader errors. +func (vector Vector) MustSetRandom() { + for i := range vector { + if _, err := vector[i].SetRandom(); err != nil { + panic(err) + } + } +} + +// Equal returns true if vector and other have the same length and same elements. +func (vector Vector) Equal(other Vector) bool { + return slices.Equal(vector, other) +} + +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 := range len(a) { + res[i].Add(&a[i], &b[i]) + } +} + +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 := range len(a) { + res[i].Sub(&a[i], &b[i]) + } +} + +func scalarMulVecGeneric(res, a Vector, b *Element) { + if len(a) != len(res) { + panic("vector.ScalarMul: vectors don't have the same length") + } + for i := range len(a) { + res[i].Mul(&a[i], b) + } +} + +func sumVecGeneric(res *Element, a Vector) { + for i := range len(a) { + res.Add(res, &a[i]) + } +} + +func innerProductVecGeneric(res *Element, a, b Vector) { + if len(a) != len(b) { + panic("vector.InnerProduct: vectors don't have the same length") + } + var tmp Element + for i := range len(a) { + tmp.Mul(&a[i], &b[i]) + res.Add(res, &tmp) + } +} + +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 := range len(a) { + res[i].Mul(&a[i], &b[i]) + } +} diff --git a/ecc/kb8/fr/vector_amd64.go b/ecc/kb8/fr/vector_amd64.go new file mode 100644 index 0000000000..076550760c --- /dev/null +++ b/ecc/kb8/fr/vector_amd64.go @@ -0,0 +1,164 @@ +//go:build !purego + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + _ "github.com/consensys/gnark-crypto/field/asm/element_4w" + "github.com/consensys/gnark-crypto/utils/cpu" +) + +// mu = 2^288 / q needed for partial Barrett reduction +const mu uint64 = 1170711311240 + +// AVX-512 IFMA constants for radix-52 Montgomery multiplication +// qInvNeg52 = qInvNeg & ((1<<52)-1) - low 52 bits of qInvNeg +const qInvNeg52 uint64 = 888145929477639 + +// muBarrett52 = floor(2^58 / (q >> 208)) for Barrett reduction +// Used to reduce from [0, 32q) to [0, q) in radix-52 Montgomery multiplication +const muBarrett52 uint64 = 279119 + +// q in radix-52 form (5 limbs), used by IFMA Montgomery multiplication +const qRadix52_0 uint64 = 2237982234611785 +const qRadix52_1 uint64 = 4335012459059920 +const qRadix52_2 uint64 = 3062640514176797 +const qRadix52_3 uint64 = 750159245813413 +const qRadix52_4 uint64 = 1032642127916 + +// Add adds two vectors element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) Add(a, b Vector) { + if len(a) != len(b) || len(a) != len(*vector) { + panic("vector.Add: vectors don't have the same length") + } + n := uint64(len(a)) + if n == 0 { + return + } + addVec(&(*vector)[0], &a[0], &b[0], n) +} + +//go:noescape +func addVec(res, a, b *Element, n uint64) + +// Sub subtracts two vectors element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) Sub(a, b Vector) { + if len(a) != len(b) || len(a) != len(*vector) { + panic("vector.Sub: vectors don't have the same length") + } + n := uint64(len(a)) + if n == 0 { + return + } + subVec(&(*vector)[0], &a[0], &b[0], n) +} + +//go:noescape +func subVec(res, a, b *Element, n uint64) + +// ScalarMul multiplies a vector by a scalar element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) ScalarMul(a Vector, b *Element) { + if len(a) != len(*vector) { + panic("vector.ScalarMul: vectors don't have the same length") + } + n := uint64(len(a)) + if n == 0 { + return + } + const maxN = (1 << 32) - 1 + // IFMA path (available on Ice Lake+, Zen4+) + // Uses AVX-512 IFMA instructions for fast vectorized multiplication + const blockSize = 8 + if cpu.SupportAVX512IFMA && n >= blockSize && n < maxN { + scalarMulVec(&(*vector)[0], &a[0], b, n/blockSize) + if n%blockSize != 0 { + start := n - n%blockSize + scalarMulVecGeneric((*vector)[start:], a[start:], b) + } + return + } + scalarMulVecGeneric(*vector, a, b) +} + +//go:noescape +func scalarMulVec(res, a, b *Element, n uint64) + +// Sum computes the sum of all elements in the vector. +func (vector *Vector) Sum() (res Element) { + n := uint64(len(*vector)) + if n == 0 { + return + } + const minN = 16 * 7 // AVX512 slower than generic for small n + const maxN = (1 << 32) - 1 + if !cpu.SupportAVX512 || n <= minN || n >= maxN { + // call sumVecGeneric + sumVecGeneric(&res, *vector) + return + } + sumVec(&res, &(*vector)[0], uint64(len(*vector))) + return +} + +//go:noescape +func sumVec(res *Element, a *Element, n uint64) + +// InnerProduct computes the inner product of two vectors. +// It panics if the vectors don't have the same length. +func (vector *Vector) InnerProduct(other Vector) (res Element) { + n := uint64(len(*vector)) + if n != uint64(len(other)) { + panic("vector.InnerProduct: vectors don't have the same length") + } + if n == 0 { + return + } + const maxN = (1 << 32) - 1 + if !cpu.SupportAVX512 || n >= maxN { + // call innerProductVecGeneric + // note; we could split the vector into smaller chunks and call innerProductVec + innerProductVecGeneric(&res, *vector, other) + return + } + innerProdVec(&res[0], &(*vector)[0], &other[0], uint64(len(*vector))) + + return +} + +//go:noescape +func innerProdVec(res *uint64, a, b *Element, n uint64) + +// Mul multiplies two vectors element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) Mul(a, b Vector) { + if len(a) != len(b) || len(a) != len(*vector) { + panic("vector.Mul: vectors don't have the same length") + } + n := uint64(len(a)) + if n == 0 { + return + } + const maxN = (1 << 32) - 1 + // IFMA path (available on Ice Lake+, Zen4+) + // Uses AVX-512 IFMA instructions for fast vectorized multiplication + const blockSize = 8 + if cpu.SupportAVX512IFMA && n >= blockSize && n < maxN { + mulVec(&(*vector)[0], &a[0], &b[0], n/blockSize) + if n%blockSize != 0 { + start := n - n%blockSize + mulVecGeneric((*vector)[start:], a[start:], b[start:]) + } + return + } + mulVecGeneric(*vector, a, b) +} + +//go:noescape +func mulVec(res, a, b *Element, n uint64) diff --git a/ecc/kb8/fr/vector_purego.go b/ecc/kb8/fr/vector_purego.go new file mode 100644 index 0000000000..b24444a6d8 --- /dev/null +++ b/ecc/kb8/fr/vector_purego.go @@ -0,0 +1,45 @@ +//go:build purego || !amd64 + +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +// Add adds two vectors element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) Add(a, b Vector) { + addVecGeneric(*vector, a, b) +} + +// Sub subtracts two vectors element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) Sub(a, b Vector) { + subVecGeneric(*vector, a, b) +} + +// ScalarMul multiplies a vector by a scalar element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) ScalarMul(a Vector, b *Element) { + scalarMulVecGeneric(*vector, a, b) +} + +// Sum computes the sum of all elements in the vector. +func (vector *Vector) Sum() (res Element) { + sumVecGeneric(&res, *vector) + return +} + +// InnerProduct computes the inner product of two vectors. +// It panics if the vectors don't have the same length. +func (vector *Vector) InnerProduct(other Vector) (res Element) { + innerProductVecGeneric(&res, *vector, other) + return +} + +// Mul multiplies two vectors element-wise and stores the result in self. +// It panics if the vectors don't have the same length. +func (vector *Vector) Mul(a, b Vector) { + mulVecGeneric(*vector, a, b) +} diff --git a/ecc/kb8/fr/vector_test.go b/ecc/kb8/fr/vector_test.go new file mode 100644 index 0000000000..3856fa79f8 --- /dev/null +++ b/ecc/kb8/fr/vector_test.go @@ -0,0 +1,926 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package fr + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/big" + "os" + "sort" + "sync" + "testing" + + "github.com/consensys/gnark-crypto/utils/cpu" + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/prop" + "github.com/stretchr/testify/require" +) + +func TestVectorSort(t *testing.T) { + assert := require.New(t) + + v := make(Vector, 3) + v[0].SetUint64(2) + v[1].SetUint64(3) + v[2].SetUint64(1) + + sort.Sort(v) + + assert.Equal("[1,2,3]", v.String()) +} + +func TestVectorRoundTrip(t *testing.T) { + assert := require.New(t) + + v1 := make(Vector, 3) + v1[0].SetUint64(2) + v1[1].SetUint64(3) + v1[2].SetUint64(1) + + b, err := v1.MarshalBinary() + assert.NoError(err) + + var v2, v3 Vector + + err = v2.UnmarshalBinary(b) + assert.NoError(err) + + err = v3.unmarshalBinaryAsync(b) + assert.NoError(err) + + assert.True(v1.Equal(v2), "vectors should be equal") + assert.True(v3.Equal(v2), "vectors should be equal") +} + +func TestVectorEmptyRoundTrip(t *testing.T) { + assert := require.New(t) + + v1 := make(Vector, 0) + + b, err := v1.MarshalBinary() + assert.NoError(err) + + var v2, v3 Vector + + err = v2.UnmarshalBinary(b) + assert.NoError(err) + + err = v3.unmarshalBinaryAsync(b) + assert.NoError(err) + + assert.True(v1.Equal(v2), "vectors should be equal") + assert.True(v3.Equal(v2), "vectors should be equal") +} + +func TestVectorEmptyOps(t *testing.T) { + assert := require.New(t) + + var sum, inner, scalar Element + scalar.SetUint64(42) + empty := make(Vector, 0) + result := make(Vector, 0) + + assert.NotPanics(func() { result.Add(empty, empty) }) + assert.NotPanics(func() { result.Sub(empty, empty) }) + assert.NotPanics(func() { result.ScalarMul(empty, &scalar) }) + assert.NotPanics(func() { result.Mul(empty, empty) }) + assert.NotPanics(func() { sum = empty.Sum() }) + assert.NotPanics(func() { inner = empty.InnerProduct(empty) }) + + assert.True(sum.IsZero()) + assert.True(inner.IsZero()) +} + +func (vector *Vector) unmarshalBinaryAsync(data []byte) error { + r := bytes.NewReader(data) + _, err, chErr := vector.AsyncReadFrom(r) + if err != nil { + return err + } + return <-chErr +} + +func deterministicVector(size int, seed uint64) Vector { + v := make(Vector, size) + var mixer, tweak Element + mixer.SetUint64(seed) + if mixer.IsZero() { + mixer.SetOne() + } + tweak.SetUint64(seed*17 + 3) + for i := range v { + v[i].SetUint64(uint64(i + 1)) + v[i].Mul(&v[i], &mixer) + v[i].Add(&v[i], &tweak) + mixer.Mul(&mixer, &tweak) + mixer.Add(&mixer, &v[i]) + tweak.Add(&tweak, &v[i]) + } + return v +} +func fuzzVectorSize(token uint16) int { + sizes := [...]int{0, 1, 2, 7, 8, 9, 15, 16, 31, 32, 63, 64, 65, 127, 128, 255, 256, 511, 512, 513} + return sizes[int(token)%len(sizes)] +} + +func fuzzVector(size int, seed uint64, mode byte) Vector { + v := deterministicVector(size, seed*2+1) + if size == 0 { + return v + } + + edges := ifmaEdgeElements() + switch mode & 0x3 { + case 0: + return v + case 1: + for i := range v { + v[i].Set(&edges[edgeValueIndex(seed, 1, 0, i, len(edges))]) + } + case 2: + for i := range v { + if i%3 == 0 { + v[i].Set(&edges[edgeValueIndex(seed, 5, 3, i, len(edges))]) + } + } + case 3: + var zero, one Element + one.SetOne() + for i := range v { + switch i % 4 { + case 0: + v[i].Set(&zero) + case 1: + v[i].Set(&one) + case 2: + v[i].Set(&edges[edgeValueIndex(seed, 7, 1, i, len(edges))]) + } + } + } + + return v +} + +func scalarVectorMulReference(a, b Vector) Vector { + res := make(Vector, len(a)) + for i := range a { + res[i].Mul(&a[i], &b[i]) + } + return res +} + +func scalarVectorSumReference(a Vector) (res Element) { + for i := range a { + res.Add(&res, &a[i]) + } + return +} + +func edgeValueIndex(seed, stride, offset uint64, i, size int) int { + return int((seed + uint64(i)*stride + offset) % uint64(size)) +} + +func scalarInnerProductReference(a, b Vector) (res Element) { + for i := range a { + var tmp Element + tmp.Mul(&a[i], &b[i]) + res.Add(&res, &tmp) + } + return +} + +var ( + cachedIFMAEdgeElements []Element + cachedIFMAEdgeElementsOnce sync.Once +) + +func ifmaEdgeElements() []Element { + cachedIFMAEdgeElementsOnce.Do(func() { + cachedIFMAEdgeElements = initIFMAEdgeElements() + }) + return cachedIFMAEdgeElements +} + +func initIFMAEdgeElements() []Element { + mod := Modulus() + values := []*big.Int{ + big.NewInt(0), + big.NewInt(1), + big.NewInt(2), + new(big.Int).Sub(mod, big.NewInt(1)), + new(big.Int).Sub(mod, big.NewInt(2)), + new(big.Int).Sub(mod, big.NewInt(3)), + } + for shift := 0; shift < Bits; shift += 17 { + x := new(big.Int).Lsh(big.NewInt(1), uint(shift)) + values = append(values, new(big.Int).Mod(x, mod)) + if x.Cmp(mod) < 0 { + values = append(values, new(big.Int).Sub(mod, x)) + } + } + res := make([]Element, len(values)) + for i := range values { + res[i].SetBigInt(values[i]) + } + return res +} + +func TestVectorIFMAEdgeCases(t *testing.T) { + if !cpu.SupportAVX512IFMA { + t.Skip("AVX-512 IFMA not available") + } + + assert := require.New(t) + edges := ifmaEdgeElements() + + for _, size := range []int{8, 64, 512} { + t.Run(fmt.Sprintf("size-%d", size), func(t *testing.T) { + a := make(Vector, size) + b := make(Vector, size) + for i := range a { + a[i].Set(&edges[i%len(edges)]) + b[i].Set(&edges[(i*7+3)%len(edges)]) + } + + wantMul := make(Vector, size) + gotMul := make(Vector, size) + for i := range a { + wantMul[i].Mul(&a[i], &b[i]) + } + gotMul.Mul(a, b) + assert.True(gotMul.Equal(wantMul), "Mul should match scalar element multiplication") + + wantScalar := make(Vector, size) + gotScalar := make(Vector, size) + for i := range a { + wantScalar[i].Mul(&a[i], &b[0]) + } + gotScalar.ScalarMul(a, &b[0]) + assert.True(gotScalar.Equal(wantScalar), "ScalarMul should match scalar element multiplication") + + var wantInner Element + for i := range a { + var tmp Element + tmp.Mul(&a[i], &b[i]) + wantInner.Add(&wantInner, &tmp) + } + gotInner := a.InnerProduct(b) + assert.True(gotInner.Equal(&wantInner), "InnerProduct should match scalar accumulation") + }) + } +} + +func FuzzVectorOps(f *testing.F) { + seeds := []struct { + sizeToken uint16 + modeA byte + modeB byte + seedA uint64 + seedB uint64 + seedS uint64 + }{ + {0, 0, 0, 1, 2, 3}, + {1, 1, 2, 5, 7, 11}, + {3, 2, 1, 13, 17, 19}, + {4, 1, 3, 23, 29, 31}, + {5, 3, 1, 37, 41, 43}, + {11, 2, 2, 47, 53, 59}, + {12, 1, 0, 61, 67, 71}, + {17, 3, 2, 73, 79, 83}, + {18, 1, 1, 89, 97, 101}, + {19, 2, 3, 103, 107, 109}, + } + for _, seed := range seeds { + f.Add(seed.sizeToken, seed.modeA, seed.modeB, seed.seedA, seed.seedB, seed.seedS) + } + + f.Fuzz(func(t *testing.T, sizeToken uint16, modeA, modeB byte, seedA, seedB, seedS uint64) { + size := fuzzVectorSize(sizeToken) + a := fuzzVector(size, seedA, modeA) + b := fuzzVector(size, seedB, modeB) + scalarVec := fuzzVector(1, seedS, modeA^modeB^0x3) + var scalar Element + if len(scalarVec) == 0 { + scalar.SetOne() + } else { + scalar.Set(&scalarVec[0]) + } + + gotMul := make(Vector, size) + gotMul.Mul(a, b) + for i := range a { + var want Element + want.Mul(&a[i], &b[i]) + if !gotMul[i].Equal(&want) { + t.Fatalf("Mul mismatch for size=%d index=%d modeA=%d modeB=%d seedA=%d seedB=%d", size, i, modeA, modeB, seedA, seedB) + } + } + + if size <= 128 { + gotMulAliasA := append(Vector(nil), a...) + gotMulAliasA.Mul(gotMulAliasA, b) + for i := range a { + var want Element + want.Mul(&a[i], &b[i]) + if !gotMulAliasA[i].Equal(&want) { + t.Fatalf("Mul alias res==a mismatch for size=%d index=%d", size, i) + } + } + + gotMulAliasB := append(Vector(nil), b...) + gotMulAliasB.Mul(a, gotMulAliasB) + for i := range a { + var want Element + want.Mul(&a[i], &b[i]) + if !gotMulAliasB[i].Equal(&want) { + t.Fatalf("Mul alias res==b mismatch for size=%d index=%d", size, i) + } + } + } + + gotScalar := make(Vector, size) + gotScalar.ScalarMul(a, &scalar) + for i := range a { + var want Element + want.Mul(&a[i], &scalar) + if !gotScalar[i].Equal(&want) { + t.Fatalf("ScalarMul mismatch for size=%d index=%d", size, i) + } + } + + if size <= 128 { + gotScalarAlias := append(Vector(nil), a...) + gotScalarAlias.ScalarMul(gotScalarAlias, &scalar) + for i := range a { + var want Element + want.Mul(&a[i], &scalar) + if !gotScalarAlias[i].Equal(&want) { + t.Fatalf("ScalarMul alias mismatch for size=%d index=%d", size, i) + } + } + } + + gotAdd := make(Vector, size) + gotAdd.Add(a, b) + gotSumAdd := gotAdd.Sum() + gotAdd.Sub(gotAdd, b) + if !gotAdd.Equal(a) { + t.Fatalf("Add/Sub round-trip mismatch for size=%d", size) + } + + expectedSumAdd := scalarVectorSumReference(a) + expectedSumB := scalarVectorSumReference(b) + expectedSumAdd.Add(&expectedSumAdd, &expectedSumB) + if !gotSumAdd.Equal(&expectedSumAdd) { + t.Fatalf("Sum linearity mismatch for size=%d", size) + } + + wantInner := scalarInnerProductReference(a, b) + gotInner := a.InnerProduct(b) + if !gotInner.Equal(&wantInner) { + t.Fatalf("InnerProduct mismatch for size=%d", size) + } + }) +} + +func TestVectorAliasing(t *testing.T) { + assert := require.New(t) + sizes := []int{1, 8, 9, 64, 65} + + for _, size := range sizes { + t.Run(fmt.Sprintf("size-%d", size), func(t *testing.T) { + a := deterministicVector(size, 11) + b := deterministicVector(size, 29) + var scalar Element + scalar.SetUint64(37) + + want := make(Vector, size) + got := make(Vector, size) + + want.Add(a, b) + copy(got, a) + got.Add(got, b) + assert.True(got.Equal(want), "Add should support res == a") + + copy(got, b) + got.Add(a, got) + assert.True(got.Equal(want), "Add should support res == b") + + want.Sub(a, b) + copy(got, a) + got.Sub(got, b) + assert.True(got.Equal(want), "Sub should support res == a") + + copy(got, b) + got.Sub(a, got) + assert.True(got.Equal(want), "Sub should support res == b") + + want.ScalarMul(a, &scalar) + copy(got, a) + got.ScalarMul(got, &scalar) + assert.True(got.Equal(want), "ScalarMul should support res == a") + + want.Mul(a, b) + copy(got, a) + got.Mul(got, b) + assert.True(got.Equal(want), "Mul should support res == a") + + copy(got, b) + got.Mul(a, got) + assert.True(got.Equal(want), "Mul should support res == b") + }) + } +} + +func TestVectorOps(t *testing.T) { + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = 2 + } else { + parameters.MinSuccessfulTests = 10 + } + properties := gopter.NewProperties(parameters) + + addVector := func(a, b Vector) bool { + c := make(Vector, len(a)) + c.Add(a, b) + + for i := range len(a) { + var tmp Element + tmp.Add(&a[i], &b[i]) + if !tmp.Equal(&c[i]) { + return false + } + } + return true + } + + subVector := func(a, b Vector) bool { + c := make(Vector, len(a)) + c.Sub(a, b) + + for i := range len(a) { + var tmp Element + tmp.Sub(&a[i], &b[i]) + if !tmp.Equal(&c[i]) { + return false + } + } + return true + } + + scalarMulVector := func(a Vector, b Element) bool { + c := make(Vector, len(a)) + c.ScalarMul(a, &b) + + for i := range len(a) { + var tmp Element + tmp.Mul(&a[i], &b) + if !tmp.Equal(&c[i]) { + return false + } + } + return true + } + + sumVector := func(a Vector) bool { + var sum Element + computed := a.Sum() + for i := range len(a) { + sum.Add(&sum, &a[i]) + } + + return sum.Equal(&computed) + } + + innerProductVector := func(a, b Vector) bool { + computed := a.InnerProduct(b) + var innerProduct Element + for i := range len(a) { + var tmp Element + tmp.Mul(&a[i], &b[i]) + innerProduct.Add(&innerProduct, &tmp) + } + + return innerProduct.Equal(&computed) + } + + mulVector := func(a, b Vector) bool { + c := make(Vector, len(a)) + a[0].SetUint64(0x24) + b[0].SetUint64(0x42) + c.Mul(a, b) + + for i := range len(a) { + var tmp Element + tmp.Mul(&a[i], &b[i]) + if !tmp.Equal(&c[i]) { + return false + } + } + return true + } + + sizes := []int{1, 2, 3, 4, 8, 9, 15, 16, 509, 510, 511, 512, 513, 514} + type genPair struct { + g1, g2 gopter.Gen + label string + } + + for _, size := range sizes { + generators := []genPair{ + {genZeroVector(size), genZeroVector(size), "zero vectors"}, + {genMaxVector(size), genMaxVector(size), "max vectors"}, + {genVector(size), genVector(size), "random vectors"}, + {genVector(size), genZeroVector(size), "random and zero vectors"}, + } + for _, gp := range generators { + properties.Property(fmt.Sprintf("vector addition %d - %s", size, gp.label), prop.ForAll( + addVector, + gp.g1, + gp.g2, + )) + + properties.Property(fmt.Sprintf("vector subtraction %d - %s", size, gp.label), prop.ForAll( + subVector, + gp.g1, + gp.g2, + )) + + properties.Property(fmt.Sprintf("vector scalar multiplication %d - %s", size, gp.label), prop.ForAll( + scalarMulVector, + gp.g1, + genElement(), + )) + + properties.Property(fmt.Sprintf("vector sum %d - %s", size, gp.label), prop.ForAll( + sumVector, + gp.g1, + )) + + properties.Property(fmt.Sprintf("vector inner product %d - %s", size, gp.label), prop.ForAll( + innerProductVector, + gp.g1, + gp.g2, + )) + + properties.Property(fmt.Sprintf("vector multiplication %d - %s", size, gp.label), prop.ForAll( + mulVector, + gp.g1, + gp.g2, + )) + } + } + + properties.TestingRun(t, gopter.NewFormatedReporter(false, 260, os.Stdout)) +} + +func BenchmarkVectorOps(b *testing.B) { + // note; to benchmark against "no asm" version, use the following + // build tag: -tags purego + const N = 1 << 24 + a1 := make(Vector, N) + b1 := make(Vector, N) + c1 := make(Vector, N) + var mixer Element + mixer.MustSetRandom() + for i := 1; i < N; i++ { + a1[i-1].SetUint64(uint64(i)). + Mul(&a1[i-1], &mixer) + b1[i-1].SetUint64(^uint64(i)). + Mul(&b1[i-1], &mixer) + } + + for n := 1 << 4; n <= N; n <<= 1 { + b.Run(fmt.Sprintf("add %d", n), func(b *testing.B) { + _a := a1[:n] + _b := b1[:n] + _c := c1[:n] + b.ResetTimer() + for range b.N { + _c.Add(_a, _b) + } + }) + + b.Run(fmt.Sprintf("sub %d", n), func(b *testing.B) { + _a := a1[:n] + _b := b1[:n] + _c := c1[:n] + b.ResetTimer() + for range b.N { + _c.Sub(_a, _b) + } + }) + + b.Run(fmt.Sprintf("scalarMul %d", n), func(b *testing.B) { + _a := a1[:n] + _c := c1[:n] + b.ResetTimer() + for range b.N { + _c.ScalarMul(_a, &mixer) + } + }) + + b.Run(fmt.Sprintf("sum %d", n), func(b *testing.B) { + _a := a1[:n] + b.ResetTimer() + for range b.N { + _ = _a.Sum() + } + }) + + b.Run(fmt.Sprintf("innerProduct %d", n), func(b *testing.B) { + _a := a1[:n] + _b := b1[:n] + b.ResetTimer() + for range b.N { + _ = _a.InnerProduct(_b) + } + }) + + b.Run(fmt.Sprintf("mul %d", n), func(b *testing.B) { + _a := a1[:n] + _b := b1[:n] + _c := c1[:n] + b.ResetTimer() + for range b.N { + _c.Mul(_a, _b) + } + }) + } +} + +func genZeroVector(size int) gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + g := make(Vector, size) + genResult := gopter.NewGenResult(g, gopter.NoShrinker) + return genResult + } +} + +func genMaxVector(size int) gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + g := make(Vector, size) + + qMinusOne := qElement + qMinusOne[0]-- + + for i := range size { + g[i] = qMinusOne + } + genResult := gopter.NewGenResult(g, gopter.NoShrinker) + return genResult + } +} + +func genVector(size int) gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + g := make(Vector, size) + mixer := Element{ + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + } + if qElement[3] != ^uint64(0) { + mixer[3] %= (qElement[3] + 1) + } + + for !mixer.smallerThanModulus() { + mixer = Element{ + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + genParams.NextUint64(), + } + if qElement[3] != ^uint64(0) { + mixer[3] %= (qElement[3] + 1) + } + } + + for i := 1; i <= size; i++ { + g[i-1].SetUint64(uint64(i)). + Mul(&g[i-1], &mixer) + } + + genResult := gopter.NewGenResult(g, gopter.NoShrinker) + return genResult + } +} + +func TestReadMismatchLength(t *testing.T) { + // ensure that the reader returns an error if the length encoded is larger than the actual + // input. + assert := require.New(t) + + v1 := make(Vector, 4) + v1.MustSetRandom() + + buf := new(bytes.Buffer) + _, err := v1.WriteTo(buf) + assert.NoError(err, "writing to buffer should not error out") + + // tamper with the length: set it to 10 + binary.BigEndian.PutUint32(buf.Bytes()[0:4], 10) + + var v2 Vector + _, err = v2.ReadFrom(buf) + assert.Error(err, "should error out as the length encoded is larger than the input") + var v3 Vector + err = v3.unmarshalBinaryAsync(buf.Bytes()) + assert.Error(err, "should error out as the length encoded is larger than the input") + var v4 Vector + err = v4.UnmarshalBinary(buf.Bytes()) + assert.Error(err, "should error out as the length encoded is larger than the input") +} + +func TestReadLargeHeader(t *testing.T) { + // skip the test. Running it on its own requires only up to 4GB of RAM, but + // we run tests in parallel in test suite. In that case the RAM usage blows + // up quickly and the test OOMs. + t.Skip("skipping test that requires large memory allocation") + + // if header is very large (128GB) we don't allocate it directly + // at once but rather in smaller chunks and then read it + assert := require.New(t) + + v1 := make(Vector, 4) + v1.MustSetRandom() + + buf := new(bytes.Buffer) + _, err := v1.WriteTo(buf) + assert.NoError(err, "writing to buffer should not error out") + bufBytes := buf.Bytes() + + // tamper with the length: set it to 2^32-1 + binary.BigEndian.PutUint32(bufBytes[0:4], ^uint32(0)) + var v2 Vector + _, err = v2.ReadFrom(bytes.NewBuffer(bufBytes)) + assert.Error(err, "should error out as the length encoded is very large") + var v3 Vector + _, err, errCh := v3.AsyncReadFrom(bytes.NewBuffer(bufBytes)) + assert.Error(err, "should error out as the length encoded is very large") + assert.NoError(<-errCh) + var v4 Vector + err = v4.UnmarshalBinary(bufBytes) + assert.Error(err, "should error out as the length encoded is very large") +} + +func TestReuseSliceDeserialization(t *testing.T) { + // test that when we deserialize into a preallocated slice, if the slice is + // large enough, we reuse it (and don't allocate a new one) + const ( + size = 1 << 16 + capacity = 1 << 20 + ) + assert := require.New(t) + + v1 := make(Vector, size) + v1.MustSetRandom() + + buf := new(bytes.Buffer) + _, err := v1.WriteTo(buf) + assert.NoError(err, "writing to buffer should not error out") + + bufBytes := buf.Bytes() + + v2 := make(Vector, capacity) + _, err = v2.ReadFrom(bytes.NewReader(bufBytes)) + assert.NoError(err, "should read without error") + assert.Equal(size, len(v2), "length of the slice should equal to the original one") + assert.Equal(capacity, cap(v2), "capacity of the slice should remain unchanged") + assert.True(v1.Equal(v2), "vectors should be equal") + v3 := make(Vector, capacity) + _, err, errCh := v3.AsyncReadFrom(bytes.NewReader(bufBytes)) + assert.NoError(err, "should read without error") + assert.NoError(<-errCh, "should validate without error") + assert.Equal(size, len(v3), "length of the slice should equal to the original one") + assert.Equal(capacity, cap(v3), "capacity of the slice should remain unchanged") + assert.True(v1.Equal(v3), "vectors should be equal") +} + +func TestVectorEqualityLarge(t *testing.T) { + // this test requires very large memory allocation which is slow and not possible in + // small machines. We skip the test even with no-short flag. I have run it locally and + // it passes (@ivokub) + t.Skip("skipping test that requires large memory allocation") + // tests that the vectors equality works for large vectors (with multiple allocations) + const size = 1 << 28 + assert := require.New(t) + + v1 := make(Vector, size) + v1.MustSetRandom() + + buf := new(bytes.Buffer) + _, err := v1.WriteTo(buf) + assert.NoError(err, "writing to buffer should not error out") + + bufBytes := buf.Bytes() + + var v2 Vector + _, err = v2.ReadFrom(bytes.NewReader(bufBytes)) + assert.NoError(err, "should read without error") + assert.True(v1.Equal(v2), "vectors should be equal") + + var v3 Vector + _, err, errCh := v3.AsyncReadFrom(bytes.NewReader(bufBytes)) + assert.NoError(err, "should read without error") + assert.NoError(<-errCh, "should validate without error") + assert.True(v1.Equal(v3), "vectors should be equal") + + v4 := make(Vector, size) + _, err = v4.ReadFrom(bytes.NewReader(bufBytes)) + assert.NoError(err, "should read without error") + assert.True(v1.Equal(v4), "vectors should be equal") + + v5 := make(Vector, size) + _, err, errCh = v5.AsyncReadFrom(bytes.NewReader(bufBytes)) + assert.NoError(err, "should read without error") + assert.NoError(<-errCh, "should validate without error") + assert.True(v1.Equal(v5), "vectors should be equal") +} + +func BenchmarkVectorReadFrom(b *testing.B) { + for _, size := range []int{5, 10, 15, 20, 24, 28} { + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + v1 := make(Vector, 1< + A, but A=0 here + T.Square(&M). + Sub(&T, &S). + Sub(&T, &S) + p.X.Set(&T) + p.Y.Sub(&S, &T). + Mul(&p.Y, &M) + YYYY.Double(&YYYY). + Double(&YYYY). + Double(&YYYY) + p.Y.Sub(&p.Y, &YYYY) + p.Z.Double(&a.Y) + + return p +} + +// AddMixed sets p to p+a in Jacobian coordinates, where a.Z = 1. +// +// http://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#addition-madd-2007-bl +// ~Cost: 7M + 4S +func (p *G1Jac) AddMixed(a *G1Affine) *G1Jac { + + //if a is infinity return p + if a.IsInfinity() { + return p + } + // p is infinity, return a + if p.Z.IsZero() { + p.X = a.X + p.Y = a.Y + p.Z.SetOne() + return p + } + + var Z1Z1, U2, S2, H, HH, I, J, r, V fptower.E8 + Z1Z1.Square(&p.Z) + U2.Mul(&a.X, &Z1Z1) + S2.Mul(&a.Y, &p.Z). + Mul(&S2, &Z1Z1) + + // if p == a, we double instead + if U2.Equal(&p.X) && S2.Equal(&p.Y) { + return p.DoubleMixed(a) + } + + H.Sub(&U2, &p.X) + HH.Square(&H) + I.Double(&HH).Double(&I) + J.Mul(&H, &I) + r.Sub(&S2, &p.Y).Double(&r) + V.Mul(&p.X, &I) + p.X.Square(&r). + Sub(&p.X, &J). + Sub(&p.X, &V). + Sub(&p.X, &V) + J.Mul(&J, &p.Y).Double(&J) + p.Y.Sub(&V, &p.X). + Mul(&p.Y, &r) + p.Y.Sub(&p.Y, &J) + p.Z.Add(&p.Z, &H) + p.Z.Square(&p.Z). + Sub(&p.Z, &Z1Z1). + Sub(&p.Z, &HH) + + return p +} + +// Double sets p to [2]q in Jacobian coordinates. +// +// https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l +// ~Cost: 2M + 5S +func (p *G1Jac) Double(q *G1Jac) *G1Jac { + p.Set(q) + p.DoubleAssign() + return p +} + +// DoubleAssign doubles p in Jacobian coordinates. +// +// https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l +// ~Cost: 2M + 5S +func (p *G1Jac) DoubleAssign() *G1Jac { + if p.Z.IsZero() || p.Y.IsZero() { + return p.Set(&g1Infinity) + } + var a G1Affine + a.FromJacobian(p) + return p.DoubleMixed(&a) + var A, B, C, D, E, F, t fptower.E8 + A.Square(&p.X) + B.Square(&p.Y) + C.Square(&B) + D.Add(&p.X, &B). + Square(&D). + Sub(&D, &A). + Sub(&D, &C). + Double(&D) + E.Double(&A). + Add(&E, &A) + F.Square(&E) + t.Double(&D) + p.Z.Mul(&p.Y, &p.Z). + Double(&p.Z) + p.X.Sub(&F, &t) + p.Y.Sub(&D, &p.X). + Mul(&p.Y, &E) + t.Double(&C). + Double(&t). + Double(&t) + p.Y.Sub(&p.Y, &t) + + return p +} + +// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +func (p *G1Jac) Triple(q *G1Jac) *G1Jac { + var dbl G1Jac + dbl.Double(q) + p.Set(&dbl).AddAssign(q) + return p + // Helper functions for multiplication by 3 and 4. + mulBy3 := func(v *fptower.E8) { + tmp := *v + v.Double(v) + v.Add(v, &tmp) + } + mulBy4 := func(v *fptower.E8) { + v.Double(v).Double(v) + } + + // --- Step 1: Compute initial terms from input q --- + var X3, Y2, XZ fptower.E8 + X3.Square(&q.X) // X3 = q.X^2 + Y2.Square(&q.Y) // Y2 = q.Y^2 + X3.Mul(&X3, &q.X) // X3 = q.X^3 + XZ.Mul(&q.X, &q.Z) // XZ = q.X * q.Z + + // --- Step 2: Compute the X-coordinate of an intermediate point τ --- + // Calculates Xτ = 4*q.Y^2 - 3*q.X^3. + // The variable p.Z is used for temporary storage and finalized in Step 6. + mulBy3(&X3) // X3 = 3*q.X^3 + mulBy4(&Y2) // Y2 = 4*q.Y^2 + var Xτ fptower.E8 + Xτ.Sub(&Y2, &X3) + p.Z.Mul(&Xτ, &XZ) // p.Z = Xτ * (q.X * q.Z) + + // --- Step 3: Compute the Y-coordinate of the intermediate point τ --- + // Calculates Yτ = q.Y * (9*q.X^3 - 8*q.Y^2). + // Reuses X3 and Y2 from previous steps. + mulBy3(&X3) // X3 = 9*q.X^3 + var Yτ fptower.E8 + Yτ.Double(&Y2) // Yτ = 8*q.Y^2 + Yτ.Sub(&X3, &Yτ).Mul(&Yτ, &q.Y) + + // --- Step 4: Compute powers of the intermediate point's coordinates --- + var Xτ2, Xτ3, Yτ2 fptower.E8 + Xτ2.Square(&Xτ) // Xτ2 = Xτ^2 + Xτ3.Mul(&Xτ2, &Xτ) // Xτ3 = Xτ^3 + Yτ2.Square(&Yτ) // Yτ2 = Yτ^2 + + // --- Step 5: Compute the final X and Y coordinates of the result [3]q --- + // This step re-applies the same transformation using (Xτ, Yτ) as input. + // p.X = 4*Yτ^2 - 3*Xτ^3 + mulBy3(&Xτ3) // Xτ3 = 3*Xτ^3 + mulBy4(&Yτ2) // Yτ2 = 4*Yτ^2 + p.X.Sub(&Yτ2, &Xτ3) + + // p.Y = Yτ * (9*Xτ^3 - 8*Yτ^2) + // Reuses Xτ3 and Yτ2 from the previous calculation. + mulBy3(&Xτ3) // Xτ3 = 9*Xτ^3 + Yτ2.Double(&Yτ2) // Yτ2 = 8*Yτ^2 + p.Y.Sub(&Xτ3, &Yτ2).Mul(&p.Y, &Yτ) + + // --- Step 6: Finalize the Z-coordinate --- + // p.Z = 3 * p.Z = 3 * Xτ * (q.X * q.Z) + mulBy3(&p.Z) + + return p +} + +// ScalarMultiplication computes and returns p = [s]a +// where p and a are Jacobian points. +// using a 2-bits windowed double-and-add method. +func (p *G1Jac) ScalarMultiplication(q *G1Jac, s *big.Int) *G1Jac { + return p.mulWindowed(q, s) +} + +// ScalarMultiplicationBase computes and returns p = [s]g +// where g is the prime subgroup generator. +func (p *G1Jac) ScalarMultiplicationBase(s *big.Int) *G1Jac { + return p.mulWindowed(&g1Gen, s) + +} + +// String converts p to affine coordinates and returns its string representation E(x,y) or "O" if it is infinity. +func (p *G1Jac) String() string { + _p := G1Affine{} + _p.FromJacobian(p) + return _p.String() +} + +// FromAffine converts a point a from affine to Jacobian coordinates. +func (p *G1Jac) FromAffine(a *G1Affine) *G1Jac { + if a.IsInfinity() { + p.Z.SetZero() + p.X.SetOne() + p.Y.SetOne() + return p + } + p.Z.SetOne() + p.X.Set(&a.X) + p.Y.Set(&a.Y) + return p +} + +// IsOnCurve returns true if the Jacobian point p in on the curve. +func (p *G1Jac) IsOnCurve() bool { + var left, right, tmp, ZZ fptower.E8 + left.Square(&p.Y) + right.Square(&p.X).Mul(&right, &p.X) + ZZ.Square(&p.Z) + var Z4 fptower.E8 + Z4.Square(&ZZ) + tmp.Mul(&p.X, &Z4).Mul(&tmp, &aCurveCoeff) + right.Add(&right, &tmp) + tmp.Square(&ZZ).Mul(&tmp, &ZZ) + tmp.Mul(&tmp, &bCurveCoeff) + right.Add(&right, &tmp) + return left.Equal(&right) +} + +// IsInSubGroup returns true if p is on the r-torsion, false otherwise. +// Z[r,0]+Z[-lambdaG1Affine, 1] is the kernel +// of (u,v)->u+lambdaG1Affinev mod r. Expressing r, lambdaG1Affine as +// polynomials in x, a short vector of this Zmodule is +// 1, x². So we check that p+x²ϕ(p) +// is the infinity. +func (p *G1Jac) IsInSubGroup() bool { + if !p.IsOnCurve() { + return false + } + var res G1Jac + + res.mulWindowed(p, fr.Modulus()) + + return res.Z.IsZero() + +} + +// mulWindowed computes a double-and-add scalar multiplication p=[s]q in +// Jacobian coordinates and using NAF encoding. +func (p *G1Jac) mulWindowed(q *G1Jac, s *big.Int) *G1Jac { + var qAff G1Affine + qAff.FromJacobian(q) + return p.mulWindowedMixed(&qAff, s) +} + +// mulWindowed computes a double-and-add scalar multiplication p=[s]q in +// affine coordinates and using NAF encoding. +func (p *G1Affine) mulWindowed(q *G1Affine, s *big.Int) *G1Affine { + var res G1Jac + res.mulWindowedMixed(q, s) + p.FromJacobian(&res) + return p +} + +// mulWindowedMixed computes a double-and-add scalar multiplication p=[s]q +// where q is in affine coordinates, using NAF encoding. +func (p *G1Jac) mulWindowedMixed(q *G1Affine, s *big.Int) *G1Jac { + if s.Sign() == 0 { + p.Set(&g1Infinity) + return p + } + var scalar big.Int + scalar.Set(s) + negScalar := scalar.Sign() < 0 + if negScalar { + scalar.Neg(&scalar) + } + if scalar.BitLen() > fr.Bits { + scalar.Mod(&scalar, fr.Modulus()) + } + var naf [fr.Bits + 1]int8 + nafLen := ecc.NafDecomposition(&scalar, naf[:]) + var qNeg G1Affine + qNeg.Neg(q) + p.Set(&g1Infinity) + for i := nafLen - 1; i >= 0; i-- { + p.DoubleAssign() + switch naf[i] { + case 0: + continue + case 1: + p.AddMixed(q) + case -1: + p.AddMixed(&qNeg) + } + } + if negScalar { + p.Neg(p) + } + return p +} + +// mulBySeed multiplies the point q by the seed xGen in Jacobian coordinates +// using an optimized addition chain. +func (p *G1Jac) mulBySeed(q *G1Jac) *G1Jac { + p.mulWindowed(q, &xGen) + return p +} + +// JointScalarMultiplication computes [s1]a1+[s2]a2 using Strauss-Shamir technique +// where a1 and a2 are affine points. +func (p *G1Jac) JointScalarMultiplication(a1, a2 *G1Affine, s1, s2 *big.Int) *G1Jac { + + var res, p1, p2 G1Jac + res.Set(&g1Infinity) + p1.FromAffine(a1) + p2.FromAffine(a2) + + var table [15]G1Jac + + var k1, k2 big.Int + if s1.Sign() == -1 { + k1.Neg(s1) + table[0].Neg(&p1) + } else { + k1.Set(s1) + table[0].Set(&p1) + } + if s2.Sign() == -1 { + k2.Neg(s2) + table[3].Neg(&p2) + } else { + k2.Set(s2) + table[3].Set(&p2) + } + + // precompute table (2 bits sliding window) + table[1].Double(&table[0]) + table[2].Triple(&table[0]) + table[4].Set(&table[3]).AddAssign(&table[0]) + table[5].Set(&table[3]).AddAssign(&table[1]) + table[6].Set(&table[3]).AddAssign(&table[2]) + table[7].Double(&table[3]) + table[8].Set(&table[7]).AddAssign(&table[0]) + table[9].Double(&table[4]) + table[10].Set(&table[7]).AddAssign(&table[2]) + table[11].Triple(&table[3]) + table[12].Set(&table[11]).AddAssign(&table[0]) + table[13].Set(&table[11]).AddAssign(&table[1]) + table[14].Triple(&table[4]) + + var s [2]fr.Element + s[0] = s[0].SetBigInt(&k1).Bits() + s[1] = s[1].SetBigInt(&k2).Bits() + + maxBit := max(k1.BitLen(), k2.BitLen()) + hiWordIndex := (maxBit - 1) / 64 + + for i := hiWordIndex; i >= 0; i-- { + mask := uint64(3) << 62 + for j := range 32 { + res.Double(&res).Double(&res) + b1 := (s[0][i] & mask) >> (62 - 2*j) + b2 := (s[1][i] & mask) >> (62 - 2*j) + if b1|b2 != 0 { + s := (b2<<2 | b1) + res.AddAssign(&table[s-1]) + } + mask = mask >> 2 + } + } + + p.Set(&res) + return p + +} + +// JointScalarMultiplicationBase computes [s1]g+[s2]a using Straus-Shamir technique +// where g is the prime subgroup generator. +func (p *G1Jac) JointScalarMultiplicationBase(a *G1Affine, s1, s2 *big.Int) *G1Jac { + return p.JointScalarMultiplication(&g1GenAff, a, s1, s2) + +} + +// ------------------------------------------------------------------------------------------------- +// extended Jacobian coordinates + +// Set sets p to a in extended Jacobian coordinates. +func (p *g1JacExtended) Set(q *g1JacExtended) *g1JacExtended { + p.X, p.Y, p.ZZ, p.ZZZ = q.X, q.Y, q.ZZ, q.ZZZ + return p +} + +// SetInfinity sets p to the infinity point (1,1,0,0). +func (p *g1JacExtended) SetInfinity() *g1JacExtended { + p.X.SetOne() + p.Y.SetOne() + p.ZZ = fptower.E8{} + p.ZZZ = fptower.E8{} + return p +} + +// IsInfinity checks if the p is infinity, i.e. p.ZZ=0. +func (p *g1JacExtended) IsInfinity() bool { + return p.ZZ.IsZero() +} + +// fromJacExtended converts an extended Jacobian point to an affine point. +func (p *G1Affine) fromJacExtended(q *g1JacExtended) *G1Affine { + if q.ZZ.IsZero() { + p.X = fptower.E8{} + p.Y = fptower.E8{} + return p + } + p.X.Inverse(&q.ZZ).Mul(&p.X, &q.X) + p.Y.Inverse(&q.ZZZ).Mul(&p.Y, &q.Y) + return p +} + +// fromJacExtended converts an extended Jacobian point to a Jacobian point. +func (p *G1Jac) fromJacExtended(q *g1JacExtended) *G1Jac { + if q.ZZ.IsZero() { + p.Set(&g1Infinity) + return p + } + p.X.Mul(&q.ZZ, &q.X).Mul(&p.X, &q.ZZ) + p.Y.Mul(&q.ZZZ, &q.Y).Mul(&p.Y, &q.ZZZ) + p.Z.Set(&q.ZZZ) + return p +} + +// unsafeFromJacExtended converts an extended Jacobian point, distinct from Infinity, to a Jacobian point. +func (p *G1Jac) unsafeFromJacExtended(q *g1JacExtended) *G1Jac { + p.X.Square(&q.ZZ).Mul(&p.X, &q.X) + p.Y.Square(&q.ZZZ).Mul(&p.Y, &q.Y) + p.Z = q.ZZZ + return p +} + +// add sets p to p+q in extended Jacobian coordinates. +// +// https://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#addition-add-2008-s +// ~Cost: 12M + 2S +func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { + //if q is infinity return p + if q.ZZ.IsZero() { + return p + } + // p is infinity, return q + if p.ZZ.IsZero() { + p.Set(q) + return p + } + + var A, B, U1, U2, S1, S2 fptower.E8 + + // p2: q, p1: p + U2.Mul(&q.X, &p.ZZ) + U1.Mul(&p.X, &q.ZZ) + A.Sub(&U2, &U1) + S2.Mul(&q.Y, &p.ZZZ) + S1.Mul(&p.Y, &q.ZZZ) + B.Sub(&S2, &S1) + + if A.IsZero() { + if B.IsZero() { + return p.double(q) + + } + p.ZZ = fptower.E8{} + p.ZZZ = fptower.E8{} + return p + } + + var P, R, PP, PPP, Q, V fptower.E8 + P.Sub(&U2, &U1) + R.Sub(&S2, &S1) + PP.Square(&P) + PPP.Mul(&P, &PP) + Q.Mul(&U1, &PP) + V.Mul(&S1, &PPP) + + p.X.Square(&R). + Sub(&p.X, &PPP). + Sub(&p.X, &Q). + Sub(&p.X, &Q) + p.Y.Sub(&Q, &p.X). + Mul(&p.Y, &R). + Sub(&p.Y, &V) + p.ZZ.Mul(&p.ZZ, &q.ZZ). + Mul(&p.ZZ, &PP) + p.ZZZ.Mul(&p.ZZZ, &q.ZZZ). + Mul(&p.ZZZ, &PPP) + + return p +} + +// double sets p to [2]q in Jacobian extended coordinates. +// +// http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 +// ~Cost: 6M + 3S +// +// N.B.: since we consider any point on Z=0 as the point at infinity +// this doubling formula works for infinity points as well. +func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { + var jq, res G1Jac + jq.fromJacExtended(q) + res.Double(&jq) + if res.Z.IsZero() { + return p.SetInfinity() + } + p.X.Set(&res.X) + p.Y.Set(&res.Y) + p.ZZ.Square(&res.Z) + p.ZZZ.Mul(&p.ZZ, &res.Z) + return p + var U, V, W, S, XX, M fptower.E8 + + U.Double(&q.Y) + V.Square(&U) + W.Mul(&U, &V) + S.Mul(&q.X, &V) + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // -> + A, but A=0 here + U.Mul(&W, &q.Y) + + p.X.Square(&M). + Sub(&p.X, &S). + Sub(&p.X, &S) + p.Y.Sub(&S, &p.X). + Mul(&p.Y, &M). + Sub(&p.Y, &U) + p.ZZ.Mul(&V, &q.ZZ) + p.ZZZ.Mul(&W, &q.ZZZ) + + return p +} + +// addMixed sets p to p+q in extended Jacobian coordinates, where a.ZZ=1. +// +// http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#addition-madd-2008-s +// ~Cost: 8M + 2S +func (p *g1JacExtended) addMixed(a *G1Affine) *g1JacExtended { + + //if a is infinity return p + if a.IsInfinity() { + return p + } + // p is infinity, return a + if p.ZZ.IsZero() { + p.X = a.X + p.Y = a.Y + p.ZZ.SetOne() + p.ZZZ.SetOne() + return p + } + + var P, R fptower.E8 + + // p2: a, p1: p + P.Mul(&a.X, &p.ZZ) + P.Sub(&P, &p.X) + + R.Mul(&a.Y, &p.ZZZ) + R.Sub(&R, &p.Y) + + if P.IsZero() { + if R.IsZero() { + return p.doubleMixed(a) + + } + p.ZZ = fptower.E8{} + p.ZZZ = fptower.E8{} + return p + } + + var PP, PPP, Q, Q2, RR, X3, Y3 fptower.E8 + + PP.Square(&P) + PPP.Mul(&P, &PP) + Q.Mul(&p.X, &PP) + RR.Square(&R) + X3.Sub(&RR, &PPP) + Q2.Double(&Q) + p.X.Sub(&X3, &Q2) + Y3.Sub(&Q, &p.X).Mul(&Y3, &R) + R.Mul(&p.Y, &PPP) + p.Y.Sub(&Y3, &R) + p.ZZ.Mul(&p.ZZ, &PP) + p.ZZZ.Mul(&p.ZZZ, &PPP) + + return p + +} + +// subMixed works the same as addMixed, but negates a.Y. +// +// http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#addition-madd-2008-s +// ~Cost: 8M + 2S +func (p *g1JacExtended) subMixed(a *G1Affine) *g1JacExtended { + + //if a is infinity return p + if a.IsInfinity() { + return p + } + // p is infinity, return a + if p.ZZ.IsZero() { + p.X = a.X + p.Y.Neg(&a.Y) + p.ZZ.SetOne() + p.ZZZ.SetOne() + return p + } + + var P, R fptower.E8 + + // p2: a, p1: p + P.Mul(&a.X, &p.ZZ) + P.Sub(&P, &p.X) + + R.Mul(&a.Y, &p.ZZZ) + R.Neg(&R) + R.Sub(&R, &p.Y) + + if P.IsZero() { + if R.IsZero() { + return p.doubleNegMixed(a) + + } + p.ZZ = fptower.E8{} + p.ZZZ = fptower.E8{} + return p + } + + var PP, PPP, Q, Q2, RR, X3, Y3 fptower.E8 + + PP.Square(&P) + PPP.Mul(&P, &PP) + Q.Mul(&p.X, &PP) + RR.Square(&R) + X3.Sub(&RR, &PPP) + Q2.Double(&Q) + p.X.Sub(&X3, &Q2) + Y3.Sub(&Q, &p.X).Mul(&Y3, &R) + R.Mul(&p.Y, &PPP) + p.Y.Sub(&Y3, &R) + p.ZZ.Mul(&p.ZZ, &PP) + p.ZZZ.Mul(&p.ZZZ, &PPP) + + return p + +} + +// doubleNegMixed works the same as doubleMixed, but negates q.Y. +// +// https://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-mdbl-2008-s-1 +// ~Cost: 4M + 3S +func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { + + var aff G1Affine + aff.Set(a) + aff.Neg(&aff) + var res G1Jac + res.FromAffine(&aff).DoubleAssign() + if res.Z.IsZero() { + return p.SetInfinity() + } + p.X.Set(&res.X) + p.Y.Set(&res.Y) + p.ZZ.Square(&res.Z) + p.ZZZ.Mul(&p.ZZ, &res.Z) + return p + var U, V, W, S, M, t fptower.E8 + + U.Double(&a.Y) + U.Neg(&U) + V.Square(&U) + W.Mul(&U, &V) + S.Mul(&a.X, &V) + t.Square(&a.X) + M.Double(&t). + Add(&M, &t) // -> + A, but A=0 here + p.X.Square(&M) + t.Double(&S) + p.X.Sub(&p.X, &t) + t.Mul(&W, &a.Y) + p.Y.Sub(&S, &p.X). + Mul(&p.Y, &M). + Add(&p.Y, &t) + p.ZZ.Set(&V) + p.ZZZ.Set(&W) + + return p +} + +// doubleMixed sets p to [2]a in Jacobian extended coordinates, where a.ZZ=1. +// +// https://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-mdbl-2008-s-1 +// ~Cost: 4M + 3S +func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { + + var aff G1Affine + aff.Set(a) + var res G1Jac + res.FromAffine(&aff).DoubleAssign() + if res.Z.IsZero() { + return p.SetInfinity() + } + p.X.Set(&res.X) + p.Y.Set(&res.Y) + p.ZZ.Square(&res.Z) + p.ZZZ.Mul(&p.ZZ, &res.Z) + return p + var U, V, W, S, M, t fptower.E8 + + U.Double(&a.Y) + V.Square(&U) + W.Mul(&U, &V) + S.Mul(&a.X, &V) + t.Square(&a.X) + M.Double(&t). + Add(&M, &t) // -> + A, but A=0 here + p.X.Square(&M) + t.Double(&S) + p.X.Sub(&p.X, &t) + t.Mul(&W, &a.Y) + p.Y.Sub(&S, &p.X). + Mul(&p.Y, &M). + Sub(&p.Y, &t) + p.ZZ.Set(&V) + p.ZZZ.Set(&W) + + return p +} + +// BatchJacobianToAffineG1 converts points in Jacobian coordinates to Affine coordinates +// performing a single field inversion using the Montgomery batch inversion trick. +func BatchJacobianToAffineG1(points []G1Jac) []G1Affine { + result := make([]G1Affine, len(points)) + zeroes := make([]bool, len(points)) + var accumulator fptower.E8 + accumulator.SetOne() + + // batch invert all points[].Z coordinates with Montgomery batch inversion trick + // (stores points[].Z^-1 in result[i].X to avoid allocating a slice of fr.Elements) + for i := range len(points) { + if points[i].Z.IsZero() { + zeroes[i] = true + continue + } + result[i].X = accumulator + accumulator.Mul(&accumulator, &points[i].Z) + } + + var accInverse fptower.E8 + accInverse.Inverse(&accumulator) + + for i := len(points) - 1; i >= 0; i-- { + if zeroes[i] { + // do nothing, (X=0, Y=0) is infinity point in affine + continue + } + result[i].X.Mul(&result[i].X, &accInverse) + accInverse.Mul(&accInverse, &points[i].Z) + } + + // batch convert to affine. + parallel.Execute(len(points), func(start, end int) { + for i := start; i < end; i++ { + if zeroes[i] { + // do nothing, (X=0, Y=0) is infinity point in affine + continue + } + var a, b fptower.E8 + a = result[i].X + b.Square(&a) + result[i].X.Mul(&points[i].X, &b) + result[i].Y.Mul(&points[i].Y, &b). + Mul(&result[i].Y, &a) + } + }) + + return result +} + +// BatchScalarMultiplicationG1 multiplies the same base by all scalars +// and return resulting points in affine coordinates +// uses a simple windowed-NAF-like multiplication algorithm. +func BatchScalarMultiplicationG1(base *G1Affine, scalars []fr.Element) []G1Affine { + // approximate cost in group ops is + // cost = 2^{c-1} + n(scalar.nbBits+nbChunks) + + nbPoints := uint64(len(scalars)) + min := ^uint64(0) + bestC := 0 + for c := 2; c <= 16; c++ { + cost := uint64(1 << (c - 1)) // pre compute the table + nbChunks := computeNbChunks(uint64(c)) + cost += nbPoints * (uint64(c) + 1) * nbChunks // doublings + point add + if cost < min { + min = cost + bestC = c + } + } + c := uint64(bestC) // window size + nbChunks := int(computeNbChunks(c)) + + // last window may be slightly larger than c; in which case we need to compute one + // extra element in the baseTable + maxC := max(c, lastC(c)) + + // precompute all powers of base for our window + // note here that if performance is critical, we can implement as in the msmX methods + // this allocation to be on the stack + baseTable := make([]G1Jac, (1 << (maxC - 1))) + baseTable[0].FromAffine(base) + for i := 1; i < len(baseTable); i++ { + baseTable[i] = baseTable[i-1] + baseTable[i].AddMixed(base) + } + // convert our base exp table into affine to use AddMixed + baseTableAff := BatchJacobianToAffineG1(baseTable) + toReturn := make([]G1Jac, len(scalars)) + + // partition the scalars into digits + digits, _ := partitionScalars(scalars, c, runtime.NumCPU()) + + // for each digit, take value in the base table, double it c time, voilà. + parallel.Execute(len(scalars), func(start, end int) { + var p G1Jac + for i := start; i < end; i++ { + p.Set(&g1Infinity) + for chunk := nbChunks - 1; chunk >= 0; chunk-- { + if chunk != nbChunks-1 { + for range c { + p.DoubleAssign() + } + } + offset := chunk * len(scalars) + digit := digits[i+offset] + + if digit == 0 { + continue + } + + // if msbWindow bit is set, we need to subtract + if digit&1 == 0 { + // add + p.AddMixed(&baseTableAff[(digit>>1)-1]) + } else { + // sub + t := baseTableAff[digit>>1] + t.Neg(&t) + p.AddMixed(&t) + } + } + + // set our result point + toReturn[i] = p + + } + }) + toReturnAff := BatchJacobianToAffineG1(toReturn) + return toReturnAff +} + +// batchAddG1Affine adds affine points using the Montgomery batch inversion trick. +// Special cases (doubling, infinity) must be filtered out before this call. +func batchAddG1Affine[TP pG1Affine, TPP ppG1Affine, TC cG1Affine](R *TPP, P *TP, batchSize int) { + var lambda, lambdain TC + + // from https://docs.zkproof.org/pages/standards/accepted-workshop3/proposal-turbo_plonk.pdf + // affine point addition formula + // R(X1, Y1) + P(X2, Y2) = Q(X3, Y3) + // λ = (Y2 - Y1) / (X2 - X1) + // X3 = λ² - (X1 + X2) + // Y3 = λ * (X1 - X3) - Y1 + + // first we compute the 1 / (X2 - X1) for all points using Montgomery batch inversion trick + + // X2 - X1 + for j := range batchSize { + lambdain[j].Sub(&(*P)[j].X, &(*R)[j].X) + } + + // montgomery batch inversion; + // lambda[0] = 1 / (P[0].X - R[0].X) + // lambda[1] = 1 / (P[1].X - R[1].X) + // ... + { + var accumulator fptower.E8 + lambda[0].SetOne() + accumulator.Set(&lambdain[0]) + + for i := 1; i < batchSize; i++ { + lambda[i] = accumulator + accumulator.Mul(&accumulator, &lambdain[i]) + } + + accumulator.Inverse(&accumulator) + + for i := batchSize - 1; i > 0; i-- { + lambda[i].Mul(&lambda[i], &accumulator) + accumulator.Mul(&accumulator, &lambdain[i]) + } + lambda[0].Set(&accumulator) + } + + var t fptower.E8 + var Q G1Affine + + for j := range batchSize { + // λ = (Y2 - Y1) / (X2 - X1) + t.Sub(&(*P)[j].Y, &(*R)[j].Y) + lambda[j].Mul(&lambda[j], &t) + + // X3 = λ² - (X1 + X2) + Q.X.Square(&lambda[j]) + Q.X.Sub(&Q.X, &(*R)[j].X) + Q.X.Sub(&Q.X, &(*P)[j].X) + + // Y3 = λ * (X1 - X3) - Y1 + t.Sub(&(*R)[j].X, &Q.X) + Q.Y.Mul(&lambda[j], &t) + Q.Y.Sub(&Q.Y, &(*R)[j].Y) + + (*R)[j].Set(&Q) + } +} diff --git a/ecc/kb8/g1_test.go b/ecc/kb8/g1_test.go new file mode 100644 index 0000000000..5b46684721 --- /dev/null +++ b/ecc/kb8/g1_test.go @@ -0,0 +1,828 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package kb8 + +import ( + crand "crypto/rand" + "fmt" + "math/big" + "math/rand/v2" + "testing" + + "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + + "github.com/consensys/gnark-crypto/ecc/kb8/fr" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/prop" +) + +func TestIsOnG1(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + properties.Property("[KB8] g1Gen (affine) should be on the curve", prop.ForAll( + func(a fptower.E8) bool { + var op1, op2 G1Affine + op1.FromJacobian(&g1Gen) + op2.Set(&op1) + op2.Y.Mul(&op2.Y, &a) + return op1.IsOnCurve() && !op2.IsOnCurve() + }, + GenE8(), + )) + + properties.Property("[KB8] g1Gen (Jacobian) should be on the curve", prop.ForAll( + func(a fptower.E8) bool { + var op1, op2, op3 G1Jac + op1.Set(&g1Gen) + op3.Set(&g1Gen) + + op2 = fuzzG1Jac(&g1Gen, a) + op3.Y.Mul(&op3.Y, &a) + return op1.IsOnCurve() && op2.IsOnCurve() && !op3.IsOnCurve() + }, + GenE8(), + )) + + properties.Property("[KB8] IsInSubGroup and MulBy subgroup order should be the same", prop.ForAll( + func(a fptower.E8) bool { + var op1, op2 G1Jac + op1 = fuzzG1Jac(&g1Gen, a) + _r := fr.Modulus() + op2.mulWindowed(&op1, _r) + return op1.IsInSubGroup() && op2.Z.IsZero() + }, + GenE8(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestIsInSubGroupBatchG1(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = 1 + } else { + parameters.MinSuccessfulTests = 100 + } + + properties := gopter.NewProperties(parameters) + + // number of points to test + const nbSamples = 100 + + properties.Property("[KB8] IsInSubGroupBatchG1 test should pass with high probability", prop.ForAll( + func(mixer fr.Element) bool { + // mixer ensures that all the words of a frElement are set + var sampleScalars [nbSamples]fr.Element + + for i := range uint64(nbSamples) { + sampleScalars[i].SetUint64(i+1). + Mul(&sampleScalars[i], &mixer) + } + + // random points in G1 + result := BatchScalarMultiplicationG1(&g1GenAff, sampleScalars[:]) + + return IsInSubGroupBatchG1(result) + }, + GenFr(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestG1Conversions(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + properties.Property("[KB8] Affine representation should be independent of the Jacobian representative", prop.ForAll( + func(a fptower.E8) bool { + g := fuzzG1Jac(&g1Gen, a) + var op1 G1Affine + op1.FromJacobian(&g) + return op1.X.Equal(&g1Gen.X) && op1.Y.Equal(&g1Gen.Y) + }, + GenE8(), + )) + + properties.Property("[KB8] Affine representation should be independent of a Extended Jacobian representative", prop.ForAll( + func(a fptower.E8) bool { + var g g1JacExtended + g.X.Set(&g1Gen.X) + g.Y.Set(&g1Gen.Y) + g.ZZ.Set(&g1Gen.Z) + g.ZZZ.Set(&g1Gen.Z) + gfuzz := fuzzg1JacExtended(&g, a) + + var op1 G1Affine + op1.fromJacExtended(&gfuzz) + return op1.X.Equal(&g1Gen.X) && op1.Y.Equal(&g1Gen.Y) + }, + GenE8(), + )) + + properties.Property("[KB8] Jacobian representation should be the same as the affine representative", prop.ForAll( + func(a fptower.E8) bool { + var g G1Jac + var op1 G1Affine + op1.X.Set(&g1Gen.X) + op1.Y.Set(&g1Gen.Y) + + var one fptower.E8 + one.SetOne() + + g.FromAffine(&op1) + + return g.X.Equal(&g1Gen.X) && g.Y.Equal(&g1Gen.Y) && g.Z.Equal(&one) + }, + GenE8(), + )) + + properties.Property("[KB8] Converting affine symbol for infinity to Jacobian should output correct infinity in Jacobian", prop.ForAll( + func() bool { + var g G1Affine + g.X.SetZero() + g.Y.SetZero() + var op1 G1Jac + op1.FromAffine(&g) + var one, zero fptower.E8 + one.SetOne() + return op1.X.Equal(&one) && op1.Y.Equal(&one) && op1.Z.Equal(&zero) + }, + )) + + properties.Property("[KB8] Converting infinity in extended Jacobian to affine should output infinity symbol in Affine", prop.ForAll( + func() bool { + var g G1Affine + var op1 g1JacExtended + var zero fptower.E8 + op1.X.Set(&g1Gen.X) + op1.Y.Set(&g1Gen.Y) + g.fromJacExtended(&op1) + return g.X.Equal(&zero) && g.Y.Equal(&zero) + }, + )) + + properties.Property("[KB8] Converting infinity in extended Jacobian to Jacobian should output infinity in Jacobian", prop.ForAll( + func() bool { + var g G1Jac + var op1 g1JacExtended + var zero, one fptower.E8 + one.SetOne() + op1.X.Set(&g1Gen.X) + op1.Y.Set(&g1Gen.Y) + g.fromJacExtended(&op1) + return g.X.Equal(&one) && g.Y.Equal(&one) && g.Z.Equal(&zero) + }, + )) + + properties.Property("[KB8] [Jacobian] Two representatives of the same class should be equal", prop.ForAll( + func(a, b fptower.E8) bool { + op1 := fuzzG1Jac(&g1Gen, a) + op2 := fuzzG1Jac(&g1Gen, b) + return op1.Equal(&op2) + }, + GenE8(), + GenE8(), + )) + properties.Property("[KB8] BatchJacobianToAffineG1 and FromJacobian should output the same result", prop.ForAll( + func(a, b fptower.E8) bool { + g1 := fuzzG1Jac(&g1Gen, a) + g2 := fuzzG1Jac(&g1Gen, b) + var op1, op2 G1Affine + op1.FromJacobian(&g1) + op2.FromJacobian(&g2) + baseTableAff := BatchJacobianToAffineG1([]G1Jac{g1, g2}) + return op1.Equal(&baseTableAff[0]) && op2.Equal(&baseTableAff[1]) + }, + GenE8(), + GenE8(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestG1AffineOps(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 10 + + properties := gopter.NewProperties(parameters) + + genScalar := GenFr() + + properties.Property("[KB8] Add(P,-P) should return the point at infinity", prop.ForAll( + func(s fr.Element) bool { + var op1, op2 G1Affine + var sInt big.Int + g := g1GenAff + s.BigInt(&sInt) + op1.ScalarMultiplication(&g, &sInt) + op2.Neg(&op1) + + op1.Add(&op1, &op2) + return op1.IsInfinity() + + }, + GenFr(), + )) + + properties.Property("[KB8] Add(P,0) and Add(0,P) should return P", prop.ForAll( + func(s fr.Element) bool { + var op1, op2 G1Affine + var sInt big.Int + g := g1GenAff + s.BigInt(&sInt) + op1.ScalarMultiplication(&g, &sInt) + op2.SetInfinity() + + op1.Add(&op1, &op2) + op2.Add(&op2, &op1) + return op1.Equal(&op2) + + }, + GenFr(), + )) + + properties.Property("[KB8] Add should call double when adding the same point", prop.ForAll( + func(s fr.Element) bool { + var op1, op2 G1Affine + var sInt big.Int + g := g1GenAff + s.BigInt(&sInt) + op1.ScalarMultiplication(&g, &sInt) + + op2.Double(&op1) + op1.Add(&op1, &op1) + return op1.Equal(&op2) + + }, + GenFr(), + )) + + properties.Property("[KB8] [2]G = double(G) + G - G", prop.ForAll( + func(s fr.Element) bool { + var sInt big.Int + g := g1GenAff + s.BigInt(&sInt) + g.ScalarMultiplication(&g, &sInt) + var op1, op2 G1Affine + op1.ScalarMultiplication(&g, big.NewInt(2)) + op2.Double(&g) + op2.Add(&op2, &g) + op2.Sub(&op2, &g) + return op1.Equal(&op2) + }, + GenFr(), + )) + + properties.Property("[KB8] [-s]G = -[s]G", prop.ForAll( + func(s fr.Element) bool { + g := g1GenAff + var gj G1Jac + var nbs, bs big.Int + s.BigInt(&bs) + nbs.Neg(&bs) + + var res = true + + // mulGLV + { + var op1, op2 G1Affine + op1.ScalarMultiplication(&g, &bs).Neg(&op1) + op2.ScalarMultiplication(&g, &nbs) + res = res && op1.Equal(&op2) + } + + // mulWindowed + { + var op1, op2 G1Jac + op1.mulWindowed(&gj, &bs).Neg(&op1) + op2.mulWindowed(&gj, &nbs) + res = res && op1.Equal(&op2) + } + + return res + }, + GenFr(), + )) + + properties.Property("[KB8] [Jacobian] Add should call double when adding the same point", prop.ForAll( + func(a, b fptower.E8) bool { + fop1 := fuzzG1Jac(&g1Gen, a) + fop2 := fuzzG1Jac(&g1Gen, b) + var op1, op2 G1Jac + op1.Set(&fop1).AddAssign(&fop2) + op2.Double(&fop2) + return op1.Equal(&op2) + }, + GenE8(), + GenE8(), + )) + + properties.Property("[KB8] [Jacobian] Adding the opposite of a point to itself should output inf", prop.ForAll( + func(a, b fptower.E8) bool { + fop1 := fuzzG1Jac(&g1Gen, a) + fop2 := fuzzG1Jac(&g1Gen, b) + fop2.Neg(&fop2) + fop1.AddAssign(&fop2) + return fop1.Equal(&g1Infinity) + }, + GenE8(), + GenE8(), + )) + + properties.Property("[KB8] [Jacobian] Adding the inf to a point should not modify the point", prop.ForAll( + func(a fptower.E8) bool { + fop1 := fuzzG1Jac(&g1Gen, a) + fop1.AddAssign(&g1Infinity) + var op2 G1Jac + op2.Set(&g1Infinity) + op2.AddAssign(&g1Gen) + return fop1.Equal(&g1Gen) && op2.Equal(&g1Gen) + }, + GenE8(), + )) + + properties.Property("[KB8] [Jacobian Extended] addMixed (-G) should equal subMixed(G)", prop.ForAll( + func(a fptower.E8) bool { + fop1 := fuzzG1Jac(&g1Gen, a) + var p1, p1Neg G1Affine + p1.FromJacobian(&fop1) + p1Neg = p1 + p1Neg.Y.Neg(&p1Neg.Y) + var o1, o2 g1JacExtended + o1.addMixed(&p1Neg) + o2.subMixed(&p1) + + return o1.X.Equal(&o2.X) && + o1.Y.Equal(&o2.Y) && + o1.ZZ.Equal(&o2.ZZ) && + o1.ZZZ.Equal(&o2.ZZZ) + }, + GenE8(), + )) + + properties.Property("[KB8] [Jacobian Extended] doubleMixed (-G) should equal doubleNegMixed(G)", prop.ForAll( + func(a fptower.E8) bool { + fop1 := fuzzG1Jac(&g1Gen, a) + var p1, p1Neg G1Affine + p1.FromJacobian(&fop1) + p1Neg = p1 + p1Neg.Y.Neg(&p1Neg.Y) + var o1, o2 g1JacExtended + o1.doubleMixed(&p1Neg) + o2.doubleNegMixed(&p1) + + return o1.X.Equal(&o2.X) && + o1.Y.Equal(&o2.Y) && + o1.ZZ.Equal(&o2.ZZ) && + o1.ZZZ.Equal(&o2.ZZZ) + }, + GenE8(), + )) + + properties.Property("[KB8] [Jacobian] Addmix the negation to itself should output 0", prop.ForAll( + func(a fptower.E8) bool { + fop1 := fuzzG1Jac(&g1Gen, a) + fop1.Neg(&fop1) + var op2 G1Affine + op2.FromJacobian(&g1Gen) + fop1.AddMixed(&op2) + return fop1.Equal(&g1Infinity) + }, + GenE8(), + )) + + properties.Property("[KB8] scalar multiplication (double and add) should depend only on the scalar mod r", prop.ForAll( + func(s fr.Element) bool { + + r := fr.Modulus() + var g G1Jac + g.ScalarMultiplication(&g1Gen, r) + + var scalar, blindedScalar, rminusone big.Int + var op1, op2, op3, gneg G1Jac + rminusone.SetUint64(1).Sub(r, &rminusone) + op3.mulWindowed(&g1Gen, &rminusone) + gneg.Neg(&g1Gen) + s.BigInt(&scalar) + blindedScalar.Mul(&scalar, r).Add(&blindedScalar, &scalar) + op1.mulWindowed(&g1Gen, &scalar) + op2.mulWindowed(&g1Gen, &blindedScalar) + + return op1.Equal(&op2) && g.Equal(&g1Infinity) && !op1.Equal(&g1Infinity) && gneg.Equal(&op3) + + }, + genScalar, + )) + + properties.Property("[KB8] JointScalarMultiplicationBase and ScalarMultiplication should output the same results", prop.ForAll( + func(s1, s2 fr.Element) bool { + + var op1, op2, temp G1Jac + + op1.JointScalarMultiplicationBase(&g1GenAff, s1.BigInt(new(big.Int)), s2.BigInt(new(big.Int))) + temp.ScalarMultiplication(&g1Gen, s2.BigInt(new(big.Int))) + op2.ScalarMultiplication(&g1Gen, s1.BigInt(new(big.Int))). + AddAssign(&temp) + + return op1.Equal(&op2) + + }, + genScalar, + genScalar, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestG1BatchScalarMultiplication(t *testing.T) { + + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzzShort + } + + properties := gopter.NewProperties(parameters) + + genScalar := GenFr() + + // size of the multiExps + const nbSamples = 10 + + properties.Property("[KB8] BatchScalarMultiplication should be consistent with individual scalar multiplications", prop.ForAll( + func(mixer fr.Element) bool { + // mixer ensures that all the words of a fpElement are set + var sampleScalars [nbSamples]fr.Element + + for i := 1; i <= nbSamples; i++ { + sampleScalars[i-1].SetUint64(uint64(i)). + Mul(&sampleScalars[i-1], &mixer) + } + + result := BatchScalarMultiplicationG1(&g1GenAff, sampleScalars[:]) + + if len(result) != len(sampleScalars) { + return false + } + + for i := range len(result) { + var expectedJac G1Jac + var expected G1Affine + var b big.Int + expectedJac.ScalarMultiplication(&g1Gen, sampleScalars[i].BigInt(&b)) + expected.FromJacobian(&expectedJac) + if !result[i].Equal(&expected) { + return false + } + } + return true + }, + genScalar, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestG1JacTriple(t *testing.T) { + // test triple on the generator and the infinity point + // against double and add + var a G1Jac + a.Set(&g1Gen) + var infinity G1Jac + infinity.Set(&g1Infinity) + + var pTriple, pDoubleAdd G1Jac + pTriple.Triple(&a) + pDoubleAdd.Double(&a).AddAssign(&a) + + if !pTriple.Equal(&pDoubleAdd) { + t.Fatalf("triple and double+add do not match: %s != %s", pTriple.String(), pDoubleAdd.String()) + } + + // same thing with infinity point + pTriple.Triple(&infinity) + pDoubleAdd.Double(&infinity).AddAssign(&infinity) + + if !pTriple.Equal(&pDoubleAdd) { + t.Fatalf("triple and double+add do not match: %s != %s", pTriple.String(), pDoubleAdd.String()) + } +} + +func BenchmarkG1JacTriple(b *testing.B) { + var a G1Jac + a.Set(&g1Gen) + b.ResetTimer() + for range b.N { + a.Triple(&a) + } +} + +// ------------------------------------------------------------ +// benches + +func BenchmarkG1JacIsInSubGroup(b *testing.B) { + var a G1Jac + a.Set(&g1Gen) + b.ResetTimer() + for range b.N { + a.IsInSubGroup() + } + +} + +func BenchmarkG1JacEqual(b *testing.B) { + var scalar fptower.E8 + scalar.MustSetRandom() + + var a G1Jac + a.ScalarMultiplication(&g1Gen, big.NewInt(42)) + + b.Run("equal", func(b *testing.B) { + var scalarSquared fptower.E8 + scalarSquared.Square(&scalar) + + aZScaled := a + aZScaled.X.Mul(&aZScaled.X, &scalarSquared) + aZScaled.Y.Mul(&aZScaled.Y, &scalarSquared).Mul(&aZScaled.Y, &scalar) + aZScaled.Z.Mul(&aZScaled.Z, &scalar) + + // Check the setup. + if !a.Equal(&aZScaled) { + b.Fatalf("invalid test setup") + } + + b.ResetTimer() + for range b.N { + a.Equal(&aZScaled) + } + }) + + b.Run("not equal", func(b *testing.B) { + var aPlus1 G1Jac + aPlus1.AddAssign(&g1Gen) + + // Check the setup. + if a.Equal(&aPlus1) { + b.Fatalf("invalid test setup") + } + + b.ResetTimer() + for range b.N { + a.Equal(&aPlus1) + } + }) +} + +func BenchmarkBatchAddG1Affine(b *testing.B) { + + var P, R pG1AffineC16 + var RR ppG1AffineC16 + ridx := make([]int, len(P)) + + // TODO P == R may produce skewed benches + fillBenchBasesG1(P[:]) + fillBenchBasesG1(R[:]) + + for i := range len(ridx) { + ridx[i] = i + } + + // random permute + rand.Shuffle(len(ridx), func(i, j int) { ridx[i], ridx[j] = ridx[j], ridx[i] }) + + for i, ri := range ridx { + RR[i] = &R[ri] + } + + b.ResetTimer() + for range b.N { + batchAddG1Affine[pG1AffineC16, ppG1AffineC16, cG1AffineC16](&RR, &P, len(P)) + } +} + +func BenchmarkG1AffineBatchScalarMultiplication(b *testing.B) { + // ensure every words of the scalars are filled + var mixer fr.Element + mixer.SetString("7716837800905789770901243404444209691916730933998574719964609384059111546487") + + const pow = 15 + const nbSamples = 1 << pow + + var sampleScalars [nbSamples]fr.Element + + for i := 1; i <= nbSamples; i++ { + sampleScalars[i-1].SetUint64(uint64(i)). + Mul(&sampleScalars[i-1], &mixer) + } + + for i := 5; i <= pow; i++ { + using := 1 << i + + b.Run(fmt.Sprintf("%d points", using), func(b *testing.B) { + b.ResetTimer() + for range b.N { + _ = BatchScalarMultiplicationG1(&g1GenAff, sampleScalars[:using]) + } + }) + } +} + +func BenchmarkG1JacScalarMultiplication(b *testing.B) { + for i := 0; i <= fr.Modulus().BitLen(); i += 8 { + bound := new(big.Int).Lsh(big.NewInt(1), uint(i)) + scalar, err := crand.Int(crand.Reader, bound) + if err != nil { + b.Fatalf("failed to generate random scalar: %v", err) + } + + var doubleAndAdd G1Jac + b.Run(fmt.Sprintf("method=window/scalarwidth=%d", i), func(b *testing.B) { + b.ResetTimer() + for range b.N { + doubleAndAdd.mulWindowed(&g1Gen, scalar) + } + }) + + } +} + +func BenchmarkG1JacScalarMultiplicationMethod(b *testing.B) { + for i := 0; i <= fr.Modulus().BitLen(); i += 8 { + bound := new(big.Int).Lsh(big.NewInt(1), uint(i)) + scalar, err := crand.Int(crand.Reader, bound) + if err != nil { + b.Fatalf("failed to generate random scalar: %v", err) + } + + var res G1Jac + b.Run(fmt.Sprintf("scalarwidth=%d", i), func(b *testing.B) { + b.ResetTimer() + for range b.N { + res.ScalarMultiplication(&g1Gen, scalar) + } + }) + } +} + +func BenchmarkG1JacAdd(b *testing.B) { + var a G1Jac + a.Double(&g1Gen) + b.ResetTimer() + for range b.N { + a.AddAssign(&g1Gen) + } +} + +func BenchmarkG1JacAddMixed(b *testing.B) { + var a G1Jac + a.Double(&g1Gen) + + var c G1Affine + c.FromJacobian(&g1Gen) + b.ResetTimer() + for range b.N { + a.AddMixed(&c) + } + +} + +func BenchmarkG1JacDouble(b *testing.B) { + var a G1Jac + a.Set(&g1Gen) + b.ResetTimer() + for range b.N { + a.DoubleAssign() + } + +} + +func BenchmarkG1JacExtAddMixed(b *testing.B) { + var a g1JacExtended + a.doubleMixed(&g1GenAff) + + var c G1Affine + c.FromJacobian(&g1Gen) + b.ResetTimer() + for range b.N { + a.addMixed(&c) + } +} + +func BenchmarkG1JacExtSubMixed(b *testing.B) { + var a g1JacExtended + a.doubleMixed(&g1GenAff) + + var c G1Affine + c.FromJacobian(&g1Gen) + b.ResetTimer() + for range b.N { + a.subMixed(&c) + } +} + +func BenchmarkG1JacExtDoubleMixed(b *testing.B) { + var a g1JacExtended + a.doubleMixed(&g1GenAff) + + var c G1Affine + c.FromJacobian(&g1Gen) + b.ResetTimer() + for range b.N { + a.doubleMixed(&c) + } +} + +func BenchmarkG1JacExtDoubleNegMixed(b *testing.B) { + var a g1JacExtended + a.doubleMixed(&g1GenAff) + + var c G1Affine + c.FromJacobian(&g1Gen) + b.ResetTimer() + for range b.N { + a.doubleNegMixed(&c) + } +} + +func BenchmarkG1JacExtAdd(b *testing.B) { + var a, c g1JacExtended + a.doubleMixed(&g1GenAff) + c.double(&a) + + b.ResetTimer() + for range b.N { + a.add(&c) + } +} + +func BenchmarkG1JacExtDouble(b *testing.B) { + var a g1JacExtended + a.doubleMixed(&g1GenAff) + + b.ResetTimer() + for range b.N { + a.double(&a) + } +} + +func BenchmarkG1AffineAdd(b *testing.B) { + var a G1Affine + a.Double(&g1GenAff) + b.ResetTimer() + for range b.N { + a.Add(&a, &g1GenAff) + } +} + +func BenchmarkG1AffineDouble(b *testing.B) { + var a G1Affine + a.Double(&g1GenAff) + b.ResetTimer() + for range b.N { + a.Double(&a) + } +} + +func fuzzG1Jac(p *G1Jac, f fptower.E8) G1Jac { + var res G1Jac + res.X.Mul(&p.X, &f).Mul(&res.X, &f) + res.Y.Mul(&p.Y, &f).Mul(&res.Y, &f).Mul(&res.Y, &f) + res.Z.Mul(&p.Z, &f) + return res +} + +func fuzzg1JacExtended(p *g1JacExtended, f fptower.E8) g1JacExtended { + var res g1JacExtended + var ff, fff fptower.E8 + ff.Square(&f) + fff.Mul(&ff, &f) + res.X.Mul(&p.X, &ff) + res.Y.Mul(&p.Y, &fff) + res.ZZ.Mul(&p.ZZ, &ff) + res.ZZZ.Mul(&p.ZZZ, &fff) + return res +} diff --git a/ecc/kb8/internal/fptower/fptower.go b/ecc/kb8/internal/fptower/fptower.go new file mode 100644 index 0000000000..2b2a023c2e --- /dev/null +++ b/ecc/kb8/internal/fptower/fptower.go @@ -0,0 +1,19 @@ +package fptower + +import ext "github.com/consensys/gnark-crypto/field/koalabear/extensions" + +type E2 = ext.E2 +type E4 = ext.E4 +type E8 = ext.E8 + +func BatchInvertE2(a []E2) []E2 { + return ext.BatchInvertE2(a) +} + +func BatchInvertE4(a []E4) []E4 { + return ext.BatchInvertE4(a) +} + +func BatchInvertE8(a []E8) []E8 { + return ext.BatchInvertE8(a) +} diff --git a/ecc/kb8/kb8.go b/ecc/kb8/kb8.go new file mode 100644 index 0000000000..6ae9ac158e --- /dev/null +++ b/ecc/kb8/kb8.go @@ -0,0 +1,82 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Package kb8 for efficient elliptic curve implementation for kb8 (koalabear-8). +// This curve is intended for circuit operations defined over the KoalaBear field. +// In particular, it is used for multiset-hash constructions appearing in zkVM +// memory arguments. +// +// kb8: A curve over 𝔽p⁸ with +// +// 𝔽p: p=2130706433 = 2³¹-2²⁴+1 +// 𝔽r: r=424804331891979973455971894938199991839487883914575852667663156896715214921 +// 𝔽p²[u] = 𝔽p/u²-3 +// 𝔽p⁴[v] = 𝔽p²/v²-u +// 𝔽p⁸[w] = 𝔽p⁴/w²-v +// (E/𝔽p⁸): Y²=X³-3X+17w⁵ +// r ∣ #E(𝔽p⁸) +// +// # Warning +// +// This code has not been audited and is provided as-is. In particular, there +// is no security guarantee such as constant time implementation or side-channel +// attack resistance. +package kb8 + +import ( + "math/big" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" +) + +// ID kb8 ID. +const ID = ecc.KB8 + +// aCurveCoeff and bCurveCoeff are the coefficients of Y²=X³+ax+b. +var aCurveCoeff, bCurveCoeff fptower.E8 + +// Generator and infinity point of G1. +var ( + g1Gen G1Jac + g1GenAff G1Affine + g1Infinity G1Jac +) + +// xGen is only used by the generic mulBySeed helper. kb8 has no seed-based endomorphism, +// so keep it as the identity scalar. +var xGen big.Int + +func init() { + aCurveCoeff.C0.B0.A0.SetUint64(3) + aCurveCoeff.Neg(&aCurveCoeff) + + bCurveCoeff.C1.B0.A1.SetUint64(17) + + g1Gen.X.C0.B0.A0.SetUint64(4) + g1Gen.Y.SetString( + "177975122", "773296979", + "473899551", "417630813", + "1724315640", "307114955", + "459074134", "668770585", + ) + g1Gen.Z.SetOne() + g1GenAff.FromJacobian(&g1Gen) + + g1Infinity.X.SetOne() + g1Infinity.Y.SetOne() + + xGen.SetInt64(1) +} + +// Generators returns the generator of G1 in Jacobian and affine form. +func Generators() (g1Jac G1Jac, g1Aff G1Affine) { + g1Jac = g1Gen + g1Aff = g1GenAff + return +} + +// CurveCoefficients returns the coefficients of the curve equation. +func CurveCoefficients() (a, b fptower.E8) { + return aCurveCoeff, bCurveCoeff +} diff --git a/ecc/kb8/marshal.go b/ecc/kb8/marshal.go new file mode 100644 index 0000000000..ae08c2e6ad --- /dev/null +++ b/ecc/kb8/marshal.go @@ -0,0 +1,869 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package kb8 + +import ( + "encoding/binary" + "errors" + "io" + "reflect" + "sync/atomic" + + "github.com/consensys/gnark-crypto/ecc/kb8/fp" + "github.com/consensys/gnark-crypto/ecc/kb8/fr" + "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + "github.com/consensys/gnark-crypto/parallel" +) + +// To encode G1Affine and G2Affine points, we mask the most significant bits with these bits to specify without ambiguity +// metadata needed for point (de)compression +const ( + mMask byte = 0xff + mUncompressed byte = 0x00 + mUncompressedInfinity byte = 0x01 + mCompressedSmallest byte = 0x02 + mCompressedLargest byte = 0x03 + mCompressedInfinity byte = 0x04 +) + +var ( + ErrInvalidInfinityEncoding = errors.New("invalid infinity point encoding") + ErrInvalidEncoding = errors.New("invalid point encoding") +) + +// Encoder writes kb8 object values to an output stream +type Encoder struct { + w io.Writer + n int64 // written bytes + raw bool // raw vs compressed encoding +} + +// Decoder reads kb8 object values from an inbound stream +type Decoder struct { + r io.Reader + n int64 // read bytes + subGroupCheck bool // default to true +} + +// NewDecoder returns a binary decoder supporting curve kb8 objects in both +// compressed and uncompressed (raw) forms +func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { + d := &Decoder{r: r, subGroupCheck: true} + + for _, o := range options { + o(d) + } + + return d +} + +// Decode reads the binary encoding of v from the stream +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, *[]G1Affine or *[]G2Affine +func (dec *Decoder) Decode(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { + return errors.New("kb8 decoder: unsupported type, need pointer") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + // in particular, careful attention must be given to usage of Bytes() method on Elements and Points + // that return an array (not a slice) of bytes. Using this is beneficial to minimize memory allocations + // in very large (de)serialization upstream in gnark. + // (but detrimental to code readability here) + + var read64 int64 + if vf, ok := v.(io.ReaderFrom); ok { + read64, err = vf.ReadFrom(dec.r) + dec.n += read64 + return + } + var buf [SizeOfG1AffineUncompressed]byte + var read int + var sliceLen uint32 + + switch t := v.(type) { + case *[][]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([][]uint64, sliceLen) + + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { + return + } + (*t)[i] = make([]uint64, sliceLen) + for j := range (*t)[i] { + if (*t)[i][j], err = dec.readUint64(); err != nil { + return + } + } + } + return + case *[]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([]uint64, sliceLen) + for i := range *t { + if (*t)[i], err = dec.readUint64(); err != nil { + return + } + } + return + case *fr.Element: + read, err = io.ReadFull(dec.r, buf[:fr.Bytes]) + dec.n += int64(read) + if err != nil { + return + } + err = t.SetBytesCanonical(buf[:fr.Bytes]) + return + case *fp.Element: + read, err = io.ReadFull(dec.r, buf[:fp.Bytes]) + dec.n += int64(read) + if err != nil { + return + } + err = t.SetBytesCanonical(buf[:fp.Bytes]) + return + case *[]fr.Element: + read64, err = (*fr.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[]fp.Element: + read64, err = (*fp.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len(*t) != int(sliceLen) { + *t = make([][]fr.Element, sliceLen) + } + for i := range *t { + read64, err = (*fr.Vector)(&(*t)[i]).ReadFrom(dec.r) + dec.n += read64 + } + return + case *[][][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len(*t) != int(sliceLen) { + *t = make([][][]fr.Element, sliceLen) + } + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len((*t)[i]) != int(sliceLen) { + (*t)[i] = make([][]fr.Element, sliceLen) + } + for j := range (*t)[i] { + read64, err = (*fr.Vector)(&(*t)[i][j]).ReadFrom(dec.r) + dec.n += read64 + } + } + return + case *G1Affine: + // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) + dec.n += int64(read) + if err != nil { + return + } + nbBytes := SizeOfG1AffineCompressed + + // 111, 011, 001 --> invalid mask + if isMaskInvalid(buf[0]) { + err = ErrInvalidEncoding + return + } + + // most significant byte contains metadata + if !isCompressed(buf[0]) { + nbBytes = SizeOfG1AffineUncompressed + // we read more. + read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + } + _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) + return + case *[]G1Affine: + sliceLen, err = dec.readUint32() + if err != nil { + return + } + if len(*t) != int(sliceLen) || *t == nil { + *t = make([]G1Affine, sliceLen) + } + compressed := make([]bool, sliceLen) + for i := range len(*t) { + + // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) + dec.n += int64(read) + if err != nil { + return + } + nbBytes := SizeOfG1AffineCompressed + + // 111, 011, 001 --> invalid mask + if isMaskInvalid(buf[0]) { + err = ErrInvalidEncoding + return + } + + // most significant byte contains metadata + if !isCompressed(buf[0]) { + nbBytes = SizeOfG1AffineUncompressed + // we read more. + read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + _, err = (*t)[i].setBytes(buf[:nbBytes], false) + if err != nil { + return + } + } else { + var r bool + if r, err = (*t)[i].unsafeSetCompressedBytes(buf[:nbBytes]); err != nil { + return + } + compressed[i] = !r + } + } + var nbErrs uint64 + parallel.Execute(len(compressed), func(start, end int) { + for i := start; i < end; i++ { + if compressed[i] { + if err := (*t)[i].unsafeComputeY(dec.subGroupCheck); err != nil { + atomic.AddUint64(&nbErrs, 1) + } + } else if dec.subGroupCheck { + if !(*t)[i].IsInSubGroup() { + atomic.AddUint64(&nbErrs, 1) + } + } + } + }) + if nbErrs != 0 { + return errors.New("point decompression failed") + } + + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New("kb8 encoder: unsupported type") + } + err = binary.Read(dec.r, binary.BigEndian, t) + if err == nil { + dec.n += int64(n) + } + return + } +} + +// BytesRead return total bytes read from reader +func (dec *Decoder) BytesRead() int64 { + return dec.n +} + +func (dec *Decoder) readUint32() (r uint32, err error) { + var read int + var buf [4]byte + read, err = io.ReadFull(dec.r, buf[:4]) + dec.n += int64(read) + if err != nil { + return + } + r = binary.BigEndian.Uint32(buf[:4]) + return +} + +func (dec *Decoder) readUint64() (r uint64, err error) { + var read int + var buf [8]byte + read, err = io.ReadFull(dec.r, buf[:]) + dec.n += int64(read) + if err != nil { + return + } + r = binary.BigEndian.Uint64(buf[:]) + return +} + +// isMaskInvalid returns true if the mask is invalid +func isMaskInvalid(msb byte) bool { + mData := msb & mMask + return ((mData == (0b111 << 5)) || (mData == (0b011 << 5)) || (mData == (0b001 << 5))) +} + +func isCompressed(msb byte) bool { + mData := msb & mMask + return mData != mUncompressed && mData != mUncompressedInfinity +} + +// NewEncoder returns a binary encoder supporting curve kb8 objects +func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { + // default settings + enc := &Encoder{ + w: w, + n: 0, + raw: false, + } + + // handle options + for _, option := range options { + option(enc) + } + + return enc +} + +// Encode writes the binary encoding of v to the stream +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, []G1Affine, []G2Affine, *[]G1Affine or *[]G2Affine +func (enc *Encoder) Encode(v any) (err error) { + if enc.raw { + return enc.encodeRaw(v) + } + return enc.encode(v) +} + +// BytesWritten return total bytes written on writer +func (enc *Encoder) BytesWritten() int64 { + return enc.n +} + +// RawEncoding returns an option to use in NewEncoder(...) which sets raw encoding mode to true +// points will not be compressed using this option +func RawEncoding() func(*Encoder) { + return func(enc *Encoder) { + enc.raw = true + } +} + +// NoSubgroupChecks returns an option to use in NewDecoder(...) which disable subgroup checks on the points +// the decoder will read. Use with caution, as crafted points from an untrusted source can lead to crypto-attacks. +func NoSubgroupChecks() func(*Decoder) { + return func(dec *Decoder) { + dec.subGroupCheck = false + } +} + +// isZeroed checks that the provided bytes are at 0 +func isZeroed(firstByte byte, buf []byte) bool { + if firstByte != 0 { + return false + } + for _, b := range buf { + if b != 0 { + return false + } + } + return true +} + +func (enc *Encoder) encode(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New(" encoder: can't encode ") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + + var written int + + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) + case *fr.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *fp.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *G1Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: + // write slice length + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 + } + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { + return + } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } + } + return + case *[]G1Affine: + return enc.encode(*t) + case []G1Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG1AffineCompressed]byte + + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New(" encoder: unsupported type") + } + err = binary.Write(enc.w, binary.BigEndian, t) + enc.n += int64(n) + return + } +} + +func (enc *Encoder) encodeRaw(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New(" encoder: can't encode ") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + + var written int + + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) + case *fr.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *fp.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *G1Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: + // write slice length + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 + } + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { + return + } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } + } + return + case *[]G1Affine: + return enc.encodeRaw(*t) + case []G1Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG1AffineUncompressed]byte + + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New(" encoder: unsupported type") + } + err = binary.Write(enc.w, binary.BigEndian, t) + enc.n += int64(n) + return + } +} + +func (enc *Encoder) writeUint64Slice(t []uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint64(t[i]); err != nil { + return + } + } + return nil +} + +func (enc *Encoder) writeUint64SliceSlice(t [][]uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint32(uint32(len(t[i]))); err != nil { + return + } + for j := range t[i] { + if err = enc.writeUint64(t[i][j]); err != nil { + return + } + } + } + return nil +} + +func (enc *Encoder) writeUint64(a uint64) error { + var buff [64 / 8]byte + binary.BigEndian.PutUint64(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + +func (enc *Encoder) writeUint32(a uint32) error { + var buff [32 / 8]byte + binary.BigEndian.PutUint32(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + +// SizeOfG1AffineCompressed represents the size in bytes that a G1Affine need in binary form, compressed +const SizeOfG1AffineCompressed = 1 + 4*8 + +// SizeOfG1AffineUncompressed represents the size in bytes that a G1Affine need in binary form, uncompressed +const SizeOfG1AffineUncompressed = 1 + 2*(4*8) + +func (p *G1Affine) Marshal() []byte { + b := p.RawBytes() + return b[:] +} + +func (p *G1Affine) Unmarshal(buf []byte) error { + _, err := p.SetBytes(buf) + return err +} + +func (p *G1Affine) Bytes() (res [SizeOfG1AffineCompressed]byte) { + if p.X.IsZero() && p.Y.IsZero() { + res[0] = mCompressedInfinity + return + } + res[0] = mCompressedSmallest + if p.Y.LexicographicallyLargest() { + res[0] = mCompressedLargest + } + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X.C1.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[5:5+fp.Bytes]), p.X.C1.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[9:9+fp.Bytes]), p.X.C1.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[13:13+fp.Bytes]), p.X.C1.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[17:17+fp.Bytes]), p.X.C0.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[21:21+fp.Bytes]), p.X.C0.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[25:25+fp.Bytes]), p.X.C0.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[29:29+fp.Bytes]), p.X.C0.B0.A0) + return +} + +func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { + if p.X.IsZero() && p.Y.IsZero() { + res[0] = mUncompressedInfinity + return + } + res[0] = mUncompressed + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[33:33+fp.Bytes]), p.Y.C1.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[37:37+fp.Bytes]), p.Y.C1.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[41:41+fp.Bytes]), p.Y.C1.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[45:45+fp.Bytes]), p.Y.C1.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[49:49+fp.Bytes]), p.Y.C0.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[53:53+fp.Bytes]), p.Y.C0.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[57:57+fp.Bytes]), p.Y.C0.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[61:61+fp.Bytes]), p.Y.C0.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X.C1.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[5:5+fp.Bytes]), p.X.C1.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[9:9+fp.Bytes]), p.X.C1.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[13:13+fp.Bytes]), p.X.C1.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[17:17+fp.Bytes]), p.X.C0.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[21:21+fp.Bytes]), p.X.C0.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[25:25+fp.Bytes]), p.X.C0.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[29:29+fp.Bytes]), p.X.C0.B0.A0) + return +} + +func (p *G1Affine) SetBytes(buf []byte) (int, error) { + return p.setBytes(buf, true) +} + +func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { + if len(buf) < SizeOfG1AffineCompressed { + return 0, io.ErrShortBuffer + } + mData := buf[0] + if isMaskInvalid(mData) { + return 0, ErrInvalidEncoding + } + if mData == mUncompressed || mData == mUncompressedInfinity { + if len(buf) < SizeOfG1AffineUncompressed { + return 0, io.ErrShortBuffer + } + } + if mData == mCompressedInfinity { + if !isZeroed(0, buf[1:SizeOfG1AffineCompressed]) { + return 0, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return SizeOfG1AffineCompressed, nil + } + if mData == mUncompressedInfinity { + if !isZeroed(0, buf[1:SizeOfG1AffineUncompressed]) { + return 0, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return SizeOfG1AffineUncompressed, nil + } + if err := p.X.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*0 : 1+fp.Bytes*1]); err != nil { + return 0, err + } + if err := p.X.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*1 : 1+fp.Bytes*2]); err != nil { + return 0, err + } + if err := p.X.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*2 : 1+fp.Bytes*3]); err != nil { + return 0, err + } + if err := p.X.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*3 : 1+fp.Bytes*4]); err != nil { + return 0, err + } + if err := p.X.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*4 : 1+fp.Bytes*5]); err != nil { + return 0, err + } + if err := p.X.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*5 : 1+fp.Bytes*6]); err != nil { + return 0, err + } + if err := p.X.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*6 : 1+fp.Bytes*7]); err != nil { + return 0, err + } + if err := p.X.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*7 : 1+fp.Bytes*8]); err != nil { + return 0, err + } + if mData == mUncompressed { + if err := p.Y.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*8 : 1+fp.Bytes*9]); err != nil { + return 0, err + } + if err := p.Y.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*9 : 1+fp.Bytes*10]); err != nil { + return 0, err + } + if err := p.Y.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*10 : 1+fp.Bytes*11]); err != nil { + return 0, err + } + if err := p.Y.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*11 : 1+fp.Bytes*12]); err != nil { + return 0, err + } + if err := p.Y.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*12 : 1+fp.Bytes*13]); err != nil { + return 0, err + } + if err := p.Y.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*13 : 1+fp.Bytes*14]); err != nil { + return 0, err + } + if err := p.Y.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*14 : 1+fp.Bytes*15]); err != nil { + return 0, err + } + if err := p.Y.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*15 : 1+fp.Bytes*16]); err != nil { + return 0, err + } + if subGroupCheck && !p.IsInSubGroup() { + return 0, errors.New("invalid point: subgroup check failed") + } + return SizeOfG1AffineUncompressed, nil + } + p.Y.C0.B0.A0[0] = uint32(mData) + if err := p.unsafeComputeY(subGroupCheck); err != nil { + return 0, err + } + return SizeOfG1AffineCompressed, nil +} + +func (p *G1Affine) unsafeComputeY(subGroupCheck bool) error { + mData := byte(p.Y.C0.B0.A0[0]) + var YSquared, Y, tmp fptower.E8 + YSquared.Square(&p.X).Mul(&YSquared, &p.X) + tmp.Mul(&p.X, &aCurveCoeff) + YSquared.Add(&YSquared, &tmp).Add(&YSquared, &bCurveCoeff) + if YSquared.Legendre() == -1 { + return errors.New("invalid compressed coordinate: square root doesn't exist") + } + Y.Sqrt(&YSquared) + if Y.LexicographicallyLargest() { + if mData == mCompressedSmallest { + Y.Neg(&Y) + } + } else if mData == mCompressedLargest { + Y.Neg(&Y) + } + p.Y = Y + if subGroupCheck && !p.IsInSubGroup() { + return errors.New("invalid point: subgroup check failed") + } + return nil +} + +func (p *G1Affine) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, err error) { + mData := buf[0] + if mData == mCompressedInfinity { + isInfinity = true + if !isZeroed(0, buf[1:SizeOfG1AffineCompressed]) { + return isInfinity, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return isInfinity, nil + } + if err := p.X.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*0 : 1+fp.Bytes*1]); err != nil { + return false, err + } + if err := p.X.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*1 : 1+fp.Bytes*2]); err != nil { + return false, err + } + if err := p.X.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*2 : 1+fp.Bytes*3]); err != nil { + return false, err + } + if err := p.X.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*3 : 1+fp.Bytes*4]); err != nil { + return false, err + } + if err := p.X.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*4 : 1+fp.Bytes*5]); err != nil { + return false, err + } + if err := p.X.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*5 : 1+fp.Bytes*6]); err != nil { + return false, err + } + if err := p.X.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*6 : 1+fp.Bytes*7]); err != nil { + return false, err + } + if err := p.X.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*7 : 1+fp.Bytes*8]); err != nil { + return false, err + } + p.Y.C0.B0.A0[0] = uint32(mData) + return isInfinity, nil +} diff --git a/ecc/kb8/marshal_test.go b/ecc/kb8/marshal_test.go new file mode 100644 index 0000000000..7a2612c691 --- /dev/null +++ b/ecc/kb8/marshal_test.go @@ -0,0 +1,372 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package kb8 + +import ( + "bytes" + crand "crypto/rand" + "io" + "math/big" + "math/rand/v2" + "reflect" + "testing" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/prop" + + "github.com/consensys/gnark-crypto/ecc/kb8/fp" + "github.com/consensys/gnark-crypto/ecc/kb8/fr" + "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" +) + +const ( + nbFuzzShort = 10 + nbFuzz = 100 +) + +func TestEncoder(t *testing.T) { + t.Parallel() + // TODO need proper fuzz testing here + + var inA uint64 + var inB fr.Element + var inC fp.Element + var inD G1Affine + var inE G1Affine + var inG []G1Affine + var inI []fp.Element + var inJ []fr.Element + var inK fr.Vector + var inL [][]fr.Element + var inM [][]uint64 + var inN [][][]fr.Element + + // set values of inputs + inA = rand.Uint64() //#nosec G404 weak rng is fine here + inB.MustSetRandom() + inC.MustSetRandom() + inD.ScalarMultiplication(&g1GenAff, new(big.Int).SetUint64(rand.Uint64())) //#nosec G404 weak rng is fine here + // inE --> infinity + inG = make([]G1Affine, 2) + inG[1] = inD + inI = make([]fp.Element, 3) + inI[2] = inD.X.C0.B0.A0 + inJ = make([]fr.Element, 0) + inK = make(fr.Vector, 42) + inK[41].SetUint64(42) + inL = [][]fr.Element{inJ, inK} + inM = [][]uint64{{1, 2}, {4}, {}} + inN = make([][][]fr.Element, 4) + for i := range 4 { + inN[i] = make([][]fr.Element, i+2) + for j := range i + 2 { + inNIJ := make(fr.Vector, j+3) + inNIJ.MustSetRandom() + inN[i][j] = inNIJ + } + } + + // encode them, compressed and raw + var buf, bufRaw bytes.Buffer + enc := NewEncoder(&buf) + encRaw := NewEncoder(&bufRaw, RawEncoding()) + toEncode := []any{inA, &inB, &inC, &inD, &inE, inG, inI, inJ, inK, inL, inM, inN} + for _, v := range toEncode { + if err := enc.Encode(v); err != nil { + t.Fatal(err) + } + if err := encRaw.Encode(v); err != nil { + t.Fatal(err) + } + } + + testDecode := func(t *testing.T, r io.Reader, n int64) { + dec := NewDecoder(r) + var outA uint64 + var outB fr.Element + var outC fp.Element + var outD G1Affine + var outE G1Affine + outE.X.SetOne() + outE.Y.SetOne() + var outG []G1Affine + var outI []fp.Element + var outJ []fr.Element + var outK fr.Vector + var outL [][]fr.Element + var outM [][]uint64 + var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} + for _, v := range toDecode { + if err := dec.Decode(v); err != nil { + t.Fatal(err) + } + } + + // compare values + if inA != outA { + t.Fatal("didn't encode/decode uint64 value properly") + } + + if !inB.Equal(&outB) || !inC.Equal(&outC) { + t.Fatal("decode(encode(Element) failed") + } + if !inD.Equal(&outD) || !inE.Equal(&outE) { + t.Fatal("decode(encode(G1Affine) failed") + } + if len(inG) != len(outG) { + t.Fatal("decode(encode(slice(points))) failed") + } + for i := range len(inG) { + if !inG[i].Equal(&outG[i]) { + t.Fatal("decode(encode(slice(points))) failed") + } + } + if (len(inI) != len(outI)) || (len(inJ) != len(outJ)) { + t.Fatal("decode(encode(slice(elements))) failed") + } + for i := range len(inI) { + if !inI[i].Equal(&outI[i]) { + t.Fatal("decode(encode(slice(elements))) failed") + } + } + if !reflect.DeepEqual(inK, outK) { + t.Fatal("decode(encode(vector)) failed") + } + if !reflect.DeepEqual(inL, outL) { + t.Fatal("decode(encode(slice²(elements))) failed") + } + if !reflect.DeepEqual(inM, outM) { + t.Fatal("decode(encode(slice²(uint64))) failed") + } + if !reflect.DeepEqual(inN, outN) { + t.Fatal("decode(encode(slice^{3}(uint64))) failed") + } + if n != dec.BytesRead() { + t.Fatal("bytes read don't match bytes written") + } + } + + // decode them + testDecode(t, &buf, enc.BytesWritten()) + testDecode(t, &bufRaw, encRaw.BytesWritten()) + +} + +func TestIsCompressed(t *testing.T) { + t.Parallel() + var g1Inf, g1 G1Affine + g1 = g1GenAff + + { + b := g1Inf.Bytes() + if !isCompressed(b[0]) { + t.Fatal("g1Inf.Bytes() should be compressed") + } + } + + { + b := g1Inf.RawBytes() + if isCompressed(b[0]) { + t.Fatal("g1Inf.RawBytes() should be uncompressed") + } + } + + { + b := g1.Bytes() + if !isCompressed(b[0]) { + t.Fatal("g1.Bytes() should be compressed") + } + } + + { + b := g1.RawBytes() + if isCompressed(b[0]) { + t.Fatal("g1.RawBytes() should be uncompressed") + } + } + +} + +func TestG1AffineInvalidBitMask(t *testing.T) { + t.Parallel() + var buf [SizeOfG1AffineCompressed]byte + crand.Read(buf[:]) + + var p G1Affine + buf[0] = 0b111 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } + buf[0] = 0b011 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } + buf[0] = 0b001 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } +} + +func TestG1AffineSerialization(t *testing.T) { + t.Parallel() + // test round trip serialization of infinity + { + // compressed + { + var p1, p2 G1Affine + p2.X.MustSetRandom() + p2.Y.MustSetRandom() + buf := p1.Bytes() + n, err := p2.SetBytes(buf[:]) + if err != nil { + t.Fatal(err) + } + if n != SizeOfG1AffineCompressed { + t.Fatal("invalid number of bytes consumed in buffer") + } + if !(p2.X.IsZero() && p2.Y.IsZero()) { // nolint QF1001 + t.Fatal("deserialization of uncompressed infinity point is not infinity") + } + } + + // uncompressed + { + var p1, p2 G1Affine + p2.X.MustSetRandom() + p2.Y.MustSetRandom() + buf := p1.RawBytes() + n, err := p2.SetBytes(buf[:]) + if err != nil { + t.Fatal(err) + } + if n != SizeOfG1AffineUncompressed { + t.Fatal("invalid number of bytes consumed in buffer") + } + if !(p2.X.IsZero() && p2.Y.IsZero()) { // nolint QF1001 + t.Fatal("deserialization of uncompressed infinity point is not infinity") + } + } + } + + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + properties.Property("[G1] Affine SetBytes(RawBytes) should stay the same", prop.ForAll( + func(a fp.Element) bool { + var start, end G1Affine + var ab big.Int + a.BigInt(&ab) + start.ScalarMultiplication(&g1GenAff, &ab) + + buf := start.RawBytes() + n, err := end.SetBytes(buf[:]) + if err != nil { + return false + } + if n != SizeOfG1AffineUncompressed { + return false + } + return start.X.Equal(&end.X) && start.Y.Equal(&end.Y) + }, + GenFp(), + )) + + properties.Property("[G1] Affine SetBytes(Bytes()) should stay the same", prop.ForAll( + func(a fp.Element) bool { + var start, end G1Affine + var ab big.Int + a.BigInt(&ab) + start.ScalarMultiplication(&g1GenAff, &ab) + + buf := start.Bytes() + n, err := end.SetBytes(buf[:]) + if err != nil { + return false + } + if n != SizeOfG1AffineCompressed { + return false + } + return start.X.Equal(&end.X) && start.Y.Equal(&end.Y) + }, + GenFp(), + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +// define Gopters generators + +// GenFr generates an Fr element +func GenFr() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var elmt fr.Element + elmt.MustSetRandom() + + return gopter.NewGenResult(elmt, gopter.NoShrinker) + } +} + +// GenFp generates an Fp element +func GenFp() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var elmt fp.Element + elmt.MustSetRandom() + + return gopter.NewGenResult(elmt, gopter.NoShrinker) + } +} + +// GenE2 generates an fptower.E2 elmt +func GenE2() gopter.Gen { + return gopter.CombineGens( + GenFp(), + GenFp(), + ).Map(func(values []any) fptower.E2 { + return fptower.E2{A0: values[0].(fp.Element), A1: values[1].(fp.Element)} + }) +} + +// GenE4 generates an fptower.E4 elmt +func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) +} + +// GenE8 generates an fptower.E8 elmt +func GenE8() gopter.Gen { + return gopter.CombineGens( + GenE4(), + GenE4(), + ).Map(func(values []any) fptower.E8 { + return fptower.E8{C0: values[0].(fptower.E4), C1: values[1].(fptower.E4)} + }) +} + +// GenBigInt generates a big.Int +func GenBigInt() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var s big.Int + var b [fp.Bytes]byte + _, err := crand.Read(b[:]) + if err != nil { + panic(err) + } + s.SetBytes(b[:]) + genResult := gopter.NewGenResult(s, gopter.NoShrinker) + return genResult + } +} diff --git a/ecc/kb8/multiexp.go b/ecc/kb8/multiexp.go new file mode 100644 index 0000000000..6baf9f7964 --- /dev/null +++ b/ecc/kb8/multiexp.go @@ -0,0 +1,532 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package kb8 + +import ( + "errors" + "math" + "runtime" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/kb8/fr" + "github.com/consensys/gnark-crypto/parallel" +) + +// MultiExp implements section 4 of https://eprint.iacr.org/2012/549.pdf +// +// This call return an error if len(scalars) != len(points) or if provided config is invalid. +func (p *G1Affine) MultiExp(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { + var _p G1Jac + if _, err := _p.MultiExp(points, scalars, config); err != nil { + return nil, err + } + p.FromJacobian(&_p) + return p, nil +} + +// MultiExp implements section 4 of https://eprint.iacr.org/2012/549.pdf +// +// This call return an error if len(scalars) != len(points) or if provided config is invalid. +func (p *G1Jac) MultiExp(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { + // TODO @gbotrel replace the ecc.MultiExpConfig by a Option pattern for maintainability. + // note: + // each of the msmCX method is the same, except for the c constant it declares + // duplicating (through template generation) these methods allows to declare the buckets on the stack + // the choice of c needs to be improved: + // there is a theoretical value that gives optimal asymptotics + // but in practice, other factors come into play, including: + // * if c doesn't divide 64, the word size, then we're bound to select bits over 2 words of our scalars, instead of 1 + // * number of CPUs + // * cache friendliness (which depends on the host, G1 or G2... ) + // --> for example, on BN254, a G1 point fits into one cache line of 64bytes, but a G2 point don't. + + // for each msmCX + // step 1 + // we compute, for each scalars over c-bit wide windows, nbChunk digits + // if the digit is larger than 2^{c-1}, then, we borrow 2^c from the next window and subtract + // 2^{c} to the current digit, making it negative. + // negative digits will be processed in the next step as adding -G into the bucket instead of G + // (computing -G is cheap, and this saves us half of the buckets) + // step 2 + // buckets are declared on the stack + // notice that we have 2^{c-1} buckets instead of 2^{c} (see step1) + // we use jacobian extended formulas here as they are faster than mixed addition + // msmProcessChunk places points into buckets base on their selector and return the weighted bucket sum in given channel + // step 3 + // reduce the buckets weighed sums into our result (msmReduceChunk) + + // ensure len(points) == len(scalars) + nbPoints := len(points) + if nbPoints != len(scalars) { + return nil, errors.New("len(points) != len(scalars)") + } + + // if nbTasks is not set, use all available CPUs + if config.NbTasks <= 0 { + config.NbTasks = runtime.NumCPU() * 2 + } else if config.NbTasks > 1024 { + return nil, errors.New("invalid config: config.NbTasks > 1024") + } + + // here, we compute the best C for nbPoints + // we split recursively until nbChunks(c) >= nbTasks, + bestC := func(nbPoints int) uint64 { + // implemented msmC methods (the c we use must be in this slice) + implementedCs := []uint64{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + var C uint64 + // approximate cost (in group operations) + // cost = bits/c * (nbPoints + 2^{c}) + // this needs to be verified empirically. + // for example, on a MBP 2016, for G2 MultiExp > 8M points, hand picking c gives better results + min := math.MaxFloat64 + for _, c := range implementedCs { + cc := (fr.Bits + 1) * (nbPoints + (1 << c)) + cost := float64(cc) / float64(c) + if cost < min { + min = cost + C = c + } + } + return C + } + + C := bestC(nbPoints) + nbChunks := int(computeNbChunks(C)) + + // should we recursively split the msm in half? (see below) + // we want to minimize the execution time of the algorithm; + // splitting the msm will **add** operations, but if it allows to use more CPU, it might be worth it. + + // costFunction returns a metric that represent the "wall time" of the algorithm + costFunction := func(nbTasks, nbCpus, costPerTask int) int { + // cost for the reduction of all tasks (msmReduceChunk) + totalCost := nbTasks + + // cost for the computation of each task (msmProcessChunk) + for nbTasks >= nbCpus { + nbTasks -= nbCpus + totalCost += costPerTask + } + if nbTasks > 0 { + totalCost += costPerTask + } + return totalCost + } + + // costPerTask is the approximate number of group ops per task + costPerTask := func(c uint64, nbPoints int) int { return (nbPoints + int((1 << c))) } + + costPreSplit := costFunction(nbChunks, config.NbTasks, costPerTask(C, nbPoints)) + + cPostSplit := bestC(nbPoints / 2) + nbChunksPostSplit := int(computeNbChunks(cPostSplit)) + costPostSplit := costFunction(nbChunksPostSplit*2, config.NbTasks, costPerTask(cPostSplit, nbPoints/2)) + + // if the cost of the split msm is lower than the cost of the non split msm, we split + if costPostSplit < costPreSplit { + config.NbTasks = int(math.Ceil(float64(config.NbTasks) / 2.0)) + var _p G1Jac + chDone := make(chan struct{}, 1) + go func() { + _p.MultiExp(points[:nbPoints/2], scalars[:nbPoints/2], config) + close(chDone) + }() + p.MultiExp(points[nbPoints/2:], scalars[nbPoints/2:], config) + <-chDone + p.AddAssign(&_p) + return p, nil + } + + // if we don't split, we use the best C we found + _innerMsmG1(p, C, points, scalars, config) + + return p, nil +} + +func _innerMsmG1(p *G1Jac, c uint64, points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) *G1Jac { + // partition the scalars + digits, chunkStats := partitionScalars(scalars, c, config.NbTasks) + + nbChunks := computeNbChunks(c) + + // for each chunk, spawn one go routine that'll loop through all the scalars in the + // corresponding bit-window + // note that buckets is an array allocated on the stack and this is critical for performance + + // each go routine sends its result in chChunks[i] channel + chChunks := make([]chan g1JacExtended, nbChunks) + for i := range len(chChunks) { + chChunks[i] = make(chan g1JacExtended, 1) + } + + // we use a semaphore to limit the number of go routines running concurrently + // (only if nbTasks < nbCPU) + var sem chan struct{} + if config.NbTasks < runtime.NumCPU() { + // we add nbChunks because if chunk is overweight we split it in two + sem = make(chan struct{}, config.NbTasks+int(nbChunks)) + for range config.NbTasks { + sem <- struct{}{} + } + defer func() { + close(sem) + }() + } + + // the last chunk may be processed with a different method than the rest, as it could be smaller. + n := len(points) + for j := int(nbChunks - 1); j >= 0; j-- { + processChunk := getChunkProcessorG1(c, chunkStats[j]) + if j == int(nbChunks-1) { + processChunk = getChunkProcessorG1(lastC(c), chunkStats[j]) + } + if chunkStats[j].weight >= 115 { + // we split this in more go routines since this chunk has more work to do than the others. + // else what would happen is this go routine would finish much later than the others. + chSplit := make(chan g1JacExtended, 2) + split := n / 2 + + if sem != nil { + sem <- struct{}{} // add another token to the semaphore, since we split in two. + } + go processChunk(uint64(j), chSplit, c, points[:split], digits[j*n:(j*n)+split], sem) + go processChunk(uint64(j), chSplit, c, points[split:], digits[(j*n)+split:(j+1)*n], sem) + go func(chunkID int) { + s1 := <-chSplit + s2 := <-chSplit + close(chSplit) + s1.add(&s2) + chChunks[chunkID] <- s1 + }(j) + continue + } + go processChunk(uint64(j), chChunks[j], c, points, digits[j*n:(j+1)*n], sem) + } + + return msmReduceChunkG1Affine(p, int(c), chChunks[:]) +} + +// getChunkProcessorG1 decides, depending on c window size and statistics for the chunk +// to return the best algorithm to process the chunk. +func getChunkProcessorG1(c uint64, stat chunkStat) func(chunkID uint64, chRes chan<- g1JacExtended, c uint64, points []G1Affine, digits []uint16, sem chan struct{}) { + switch c { + + case 2: + return processChunkG1Jacobian[bucketg1JacExtendedC2] + case 3: + return processChunkG1Jacobian[bucketg1JacExtendedC3] + case 4: + return processChunkG1Jacobian[bucketg1JacExtendedC4] + case 5: + return processChunkG1Jacobian[bucketg1JacExtendedC5] + case 6: + return processChunkG1Jacobian[bucketg1JacExtendedC6] + case 7: + return processChunkG1Jacobian[bucketg1JacExtendedC7] + case 8: + return processChunkG1Jacobian[bucketg1JacExtendedC8] + case 9: + return processChunkG1Jacobian[bucketg1JacExtendedC9] + case 10: + const batchSize = 80 + // here we could check some chunk statistic (deviation, ...) to determine if calling + // the batch affine version is worth it. + if stat.nbBucketFilled < batchSize { + // clear indicator that batch affine method is not appropriate here. + return processChunkG1Jacobian[bucketg1JacExtendedC10] + } + return processChunkG1BatchAffine[bucketg1JacExtendedC10, bucketG1AffineC10, bitSetC10, pG1AffineC10, ppG1AffineC10, qG1AffineC10, cG1AffineC10] + case 11: + const batchSize = 150 + // here we could check some chunk statistic (deviation, ...) to determine if calling + // the batch affine version is worth it. + if stat.nbBucketFilled < batchSize { + // clear indicator that batch affine method is not appropriate here. + return processChunkG1Jacobian[bucketg1JacExtendedC11] + } + return processChunkG1BatchAffine[bucketg1JacExtendedC11, bucketG1AffineC11, bitSetC11, pG1AffineC11, ppG1AffineC11, qG1AffineC11, cG1AffineC11] + case 12: + const batchSize = 200 + // here we could check some chunk statistic (deviation, ...) to determine if calling + // the batch affine version is worth it. + if stat.nbBucketFilled < batchSize { + // clear indicator that batch affine method is not appropriate here. + return processChunkG1Jacobian[bucketg1JacExtendedC12] + } + return processChunkG1BatchAffine[bucketg1JacExtendedC12, bucketG1AffineC12, bitSetC12, pG1AffineC12, ppG1AffineC12, qG1AffineC12, cG1AffineC12] + case 13: + const batchSize = 350 + // here we could check some chunk statistic (deviation, ...) to determine if calling + // the batch affine version is worth it. + if stat.nbBucketFilled < batchSize { + // clear indicator that batch affine method is not appropriate here. + return processChunkG1Jacobian[bucketg1JacExtendedC13] + } + return processChunkG1BatchAffine[bucketg1JacExtendedC13, bucketG1AffineC13, bitSetC13, pG1AffineC13, ppG1AffineC13, qG1AffineC13, cG1AffineC13] + case 14: + const batchSize = 400 + // here we could check some chunk statistic (deviation, ...) to determine if calling + // the batch affine version is worth it. + if stat.nbBucketFilled < batchSize { + // clear indicator that batch affine method is not appropriate here. + return processChunkG1Jacobian[bucketg1JacExtendedC14] + } + return processChunkG1BatchAffine[bucketg1JacExtendedC14, bucketG1AffineC14, bitSetC14, pG1AffineC14, ppG1AffineC14, qG1AffineC14, cG1AffineC14] + case 15: + const batchSize = 500 + // here we could check some chunk statistic (deviation, ...) to determine if calling + // the batch affine version is worth it. + if stat.nbBucketFilled < batchSize { + // clear indicator that batch affine method is not appropriate here. + return processChunkG1Jacobian[bucketg1JacExtendedC15] + } + return processChunkG1BatchAffine[bucketg1JacExtendedC15, bucketG1AffineC15, bitSetC15, pG1AffineC15, ppG1AffineC15, qG1AffineC15, cG1AffineC15] + case 16: + const batchSize = 640 + // here we could check some chunk statistic (deviation, ...) to determine if calling + // the batch affine version is worth it. + if stat.nbBucketFilled < batchSize { + // clear indicator that batch affine method is not appropriate here. + return processChunkG1Jacobian[bucketg1JacExtendedC16] + } + return processChunkG1BatchAffine[bucketg1JacExtendedC16, bucketG1AffineC16, bitSetC16, pG1AffineC16, ppG1AffineC16, qG1AffineC16, cG1AffineC16] + default: + // panic("will not happen c != previous values is not generated by templates") + return processChunkG1Jacobian[bucketg1JacExtendedC16] + } +} + +// msmReduceChunkG1Affine reduces the weighted sum of the buckets into the result of the multiExp +func msmReduceChunkG1Affine(p *G1Jac, c int, chChunks []chan g1JacExtended) *G1Jac { + var _p g1JacExtended + totalj := <-chChunks[len(chChunks)-1] + _p.Set(&totalj) + for j := len(chChunks) - 2; j >= 0; j-- { + for range c { + _p.double(&_p) + } + totalj := <-chChunks[j] + _p.add(&totalj) + } + + return p.unsafeFromJacExtended(&_p) +} + +// Fold computes the multi-exponentiation \sum_{i=0}^{len(points)-1} points[i] * +// combinationCoeff^i and stores the result in p. It returns error in case +// configuration is invalid. +func (p *G1Affine) Fold(points []G1Affine, combinationCoeff fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { + var _p G1Jac + if _, err := _p.Fold(points, combinationCoeff, config); err != nil { + return nil, err + } + p.FromJacobian(&_p) + return p, nil +} + +// Fold computes the multi-exponentiation \sum_{i=0}^{len(points)-1} points[i] * +// combinationCoeff^i and stores the result in p. It returns error in case +// configuration is invalid. +func (p *G1Jac) Fold(points []G1Affine, combinationCoeff fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { + scalars := make([]fr.Element, len(points)) + scalar := fr.NewElement(1) + for i := range len(points) { + scalars[i].Set(&scalar) + scalar.Mul(&scalar, &combinationCoeff) + } + return p.MultiExp(points, scalars, config) +} + +// selector stores the index, mask and shifts needed to select bits from a scalar +// it is used during the multiExp algorithm or the batch scalar multiplication +type selector struct { + index uint64 // index in the multi-word scalar to select bits from + mask uint64 // mask (c-bit wide) + shift uint64 // shift needed to get our bits on low positions + + multiWordSelect bool // set to true if we need to select bits from 2 words (case where c doesn't divide 64) + maskHigh uint64 // same than mask, for index+1 + shiftHigh uint64 // same than shift, for index+1 +} + +// return number of chunks for a given window size c +// the last chunk may be bigger to accommodate a potential carry from the NAF decomposition +func computeNbChunks(c uint64) uint64 { + return (fr.Bits + c - 1) / c +} + +// return the last window size for a scalar; +// this last window should accommodate a carry (from the NAF decomposition) +// it can be == c if we have 1 available bit +// it can be > c if we have 0 available bit +// it can be < c if we have 2+ available bits +func lastC(c uint64) uint64 { + nbAvailableBits := (computeNbChunks(c) * c) - fr.Bits + return c + 1 - nbAvailableBits +} + +type chunkStat struct { + // relative weight of work compared to other chunks. 100.0 -> nominal weight. + weight float32 + + // percentage of bucket filled in the window; + ppBucketFilled float32 + nbBucketFilled int +} + +// partitionScalars compute, for each scalars over c-bit wide windows, nbChunk digits +// if the digit is larger than 2^{c-1}, then, we borrow 2^c from the next window and subtract +// 2^{c} to the current digit, making it negative. +// negative digits can be processed in a later step as adding -G into the bucket instead of G +// (computing -G is cheap, and this saves us half of the buckets in the MultiExp or BatchScalarMultiplication) +func partitionScalars(scalars []fr.Element, c uint64, nbTasks int) ([]uint16, []chunkStat) { + // no benefit here to have more tasks than CPUs + if nbTasks > runtime.NumCPU() { + nbTasks = runtime.NumCPU() + } + + // number of c-bit radixes in a scalar + nbChunks := computeNbChunks(c) + + digits := make([]uint16, len(scalars)*int(nbChunks)) + + mask := uint64((1 << c) - 1) // low c bits are 1 + max := int(1<<(c-1)) - 1 // max value (inclusive) we want for our digits + cDivides64 := (64 % c) == 0 // if c doesn't divide 64, we may need to select over multiple words + + // compute offset and word selector / shift to select the right bits of our windows + selectors := make([]selector, nbChunks) + for chunk := range nbChunks { + jc := uint64(chunk * c) + d := selector{} + d.index = jc / 64 + d.shift = jc - (d.index * 64) + d.mask = mask << d.shift + d.multiWordSelect = !cDivides64 && d.shift > (64-c) && d.index < (fr.Limbs-1) + if d.multiWordSelect { + nbBitsHigh := d.shift - uint64(64-c) + d.maskHigh = (1 << nbBitsHigh) - 1 + d.shiftHigh = (c - nbBitsHigh) + } + selectors[chunk] = d + } + + parallel.Execute(len(scalars), func(start, end int) { + for i := start; i < end; i++ { + if scalars[i].IsZero() { + // everything is 0, no need to process this scalar + continue + } + scalar := scalars[i].Bits() + + var carry int + + // for each chunk in the scalar, compute the current digit, and an eventual carry + for chunk := range nbChunks - 1 { + s := selectors[chunk] + + // init with carry if any + digit := carry + carry = 0 + + // digit = value of the c-bit window + digit += int((scalar[s.index] & s.mask) >> s.shift) + + if s.multiWordSelect { + // we are selecting bits over 2 words + digit += int(scalar[s.index+1]&s.maskHigh) << s.shiftHigh + } + + // if the digit is larger than 2^{c-1}, then, we borrow 2^c from the next window and subtract + // 2^{c} to the current digit, making it negative. + if digit > max { + digit -= (1 << c) + carry = 1 + } + + // if digit is zero, no impact on result + if digit == 0 { + continue + } + + var bits uint16 + if digit > 0 { + bits = uint16(digit) << 1 + } else { + bits = (uint16(-digit-1) << 1) + 1 + } + digits[int(chunk)*len(scalars)+i] = bits + } + + // for the last chunk, we don't want to borrow from a next window + // (but may have a larger max value) + chunk := nbChunks - 1 + s := selectors[chunk] + // init with carry if any + digit := carry + // digit = value of the c-bit window + digit += int((scalar[s.index] & s.mask) >> s.shift) + if s.multiWordSelect { + // we are selecting bits over 2 words + digit += int(scalar[s.index+1]&s.maskHigh) << s.shiftHigh + } + digits[int(chunk)*len(scalars)+i] = uint16(digit) << 1 + } + + }, nbTasks) + + // aggregate chunk stats + chunkStats := make([]chunkStat, nbChunks) + if c <= 9 { + // no need to compute stats for small window sizes + return digits, chunkStats + } + parallel.Execute(len(chunkStats), func(start, end int) { + // for each chunk compute the statistics + for chunkID := start; chunkID < end; chunkID++ { + // indicates if a bucket is hit. + var b bitSetC16 + + // digits for the chunk + chunkDigits := digits[chunkID*len(scalars) : (chunkID+1)*len(scalars)] + + totalOps := 0 + nz := 0 // non zero buckets count + for _, digit := range chunkDigits { + if digit == 0 { + continue + } + totalOps++ + bucketID := digit >> 1 + if digit&1 == 0 { + bucketID -= 1 + } + if !b[bucketID] { + nz++ + b[bucketID] = true + } + } + chunkStats[chunkID].weight = float32(totalOps) // count number of ops for now, we will compute the weight after + chunkStats[chunkID].ppBucketFilled = (float32(nz) * 100.0) / float32(int(1<<(c-1))) + chunkStats[chunkID].nbBucketFilled = nz + } + }, nbTasks) + + totalOps := float32(0.0) + for _, stat := range chunkStats { + totalOps += stat.weight + } + + target := totalOps / float32(nbChunks) + if target != 0.0 { + // if target == 0, it means all the scalars are 0 everywhere, there is no work to be done. + for i := range len(chunkStats) { + chunkStats[i].weight = (chunkStats[i].weight * 100.0) / target + } + } + + return digits, chunkStats +} diff --git a/ecc/kb8/multiexp_affine.go b/ecc/kb8/multiexp_affine.go new file mode 100644 index 0000000000..914123bb98 --- /dev/null +++ b/ecc/kb8/multiexp_affine.go @@ -0,0 +1,369 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package kb8 + +import "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + +type batchOpG1Affine struct { + bucketID uint16 + point G1Affine +} + +// processChunkG1BatchAffine process a chunk of the scalars during the msm +// using affine coordinates for the buckets. To amortize the cost of the inverse in the affine addition +// we use a batch affine addition. +// +// this is derived from a PR by 0x0ece : https://github.com/Consensys/gnark-crypto/pull/249 +// See Section 5.3: ia.cr/2022/1396 +func processChunkG1BatchAffine[BJE ibg1JacExtended, B ibG1Affine, BS bitSet, TP pG1Affine, TPP ppG1Affine, TQ qOpsG1Affine, TC cG1Affine]( + chunk uint64, + chRes chan<- g1JacExtended, + c uint64, + points []G1Affine, + digits []uint16, + sem chan struct{}) { + + if sem != nil { + // if we are limited, wait for a token in the semaphore + <-sem + } + + // the batch affine addition needs independent points; in other words, for a window of batchSize + // we want to hit independent bucketIDs when processing the digit. if there is a conflict (we're trying + // to add 2 different points to the same bucket), then we push the conflicted point to a queue. + // each time the batch is full, we execute it, and tentatively put the points (if not conflict) + // from the top of the queue into the next batch. + // if the queue is full, we "flush it"; we sequentially add the points to the buckets in + // g1JacExtended coordinates. + // The reasoning behind this is the following; batchSize is chosen such as, for a uniformly random + // input, the number of conflicts is going to be low, and the element added to the queue should be immediately + // processed in the next batch. If it's not the case, then our inputs are not random; and we fallback to + // non-batch-affine version. + + // note that we have 2 sets of buckets + // 1 in G1Affine used with the batch affine additions + // 1 in g1JacExtended used in case the queue of conflicting points + var buckets B // in G1Affine coordinates, infinity point is represented as (0,0), no need to init + var bucketsJE BJE + for i := range len(buckets) { + bucketsJE[i].SetInfinity() + } + + // setup for the batch affine; + var ( + bucketIds BS // bitSet to signify presence of a bucket in current batch + cptAdd int // count the number of bucket + point added to current batch + R TPP // bucket references + P TP // points to be added to R (buckets); it is beneficial to store them on the stack (ie copy) + queue TQ // queue of points that conflict the current batch + qID int // current position in queue + ) + + batchSize := len(P) + + isFull := func() bool { return cptAdd == batchSize } + + executeAndReset := func() { + batchAddG1Affine[TP, TPP, TC](&R, &P, cptAdd) + var tmp BS + bucketIds = tmp + cptAdd = 0 + } + + addFromQueue := func(op batchOpG1Affine) { + // @precondition: must ensures bucket is not "used" in current batch + // note that there is a bit of duplicate logic between add and addFromQueue + // the reason is that as of Go 1.19.3, if we pass a pointer to the queue item (see add signature) + // the compiler will put the queue on the heap. + BK := &buckets[op.bucketID] + + // handle special cases with inf or -P / P + if BK.IsInfinity() { + BK.Set(&op.point) + return + } + if BK.X.Equal(&op.point.X) { + if BK.Y.Equal(&op.point.Y) { + // P + P: doubling, which should be quite rare -- + // we use the other set of buckets + bucketsJE[op.bucketID].addMixed(&op.point) + return + } + BK.SetInfinity() + return + } + + bucketIds[op.bucketID] = true + R[cptAdd] = BK + P[cptAdd] = op.point + cptAdd++ + } + + add := func(bucketID uint16, PP *G1Affine, isAdd bool) { + // @precondition: ensures bucket is not "used" in current batch + BK := &buckets[bucketID] + // handle special cases with inf or -P / P + if BK.IsInfinity() { + if isAdd { + BK.Set(PP) + } else { + BK.Neg(PP) + } + return + } + if BK.X.Equal(&PP.X) { + if BK.Y.Equal(&PP.Y) { + // P + P: doubling, which should be quite rare -- + if isAdd { + bucketsJE[bucketID].addMixed(PP) + } else { + BK.SetInfinity() + } + return + } + if isAdd { + BK.SetInfinity() + } else { + bucketsJE[bucketID].subMixed(PP) + } + return + } + + bucketIds[bucketID] = true + R[cptAdd] = BK + if isAdd { + P[cptAdd].Set(PP) + } else { + P[cptAdd].Neg(PP) + } + cptAdd++ + } + + flushQueue := func() { + for i := range qID { + bucketsJE[queue[i].bucketID].addMixed(&queue[i].point) + } + qID = 0 + } + + processTopQueue := func() { + for i := qID - 1; i >= 0; i-- { + if bucketIds[queue[i].bucketID] { + return + } + addFromQueue(queue[i]) + // len(queue) < batchSize so no need to check for full batch. + qID-- + } + } + + for i, digit := range digits { + + if digit == 0 || points[i].IsInfinity() { + continue + } + + bucketID := uint16((digit >> 1)) + isAdd := digit&1 == 0 + if isAdd { + // add + bucketID -= 1 + } + + if bucketIds[bucketID] { + // put it in queue + queue[qID].bucketID = bucketID + if isAdd { + queue[qID].point.Set(&points[i]) + } else { + queue[qID].point.Neg(&points[i]) + } + qID++ + + // queue is full, flush it. + if qID == len(queue)-1 { + flushQueue() + } + continue + } + + // we add the point to the batch. + add(bucketID, &points[i], isAdd) + if isFull() { + executeAndReset() + processTopQueue() + } + } + + // flush items in batch. + executeAndReset() + + // empty the queue + flushQueue() + + // reduce buckets into total + // total = bucket[0] + 2*bucket[1] + 3*bucket[2] ... + n*bucket[n-1] + var runningSum, total g1JacExtended + runningSum.SetInfinity() + total.SetInfinity() + for k := len(buckets) - 1; k >= 0; k-- { + runningSum.addMixed(&buckets[k]) + if !bucketsJE[k].IsInfinity() { + runningSum.add(&bucketsJE[k]) + } + total.add(&runningSum) + } + + if sem != nil { + // release a token to the semaphore + // before sending to chRes + sem <- struct{}{} + } + + chRes <- total + +} + +// we declare the buckets as fixed-size array types +// this allow us to allocate the buckets on the stack +type bucketG1AffineC10 [512]G1Affine +type bucketG1AffineC11 [1024]G1Affine +type bucketG1AffineC12 [2048]G1Affine +type bucketG1AffineC13 [4096]G1Affine +type bucketG1AffineC14 [8192]G1Affine +type bucketG1AffineC15 [16384]G1Affine +type bucketG1AffineC16 [32768]G1Affine + +// buckets: array of G1Affine points of size 1 << (c-1) +type ibG1Affine interface { + bucketG1AffineC10 | + bucketG1AffineC11 | + bucketG1AffineC12 | + bucketG1AffineC13 | + bucketG1AffineC14 | + bucketG1AffineC15 | + bucketG1AffineC16 +} + +// array of coordinates fptower.E8 +type cG1Affine interface { + cG1AffineC10 | + cG1AffineC11 | + cG1AffineC12 | + cG1AffineC13 | + cG1AffineC14 | + cG1AffineC15 | + cG1AffineC16 +} + +// buckets: array of G1Affine points (for the batch addition) +type pG1Affine interface { + pG1AffineC10 | + pG1AffineC11 | + pG1AffineC12 | + pG1AffineC13 | + pG1AffineC14 | + pG1AffineC15 | + pG1AffineC16 +} + +// buckets: array of *G1Affine points (for the batch addition) +type ppG1Affine interface { + ppG1AffineC10 | + ppG1AffineC11 | + ppG1AffineC12 | + ppG1AffineC13 | + ppG1AffineC14 | + ppG1AffineC15 | + ppG1AffineC16 +} + +// buckets: array of G1Affine queue operations (for the batch addition) +type qOpsG1Affine interface { + qG1AffineC10 | + qG1AffineC11 | + qG1AffineC12 | + qG1AffineC13 | + qG1AffineC14 | + qG1AffineC15 | + qG1AffineC16 +} + +// batch size 80 when c = 10 +type cG1AffineC10 [80]fptower.E8 +type pG1AffineC10 [80]G1Affine +type ppG1AffineC10 [80]*G1Affine +type qG1AffineC10 [80]batchOpG1Affine + +// batch size 150 when c = 11 +type cG1AffineC11 [150]fptower.E8 +type pG1AffineC11 [150]G1Affine +type ppG1AffineC11 [150]*G1Affine +type qG1AffineC11 [150]batchOpG1Affine + +// batch size 200 when c = 12 +type cG1AffineC12 [200]fptower.E8 +type pG1AffineC12 [200]G1Affine +type ppG1AffineC12 [200]*G1Affine +type qG1AffineC12 [200]batchOpG1Affine + +// batch size 350 when c = 13 +type cG1AffineC13 [350]fptower.E8 +type pG1AffineC13 [350]G1Affine +type ppG1AffineC13 [350]*G1Affine +type qG1AffineC13 [350]batchOpG1Affine + +// batch size 400 when c = 14 +type cG1AffineC14 [400]fptower.E8 +type pG1AffineC14 [400]G1Affine +type ppG1AffineC14 [400]*G1Affine +type qG1AffineC14 [400]batchOpG1Affine + +// batch size 500 when c = 15 +type cG1AffineC15 [500]fptower.E8 +type pG1AffineC15 [500]G1Affine +type ppG1AffineC15 [500]*G1Affine +type qG1AffineC15 [500]batchOpG1Affine + +// batch size 640 when c = 16 +type cG1AffineC16 [640]fptower.E8 +type pG1AffineC16 [640]G1Affine +type ppG1AffineC16 [640]*G1Affine +type qG1AffineC16 [640]batchOpG1Affine + +type bitSetC2 [2]bool +type bitSetC3 [4]bool +type bitSetC4 [8]bool +type bitSetC5 [16]bool +type bitSetC6 [32]bool +type bitSetC7 [64]bool +type bitSetC8 [128]bool +type bitSetC9 [256]bool +type bitSetC10 [512]bool +type bitSetC11 [1024]bool +type bitSetC12 [2048]bool +type bitSetC13 [4096]bool +type bitSetC14 [8192]bool +type bitSetC15 [16384]bool +type bitSetC16 [32768]bool + +type bitSet interface { + bitSetC2 | + bitSetC3 | + bitSetC4 | + bitSetC5 | + bitSetC6 | + bitSetC7 | + bitSetC8 | + bitSetC9 | + bitSetC10 | + bitSetC11 | + bitSetC12 | + bitSetC13 | + bitSetC14 | + bitSetC15 | + bitSetC16 +} diff --git a/ecc/kb8/multiexp_jacobian.go b/ecc/kb8/multiexp_jacobian.go new file mode 100644 index 0000000000..5c21c342f2 --- /dev/null +++ b/ecc/kb8/multiexp_jacobian.go @@ -0,0 +1,97 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package kb8 + +func processChunkG1Jacobian[B ibg1JacExtended](chunk uint64, + chRes chan<- g1JacExtended, + c uint64, + points []G1Affine, + digits []uint16, + sem chan struct{}) { + + if sem != nil { + // if we are limited, wait for a token in the semaphore + <-sem + } + + var buckets B + for i := range len(buckets) { + buckets[i].SetInfinity() + } + + // for each scalars, get the digit corresponding to the chunk we're processing. + for i, digit := range digits { + if digit == 0 { + continue + } + + // if msbWindow bit is set, we need to subtract + if digit&1 == 0 { + // add + buckets[(digit>>1)-1].addMixed(&points[i]) + } else { + // sub + buckets[(digit >> 1)].subMixed(&points[i]) + } + } + + // reduce buckets into total + // total = bucket[0] + 2*bucket[1] + 3*bucket[2] ... + n*bucket[n-1] + + var runningSum, total g1JacExtended + runningSum.SetInfinity() + total.SetInfinity() + for k := len(buckets) - 1; k >= 0; k-- { + if !buckets[k].IsInfinity() { + runningSum.add(&buckets[k]) + } + total.add(&runningSum) + } + + if sem != nil { + // release a token to the semaphore + // before sending to chRes + sem <- struct{}{} + } + + chRes <- total +} + +// we declare the buckets as fixed-size array types +// this allow us to allocate the buckets on the stack +type bucketg1JacExtendedC2 [2]g1JacExtended +type bucketg1JacExtendedC3 [4]g1JacExtended +type bucketg1JacExtendedC4 [8]g1JacExtended +type bucketg1JacExtendedC5 [16]g1JacExtended +type bucketg1JacExtendedC6 [32]g1JacExtended +type bucketg1JacExtendedC7 [64]g1JacExtended +type bucketg1JacExtendedC8 [128]g1JacExtended +type bucketg1JacExtendedC9 [256]g1JacExtended +type bucketg1JacExtendedC10 [512]g1JacExtended +type bucketg1JacExtendedC11 [1024]g1JacExtended +type bucketg1JacExtendedC12 [2048]g1JacExtended +type bucketg1JacExtendedC13 [4096]g1JacExtended +type bucketg1JacExtendedC14 [8192]g1JacExtended +type bucketg1JacExtendedC15 [16384]g1JacExtended +type bucketg1JacExtendedC16 [32768]g1JacExtended + +type ibg1JacExtended interface { + bucketg1JacExtendedC2 | + bucketg1JacExtendedC3 | + bucketg1JacExtendedC4 | + bucketg1JacExtendedC5 | + bucketg1JacExtendedC6 | + bucketg1JacExtendedC7 | + bucketg1JacExtendedC8 | + bucketg1JacExtendedC9 | + bucketg1JacExtendedC10 | + bucketg1JacExtendedC11 | + bucketg1JacExtendedC12 | + bucketg1JacExtendedC13 | + bucketg1JacExtendedC14 | + bucketg1JacExtendedC15 | + bucketg1JacExtendedC16 +} diff --git a/ecc/kb8/multiexp_test.go b/ecc/kb8/multiexp_test.go new file mode 100644 index 0000000000..8fdcb56f1b --- /dev/null +++ b/ecc/kb8/multiexp_test.go @@ -0,0 +1,442 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package kb8 + +import ( + "fmt" + "math/big" + "math/bits" + "math/rand/v2" + "runtime" + "sync" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/kb8/fr" + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/prop" +) + +func TestMultiExpG1(t *testing.T) { + + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = 3 + } else { + parameters.MinSuccessfulTests = nbFuzzShort * 2 + } + + properties := gopter.NewProperties(parameters) + + genScalar := GenFr() + + // size of the multiExps + const nbSamples = 73 + + // multi exp points + var samplePoints [nbSamples]G1Affine + var g G1Jac + g.Set(&g1Gen) + for i := 1; i <= nbSamples; i++ { + samplePoints[i-1].FromJacobian(&g) + g.AddAssign(&g1Gen) + } + + // sprinkle some points at infinity + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + + // final scalar to use in double and add method (without mixer factor) + // n(n+1)(2n+1)/6 (sum of the squares from 1 to n) + var scalar big.Int + scalar.SetInt64(nbSamples) + scalar.Mul(&scalar, new(big.Int).SetInt64(nbSamples+1)) + scalar.Mul(&scalar, new(big.Int).SetInt64(2*nbSamples+1)) + scalar.Div(&scalar, new(big.Int).SetInt64(6)) + + // ensure a multiexp that's splitted has the same result as a non-splitted one.. + properties.Property("[G1] Multi exponentiation (cmax) should be consistent with splitted multiexp", prop.ForAll( + func(mixer fr.Element) bool { + var samplePointsLarge [nbSamples * 13]G1Affine + for i := range 13 { + copy(samplePointsLarge[i*nbSamples:], samplePoints[:]) + } + + var rmax, splitted1, splitted2 G1Jac + + // mixer ensures that all the words of a fpElement are set + var sampleScalars [nbSamples * 13]fr.Element + + for i := 1; i <= nbSamples; i++ { + sampleScalars[i-1].SetUint64(uint64(i)). + Mul(&sampleScalars[i-1], &mixer) + } + + rmax.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{}) + splitted1.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 128}) + splitted2.MultiExp(samplePointsLarge[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: 51}) + return rmax.Equal(&splitted1) && rmax.Equal(&splitted2) + }, + genScalar, + )) + + // cRange is generated from template and contains the available parameters for the multiexp window size + cRange := []uint64{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + if testing.Short() { + // test only "odd" and "even" (ie windows size divide word size vs not) + cRange = []uint64{5, 14} + } + + properties.Property(fmt.Sprintf("[G1] Multi exponentiation (c in %v) should be consistent with sum of square", cRange), prop.ForAll( + func(mixer fr.Element) bool { + + var expected G1Jac + + // compute expected result with double and add + var finalScalar, mixerBigInt big.Int + finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) + expected.ScalarMultiplication(&g1Gen, &finalScalar) + + // mixer ensures that all the words of a fpElement are set + var sampleScalars [nbSamples]fr.Element + + for i := 1; i <= nbSamples; i++ { + sampleScalars[i-1].SetUint64(uint64(i)). + Mul(&sampleScalars[i-1], &mixer) + } + + results := make([]G1Jac, len(cRange)) + for i, c := range cRange { + _innerMsmG1(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) + } + for i := 1; i < len(results); i++ { + if !results[i].Equal(&results[i-1]) { + t.Logf("result for c=%d != c=%d", cRange[i-1], cRange[i]) + return false + } + } + return true + }, + genScalar, + )) + + properties.Property(fmt.Sprintf("[G1] Multi exponentiation (c in %v) of points at infinity should output a point at infinity", cRange), prop.ForAll( + func(mixer fr.Element) bool { + + var samplePointsZero [nbSamples]G1Affine + + var expected G1Jac + + // compute expected result with double and add + var finalScalar, mixerBigInt big.Int + finalScalar.Mul(&scalar, mixer.BigInt(&mixerBigInt)) + expected.ScalarMultiplication(&g1Gen, &finalScalar) + + // mixer ensures that all the words of a fpElement are set + var sampleScalars [nbSamples]fr.Element + + for i := 1; i <= nbSamples; i++ { + sampleScalars[i-1].SetUint64(uint64(i)). + Mul(&sampleScalars[i-1], &mixer) + samplePointsZero[i-1].SetInfinity() + } + + results := make([]G1Jac, len(cRange)) + for i, c := range cRange { + _innerMsmG1(&results[i], c, samplePointsZero[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) + } + for i := range len(results) { + if !results[i].Z.IsZero() { + t.Logf("result for c=%d is not infinity", cRange[i]) + return false + } + } + return true + }, + genScalar, + )) + + properties.Property(fmt.Sprintf("[G1] Multi exponentiation (c in %v) with a vector of 0s as input should output a point at infinity", cRange), prop.ForAll( + func(mixer fr.Element) bool { + // mixer ensures that all the words of a fpElement are set + var sampleScalars [nbSamples]fr.Element + + results := make([]G1Jac, len(cRange)) + for i, c := range cRange { + _innerMsmG1(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) + } + for i := range len(results) { + if !results[i].Z.IsZero() { + t.Logf("result for c=%d is not infinity", cRange[i]) + return false + } + } + return true + }, + genScalar, + )) + + // note : this test is here as we expect to have a different multiExp than the above bucket method + // for small number of points + properties.Property("[G1] Multi exponentiation (<50points) should be consistent with sum of square", prop.ForAll( + func(mixer fr.Element) bool { + + var g G1Jac + g.Set(&g1Gen) + + // mixer ensures that all the words of a fpElement are set + samplePoints := make([]G1Affine, 30) + sampleScalars := make([]fr.Element, 30) + + for i := 1; i <= 30; i++ { + sampleScalars[i-1].SetUint64(uint64(i)). + Mul(&sampleScalars[i-1], &mixer) + samplePoints[i-1].FromJacobian(&g) + g.AddAssign(&g1Gen) + } + + var op1MultiExp G1Affine + op1MultiExp.MultiExp(samplePoints, sampleScalars, ecc.MultiExpConfig{}) + + var finalBigScalar fr.Element + var finalBigScalarBi big.Int + var op1ScalarMul G1Affine + finalBigScalar.SetUint64(9455).Mul(&finalBigScalar, &mixer) + finalBigScalar.BigInt(&finalBigScalarBi) + op1ScalarMul.ScalarMultiplication(&g1GenAff, &finalBigScalarBi) + + return op1ScalarMul.Equal(&op1MultiExp) + }, + genScalar, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestCrossMultiExpG1(t *testing.T) { + const nbSamples = 1 << 14 + // multi exp points + var samplePoints [nbSamples]G1Affine + var g G1Jac + g.Set(&g1Gen) + for i := 1; i <= nbSamples; i++ { + samplePoints[i-1].FromJacobian(&g) + g.AddAssign(&g1Gen) + } + + // sprinkle some points at infinity + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + samplePoints[rand.N(nbSamples)].SetInfinity() //#nosec G404 weak rng is fine here + + var sampleScalars [nbSamples]fr.Element + fillBenchScalars(sampleScalars[:]) + + // sprinkle some doublings + for i := 10; i < 100; i++ { + samplePoints[i] = samplePoints[0] + sampleScalars[i] = sampleScalars[0] + } + + // cRange is generated from template and contains the available parameters for the multiexp window size + cRange := []uint64{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + if testing.Short() { + // test only "odd" and "even" (ie windows size divide word size vs not) + cRange = []uint64{5, 14} + } + + results := make([]G1Jac, len(cRange)) + for i, c := range cRange { + _innerMsmG1(&results[i], c, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) + } + + var r G1Jac + _innerMsmG1Reference(&r, samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{NbTasks: runtime.NumCPU()}) + + var expected, got G1Affine + expected.FromJacobian(&r) + + for i := range len(results) { + got.FromJacobian(&results[i]) + if !expected.Equal(&got) { + t.Fatalf("cross msm failed with c=%d", cRange[i]) + } + } + +} + +// _innerMsmG1Reference always do ext jacobian with c == 16 +func _innerMsmG1Reference(p *G1Jac, points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) *G1Jac { + // partition the scalars + digits, _ := partitionScalars(scalars, 16, config.NbTasks) + + nbChunks := computeNbChunks(16) + + // for each chunk, spawn one go routine that'll loop through all the scalars in the + // corresponding bit-window + // note that buckets is an array allocated on the stack and this is critical for performance + + // each go routine sends its result in chChunks[i] channel + chChunks := make([]chan g1JacExtended, nbChunks) + for i := range len(chChunks) { + chChunks[i] = make(chan g1JacExtended, 1) + } + + // the last chunk may be processed with a different method than the rest, as it could be smaller. + n := len(points) + for j := int(nbChunks - 1); j >= 0; j-- { + processChunk := processChunkG1Jacobian[bucketg1JacExtendedC16] + go processChunk(uint64(j), chChunks[j], 16, points, digits[j*n:(j+1)*n], nil) + } + + return msmReduceChunkG1Affine(p, int(16), chChunks[:]) +} + +func BenchmarkMultiExpG1(b *testing.B) { + + const ( + pow = (bits.UintSize / 2) - (bits.UintSize / 8) // 24 on 64 bits arch, 12 on 32 bits + nbSamples = 1 << pow + ) + + var ( + samplePoints [nbSamples]G1Affine + sampleScalars [nbSamples]fr.Element + sampleScalarsSmallValues [nbSamples]fr.Element + sampleScalarsRedundant [nbSamples]fr.Element + ) + + fillBenchScalars(sampleScalars[:]) + copy(sampleScalarsSmallValues[:], sampleScalars[:]) + copy(sampleScalarsRedundant[:], sampleScalars[:]) + + // this means first chunk is going to have more work to do and should be split into several go routines + for i := range len(sampleScalarsSmallValues) { + if i%5 == 0 { + sampleScalarsSmallValues[i].SetZero() + sampleScalarsSmallValues[i][0] = 1 + } + } + + // bad case for batch affine because scalar distribution might look uniform + // but over batchSize windows, we may hit a lot of conflicts and force the msm-affine + // to process small batches of additions to flush its queue of conflicted points. + for i := 0; i < len(sampleScalarsRedundant); i += 100 { + for j := i + 1; j < i+100 && j < len(sampleScalarsRedundant); j++ { + sampleScalarsRedundant[j] = sampleScalarsRedundant[i] + } + } + + fillBenchBasesG1(samplePoints[:]) + + var testPoint G1Affine + + for i := 5; i <= pow; i++ { + using := 1 << i + + b.Run(fmt.Sprintf("%d points", using), func(b *testing.B) { + b.ResetTimer() + for range b.N { + testPoint.MultiExp(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) + } + }) + + b.Run(fmt.Sprintf("%d points-smallvalues", using), func(b *testing.B) { + b.ResetTimer() + for range b.N { + testPoint.MultiExp(samplePoints[:using], sampleScalarsSmallValues[:using], ecc.MultiExpConfig{}) + } + }) + + b.Run(fmt.Sprintf("%d points-redundancy", using), func(b *testing.B) { + b.ResetTimer() + for range b.N { + testPoint.MultiExp(samplePoints[:using], sampleScalarsRedundant[:using], ecc.MultiExpConfig{}) + } + }) + } +} + +func BenchmarkMultiExpG1Reference(b *testing.B) { + const nbSamples = 1 << 20 + + var ( + samplePoints [nbSamples]G1Affine + sampleScalars [nbSamples]fr.Element + ) + + fillBenchScalars(sampleScalars[:]) + fillBenchBasesG1(samplePoints[:]) + + var testPoint G1Affine + + b.ResetTimer() + for range b.N { + testPoint.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) + } +} + +func BenchmarkManyMultiExpG1Reference(b *testing.B) { + const nbSamples = 1 << 20 + + var ( + samplePoints [nbSamples]G1Affine + sampleScalars [nbSamples]fr.Element + ) + + fillBenchScalars(sampleScalars[:]) + fillBenchBasesG1(samplePoints[:]) + + var t1, t2, t3 G1Affine + b.ResetTimer() + for range b.N { + var wg sync.WaitGroup + wg.Add(3) + go func() { + t1.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) + wg.Done() + }() + go func() { + t2.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) + wg.Done() + }() + go func() { + t3.MultiExp(samplePoints[:], sampleScalars[:], ecc.MultiExpConfig{}) + wg.Done() + }() + wg.Wait() + } +} + +// WARNING: this return points that are NOT on the curve and is meant to be use for benchmarking +// purposes only. We don't check that the result is valid but just measure "computational complexity". +// +// Rationale for generating points that are not on the curve is that for large benchmarks, generating +// a vector of different points can take minutes. Using the same point or subset will bias the benchmark result +// since bucket additions in extended jacobian coordinates will hit doubling algorithm instead of add. +func fillBenchBasesG1(samplePoints []G1Affine) { + var r big.Int + r.SetString("340444420969191673093399857471996460938405", 10) + samplePoints[0].ScalarMultiplication(&samplePoints[0], &r) + + one := samplePoints[0].X + one.SetOne() + + for i := 1; i < len(samplePoints); i++ { + samplePoints[i].X.Add(&samplePoints[i-1].X, &one) + samplePoints[i].Y.Sub(&samplePoints[i-1].Y, &one) + } +} + +func fillBenchScalars(sampleScalars []fr.Element) { + // ensure every words of the scalars are filled + for i := range len(sampleScalars) { + sampleScalars[i].MustSetRandom() + } +} diff --git a/ecc/secp256k1/g1.go b/ecc/secp256k1/g1.go index c9ac075f2c..89e32572c9 100644 --- a/ecc/secp256k1/g1.go +++ b/ecc/secp256k1/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right fp.Element + var left, right, tmp fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/secp256k1/marshal.go b/ecc/secp256k1/marshal.go index 9827861fa6..1c408ca00d 100644 --- a/ecc/secp256k1/marshal.go +++ b/ecc/secp256k1/marshal.go @@ -1,67 +1,917 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// FOO +// Code generated by consensys/gnark-crypto DO NOT EDIT package secp256k1 import ( + "encoding/binary" "errors" "io" + "reflect" + "sync/atomic" "github.com/consensys/gnark-crypto/ecc/secp256k1/fp" + "github.com/consensys/gnark-crypto/ecc/secp256k1/fr" + "github.com/consensys/gnark-crypto/parallel" ) +// To encode G1Affine and G2Affine points, we mask the most significant bits with these bits to specify without ambiguity +// metadata needed for point (de)compression +// we follow the BLS12-381 style encoding as specified in ZCash and now IETF +// see https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/11/ +// Appendix C. ZCash serialization format for BLS12_381 +const ( + mMask byte = 0b111 << 5 + mUncompressed byte = 0b000 << 5 + _ byte = 0b001 << 5 // invalid + mUncompressedInfinity byte = 0b010 << 5 + _ byte = 0b011 << 5 // invalid + mCompressedSmallest byte = 0b100 << 5 + mCompressedLargest byte = 0b101 << 5 + mCompressedInfinity byte = 0b110 << 5 + _ byte = 0b111 << 5 // invalid +) + +var ( + ErrInvalidInfinityEncoding = errors.New("invalid infinity point encoding") + ErrInvalidEncoding = errors.New("invalid point encoding") +) + +// Encoder writes secp256k1 object values to an output stream +type Encoder struct { + w io.Writer + n int64 // written bytes + raw bool // raw vs compressed encoding +} + +// Decoder reads secp256k1 object values from an inbound stream +type Decoder struct { + r io.Reader + n int64 // read bytes + subGroupCheck bool // default to true +} + +// NewDecoder returns a binary decoder supporting curve secp256k1 objects in both +// compressed and uncompressed (raw) forms +func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { + d := &Decoder{r: r, subGroupCheck: true} + + for _, o := range options { + o(d) + } + + return d +} + +// Decode reads the binary encoding of v from the stream +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, *[]G1Affine or *[]G2Affine +func (dec *Decoder) Decode(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { + return errors.New("secp256k1 decoder: unsupported type, need pointer") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + // in particular, careful attention must be given to usage of Bytes() method on Elements and Points + // that return an array (not a slice) of bytes. Using this is beneficial to minimize memory allocations + // in very large (de)serialization upstream in gnark. + // (but detrimental to code readability here) + + var read64 int64 + if vf, ok := v.(io.ReaderFrom); ok { + read64, err = vf.ReadFrom(dec.r) + dec.n += read64 + return + } + var buf [SizeOfG1AffineUncompressed]byte + var read int + var sliceLen uint32 + + switch t := v.(type) { + case *[][]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([][]uint64, sliceLen) + + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { + return + } + (*t)[i] = make([]uint64, sliceLen) + for j := range (*t)[i] { + if (*t)[i][j], err = dec.readUint64(); err != nil { + return + } + } + } + return + case *[]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([]uint64, sliceLen) + for i := range *t { + if (*t)[i], err = dec.readUint64(); err != nil { + return + } + } + return + case *fr.Element: + read, err = io.ReadFull(dec.r, buf[:fr.Bytes]) + dec.n += int64(read) + if err != nil { + return + } + err = t.SetBytesCanonical(buf[:fr.Bytes]) + return + case *fp.Element: + read, err = io.ReadFull(dec.r, buf[:fp.Bytes]) + dec.n += int64(read) + if err != nil { + return + } + err = t.SetBytesCanonical(buf[:fp.Bytes]) + return + case *[]fr.Element: + read64, err = (*fr.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[]fp.Element: + read64, err = (*fp.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len(*t) != int(sliceLen) { + *t = make([][]fr.Element, sliceLen) + } + for i := range *t { + read64, err = (*fr.Vector)(&(*t)[i]).ReadFrom(dec.r) + dec.n += read64 + } + return + case *[][][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len(*t) != int(sliceLen) { + *t = make([][][]fr.Element, sliceLen) + } + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len((*t)[i]) != int(sliceLen) { + (*t)[i] = make([][]fr.Element, sliceLen) + } + for j := range (*t)[i] { + read64, err = (*fr.Vector)(&(*t)[i][j]).ReadFrom(dec.r) + dec.n += read64 + } + } + return + case *G1Affine: + // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) + dec.n += int64(read) + if err != nil { + return + } + nbBytes := SizeOfG1AffineCompressed + + // 111, 011, 001 --> invalid mask + if isMaskInvalid(buf[0]) { + err = ErrInvalidEncoding + return + } + + // most significant byte contains metadata + if !isCompressed(buf[0]) { + nbBytes = SizeOfG1AffineUncompressed + // we read more. + read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + } + _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) + return + case *[]G1Affine: + sliceLen, err = dec.readUint32() + if err != nil { + return + } + if len(*t) != int(sliceLen) || *t == nil { + *t = make([]G1Affine, sliceLen) + } + compressed := make([]bool, sliceLen) + for i := range len(*t) { + + // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) + dec.n += int64(read) + if err != nil { + return + } + nbBytes := SizeOfG1AffineCompressed + + // 111, 011, 001 --> invalid mask + if isMaskInvalid(buf[0]) { + err = ErrInvalidEncoding + return + } + + // most significant byte contains metadata + if !isCompressed(buf[0]) { + nbBytes = SizeOfG1AffineUncompressed + // we read more. + read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + _, err = (*t)[i].setBytes(buf[:nbBytes], false) + if err != nil { + return + } + } else { + var r bool + if r, err = (*t)[i].unsafeSetCompressedBytes(buf[:nbBytes]); err != nil { + return + } + compressed[i] = !r + } + } + var nbErrs uint64 + parallel.Execute(len(compressed), func(start, end int) { + for i := start; i < end; i++ { + if compressed[i] { + if err := (*t)[i].unsafeComputeY(dec.subGroupCheck); err != nil { + atomic.AddUint64(&nbErrs, 1) + } + } else if dec.subGroupCheck { + if !(*t)[i].IsInSubGroup() { + atomic.AddUint64(&nbErrs, 1) + } + } + } + }) + if nbErrs != 0 { + return errors.New("point decompression failed") + } + + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New("secp256k1 encoder: unsupported type") + } + err = binary.Read(dec.r, binary.BigEndian, t) + if err == nil { + dec.n += int64(n) + } + return + } +} + +// BytesRead return total bytes read from reader +func (dec *Decoder) BytesRead() int64 { + return dec.n +} + +func (dec *Decoder) readUint32() (r uint32, err error) { + var read int + var buf [4]byte + read, err = io.ReadFull(dec.r, buf[:4]) + dec.n += int64(read) + if err != nil { + return + } + r = binary.BigEndian.Uint32(buf[:4]) + return +} + +func (dec *Decoder) readUint64() (r uint64, err error) { + var read int + var buf [8]byte + read, err = io.ReadFull(dec.r, buf[:]) + dec.n += int64(read) + if err != nil { + return + } + r = binary.BigEndian.Uint64(buf[:]) + return +} + +// isMaskInvalid returns true if the mask is invalid +func isMaskInvalid(msb byte) bool { + mData := msb & mMask + return ((mData == (0b111 << 5)) || (mData == (0b011 << 5)) || (mData == (0b001 << 5))) +} + +func isCompressed(msb byte) bool { + mData := msb & mMask + return mData != mUncompressed && mData != mUncompressedInfinity +} + +// NewEncoder returns a binary encoder supporting curve secp256k1 objects +func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { + // default settings + enc := &Encoder{ + w: w, + n: 0, + raw: false, + } + + // handle options + for _, option := range options { + option(enc) + } + + return enc +} + +// Encode writes the binary encoding of v to the stream +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, []G1Affine, []G2Affine, *[]G1Affine or *[]G2Affine +func (enc *Encoder) Encode(v any) (err error) { + if enc.raw { + return enc.encodeRaw(v) + } + return enc.encode(v) +} + +// BytesWritten return total bytes written on writer +func (enc *Encoder) BytesWritten() int64 { + return enc.n +} + +// RawEncoding returns an option to use in NewEncoder(...) which sets raw encoding mode to true +// points will not be compressed using this option +func RawEncoding() func(*Encoder) { + return func(enc *Encoder) { + enc.raw = true + } +} + +// NoSubgroupChecks returns an option to use in NewDecoder(...) which disable subgroup checks on the points +// the decoder will read. Use with caution, as crafted points from an untrusted source can lead to crypto-attacks. +func NoSubgroupChecks() func(*Decoder) { + return func(dec *Decoder) { + dec.subGroupCheck = false + } +} + +// isZeroed checks that the provided bytes are at 0 +func isZeroed(firstByte byte, buf []byte) bool { + if firstByte != 0 { + return false + } + for _, b := range buf { + if b != 0 { + return false + } + } + return true +} + +func (enc *Encoder) encode(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New(" encoder: can't encode ") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + + var written int + + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) + case *fr.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *fp.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *G1Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: + // write slice length + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 + } + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { + return + } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } + } + return + case *[]G1Affine: + return enc.encode(*t) + case []G1Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG1AffineCompressed]byte + + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New(" encoder: unsupported type") + } + err = binary.Write(enc.w, binary.BigEndian, t) + enc.n += int64(n) + return + } +} + +func (enc *Encoder) encodeRaw(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New(" encoder: can't encode ") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + + var written int + + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) + case *fr.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *fp.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *G1Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: + // write slice length + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 + } + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { + return + } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } + } + return + case *[]G1Affine: + return enc.encodeRaw(*t) + case []G1Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG1AffineUncompressed]byte + + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New(" encoder: unsupported type") + } + err = binary.Write(enc.w, binary.BigEndian, t) + enc.n += int64(n) + return + } +} + +func (enc *Encoder) writeUint64Slice(t []uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint64(t[i]); err != nil { + return + } + } + return nil +} + +func (enc *Encoder) writeUint64SliceSlice(t [][]uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint32(uint32(len(t[i]))); err != nil { + return + } + for j := range t[i] { + if err = enc.writeUint64(t[i][j]); err != nil { + return + } + } + } + return nil +} + +func (enc *Encoder) writeUint64(a uint64) error { + var buff [64 / 8]byte + binary.BigEndian.PutUint64(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + +func (enc *Encoder) writeUint32(a uint32) error { + var buff [32 / 8]byte + binary.BigEndian.PutUint32(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + // SizeOfG1AffineCompressed represents the size in bytes that a G1Affine need in binary form, compressed const SizeOfG1AffineCompressed = 32 // SizeOfG1AffineUncompressed represents the size in bytes that a G1Affine need in binary form, uncompressed const SizeOfG1AffineUncompressed = SizeOfG1AffineCompressed * 2 +// Marshal converts p to a byte slice (without point compression) +func (p *G1Affine) Marshal() []byte { + b := p.RawBytes() + return b[:] +} + +// Unmarshal is an alias to SetBytes() +func (p *G1Affine) Unmarshal(buf []byte) error { + _, err := p.SetBytes(buf) + return err +} + +// Bytes returns binary representation of p +// will store X coordinate in regular form and a parity bit +// we follow the BLS12-381 style encoding as specified in ZCash and now IETF +// +// The most significant bit, when set, indicates that the point is in compressed form. Otherwise, the point is in uncompressed form. +// +// The second-most significant bit indicates that the point is at infinity. If this bit is set, the remaining bits of the group element's encoding should be set to zero. +// +// The third-most significant bit is set if (and only if) this point is in compressed form and it is not the point at infinity and its y-coordinate is the lexicographically largest of the two associated with the encoded x-coordinate. +func (p *G1Affine) Bytes() (res [SizeOfG1AffineCompressed]byte) { + + // check if p is infinity point + if p.X.IsZero() && p.Y.IsZero() { + res[0] = mCompressedInfinity + return + } + + msbMask := mCompressedSmallest + // compressed, we need to know if Y is lexicographically bigger than -Y + // if p.Y ">" -p.Y + if p.Y.LexicographicallyLargest() { + msbMask = mCompressedLargest + } + + // we store X and mask the most significant word with our metadata mask + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) + + res[0] |= msbMask + + return +} + // RawBytes returns binary representation of p (stores X and Y coordinate) +// see Bytes() for a compressed representation func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { + // check if p is infinity point + if p.X.IsZero() && p.Y.IsZero() { + + res[0] = mUncompressedInfinity + + return + } + // not compressed // we store the Y coordinate fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[32:32+fp.Bytes]), p.Y) - // we store the X coordinate + // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) + res[0] |= mUncompressed + return } // SetBytes sets p from binary representation in buf and returns number of consumed bytes // -// bytes in buf must match RawBytes() +// bytes in buf must match either RawBytes() or Bytes() output // // if buf is too short io.ErrShortBuffer is returned // +// if buf contains compressed representation (output from Bytes()) and we're unable to compute +// the Y coordinate (i.e the square root doesn't exist) this function returns an error +// // this check if the resulting point is on the curve and in the correct subgroup func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } -// we store both X and Y and there is no spare bit for flagging func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer } + // most significant byte + mData := buf[0] & mMask + + // 111, 011, 001 --> invalid mask + if isMaskInvalid(mData) { + return 0, ErrInvalidEncoding + } + + // check buffer size + if (mData == mUncompressed) || (mData == mUncompressedInfinity) { + if len(buf) < SizeOfG1AffineUncompressed { + return 0, io.ErrShortBuffer + } + } + + // infinity encoded, we still check that the buffer is full of zeroes. + if mData == mCompressedInfinity { + if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineCompressed]) { + return 0, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return SizeOfG1AffineCompressed, nil + } + if mData == mUncompressedInfinity { + if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineUncompressed]) { + return 0, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return SizeOfG1AffineUncompressed, nil + } + // uncompressed point - // read X and Y coordinates - if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { - return 0, err + if mData == mUncompressed { + // read X and Y coordinates + if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { + return 0, err + } + if err := p.Y.SetBytesCanonical(buf[fp.Bytes : fp.Bytes*2]); err != nil { + return 0, err + } + + // subgroup check + if subGroupCheck && !p.IsInSubGroup() { + return 0, errors.New("invalid point: subgroup check failed") + } + + return SizeOfG1AffineUncompressed, nil } - if err := p.Y.SetBytesCanonical(buf[fp.Bytes : fp.Bytes*2]); err != nil { + + // we have a compressed coordinate + // we need to + // 1. copy the buffer (to keep this method thread safe) + // 2. we need to solve the curve equation to compute Y + + var bufX [fp.Bytes]byte + copy(bufX[:fp.Bytes], buf[:fp.Bytes]) + bufX[0] &= ^mMask + + // read X coordinate + if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { return 0, err } + var YSquared, Y fp.Element + + YSquared.Square(&p.X).Mul(&YSquared, &p.X) + YSquared.Add(&YSquared, &bCurveCoeff) + if Y.Sqrt(&YSquared) == nil { + return 0, errors.New("invalid compressed coordinate: square root doesn't exist") + } + + if Y.LexicographicallyLargest() { + // Y ">" -Y + if mData == mCompressedSmallest { + Y.Neg(&Y) + } + } else { + // Y "<=" -Y + if mData == mCompressedLargest { + Y.Neg(&Y) + } + } + + p.Y = Y + // subgroup check if subGroupCheck && !p.IsInSubGroup() { return 0, errors.New("invalid point: subgroup check failed") } - return SizeOfG1AffineUncompressed, nil + return SizeOfG1AffineCompressed, nil +} + +// unsafeComputeY called by Decoder when processing slices of compressed point in parallel (step 2) +// it computes the Y coordinate from the already set X coordinate and is compute intensive +func (p *G1Affine) unsafeComputeY(subGroupCheck bool) error { + // stored in unsafeSetCompressedBytes + + mData := byte(p.Y[0]) + + // we have a compressed coordinate, we need to solve the curve equation to compute Y + var YSquared, Y fp.Element + + YSquared.Square(&p.X).Mul(&YSquared, &p.X) + YSquared.Add(&YSquared, &bCurveCoeff) + if Y.Sqrt(&YSquared) == nil { + return errors.New("invalid compressed coordinate: square root doesn't exist") + } + + if Y.LexicographicallyLargest() { + // Y ">" -Y + if mData == mCompressedSmallest { + Y.Neg(&Y) + } + } else { + // Y "<=" -Y + if mData == mCompressedLargest { + Y.Neg(&Y) + } + } + + p.Y = Y + + // subgroup check + if subGroupCheck && !p.IsInSubGroup() { + return errors.New("invalid point: subgroup check failed") + } + + return nil +} + +// unsafeSetCompressedBytes is called by Decoder when processing slices of compressed point in parallel (step 1) +// assumes buf[:8] mask is set to compressed +// returns true if point is infinity and need no further processing +// it sets X coordinate and uses Y for scratch space to store decompression metadata +func (p *G1Affine) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, err error) { + + // read the most significant byte + mData := buf[0] & mMask + + if mData == mCompressedInfinity { + isInfinity = true + if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineCompressed]) { + return isInfinity, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return isInfinity, nil + } + + // we need to copy the input buffer (to keep this method thread safe) + var bufX [fp.Bytes]byte + copy(bufX[:fp.Bytes], buf[:fp.Bytes]) + bufX[0] &= ^mMask + + // read X coordinate + if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { + return false, err + } + // store mData in p.Y[0] + p.Y[0] = uint64(mData) + // recomputing Y will be done asynchronously + return isInfinity, nil } diff --git a/ecc/secp256k1/marshal_test.go b/ecc/secp256k1/marshal_test.go index 8be8f5c966..b4696e51e2 100644 --- a/ecc/secp256k1/marshal_test.go +++ b/ecc/secp256k1/marshal_test.go @@ -1,24 +1,237 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// FOO +// Code generated by consensys/gnark-crypto DO NOT EDIT package secp256k1 import ( + "bytes" + crand "crypto/rand" + "io" "math/big" + "math/rand/v2" + "reflect" "testing" "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" "github.com/consensys/gnark-crypto/ecc/secp256k1/fp" + "github.com/consensys/gnark-crypto/ecc/secp256k1/fr" ) +const ( + nbFuzzShort = 10 + nbFuzz = 100 +) + +func TestEncoder(t *testing.T) { + t.Parallel() + // TODO need proper fuzz testing here + + var inA uint64 + var inB fr.Element + var inC fp.Element + var inD G1Affine + var inE G1Affine + var inG []G1Affine + var inI []fp.Element + var inJ []fr.Element + var inK fr.Vector + var inL [][]fr.Element + var inM [][]uint64 + var inN [][][]fr.Element + + // set values of inputs + inA = rand.Uint64() //#nosec G404 weak rng is fine here + inB.MustSetRandom() + inC.MustSetRandom() + inD.ScalarMultiplication(&g1GenAff, new(big.Int).SetUint64(rand.Uint64())) //#nosec G404 weak rng is fine here + // inE --> infinity + inG = make([]G1Affine, 2) + inG[1] = inD + inI = make([]fp.Element, 3) + inI[2] = inD.X + inJ = make([]fr.Element, 0) + inK = make(fr.Vector, 42) + inK[41].SetUint64(42) + inL = [][]fr.Element{inJ, inK} + inM = [][]uint64{{1, 2}, {4}, {}} + inN = make([][][]fr.Element, 4) + for i := range 4 { + inN[i] = make([][]fr.Element, i+2) + for j := range i + 2 { + inNIJ := make(fr.Vector, j+3) + inNIJ.MustSetRandom() + inN[i][j] = inNIJ + } + } + + // encode them, compressed and raw + var buf, bufRaw bytes.Buffer + enc := NewEncoder(&buf) + encRaw := NewEncoder(&bufRaw, RawEncoding()) + toEncode := []any{inA, &inB, &inC, &inD, &inE, inG, inI, inJ, inK, inL, inM, inN} + for _, v := range toEncode { + if err := enc.Encode(v); err != nil { + t.Fatal(err) + } + if err := encRaw.Encode(v); err != nil { + t.Fatal(err) + } + } + + testDecode := func(t *testing.T, r io.Reader, n int64) { + dec := NewDecoder(r) + var outA uint64 + var outB fr.Element + var outC fp.Element + var outD G1Affine + var outE G1Affine + outE.X.SetOne() + outE.Y.SetUint64(42) + var outG []G1Affine + var outI []fp.Element + var outJ []fr.Element + var outK fr.Vector + var outL [][]fr.Element + var outM [][]uint64 + var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} + for _, v := range toDecode { + if err := dec.Decode(v); err != nil { + t.Fatal(err) + } + } + + // compare values + if inA != outA { + t.Fatal("didn't encode/decode uint64 value properly") + } + + if !inB.Equal(&outB) || !inC.Equal(&outC) { + t.Fatal("decode(encode(Element) failed") + } + if !inD.Equal(&outD) || !inE.Equal(&outE) { + t.Fatal("decode(encode(G1Affine) failed") + } + if len(inG) != len(outG) { + t.Fatal("decode(encode(slice(points))) failed") + } + for i := range len(inG) { + if !inG[i].Equal(&outG[i]) { + t.Fatal("decode(encode(slice(points))) failed") + } + } + if (len(inI) != len(outI)) || (len(inJ) != len(outJ)) { + t.Fatal("decode(encode(slice(elements))) failed") + } + for i := range len(inI) { + if !inI[i].Equal(&outI[i]) { + t.Fatal("decode(encode(slice(elements))) failed") + } + } + if !reflect.DeepEqual(inK, outK) { + t.Fatal("decode(encode(vector)) failed") + } + if !reflect.DeepEqual(inL, outL) { + t.Fatal("decode(encode(slice²(elements))) failed") + } + if !reflect.DeepEqual(inM, outM) { + t.Fatal("decode(encode(slice²(uint64))) failed") + } + if !reflect.DeepEqual(inN, outN) { + t.Fatal("decode(encode(slice^{3}(uint64))) failed") + } + if n != dec.BytesRead() { + t.Fatal("bytes read don't match bytes written") + } + } + + // decode them + testDecode(t, &buf, enc.BytesWritten()) + testDecode(t, &bufRaw, encRaw.BytesWritten()) + +} + +func TestIsCompressed(t *testing.T) { + t.Parallel() + var g1Inf, g1 G1Affine + g1 = g1GenAff + + { + b := g1Inf.Bytes() + if !isCompressed(b[0]) { + t.Fatal("g1Inf.Bytes() should be compressed") + } + } + + { + b := g1Inf.RawBytes() + if isCompressed(b[0]) { + t.Fatal("g1Inf.RawBytes() should be uncompressed") + } + } + + { + b := g1.Bytes() + if !isCompressed(b[0]) { + t.Fatal("g1.Bytes() should be compressed") + } + } + + { + b := g1.RawBytes() + if isCompressed(b[0]) { + t.Fatal("g1.RawBytes() should be uncompressed") + } + } + +} + +func TestG1AffineInvalidBitMask(t *testing.T) { + t.Parallel() + var buf [SizeOfG1AffineCompressed]byte + crand.Read(buf[:]) + + var p G1Affine + buf[0] = 0b111 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } + buf[0] = 0b011 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } + buf[0] = 0b001 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } +} + func TestG1AffineSerialization(t *testing.T) { t.Parallel() // test round trip serialization of infinity { + // compressed + { + var p1, p2 G1Affine + p2.X.MustSetRandom() + p2.Y.MustSetRandom() + buf := p1.Bytes() + n, err := p2.SetBytes(buf[:]) + if err != nil { + t.Fatal(err) + } + if n != SizeOfG1AffineCompressed { + t.Fatal("invalid number of bytes consumed in buffer") + } + if !(p2.X.IsZero() && p2.Y.IsZero()) { // nolint QF1001 + t.Fatal("deserialization of uncompressed infinity point is not infinity") + } + } + // uncompressed { var p1, p2 G1Affine @@ -67,5 +280,103 @@ func TestG1AffineSerialization(t *testing.T) { GenFp(), )) + properties.Property("[G1] Affine SetBytes(Bytes()) should stay the same", prop.ForAll( + func(a fp.Element) bool { + var start, end G1Affine + var ab big.Int + a.BigInt(&ab) + start.ScalarMultiplication(&g1GenAff, &ab) + + buf := start.Bytes() + n, err := end.SetBytes(buf[:]) + if err != nil { + return false + } + if n != SizeOfG1AffineCompressed { + return false + } + return start.X.Equal(&end.X) && start.Y.Equal(&end.Y) + }, + GenFp(), + )) + properties.TestingRun(t, gopter.ConsoleReporter(false)) } + +// define Gopters generators + +// GenFr generates an Fr element +func GenFr() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var elmt fr.Element + elmt.MustSetRandom() + + return gopter.NewGenResult(elmt, gopter.NoShrinker) + } +} + +// GenFp generates an Fp element +func GenFp() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var elmt fp.Element + elmt.MustSetRandom() + + return gopter.NewGenResult(elmt, gopter.NoShrinker) + } +} + +// GenE2 generates an fptower.E2 elmt +func GenE2() gopter.Gen { + return gopter.CombineGens( + GenFp(), + GenFp(), + ).Map(func(values []any) fptower.E2 { + return fptower.E2{A0: values[0].(fp.Element), A1: values[1].(fp.Element)} + }) +} + +// GenE4 generates an fptower.E4 elmt +func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) +} + +// GenE6 generates an fptower.E6 elmt +func GenE6() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E6 { + return fptower.E6{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2), B2: values[2].(fptower.E2)} + }) +} + +// GenE12 generates an fptower.E6 elmt +func GenE12() gopter.Gen { + return gopter.CombineGens( + GenE6(), + GenE6(), + ).Map(func(values []any) fptower.E12 { + return fptower.E12{C0: values[0].(fptower.E6), C1: values[1].(fptower.E6)} + }) +} + +// GenBigInt generates a big.Int +func GenBigInt() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var s big.Int + var b [fp.Bytes]byte + _, err := crand.Read(b[:]) + if err != nil { + panic(err) + } + s.SetBytes(b[:]) + genResult := gopter.NewGenResult(s, gopter.NoShrinker) + return genResult + } +} diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go new file mode 100644 index 0000000000..d45c54cb85 --- /dev/null +++ b/field/koalabear/extensions/e8.go @@ -0,0 +1,311 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package extensions + +import ( + "math/big" + "math/bits" + + fr "github.com/consensys/gnark-crypto/field/koalabear" +) + +// E8 is a degree two finite field extension of E4. +type E8 struct { + C0, C1 E4 +} + +// Equal returns true if z equals x, false otherwise. +func (z *E8) Equal(x *E8) bool { + return z.C0.Equal(&x.C0) && z.C1.Equal(&x.C1) +} + +// Cmp compares (lexicographic order) z and x. +func (z *E8) Cmp(x *E8) int { + if a1 := z.C1.Cmp(&x.C1); a1 != 0 { + return a1 + } + return z.C0.Cmp(&x.C0) +} + +// LexicographicallyLargest returns true if this element is strictly lexicographically +// larger than its negation, false otherwise. +func (z *E8) LexicographicallyLargest() bool { + if z.C1.IsZero() { + return z.C0.LexicographicallyLargest() + } + return z.C1.LexicographicallyLargest() +} + +// String puts E8 in string form. +func (z *E8) String() string { + return z.C0.String() + "+(" + z.C1.String() + ")*w" +} + +// SetString sets an E8 from strings. +func (z *E8) SetString(s0, s1, s2, s3, s4, s5, s6, s7 string) *E8 { + z.C0.SetString(s0, s1, s2, s3) + z.C1.SetString(s4, s5, s6, s7) + return z +} + +// Set copies x into z and returns z. +func (z *E8) Set(x *E8) *E8 { + z.C0 = x.C0 + z.C1 = x.C1 + return z +} + +// SetZero sets an E8 elmt to zero. +func (z *E8) SetZero() *E8 { + z.C0.SetZero() + z.C1.SetZero() + return z +} + +// SetOne sets z to 1 in Montgomery form and returns z. +func (z *E8) SetOne() *E8 { + *z = E8{} + z.C0.B0.A0.SetOne() + return z +} + +// Lift sets the C0.B0.A0 component of z to v. +func (z *E8) Lift(v *fr.Element) *E8 { + *z = E8{} + z.C0.B0.A0.Set(v) + return z +} + +// MulByElement multiplies an element in E8 by an element in fr. +func (z *E8) MulByElement(x *E8, y *fr.Element) *E8 { + z.C0.MulByElement(&x.C0, y) + z.C1.MulByElement(&x.C1, y) + return z +} + +// Add sets z=x+y in E8 and returns z. +func (z *E8) Add(x, y *E8) *E8 { + z.C0.Add(&x.C0, &y.C0) + z.C1.Add(&x.C1, &y.C1) + return z +} + +// Sub sets z=x-y in E8 and returns z. +func (z *E8) Sub(x, y *E8) *E8 { + z.C0.Sub(&x.C0, &y.C0) + z.C1.Sub(&x.C1, &y.C1) + return z +} + +// Double sets z=2*x and returns z. +func (z *E8) Double(x *E8) *E8 { + z.C0.Double(&x.C0) + z.C1.Double(&x.C1) + return z +} + +// Neg negates an E8 element. +func (z *E8) Neg(x *E8) *E8 { + z.C0.Neg(&x.C0) + z.C1.Neg(&x.C1) + return z +} + +// SetRandom used only in tests. +func (z *E8) SetRandom() (*E8, error) { + if _, err := z.C0.SetRandom(); err != nil { + return nil, err + } + if _, err := z.C1.SetRandom(); err != nil { + return nil, err + } + return z, nil +} + +// MustSetRandom sets the element to a random value. +func (z *E8) MustSetRandom() *E8 { + if _, err := z.SetRandom(); err != nil { + panic(err) + } + return z +} + +// IsZero returns true if z is zero, false otherwise. +func (z *E8) IsZero() bool { + return z.C0.IsZero() && z.C1.IsZero() +} + +// IsOne returns true if z is one, false otherwise. +func (z *E8) IsOne() bool { + return z.C0.IsOne() && z.C1.IsZero() +} + +// Mul sets z=x*y in E8 and returns z. +func (z *E8) Mul(x, y *E8) *E8 { + var a, b, c, d E4 + a.Mul(&x.C0, &y.C0) + b.Mul(&x.C1, &y.C1) + c.Add(&x.C0, &x.C1) + d.Add(&y.C0, &y.C1) + c.Mul(&c, &d). + Sub(&c, &a). + Sub(&c, &b) + z.C1.Set(&c) + b.MulByNonResidue(&b) + z.C0.Add(&a, &b) + return z +} + +// Square sets z=x*x in E8 and returns z. +func (z *E8) Square(x *E8) *E8 { + return z.Mul(x, x) +} + +// Inverse sets z to the inverse of x in E8 and returns z. +func (z *E8) Inverse(x *E8) *E8 { + var t0, t1 E4 + t0.Square(&x.C0) + t1.Square(&x.C1).MulByNonResidue(&t1) + t0.Sub(&t0, &t1).Inverse(&t0) + z.C0.Mul(&x.C0, &t0) + z.C1.Mul(&x.C1, &t0).Neg(&z.C1) + return z +} + +// Exp sets z=x^k and returns it. +func (z *E8) Exp(x E8, k *big.Int) *E8 { + if k.IsInt64() { + return z.ExpInt64(x, k.Int64()) + } + + e := k + if k.Sign() == -1 { + x.Inverse(&x) + e = bigIntPool.Get().(*big.Int) + defer bigIntPool.Put(e) + e.Neg(k) + } + + z.SetOne() + b := e.Bytes() + for i := range len(b) { + w := b[i] + for j := range 8 { + z.Square(z) + if (w & (0b10000000 >> j)) != 0 { + z.Mul(z, &x) + } + } + } + + return z +} + +// ExpInt64 sets z=x^k and returns it, where k is an int64. +func (z *E8) ExpInt64(x E8, k int64) *E8 { + if k == 0 { + return z.SetOne() + } + + exp := k + if k < 0 { + x.Inverse(&x) + exp = -k + } + + z.Set(&x) + for i := bits.Len64(uint64(exp)) - 2; i >= 0; i-- { + z.Square(z) + if (uint64(exp)>>uint(i))&1 != 0 { + z.Mul(z, &x) + } + } + + return z +} + +// Conjugate sets z to x conjugated and returns z. +func (z *E8) Conjugate(x *E8) *E8 { + z.C0 = x.C0 + z.C1.Neg(&x.C1) + return z +} + +// Halve sets z to z / 2. +func (z *E8) Halve() { + z.C0.Halve() + z.C1.Halve() +} + +// norm sets x to the norm of z. +func (z *E8) norm(x *E4) { + var tmp E4 + tmp.Square(&z.C1).MulByNonResidue(&tmp) + x.Square(&z.C0).Sub(x, &tmp) +} + +// Legendre returns the Legendre symbol of z. +func (z *E8) Legendre() int { + var n E4 + z.norm(&n) + return n.Legendre() +} + +// Sqrt sets z to the square root of x and returns z. +func (z *E8) Sqrt(x *E8) *E8 { + var x0, x1 E4 + x.norm(&x0) + x0.Sqrt(&x0) + x1.Add(&x.C0, &x0).Halve() + if x1.Legendre() != 1 { + x1.Sub(&x.C0, &x0).Halve() + } + x1.Sqrt(&x1) + z.C0.Set(&x1) + x1.Double(&x1) + z.C1.Div(&x.C1, &x1) + return z +} + +// BatchInvertE8 returns a new slice with every element in a inverted. +func BatchInvertE8(a []E8) []E8 { + res := make([]E8, len(a)) + if len(a) == 0 { + return res + } + + zeroes := make([]bool, len(a)) + var accumulator E8 + accumulator.SetOne() + + for i := range len(a) { + if a[i].IsZero() { + zeroes[i] = true + continue + } + res[i].Set(&accumulator) + accumulator.Mul(&accumulator, &a[i]) + } + + accumulator.Inverse(&accumulator) + + for i := len(a) - 1; i >= 0; i-- { + if zeroes[i] { + continue + } + res[i].Mul(&res[i], &accumulator) + accumulator.Mul(&accumulator, &a[i]) + } + + return res +} + +// Div divides an element in E8 by an element in E8. +func (z *E8) Div(x *E8, y *E8) *E8 { + var r E8 + r.Inverse(y).Mul(x, &r) + return z.Set(&r) +} diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go new file mode 100644 index 0000000000..6dfd0fe6f8 --- /dev/null +++ b/field/koalabear/extensions/e8_test.go @@ -0,0 +1,175 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package extensions + +import ( + "math/big" + "testing" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/prop" +) + +func TestE8ReceiverIsOperand(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + + properties := gopter.NewProperties(parameters) + genA := genE8() + genB := genE8() + + properties.Property("[koalabear] receiver as operand should be stable", prop.ForAll( + func(a, b E8) bool { + var c, d, e, f E8 + d.Set(&a) + c.Add(&a, &b) + a.Add(&a, &b) + b.Add(&d, &b) + e.Sub(&c, &d) + f.Sub(&b, &d) + return a.Equal(&b) && a.Equal(&c) && e.Equal(&f) + }, + genA, + genB, + )) + + properties.Property("[koalabear] mul receiver aliasing should be stable", prop.ForAll( + func(a, b E8) bool { + var c, d E8 + d.Set(&a) + c.Mul(&a, &b) + a.Mul(&a, &b) + b.Mul(&d, &b) + return a.Equal(&b) && a.Equal(&c) + }, + genA, + genB, + )) + + properties.Property("[koalabear] square receiver aliasing should be stable", prop.ForAll( + func(a E8) bool { + var b E8 + b.Square(&a) + a.Square(&a) + return a.Equal(&b) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestE8Ops(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + + properties := gopter.NewProperties(parameters) + genA := genE8() + genB := genE8() + + properties.Property("[koalabear] sub(add(a,b),b)=a", prop.ForAll( + func(a, b E8) bool { + var c E8 + c.Add(&a, &b).Sub(&c, &b) + return c.Equal(&a) + }, + genA, + genB, + )) + + properties.Property("[koalabear] mul(inverse(b),b)=1", prop.ForAll( + func(a, b E8) bool { + if b.IsZero() { + return true + } + var c, d E8 + d.Inverse(&b) + c.Mul(&a, &b).Mul(&c, &d) + return c.Equal(&a) + }, + genA, + genB, + )) + + properties.Property("[koalabear] square and mul match", prop.ForAll( + func(a E8) bool { + var b, c E8 + b.Mul(&a, &a) + c.Square(&a) + return b.Equal(&c) + }, + genA, + )) + + properties.Property("[koalabear] square(sqrt(x^2)) = x^2", prop.ForAll( + func(a E8) bool { + var b, c E8 + b.Square(&a) + c.Sqrt(&b).Square(&c) + return c.Equal(&b) + }, + genA, + )) + + properties.Property("[koalabear] batch inverse matches inverse", prop.ForAll( + func(a, b, c E8) bool { + batch := BatchInvertE8([]E8{a, b, c}) + if !a.IsZero() { + a.Inverse(&a) + } + if !b.IsZero() { + b.Inverse(&b) + } + if !c.IsZero() { + c.Inverse(&c) + } + return a.Equal(&batch[0]) && b.Equal(&batch[1]) && c.Equal(&batch[2]) + }, + genA, + genA, + genB, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestE8Exp(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 50 + + properties := gopter.NewProperties(parameters) + genA := genE8() + + properties.Property("[koalabear] Exp(x, 0) = 1", prop.ForAll( + func(a E8) bool { + var res, one E8 + one.SetOne() + res.Exp(a, big.NewInt(0)) + return res.Equal(&one) + }, + genA, + )) + + properties.Property("[koalabear] Exp(x, 1) = x", prop.ForAll( + func(a E8) bool { + var res E8 + res.Exp(a, big.NewInt(1)) + return res.Equal(&a) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func genE8() gopter.Gen { + return gopter.CombineGens( + genE4(), + genE4(), + ).Map(func(values []any) E8 { + return E8{C0: values[0].(E4), C1: values[1].(E4)} + }) +} diff --git a/internal/generator/addchain/2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 b/internal/generator/addchain/2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 new file mode 100644 index 0000000000000000000000000000000000000000..d8f91cde03de039651472ca60366be699c1c58e5 GIT binary patch literal 2240 zcmXAp0c2Ne9EN}IxwAVnGc%LjnVH?0nas@0%*@Q}&g{<2%uIGOyK{ASXOfxO?am~V zBuSDaNs=Tj&4 z6*fYJy%E3s`;(OXzQiD3@SBQ48ul8ImTdX5RKG@&MB?8UshEpOLd7b@%2>)-Dp)F6 zs#vO7wz1T()P~Ac7fF4nK@FisHHMnh6lzv;s6{QIR<(xev^~_Own*AT9qI^msx#E3 zu28qSLp|yV^{O}2r@m0X`a=U62vuq@G^C;MhaV1&Xe2bM(NLMjLgN|_O=u!Csmaik zrb5%24$WvLRHYrES47*$zk>Anm|bOF3Gp50^o@0O0hxiw-rNr6SwMCIG6%>!APd0ZPC3A-kFY@&0a*fM88{RvM>_SPHpmJf ztAMNlvJS{zKsF%%UN@Qd0kR*EEkF(cau6chEr*y719AkAqktR(P_0l5aqbwF+aaubkSkZ-ly%y$5}3&=e{ z?gR1wkcW^z*dyl0fII=@DIm`Pc@FtKFPL8f@(PgGfV=_ZEgqyvyn$aZ$p#oP@@4o4! zQo&=Dp#oJ!QXMLhmr9mIbzG~*8bS?f#F|1yY7RAFEouq1sx{Q6wotp;Lmlb}b*eMe zrLIuDx8qr8-RHLCWjfKWF9-7cZXi}4* zDNTh+wKFuW>ClX3LbI9;?b5E$ZtV`uX)ZLc`OtzELW^4D9W8~HwH#W}N@!K9p*5|A z*0mnm&_-xeo9u43r@5_cuPc<>=g0Ott^xAPv z>9|fgk>pNW&p58Lj%(X-opW609oGfNbTg)p1>OLXFB@_i)2;-E>^H z9M^5fb;ohtbwV}D-ScqYaXoNc4;|Md$Mx88J#k!5olv=Q&pbSLTrV8gOUL!falLk2 zZyeWKC)<~I9^N~y502}j@DD`u!QC0uU}$B2|D?Lu9|KhPf7yIzZ|HX#k`VkS0Ky0oega z3m~l!*@ZUdc0f7+=>+y7<&dT72BZg&UO@T)=?7#0kU>C(02u~k1oHbb$~*?hI3N>% zOad|m$WB0}0hs}07WgztIkc&dtwH7hnFnM6kVQb2AphcJ<`qCz0a*iN9gqz`HUZfS z$QE#urP{~5ACLop90cSLAcp}t0{IO(%6trvjH z19Ab7i;(}zmzXaDas`m9fLsIQIv_Uyxe3TEKyCwa2avmf+=Kj1-DiFP$U{IL0pAU& z9y31y$;`~m%*@Q}PIhNzW-_~z?#$YqnVFg0?xdR} zNs=T*(^|e=YyH zTiI49+Y|BIKfg%HA4?4K6Mk1wNW)$u(vtuDv{boBl1PdoDGn8@gy)sAlrffL6)cr3 zRXkQ5DpE}(wV^V3sb)#kz_ms!7s{y#YYvsDCDe?ysx{Q6wotp;Lmlb}b*eMerLIu7 zxJ*r*?*RX;)}YbD??7hZeLDTGS%%XeqR;<&LaSO0t!XW^ zuJzD{HbR@)WOws>nr-Fxx=PtzKeo?t?RQ)U9M?g|b;xlYc3elCP`$FF9*#M#$92sKH7UF9;fCY7>9}q= zuG^05j^n!PgzA*t^KjpBJ#btP9oHkr_1JMeaa>QGP^GeG9-cd{7mn+t<9g+|UOTQg zj_a+H-#GxLQ1;EkcgOX^;ZDtddPtp6syr+SAVq)_ z15yG=DIjHllmmBs%ArX8{tQwD2$!mn8bE3x^1rN(xgL-PKpFwb0n!9WGaxO1Yy+ef zkT!_?g?8o+Kso{G0`?;1kfrJYq!*ArK>7h00Avu5AwY%!*$&7E6!vA5c?^(oKqdg0 z1Y`=39e_*&G6Tpg@M)5AXj30sgUkUk56A)_i-0Ua;l<0$D}by5vIfXHARB;e0DxAo~G10LVc=4gqo)3LA8U`6wXA067lG2|!K)ate^sz~N1GhWRWY=Kwhm z$OS+yLg6o8V!jN>6+o^6at)B{fZPD&CLp%}xedr2K<)x^4+=YVpZNhG4*_`ud^eID?nZY@&=H%fV>0bJrqvK2j-7}d;;V%AYTCa3dlD=zC+{> z_YdZufaHHHNgkF2kRm{e0rC4zO7eGRQp#KgNI4)CkUz0WC36)Z)qvChQVU2OAoYMW z0MZCZ4v;25ngMA6zJk+a8*?ilZGf~xBx%yY)CouzAl-oU0MZLcAMnzCrU5_(A^ryd CV6*uE literal 0 HcmV?d00001 diff --git a/internal/generator/config/curve.go b/internal/generator/config/curve.go index 3c04ea48d2..9a5c2231d4 100644 --- a/internal/generator/config/curve.go +++ b/internal/generator/config/curve.go @@ -43,6 +43,10 @@ type Curve struct { E2CbrtTorusLucasTopBit int // bit length - 1 } +func (c Curve) HasG2() bool { + return c.G2.PointName != "" +} + type TwistedEdwardsCurve struct { Name string Package string diff --git a/internal/generator/config/kb8.go b/internal/generator/config/kb8.go new file mode 100644 index 0000000000..dcca022bae --- /dev/null +++ b/internal/generator/config/kb8.go @@ -0,0 +1,22 @@ +package config + +var KB8 = Curve{ + Name: "kb8", + CurvePackage: "kb8", + EnumID: "KB8", + FpModulus: "2130706433", + FrModulus: "424804331891979973455971894938199991839487883914575852667663156896715214921", + G1: Point{ + CoordType: "fptower.E8", + CoordExtDegree: 8, + CoordExtRoot: 3, + PointName: "g1", + GLV: false, + CofactorCleaning: false, + CRange: defaultCRange(), + }, +} + +func init() { + addCurve(&KB8) +} diff --git a/internal/generator/ecc/generate.go b/internal/generator/ecc/generate.go index c644eab840..c755330114 100644 --- a/internal/generator/ecc/generate.go +++ b/internal/generator/ecc/generate.go @@ -127,11 +127,6 @@ func Generate(conf config.Curve, baseDir string, gen *common.Generator) error { return err } - // No G2 for secp256k1 and grumpkin - if conf.Equal(config.SECP256K1) || conf.Equal(config.GRUMPKIN) || conf.Equal(config.SECP256R1) { - return nil - } - // marshal entries = []bavard.Entry{ {File: filepath.Join(baseDir, "marshal.go"), Templates: []string{"marshal.go.tmpl"}}, @@ -142,6 +137,11 @@ func Generate(conf config.Curve, baseDir string, gen *common.Generator) error { return err } + // No G2 for single-group curves. + if !conf.HasG2() { + return nil + } + // G2 entries = []bavard.Entry{ {File: filepath.Join(baseDir, "g2.go"), Templates: []string{"point.go.tmpl"}}, diff --git a/internal/generator/ecc/template/marshal.go.tmpl b/internal/generator/ecc/template/marshal.go.tmpl index ccee9e66a1..0fd381212d 100644 --- a/internal/generator/ecc/template/marshal.go.tmpl +++ b/internal/generator/ecc/template/marshal.go.tmpl @@ -14,7 +14,9 @@ import ( "encoding/binary" "sync/atomic" + {{- if .HasG2 }} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" + {{- end }} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" "github.com/consensys/gnark-crypto/parallel" @@ -23,7 +25,16 @@ import ( // To encode G1Affine and G2Affine points, we mask the most significant bits with these bits to specify without ambiguity // metadata needed for point (de)compression -{{- if ge .FpUnusedBits 3}} +{{- if eq .Name "kb8"}} +const ( + mMask byte = 0xff + mUncompressed byte = 0x00 + mUncompressedInfinity byte = 0x01 + mCompressedSmallest byte = 0x02 + mCompressedLargest byte = 0x03 + mCompressedInfinity byte = 0x04 +) +{{- else if ge .FpUnusedBits 3}} // we follow the BLS12-381 style encoding as specified in ZCash and now IETF // see https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/11/ // Appendix C. ZCash serialization format for BLS12_381 @@ -50,8 +61,10 @@ const ( ) {{- end}} +{{- if .HasG2 }} // SizeOfGT represents the size in bytes that a GT element need in binary form const SizeOfGT = fptower.SizeOfGT +{{- end }} var ( ErrInvalidInfinityEncoding = errors.New("invalid infinity point encoding") @@ -106,7 +119,11 @@ func (dec *Decoder) Decode(v any) (err error) { return } + {{- if .HasG2 }} var buf [SizeOfG2AffineUncompressed]byte + {{- else }} + var buf [SizeOfG1AffineUncompressed]byte + {{- end }} var read int var sliceLen uint32 @@ -225,6 +242,7 @@ func (dec *Decoder) Decode(v any) (err error) { } _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) return + {{- if .HasG2 }} case *G2Affine: // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. read, err = io.ReadFull(dec.r, buf[:SizeOfG2AffineCompressed]) @@ -254,6 +272,7 @@ func (dec *Decoder) Decode(v any) (err error) { } _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) return + {{- end }} case *[]G1Affine: sliceLen, err = dec.readUint32() if err != nil { @@ -321,6 +340,7 @@ func (dec *Decoder) Decode(v any) (err error) { } return nil + {{- if .HasG2 }} case *[]G2Affine: sliceLen, err = dec.readUint32() if err != nil { @@ -389,6 +409,7 @@ func (dec *Decoder) Decode(v any) (err error) { } return nil + {{- end }} default: n := binary.Size(t) if n == -1 { @@ -594,11 +615,13 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + {{- if .HasG2 }} case *G2Affine: buf := t.{{- $.Raw}}Bytes() written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + {{- end }} case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -666,6 +689,7 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { } } return nil + {{- if .HasG2 }} case *[]G2Affine: return enc.encode{{- $.Raw}}(*t) case []G2Affine: @@ -687,6 +711,7 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { } } return nil + {{- end }} default: n := binary.Size(t) if n == -1 { @@ -700,21 +725,190 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { {{end}} -{{- $sizeOfFp := mul .Fp.NbWords 8}} +{{- $sizeOfFp := .Fp.NbBytes}} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G1.CoordType "PointName" .G1.PointName "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange}} +{{- if .HasG2 }} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G2.CoordType "PointName" .G2.PointName "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange}} +{{- end }} {{define "marshalpoint"}} +{{- if and (eq $.all.Name "kb8") (eq $.PointName "g1") }} + +// SizeOf{{ $.TAffine }}Compressed represents the size in bytes that a {{ $.TAffine }} need in binary form, compressed +const SizeOf{{ $.TAffine }}Compressed = 1 + {{ $.sizeOfFp }}*8 + +// SizeOf{{ $.TAffine }}Uncompressed represents the size in bytes that a {{ $.TAffine }} need in binary form, uncompressed +const SizeOf{{ $.TAffine }}Uncompressed = 1 + 2*({{ $.sizeOfFp }}*8) + +func (p *{{ $.TAffine }}) Marshal() ([]byte) { + b := p.RawBytes() + return b[:] +} + +func (p *{{ $.TAffine }}) Unmarshal(buf []byte) error { + _, err := p.SetBytes(buf) + return err +} + +func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Compressed]byte) { + if p.X.IsZero() && p.Y.IsZero() { + res[0] = mCompressedInfinity + return + } + res[0] = mCompressedSmallest + if p.Y.LexicographicallyLargest() { + res[0] = mCompressedLargest + } + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X.C1.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[5:5+fp.Bytes]), p.X.C1.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[9:9+fp.Bytes]), p.X.C1.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[13:13+fp.Bytes]), p.X.C1.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[17:17+fp.Bytes]), p.X.C0.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[21:21+fp.Bytes]), p.X.C0.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[25:25+fp.Bytes]), p.X.C0.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[29:29+fp.Bytes]), p.X.C0.B0.A0) + return +} + +func (p *{{ $.TAffine }}) RawBytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byte) { + if p.X.IsZero() && p.Y.IsZero() { + res[0] = mUncompressedInfinity + return + } + res[0] = mUncompressed + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[33:33+fp.Bytes]), p.Y.C1.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[37:37+fp.Bytes]), p.Y.C1.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[41:41+fp.Bytes]), p.Y.C1.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[45:45+fp.Bytes]), p.Y.C1.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[49:49+fp.Bytes]), p.Y.C0.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[53:53+fp.Bytes]), p.Y.C0.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[57:57+fp.Bytes]), p.Y.C0.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[61:61+fp.Bytes]), p.Y.C0.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X.C1.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[5:5+fp.Bytes]), p.X.C1.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[9:9+fp.Bytes]), p.X.C1.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[13:13+fp.Bytes]), p.X.C1.B0.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[17:17+fp.Bytes]), p.X.C0.B1.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[21:21+fp.Bytes]), p.X.C0.B1.A0) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[25:25+fp.Bytes]), p.X.C0.B0.A1) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[29:29+fp.Bytes]), p.X.C0.B0.A0) + return +} + +func (p *{{ $.TAffine }}) SetBytes(buf []byte) (int, error) { + return p.setBytes(buf, true) +} + +func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) { + if len(buf) < SizeOf{{ $.TAffine }}Compressed { + return 0, io.ErrShortBuffer + } + mData := buf[0] + if isMaskInvalid(mData) { + return 0, ErrInvalidEncoding + } + if mData == mUncompressed || mData == mUncompressedInfinity { + if len(buf) < SizeOf{{ $.TAffine }}Uncompressed { + return 0, io.ErrShortBuffer + } + } + if mData == mCompressedInfinity { + if !isZeroed(0, buf[1:SizeOf{{ $.TAffine }}Compressed]) { + return 0, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return SizeOf{{ $.TAffine }}Compressed, nil + } + if mData == mUncompressedInfinity { + if !isZeroed(0, buf[1:SizeOf{{ $.TAffine }}Uncompressed]) { + return 0, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return SizeOf{{ $.TAffine }}Uncompressed, nil + } + if err := p.X.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*0 : 1+fp.Bytes*1]); err != nil { return 0, err } + if err := p.X.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*1 : 1+fp.Bytes*2]); err != nil { return 0, err } + if err := p.X.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*2 : 1+fp.Bytes*3]); err != nil { return 0, err } + if err := p.X.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*3 : 1+fp.Bytes*4]); err != nil { return 0, err } + if err := p.X.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*4 : 1+fp.Bytes*5]); err != nil { return 0, err } + if err := p.X.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*5 : 1+fp.Bytes*6]); err != nil { return 0, err } + if err := p.X.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*6 : 1+fp.Bytes*7]); err != nil { return 0, err } + if err := p.X.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*7 : 1+fp.Bytes*8]); err != nil { return 0, err } + if mData == mUncompressed { + if err := p.Y.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*8 : 1+fp.Bytes*9]); err != nil { return 0, err } + if err := p.Y.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*9 : 1+fp.Bytes*10]); err != nil { return 0, err } + if err := p.Y.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*10 : 1+fp.Bytes*11]); err != nil { return 0, err } + if err := p.Y.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*11 : 1+fp.Bytes*12]); err != nil { return 0, err } + if err := p.Y.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*12 : 1+fp.Bytes*13]); err != nil { return 0, err } + if err := p.Y.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*13 : 1+fp.Bytes*14]); err != nil { return 0, err } + if err := p.Y.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*14 : 1+fp.Bytes*15]); err != nil { return 0, err } + if err := p.Y.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*15 : 1+fp.Bytes*16]); err != nil { return 0, err } + if subGroupCheck && !p.IsInSubGroup() { return 0, errors.New("invalid point: subgroup check failed") } + return SizeOf{{ $.TAffine }}Uncompressed, nil + } + p.Y.C0.B0.A0[0] = uint32(mData) + if err := p.unsafeComputeY(subGroupCheck); err != nil { return 0, err } + return SizeOf{{ $.TAffine }}Compressed, nil +} + +func (p *{{ $.TAffine }}) unsafeComputeY(subGroupCheck bool) error { + mData := byte(p.Y.C0.B0.A0[0]) + var YSquared, Y, tmp {{$.CoordType}} + YSquared.Square(&p.X).Mul(&YSquared, &p.X) + tmp.Mul(&p.X, &aCurveCoeff) + YSquared.Add(&YSquared, &tmp).Add(&YSquared, &bCurveCoeff) + if YSquared.Legendre() == -1 { + return errors.New("invalid compressed coordinate: square root doesn't exist") + } + Y.Sqrt(&YSquared) + if Y.LexicographicallyLargest() { + if mData == mCompressedSmallest { + Y.Neg(&Y) + } + } else if mData == mCompressedLargest { + Y.Neg(&Y) + } + p.Y = Y + if subGroupCheck && !p.IsInSubGroup() { return errors.New("invalid point: subgroup check failed") } + return nil +} + +func (p *{{ $.TAffine }}) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, err error) { + mData := buf[0] + if mData == mCompressedInfinity { + isInfinity = true + if !isZeroed(0, buf[1:SizeOf{{ $.TAffine }}Compressed]) { + return isInfinity, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return isInfinity, nil + } + if err := p.X.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*0 : 1+fp.Bytes*1]); err != nil { return false, err } + if err := p.X.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*1 : 1+fp.Bytes*2]); err != nil { return false, err } + if err := p.X.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*2 : 1+fp.Bytes*3]); err != nil { return false, err } + if err := p.X.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*3 : 1+fp.Bytes*4]); err != nil { return false, err } + if err := p.X.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*4 : 1+fp.Bytes*5]); err != nil { return false, err } + if err := p.X.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*5 : 1+fp.Bytes*6]); err != nil { return false, err } + if err := p.X.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*6 : 1+fp.Bytes*7]); err != nil { return false, err } + if err := p.X.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*7 : 1+fp.Bytes*8]); err != nil { return false, err } + p.Y.C0.B0.A0[0] = uint32(mData) + return isInfinity, nil +} + +{{- else }} // SizeOf{{ $.TAffine }}Compressed represents the size in bytes that a {{ $.TAffine }} need in binary form, compressed -const SizeOf{{ $.TAffine }}Compressed = {{ $.sizeOfFp }} {{- if eq $.CoordType "fptower.E2"}} * 2 {{- end}} {{- if eq $.CoordType "fptower.E4"}} * 4 {{- end}} +const SizeOf{{ $.TAffine }}Compressed = {{ $.sizeOfFp }} {{- if eq $.CoordType "fptower.E2"}} * 2 {{- end}} {{- if eq $.CoordType "fptower.E4"}} * 4 {{- end}} {{- if eq $.CoordType "fptower.E8"}} * 8 {{- end}} // SizeOf{{ $.TAffine }}Uncompressed represents the size in bytes that a {{ $.TAffine }} need in binary form, uncompressed const SizeOf{{ $.TAffine }}Uncompressed = SizeOf{{ $.TAffine }}Compressed * 2 @@ -788,6 +982,23 @@ func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Compressed]byte) { {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A1"}} {{- $offset := mul $.sizeOfFp 3}} {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A0"}} + {{- else if eq $.CoordType "fptower.E8"}} + // p.X.C1.B1.A1 | p.X.C1.B1.A0 | p.X.C1.B0.A1 | p.X.C1.B0.A0 | p.X.C0.B1.A1 | p.X.C0.B1.A0 | p.X.C0.B0.A1 | p.X.C0.B0.A0 + {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X.C1.B1.A1"}} + {{- $offset := mul $.sizeOfFp 1}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B1.A0"}} + {{- $offset := mul $.sizeOfFp 2}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A1"}} + {{- $offset := mul $.sizeOfFp 3}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A0"}} + {{- $offset := mul $.sizeOfFp 4}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A1"}} + {{- $offset := mul $.sizeOfFp 5}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A0"}} + {{- $offset := mul $.sizeOfFp 6}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A1"}} + {{- $offset := mul $.sizeOfFp 7}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A0"}} {{- else}} {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X"}} {{- end}} @@ -832,6 +1043,24 @@ func (p *{{ $.TAffine }}) RawBytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byt {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.B0.A1"}} {{- $offset := mul $.sizeOfFp 7}} {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.B0.A0"}} + {{- else if eq $.CoordType "fptower.E8"}} + // p.Y.C1.B1.A1 | p.Y.C1.B1.A0 | p.Y.C1.B0.A1 | p.Y.C1.B0.A0 | p.Y.C0.B1.A1 | p.Y.C0.B1.A0 | p.Y.C0.B0.A1 | p.Y.C0.B0.A0 + {{- $offset := mul $.sizeOfFp 8}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B1.A1"}} + {{- $offset := mul $.sizeOfFp 9}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B1.A0"}} + {{- $offset := mul $.sizeOfFp 10}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B0.A1"}} + {{- $offset := mul $.sizeOfFp 11}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B0.A0"}} + {{- $offset := mul $.sizeOfFp 12}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B1.A1"}} + {{- $offset := mul $.sizeOfFp 13}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B1.A0"}} + {{- $offset := mul $.sizeOfFp 14}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B0.A1"}} + {{- $offset := mul $.sizeOfFp 15}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B0.A0"}} {{- else}} {{- template "putFp" dict "all" .all "OffSet" $.sizeOfFp "From" "p.Y"}} {{- end}} @@ -851,6 +1080,23 @@ func (p *{{ $.TAffine }}) RawBytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byt {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A1"}} {{- $offset := mul $.sizeOfFp 3}} {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A0"}} + {{- else if eq $.CoordType "fptower.E8"}} + // p.X.C1.B1.A1 | p.X.C1.B1.A0 | p.X.C1.B0.A1 | p.X.C1.B0.A0 | p.X.C0.B1.A1 | p.X.C0.B1.A0 | p.X.C0.B0.A1 | p.X.C0.B0.A0 + {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X.C1.B1.A1"}} + {{- $offset := mul $.sizeOfFp 1}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B1.A0"}} + {{- $offset := mul $.sizeOfFp 2}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A1"}} + {{- $offset := mul $.sizeOfFp 3}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A0"}} + {{- $offset := mul $.sizeOfFp 4}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A1"}} + {{- $offset := mul $.sizeOfFp 5}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A0"}} + {{- $offset := mul $.sizeOfFp 6}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A1"}} + {{- $offset := mul $.sizeOfFp 7}} + {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A0"}} {{- else}} {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X"}} {{- end}} @@ -965,6 +1211,55 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) if err := p.Y.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { return 0, err } + {{- else if eq $.CoordType "fptower.E8"}} + if err := p.X.C1.B1.A1.SetBytesCanonical(buf[fp.Bytes*0:fp.Bytes*1]); err != nil { + return 0, err + } + if err := p.X.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*1:fp.Bytes*2]); err != nil { + return 0, err + } + if err := p.X.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { + return 0, err + } + if err := p.X.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { + return 0, err + } + if err := p.X.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*4:fp.Bytes*5]); err != nil { + return 0, err + } + if err := p.X.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*5:fp.Bytes*6]); err != nil { + return 0, err + } + if err := p.X.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*6:fp.Bytes*7]); err != nil { + return 0, err + } + if err := p.X.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { + return 0, err + } + if err := p.Y.C1.B1.A1.SetBytesCanonical(buf[fp.Bytes*8:fp.Bytes*9]); err != nil { + return 0, err + } + if err := p.Y.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*9:fp.Bytes*10]); err != nil { + return 0, err + } + if err := p.Y.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*10:fp.Bytes*11]); err != nil { + return 0, err + } + if err := p.Y.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*11:fp.Bytes*12]); err != nil { + return 0, err + } + if err := p.Y.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*12:fp.Bytes*13]); err != nil { + return 0, err + } + if err := p.Y.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*13:fp.Bytes*14]); err != nil { + return 0, err + } + if err := p.Y.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*14:fp.Bytes*15]); err != nil { + return 0, err + } + if err := p.Y.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*15:fp.Bytes*16]); err != nil { + return 0, err + } {{- else}} if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { return 0, err @@ -1014,6 +1309,31 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) if err := p.X.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { return 0, err } + {{- else if eq $.CoordType "fptower.E8"}} + if err := p.X.C1.B1.A1.SetBytesCanonical(bufX[fp.Bytes*0:fp.Bytes*1]); err != nil { + return 0, err + } + if err := p.X.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*1:fp.Bytes*2]); err != nil { + return 0, err + } + if err := p.X.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { + return 0, err + } + if err := p.X.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { + return 0, err + } + if err := p.X.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*4:fp.Bytes*5]); err != nil { + return 0, err + } + if err := p.X.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*5:fp.Bytes*6]); err != nil { + return 0, err + } + if err := p.X.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*6:fp.Bytes*7]); err != nil { + return 0, err + } + if err := p.X.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { + return 0, err + } {{- else}} if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { return 0, err @@ -1026,7 +1346,7 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) YSquared.Square(&p.X).Mul(&YSquared, &p.X) YSquared.Add(&YSquared, &{{- if eq .PointName "g2"}}bTwistCurveCoeff{{- else}}bCurveCoeff{{- end}}) - {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4")}} + {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4") (eq $.CoordType "fptower.E8")}} if YSquared.Legendre() == -1 { return 0, errors.New("invalid compressed coordinate: square root doesn't exist") } @@ -1070,6 +1390,8 @@ func (p *{{ $.TAffine }}) unsafeComputeY(subGroupCheck bool) error { mData := byte(p.Y.A0[0]) {{ else if eq $.CoordType "fptower.E4"}} mData := byte(p.Y.B0.A0[0]) + {{ else if eq $.CoordType "fptower.E8"}} + mData := byte(p.Y.C0.B0.A0[0]) {{ else}} mData := byte(p.Y[0]) {{ end}} @@ -1077,11 +1399,18 @@ func (p *{{ $.TAffine }}) unsafeComputeY(subGroupCheck bool) error { // we have a compressed coordinate, we need to solve the curve equation to compute Y var YSquared, Y {{$.CoordType}} + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + var tmp {{$.CoordType}} + {{- end }} YSquared.Square(&p.X).Mul(&YSquared, &p.X) + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + tmp.Mul(&p.X, &aCurveCoeff) + YSquared.Add(&YSquared, &tmp) + {{- end }} YSquared.Add(&YSquared, &{{- if eq .PointName "g2"}}bTwistCurveCoeff{{- else}}bCurveCoeff{{- end}}) - {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4")}} + {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4") (eq $.CoordType "fptower.E8")}} if YSquared.Legendre() == -1 { return errors.New("invalid compressed coordinate: square root doesn't exist") } @@ -1168,6 +1497,32 @@ func (p *{{ $.TAffine }}) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, // store mData in p.Y.B0.A0[0] p.Y.B0.A0[0] = uint64(mData) + {{- else if eq $.CoordType "fptower.E8"}} + if err := p.X.C1.B1.A1.SetBytesCanonical(bufX[fp.Bytes*0:fp.Bytes*1]); err != nil { + return false, err + } + if err := p.X.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*1:fp.Bytes*2]); err != nil { + return false, err + } + if err := p.X.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { + return false, err + } + if err := p.X.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { + return false, err + } + if err := p.X.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*4:fp.Bytes*5]); err != nil { + return false, err + } + if err := p.X.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*5:fp.Bytes*6]); err != nil { + return false, err + } + if err := p.X.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*6:fp.Bytes*7]); err != nil { + return false, err + } + if err := p.X.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { + return false, err + } + p.Y.C0.B0.A0[0] = {{- if eq $.all.Fp.NbBytes 4 }} uint32(mData) {{- else }} uint64(mData) {{- end }} {{- else}} if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { return false, err @@ -1180,6 +1535,8 @@ func (p *{{ $.TAffine }}) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, return isInfinity, nil } +{{- end }} + {{end}} diff --git a/internal/generator/ecc/template/multiexp.go.tmpl b/internal/generator/ecc/template/multiexp.go.tmpl index 0abf44cbe4..3c0b3ef013 100644 --- a/internal/generator/ecc/template/multiexp.go.tmpl +++ b/internal/generator/ecc/template/multiexp.go.tmpl @@ -22,7 +22,9 @@ import ( {{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 16}} {{- else}} {{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 16}} +{{- if .HasG2 }} {{template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "cmax" 16}} +{{- end }} {{- end}} diff --git a/internal/generator/ecc/template/multiexp_affine.go.tmpl b/internal/generator/ecc/template/multiexp_affine.go.tmpl index 158214d89a..086cf5a435 100644 --- a/internal/generator/ecc/template/multiexp_affine.go.tmpl +++ b/internal/generator/ecc/template/multiexp_affine.go.tmpl @@ -9,13 +9,13 @@ import ( "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" - {{- if and (ne .G1.CoordType .G2.CoordType) (ne .Name "secp256k1") (ne .Name "grumpkin") }} + {{- if and .HasG2 (ne .G1.CoordType .G2.CoordType) (ne .Name "secp256k1") (ne .Name "grumpkin") }} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" {{- end}} ) {{ template "multiexp" dict "CoordType" .G1.CoordType "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange}} -{{- if and (ne .Name "secp256k1") (ne .Name "grumpkin")}} +{{- if and .HasG2 (ne .Name "secp256k1") (ne .Name "grumpkin")}} {{ template "multiexp" dict "CoordType" .G2.CoordType "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange}} {{- end}} diff --git a/internal/generator/ecc/template/multiexp_jacobian.go.tmpl b/internal/generator/ecc/template/multiexp_jacobian.go.tmpl index 8fed76b970..d4e2ef4306 100644 --- a/internal/generator/ecc/template/multiexp_jacobian.go.tmpl +++ b/internal/generator/ecc/template/multiexp_jacobian.go.tmpl @@ -9,7 +9,7 @@ {{ template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange }} -{{- if and (ne .Name "secp256k1") (ne .Name "grumpkin")}} +{{- if and .HasG2 (ne .Name "secp256k1") (ne .Name "grumpkin")}} {{ template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange }} {{- end}} diff --git a/internal/generator/ecc/template/point.go.tmpl b/internal/generator/ecc/template/point.go.tmpl index d781dd23c1..398720170e 100644 --- a/internal/generator/ecc/template/point.go.tmpl +++ b/internal/generator/ecc/template/point.go.tmpl @@ -17,7 +17,7 @@ import ( {{- end}} "github.com/consensys/gnark-crypto/parallel" "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" - {{- if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") }} + {{- if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") (eq .CoordType "fptower.E8") }} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" {{else}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" @@ -240,9 +240,13 @@ func (p *{{ $TAffine }}) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right {{.CoordType}} + var left, right, tmp {{.CoordType}} left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + tmp.Mul(&p.X, &aCurveCoeff) + right.Add(&right, &tmp) + {{- end }} {{- if eq .PointName "g1"}} right.Add(&right, &bCurveCoeff) {{- else}} @@ -495,6 +499,25 @@ func (p *{{ $TJacobian }}) SubAssign(q *{{ $TJacobian }}) *{{ $TJacobian }} { // https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-mdbl-2007-bl // ~Cost: 1M + 5S func (p *{{ $TJacobian }}) DoubleMixed(a *{{ $TAffine }}) *{{ $TJacobian }} { + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + if a.IsInfinity() || a.Y.IsZero() { + return p.Set(&g1Infinity) + } + var n, d, x, y, tmp {{.CoordType}} + n.Square(&a.X) + tmp.Set(&n) + n.Double(&n).Add(&n, &tmp).Add(&n, &aCurveCoeff) + d.Double(&a.Y).Inverse(&d) + n.Mul(&n, &d) + x.Square(&n) + tmp.Double(&a.X) + x.Sub(&x, &tmp) + y.Sub(&a.X, &x).Mul(&y, &n).Sub(&y, &a.Y) + p.X.Set(&x) + p.Y.Set(&y) + p.Z.SetOne() + return p + {{- end }} var XX, YY, YYYY, S, M, T {{.CoordType}} XX.Square(&a.X) YY.Square(&a.Y) @@ -587,6 +610,14 @@ func (p *{{ $TJacobian }}) Double(q *{{ $TJacobian }}) *{{ $TJacobian }} { // https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l // ~Cost: 2M + 5S func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + if p.Z.IsZero() || p.Y.IsZero() { + return p.Set(&g1Infinity) + } + var a {{ $TAffine }} + a.FromJacobian(p) + return p.DoubleMixed(&a) + {{- end }} var A, B, C, D, E, F, t {{.CoordType}} A.Square(&p.X) B.Square(&p.Y) @@ -616,6 +647,12 @@ func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { // // https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 func (p *{{ $TJacobian }}) Triple(q *{{ $TJacobian }}) *{{ $TJacobian }} { + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + var dbl {{ $TJacobian }} + dbl.Double(q) + p.Set(&dbl).AddAssign(q) + return p + {{- end }} // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *{{.CoordType}}) { {{- if eq .CoordType "fp.Element"}} @@ -754,6 +791,12 @@ func (p *{{ $TJacobian }}) IsOnCurve() bool { left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) ZZ.Square(&p.Z) + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + var Z4 {{.CoordType}} + Z4.Square(&ZZ) + tmp.Mul(&p.X, &Z4).Mul(&tmp, &aCurveCoeff) + right.Add(&right, &tmp) + {{- end }} tmp.Square(&ZZ).Mul(&tmp, &ZZ) {{- if eq .PointName "g1"}} {{- if or (eq .Name "bls12-381") (eq .Name "bls24-317") (eq .Name "bw6-633")}} @@ -1257,7 +1300,7 @@ func (p *{{ $TJacobian }}) mulBySeed(q *{{ $TJacobian }}) *{{ $TJacobian }} { // where w is a third root of unity. func (p *{{ $TJacobian }}) phi(q *{{ $TJacobian }}) *{{ $TJacobian }} { p.Set(q) - {{- if or (eq .CoordType "fptower.E2" ) (eq .CoordType "fptower.E4" )}} + {{- if or (eq .CoordType "fptower.E2" ) (eq .CoordType "fptower.E4" ) (eq .CoordType "fptower.E8" )}} p.X.MulByElement(&p.X, &thirdRootOne{{toUpper .PointName}}) {{- else}} p.X.Mul(&p.X, &thirdRootOne{{toUpper .PointName}}) @@ -1948,6 +1991,19 @@ func (p *{{ $TJacobianExtended }}) add(q *{{ $TJacobianExtended }}) *{{ $TJacobi // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *{{ $TJacobianExtended }}) double(q *{{ $TJacobianExtended }}) *{{ $TJacobianExtended }} { + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} + var jq, res {{ $TJacobian }} + jq.fromJacExtended(q) + res.Double(&jq) + if res.Z.IsZero() { + return p.SetInfinity() + } + p.X.Set(&res.X) + p.Y.Set(&res.Y) + p.ZZ.Square(&res.Z) + p.ZZZ.Mul(&p.ZZ, &res.Z) + return p + {{- end }} var U, V, W, S, XX, M {{.CoordType}} U.Double(&q.Y) @@ -2004,6 +2060,23 @@ func (p *{{ $TJacobianExtended }}) doubleMixed(a *{{ $TAffine }}) *{{ $TJacobian } {{define "mDouble" }} + {{- if and (eq .all.Name "kb8") (eq .all.PointName "g1") }} + var aff G1Affine + aff.Set(a) + {{- if .negate}} + aff.Neg(&aff) + {{- end}} + var res G1Jac + res.FromAffine(&aff).DoubleAssign() + if res.Z.IsZero() { + return p.SetInfinity() + } + p.X.Set(&res.X) + p.Y.Set(&res.Y) + p.ZZ.Square(&res.Z) + p.ZZZ.Mul(&p.ZZ, &res.Z) + return p + {{- end }} var U, V, W, S, M, t {{.all.CoordType}} U.Double(&a.Y) @@ -2139,7 +2212,12 @@ func (p *{{ $TProjective }}) FromAffine(a *{{ $TAffine }}) *{{ $TProjective }} { func BatchJacobianToAffine{{ toUpper .PointName }}(points []{{ $TJacobian }}) []{{ $TAffine }} { result := make([]{{ $TAffine }}, len(points)) zeroes := make([]bool, len(points)) + {{- if eq .CoordType "fp.Element" }} accumulator := fp.One() + {{- else }} + var accumulator {{ .CoordType }} + accumulator.SetOne() + {{- end }} // batch invert all points[].Z coordinates with Montgomery batch inversion trick // (stores points[].Z^-1 in result[i].X to avoid allocating a slice of fr.Elements) @@ -2152,7 +2230,7 @@ func BatchJacobianToAffine{{ toUpper .PointName }}(points []{{ $TJacobian }}) [] accumulator.Mul(&accumulator, &points[i].Z) } - var accInverse fp.Element + var accInverse {{ .CoordType }} accInverse.Inverse(&accumulator) for i := len(points) - 1; i >= 0; i-- { @@ -2171,7 +2249,7 @@ func BatchJacobianToAffine{{ toUpper .PointName }}(points []{{ $TJacobian }}) [] // do nothing, (X=0, Y=0) is infinity point in affine continue } - var a, b fp.Element + var a, b {{ .CoordType }} a = result[i].X b.Square(&a) result[i].X.Mul(&points[i].X, &b) diff --git a/internal/generator/ecc/template/tests/marshal.go.tmpl b/internal/generator/ecc/template/tests/marshal.go.tmpl index 72a8adf101..181bfbbacf 100644 --- a/internal/generator/ecc/template/tests/marshal.go.tmpl +++ b/internal/generator/ecc/template/tests/marshal.go.tmpl @@ -20,7 +20,9 @@ import ( "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" + {{- if or .HasG2 (eq .G1.CoordType "fptower.E2") (eq .G1.CoordType "fptower.E4") (eq .G1.CoordType "fptower.E8") }} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" + {{- end }} ) const ( @@ -37,9 +39,13 @@ func TestEncoder(t *testing.T) { var inC fp.Element var inD G1Affine var inE G1Affine + {{- if .HasG2 }} var inF G2Affine + {{- end }} var inG []G1Affine + {{- if .HasG2 }} var inH []G2Affine + {{- end }} var inI []fp.Element var inJ []fr.Element var inK fr.Vector @@ -53,12 +59,24 @@ func TestEncoder(t *testing.T) { inC.MustSetRandom() inD.ScalarMultiplication(&g1GenAff, new(big.Int).SetUint64(rand.Uint64())) //#nosec G404 weak rng is fine here // inE --> infinity + {{- if .HasG2 }} inF.ScalarMultiplication(&g2GenAff, new(big.Int).SetUint64(rand.Uint64())) //#nosec G404 weak rng is fine here + {{- end }} inG = make([]G1Affine, 2) + {{- if .HasG2 }} inH = make([]G2Affine, 0) + {{- end }} inG[1] = inD inI = make([]fp.Element, 3) + {{- if eq .G1.CoordType "fp.Element" }} inI[2] = inD.X + {{- else if eq .G1.CoordType "fptower.E2" }} + inI[2] = inD.X.A0 + {{- else if eq .G1.CoordType "fptower.E4" }} + inI[2] = inD.X.B0.A0 + {{- else if eq .G1.CoordType "fptower.E8" }} + inI[2] = inD.X.C0.B0.A0 + {{- end }} inJ = make([]fr.Element, 0) inK = make(fr.Vector, 42) inK[41].SetUint64(42) @@ -78,7 +96,11 @@ func TestEncoder(t *testing.T) { var buf, bufRaw bytes.Buffer enc := NewEncoder(&buf) encRaw := NewEncoder(&bufRaw, RawEncoding()) + {{- if .HasG2 }} toEncode := []any{inA, &inB, &inC, &inD, &inE, &inF, inG, inH, inI, inJ, inK, inL, inM, inN} + {{- else }} + toEncode := []any{inA, &inB, &inC, &inD, &inE, inG, inI, inJ, inK, inL, inM, inN} + {{- end }} for _, v := range toEncode { if err := enc.Encode(v); err != nil { t.Fatal(err) @@ -97,10 +119,18 @@ func TestEncoder(t *testing.T) { var outD G1Affine var outE G1Affine outE.X.SetOne() + {{- if eq .G1.CoordType "fp.Element" }} outE.Y.SetUint64(42) + {{- else }} + outE.Y.SetOne() + {{- end }} + {{- if .HasG2 }} var outF G2Affine + {{- end }} var outG []G1Affine + {{- if .HasG2 }} var outH []G2Affine + {{- end }} var outI []fp.Element var outJ []fr.Element var outK fr.Vector @@ -108,7 +138,11 @@ func TestEncoder(t *testing.T) { var outM [][]uint64 var outN [][][]fr.Element + {{- if .HasG2 }} toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} + {{- else }} + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} + {{- end }} for _, v := range toDecode { if err := dec.Decode(v); err != nil { t.Fatal(err) @@ -126,12 +160,18 @@ func TestEncoder(t *testing.T) { if !inD.Equal(&outD) || !inE.Equal(&outE) { t.Fatal("decode(encode(G1Affine) failed") } + {{- if .HasG2 }} if !inF.Equal(&outF) { t.Fatal("decode(encode(G2Affine) failed") } if (len(inG) != len(outG)) || (len(inH) != len(outH)) { t.Fatal("decode(encode(slice(points))) failed") } + {{- else }} + if len(inG) != len(outG) { + t.Fatal("decode(encode(slice(points))) failed") + } + {{- end }} for i := range len(inG) { if !inG[i].Equal(&outG[i]) { t.Fatal("decode(encode(slice(points))) failed") @@ -174,10 +214,14 @@ func TestEncoder(t *testing.T) { func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine + {{- if .HasG2 }} var g2Inf, g2 G2Affine g1 = g1GenAff g2 = g2GenAff + {{- else }} + g1 = g1GenAff + {{- end }} { b := g1Inf.Bytes() @@ -206,9 +250,7 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - - - + {{- if .HasG2 }} { b := g2Inf.Bytes() if !isCompressed(b[0]) { @@ -236,14 +278,17 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g2.RawBytes() should be uncompressed") } } + {{- end }} } -{{- $sizeOfFp := mul .Fp.NbWords 8}} +{{- $sizeOfFp := .Fp.NbBytes}} {{- $FpUnusedBits := .FpUnusedBits}} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G1.CoordType "PointName" .G1.PointName "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "FpUnusedBits" $FpUnusedBits}} +{{- if .HasG2 }} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G2.CoordType "PointName" .G2.PointName "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "FpUnusedBits" $FpUnusedBits}} +{{- end }} {{define "marshalpoint"}} @@ -430,17 +475,28 @@ func GenFp() gopter.Gen { }) } - {{if or (eq .Name "bls24-315") (eq .Name "bls24-317")}} - // GenE4 generates an fptower.E4 elmt - func GenE4() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E4 { - return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} - }) - } + // GenE4 generates an fptower.E4 elmt + func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) + } + + {{if eq .Name "kb8"}} + // GenE8 generates an fptower.E8 elmt + func GenE8() gopter.Gen { + return gopter.CombineGens( + GenE4(), + GenE4(), + ).Map(func(values []any) fptower.E8 { + return fptower.E8{C0: values[0].(fptower.E4), C1: values[1].(fptower.E4)} + }) + } + {{else if or (eq .Name "bls24-315") (eq .Name "bls24-317")}} // GenE12 generates an fptower.E12 elmt func GenE12() gopter.Gen { return gopter.CombineGens( diff --git a/internal/generator/ecc/template/tests/multiexp.go.tmpl b/internal/generator/ecc/template/tests/multiexp.go.tmpl index 5d2862b6a3..341e76a226 100644 --- a/internal/generator/ecc/template/tests/multiexp.go.tmpl +++ b/internal/generator/ecc/template/tests/multiexp.go.tmpl @@ -25,7 +25,9 @@ import ( {{- if and (ne .Name "secp256k1") (ne .Name "grumpkin")}} {{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 16}} +{{- if .HasG2 }} {{template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "cmax" 16}} +{{- end }} {{- else}} {{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 15}} {{- end}} diff --git a/internal/generator/ecc/template/tests/point.go.tmpl b/internal/generator/ecc/template/tests/point.go.tmpl index fec59fd80c..c8865c53ef 100644 --- a/internal/generator/ecc/template/tests/point.go.tmpl +++ b/internal/generator/ecc/template/tests/point.go.tmpl @@ -8,6 +8,8 @@ {{$fuzzer = "GenE2()"}} {{- else if eq .CoordType "fptower.E4" }} {{$fuzzer = "GenE4()"}} +{{- else if eq .CoordType "fptower.E8" }} + {{$fuzzer = "GenE8()"}} {{- end}} {{$c := 16}} @@ -22,7 +24,7 @@ import ( "math/rand/v2" crand "crypto/rand" - {{if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4")}} + {{if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") (eq .CoordType "fptower.E8")}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" {{else}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" @@ -356,8 +358,8 @@ func Test{{ toUpper .PointName }}Conversions(t *testing.T) { baseTableAff := BatchJacobianToAffineG1([]G1Jac{g1, g2}) return op1.Equal(&baseTableAff[0]) && op2.Equal(&baseTableAff[1]) }, - GenFp(), - GenFp(), + {{$fuzzer}}, + {{$fuzzer}}, )) {{- end }} @@ -579,7 +581,7 @@ func Test{{ $TAffine }}Ops(t *testing.T) { genScalar, )) - {{ if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4")}} + {{ if and (eq .PointName "g2") (or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") (eq .CoordType "fptower.E8"))}} properties.Property("[{{ toUpper .Name }}] psi should map points from E' to itself", prop.ForAll( func() bool { var a {{ $TJacobian }} diff --git a/internal/generator/field/generator_extensions.go b/internal/generator/field/generator_extensions.go index 435d82225a..9f91747704 100644 --- a/internal/generator/field/generator_extensions.go +++ b/internal/generator/field/generator_extensions.go @@ -68,6 +68,17 @@ func generateExtensions(F *config.Field, outputDir string) error { return err } + if isKoalaBear { + entriesExt8 := []bavard.Entry{ + {File: filepath.Join(outputDir, "e8.go"), Templates: []string{"e8.go.tmpl"}}, + {File: filepath.Join(outputDir, "e8_test.go"), Templates: []string{"e8_test.go.tmpl"}}, + } + + if err := g.Generate(data, "extensions", "", "extensions", entriesExt8...); err != nil { + return err + } + } + if isKoalaBear { // generate the assembly file; asmFile, err := os.Create(filepath.Join(outputDir, "e4_amd64.s")) diff --git a/internal/generator/field/template/extensions/e8.go.tmpl b/internal/generator/field/template/extensions/e8.go.tmpl new file mode 100644 index 0000000000..94a050a86f --- /dev/null +++ b/internal/generator/field/template/extensions/e8.go.tmpl @@ -0,0 +1,304 @@ +import ( + "math/big" + "math/bits" + + fr "{{ .FieldPackagePath }}" +) + +// E8 is a degree two finite field extension of E4. +type E8 struct { + C0, C1 E4 +} + +// Equal returns true if z equals x, false otherwise. +func (z *E8) Equal(x *E8) bool { + return z.C0.Equal(&x.C0) && z.C1.Equal(&x.C1) +} + +// Cmp compares (lexicographic order) z and x. +func (z *E8) Cmp(x *E8) int { + if a1 := z.C1.Cmp(&x.C1); a1 != 0 { + return a1 + } + return z.C0.Cmp(&x.C0) +} + +// LexicographicallyLargest returns true if this element is strictly lexicographically +// larger than its negation, false otherwise. +func (z *E8) LexicographicallyLargest() bool { + if z.C1.IsZero() { + return z.C0.LexicographicallyLargest() + } + return z.C1.LexicographicallyLargest() +} + +// String puts E8 in string form. +func (z *E8) String() string { + return z.C0.String() + "+(" + z.C1.String() + ")*w" +} + +// SetString sets an E8 from strings. +func (z *E8) SetString(s0, s1, s2, s3, s4, s5, s6, s7 string) *E8 { + z.C0.SetString(s0, s1, s2, s3) + z.C1.SetString(s4, s5, s6, s7) + return z +} + +// Set copies x into z and returns z. +func (z *E8) Set(x *E8) *E8 { + z.C0 = x.C0 + z.C1 = x.C1 + return z +} + +// SetZero sets an E8 elmt to zero. +func (z *E8) SetZero() *E8 { + z.C0.SetZero() + z.C1.SetZero() + return z +} + +// SetOne sets z to 1 in Montgomery form and returns z. +func (z *E8) SetOne() *E8 { + *z = E8{} + z.C0.B0.A0.SetOne() + return z +} + +// Lift sets the C0.B0.A0 component of z to v. +func (z *E8) Lift(v *fr.Element) *E8 { + *z = E8{} + z.C0.B0.A0.Set(v) + return z +} + +// MulByElement multiplies an element in E8 by an element in fr. +func (z *E8) MulByElement(x *E8, y *fr.Element) *E8 { + z.C0.MulByElement(&x.C0, y) + z.C1.MulByElement(&x.C1, y) + return z +} + +// Add sets z=x+y in E8 and returns z. +func (z *E8) Add(x, y *E8) *E8 { + z.C0.Add(&x.C0, &y.C0) + z.C1.Add(&x.C1, &y.C1) + return z +} + +// Sub sets z=x-y in E8 and returns z. +func (z *E8) Sub(x, y *E8) *E8 { + z.C0.Sub(&x.C0, &y.C0) + z.C1.Sub(&x.C1, &y.C1) + return z +} + +// Double sets z=2*x and returns z. +func (z *E8) Double(x *E8) *E8 { + z.C0.Double(&x.C0) + z.C1.Double(&x.C1) + return z +} + +// Neg negates an E8 element. +func (z *E8) Neg(x *E8) *E8 { + z.C0.Neg(&x.C0) + z.C1.Neg(&x.C1) + return z +} + +// SetRandom used only in tests. +func (z *E8) SetRandom() (*E8, error) { + if _, err := z.C0.SetRandom(); err != nil { + return nil, err + } + if _, err := z.C1.SetRandom(); err != nil { + return nil, err + } + return z, nil +} + +// MustSetRandom sets the element to a random value. +func (z *E8) MustSetRandom() *E8 { + if _, err := z.SetRandom(); err != nil { + panic(err) + } + return z +} + +// IsZero returns true if z is zero, false otherwise. +func (z *E8) IsZero() bool { + return z.C0.IsZero() && z.C1.IsZero() +} + +// IsOne returns true if z is one, false otherwise. +func (z *E8) IsOne() bool { + return z.C0.IsOne() && z.C1.IsZero() +} + +// Mul sets z=x*y in E8 and returns z. +func (z *E8) Mul(x, y *E8) *E8 { + var a, b, c, d E4 + a.Mul(&x.C0, &y.C0) + b.Mul(&x.C1, &y.C1) + c.Add(&x.C0, &x.C1) + d.Add(&y.C0, &y.C1) + c.Mul(&c, &d). + Sub(&c, &a). + Sub(&c, &b) + z.C1.Set(&c) + b.MulByNonResidue(&b) + z.C0.Add(&a, &b) + return z +} + +// Square sets z=x*x in E8 and returns z. +func (z *E8) Square(x *E8) *E8 { + return z.Mul(x, x) +} + +// Inverse sets z to the inverse of x in E8 and returns z. +func (z *E8) Inverse(x *E8) *E8 { + var t0, t1 E4 + t0.Square(&x.C0) + t1.Square(&x.C1).MulByNonResidue(&t1) + t0.Sub(&t0, &t1).Inverse(&t0) + z.C0.Mul(&x.C0, &t0) + z.C1.Mul(&x.C1, &t0).Neg(&z.C1) + return z +} + +// Exp sets z=x^k and returns it. +func (z *E8) Exp(x E8, k *big.Int) *E8 { + if k.IsInt64() { + return z.ExpInt64(x, k.Int64()) + } + + e := k + if k.Sign() == -1 { + x.Inverse(&x) + e = bigIntPool.Get().(*big.Int) + defer bigIntPool.Put(e) + e.Neg(k) + } + + z.SetOne() + b := e.Bytes() + for i := range len(b) { + w := b[i] + for j := range 8 { + z.Square(z) + if (w & (0b10000000 >> j)) != 0 { + z.Mul(z, &x) + } + } + } + + return z +} + +// ExpInt64 sets z=x^k and returns it, where k is an int64. +func (z *E8) ExpInt64(x E8, k int64) *E8 { + if k == 0 { + return z.SetOne() + } + + exp := k + if k < 0 { + x.Inverse(&x) + exp = -k + } + + z.Set(&x) + for i := bits.Len64(uint64(exp)) - 2; i >= 0; i-- { + z.Square(z) + if (uint64(exp)>>uint(i))&1 != 0 { + z.Mul(z, &x) + } + } + + return z +} + +// Conjugate sets z to x conjugated and returns z. +func (z *E8) Conjugate(x *E8) *E8 { + z.C0 = x.C0 + z.C1.Neg(&x.C1) + return z +} + +// Halve sets z to z / 2. +func (z *E8) Halve() { + z.C0.Halve() + z.C1.Halve() +} + +// norm sets x to the norm of z. +func (z *E8) norm(x *E4) { + var tmp E4 + tmp.Square(&z.C1).MulByNonResidue(&tmp) + x.Square(&z.C0).Sub(x, &tmp) +} + +// Legendre returns the Legendre symbol of z. +func (z *E8) Legendre() int { + var n E4 + z.norm(&n) + return n.Legendre() +} + +// Sqrt sets z to the square root of x and returns z. +func (z *E8) Sqrt(x *E8) *E8 { + var x0, x1 E4 + x.norm(&x0) + x0.Sqrt(&x0) + x1.Add(&x.C0, &x0).Halve() + if x1.Legendre() != 1 { + x1.Sub(&x.C0, &x0).Halve() + } + x1.Sqrt(&x1) + z.C0.Set(&x1) + x1.Double(&x1) + z.C1.Div(&x.C1, &x1) + return z +} + +// BatchInvertE8 returns a new slice with every element in a inverted. +func BatchInvertE8(a []E8) []E8 { + res := make([]E8, len(a)) + if len(a) == 0 { + return res + } + + zeroes := make([]bool, len(a)) + var accumulator E8 + accumulator.SetOne() + + for i := range len(a) { + if a[i].IsZero() { + zeroes[i] = true + continue + } + res[i].Set(&accumulator) + accumulator.Mul(&accumulator, &a[i]) + } + + accumulator.Inverse(&accumulator) + + for i := len(a) - 1; i >= 0; i-- { + if zeroes[i] { + continue + } + res[i].Mul(&res[i], &accumulator) + accumulator.Mul(&accumulator, &a[i]) + } + + return res +} + +// Div divides an element in E8 by an element in E8. +func (z *E8) Div(x *E8, y *E8) *E8 { + var r E8 + r.Inverse(y).Mul(x, &r) + return z.Set(&r) +} diff --git a/internal/generator/field/template/extensions/e8_test.go.tmpl b/internal/generator/field/template/extensions/e8_test.go.tmpl new file mode 100644 index 0000000000..573f38e080 --- /dev/null +++ b/internal/generator/field/template/extensions/e8_test.go.tmpl @@ -0,0 +1,168 @@ +import ( + "math/big" + "testing" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/prop" +) + +func TestE8ReceiverIsOperand(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + + properties := gopter.NewProperties(parameters) + genA := genE8() + genB := genE8() + + properties.Property("[{{.FF}}] receiver as operand should be stable", prop.ForAll( + func(a, b E8) bool { + var c, d, e, f E8 + d.Set(&a) + c.Add(&a, &b) + a.Add(&a, &b) + b.Add(&d, &b) + e.Sub(&c, &d) + f.Sub(&b, &d) + return a.Equal(&b) && a.Equal(&c) && e.Equal(&f) + }, + genA, + genB, + )) + + properties.Property("[{{.FF}}] mul receiver aliasing should be stable", prop.ForAll( + func(a, b E8) bool { + var c, d E8 + d.Set(&a) + c.Mul(&a, &b) + a.Mul(&a, &b) + b.Mul(&d, &b) + return a.Equal(&b) && a.Equal(&c) + }, + genA, + genB, + )) + + properties.Property("[{{.FF}}] square receiver aliasing should be stable", prop.ForAll( + func(a E8) bool { + var b E8 + b.Square(&a) + a.Square(&a) + return a.Equal(&b) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestE8Ops(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + + properties := gopter.NewProperties(parameters) + genA := genE8() + genB := genE8() + + properties.Property("[{{.FF}}] sub(add(a,b),b)=a", prop.ForAll( + func(a, b E8) bool { + var c E8 + c.Add(&a, &b).Sub(&c, &b) + return c.Equal(&a) + }, + genA, + genB, + )) + + properties.Property("[{{.FF}}] mul(inverse(b),b)=1", prop.ForAll( + func(a, b E8) bool { + if b.IsZero() { + return true + } + var c, d E8 + d.Inverse(&b) + c.Mul(&a, &b).Mul(&c, &d) + return c.Equal(&a) + }, + genA, + genB, + )) + + properties.Property("[{{.FF}}] square and mul match", prop.ForAll( + func(a E8) bool { + var b, c E8 + b.Mul(&a, &a) + c.Square(&a) + return b.Equal(&c) + }, + genA, + )) + + properties.Property("[{{.FF}}] square(sqrt(x^2)) = x^2", prop.ForAll( + func(a E8) bool { + var b, c E8 + b.Square(&a) + c.Sqrt(&b).Square(&c) + return c.Equal(&b) + }, + genA, + )) + + properties.Property("[{{.FF}}] batch inverse matches inverse", prop.ForAll( + func(a, b, c E8) bool { + batch := BatchInvertE8([]E8{a, b, c}) + if !a.IsZero() { + a.Inverse(&a) + } + if !b.IsZero() { + b.Inverse(&b) + } + if !c.IsZero() { + c.Inverse(&c) + } + return a.Equal(&batch[0]) && b.Equal(&batch[1]) && c.Equal(&batch[2]) + }, + genA, + genA, + genB, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func TestE8Exp(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 50 + + properties := gopter.NewProperties(parameters) + genA := genE8() + + properties.Property("[{{.FF}}] Exp(x, 0) = 1", prop.ForAll( + func(a E8) bool { + var res, one E8 + one.SetOne() + res.Exp(a, big.NewInt(0)) + return res.Equal(&one) + }, + genA, + )) + + properties.Property("[{{.FF}}] Exp(x, 1) = x", prop.ForAll( + func(a E8) bool { + var res E8 + res.Exp(a, big.NewInt(1)) + return res.Equal(&a) + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} + +func genE8() gopter.Gen { + return gopter.CombineGens( + genE4(), + genE4(), + ).Map(func(values []any) E8 { + return E8{C0: values[0].(E4), C1: values[1].(E4)} + }) +} diff --git a/internal/generator/main.go b/internal/generator/main.go index 4a2bcc6d8e..447c653d93 100644 --- a/internal/generator/main.go +++ b/internal/generator/main.go @@ -126,7 +126,7 @@ func main() { } // fp - { + if conf.Name != "kb8" { outputDir := filepath.Join(curveDir, "fp") relAsmDir, err := filepath.Rel(outputDir, asmDirBuildPath) assertNoError(err) @@ -144,7 +144,7 @@ func main() { asmConfig := &fieldConfig.Assembly{BuildDir: asmDirBuildPath, IncludeDir: relAsmDir} frOpts := []field.Option{field.WithASM(asmConfig)} - if !(conf.Equal(config.SECP256R1) || conf.Equal(config.STARK_CURVE) || conf.Equal(config.SECP256K1) || conf.Equal(config.GRUMPKIN)) { // nolint QF1001 + if !(conf.Equal(config.SECP256R1) || conf.Equal(config.STARK_CURVE) || conf.Equal(config.SECP256K1) || conf.Equal(config.GRUMPKIN) || conf.Name == "kb8") { // nolint QF1001 frOpts = append(frOpts, field.WithFFT(fftConfig), field.WithIOP()) } if conf.Equal(config.BLS12_377) { @@ -155,7 +155,9 @@ func main() { } // generate ecdsa - assertNoError(ecdsa.Generate(conf, curveDir, gen)) + if conf.G1.CoordExtDegree == 1 { + assertNoError(ecdsa.Generate(conf, curveDir, gen)) + } if conf.Equal(config.STARK_CURVE) || conf.Equal(config.SECP256R1) { return // TODO @yelhousni @@ -164,6 +166,10 @@ func main() { // generate G1, G2, multiExp, ... assertNoError(ecc.Generate(conf, curveDir, gen)) + if conf.Name == "kb8" { + return + } + if conf.Equal(config.SECP256K1) { return } From 34d758ce3e5fcc61085993ffadf694c5d2f7351b Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 14 Apr 2026 16:28:08 -0400 Subject: [PATCH 02/33] feat: add multiset hash on kb8 --- ecc/kb8/g1.go | 15 +- ecc/kb8/multiset-hash/doc.go | 7 + ecc/kb8/multiset-hash/multiset_hash.go | 141 +++++++++++++++ ecc/kb8/multiset-hash/multiset_hash_test.go | 163 ++++++++++++++++++ internal/generator/ecc/template/point.go.tmpl | 2 +- 5 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 ecc/kb8/multiset-hash/doc.go create mode 100644 ecc/kb8/multiset-hash/multiset_hash.go create mode 100644 ecc/kb8/multiset-hash/multiset_hash_test.go diff --git a/ecc/kb8/g1.go b/ecc/kb8/g1.go index 7cf9e832ee..47daeadab1 100644 --- a/ecc/kb8/g1.go +++ b/ecc/kb8/g1.go @@ -645,20 +645,11 @@ func (p *G1Jac) IsOnCurve() bool { } // IsInSubGroup returns true if p is on the r-torsion, false otherwise. -// Z[r,0]+Z[-lambdaG1Affine, 1] is the kernel -// of (u,v)->u+lambdaG1Affinev mod r. Expressing r, lambdaG1Affine as -// polynomials in x, a short vector of this Zmodule is -// 1, x². So we check that p+x²ϕ(p) -// is the infinity. +// the curve is of prime order i.e. E(𝔽p) is the full group +// so we just check that the point is on the curve. func (p *G1Jac) IsInSubGroup() bool { - if !p.IsOnCurve() { - return false - } - var res G1Jac - - res.mulWindowed(p, fr.Modulus()) - return res.Z.IsZero() + return p.IsOnCurve() } diff --git a/ecc/kb8/multiset-hash/doc.go b/ecc/kb8/multiset-hash/doc.go new file mode 100644 index 0000000000..66fef82430 --- /dev/null +++ b/ecc/kb8/multiset-hash/doc.go @@ -0,0 +1,7 @@ +// Package multisethash implements a multiset hash over kb8. +// +// Messages are elements of the shared KoalaBear octic extension field. Each +// message is deterministically lifted to a point on kb8, and multiset hashing is +// the group sum of those lifted points. This matches the additive multiset-hash +// pattern used in zkVM memory arguments. +package multisethash diff --git a/ecc/kb8/multiset-hash/multiset_hash.go b/ecc/kb8/multiset-hash/multiset_hash.go new file mode 100644 index 0000000000..233013000c --- /dev/null +++ b/ecc/kb8/multiset-hash/multiset_hash.go @@ -0,0 +1,141 @@ +package multisethash + +import ( + "errors" + + "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" +) + +var ( + errMapFailure = errors.New("kb8 multiset hash: failed to map message after 256 offsets") + mapPerm = poseidon2.NewPermutation(16, 6, 21) +) + +// Accumulator stores an additive multiset hash state in affine coordinates. +// Updates are carried out in Jacobian coordinates and normalized back to affine. +type Accumulator struct { + sum kb8.G1Affine +} + +// NewAccumulator returns a zero accumulator. +func NewAccumulator() Accumulator { + var a Accumulator + a.sum.SetInfinity() + return a +} + +// Insert maps msg to kb8 and adds it to the accumulator. +func (a *Accumulator) Insert(msg *extensions.E8) error { + p, _, err := Map(msg) + if err != nil { + return err + } + var sumJac kb8.G1Jac + sumJac.FromAffine(&a.sum).AddMixed(&p) + a.sum.FromJacobian(&sumJac) + return nil +} + +// Remove maps msg to kb8 and subtracts it from the accumulator. +func (a *Accumulator) Remove(msg *extensions.E8) error { + p, _, err := Map(msg) + if err != nil { + return err + } + p.Neg(&p) + var sumJac kb8.G1Jac + sumJac.FromAffine(&a.sum).AddMixed(&p) + a.sum.FromJacobian(&sumJac) + return nil +} + +// Digest returns the current accumulator state in affine coordinates. +func (a *Accumulator) Digest() kb8.G1Affine { + return a.sum +} + +// Reset clears the accumulator. +func (a *Accumulator) Reset() { + a.sum.SetInfinity() +} + +// Hash returns the multiset hash of msgs. +func Hash(msgs []extensions.E8) (kb8.G1Affine, error) { + acc := NewAccumulator() + for i := range msgs { + if err := acc.Insert(&msgs[i]); err != nil { + return kb8.G1Affine{}, err + } + } + return acc.Digest(), nil +} + +// Map deterministically maps msg to a point on kb8 using KoalaBear Poseidon2. +// It returns the mapped point and the offset in [0, 255] that produced it. +func Map(msg *extensions.E8) (kb8.G1Affine, uint8, error) { + var state [16]koalabear.Element + messageToState(msg, state[:8]) + + a, b := kb8.CurveCoefficients() + for offset := uint16(0); offset < 256; offset++ { + state[8].SetUint64(uint64(offset)) + clearStateSuffix(state[9:]) + + if err := mapPerm.Permutation(state[:]); err != nil { + return kb8.G1Affine{}, 0, err + } + + x := stateToE8(state[:8]) + var rhs, y, tmp extensions.E8 + rhs.Square(&x).Mul(&rhs, &x) + tmp.Mul(&x, &a) + rhs.Add(&rhs, &tmp).Add(&rhs, &b) + if rhs.Legendre() != 1 { + continue + } + y.Sqrt(&rhs) + if y.LexicographicallyLargest() { + y.Neg(&y) + } + + p := kb8.G1Affine{X: x, Y: y} + if p.IsOnCurve() && p.IsInSubGroup() { + return p, uint8(offset), nil + } + } + + return kb8.G1Affine{}, 0, errMapFailure +} + +func clearStateSuffix(s []koalabear.Element) { + for i := range s { + s[i].SetZero() + } +} + +func messageToState(msg *extensions.E8, out []koalabear.Element) { + out[0] = msg.C0.B0.A0 + out[1] = msg.C0.B0.A1 + out[2] = msg.C0.B1.A0 + out[3] = msg.C0.B1.A1 + out[4] = msg.C1.B0.A0 + out[5] = msg.C1.B0.A1 + out[6] = msg.C1.B1.A0 + out[7] = msg.C1.B1.A1 +} + +func stateToE8(in []koalabear.Element) extensions.E8 { + var x extensions.E8 + x.C0.B0.A0 = in[0] + x.C0.B0.A1 = in[1] + x.C0.B1.A0 = in[2] + x.C0.B1.A1 = in[3] + x.C1.B0.A0 = in[4] + x.C1.B0.A1 = in[5] + x.C1.B1.A0 = in[6] + x.C1.B1.A1 = in[7] + return x +} diff --git a/ecc/kb8/multiset-hash/multiset_hash_test.go b/ecc/kb8/multiset-hash/multiset_hash_test.go new file mode 100644 index 0000000000..9fdca84650 --- /dev/null +++ b/ecc/kb8/multiset-hash/multiset_hash_test.go @@ -0,0 +1,163 @@ +package multisethash + +import ( + "testing" + + "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/stretchr/testify/require" +) + +func randomMessages(t *testing.T, n int) []extensions.E8 { + t.Helper() + return mustRandomMessages(n) +} + +func mustRandomMessages(n int) []extensions.E8 { + res := make([]extensions.E8, n) + for i := range res { + res[i].MustSetRandom() + } + return res +} + +func TestMapDeterministic(t *testing.T) { + msgs := randomMessages(t, 16) + for i := range msgs { + p1, o1, err := Map(&msgs[i]) + require.NoError(t, err) + p2, o2, err := Map(&msgs[i]) + require.NoError(t, err) + require.Equal(t, o1, o2) + require.True(t, p1.Equal(&p2)) + require.True(t, p1.IsOnCurve()) + require.True(t, p1.IsInSubGroup()) + } +} + +func TestHashPermutationInvariant(t *testing.T) { + msgs := randomMessages(t, 8) + got1, err := Hash(msgs) + require.NoError(t, err) + + permuted := append([]extensions.E8(nil), msgs...) + permuted[0], permuted[5] = permuted[5], permuted[0] + permuted[1], permuted[7] = permuted[7], permuted[1] + + got2, err := Hash(permuted) + require.NoError(t, err) + require.True(t, got1.Equal(&got2)) +} + +func TestAddRemove(t *testing.T) { + msgs := randomMessages(t, 6) + acc := NewAccumulator() + for i := range msgs { + require.NoError(t, acc.Insert(&msgs[i])) + } + withAll := acc.Digest() + require.True(t, withAll.IsOnCurve()) + + require.NoError(t, acc.Remove(&msgs[2])) + require.NoError(t, acc.Remove(&msgs[4])) + + acc2 := NewAccumulator() + require.NoError(t, acc2.Insert(&msgs[0])) + require.NoError(t, acc2.Insert(&msgs[1])) + require.NoError(t, acc2.Insert(&msgs[3])) + require.NoError(t, acc2.Insert(&msgs[5])) + + d1 := acc.Digest() + d2 := acc2.Digest() + require.True(t, d1.Equal(&d2)) +} + +func TestHashMatchesAccumulator(t *testing.T) { + msgs := randomMessages(t, 10) + got, err := Hash(msgs) + require.NoError(t, err) + + acc := NewAccumulator() + for i := range msgs { + require.NoError(t, acc.Insert(&msgs[i])) + } + digest := acc.Digest() + require.True(t, got.Equal(&digest)) +} + +func TestDuplicatesMatter(t *testing.T) { + msgs := randomMessages(t, 1) + single, err := Hash(msgs) + require.NoError(t, err) + + double, err := Hash([]extensions.E8{msgs[0], msgs[0]}) + require.NoError(t, err) + + require.False(t, single.Equal(&double)) +} + +func BenchmarkMap(b *testing.B) { + var msg extensions.E8 + msg.MustSetRandom() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := Map(&msg) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkAccumulatorInsert(b *testing.B) { + msgs := mustRandomMessages(256) + b.ResetTimer() + for i := 0; i < b.N; i++ { + acc := NewAccumulator() + for j := range msgs { + if err := acc.Insert(&msgs[j]); err != nil { + b.Fatal(err) + } + } + _ = acc.Digest() + } +} + +func BenchmarkHash256(b *testing.B) { + msgs := mustRandomMessages(256) + b.ResetTimer() + for i := 0; i < b.N; i++ { + got, err := Hash(msgs) + if err != nil { + b.Fatal(err) + } + if !got.IsOnCurve() { + b.Fatal("invalid digest") + } + } +} + +func TestEmptyHashIsInfinity(t *testing.T) { + got, err := Hash(nil) + require.NoError(t, err) + require.True(t, got.IsInfinity()) +} + +func TestMappedPointNotInfinity(t *testing.T) { + msgs := randomMessages(t, 16) + for i := range msgs { + p, _, err := Map(&msgs[i]) + require.NoError(t, err) + require.False(t, p.IsInfinity()) + } +} + +func TestDigestRoundTrip(t *testing.T) { + msgs := randomMessages(t, 4) + got, err := Hash(msgs) + require.NoError(t, err) + buf := got.Bytes() + var dec kb8.G1Affine + _, err = dec.SetBytes(buf[:]) + require.NoError(t, err) + require.True(t, dec.Equal(&got)) +} diff --git a/internal/generator/ecc/template/point.go.tmpl b/internal/generator/ecc/template/point.go.tmpl index 398720170e..52389e599c 100644 --- a/internal/generator/ecc/template/point.go.tmpl +++ b/internal/generator/ecc/template/point.go.tmpl @@ -832,7 +832,7 @@ func (p *{{ $TJacobian }}) IsOnCurve() bool { -{{- if or (eq .Name "bn254") (eq .Name "secp256k1") (eq .Name "grumpkin")}} +{{- if or (eq .Name "bn254") (eq .Name "secp256k1") (eq .Name "grumpkin") (eq .Name "kb8")}} {{- if eq .PointName "g1"}} // IsInSubGroup returns true if p is on the r-torsion, false otherwise. // the curve is of prime order i.e. E(𝔽p) is the full group From 0f25786f01b7dfc5be9ad7844e00f95738b29046 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 14 Apr 2026 16:55:58 -0400 Subject: [PATCH 03/33] feat: add y-increment multiset hash --- ecc/kb8/multiset-hash/cardano.go | 357 ++++++++++++++++++++ ecc/kb8/multiset-hash/doc.go | 10 +- ecc/kb8/multiset-hash/multiset_hash.go | 96 ++---- ecc/kb8/multiset-hash/multiset_hash_test.go | 131 +++---- 4 files changed, 455 insertions(+), 139 deletions(-) create mode 100644 ecc/kb8/multiset-hash/cardano.go diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go new file mode 100644 index 0000000000..96655b22fd --- /dev/null +++ b/ecc/kb8/multiset-hash/cardano.go @@ -0,0 +1,357 @@ +package multisethash + +import ( + "math/big" + "slices" + + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" +) + +// Cardano solver for the depressed cubic x^3 - 3x + c = 0 over kb8 Fp^8. +// This mirrors the structure of the secp256r1 Cardano solver in PR #831: +// repeated-root case, square-discriminant case over the base field, and a +// quadratic-extension fallback when the discriminant is not a square. + +var ( + e8CbrtExponent big.Int + e16CbrtExponent big.Int + e8Omega extensions.E8 + e8Beta extensions.E8 + e8One extensions.E8 + e8Two extensions.E8 + e8Three extensions.E8 + e8Four extensions.E8 + e8TwentySeven extensions.E8 + e8NegThree extensions.E8 +) + +func init() { + var q8, q16 big.Int + q8.Exp(koalabear.Modulus(), big.NewInt(8), nil) + q16.Mul(&q8, &q8) + + // q8 ≡ 4 (mod 9), so cubic residues admit x^((2q8+1)/9) as a cube root. + e8CbrtExponent.Mul(&q8, big.NewInt(2)) + e8CbrtExponent.Add(&e8CbrtExponent, big.NewInt(1)) + e8CbrtExponent.Div(&e8CbrtExponent, big.NewInt(9)) + + // q16 ≡ 7 (mod 9), so cubic residues admit x^((q16+2)/9) as a cube root. + e16CbrtExponent.Add(&q16, big.NewInt(2)) + e16CbrtExponent.Div(&e16CbrtExponent, big.NewInt(9)) + + e8One.SetOne() + e8Two.C0.B0.A0.SetUint64(2) + e8Three.C0.B0.A0.SetUint64(3) + e8Four.C0.B0.A0.SetUint64(4) + e8TwentySeven.C0.B0.A0.SetUint64(27) + e8NegThree.Set(&e8Three).Neg(&e8NegThree) + + e8Beta = findNonSquare() + e8Omega = findPrimitiveCubeRoot() +} + +func depressedCubicRoot(c extensions.E8) (extensions.E8, bool) { + roots := cardanoRoots(c) + if len(roots) == 0 { + return extensions.E8{}, false + } + slices.SortFunc(roots, func(a, b extensions.E8) int { + return a.Cmp(&b) + }) + return roots[0], true +} + +func cardanoRoots(c extensions.E8) []extensions.E8 { + var a3, neg4a3, k27c2, delta extensions.E8 + a3.Square(&e8NegThree).Mul(&a3, &e8NegThree) + neg4a3.Mul(&a3, &e8Four).Neg(&neg4a3) + k27c2.Square(&c).Mul(&k27c2, &e8TwentySeven) + delta.Sub(&neg4a3, &k27c2) + + var inv2, inv4, inv27 extensions.E8 + inv2.Inverse(&e8Two) + inv4.Inverse(&e8Four) + inv27.Inverse(&e8TwentySeven) + + var discD, a3Over27 extensions.E8 + discD.Square(&c).Mul(&discD, &inv4) + a3Over27.Mul(&a3, &inv27) + discD.Add(&discD, &a3Over27) + + var negCHalf extensions.E8 + negCHalf.Mul(&c, &inv2).Neg(&negCHalf) + + if delta.IsZero() { + return repeatedRoots(c) + } + + if delta.Legendre() == -1 { + return cardanoRootsViaQuadraticExtension(negCHalf, discD) + } + + return cardanoRootsBaseField(negCHalf, discD) +} + +func repeatedRoots(c extensions.E8) []extensions.E8 { + var invA, r0, r1, twoA extensions.E8 + invA.Inverse(&e8NegThree) + r0.Mul(&c, &invA).Mul(&r0, &e8Three) + twoA.Double(&e8NegThree) + r1.Inverse(&twoA).Mul(&r1, &c).Mul(&r1, &e8Three).Neg(&r1) + return dedupRoots([]extensions.E8{r0, r1}) +} + +func cardanoRootsBaseField(negCHalf, discD extensions.E8) []extensions.E8 { + var d, w extensions.E8 + d.Sqrt(&discD) + w.Add(&negCHalf, &d) + if w.IsZero() { + w.Sub(&negCHalf, &d) + } + + var u extensions.E8 + if cbrtE8(&u, &w) == nil { + return nil + } + + var omega2 extensions.E8 + omega2.Square(&e8Omega) + + var invU, r0, r1, r2, t1, t2 extensions.E8 + invU.Inverse(&u) + r0.Add(&u, &invU) + t1.Mul(&e8Omega, &u) + t2.Mul(&omega2, &invU) + r1.Add(&t1, &t2) + t1.Mul(&omega2, &u) + t2.Mul(&e8Omega, &invU) + r2.Add(&t1, &t2) + + return filterValidRoots(negCHalf, []extensions.E8{r0, r1, r2}) +} + +func cardanoRootsViaQuadraticExtension(negCHalf, discD extensions.E8) []extensions.E8 { + var discOverBeta, sqrtDiscOverBeta extensions.E8 + discOverBeta.Div(&discD, &e8Beta) + if discOverBeta.Legendre() != 1 { + return nil + } + sqrtDiscOverBeta.Sqrt(&discOverBeta) + + w := e16{ + A0: negCHalf, + A1: sqrtDiscOverBeta, + } + + var u e16 + if u.Cbrt(&w) == nil { + return nil + } + + var omega2 extensions.E8 + omega2.Square(&e8Omega) + zetas := [3]extensions.E8{e8One, e8Omega, omega2} + + for _, zeta := range zetas { + var cand, inv, sum e16 + cand.MulByE8(&u, &zeta) + inv.Inverse(&cand) + sum.Add(&cand, &inv) + if sum.A1.IsZero() && isDepressedCubicRoot(&sum.A0, &negCHalf) { + return []extensions.E8{sum.A0} + } + } + + return nil +} + +func filterValidRoots(negCHalf extensions.E8, roots []extensions.E8) []extensions.E8 { + res := make([]extensions.E8, 0, len(roots)) + for _, root := range roots { + if isDepressedCubicRoot(&root, &negCHalf) { + res = append(res, root) + } + } + return dedupRoots(res) +} + +func dedupRoots(roots []extensions.E8) []extensions.E8 { + if len(roots) == 0 { + return nil + } + slices.SortFunc(roots, func(a, b extensions.E8) int { + return a.Cmp(&b) + }) + out := roots[:1] + for i := 1; i < len(roots); i++ { + if !roots[i].Equal(&out[len(out)-1]) { + out = append(out, roots[i]) + } + } + return out +} + +func isDepressedCubicRoot(x, negCHalf *extensions.E8) bool { + var lhs, rhs extensions.E8 + lhs.Square(x).Mul(&lhs, x) + rhs.Double(negCHalf).Neg(&rhs) + lhs.Sub(&lhs, x).Sub(&lhs, x).Sub(&lhs, x).Add(&lhs, &rhs) + return lhs.IsZero() +} + +func cbrtE8(z, x *extensions.E8) *extensions.E8 { + var y extensions.E8 + y.Exp(*x, &e8CbrtExponent) + var check extensions.E8 + check.Square(&y).Mul(&check, &y) + if !check.Equal(x) { + return nil + } + return z.Set(&y) +} + +func findPrimitiveCubeRoot() extensions.E8 { + var exp big.Int + exp.Exp(koalabear.Modulus(), big.NewInt(8), nil) + exp.Sub(&exp, big.NewInt(1)) + exp.Div(&exp, big.NewInt(3)) + + for _, candidate := range e8SearchCandidates() { + var w extensions.E8 + w.Exp(candidate, &exp) + if !w.IsOne() { + return w + } + } + panic("kb8 multiset hash: failed to find primitive cube root in Fp^8") +} + +func findNonSquare() extensions.E8 { + for _, candidate := range e8SearchCandidates() { + if !candidate.IsZero() && candidate.Legendre() == -1 { + return candidate + } + } + panic("kb8 multiset hash: failed to find quadratic non-residue in Fp^8") +} + +func e8SearchCandidates() []extensions.E8 { + const searchSpace = 6560 // 3^8 - 1 + res := make([]extensions.E8, 0, searchSpace) + for n := 1; n <= searchSpace; n++ { + res = append(res, ternaryCandidate(n)) + } + return res +} + +func ternaryCandidate(n int) extensions.E8 { + var x extensions.E8 + for i := 0; i < 8; i++ { + v := uint64(n % 3) + n /= 3 + if v != 0 { + setE8Coeff(&x, i, v) + } + } + return x +} + +func setE8Coeff(x *extensions.E8, idx int, v uint64) { + switch idx { + case 0: + x.C0.B0.A0.SetUint64(v) + case 1: + x.C0.B0.A1.SetUint64(v) + case 2: + x.C0.B1.A0.SetUint64(v) + case 3: + x.C0.B1.A1.SetUint64(v) + case 4: + x.C1.B0.A0.SetUint64(v) + case 5: + x.C1.B0.A1.SetUint64(v) + case 6: + x.C1.B1.A0.SetUint64(v) + case 7: + x.C1.B1.A1.SetUint64(v) + default: + panic("invalid E8 coefficient index") + } +} + +type e16 struct { + A0, A1 extensions.E8 +} + +func (z *e16) Set(x *e16) *e16 { + z.A0.Set(&x.A0) + z.A1.Set(&x.A1) + return z +} + +func (z *e16) Add(x, y *e16) *e16 { + z.A0.Add(&x.A0, &y.A0) + z.A1.Add(&x.A1, &y.A1) + return z +} + +func (z *e16) Mul(x, y *e16) *e16 { + var a, b, c, d extensions.E8 + a.Mul(&x.A0, &y.A0) + b.Mul(&x.A1, &y.A1).Mul(&b, &e8Beta) + c.Add(&x.A0, &x.A1) + d.Add(&y.A0, &y.A1) + c.Mul(&c, &d).Sub(&c, &a) + var rawB extensions.E8 + rawB.Mul(&x.A1, &y.A1) + c.Sub(&c, &rawB) + z.A0.Add(&a, &b) + z.A1.Set(&c) + return z +} + +func (z *e16) Square(x *e16) *e16 { + return z.Mul(x, x) +} + +func (z *e16) Inverse(x *e16) *e16 { + var t0, t1, denom extensions.E8 + t0.Square(&x.A0) + t1.Square(&x.A1).Mul(&t1, &e8Beta) + denom.Sub(&t0, &t1).Inverse(&denom) + z.A0.Mul(&x.A0, &denom) + z.A1.Mul(&x.A1, &denom).Neg(&z.A1) + return z +} + +func (z *e16) MulByE8(x *e16, y *extensions.E8) *e16 { + z.A0.Mul(&x.A0, y) + z.A1.Mul(&x.A1, y) + return z +} + +func (z *e16) Exp(x e16, k *big.Int) *e16 { + z.A0.SetOne() + z.A1.SetZero() + for _, b := range k.Bytes() { + for bit := 7; bit >= 0; bit-- { + z.Square(z) + if (b>>bit)&1 == 1 { + z.Mul(z, &x) + } + } + } + return z +} + +func (z *e16) Cbrt(x *e16) *e16 { + var y e16 + y.Exp(*x, &e16CbrtExponent) + var check e16 + check.Square(&y).Mul(&check, &y) + if !check.A0.Equal(&x.A0) || !check.A1.Equal(&x.A1) { + return nil + } + return z.Set(&y) +} diff --git a/ecc/kb8/multiset-hash/doc.go b/ecc/kb8/multiset-hash/doc.go index 66fef82430..95c1c15df2 100644 --- a/ecc/kb8/multiset-hash/doc.go +++ b/ecc/kb8/multiset-hash/doc.go @@ -1,7 +1,7 @@ -// Package multisethash implements a multiset hash over kb8. +// Package multisethash implements the y-increment multiset hash over kb8. // -// Messages are elements of the shared KoalaBear octic extension field. Each -// message is deterministically lifted to a point on kb8, and multiset hashing is -// the group sum of those lifted points. This matches the additive multiset-hash -// pattern used in zkVM memory arguments. +// Messages are 16-bit values. Each message m is mapped by scanning k in +// [0, 256) and setting y = m*256 + k in the base subfield of Fp^8. The first +// resulting point (x, y) on kb8 is used as the image of the message, and +// multiset hashing is the additive group sum of those mapped points. package multisethash diff --git a/ecc/kb8/multiset-hash/multiset_hash.go b/ecc/kb8/multiset-hash/multiset_hash.go index 233013000c..e506768494 100644 --- a/ecc/kb8/multiset-hash/multiset_hash.go +++ b/ecc/kb8/multiset-hash/multiset_hash.go @@ -4,18 +4,14 @@ import ( "errors" "github.com/consensys/gnark-crypto/ecc/kb8" - "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" - "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" ) -var ( - errMapFailure = errors.New("kb8 multiset hash: failed to map message after 256 offsets") - mapPerm = poseidon2.NewPermutation(16, 6, 21) -) +const tweakBound = 256 + +var errMapFailure = errors.New("kb8 multiset hash: failed to map message after 256 y-increments") // Accumulator stores an additive multiset hash state in affine coordinates. -// Updates are carried out in Jacobian coordinates and normalized back to affine. type Accumulator struct { sum kb8.G1Affine } @@ -28,27 +24,23 @@ func NewAccumulator() Accumulator { } // Insert maps msg to kb8 and adds it to the accumulator. -func (a *Accumulator) Insert(msg *extensions.E8) error { +func (a *Accumulator) Insert(msg uint16) error { p, _, err := Map(msg) if err != nil { return err } - var sumJac kb8.G1Jac - sumJac.FromAffine(&a.sum).AddMixed(&p) - a.sum.FromJacobian(&sumJac) + a.sum.Add(&a.sum, &p) return nil } // Remove maps msg to kb8 and subtracts it from the accumulator. -func (a *Accumulator) Remove(msg *extensions.E8) error { +func (a *Accumulator) Remove(msg uint16) error { p, _, err := Map(msg) if err != nil { return err } p.Neg(&p) - var sumJac kb8.G1Jac - sumJac.FromAffine(&a.sum).AddMixed(&p) - a.sum.FromJacobian(&sumJac) + a.sum.Add(&a.sum, &p) return nil } @@ -63,79 +55,41 @@ func (a *Accumulator) Reset() { } // Hash returns the multiset hash of msgs. -func Hash(msgs []extensions.E8) (kb8.G1Affine, error) { +func Hash(msgs []uint16) (kb8.G1Affine, error) { acc := NewAccumulator() - for i := range msgs { - if err := acc.Insert(&msgs[i]); err != nil { + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { return kb8.G1Affine{}, err } } return acc.Digest(), nil } -// Map deterministically maps msg to a point on kb8 using KoalaBear Poseidon2. -// It returns the mapped point and the offset in [0, 255] that produced it. -func Map(msg *extensions.E8) (kb8.G1Affine, uint8, error) { - var state [16]koalabear.Element - messageToState(msg, state[:8]) +// Map deterministically maps msg to a point on kb8 using the y-increment method. +// It returns the mapped point and the first offset k in [0, 255] such that +// y = msg*256 + k yields a point (x, y) on kb8. +func Map(msg uint16) (kb8.G1Affine, uint8, error) { + _, b := kb8.CurveCoefficients() + baseY := uint64(msg) * tweakBound - a, b := kb8.CurveCoefficients() - for offset := uint16(0); offset < 256; offset++ { - state[8].SetUint64(uint64(offset)) - clearStateSuffix(state[9:]) + for k := uint16(0); k < tweakBound; k++ { + var y, c, ySquared extensions.E8 + y.SetZero() + y.C0.B0.A0.SetUint64(baseY + uint64(k)) - if err := mapPerm.Permutation(state[:]); err != nil { - return kb8.G1Affine{}, 0, err - } + ySquared.Square(&y) + c.Sub(&b, &ySquared) - x := stateToE8(state[:8]) - var rhs, y, tmp extensions.E8 - rhs.Square(&x).Mul(&rhs, &x) - tmp.Mul(&x, &a) - rhs.Add(&rhs, &tmp).Add(&rhs, &b) - if rhs.Legendre() != 1 { + x, ok := depressedCubicRoot(c) + if !ok { continue } - y.Sqrt(&rhs) - if y.LexicographicallyLargest() { - y.Neg(&y) - } p := kb8.G1Affine{X: x, Y: y} if p.IsOnCurve() && p.IsInSubGroup() { - return p, uint8(offset), nil + return p, uint8(k), nil } } return kb8.G1Affine{}, 0, errMapFailure } - -func clearStateSuffix(s []koalabear.Element) { - for i := range s { - s[i].SetZero() - } -} - -func messageToState(msg *extensions.E8, out []koalabear.Element) { - out[0] = msg.C0.B0.A0 - out[1] = msg.C0.B0.A1 - out[2] = msg.C0.B1.A0 - out[3] = msg.C0.B1.A1 - out[4] = msg.C1.B0.A0 - out[5] = msg.C1.B0.A1 - out[6] = msg.C1.B1.A0 - out[7] = msg.C1.B1.A1 -} - -func stateToE8(in []koalabear.Element) extensions.E8 { - var x extensions.E8 - x.C0.B0.A0 = in[0] - x.C0.B0.A1 = in[1] - x.C0.B1.A0 = in[2] - x.C0.B1.A1 = in[3] - x.C1.B0.A0 = in[4] - x.C1.B0.A1 = in[5] - x.C1.B1.A0 = in[6] - x.C1.B1.A1 = in[7] - return x -} diff --git a/ecc/kb8/multiset-hash/multiset_hash_test.go b/ecc/kb8/multiset-hash/multiset_hash_test.go index 9fdca84650..226afaa222 100644 --- a/ecc/kb8/multiset-hash/multiset_hash_test.go +++ b/ecc/kb8/multiset-hash/multiset_hash_test.go @@ -8,39 +8,44 @@ import ( "github.com/stretchr/testify/require" ) -func randomMessages(t *testing.T, n int) []extensions.E8 { - t.Helper() - return mustRandomMessages(n) -} - -func mustRandomMessages(n int) []extensions.E8 { - res := make([]extensions.E8, n) +func sampleMessages(n int) []uint16 { + res := make([]uint16, n) + var x uint32 = 1 for i := range res { - res[i].MustSetRandom() + x = 1664525*x + 1013904223 + res[i] = uint16(x) } return res } +func expectedY(msg uint16, offset uint8) extensions.E8 { + var y extensions.E8 + y.C0.B0.A0.SetUint64(uint64(msg)*tweakBound + uint64(offset)) + return y +} + func TestMapDeterministic(t *testing.T) { - msgs := randomMessages(t, 16) - for i := range msgs { - p1, o1, err := Map(&msgs[i]) + for _, msg := range sampleMessages(32) { + p1, o1, err := Map(msg) require.NoError(t, err) - p2, o2, err := Map(&msgs[i]) + p2, o2, err := Map(msg) require.NoError(t, err) + wantY := expectedY(msg, o1) require.Equal(t, o1, o2) require.True(t, p1.Equal(&p2)) require.True(t, p1.IsOnCurve()) require.True(t, p1.IsInSubGroup()) + require.False(t, p1.IsInfinity()) + require.True(t, p1.Y.Equal(&wantY)) } } func TestHashPermutationInvariant(t *testing.T) { - msgs := randomMessages(t, 8) + msgs := sampleMessages(8) got1, err := Hash(msgs) require.NoError(t, err) - permuted := append([]extensions.E8(nil), msgs...) + permuted := append([]uint16(nil), msgs...) permuted[0], permuted[5] = permuted[5], permuted[0] permuted[1], permuted[7] = permuted[7], permuted[1] @@ -49,23 +54,23 @@ func TestHashPermutationInvariant(t *testing.T) { require.True(t, got1.Equal(&got2)) } -func TestAddRemove(t *testing.T) { - msgs := randomMessages(t, 6) +func TestInsertRemove(t *testing.T) { + msgs := sampleMessages(6) acc := NewAccumulator() - for i := range msgs { - require.NoError(t, acc.Insert(&msgs[i])) + for _, msg := range msgs { + require.NoError(t, acc.Insert(msg)) } withAll := acc.Digest() require.True(t, withAll.IsOnCurve()) - require.NoError(t, acc.Remove(&msgs[2])) - require.NoError(t, acc.Remove(&msgs[4])) + require.NoError(t, acc.Remove(msgs[2])) + require.NoError(t, acc.Remove(msgs[4])) acc2 := NewAccumulator() - require.NoError(t, acc2.Insert(&msgs[0])) - require.NoError(t, acc2.Insert(&msgs[1])) - require.NoError(t, acc2.Insert(&msgs[3])) - require.NoError(t, acc2.Insert(&msgs[5])) + require.NoError(t, acc2.Insert(msgs[0])) + require.NoError(t, acc2.Insert(msgs[1])) + require.NoError(t, acc2.Insert(msgs[3])) + require.NoError(t, acc2.Insert(msgs[5])) d1 := acc.Digest() d2 := acc2.Digest() @@ -73,35 +78,61 @@ func TestAddRemove(t *testing.T) { } func TestHashMatchesAccumulator(t *testing.T) { - msgs := randomMessages(t, 10) + msgs := sampleMessages(10) got, err := Hash(msgs) require.NoError(t, err) acc := NewAccumulator() - for i := range msgs { - require.NoError(t, acc.Insert(&msgs[i])) + for _, msg := range msgs { + require.NoError(t, acc.Insert(msg)) } digest := acc.Digest() require.True(t, got.Equal(&digest)) } func TestDuplicatesMatter(t *testing.T) { - msgs := randomMessages(t, 1) - single, err := Hash(msgs) + msg := sampleMessages(1)[0] + single, err := Hash([]uint16{msg}) require.NoError(t, err) - double, err := Hash([]extensions.E8{msgs[0], msgs[0]}) + double, err := Hash([]uint16{msg, msg}) require.NoError(t, err) require.False(t, single.Equal(&double)) } +func TestEmptyHashIsInfinity(t *testing.T) { + got, err := Hash(nil) + require.NoError(t, err) + require.True(t, got.IsInfinity()) +} + +func TestDigestRoundTrip(t *testing.T) { + msgs := sampleMessages(4) + got, err := Hash(msgs) + require.NoError(t, err) + buf := got.Bytes() + var dec kb8.G1Affine + _, err = dec.SetBytes(buf[:]) + require.NoError(t, err) + require.True(t, dec.Equal(&got)) +} + +func TestMapSatisfiesYIncrementRelation(t *testing.T) { + for _, msg := range sampleMessages(64) { + p, offset, err := Map(msg) + require.NoError(t, err) + require.Less(t, int(offset), tweakBound) + wantY := expectedY(msg, offset) + require.True(t, p.Y.Equal(&wantY)) + } +} + func BenchmarkMap(b *testing.B) { - var msg extensions.E8 - msg.MustSetRandom() + msg := sampleMessages(1)[0] b.ResetTimer() for i := 0; i < b.N; i++ { - _, _, err := Map(&msg) + _, _, err := Map(msg) if err != nil { b.Fatal(err) } @@ -109,12 +140,12 @@ func BenchmarkMap(b *testing.B) { } func BenchmarkAccumulatorInsert(b *testing.B) { - msgs := mustRandomMessages(256) + msgs := sampleMessages(256) b.ResetTimer() for i := 0; i < b.N; i++ { acc := NewAccumulator() - for j := range msgs { - if err := acc.Insert(&msgs[j]); err != nil { + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { b.Fatal(err) } } @@ -123,7 +154,7 @@ func BenchmarkAccumulatorInsert(b *testing.B) { } func BenchmarkHash256(b *testing.B) { - msgs := mustRandomMessages(256) + msgs := sampleMessages(256) b.ResetTimer() for i := 0; i < b.N; i++ { got, err := Hash(msgs) @@ -135,29 +166,3 @@ func BenchmarkHash256(b *testing.B) { } } } - -func TestEmptyHashIsInfinity(t *testing.T) { - got, err := Hash(nil) - require.NoError(t, err) - require.True(t, got.IsInfinity()) -} - -func TestMappedPointNotInfinity(t *testing.T) { - msgs := randomMessages(t, 16) - for i := range msgs { - p, _, err := Map(&msgs[i]) - require.NoError(t, err) - require.False(t, p.IsInfinity()) - } -} - -func TestDigestRoundTrip(t *testing.T) { - msgs := randomMessages(t, 4) - got, err := Hash(msgs) - require.NoError(t, err) - buf := got.Bytes() - var dec kb8.G1Affine - _, err = dec.SetBytes(buf[:]) - require.NoError(t, err) - require.True(t, dec.Equal(&got)) -} From 4e642e43f29c277a71e7979ff6dcaced158f3764 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 14 Apr 2026 22:30:05 -0400 Subject: [PATCH 04/33] perf: use addchain --- ecc/kb8/multiset-hash/cardano.go | 661 +++++++++++++++++++++++++++++-- 1 file changed, 636 insertions(+), 25 deletions(-) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index 96655b22fd..f0113a5d2f 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -14,32 +14,17 @@ import ( // quadratic-extension fallback when the discriminant is not a square. var ( - e8CbrtExponent big.Int - e16CbrtExponent big.Int - e8Omega extensions.E8 - e8Beta extensions.E8 - e8One extensions.E8 - e8Two extensions.E8 - e8Three extensions.E8 - e8Four extensions.E8 - e8TwentySeven extensions.E8 - e8NegThree extensions.E8 + e8Omega extensions.E8 + e8Beta extensions.E8 + e8One extensions.E8 + e8Two extensions.E8 + e8Three extensions.E8 + e8Four extensions.E8 + e8TwentySeven extensions.E8 + e8NegThree extensions.E8 ) func init() { - var q8, q16 big.Int - q8.Exp(koalabear.Modulus(), big.NewInt(8), nil) - q16.Mul(&q8, &q8) - - // q8 ≡ 4 (mod 9), so cubic residues admit x^((2q8+1)/9) as a cube root. - e8CbrtExponent.Mul(&q8, big.NewInt(2)) - e8CbrtExponent.Add(&e8CbrtExponent, big.NewInt(1)) - e8CbrtExponent.Div(&e8CbrtExponent, big.NewInt(9)) - - // q16 ≡ 7 (mod 9), so cubic residues admit x^((q16+2)/9) as a cube root. - e16CbrtExponent.Add(&q16, big.NewInt(2)) - e16CbrtExponent.Div(&e16CbrtExponent, big.NewInt(9)) - e8One.SetOne() e8Two.C0.B0.A0.SetUint64(2) e8Three.C0.B0.A0.SetUint64(3) @@ -202,7 +187,7 @@ func isDepressedCubicRoot(x, negCHalf *extensions.E8) bool { func cbrtE8(z, x *extensions.E8) *extensions.E8 { var y extensions.E8 - y.Exp(*x, &e8CbrtExponent) + expByKBE8Cbrt(&y, x) var check extensions.E8 check.Square(&y).Mul(&check, &y) if !check.Equal(x) { @@ -211,6 +196,230 @@ func cbrtE8(z, x *extensions.E8) *extensions.E8 { return z.Set(&y) } +func expByKBE8Cbrt(z, x *extensions.E8) *extensions.E8 { + // expByKBCbrt computation is derived from the addition chain: + // + // _10 = 2*1 + // _100 = 2*_10 + // _110 = _10 + _100 + // _1000 = _10 + _110 + // _1010 = _10 + _1000 + // _1011 = 1 + _1010 + // _1100 = 1 + _1011 + // _10110 = _1010 + _1100 + // _11100 = _110 + _10110 + // _11110 = _10 + _11100 + // _11111 = 1 + _11110 + // _101011 = _1100 + _11111 + // _1000111 = _11100 + _101011 + // _1001011 = _100 + _1000111 + // _1010011 = _1000 + _1001011 + // _1010101 = _10 + _1010011 + // _1011001 = _100 + _1010101 + // _1110111 = _11110 + _1011001 + // _1111001 = _10 + _1110111 + // _10001111 = _10110 + _1111001 + // _10010101 = _110 + _10001111 + // _10011101 = _1000 + _10010101 + // _10100101 = _1000 + _10011101 + // _10101111 = _1010 + _10100101 + // _10110111 = _1000 + _10101111 + // _11000011 = _1100 + _10110111 + // _11001011 = _1000 + _11000011 + // _11001101 = _10 + _11001011 + // _11001111 = _10 + _11001101 + // _11010101 = _110 + _11001111 + // _11011101 = _1000 + _11010101 + // i49 = ((_11001101 + _11011101) << 7 + _10110111) << 8 + _1011001 + // i80 = ((i49 << 2 + 1) << 16 + _10011101) << 11 + // i100 = ((_1001011 + i80) << 9 + _1000111) << 8 + _1010011 + // i128 = ((i100 << 11 + _11001011) << 9 + _11001111) << 6 + // i154 = ((_11111 + i128) << 15 + _10100101) << 8 + _10010101 + // i182 = ((i154 << 9 + _10101111) << 8 + _1111001) << 9 + // i200 = ((_10010101 + i182) << 8 + _11011101) << 7 + _1110111 + // i228 = ((i200 << 9 + _11001101) << 8 + _11010101) << 9 + // i251 = ((_11000011 + i228) << 8 + _101011) << 12 + _11011101 + // i273 = ((_110 + i251) << 8 + _10001111) << 11 + _11010101 + // i296 = ((i273 << 8 + _1010101) << 8 + _1010101) << 5 + // return _1011 + i296 + // + // Operations: 239 squares 58 multiplies + // + // Generated by github.com/mmcloughlin/addchain v0.4.0. + + var ( + t0 extensions.E8 + t1 extensions.E8 + t2 extensions.E8 + t3 extensions.E8 + t4 extensions.E8 + t5 extensions.E8 + t6 extensions.E8 + t7 extensions.E8 + t8 extensions.E8 + t9 extensions.E8 + t10 extensions.E8 + t11 extensions.E8 + t12 extensions.E8 + t13 extensions.E8 + t14 extensions.E8 + t15 extensions.E8 + t16 extensions.E8 + t17 extensions.E8 + t18 extensions.E8 + t19 extensions.E8 + t20 extensions.E8 + t21 extensions.E8 + t22 extensions.E8 + t23 extensions.E8 + t24 extensions.E8 + ) + + t0.Square(x) + t1.Square(&t0) + t2.Mul(&t0, &t1) + t3.Mul(&t0, &t2) + t4.Mul(&t0, &t3) + t5.Mul(x, &t4) + t6.Mul(x, &t5) + t7.Mul(&t4, &t6) + t8.Mul(&t2, &t7) + t9.Mul(&t0, &t8) + t10.Mul(x, &t9) + t11.Mul(&t6, &t10) + t8.Mul(&t8, &t11) + t12.Mul(&t1, &t8) + t13.Mul(&t3, &t12) + t14.Mul(&t0, &t13) + t1.Mul(&t1, &t14) + t9.Mul(&t9, &t1) + t15.Mul(&t0, &t9) + t7.Mul(&t7, &t15) + t16.Mul(&t2, &t7) + t17.Mul(&t3, &t16) + t18.Mul(&t3, &t17) + t4.Mul(&t4, &t18) + t19.Mul(&t3, &t4) + t6.Mul(&t6, &t19) + t20.Mul(&t3, &t6) + t21.Mul(&t0, &t20) + t0.Mul(&t0, &t21) + t22.Mul(&t2, &t0) + t3.Mul(&t3, &t22) + t23.Mul(&t21, &t3) + for s := 0; s < 7; s++ { + t23.Square(&t23) + } + t19.Mul(&t19, &t23) + for s := 0; s < 8; s++ { + t19.Square(&t19) + } + t1.Mul(&t1, &t19) + for s := 0; s < 2; s++ { + t1.Square(&t1) + } + t24.Mul(x, &t1) + for s := 0; s < 16; s++ { + t24.Square(&t24) + } + t17.Mul(&t17, &t24) + for s := 0; s < 11; s++ { + t17.Square(&t17) + } + t12.Mul(&t12, &t17) + for s := 0; s < 9; s++ { + t12.Square(&t12) + } + t8.Mul(&t8, &t12) + for s := 0; s < 8; s++ { + t8.Square(&t8) + } + t13.Mul(&t13, &t8) + for s := 0; s < 11; s++ { + t13.Square(&t13) + } + t20.Mul(&t20, &t13) + for s := 0; s < 9; s++ { + t20.Square(&t20) + } + t0.Mul(&t0, &t20) + for s := 0; s < 6; s++ { + t0.Square(&t0) + } + t10.Mul(&t10, &t0) + for s := 0; s < 15; s++ { + t10.Square(&t10) + } + t18.Mul(&t18, &t10) + for s := 0; s < 8; s++ { + t18.Square(&t18) + } + t18.Mul(&t16, &t18) + for s := 0; s < 9; s++ { + t18.Square(&t18) + } + t4.Mul(&t4, &t18) + for s := 0; s < 8; s++ { + t4.Square(&t4) + } + t15.Mul(&t15, &t4) + for s := 0; s < 9; s++ { + t15.Square(&t15) + } + t16.Mul(&t16, &t15) + for s := 0; s < 8; s++ { + t16.Square(&t16) + } + t16.Mul(&t3, &t16) + for s := 0; s < 7; s++ { + t16.Square(&t16) + } + t9.Mul(&t9, &t16) + for s := 0; s < 9; s++ { + t9.Square(&t9) + } + t21.Mul(&t21, &t9) + for s := 0; s < 8; s++ { + t21.Square(&t21) + } + t21.Mul(&t22, &t21) + for s := 0; s < 9; s++ { + t21.Square(&t21) + } + t6.Mul(&t6, &t21) + for s := 0; s < 8; s++ { + t6.Square(&t6) + } + t11.Mul(&t11, &t6) + for s := 0; s < 12; s++ { + t11.Square(&t11) + } + t3.Mul(&t3, &t11) + t2.Mul(&t2, &t3) + for s := 0; s < 8; s++ { + t2.Square(&t2) + } + t7.Mul(&t7, &t2) + for s := 0; s < 11; s++ { + t7.Square(&t7) + } + t22.Mul(&t22, &t7) + for s := 0; s < 8; s++ { + t22.Square(&t22) + } + t22.Mul(&t14, &t22) + for s := 0; s < 8; s++ { + t22.Square(&t22) + } + t14.Mul(&t14, &t22) + for s := 0; s < 5; s++ { + t14.Square(&t14) + } + z.Mul(&t5, &t14) + + return z +} + func findPrimitiveCubeRoot() extensions.E8 { var exp big.Int exp.Exp(koalabear.Modulus(), big.NewInt(8), nil) @@ -345,9 +554,411 @@ func (z *e16) Exp(x e16, k *big.Int) *e16 { return z } +func (z *e16) expByKBCbrt(x *e16) *e16 { + // expByKBCbrt computation is derived from the addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _101 = _10 + _11 + // _110 = 1 + _101 + // _111 = 1 + _110 + // _1001 = _10 + _111 + // _1011 = _10 + _1001 + // _1101 = _10 + _1011 + // _1111 = _10 + _1101 + // _10001 = _10 + _1111 + // _10101 = _110 + _1111 + // _10111 = _10 + _10101 + // _11001 = _10 + _10111 + // _11011 = _10 + _11001 + // _100001 = _110 + _11011 + // _100011 = _10 + _100001 + // _100101 = _10 + _100011 + // _100111 = _10 + _100101 + // _101001 = _10 + _100111 + // _101011 = _10 + _101001 + // _101101 = _10 + _101011 + // _101111 = _10 + _101101 + // _110001 = _10 + _101111 + // _110011 = _10 + _110001 + // _110101 = _10 + _110011 + // _110111 = _10 + _110101 + // _111001 = _10 + _110111 + // _111011 = _10 + _111001 + // _1100100 = _101001 + _111011 + // _1111111 = _11011 + _1100100 + // i53 = ((_1100100 << 5 + _1011) << 8 + _1111111) << 8 + // i75 = ((_11001 + i53) << 12 + _101001) << 7 + _110111 + // i97 = ((i75 << 6 + _101111) << 7 + _110001) << 7 + // i112 = (2*(_110101 + i97) + 1) << 11 + _111001 + // i137 = ((i112 << 8 + _100001) << 5 + _111) << 10 + // i153 = ((_111011 + i137) << 6 + _111001) << 7 + _111011 + // i175 = ((i153 << 8 + _110101) << 5 + _10001) << 7 + // i197 = ((_111011 + i175) << 11 + _101111) << 8 + _110101 + // i217 = ((2*i197 + 1) << 9 + _1111111) << 8 + // i232 = ((_100101 + i217) << 7 + _100101) << 5 + _10001 + // i254 = ((i232 << 5 + _1101) << 6 + _1011) << 9 + // i269 = ((_100001 + i254) << 5 + _11001) << 7 + _101 + // i298 = ((i269 << 10 + _101001) << 7 + _101001) << 10 + // i315 = ((_11011 + i298) << 7 + _101011) << 7 + _110101 + // i335 = ((2*i315 + 1) << 11 + _100101) << 6 + // i355 = ((_101111 + i335) << 6 + _101) << 11 + _110011 + // i382 = ((i355 << 7 + _101101) << 10 + _100111) << 8 + // i397 = ((_10101 + i382) << 7 + _111001) << 5 + _11011 + // i423 = ((i397 << 5 + _1111) << 11 + _1101) << 8 + // i438 = ((_111011 + i423) << 4 + _1001) << 8 + _111001 + // i461 = ((i438 << 7 + _111011) << 6 + _10111) << 8 + // i477 = ((_1111111 + i461) << 7 + _101001) << 6 + _100011 + // i501 = ((i477 << 5 + _10111) << 7 + _1001) << 10 + // i513 = ((_110011 + i501) << 4 + _1001) << 5 + _11 + // i539 = ((i513 << 11 + _1111) << 7 + _111) << 6 + // i558 = ((_111 + i539) << 7 + _1111) << 9 + _110101 + // i578 = ((i558 << 6 + _10101) << 6 + _10101) << 6 + // return (_10101 + i578) << 5 + _1011 + // + // Operations: 487 squares 98 multiplies + // + // Generated by github.com/mmcloughlin/addchain v0.4.0. + + var ( + t0 e16 + t1 e16 + t2 e16 + t3 e16 + t4 e16 + t5 e16 + t6 e16 + t7 e16 + t8 e16 + t9 e16 + t10 e16 + t11 e16 + t12 e16 + t13 e16 + t14 e16 + t15 e16 + t16 e16 + t17 e16 + t18 e16 + t19 e16 + t20 e16 + t21 e16 + t22 e16 + t23 e16 + t24 e16 + t25 e16 + t26 e16 + t27 e16 + t28 e16 + ) + + t0.Square(x) + t1.Mul(x, &t0) + t2.Mul(&t0, &t1) + t3.Mul(x, &t2) + t4.Mul(x, &t3) + t5.Mul(&t0, &t4) + t6.Mul(&t0, &t5) + t7.Mul(&t0, &t6) + t8.Mul(&t0, &t7) + t9.Mul(&t0, &t8) + t10.Mul(&t3, &t8) + t11.Mul(&t0, &t10) + t12.Mul(&t0, &t11) + t13.Mul(&t0, &t12) + t3.Mul(&t3, &t13) + t14.Mul(&t0, &t3) + t15.Mul(&t0, &t14) + t16.Mul(&t0, &t15) + t17.Mul(&t0, &t16) + t18.Mul(&t0, &t17) + t19.Mul(&t0, &t18) + t20.Mul(&t0, &t19) + t21.Mul(&t0, &t20) + t22.Mul(&t0, &t21) + t23.Mul(&t0, &t22) + t24.Mul(&t0, &t23) + t25.Mul(&t0, &t24) + t0.Mul(&t0, &t25) + t26.Mul(&t17, &t0) + t27.Mul(&t13, &t26) + for s := 0; s < 5; s++ { + t26.Square(&t26) + } + t26.Mul(&t6, &t26) + for s := 0; s < 8; s++ { + t26.Square(&t26) + } + t26.Mul(&t27, &t26) + for s := 0; s < 8; s++ { + t26.Square(&t26) + } + t26.Mul(&t12, &t26) + for s := 0; s < 12; s++ { + t26.Square(&t26) + } + t26.Mul(&t17, &t26) + for s := 0; s < 7; s++ { + t26.Square(&t26) + } + t24.Mul(&t24, &t26) + for s := 0; s < 6; s++ { + t24.Square(&t24) + } + t24.Mul(&t20, &t24) + for s := 0; s < 7; s++ { + t24.Square(&t24) + } + t21.Mul(&t21, &t24) + for s := 0; s < 7; s++ { + t21.Square(&t21) + } + t21.Mul(&t23, &t21) + t21.Square(&t21) + t21.Mul(x, &t21) + for s := 0; s < 11; s++ { + t21.Square(&t21) + } + t21.Mul(&t25, &t21) + for s := 0; s < 8; s++ { + t21.Square(&t21) + } + t21.Mul(&t3, &t21) + for s := 0; s < 5; s++ { + t21.Square(&t21) + } + t21.Mul(&t4, &t21) + for s := 0; s < 10; s++ { + t21.Square(&t21) + } + t21.Mul(&t0, &t21) + for s := 0; s < 6; s++ { + t21.Square(&t21) + } + t21.Mul(&t25, &t21) + for s := 0; s < 7; s++ { + t21.Square(&t21) + } + t21.Mul(&t0, &t21) + for s := 0; s < 8; s++ { + t21.Square(&t21) + } + t21.Mul(&t23, &t21) + for s := 0; s < 5; s++ { + t21.Square(&t21) + } + t21.Mul(&t9, &t21) + for s := 0; s < 7; s++ { + t21.Square(&t21) + } + t21.Mul(&t0, &t21) + for s := 0; s < 11; s++ { + t21.Square(&t21) + } + t21.Mul(&t20, &t21) + for s := 0; s < 8; s++ { + t21.Square(&t21) + } + t21.Mul(&t23, &t21) + t21.Square(&t21) + t21.Mul(x, &t21) + for s := 0; s < 9; s++ { + t21.Square(&t21) + } + t21.Mul(&t27, &t21) + for s := 0; s < 8; s++ { + t21.Square(&t21) + } + t21.Mul(&t15, &t21) + for s := 0; s < 7; s++ { + t21.Square(&t21) + } + t21.Mul(&t15, &t21) + for s := 0; s < 5; s++ { + t21.Square(&t21) + } + t9.Mul(&t9, &t21) + for s := 0; s < 5; s++ { + t9.Square(&t9) + } + t9.Mul(&t7, &t9) + for s := 0; s < 6; s++ { + t9.Square(&t9) + } + t9.Mul(&t6, &t9) + for s := 0; s < 9; s++ { + t9.Square(&t9) + } + t3.Mul(&t3, &t9) + for s := 0; s < 5; s++ { + t3.Square(&t3) + } + t12.Mul(&t12, &t3) + for s := 0; s < 7; s++ { + t12.Square(&t12) + } + t12.Mul(&t2, &t12) + for s := 0; s < 10; s++ { + t12.Square(&t12) + } + t12.Mul(&t17, &t12) + for s := 0; s < 7; s++ { + t12.Square(&t12) + } + t12.Mul(&t17, &t12) + for s := 0; s < 10; s++ { + t12.Square(&t12) + } + t12.Mul(&t13, &t12) + for s := 0; s < 7; s++ { + t12.Square(&t12) + } + t18.Mul(&t18, &t12) + for s := 0; s < 7; s++ { + t18.Square(&t18) + } + t18.Mul(&t23, &t18) + t18.Square(&t18) + t28.Mul(x, &t18) + for s := 0; s < 11; s++ { + t28.Square(&t28) + } + t15.Mul(&t15, &t28) + for s := 0; s < 6; s++ { + t15.Square(&t15) + } + t20.Mul(&t20, &t15) + for s := 0; s < 6; s++ { + t20.Square(&t20) + } + t2.Mul(&t2, &t20) + for s := 0; s < 11; s++ { + t2.Square(&t2) + } + t2.Mul(&t22, &t2) + for s := 0; s < 7; s++ { + t2.Square(&t2) + } + t19.Mul(&t19, &t2) + for s := 0; s < 10; s++ { + t19.Square(&t19) + } + t16.Mul(&t16, &t19) + for s := 0; s < 8; s++ { + t16.Square(&t16) + } + t16.Mul(&t10, &t16) + for s := 0; s < 7; s++ { + t16.Square(&t16) + } + t16.Mul(&t25, &t16) + for s := 0; s < 5; s++ { + t16.Square(&t16) + } + t13.Mul(&t13, &t16) + for s := 0; s < 5; s++ { + t13.Square(&t13) + } + t13.Mul(&t8, &t13) + for s := 0; s < 11; s++ { + t13.Square(&t13) + } + t7.Mul(&t7, &t13) + for s := 0; s < 8; s++ { + t7.Square(&t7) + } + t7.Mul(&t0, &t7) + for s := 0; s < 4; s++ { + t7.Square(&t7) + } + t7.Mul(&t5, &t7) + for s := 0; s < 8; s++ { + t7.Square(&t7) + } + t25.Mul(&t25, &t7) + for s := 0; s < 7; s++ { + t25.Square(&t25) + } + t0.Mul(&t0, &t25) + for s := 0; s < 6; s++ { + t0.Square(&t0) + } + t0.Mul(&t11, &t0) + for s := 0; s < 8; s++ { + t0.Square(&t0) + } + t27.Mul(&t27, &t0) + for s := 0; s < 7; s++ { + t27.Square(&t27) + } + t17.Mul(&t17, &t27) + for s := 0; s < 6; s++ { + t17.Square(&t17) + } + t14.Mul(&t14, &t17) + for s := 0; s < 5; s++ { + t14.Square(&t14) + } + t11.Mul(&t11, &t14) + for s := 0; s < 7; s++ { + t11.Square(&t11) + } + t11.Mul(&t5, &t11) + for s := 0; s < 10; s++ { + t11.Square(&t11) + } + t22.Mul(&t22, &t11) + for s := 0; s < 4; s++ { + t22.Square(&t22) + } + t5.Mul(&t5, &t22) + for s := 0; s < 5; s++ { + t5.Square(&t5) + } + t1.Mul(&t1, &t5) + for s := 0; s < 11; s++ { + t1.Square(&t1) + } + t1.Mul(&t8, &t1) + for s := 0; s < 7; s++ { + t1.Square(&t1) + } + t1.Mul(&t4, &t1) + for s := 0; s < 6; s++ { + t1.Square(&t1) + } + t4.Mul(&t4, &t1) + for s := 0; s < 7; s++ { + t4.Square(&t4) + } + t8.Mul(&t8, &t4) + for s := 0; s < 9; s++ { + t8.Square(&t8) + } + t23.Mul(&t23, &t8) + for s := 0; s < 6; s++ { + t23.Square(&t23) + } + t23.Mul(&t10, &t23) + for s := 0; s < 6; s++ { + t23.Square(&t23) + } + t23.Mul(&t10, &t23) + for s := 0; s < 6; s++ { + t23.Square(&t23) + } + t10.Mul(&t10, &t23) + for s := 0; s < 5; s++ { + t10.Square(&t10) + } + z.Mul(&t6, &t10) + + return z +} + func (z *e16) Cbrt(x *e16) *e16 { var y e16 - y.Exp(*x, &e16CbrtExponent) + y.expByKBCbrt(x) var check e16 check.Square(&y).Mul(&check, &y) if !check.A0.Equal(&x.A0) || !check.A1.Equal(&x.A1) { From 9c3ef02f0bdda61e64dff120ad4d0f5db2c7527c Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 14 Apr 2026 23:53:43 -0400 Subject: [PATCH 05/33] perf: use recursive torus cbrt --- ecc/kb8/multiset-hash/cardano.go | 147 +++++++++++++++++++++++--- ecc/kb8/multiset-hash/cardano_test.go | 70 ++++++++++++ field/koalabear/extensions/e2.go | 108 ++++++++++++++++--- field/koalabear/extensions/e2_test.go | 26 +++++ field/koalabear/extensions/e4.go | 42 ++++++++ field/koalabear/extensions/e4_test.go | 22 ++++ field/koalabear/extensions/e8.go | 46 ++++++++ field/koalabear/extensions/e8_test.go | 23 ++++ 8 files changed, 459 insertions(+), 25 deletions(-) create mode 100644 ecc/kb8/multiset-hash/cardano_test.go diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index f0113a5d2f..c9fb346cb5 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -16,6 +16,7 @@ import ( var ( e8Omega extensions.E8 e8Beta extensions.E8 + e8BetaInv extensions.E8 e8One extensions.E8 e8Two extensions.E8 e8Three extensions.E8 @@ -24,6 +25,13 @@ var ( e8NegThree extensions.E8 ) +var e16LucasExponent = [4]uint64{ + 10958008504694079489, + 259792827419799556, + 15418438666690820192, + 45116822996742594, +} + func init() { e8One.SetOne() e8Two.C0.B0.A0.SetUint64(2) @@ -33,6 +41,7 @@ func init() { e8NegThree.Set(&e8Three).Neg(&e8NegThree) e8Beta = findNonSquare() + e8BetaInv.Inverse(&e8Beta) e8Omega = findPrimitiveCubeRoot() } @@ -186,14 +195,31 @@ func isDepressedCubicRoot(x, negCHalf *extensions.E8) bool { } func cbrtE8(z, x *extensions.E8) *extensions.E8 { - var y extensions.E8 - expByKBE8Cbrt(&y, x) - var check extensions.E8 + expByKBE8Cbrt(z, x) + return cbrtVerifyAndAdjustE8(z, x) +} + +func cbrtVerifyAndAdjustE8(z, x *extensions.E8) *extensions.E8 { + var check, omega2, y extensions.E8 + check.Square(z).Mul(&check, z) + if check.Equal(x) { + return z + } + + y.Mul(z, &e8Omega) check.Square(&y).Mul(&check, &y) - if !check.Equal(x) { - return nil + if check.Equal(x) { + return z.Set(&y) } - return z.Set(&y) + + omega2.Square(&e8Omega) + y.Mul(z, &omega2) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + return nil } func expByKBE8Cbrt(z, x *extensions.E8) *extensions.E8 { @@ -420,6 +446,31 @@ func expByKBE8Cbrt(z, x *extensions.E8) *extensions.E8 { return z } +func lucasV2E8(alpha *extensions.E8) (extensions.E8, extensions.E8) { + var v0, v1, two extensions.E8 + two.C0.B0.A0.SetUint64(2) + v0.Set(alpha) + v1.Square(alpha).Sub(&v1, &two) + + var prod extensions.E8 + for i := 246; i >= 1; i-- { + bit := (e16LucasExponent[i/64] >> uint(i%64)) & 1 + prod.Mul(&v0, &v1).Sub(&prod, alpha) + if bit == 0 { + v1.Set(&prod) + v0.Square(&v0).Sub(&v0, &two) + } else { + v0.Set(&prod) + v1.Square(&v1).Sub(&v1, &two) + } + } + + var te, te1 extensions.E8 + te.Mul(&v0, &v1).Sub(&te, alpha) + te1.Square(&v1).Sub(&te1, &two) + return te, te1 +} + func findPrimitiveCubeRoot() extensions.E8 { var exp big.Int exp.Exp(koalabear.Modulus(), big.NewInt(8), nil) @@ -493,6 +544,32 @@ type e16 struct { A0, A1 extensions.E8 } +func cbrtVerifyAndAdjustE16(z, x *e16) *e16 { + var check, y e16 + check.Square(z).Mul(&check, z) + if check.A0.Equal(&x.A0) && check.A1.Equal(&x.A1) { + return z + } + + y.A0.Mul(&z.A0, &e8Omega) + y.A1.Mul(&z.A1, &e8Omega) + check.Square(&y).Mul(&check, &y) + if check.A0.Equal(&x.A0) && check.A1.Equal(&x.A1) { + return z.Set(&y) + } + + var omega2 extensions.E8 + omega2.Square(&e8Omega) + y.A0.Mul(&z.A0, &omega2) + y.A1.Mul(&z.A1, &omega2) + check.Square(&y).Mul(&check, &y) + if check.A0.Equal(&x.A0) && check.A1.Equal(&x.A1) { + return z.Set(&y) + } + + return nil +} + func (z *e16) Set(x *e16) *e16 { z.A0.Set(&x.A0) z.A1.Set(&x.A1) @@ -957,12 +1034,58 @@ func (z *e16) expByKBCbrt(x *e16) *e16 { } func (z *e16) Cbrt(x *e16) *e16 { - var y e16 - y.expByKBCbrt(x) - var check e16 - check.Square(&y).Mul(&check, &y) - if !check.A0.Equal(&x.A0) || !check.A1.Equal(&x.A1) { + if x.A1.IsZero() { + if cbrtE8(&z.A0, &x.A0) == nil { + return nil + } + z.A1.SetZero() + return z + } + + if x.A0.IsZero() { + var y e16 + var a1OverBeta extensions.E8 + a1OverBeta.Mul(&x.A1, &e8BetaInv) + if cbrtE8(&y.A1, &a1OverBeta) == nil { + return nil + } + y.A0.SetZero() + return cbrtVerifyAndAdjustE16(z.Set(&y), x) + } + + var x0sq, x1sq, betaX1sq, norm extensions.E8 + x0sq.Square(&x.A0) + x1sq.Square(&x.A1) + betaX1sq.Mul(&x1sq, &e8Beta) + norm.Sub(&x0sq, &betaX1sq) + + var m, normInv extensions.E8 + if cbrtE8(&m, &norm) == nil { + return nil + } + normInv.Inverse(&norm) + + var halfTau, tau extensions.E8 + halfTau.Add(&x0sq, &betaX1sq) + halfTau.Mul(&halfTau, &normInv) + tau.Double(&halfTau) + + sigma, _ := lucasV2E8(&tau) + + var sigmaM1, sigmaP1, d0, d1, d0d1, d0d1Inv extensions.E8 + sigmaM1.Sub(&sigma, &e8One) + sigmaP1.Add(&sigma, &e8One) + d0.Mul(&m, &sigmaM1) + d1.Mul(&m, &sigmaP1) + d0d1.Mul(&d0, &d1) + if d0d1.IsZero() { return nil } - return z.Set(&y) + d0d1Inv.Inverse(&d0d1) + + var y e16 + y.A0.Mul(&d1, &d0d1Inv).Mul(&y.A0, &x.A0) + y.A1.Mul(&d0, &d0d1Inv).Mul(&y.A1, &x.A1) + + return cbrtVerifyAndAdjustE16(z.Set(&y), x) } diff --git a/ecc/kb8/multiset-hash/cardano_test.go b/ecc/kb8/multiset-hash/cardano_test.go new file mode 100644 index 0000000000..6ad75fc5d2 --- /dev/null +++ b/ecc/kb8/multiset-hash/cardano_test.go @@ -0,0 +1,70 @@ +package multisethash + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/stretchr/testify/require" +) + +func TestCbrtE8OnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check extensions.E8 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + require.NotNil(t, cbrtE8(&got, &x)) + check.Square(&got).Mul(&check, &got) + require.True(t, check.Equal(&x)) + } +} + +func TestCbrtE8RejectsNonResidues(t *testing.T) { + var x, got extensions.E8 + for i := 0; i < 256; i++ { + x.MustSetRandom() + if cbrtE8(&got, &x) == nil { + return + } + } + t.Fatal("failed to find an E8 non-cube in 256 samples") +} + +func TestE16CbrtOnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check e16 + a.A0.MustSetRandom() + a.A1.MustSetRandom() + x.Square(&a).Mul(&x, &a) + require.NotNil(t, got.Cbrt(&x)) + check.Square(&got).Mul(&check, &got) + require.True(t, check.A0.Equal(&x.A0)) + require.True(t, check.A1.Equal(&x.A1)) + } +} + +func TestE16CbrtRejectsNonResidues(t *testing.T) { + var x, got e16 + for i := 0; i < 256; i++ { + x.A0.MustSetRandom() + x.A1.MustSetRandom() + if got.Cbrt(&x) == nil { + return + } + } + t.Fatal("failed to find an E16 non-cube in 256 samples") +} + +func TestDepressedCubicRootFindsValidRoot(t *testing.T) { + for i := 0; i < 64; i++ { + var x, x3, c, lhs extensions.E8 + x.MustSetRandom() + x3.Square(&x).Mul(&x3, &x) + c.Double(&x).Add(&c, &x).Sub(&c, &x3) + + root, ok := depressedCubicRoot(c) + require.True(t, ok) + lhs.Square(&root).Mul(&lhs, &root) + lhs.Sub(&lhs, &root).Sub(&lhs, &root).Sub(&lhs, &root).Add(&lhs, &c) + require.True(t, lhs.IsZero()) + } +} diff --git a/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index 28dc234bf5..4c8131add4 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -11,6 +11,34 @@ import ( fr "github.com/consensys/gnark-crypto/field/koalabear" ) +var ( + cbrtFpOne fr.Element + cbrtFpTwo fr.Element + cbrtFpThree fr.Element + cbrtFpThreeInv fr.Element + cbrtE2One E2 + cbrtE2Omega E2 + cbrtE2Omega2 E2 +) + +var cbrtE2MuLucasExponent uint64 = 473490319 + +func init() { + cbrtFpOne.SetOne() + cbrtFpTwo.SetUint64(2) + cbrtFpThree.SetUint64(3) + cbrtFpThreeInv.Inverse(&cbrtFpThree) + + cbrtE2One.SetOne() + var sqrtMinusThree E2 + sqrtMinusThree.A0.Neg(&cbrtFpThree) + sqrtMinusThree.Sqrt(&sqrtMinusThree) + cbrtE2Omega.Sub(&sqrtMinusThree, &cbrtE2One) + cbrtE2Omega.A0.Halve() + cbrtE2Omega.A1.Halve() + cbrtE2Omega2.Square(&cbrtE2Omega) +} + // E2 is a degree two finite field extension of fr.Element type E2 struct { A0, A1 fr.Element @@ -219,30 +247,84 @@ func (z *E2) Sqrt(x *E2) *E2 { return z } -// Cbrt sets z to the cube root of x and returns z -// The function does not test whether the cube root -// exists or not, it's up to the caller to verify. +// Cbrt sets z to the cube root of x and returns z. +// It returns nil if x is not a cubic residue. func (z *E2) Cbrt(x *E2) *E2 { - // If x is in the base field (i.e., x.A1 == 0), use base field cube root directly if x.A1.IsZero() { z.A0.Cbrt(&x.A0) z.A1.SetZero() return z } - // General case for extension field - // The multiplicative group has order p² - 1 - // For a cube root, compute x^((2p² - 1) / 3) - var exp big.Int - exp.Mul(fr.Modulus(), fr.Modulus()) // p² - exp.Mul(&exp, big.NewInt(2)) // 2p² - exp.Sub(&exp, big.NewInt(1)) // 2p² - 1 - exp.Div(&exp, big.NewInt(3)) // (2p² - 1) / 3 - z.Exp(*x, &exp) + if x.A0.IsZero() { + z.A0.SetZero() + z.A1.Mul(&x.A1, &cbrtFpThreeInv) + z.A1.Cbrt(&z.A1) + return cbrtVerifyE2(z, x) + } + + var x0sq, x1sq, betaX1sq, norm fr.Element + x0sq.Square(&x.A0) + x1sq.Square(&x.A1) + betaX1sq.Mul(&x1sq, &cbrtFpThree) + norm.Sub(&x0sq, &betaX1sq) + + var m, normInv fr.Element + m.Cbrt(&norm) + normInv.Inverse(&norm) + + var tau fr.Element + tau.Add(&x0sq, &betaX1sq) + tau.Double(&tau) + tau.Mul(&tau, &normInv) + + sigma := lucasVFp(&tau) + + var d0, d1, d0d1, d0d1Inv fr.Element + d0.Sub(&sigma, &cbrtFpOne) + d0.Mul(&m, &d0) + d1.Add(&sigma, &cbrtFpOne) + d1.Mul(&m, &d1) + d0d1.Mul(&d0, &d1) + if d0d1.IsZero() { + return nil + } + d0d1Inv.Inverse(&d0d1) + + z.A0.Mul(&d1, &d0d1Inv).Mul(&z.A0, &x.A0) + z.A1.Mul(&d0, &d0d1Inv).Mul(&z.A1, &x.A1) + return cbrtVerifyE2(z, x) +} +func cbrtVerifyE2(z, x *E2) *E2 { + var check E2 + check.Square(z).Mul(&check, z) + if !check.Equal(x) { + return nil + } return z } +func lucasVFp(alpha *fr.Element) fr.Element { + var v0, v1, prod fr.Element + v0.Set(alpha) + v1.Square(alpha).Sub(&v1, &cbrtFpTwo) + for i := 27; i >= 1; i-- { + bit := (cbrtE2MuLucasExponent >> uint(i)) & 1 + prod.Mul(&v0, &v1).Sub(&prod, alpha) + if bit == 0 { + v1.Set(&prod) + v0.Square(&v0).Sub(&v0, &cbrtFpTwo) + } else { + v0.Set(&prod) + v1.Square(&v1).Sub(&v1, &cbrtFpTwo) + } + } + var out fr.Element + out.Mul(&v0, &v1).Sub(&out, alpha) + return out +} + // BatchInvertE2 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. // diff --git a/field/koalabear/extensions/e2_test.go b/field/koalabear/extensions/e2_test.go index a1064e2f1b..a51f00bb64 100644 --- a/field/koalabear/extensions/e2_test.go +++ b/field/koalabear/extensions/e2_test.go @@ -544,3 +544,29 @@ func genE2() gopter.Gen { return E2{A0: values[0].(fr.Element), A1: values[1].(fr.Element)} }) } + +func TestE2CbrtOnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check E2 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + if got.Cbrt(&x) == nil { + t.Fatal("expected cubic residue to have a cube root") + } + check.Square(&got).Mul(&check, &got) + if !check.Equal(&x) { + t.Fatal("returned cube root does not verify") + } + } +} + +func TestE2CbrtRejectsNonResidues(t *testing.T) { + var x, got E2 + for i := 0; i < 256; i++ { + x.MustSetRandom() + if got.Cbrt(&x) == nil { + return + } + } + t.Fatal("failed to find an E2 non-cube in 256 samples") +} diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index 6f346fe64c..7eafd4ecaa 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -12,6 +12,16 @@ import ( fr "github.com/consensys/gnark-crypto/field/koalabear" ) +var cbrtE4Exponent big.Int + +func init() { + cbrtE4Exponent.Exp(fr.Modulus(), big.NewInt(4), nil) + cbrtE4Exponent.Sub(&cbrtE4Exponent, big.NewInt(1)) + cbrtE4Exponent.Div(&cbrtE4Exponent, big.NewInt(3)) + three := new(big.Int).SetUint64(3) + cbrtE4Exponent.ModInverse(three, &cbrtE4Exponent) +} + // q + r'.r = 1, i.e., qInvNeg = - q⁻¹ mod r // used for Montgomery reduction const qInvNeg = 2130706431 @@ -427,6 +437,38 @@ func (z *E4) Sqrt(x *E4) *E4 { return z } +// Cbrt sets z to the cube root of x and returns z. +// It returns nil if x is not a cubic residue. +func (z *E4) Cbrt(x *E4) *E4 { + z.Exp(*x, &cbrtE4Exponent) + return cbrtVerifyAndAdjustE4(z, x) +} + +func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { + var check E4 + check.Square(z).Mul(&check, z) + if check.Equal(x) { + return z + } + + var y E4 + y.B0.Mul(&z.B0, &cbrtE2Omega) + y.B1.Mul(&z.B1, &cbrtE2Omega) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + y.B0.Mul(&z.B0, &cbrtE2Omega2) + y.B1.Mul(&z.B1, &cbrtE2Omega2) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + return nil +} + // BatchInvertE4 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. // diff --git a/field/koalabear/extensions/e4_test.go b/field/koalabear/extensions/e4_test.go index 0cb81af01c..9c1abee758 100644 --- a/field/koalabear/extensions/e4_test.go +++ b/field/koalabear/extensions/e4_test.go @@ -1044,3 +1044,25 @@ func genFrVector(size int) gopter.Gen { return gopter.NewGenResult(v, gopter.NoShrinker) } } + +func TestE4CbrtOnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check E4 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + require.NotNil(t, got.Cbrt(&x)) + check.Square(&got).Mul(&check, &got) + require.True(t, check.Equal(&x)) + } +} + +func TestE4CbrtRejectsNonResidues(t *testing.T) { + var x, got E4 + for i := 0; i < 256; i++ { + x.MustSetRandom() + if got.Cbrt(&x) == nil { + return + } + } + t.Fatal("failed to find an E4 non-cube in 256 samples") +} diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index d45c54cb85..93c672d615 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -12,6 +12,23 @@ import ( fr "github.com/consensys/gnark-crypto/field/koalabear" ) +var ( + cbrtE8Exponent big.Int + cbrtE8Omega E8 + cbrtE8Omega2 E8 +) + +func init() { + cbrtE8Exponent.Exp(fr.Modulus(), big.NewInt(8), nil) + cbrtE8Exponent.Sub(&cbrtE8Exponent, big.NewInt(1)) + cbrtE8Exponent.Div(&cbrtE8Exponent, big.NewInt(3)) + three := new(big.Int).SetUint64(3) + cbrtE8Exponent.ModInverse(three, &cbrtE8Exponent) + + cbrtE8Omega.C0.B0 = cbrtE2Omega + cbrtE8Omega2.Square(&cbrtE8Omega) +} + // E8 is a degree two finite field extension of E4. type E8 struct { C0, C1 E4 @@ -270,6 +287,35 @@ func (z *E8) Sqrt(x *E8) *E8 { return z } +// Cbrt sets z to the cube root of x and returns z. +// It returns nil if x is not a cubic residue. +func (z *E8) Cbrt(x *E8) *E8 { + z.Exp(*x, &cbrtE8Exponent) + return cbrtVerifyAndAdjustE8(z, x) +} + +func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { + var check, y E8 + check.Square(z).Mul(&check, z) + if check.Equal(x) { + return z + } + + y.Mul(z, &cbrtE8Omega) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + y.Mul(z, &cbrtE8Omega2) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + return nil +} + // BatchInvertE8 returns a new slice with every element in a inverted. func BatchInvertE8(a []E8) []E8 { res := make([]E8, len(a)) diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go index 6dfd0fe6f8..3b56d58958 100644 --- a/field/koalabear/extensions/e8_test.go +++ b/field/koalabear/extensions/e8_test.go @@ -11,6 +11,7 @@ import ( "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" + "github.com/stretchr/testify/require" ) func TestE8ReceiverIsOperand(t *testing.T) { @@ -173,3 +174,25 @@ func genE8() gopter.Gen { return E8{C0: values[0].(E4), C1: values[1].(E4)} }) } + +func TestE8CbrtOnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check E8 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + require.NotNil(t, got.Cbrt(&x)) + check.Square(&got).Mul(&check, &got) + require.True(t, check.Equal(&x)) + } +} + +func TestE8CbrtRejectsNonResidues(t *testing.T) { + var x, got E8 + for i := 0; i < 256; i++ { + x.MustSetRandom() + if got.Cbrt(&x) == nil { + return + } + } + t.Fatal("failed to find an E8 non-cube in 256 samples") +} From ba5549b3ec1de6108d25c53fe14eae1ac56edad1 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 15 Apr 2026 00:06:28 -0400 Subject: [PATCH 06/33] refactor: move code to fields/ --- ecc/kb8/multiset-hash/cardano.go | 3 +- field/koalabear/extensions/e2.go | 2 + field/koalabear/extensions/e4.go | 100 ++++++++++++++++++++++++++++- field/koalabear/extensions/e8.go | 104 +++++++++++++++++++++++++++++-- 4 files changed, 202 insertions(+), 7 deletions(-) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index c9fb346cb5..f09161d3ec 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -195,8 +195,7 @@ func isDepressedCubicRoot(x, negCHalf *extensions.E8) bool { } func cbrtE8(z, x *extensions.E8) *extensions.E8 { - expByKBE8Cbrt(z, x) - return cbrtVerifyAndAdjustE8(z, x) + return z.Cbrt(x) } func cbrtVerifyAndAdjustE8(z, x *extensions.E8) *extensions.E8 { diff --git a/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index 4c8131add4..579bf45fe2 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -19,6 +19,7 @@ var ( cbrtE2One E2 cbrtE2Omega E2 cbrtE2Omega2 E2 + cbrtE2NRInv E2 ) var cbrtE2MuLucasExponent uint64 = 473490319 @@ -30,6 +31,7 @@ func init() { cbrtFpThreeInv.Inverse(&cbrtFpThree) cbrtE2One.SetOne() + cbrtE2NRInv.MulByNonResidueInv(&cbrtE2One) var sqrtMinusThree E2 sqrtMinusThree.A0.Neg(&cbrtFpThree) sqrtMinusThree.Sqrt(&sqrtMinusThree) diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index 7eafd4ecaa..8d82a50b95 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -12,9 +12,20 @@ import ( fr "github.com/consensys/gnark-crypto/field/koalabear" ) -var cbrtE4Exponent big.Int +var ( + cbrtE4One E4 + cbrtE4NRInv E4 + cbrtE4Exponent big.Int +) + +var cbrtE4LucasExponent = [1]uint64{ + 1513303301209194497, +} func init() { + cbrtE4One.SetOne() + cbrtE4NRInv.B1.SetOne() + cbrtE4NRInv.Inverse(&cbrtE4NRInv) cbrtE4Exponent.Exp(fr.Modulus(), big.NewInt(4), nil) cbrtE4Exponent.Sub(&cbrtE4Exponent, big.NewInt(1)) cbrtE4Exponent.Div(&cbrtE4Exponent, big.NewInt(3)) @@ -440,6 +451,65 @@ func (z *E4) Sqrt(x *E4) *E4 { // Cbrt sets z to the cube root of x and returns z. // It returns nil if x is not a cubic residue. func (z *E4) Cbrt(x *E4) *E4 { + if x.B1.IsZero() { + if z.B0.Cbrt(&x.B0) == nil { + return nil + } + z.B1.SetZero() + return z + } + + if x.B0.IsZero() { + var y E4 + var x1OverNR E2 + x1OverNR.Mul(&x.B1, &cbrtE2NRInv) + if y.B1.Cbrt(&x1OverNR) == nil { + return nil + } + y.B0.SetZero() + if out := cbrtVerifyAndAdjustE4(z.Set(&y), x); out != nil { + return out + } + z.Exp(*x, &cbrtE4Exponent) + return cbrtVerifyAndAdjustE4(z, x) + } + + var x0sq, x1sq, betaX1sq, norm E2 + x0sq.Square(&x.B0) + x1sq.Square(&x.B1) + betaX1sq.MulByNonResidue(&x1sq) + norm.Sub(&x0sq, &betaX1sq) + + var m, normInv E2 + if m.Cbrt(&norm) == nil { + return nil + } + normInv.Inverse(&norm) + + var halfTau, tau E2 + halfTau.Add(&x0sq, &betaX1sq) + halfTau.Mul(&halfTau, &normInv) + tau.Double(&halfTau) + + sigma, _ := lucasV2E2Cbrt(&tau) + + var sigmaM1, sigmaP1, d0, d1, d0d1, d0d1Inv E2 + sigmaM1.Sub(&sigma, &cbrtE2One) + sigmaP1.Add(&sigma, &cbrtE2One) + d0.Mul(&m, &sigmaM1) + d1.Mul(&m, &sigmaP1) + d0d1.Mul(&d0, &d1) + if d0d1.IsZero() { + return nil + } + d0d1Inv.Inverse(&d0d1) + + var y E4 + y.B0.Mul(&d1, &d0d1Inv).Mul(&y.B0, &x.B0) + y.B1.Mul(&d0, &d0d1Inv).Mul(&y.B1, &x.B1) + if out := cbrtVerifyAndAdjustE4(z.Set(&y), x); out != nil { + return out + } z.Exp(*x, &cbrtE4Exponent) return cbrtVerifyAndAdjustE4(z, x) } @@ -469,6 +539,34 @@ func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { return nil } +func lucasV2E2Cbrt(alpha *E2) (E2, E2) { + var t0, t1, tmp E2 + t0.Set(&cbrtE2One) + t1.Set(alpha) + + for i := 59; i >= 1; i-- { + bit := (cbrtE4LucasExponent[0] >> uint(i)) & 1 + if bit == 0 { + tmp.Mul(&t0, &t1).Sub(&tmp, alpha) + t0.Square(&t0).Sub(&t0, &cbrtE2One).Sub(&t0, &cbrtE2One) + t1.Set(&tmp) + } else { + tmp.Mul(&t0, &t1).Sub(&tmp, alpha) + t1.Square(&t1).Sub(&t1, &cbrtE2One).Sub(&t1, &cbrtE2One) + t0.Set(&tmp) + } + } + + tmp.Mul(&t0, &t1).Sub(&tmp, alpha) + t0.Square(&t0).Sub(&t0, &cbrtE2One).Sub(&t0, &cbrtE2One) + t1.Set(&tmp) + tmp.Mul(alpha, &t1).Sub(&tmp, &t0) + t0.Set(&t1) + t1.Set(&tmp) + + return t0, t1 +} + // BatchInvertE4 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. // diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index 93c672d615..a72c54863c 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -13,20 +13,29 @@ import ( ) var ( - cbrtE8Exponent big.Int + cbrtE8One E8 + cbrtE8NRInv E8 cbrtE8Omega E8 cbrtE8Omega2 E8 + cbrtE8Exponent big.Int ) +var cbrtE8LucasExponent = [2]uint64{ + 2930905110336765953, + 372437575807401643, +} + func init() { + cbrtE8One.SetOne() + cbrtE8NRInv.C1.SetOne() + cbrtE8NRInv.Inverse(&cbrtE8NRInv) + cbrtE8Omega.C0.B0 = cbrtE2Omega + cbrtE8Omega2.Square(&cbrtE8Omega) cbrtE8Exponent.Exp(fr.Modulus(), big.NewInt(8), nil) cbrtE8Exponent.Sub(&cbrtE8Exponent, big.NewInt(1)) cbrtE8Exponent.Div(&cbrtE8Exponent, big.NewInt(3)) three := new(big.Int).SetUint64(3) cbrtE8Exponent.ModInverse(three, &cbrtE8Exponent) - - cbrtE8Omega.C0.B0 = cbrtE2Omega - cbrtE8Omega2.Square(&cbrtE8Omega) } // E8 is a degree two finite field extension of E4. @@ -290,6 +299,65 @@ func (z *E8) Sqrt(x *E8) *E8 { // Cbrt sets z to the cube root of x and returns z. // It returns nil if x is not a cubic residue. func (z *E8) Cbrt(x *E8) *E8 { + if x.C1.IsZero() { + if z.C0.Cbrt(&x.C0) == nil { + return nil + } + z.C1.SetZero() + return z + } + + if x.C0.IsZero() { + var y E8 + var x1OverNR E4 + x1OverNR.Mul(&x.C1, &cbrtE4NRInv) + if y.C1.Cbrt(&x1OverNR) == nil { + return nil + } + y.C0.SetZero() + if out := cbrtVerifyAndAdjustE8(z.Set(&y), x); out != nil { + return out + } + z.Exp(*x, &cbrtE8Exponent) + return cbrtVerifyAndAdjustE8(z, x) + } + + var x0sq, x1sq, betaX1sq, norm E4 + x0sq.Square(&x.C0) + x1sq.Square(&x.C1) + betaX1sq.MulByNonResidue(&x1sq) + norm.Sub(&x0sq, &betaX1sq) + + var m, normInv E4 + if m.Cbrt(&norm) == nil { + return nil + } + normInv.Inverse(&norm) + + var halfTau, tau E4 + halfTau.Add(&x0sq, &betaX1sq) + halfTau.Mul(&halfTau, &normInv) + tau.Double(&halfTau) + + sigma, _ := lucasV2E4Cbrt(&tau) + + var sigmaM1, sigmaP1, d0, d1, d0d1, d0d1Inv E4 + sigmaM1.Sub(&sigma, &cbrtE4One) + sigmaP1.Add(&sigma, &cbrtE4One) + d0.Mul(&m, &sigmaM1) + d1.Mul(&m, &sigmaP1) + d0d1.Mul(&d0, &d1) + if d0d1.IsZero() { + return nil + } + d0d1Inv.Inverse(&d0d1) + + var y E8 + y.C0.Mul(&d1, &d0d1Inv).Mul(&y.C0, &x.C0) + y.C1.Mul(&d0, &d0d1Inv).Mul(&y.C1, &x.C1) + if out := cbrtVerifyAndAdjustE8(z.Set(&y), x); out != nil { + return out + } z.Exp(*x, &cbrtE8Exponent) return cbrtVerifyAndAdjustE8(z, x) } @@ -316,6 +384,34 @@ func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { return nil } +func lucasV2E4Cbrt(alpha *E4) (E4, E4) { + var t0, t1, tmp E4 + t0.Set(&cbrtE4One) + t1.Set(alpha) + + for i := 121; i >= 1; i-- { + bit := (cbrtE8LucasExponent[i/64] >> uint(i%64)) & 1 + if bit == 0 { + tmp.Mul(&t0, &t1).Sub(&tmp, alpha) + t0.Square(&t0).Sub(&t0, &cbrtE4One).Sub(&t0, &cbrtE4One) + t1.Set(&tmp) + } else { + tmp.Mul(&t0, &t1).Sub(&tmp, alpha) + t1.Square(&t1).Sub(&t1, &cbrtE4One).Sub(&t1, &cbrtE4One) + t0.Set(&tmp) + } + } + + tmp.Mul(&t0, &t1).Sub(&tmp, alpha) + t0.Square(&t0).Sub(&t0, &cbrtE4One).Sub(&t0, &cbrtE4One) + t1.Set(&tmp) + tmp.Mul(alpha, &t1).Sub(&tmp, &t0) + t0.Set(&t1) + t1.Set(&tmp) + + return t0, t1 +} + // BatchInvertE8 returns a new slice with every element in a inverted. func BatchInvertE8(a []E8) []E8 { res := make([]E8, len(a)) From 7d968a8258aca3322c777658c366a8de0cdc0657 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 15 Apr 2026 00:27:46 -0400 Subject: [PATCH 07/33] perf: use fused exp and inverse-free recovery --- field/koalabear/element_exp.go | 61 +++++++++++++++ field/koalabear/extensions/e2.go | 103 ++++++++++++++++++++++---- field/koalabear/extensions/e2_test.go | 13 ++++ field/koalabear/extensions/e4_test.go | 13 ++++ field/koalabear/extensions/e8_test.go | 13 ++++ 5 files changed, 190 insertions(+), 13 deletions(-) diff --git a/field/koalabear/element_exp.go b/field/koalabear/element_exp.go index ae81160fb4..256225a975 100644 --- a/field/koalabear/element_exp.go +++ b/field/koalabear/element_exp.go @@ -129,3 +129,64 @@ func (z *Element) ExpByCbrt2q1o3(x Element) *Element { return z } + +// ExpByCbrtHelperQMinus2Div9 is equivalent to z.Exp(x, e1c71c7). +// It raises x to the (q-2)/9 power using a shorter addition chain. +// +// uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain +func (z *Element) ExpByCbrtHelperQMinus2Div9(x Element) *Element { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _110 = 2*_11 + // _111 = 1 + _110 + // i10 = _111 << 6 + // i11 = _111 + i10 + // return ((i10 + i11) << 6 + _111) << 12 + i11 + // + // Operations: 30 squares 6 multiplies + var t0, t1, t2 Element + + // Step 1: z = x^0x2 + z.Square(&x) + + // Step 2: t0 = x^0x3 + t0.Mul(&x, z) + + // Step 3: t1 = x^0x6 + t1.Square(&t0) + + // Step 4: t0 = x^0x7 + t0.Mul(&x, &t1) + + // Step 10: t2 = x^0x1c0 + t2.Set(&t0) + for range 6 { + t2.Square(&t2) + } + + // Step 11: t1 = x^0x1c7 + t1.Mul(&t0, &t2) + + // Step 12: z = x^0x387 + z.Mul(&t2, &t1) + + // Step 18: z = x^0xe1c0 + for range 6 { + z.Square(z) + } + + // Step 19: z = x^0xe1c7 + z.Mul(z, &t0) + + // Step 31: z = x^0xe1c7000 + for range 12 { + z.Square(z) + } + + // Step 32: z = x^0xe1c71c7 + z.Mul(z, &t1) + + return z +} diff --git a/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index 579bf45fe2..482c6e085e 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -271,17 +271,52 @@ func (z *E2) Cbrt(x *E2) *E2 { betaX1sq.Mul(&x1sq, &cbrtFpThree) norm.Sub(&x0sq, &betaX1sq) - var m, normInv fr.Element - m.Cbrt(&norm) - normInv.Inverse(&norm) - - var tau fr.Element - tau.Add(&x0sq, &betaX1sq) - tau.Double(&tau) - tau.Mul(&tau, &normInv) + m, normInv, deltaInv, ok := cbrtAndNormInverseE2(&norm, &x0sq, &x1sq) + if !ok { + return nil + } - sigma := lucasVFp(&tau) + var halfTau, tau fr.Element + halfTau.Add(&x0sq, &betaX1sq) + halfTau.Mul(&halfTau, &normInv) + tau.Double(&halfTau) + + Te, Te1 := lucasV2Fp(&tau) + + var x0x1, imY fr.Element + x0x1.Mul(&x.A0, &x.A1) + imY.Double(&x0x1).Neg(&imY) + imY.Mul(&imY, &normInv) + + var WA0, WA1 fr.Element + WA0.Mul(&halfTau, &Te) + WA0.Sub(&Te1, &WA0) + WA1.Mul(&imY, &Te) + + var sIm, k fr.Element + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) + + var gamma0, gamma1 fr.Element + gamma0.Mul(&WA1, &k).Mul(&gamma0, &cbrtFpThree) + gamma1.Mul(&WA0, &k) + + var mInv fr.Element + mInv.Square(&m).Mul(&mInv, &normInv) + + var t1, t2 fr.Element + t1.Mul(&x.A0, &gamma0) + t2.Mul(&x.A1, &gamma1).Mul(&t2, &cbrtFpThree) + z.A0.Sub(&t1, &t2).Mul(&z.A0, &mInv) + t1.Mul(&x.A1, &gamma0) + t2.Mul(&x.A0, &gamma1) + z.A1.Sub(&t1, &t2).Mul(&z.A1, &mInv) + if out := cbrtVerifyE2(z, x); out != nil { + return out + } + var sigma fr.Element + sigma.Set(&Te) var d0, d1, d0d1, d0d1Inv fr.Element d0.Sub(&sigma, &cbrtFpOne) d0.Mul(&m, &d0) @@ -298,6 +333,47 @@ func (z *E2) Cbrt(x *E2) *E2 { return cbrtVerifyE2(z, x) } +func cbrtAndNormInverseE2(norm, x0sq, x1sq *fr.Element) (m, normInv, deltaInv fr.Element, ok bool) { + // Hamburg trick for KoalaBear (p ≡ 2 mod 9): + // with w = U^3 * norm and t = w^((p-2)/9), we have + // w^(-1) = t^9 and cbrt(w) = w * t^6. + var U, x0x1, U2, U3, w fr.Element + x0x1.Mul(x0sq, x1sq) + U.Mul(&x0x1, norm) + U.Double(&U).Double(&U) + U.Double(&U) + U.Mul(&U, &cbrtFpThree) + U2.Square(&U) + U3.Mul(&U2, &U) + w.Mul(&U3, norm) + + var t, t2, t4, t6, t8, t9 fr.Element + t.ExpByCbrtHelperQMinus2Div9(w) + t2.Square(&t) + t4.Square(&t2) + t6.Mul(&t4, &t2) + t8.Square(&t4) + t9.Mul(&t8, &t) + + var cbrtW, UInv fr.Element + cbrtW.Mul(&w, &t6) + UInv.Mul(&U2, norm).Mul(&UInv, &t9) + m.Mul(&cbrtW, &UInv) + normInv.Mul(&U3, &t9) + + var check fr.Element + check.Square(&m).Mul(&check, &m) + if !check.Equal(norm) { + return m, normInv, deltaInv, false + } + + var norm2, norm3 fr.Element + norm2.Square(norm) + norm3.Mul(&norm2, norm) + deltaInv.Mul(&norm3, &UInv) + return m, normInv, deltaInv, true +} + func cbrtVerifyE2(z, x *E2) *E2 { var check E2 check.Square(z).Mul(&check, z) @@ -307,7 +383,7 @@ func cbrtVerifyE2(z, x *E2) *E2 { return z } -func lucasVFp(alpha *fr.Element) fr.Element { +func lucasV2Fp(alpha *fr.Element) (fr.Element, fr.Element) { var v0, v1, prod fr.Element v0.Set(alpha) v1.Square(alpha).Sub(&v1, &cbrtFpTwo) @@ -322,9 +398,10 @@ func lucasVFp(alpha *fr.Element) fr.Element { v1.Square(&v1).Sub(&v1, &cbrtFpTwo) } } - var out fr.Element - out.Mul(&v0, &v1).Sub(&out, alpha) - return out + var te, te1 fr.Element + te.Mul(&v0, &v1).Sub(&te, alpha) + te1.Square(&v1).Sub(&te1, &cbrtFpTwo) + return te, te1 } // BatchInvertE2 returns a new slice with every element in a inverted. diff --git a/field/koalabear/extensions/e2_test.go b/field/koalabear/extensions/e2_test.go index a51f00bb64..1b62f6a5b2 100644 --- a/field/koalabear/extensions/e2_test.go +++ b/field/koalabear/extensions/e2_test.go @@ -570,3 +570,16 @@ func TestE2CbrtRejectsNonResidues(t *testing.T) { } t.Fatal("failed to find an E2 non-cube in 256 samples") } + +func BenchmarkE2Cbrt(b *testing.B) { + var a, x E2 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + var z E2 + b.ResetTimer() + for i := 0; i < b.N; i++ { + if z.Cbrt(&x) == nil { + b.Fatal("expected cubic residue to have a cube root") + } + } +} diff --git a/field/koalabear/extensions/e4_test.go b/field/koalabear/extensions/e4_test.go index 9c1abee758..679e531852 100644 --- a/field/koalabear/extensions/e4_test.go +++ b/field/koalabear/extensions/e4_test.go @@ -1066,3 +1066,16 @@ func TestE4CbrtRejectsNonResidues(t *testing.T) { } t.Fatal("failed to find an E4 non-cube in 256 samples") } + +func BenchmarkE4Cbrt(b *testing.B) { + var a, x E4 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + var z E4 + b.ResetTimer() + for i := 0; i < b.N; i++ { + if z.Cbrt(&x) == nil { + b.Fatal("expected cubic residue to have a cube root") + } + } +} diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go index 3b56d58958..5a94367d09 100644 --- a/field/koalabear/extensions/e8_test.go +++ b/field/koalabear/extensions/e8_test.go @@ -196,3 +196,16 @@ func TestE8CbrtRejectsNonResidues(t *testing.T) { } t.Fatal("failed to find an E8 non-cube in 256 samples") } + +func BenchmarkE8Cbrt(b *testing.B) { + var a, x E8 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + var z E8 + b.ResetTimer() + for i := 0; i < b.N; i++ { + if z.Cbrt(&x) == nil { + b.Fatal("expected cubic residue to have a cube root") + } + } +} From b3c96a9d92cd59e1658af15141626144b5972c01 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 15 Apr 2026 10:37:40 -0400 Subject: [PATCH 08/33] perf: recursive Cbrt and fused 2 ops --- ecc/kb8/multiset-hash/cardano.go | 95 ++++++++++++++++--- field/koalabear/extensions/e2.go | 112 +++++++++++++++++++++++ field/koalabear/extensions/e4.go | 130 ++++++++++++++++++-------- field/koalabear/extensions/e8.go | 152 ++++++++++++++++++++++--------- 4 files changed, 396 insertions(+), 93 deletions(-) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index f09161d3ec..63ba64ab80 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -23,6 +23,7 @@ var ( e8Four extensions.E8 e8TwentySeven extensions.E8 e8NegThree extensions.E8 + e16HelperExp big.Int ) var e16LucasExponent = [4]uint64{ @@ -43,6 +44,9 @@ func init() { e8Beta = findNonSquare() e8BetaInv.Inverse(&e8Beta) e8Omega = findPrimitiveCubeRoot() + e16HelperExp.Exp(koalabear.Modulus(), big.NewInt(8), nil) + e16HelperExp.Sub(&e16HelperExp, big.NewInt(4)) + e16HelperExp.Div(&e16HelperExp, big.NewInt(9)) } func depressedCubicRoot(c extensions.E8) (extensions.E8, bool) { @@ -1069,22 +1073,89 @@ func (z *e16) Cbrt(x *e16) *e16 { halfTau.Mul(&halfTau, &normInv) tau.Double(&halfTau) - sigma, _ := lucasV2E8(&tau) + te, te1 := lucasV2E8(&tau) - var sigmaM1, sigmaP1, d0, d1, d0d1, d0d1Inv extensions.E8 - sigmaM1.Sub(&sigma, &e8One) - sigmaP1.Add(&sigma, &e8One) - d0.Mul(&m, &sigmaM1) - d1.Mul(&m, &sigmaP1) - d0d1.Mul(&d0, &d1) - if d0d1.IsZero() { + var x0x1, imY extensions.E8 + x0x1.Mul(&x.A0, &x.A1) + imY.Double(&x0x1) + imY.Mul(&imY, &normInv) + + var wa0, wa1 extensions.E8 + wa0.Mul(&halfTau, &te) + wa0.Sub(&te1, &wa0) + wa1.Mul(&imY, &te) + + var delta, deltaInv, sIm, k extensions.E8 + delta.Square(&tau).Sub(&delta, &e8One).Sub(&delta, &e8One).Sub(&delta, &e8One).Sub(&delta, &e8One) + if delta.IsZero() { return nil } - d0d1Inv.Inverse(&d0d1) + deltaInv.Inverse(&delta) + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) - var y e16 - y.A0.Mul(&d1, &d0d1Inv).Mul(&y.A0, &x.A0) - y.A1.Mul(&d0, &d0d1Inv).Mul(&y.A1, &x.A1) + var gamma0, gamma1, mInv extensions.E8 + gamma0.Mul(&wa1, &k).Mul(&gamma0, &e8Beta) + gamma1.Mul(&wa0, &k) + mInv.Square(&m).Mul(&mInv, &normInv) + var y e16 + var t1, t2 extensions.E8 + t1.Mul(&x.A0, &gamma0) + t2.Mul(&x.A1, &gamma1).Mul(&t2, &e8Beta) + y.A0.Add(&t1, &t2).Mul(&y.A0, &mInv) + t1.Mul(&x.A1, &gamma0) + t2.Mul(&x.A0, &gamma1) + y.A1.Add(&t1, &t2).Mul(&y.A1, &mInv) return cbrtVerifyAndAdjustE16(z.Set(&y), x) } + +func cbrtAndNormInverseE16(norm, x0sq, x1sq *extensions.E8) (m, normInv, deltaInv extensions.E8, ok bool) { + var x0x1, betaX0x1, U, U2, U3, w extensions.E8 + x0x1.Mul(x0sq, x1sq) + betaX0x1.Mul(&x0x1, &e8Beta) + U.Mul(&betaX0x1, norm) + U.Double(&U).Double(&U) + U.Double(&U).Double(&U) + U2.Square(&U) + U3.Mul(&U2, &U) + w.Mul(&U3, norm) + + var t, t2, t4, t5, cbrtW, cw2, wInv extensions.E8 + t.Exp(w, &e16HelperExp) + t2.Square(&t) + t4.Square(&t2) + t5.Mul(&t4, &t) + cbrtW.Mul(&w, &t2) + cw2.Square(&cbrtW) + wInv.Mul(&t5, &cw2) + + var UInv, check extensions.E8 + UInv.Mul(&U2, norm).Mul(&UInv, &wInv) + m.Mul(&cbrtW, &UInv) + normInv.Mul(&U3, &wInv) + check.Square(&m).Mul(&check, &m) + if !check.Equal(norm) { + var alt extensions.E8 + alt.Mul(&m, &e8Omega) + check.Square(&alt).Mul(&check, &alt) + if check.Equal(norm) { + m.Set(&alt) + } else { + var omega2 extensions.E8 + omega2.Square(&e8Omega) + alt.Mul(&m, &omega2) + check.Square(&alt).Mul(&check, &alt) + if !check.Equal(norm) { + return m, normInv, deltaInv, false + } + m.Set(&alt) + } + } + + var norm2, norm3 extensions.E8 + norm2.Square(norm) + norm3.Mul(&norm2, norm) + deltaInv.Mul(&norm3, &UInv) + return m, normInv, deltaInv, true +} diff --git a/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index 482c6e085e..fcb26239aa 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -225,6 +225,118 @@ func (z *E2) Exp(x E2, k *big.Int) *E2 { return z } +// ExpByCbrtHelperQ2Minus4Div9 is equivalent to z.Exp(x, 7001c71e3555555). +// It raises x to the (q²-4)/9 power over E2 using a shorter addition chain. +// +// uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain +func (z *E2) ExpByCbrtHelperQ2Minus4Div9(x E2) *E2 { + // addition chain: + // + // _10 = 2*1 + // _11 = 1 + _10 + // _110 = 2*_11 + // _111 = 1 + _110 + // _1110 = 2*_111 + // _11100 = 2*_1110 + // _111000 = 2*_11100 + // _1010100 = _11100 + _111000 + // _1010101 = 1 + _1010100 + // _1100011 = _1110 + _1010101 + // _1110000 = _11100 + _1010100 + // i35 = (_1110000 << 14 + _1100011 + _1110) << 7 + _1100011 + // i60 = ((i35 << 7 + _1100011) << 8 + _1010101) << 8 + // return (_1010101 + i60) << 8 + _1010101 + // + // Operations: 64 squares 10 multiplies + var t0, t1, t2, t3 E2 + + // Step 1: z = x^0x2 + z.Square(&x) + + // Step 2: z = x^0x3 + z.Mul(&x, z) + + // Step 3: z = x^0x6 + z.Square(z) + + // Step 4: z = x^0x7 + z.Mul(&x, z) + + // Step 5: t1 = x^0xe + t1.Square(z) + + // Step 6: t2 = x^0x1c + t2.Square(&t1) + + // Step 7: z = x^0x38 + z.Square(&t2) + + // Step 8: t3 = x^0x54 + t3.Mul(&t2, z) + + // Step 9: z = x^0x55 + z.Mul(&x, &t3) + + // Step 10: t0 = x^0x63 + t0.Mul(&t1, z) + + // Step 11: t2 = x^0x70 + t2.Mul(&t2, &t3) + + // Step 25: t2 = x^0x1c00000 + for range 14 { + t2.Square(&t2) + } + + // Step 26: t2 = x^0x1c00063 + t2.Mul(&t0, &t2) + + // Step 27: t1 = x^0x1c00071 + t1.Mul(&t1, &t2) + + // Step 34: t1 = x^0xe0003880 + for range 7 { + t1.Square(&t1) + } + + // Step 35: t1 = x^0xe00038e3 + t1.Mul(&t0, &t1) + + // Step 42: t1 = x^0x70001c7180 + for range 7 { + t1.Square(&t1) + } + + // Step 43: t0 = x^0x70001c71e3 + t0.Mul(&t0, &t1) + + // Step 51: t0 = x^0x70001c71e300 + for range 8 { + t0.Square(&t0) + } + + // Step 52: t0 = x^0x70001c71e355 + t0.Mul(z, &t0) + + // Step 60: t0 = x^0x70001c71e35500 + for range 8 { + t0.Square(&t0) + } + + // Step 61: t0 = x^0x70001c71e35555 + t0.Mul(z, &t0) + + // Step 69: t0 = x^0x70001c71e3555500 + for range 8 { + t0.Square(&t0) + } + + // Step 70: z = x^0x7001c71e3555555 + z.Mul(z, &t0) + + return z +} + // Sqrt sets z to the square root of and returns z // The function does not test whether the square root // exists or not, it's up to the caller to call diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index 8d82a50b95..c70a47ef55 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -15,6 +15,8 @@ import ( var ( cbrtE4One E4 cbrtE4NRInv E4 + cbrtE4Omega E4 + cbrtE4Omega2 E4 cbrtE4Exponent big.Int ) @@ -26,6 +28,8 @@ func init() { cbrtE4One.SetOne() cbrtE4NRInv.B1.SetOne() cbrtE4NRInv.Inverse(&cbrtE4NRInv) + cbrtE4Omega.B0 = cbrtE2Omega + cbrtE4Omega2.Square(&cbrtE4Omega) cbrtE4Exponent.Exp(fr.Modulus(), big.NewInt(4), nil) cbrtE4Exponent.Sub(&cbrtE4Exponent, big.NewInt(1)) cbrtE4Exponent.Div(&cbrtE4Exponent, big.NewInt(3)) @@ -467,11 +471,7 @@ func (z *E4) Cbrt(x *E4) *E4 { return nil } y.B0.SetZero() - if out := cbrtVerifyAndAdjustE4(z.Set(&y), x); out != nil { - return out - } - z.Exp(*x, &cbrtE4Exponent) - return cbrtVerifyAndAdjustE4(z, x) + return cbrtVerifyAndAdjustE4(z.Set(&y), x) } var x0sq, x1sq, betaX1sq, norm E2 @@ -491,27 +491,84 @@ func (z *E4) Cbrt(x *E4) *E4 { halfTau.Mul(&halfTau, &normInv) tau.Double(&halfTau) - sigma, _ := lucasV2E2Cbrt(&tau) + te, te1 := lucasV2E2Cbrt(&tau) + + var x0x1, imY E2 + x0x1.Mul(&x.B0, &x.B1) + imY.Double(&x0x1) + imY.Mul(&imY, &normInv) + + var wa0, wa1 E2 + wa0.Mul(&halfTau, &te) + wa0.Sub(&te1, &wa0) + wa1.Mul(&imY, &te) - var sigmaM1, sigmaP1, d0, d1, d0d1, d0d1Inv E2 - sigmaM1.Sub(&sigma, &cbrtE2One) - sigmaP1.Add(&sigma, &cbrtE2One) - d0.Mul(&m, &sigmaM1) - d1.Mul(&m, &sigmaP1) - d0d1.Mul(&d0, &d1) - if d0d1.IsZero() { + var delta, deltaInv, sIm, k E2 + delta.Square(&tau).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One) + if delta.IsZero() { return nil } - d0d1Inv.Inverse(&d0d1) + deltaInv.Inverse(&delta) + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) + + var gamma0, gamma1 E2 + gamma0.Mul(&wa1, &k) + gamma0.MulByNonResidue(&gamma0) + gamma1.Mul(&wa0, &k) + + var mInv E2 + mInv.Square(&m).Mul(&mInv, &normInv) var y E4 - y.B0.Mul(&d1, &d0d1Inv).Mul(&y.B0, &x.B0) - y.B1.Mul(&d0, &d0d1Inv).Mul(&y.B1, &x.B1) - if out := cbrtVerifyAndAdjustE4(z.Set(&y), x); out != nil { - return out + var t1, t2 E2 + t1.Mul(&x.B0, &gamma0) + t2.Mul(&x.B1, &gamma1) + t2.MulByNonResidue(&t2) + y.B0.Sub(&t1, &t2).Mul(&y.B0, &mInv) + t1.Mul(&x.B1, &gamma0) + t2.Mul(&x.B0, &gamma1) + y.B1.Sub(&t1, &t2).Mul(&y.B1, &mInv) + return cbrtVerifyAndAdjustE4(z.Set(&y), x) +} + +func cbrtAndNormInverseE4(norm, x0sq, x1sq *E2) (m, normInv, deltaInv E2, ok bool) { + // Hamburg trick for E4 over E2, where |E2| ≡ 4 mod 9: + // with w = U^3 * norm and t = w^((q-4)/9), we have + // cbrt(w) = w * t^2 and w^(-1) = t^5 * cbrt(w)^2. + var x0x1, betaX0x1, U, U2, U3, w E2 + x0x1.Mul(x0sq, x1sq) + betaX0x1.MulByNonResidue(&x0x1) + U.Mul(&betaX0x1, norm) + U.Double(&U).Double(&U) + U.Double(&U).Double(&U) + U2.Square(&U) + U3.Mul(&U2, &U) + w.Mul(&U3, norm) + + var t, t2, t4, t5, cbrtW, cw2, wInv E2 + t.ExpByCbrtHelperQ2Minus4Div9(w) + t2.Square(&t) + cbrtW.Mul(&w, &t2) + cw2.Square(&cbrtW) + t4.Square(&t2) + t5.Mul(&t4, &t) + wInv.Mul(&t5, &cw2) + + var check, UInv E2 + UInv.Mul(&U2, norm).Mul(&UInv, &wInv) + m.Mul(&cbrtW, &UInv) + normInv.Mul(&U3, &wInv) + check.Square(&m).Mul(&check, &m) + if !check.Equal(norm) { + return m, normInv, deltaInv, false } - z.Exp(*x, &cbrtE4Exponent) - return cbrtVerifyAndAdjustE4(z, x) + + var norm2, norm3 E2 + norm2.Square(norm) + norm3.Mul(&norm2, norm) + deltaInv.Mul(&norm3, &UInv) + return m, normInv, deltaInv, true } func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { @@ -540,31 +597,28 @@ func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { } func lucasV2E2Cbrt(alpha *E2) (E2, E2) { - var t0, t1, tmp E2 - t0.Set(&cbrtE2One) - t1.Set(alpha) - + var v0, v1, prod E2 + var two E2 + two.A0.SetUint64(2) + v0.Set(alpha) + v1.Square(alpha).Sub(&v1, &two) for i := 59; i >= 1; i-- { bit := (cbrtE4LucasExponent[0] >> uint(i)) & 1 if bit == 0 { - tmp.Mul(&t0, &t1).Sub(&tmp, alpha) - t0.Square(&t0).Sub(&t0, &cbrtE2One).Sub(&t0, &cbrtE2One) - t1.Set(&tmp) + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v1.Set(&prod) + v0.Square(&v0).Sub(&v0, &two) } else { - tmp.Mul(&t0, &t1).Sub(&tmp, alpha) - t1.Square(&t1).Sub(&t1, &cbrtE2One).Sub(&t1, &cbrtE2One) - t0.Set(&tmp) + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v0.Set(&prod) + v1.Square(&v1).Sub(&v1, &two) } } - tmp.Mul(&t0, &t1).Sub(&tmp, alpha) - t0.Square(&t0).Sub(&t0, &cbrtE2One).Sub(&t0, &cbrtE2One) - t1.Set(&tmp) - tmp.Mul(alpha, &t1).Sub(&tmp, &t0) - t0.Set(&t1) - t1.Set(&tmp) - - return t0, t1 + var te, te1 E2 + te.Mul(&v0, &v1).Sub(&te, alpha) + te1.Square(&v1).Sub(&te1, &two) + return te, te1 } // BatchInvertE4 returns a new slice with every element in a inverted. diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index a72c54863c..78a87af26e 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -13,11 +13,12 @@ import ( ) var ( - cbrtE8One E8 - cbrtE8NRInv E8 - cbrtE8Omega E8 - cbrtE8Omega2 E8 - cbrtE8Exponent big.Int + cbrtE8One E8 + cbrtE8NRInv E8 + cbrtE8Omega E8 + cbrtE8Omega2 E8 + cbrtE8Exponent big.Int + cbrtE8HelperExponent big.Int ) var cbrtE8LucasExponent = [2]uint64{ @@ -36,6 +37,9 @@ func init() { cbrtE8Exponent.Div(&cbrtE8Exponent, big.NewInt(3)) three := new(big.Int).SetUint64(3) cbrtE8Exponent.ModInverse(three, &cbrtE8Exponent) + cbrtE8HelperExponent.Exp(fr.Modulus(), big.NewInt(4), nil) + cbrtE8HelperExponent.Sub(&cbrtE8HelperExponent, big.NewInt(16)) + cbrtE8HelperExponent.Div(&cbrtE8HelperExponent, big.NewInt(27)) } // E8 is a degree two finite field extension of E4. @@ -315,11 +319,7 @@ func (z *E8) Cbrt(x *E8) *E8 { return nil } y.C0.SetZero() - if out := cbrtVerifyAndAdjustE8(z.Set(&y), x); out != nil { - return out - } - z.Exp(*x, &cbrtE8Exponent) - return cbrtVerifyAndAdjustE8(z, x) + return cbrtVerifyAndAdjustE8(z.Set(&y), x) } var x0sq, x1sq, betaX1sq, norm E4 @@ -339,27 +339,96 @@ func (z *E8) Cbrt(x *E8) *E8 { halfTau.Mul(&halfTau, &normInv) tau.Double(&halfTau) - sigma, _ := lucasV2E4Cbrt(&tau) + te, te1 := lucasV2E4Cbrt(&tau) + + var x0x1, imY E4 + x0x1.Mul(&x.C0, &x.C1) + imY.Double(&x0x1) + imY.Mul(&imY, &normInv) + + var wa0, wa1 E4 + wa0.Mul(&halfTau, &te) + wa0.Sub(&te1, &wa0) + wa1.Mul(&imY, &te) - var sigmaM1, sigmaP1, d0, d1, d0d1, d0d1Inv E4 - sigmaM1.Sub(&sigma, &cbrtE4One) - sigmaP1.Add(&sigma, &cbrtE4One) - d0.Mul(&m, &sigmaM1) - d1.Mul(&m, &sigmaP1) - d0d1.Mul(&d0, &d1) - if d0d1.IsZero() { + var delta, deltaInv, sIm, k E4 + delta.Square(&tau).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One) + if delta.IsZero() { return nil } - d0d1Inv.Inverse(&d0d1) + deltaInv.Inverse(&delta) + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) + + var gamma0, gamma1 E4 + gamma0.Mul(&wa1, &k) + gamma0.MulByNonResidue(&gamma0) + gamma1.Mul(&wa0, &k) + + var mInv E4 + mInv.Square(&m).Mul(&mInv, &normInv) var y E8 - y.C0.Mul(&d1, &d0d1Inv).Mul(&y.C0, &x.C0) - y.C1.Mul(&d0, &d0d1Inv).Mul(&y.C1, &x.C1) - if out := cbrtVerifyAndAdjustE8(z.Set(&y), x); out != nil { - return out + var t1, t2 E4 + t1.Mul(&x.C0, &gamma0) + t2.Mul(&x.C1, &gamma1) + t2.MulByNonResidue(&t2) + y.C0.Sub(&t1, &t2).Mul(&y.C0, &mInv) + t1.Mul(&x.C1, &gamma0) + t2.Mul(&x.C0, &gamma1) + y.C1.Sub(&t1, &t2).Mul(&y.C1, &mInv) + return cbrtVerifyAndAdjustE8(z.Set(&y), x) +} + +func cbrtAndNormInverseE8(norm, x0sq, x1sq *E4) (m, normInv, deltaInv E4, ok bool) { + var x0x1, betaX0x1, U, U2, U3, w E4 + x0x1.Mul(x0sq, x1sq) + betaX0x1.MulByNonResidue(&x0x1) + U.Mul(&betaX0x1, norm) + U.Double(&U).Double(&U) + U.Double(&U).Double(&U) + U2.Square(&U) + U3.Mul(&U2, &U) + w.Mul(&U3, norm) + + var t, t2, t3, t6, w2, cbrtW, c2, c4, c7, wInv E4 + t.Exp(w, &cbrtE8HelperExponent) + t2.Square(&t) + t3.Mul(&t2, &t) + t6.Square(&t3) + w2.Square(&w) + cbrtW.Mul(&w2, &t3) + c2.Square(&cbrtW) + c4.Square(&c2) + c7.Mul(&c4, &c2).Mul(&c7, &cbrtW) + wInv.Mul(&t6, &c7) + + var UInv, check E4 + UInv.Mul(&U2, norm).Mul(&UInv, &wInv) + m.Mul(&cbrtW, &UInv) + normInv.Mul(&U3, &wInv) + check.Square(&m).Mul(&check, &m) + if !check.Equal(norm) { + var alt E4 + alt.Mul(&m, &cbrtE4Omega) + check.Square(&alt).Mul(&check, &alt) + if check.Equal(norm) { + m.Set(&alt) + } else { + alt.Mul(&m, &cbrtE4Omega2) + check.Square(&alt).Mul(&check, &alt) + if !check.Equal(norm) { + return m, normInv, deltaInv, false + } + m.Set(&alt) + } } - z.Exp(*x, &cbrtE8Exponent) - return cbrtVerifyAndAdjustE8(z, x) + + var norm2, norm3 E4 + norm2.Square(norm) + norm3.Mul(&norm2, norm) + deltaInv.Mul(&norm3, &UInv) + return m, normInv, deltaInv, true } func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { @@ -385,31 +454,28 @@ func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { } func lucasV2E4Cbrt(alpha *E4) (E4, E4) { - var t0, t1, tmp E4 - t0.Set(&cbrtE4One) - t1.Set(alpha) - + var v0, v1, prod E4 + var two E4 + two.B0.A0.SetUint64(2) + v0.Set(alpha) + v1.Square(alpha).Sub(&v1, &two) for i := 121; i >= 1; i-- { bit := (cbrtE8LucasExponent[i/64] >> uint(i%64)) & 1 if bit == 0 { - tmp.Mul(&t0, &t1).Sub(&tmp, alpha) - t0.Square(&t0).Sub(&t0, &cbrtE4One).Sub(&t0, &cbrtE4One) - t1.Set(&tmp) + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v1.Set(&prod) + v0.Square(&v0).Sub(&v0, &two) } else { - tmp.Mul(&t0, &t1).Sub(&tmp, alpha) - t1.Square(&t1).Sub(&t1, &cbrtE4One).Sub(&t1, &cbrtE4One) - t0.Set(&tmp) + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v0.Set(&prod) + v1.Square(&v1).Sub(&v1, &two) } } - tmp.Mul(&t0, &t1).Sub(&tmp, alpha) - t0.Square(&t0).Sub(&t0, &cbrtE4One).Sub(&t0, &cbrtE4One) - t1.Set(&tmp) - tmp.Mul(alpha, &t1).Sub(&tmp, &t0) - t0.Set(&t1) - t1.Set(&tmp) - - return t0, t1 + var te, te1 E4 + te.Mul(&v0, &v1).Sub(&te, alpha) + te1.Square(&v1).Sub(&te1, &two) + return te, te1 } // BatchInvertE8 returns a new slice with every element in a inverted. From c28b0cf0607cb5fafb87c6003122b3c066b90d8e Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 15 Apr 2026 10:41:21 -0400 Subject: [PATCH 09/33] perf: use addchains in upper layers --- ecc/kb8/multiset-hash/cardano.go | 177 ++++++++++++++++++++++++++++++- field/koalabear/extensions/e4.go | 79 ++++++++++++++ field/koalabear/extensions/e8.go | 16 ++- 3 files changed, 257 insertions(+), 15 deletions(-) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index 63ba64ab80..6aab9a7a1c 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -23,7 +23,6 @@ var ( e8Four extensions.E8 e8TwentySeven extensions.E8 e8NegThree extensions.E8 - e16HelperExp big.Int ) var e16LucasExponent = [4]uint64{ @@ -44,9 +43,6 @@ func init() { e8Beta = findNonSquare() e8BetaInv.Inverse(&e8Beta) e8Omega = findPrimitiveCubeRoot() - e16HelperExp.Exp(koalabear.Modulus(), big.NewInt(8), nil) - e16HelperExp.Sub(&e16HelperExp, big.NewInt(4)) - e16HelperExp.Div(&e16HelperExp, big.NewInt(9)) } func depressedCubicRoot(c extensions.E8) (extensions.E8, bool) { @@ -449,6 +445,177 @@ func expByKBE8Cbrt(z, x *extensions.E8) *extensions.E8 { return z } +func expByKBE8Helper(z, x *extensions.E8) *extensions.E8 { + var ( + t0 extensions.E8 + t1 extensions.E8 + t2 extensions.E8 + t3 extensions.E8 + t4 extensions.E8 + t5 extensions.E8 + t6 extensions.E8 + t7 extensions.E8 + t8 extensions.E8 + t9 extensions.E8 + t10 extensions.E8 + t11 extensions.E8 + t12 extensions.E8 + t13 extensions.E8 + t14 extensions.E8 + t15 extensions.E8 + t16 extensions.E8 + t17 extensions.E8 + t18 extensions.E8 + t19 extensions.E8 + t20 extensions.E8 + t21 extensions.E8 + t22 extensions.E8 + ) + + t1.Square(x) + t9.Square(&t1) + z.Mul(x, &t9) + t3.Mul(x, z) + t4.Mul(&t1, &t3) + t7.Mul(&t1, &t4) + t6.Mul(&t1, &t7) + t2.Mul(&t7, &t6) + t0.Mul(&t3, &t2) + t8.Mul(&t1, &t0) + t13.Mul(x, &t8) + t5.Mul(&t6, &t13) + t17.Mul(&t0, &t5) + t18.Mul(&t9, &t17) + t16.Mul(&t4, &t18) + t0.Mul(&t1, &t16) + t20.Mul(&t9, &t0) + t8.Mul(&t8, &t20) + t10.Mul(&t1, &t8) + t2.Mul(&t2, &t10) + t9.Mul(&t3, &t2) + t19.Mul(&t4, &t9) + t12.Mul(&t4, &t19) + t11.Mul(&t7, &t12) + t21.Mul(&t4, &t11) + t6.Mul(&t6, &t21) + t15.Mul(&t4, &t6) + t7.Mul(&t1, &t15) + t14.Mul(&t1, &t7) + t1.Mul(&t3, &t14) + t4.Mul(&t4, &t1) + t22.Mul(&t7, &t4) + for range 7 { + t22.Square(&t22) + } + t21.Mul(&t21, &t22) + for range 8 { + t21.Square(&t21) + } + t20.Mul(&t20, &t21) + for range 2 { + t20.Square(&t20) + } + t20.Mul(x, &t20) + for range 16 { + t20.Square(&t20) + } + t19.Mul(&t19, &t20) + for range 11 { + t19.Square(&t19) + } + t18.Mul(&t18, &t19) + for range 9 { + t18.Square(&t18) + } + t17.Mul(&t17, &t18) + for range 8 { + t17.Square(&t17) + } + t16.Mul(&t16, &t17) + for range 11 { + t16.Square(&t16) + } + t15.Mul(&t15, &t16) + for range 9 { + t15.Square(&t15) + } + t14.Mul(&t14, &t15) + for range 6 { + t14.Square(&t14) + } + t13.Mul(&t13, &t14) + for range 15 { + t13.Square(&t13) + } + t12.Mul(&t12, &t13) + for range 8 { + t12.Square(&t12) + } + t12.Mul(&t9, &t12) + for range 9 { + t12.Square(&t12) + } + t11.Mul(&t11, &t12) + for range 8 { + t11.Square(&t11) + } + t10.Mul(&t10, &t11) + for range 9 { + t10.Square(&t10) + } + t9.Mul(&t9, &t10) + for range 8 { + t9.Square(&t9) + } + t9.Mul(&t4, &t9) + for range 7 { + t9.Square(&t9) + } + t8.Mul(&t8, &t9) + for range 9 { + t8.Square(&t8) + } + t7.Mul(&t7, &t8) + for range 8 { + t7.Square(&t7) + } + t7.Mul(&t1, &t7) + for range 9 { + t7.Square(&t7) + } + t6.Mul(&t6, &t7) + for range 8 { + t6.Square(&t6) + } + t5.Mul(&t5, &t6) + for range 12 { + t5.Square(&t5) + } + t4.Mul(&t4, &t5) + t3.Mul(&t3, &t4) + for range 8 { + t3.Square(&t3) + } + t2.Mul(&t2, &t3) + for range 11 { + t2.Square(&t2) + } + t1.Mul(&t1, &t2) + for range 8 { + t1.Square(&t1) + } + t1.Mul(&t0, &t1) + for range 8 { + t1.Square(&t1) + } + t0.Mul(&t0, &t1) + for range 4 { + t0.Square(&t0) + } + z.Mul(z, &t0) + return z +} + func lucasV2E8(alpha *extensions.E8) (extensions.E8, extensions.E8) { var v0, v1, two extensions.E8 two.C0.B0.A0.SetUint64(2) @@ -1122,7 +1289,7 @@ func cbrtAndNormInverseE16(norm, x0sq, x1sq *extensions.E8) (m, normInv, deltaIn w.Mul(&U3, norm) var t, t2, t4, t5, cbrtW, cw2, wInv extensions.E8 - t.Exp(w, &e16HelperExp) + expByKBE8Helper(&t, &w) t2.Square(&t) t4.Square(&t2) t5.Mul(&t4, &t) diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index c70a47ef55..959e0461a4 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -399,6 +399,85 @@ func (z *E4) ExpInt64(x E4, k int64) *E4 { return z } +// ExpByCbrtHelperQ4Minus16Div27 is equivalent to z.Exp(x, 9304aab8c7552f764c12f6978e38e3). +// It raises x to the (q^4-16)/27 power over E4 using a shorter addition chain. +// +// uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain +func (z *E4) ExpByCbrtHelperQ4Minus16Div27(x E4) *E4 { + var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9 E4 + + t1.Square(&x) + t3.Square(&t1) + t6.Mul(&t1, &t3) + z.Mul(&t3, &t6) + t0.Mul(&x, z) + t5.Mul(&t3, z) + t4.Mul(&t3, &t5) + t2.Mul(&t6, &t4) + t7.Mul(&t6, &t2) + t8.Square(&t7) + t0.Mul(&t0, &t8) + t5.Mul(&t5, &t0) + t7.Mul(&t1, &t5) + t9.Mul(&t8, &t7) + t8.Mul(&t1, &t9) + t1.Mul(&t4, &t9) + t2.Mul(&t2, &t1) + t3.Mul(&t3, &t2) + t6.Mul(&t6, &t3) + t4.Mul(&t4, &t6) + z.Mul(z, &t4) + for range 13 { + t9.Square(&t9) + } + t8.Mul(&t8, &t9) + for range 8 { + t8.Square(&t8) + } + t7.Mul(&t7, &t8) + for range 11 { + t7.Square(&t7) + } + t6.Mul(&t6, &t7) + for range 8 { + t6.Square(&t6) + } + t5.Mul(&t5, &t6) + for range 10 { + t5.Square(&t5) + } + t5.Mul(&t2, &t5) + for range 8 { + t5.Square(&t5) + } + t4.Mul(&t4, &t5) + for range 10 { + t4.Square(&t4) + } + t3.Mul(&t3, &t4) + for range 10 { + t3.Square(&t3) + } + t2.Mul(&t2, &t3) + for range 8 { + t2.Square(&t2) + } + t1.Mul(&t1, &t2) + for range 8 { + t1.Square(&t1) + } + t1.Mul(z, &t1) + for range 7 { + t1.Square(&t1) + } + t0.Mul(&t0, &t1) + for range 11 { + t0.Square(&t0) + } + z.Mul(z, &t0) + return z +} + // Conjugate sets z to x conjugated and returns z func (z *E4) Conjugate(x *E4) *E4 { z.B0 = x.B0 diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index 78a87af26e..be60a7b758 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -13,12 +13,11 @@ import ( ) var ( - cbrtE8One E8 - cbrtE8NRInv E8 - cbrtE8Omega E8 - cbrtE8Omega2 E8 - cbrtE8Exponent big.Int - cbrtE8HelperExponent big.Int + cbrtE8One E8 + cbrtE8NRInv E8 + cbrtE8Omega E8 + cbrtE8Omega2 E8 + cbrtE8Exponent big.Int ) var cbrtE8LucasExponent = [2]uint64{ @@ -37,9 +36,6 @@ func init() { cbrtE8Exponent.Div(&cbrtE8Exponent, big.NewInt(3)) three := new(big.Int).SetUint64(3) cbrtE8Exponent.ModInverse(three, &cbrtE8Exponent) - cbrtE8HelperExponent.Exp(fr.Modulus(), big.NewInt(4), nil) - cbrtE8HelperExponent.Sub(&cbrtE8HelperExponent, big.NewInt(16)) - cbrtE8HelperExponent.Div(&cbrtE8HelperExponent, big.NewInt(27)) } // E8 is a degree two finite field extension of E4. @@ -392,7 +388,7 @@ func cbrtAndNormInverseE8(norm, x0sq, x1sq *E4) (m, normInv, deltaInv E4, ok boo w.Mul(&U3, norm) var t, t2, t3, t6, w2, cbrtW, c2, c4, c7, wInv E4 - t.Exp(w, &cbrtE8HelperExponent) + t.ExpByCbrtHelperQ4Minus16Div27(w) t2.Square(&t) t3.Mul(&t2, &t) t6.Square(&t3) From d542bcd4f14b385410bd79c359f788d7c7a9de74 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Fri, 17 Apr 2026 13:06:17 -0400 Subject: [PATCH 10/33] perf: fixed regression --- ecc/kb8/multiset-hash/cardano.go | 17 +++- ecc/kb8/multiset-hash/cardano_test.go | 9 ++ field/koalabear/extensions/e4.go | 113 +++++++++++++++++++++++++- field/koalabear/extensions/e4_test.go | 21 +++++ field/koalabear/extensions/e8.go | 113 +++++++++++++++++++++++++- field/koalabear/extensions/e8_test.go | 21 +++++ 6 files changed, 287 insertions(+), 7 deletions(-) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index 6aab9a7a1c..7433ad7533 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -40,7 +40,7 @@ func init() { e8TwentySeven.C0.B0.A0.SetUint64(27) e8NegThree.Set(&e8Three).Neg(&e8NegThree) - e8Beta = findNonSquare() + e8Beta.C1.SetOne() e8BetaInv.Inverse(&e8Beta) e8Omega = findPrimitiveCubeRoot() } @@ -746,6 +746,12 @@ func (z *e16) Set(x *e16) *e16 { return z } +func (z *e16) SetOne() *e16 { + z.A0.SetOne() + z.A1.SetZero() + return z +} + func (z *e16) Add(x, y *e16) *e16 { z.A0.Add(&x.A0, &y.A0) z.A1.Add(&x.A1, &y.A1) @@ -771,6 +777,12 @@ func (z *e16) Square(x *e16) *e16 { return z.Mul(x, x) } +func (z *e16) Conjugate(x *e16) *e16 { + z.A0.Set(&x.A0) + z.A1.Neg(&x.A1) + return z +} + func (z *e16) Inverse(x *e16) *e16 { var t0, t1, denom extensions.E8 t0.Square(&x.A0) @@ -1240,12 +1252,11 @@ func (z *e16) Cbrt(x *e16) *e16 { halfTau.Mul(&halfTau, &normInv) tau.Double(&halfTau) - te, te1 := lucasV2E8(&tau) - var x0x1, imY extensions.E8 x0x1.Mul(&x.A0, &x.A1) imY.Double(&x0x1) imY.Mul(&imY, &normInv) + te, te1 := lucasV2E8(&tau) var wa0, wa1 extensions.E8 wa0.Mul(&halfTau, &te) diff --git a/ecc/kb8/multiset-hash/cardano_test.go b/ecc/kb8/multiset-hash/cardano_test.go index 6ad75fc5d2..c99580ff39 100644 --- a/ecc/kb8/multiset-hash/cardano_test.go +++ b/ecc/kb8/multiset-hash/cardano_test.go @@ -54,6 +54,15 @@ func TestE16CbrtRejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E16 non-cube in 256 samples") } +func TestE16GLVTraceMatchesBinaryLucas(t *testing.T) { + for i := 0; i < 16; i++ { + var tau extensions.E8 + tau.MustSetRandom() + gotTe, gotTe1 := lucasV2E8(&tau) + require.False(t, gotTe.IsZero() && gotTe1.IsZero()) + } +} + func TestDepressedCubicRootFindsValidRoot(t *testing.T) { for i := 0; i < 64; i++ { var x, x3, c, lhs extensions.E8 diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index 959e0461a4..7b7acbeee6 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -9,6 +9,7 @@ import ( "math/big" "math/bits" + "github.com/consensys/gnark-crypto/ecc" fr "github.com/consensys/gnark-crypto/field/koalabear" ) @@ -18,6 +19,9 @@ var ( cbrtE4Omega E4 cbrtE4Omega2 E4 cbrtE4Exponent big.Int + cbrtE4PhiCoeff E2 + cbrtE4GLVBasis ecc.Lattice + cbrtE4GLVSplit [2]big.Int ) var cbrtE4LucasExponent = [1]uint64{ @@ -35,6 +39,21 @@ func init() { cbrtE4Exponent.Div(&cbrtE4Exponent, big.NewInt(3)) three := new(big.Int).SetUint64(3) cbrtE4Exponent.ModInverse(three, &cbrtE4Exponent) + + var minusOne E2 + minusOne.A0.SetOne() + minusOne.A0.Neg(&minusOne.A0) + if cbrtE4PhiCoeff.Sqrt(&minusOne) == nil { + panic("koalabear/e4: failed to find Frobenius coefficient") + } + + var modulus, lambda, exponent big.Int + modulus.Exp(fr.Modulus(), big.NewInt(2), nil) + modulus.Add(&modulus, big.NewInt(1)) + lambda.Set(fr.Modulus()) + exponent.ModInverse(three, &modulus) + ecc.PrecomputeLattice(&modulus, &lambda, &cbrtE4GLVBasis) + cbrtE4GLVSplit = ecc.SplitScalar(&exponent, &cbrtE4GLVBasis) } // q + r'.r = 1, i.e., qInvNeg = - q⁻¹ mod r @@ -570,13 +589,16 @@ func (z *E4) Cbrt(x *E4) *E4 { halfTau.Mul(&halfTau, &normInv) tau.Double(&halfTau) - te, te1 := lucasV2E2Cbrt(&tau) - var x0x1, imY E2 x0x1.Mul(&x.B0, &x.B1) imY.Double(&x0x1) imY.Mul(&imY, &normInv) + var alpha E4 + alpha.B0.Set(&halfTau) + alpha.B1.Neg(&imY) + te, te1 := glvTraceE4(&alpha) + var wa0, wa1 E2 wa0.Mul(&halfTau, &te) wa0.Sub(&te1, &wa0) @@ -700,6 +722,93 @@ func lucasV2E2Cbrt(alpha *E2) (E2, E2) { return te, te1 } +func (z *E4) phiCbrt(x *E4) *E4 { + z.B0.Conjugate(&x.B0) + z.B1.Conjugate(&x.B1).Mul(&z.B1, &cbrtE4PhiCoeff) + return z +} + +func expTorusGLVE4(z, x *E4) *E4 { + var res, q1, q2 E4 + q1.Set(x) + q2.phiCbrt(x) + res.SetOne() + + var k0, k1 big.Int + k0.Set(&cbrtE4GLVSplit[0]) + k1.Set(&cbrtE4GLVSplit[1]) + if k0.Sign() < 0 { + k0.Neg(&k0) + q1.Conjugate(&q1) + } + if k1.Sign() < 0 { + k1.Neg(&k1) + q2.Conjugate(&q2) + } + + const wnafWindow = 5 + naf0 := make([]int8, k0.BitLen()+wnafWindow+1) + naf1 := make([]int8, k1.BitLen()+wnafWindow+1) + len0 := ecc.WnafDecomposition(&k0, wnafWindow, naf0) + len1 := ecc.WnafDecomposition(&k1, wnafWindow, naf1) + maxLen := max(len0, len1) + if maxLen == 0 { + return z.SetOne() + } + + var table0, table1 [8]E4 + var q0Two, q1Two E4 + table0[0].Set(&q1) + table1[0].Set(&q2) + q0Two.Square(&q1) + q1Two.Square(&q2) + for i := 1; i < len(table0); i++ { + table0[i].Mul(&table0[i-1], &q0Two) + table1[i].Mul(&table1[i-1], &q1Two) + } + + for i := maxLen - 1; i >= 0; i-- { + res.Square(&res) + if i < len0 { + d := naf0[i] + if d != 0 { + if d > 0 { + res.Mul(&res, &table0[(d-1)/2]) + } else { + var inv E4 + inv.Conjugate(&table0[(-d-1)/2]) + res.Mul(&res, &inv) + } + } + } + if i < len1 { + d := naf1[i] + if d != 0 { + if d > 0 { + res.Mul(&res, &table1[(d-1)/2]) + } else { + var inv E4 + inv.Conjugate(&table1[(-d-1)/2]) + res.Mul(&res, &inv) + } + } + } + } + + return z.Set(&res) +} + +func glvTraceE4(alpha *E4) (E2, E2) { + var sigma, sigmaAlpha E4 + expTorusGLVE4(&sigma, alpha) + sigmaAlpha.Mul(&sigma, alpha) + + var te, te1 E2 + te.Double(&sigma.B0) + te1.Double(&sigmaAlpha.B0) + return te, te1 +} + // BatchInvertE4 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. // diff --git a/field/koalabear/extensions/e4_test.go b/field/koalabear/extensions/e4_test.go index 679e531852..eef84c1ab6 100644 --- a/field/koalabear/extensions/e4_test.go +++ b/field/koalabear/extensions/e4_test.go @@ -1067,6 +1067,27 @@ func TestE4CbrtRejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E4 non-cube in 256 samples") } +func TestE4GLVTraceMatchesBinaryLucas(t *testing.T) { + for i := 0; i < 64; i++ { + var x, xInv, alpha E4 + x.MustSetRandom() + if x.IsZero() { + i-- + continue + } + xInv.Inverse(&x) + alpha.Conjugate(&x).Mul(&alpha, &xInv) + + var tau E2 + tau.Double(&alpha.B0) + + gotTe, gotTe1 := glvTraceE4(&alpha) + refTe, refTe1 := lucasV2E2Cbrt(&tau) + require.True(t, gotTe.Equal(&refTe)) + require.True(t, gotTe1.Equal(&refTe1)) + } +} + func BenchmarkE4Cbrt(b *testing.B) { var a, x E4 a.MustSetRandom() diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index be60a7b758..1ade4db5b5 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -9,6 +9,7 @@ import ( "math/big" "math/bits" + "github.com/consensys/gnark-crypto/ecc" fr "github.com/consensys/gnark-crypto/field/koalabear" ) @@ -18,6 +19,9 @@ var ( cbrtE8Omega E8 cbrtE8Omega2 E8 cbrtE8Exponent big.Int + cbrtE8PhiCoeff E4 + cbrtE8GLVBasis ecc.Lattice + cbrtE8GLVSplit [2]big.Int ) var cbrtE8LucasExponent = [2]uint64{ @@ -36,6 +40,21 @@ func init() { cbrtE8Exponent.Div(&cbrtE8Exponent, big.NewInt(3)) three := new(big.Int).SetUint64(3) cbrtE8Exponent.ModInverse(three, &cbrtE8Exponent) + + var minusOne E4 + minusOne.B0.A0.SetOne() + minusOne.B0.A0.Neg(&minusOne.B0.A0) + if cbrtE8PhiCoeff.Sqrt(&minusOne) == nil { + panic("koalabear/e8: failed to find Frobenius coefficient") + } + + var modulus, lambda, exponent big.Int + modulus.Exp(fr.Modulus(), big.NewInt(4), nil) + modulus.Add(&modulus, big.NewInt(1)) + lambda.Exp(fr.Modulus(), big.NewInt(2), nil) + exponent.ModInverse(three, &modulus) + ecc.PrecomputeLattice(&modulus, &lambda, &cbrtE8GLVBasis) + cbrtE8GLVSplit = ecc.SplitScalar(&exponent, &cbrtE8GLVBasis) } // E8 is a degree two finite field extension of E4. @@ -335,13 +354,16 @@ func (z *E8) Cbrt(x *E8) *E8 { halfTau.Mul(&halfTau, &normInv) tau.Double(&halfTau) - te, te1 := lucasV2E4Cbrt(&tau) - var x0x1, imY E4 x0x1.Mul(&x.C0, &x.C1) imY.Double(&x0x1) imY.Mul(&imY, &normInv) + var alpha E8 + alpha.C0.Set(&halfTau) + alpha.C1.Neg(&imY) + te, te1 := glvTraceE8(&alpha) + var wa0, wa1 E4 wa0.Mul(&halfTau, &te) wa0.Sub(&te1, &wa0) @@ -474,6 +496,93 @@ func lucasV2E4Cbrt(alpha *E4) (E4, E4) { return te, te1 } +func (z *E8) phiCbrt(x *E8) *E8 { + z.C0.Conjugate(&x.C0) + z.C1.Conjugate(&x.C1).Mul(&z.C1, &cbrtE8PhiCoeff) + return z +} + +func expTorusGLVE8(z, x *E8) *E8 { + var res, q1, q2 E8 + q1.Set(x) + q2.phiCbrt(x) + res.SetOne() + + var k0, k1 big.Int + k0.Set(&cbrtE8GLVSplit[0]) + k1.Set(&cbrtE8GLVSplit[1]) + if k0.Sign() < 0 { + k0.Neg(&k0) + q1.Conjugate(&q1) + } + if k1.Sign() < 0 { + k1.Neg(&k1) + q2.Conjugate(&q2) + } + + const wnafWindow = 5 + naf0 := make([]int8, k0.BitLen()+wnafWindow+1) + naf1 := make([]int8, k1.BitLen()+wnafWindow+1) + len0 := ecc.WnafDecomposition(&k0, wnafWindow, naf0) + len1 := ecc.WnafDecomposition(&k1, wnafWindow, naf1) + maxLen := max(len0, len1) + if maxLen == 0 { + return z.SetOne() + } + + var table0, table1 [8]E8 + var q0Two, q1Two E8 + table0[0].Set(&q1) + table1[0].Set(&q2) + q0Two.Square(&q1) + q1Two.Square(&q2) + for i := 1; i < len(table0); i++ { + table0[i].Mul(&table0[i-1], &q0Two) + table1[i].Mul(&table1[i-1], &q1Two) + } + + for i := maxLen - 1; i >= 0; i-- { + res.Square(&res) + if i < len0 { + d := naf0[i] + if d != 0 { + if d > 0 { + res.Mul(&res, &table0[(d-1)/2]) + } else { + var inv E8 + inv.Conjugate(&table0[(-d-1)/2]) + res.Mul(&res, &inv) + } + } + } + if i < len1 { + d := naf1[i] + if d != 0 { + if d > 0 { + res.Mul(&res, &table1[(d-1)/2]) + } else { + var inv E8 + inv.Conjugate(&table1[(-d-1)/2]) + res.Mul(&res, &inv) + } + } + } + } + + return z.Set(&res) +} + +func glvTraceE8(alpha *E8) (E4, E4) { + var sigma, sigmaAlpha E8 + expTorusGLVE8(&sigma, alpha) + sigmaAlpha.Mul(&sigma, alpha) + + var te, te1 E4 + te.Double(&sigma.C0) + te1.Double(&sigmaAlpha.C0) + return te, te1 +} + // BatchInvertE8 returns a new slice with every element in a inverted. func BatchInvertE8(a []E8) []E8 { res := make([]E8, len(a)) diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go index 5a94367d09..1f15c59b3d 100644 --- a/field/koalabear/extensions/e8_test.go +++ b/field/koalabear/extensions/e8_test.go @@ -197,6 +197,27 @@ func TestE8CbrtRejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E8 non-cube in 256 samples") } +func TestE8GLVTraceMatchesBinaryLucas(t *testing.T) { + for i := 0; i < 64; i++ { + var x, xInv, alpha E8 + x.MustSetRandom() + if x.IsZero() { + i-- + continue + } + xInv.Inverse(&x) + alpha.Conjugate(&x).Mul(&alpha, &xInv) + + var tau E4 + tau.Double(&alpha.C0) + + gotTe, gotTe1 := glvTraceE8(&alpha) + refTe, refTe1 := lucasV2E4Cbrt(&tau) + require.True(t, gotTe.Equal(&refTe)) + require.True(t, gotTe1.Equal(&refTe1)) + } +} + func BenchmarkE8Cbrt(b *testing.B) { var a, x E8 a.MustSetRandom() From be1029463ef07bc9e5c2d611e5f7c97062734e8a Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 21 Apr 2026 18:17:09 -0400 Subject: [PATCH 11/33] refactor: clean code --- ecc/bls12-377/g1.go | 2 +- ecc/bls12-377/g2.go | 2 +- ecc/bls12-377/marshal.go | 60 ++++- ecc/bls12-377/marshal_test.go | 10 - ecc/bls12-381/g1.go | 2 +- ecc/bls12-381/g2.go | 2 +- ecc/bls12-381/marshal.go | 60 ++++- ecc/bls12-381/marshal_test.go | 10 - ecc/bls24-315/g1.go | 2 +- ecc/bls24-315/g2.go | 2 +- ecc/bls24-315/marshal.go | 60 ++++- ecc/bls24-317/g1.go | 2 +- ecc/bls24-317/g2.go | 2 +- ecc/bls24-317/marshal.go | 60 ++++- ecc/bn254/g1.go | 2 +- ecc/bn254/g2.go | 2 +- ecc/bn254/marshal.go | 60 ++++- ecc/bn254/marshal_test.go | 10 - ecc/bw6-633/g1.go | 2 +- ecc/bw6-633/g2.go | 2 +- ecc/bw6-633/marshal.go | 60 ++++- ecc/bw6-761/g1.go | 2 +- ecc/bw6-761/g2.go | 2 +- ecc/bw6-761/marshal.go | 60 ++++- ecc/grumpkin/g1.go | 2 +- ecc/grumpkin/marshal.go | 8 +- ecc/grumpkin/marshal_test.go | 41 ---- ecc/kb8/marshal.go | 8 +- ecc/kb8/multiset-hash/cardano.go | 9 - ecc/kb8/multiset-hash/cardano_test.go | 9 - ecc/secp256k1/ecdsa/ecdsa.go | 2 +- ecc/secp256k1/g1.go | 2 +- ecc/secp256k1/marshal.go | 133 +++------- ecc/secp256k1/marshal_test.go | 102 -------- field/koalabear/extensions/e2.go | 112 --------- field/koalabear/extensions/e4.go | 229 +----------------- field/koalabear/extensions/e4_test.go | 21 -- field/koalabear/extensions/e8.go | 162 +------------ field/koalabear/extensions/e8_test.go | 21 -- internal/generator/ecc/generate.go | 6 +- .../generator/ecc/template/marshal.go.tmpl | 12 +- internal/generator/ecc/template/point.go.tmpl | 4 + .../ecc/template/tests/marshal.go.tmpl | 60 +++-- .../generator/ecdsa/template/ecdsa.go.tmpl | 4 +- .../generator/field/generator_extensions.go | 35 +-- internal/generator/field/generator_field.go | 4 +- internal/generator/main.go | 2 +- 47 files changed, 528 insertions(+), 938 deletions(-) diff --git a/ecc/bls12-377/g1.go b/ecc/bls12-377/g1.go index de321721a4..5a2d550bec 100644 --- a/ecc/bls12-377/g1.go +++ b/ecc/bls12-377/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls12-377/g2.go b/ecc/bls12-377/g2.go index 264ea5c139..16552bb4eb 100644 --- a/ecc/bls12-377/g2.go +++ b/ecc/bls12-377/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fptower.E2 + var left, right fptower.E2 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls12-377/marshal.go b/ecc/bls12-377/marshal.go index c089220ed8..b9bb501151 100644 --- a/ecc/bls12-377/marshal.go +++ b/ecc/bls12-377/marshal.go @@ -479,7 +479,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls12-377 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -513,6 +513,11 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -571,6 +576,27 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encode(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineCompressed]byte + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -583,7 +609,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls12-377 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -594,7 +620,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls12-377 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -628,6 +654,11 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -686,6 +717,27 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encodeRaw(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineUncompressed]byte + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) @@ -698,7 +750,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls12-377 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/bls12-377/marshal_test.go b/ecc/bls12-377/marshal_test.go index 4fc2420da9..84e710d59a 100644 --- a/ecc/bls12-377/marshal_test.go +++ b/ecc/bls12-377/marshal_test.go @@ -488,16 +488,6 @@ func GenE2() gopter.Gen { }) } -// GenE4 generates an fptower.E4 elmt -func GenE4() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E4 { - return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} - }) -} - // GenE6 generates an fptower.E6 elmt func GenE6() gopter.Gen { return gopter.CombineGens( diff --git a/ecc/bls12-381/g1.go b/ecc/bls12-381/g1.go index e17fec1c09..4c5bb4dd60 100644 --- a/ecc/bls12-381/g1.go +++ b/ecc/bls12-381/g1.go @@ -198,7 +198,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls12-381/g2.go b/ecc/bls12-381/g2.go index a3e036668a..f3b048ffaa 100644 --- a/ecc/bls12-381/g2.go +++ b/ecc/bls12-381/g2.go @@ -204,7 +204,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fptower.E2 + var left, right fptower.E2 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls12-381/marshal.go b/ecc/bls12-381/marshal.go index fc71a27cba..885228ca7c 100644 --- a/ecc/bls12-381/marshal.go +++ b/ecc/bls12-381/marshal.go @@ -479,7 +479,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls12-381 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -513,6 +513,11 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -571,6 +576,27 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encode(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineCompressed]byte + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -583,7 +609,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls12-381 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -594,7 +620,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls12-381 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -628,6 +654,11 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -686,6 +717,27 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encodeRaw(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineUncompressed]byte + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) @@ -698,7 +750,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls12-381 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/bls12-381/marshal_test.go b/ecc/bls12-381/marshal_test.go index 649d3059c4..ca519dd057 100644 --- a/ecc/bls12-381/marshal_test.go +++ b/ecc/bls12-381/marshal_test.go @@ -488,16 +488,6 @@ func GenE2() gopter.Gen { }) } -// GenE4 generates an fptower.E4 elmt -func GenE4() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E4 { - return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} - }) -} - // GenE6 generates an fptower.E6 elmt func GenE6() gopter.Gen { return gopter.CombineGens( diff --git a/ecc/bls24-315/g1.go b/ecc/bls24-315/g1.go index 840d51c5f8..5872bf92b5 100644 --- a/ecc/bls24-315/g1.go +++ b/ecc/bls24-315/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls24-315/g2.go b/ecc/bls24-315/g2.go index 304e1f8edc..06e71b8b09 100644 --- a/ecc/bls24-315/g2.go +++ b/ecc/bls24-315/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fptower.E4 + var left, right fptower.E4 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls24-315/marshal.go b/ecc/bls24-315/marshal.go index caae4d7bc6..55b26affb3 100644 --- a/ecc/bls24-315/marshal.go +++ b/ecc/bls24-315/marshal.go @@ -479,7 +479,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls24-315 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -513,6 +513,11 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -571,6 +576,27 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encode(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineCompressed]byte + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -583,7 +609,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls24-315 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -594,7 +620,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls24-315 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -628,6 +654,11 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -686,6 +717,27 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encodeRaw(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineUncompressed]byte + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) @@ -698,7 +750,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls24-315 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/bls24-317/g1.go b/ecc/bls24-317/g1.go index e7991ade97..e7af58ba0d 100644 --- a/ecc/bls24-317/g1.go +++ b/ecc/bls24-317/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bls24-317/g2.go b/ecc/bls24-317/g2.go index c349583df8..a2565b729f 100644 --- a/ecc/bls24-317/g2.go +++ b/ecc/bls24-317/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fptower.E4 + var left, right fptower.E4 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bls24-317/marshal.go b/ecc/bls24-317/marshal.go index 95fb4c7841..c74dfd058e 100644 --- a/ecc/bls24-317/marshal.go +++ b/ecc/bls24-317/marshal.go @@ -479,7 +479,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls24-317 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -513,6 +513,11 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -571,6 +576,27 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encode(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineCompressed]byte + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -583,7 +609,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls24-317 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -594,7 +620,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bls24-317 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -628,6 +654,11 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -686,6 +717,27 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encodeRaw(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineUncompressed]byte + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) @@ -698,7 +750,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bls24-317 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/bn254/g1.go b/ecc/bn254/g1.go index 5d3036a900..b8475c4dac 100644 --- a/ecc/bn254/g1.go +++ b/ecc/bn254/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bn254/g2.go b/ecc/bn254/g2.go index 65170bf8d4..badc36bf4f 100644 --- a/ecc/bn254/g2.go +++ b/ecc/bn254/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fptower.E2 + var left, right fptower.E2 left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bn254/marshal.go b/ecc/bn254/marshal.go index 864f834a5c..3c94ab4651 100644 --- a/ecc/bn254/marshal.go +++ b/ecc/bn254/marshal.go @@ -444,7 +444,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bn254 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -478,6 +478,11 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -536,6 +541,27 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encode(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineCompressed]byte + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -548,7 +574,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bn254 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -559,7 +585,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bn254 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -593,6 +619,11 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -651,6 +682,27 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encodeRaw(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineUncompressed]byte + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) @@ -663,7 +715,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bn254 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/bn254/marshal_test.go b/ecc/bn254/marshal_test.go index 17f011f793..53c079b254 100644 --- a/ecc/bn254/marshal_test.go +++ b/ecc/bn254/marshal_test.go @@ -448,16 +448,6 @@ func GenE2() gopter.Gen { }) } -// GenE4 generates an fptower.E4 elmt -func GenE4() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E4 { - return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} - }) -} - // GenE6 generates an fptower.E6 elmt func GenE6() gopter.Gen { return gopter.CombineGens( diff --git a/ecc/bw6-633/g1.go b/ecc/bw6-633/g1.go index 57651102d7..97fb84c9f9 100644 --- a/ecc/bw6-633/g1.go +++ b/ecc/bw6-633/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bw6-633/g2.go b/ecc/bw6-633/g2.go index 5319f4c82f..d3504e15e3 100644 --- a/ecc/bw6-633/g2.go +++ b/ecc/bw6-633/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bw6-633/marshal.go b/ecc/bw6-633/marshal.go index eb4f6ae4f8..ad5b5703d3 100644 --- a/ecc/bw6-633/marshal.go +++ b/ecc/bw6-633/marshal.go @@ -479,7 +479,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bw6-633 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -513,6 +513,11 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -571,6 +576,27 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encode(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineCompressed]byte + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -583,7 +609,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bw6-633 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -594,7 +620,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bw6-633 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -628,6 +654,11 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -686,6 +717,27 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encodeRaw(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineUncompressed]byte + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) @@ -698,7 +750,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bw6-633 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/bw6-761/g1.go b/ecc/bw6-761/g1.go index 42ec140dd2..3b46dbf5da 100644 --- a/ecc/bw6-761/g1.go +++ b/ecc/bw6-761/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/bw6-761/g2.go b/ecc/bw6-761/g2.go index 8f6357fbb2..48d19303bf 100644 --- a/ecc/bw6-761/g2.go +++ b/ecc/bw6-761/g2.go @@ -202,7 +202,7 @@ func (p *G2Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bTwistCurveCoeff) diff --git a/ecc/bw6-761/marshal.go b/ecc/bw6-761/marshal.go index 5c8e55e503..f59e87307a 100644 --- a/ecc/bw6-761/marshal.go +++ b/ecc/bw6-761/marshal.go @@ -479,7 +479,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bw6-761 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -513,6 +513,11 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -571,6 +576,27 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encode(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineCompressed]byte + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) @@ -583,7 +609,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bw6-761 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -594,7 +620,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("bw6-761 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -628,6 +654,11 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case *G2Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -686,6 +717,27 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + case *[]G2Affine: + return enc.encodeRaw(*t) + case []G2Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG2AffineUncompressed]byte + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) @@ -698,7 +750,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("bw6-761 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/grumpkin/g1.go b/ecc/grumpkin/g1.go index 688302f4ec..f81420f0e6 100644 --- a/ecc/grumpkin/g1.go +++ b/ecc/grumpkin/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/grumpkin/marshal.go b/ecc/grumpkin/marshal.go index 54767a4ce5..6e0529b1ad 100644 --- a/ecc/grumpkin/marshal.go +++ b/ecc/grumpkin/marshal.go @@ -360,7 +360,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("grumpkin encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -464,7 +464,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("grumpkin encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -475,7 +475,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("grumpkin encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -579,7 +579,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("grumpkin encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/grumpkin/marshal_test.go b/ecc/grumpkin/marshal_test.go index 4af4d5559a..479111f86a 100644 --- a/ecc/grumpkin/marshal_test.go +++ b/ecc/grumpkin/marshal_test.go @@ -305,47 +305,6 @@ func GenFp() gopter.Gen { } } -// GenE2 generates an fptower.E2 elmt -func GenE2() gopter.Gen { - return gopter.CombineGens( - GenFp(), - GenFp(), - ).Map(func(values []any) fptower.E2 { - return fptower.E2{A0: values[0].(fp.Element), A1: values[1].(fp.Element)} - }) -} - -// GenE4 generates an fptower.E4 elmt -func GenE4() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E4 { - return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} - }) -} - -// GenE6 generates an fptower.E6 elmt -func GenE6() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E6 { - return fptower.E6{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2), B2: values[2].(fptower.E2)} - }) -} - -// GenE12 generates an fptower.E6 elmt -func GenE12() gopter.Gen { - return gopter.CombineGens( - GenE6(), - GenE6(), - ).Map(func(values []any) fptower.E12 { - return fptower.E12{C0: values[0].(fptower.E6), C1: values[1].(fptower.E6)} - }) -} - // GenBigInt generates a big.Int func GenBigInt() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { diff --git a/ecc/kb8/marshal.go b/ecc/kb8/marshal.go index ae08c2e6ad..4931920ed7 100644 --- a/ecc/kb8/marshal.go +++ b/ecc/kb8/marshal.go @@ -378,7 +378,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("kb8 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -482,7 +482,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("kb8 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -493,7 +493,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("kb8 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -597,7 +597,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("kb8 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index 7433ad7533..f364fdc337 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -657,15 +657,6 @@ func findPrimitiveCubeRoot() extensions.E8 { panic("kb8 multiset hash: failed to find primitive cube root in Fp^8") } -func findNonSquare() extensions.E8 { - for _, candidate := range e8SearchCandidates() { - if !candidate.IsZero() && candidate.Legendre() == -1 { - return candidate - } - } - panic("kb8 multiset hash: failed to find quadratic non-residue in Fp^8") -} - func e8SearchCandidates() []extensions.E8 { const searchSpace = 6560 // 3^8 - 1 res := make([]extensions.E8, 0, searchSpace) diff --git a/ecc/kb8/multiset-hash/cardano_test.go b/ecc/kb8/multiset-hash/cardano_test.go index c99580ff39..6ad75fc5d2 100644 --- a/ecc/kb8/multiset-hash/cardano_test.go +++ b/ecc/kb8/multiset-hash/cardano_test.go @@ -54,15 +54,6 @@ func TestE16CbrtRejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E16 non-cube in 256 samples") } -func TestE16GLVTraceMatchesBinaryLucas(t *testing.T) { - for i := 0; i < 16; i++ { - var tau extensions.E8 - tau.MustSetRandom() - gotTe, gotTe1 := lucasV2E8(&tau) - require.False(t, gotTe.IsZero() && gotTe1.IsZero()) - } -} - func TestDepressedCubicRootFindsValidRoot(t *testing.T) { for i := 0; i < 64; i++ { var x, x3, c, lhs extensions.E8 diff --git a/ecc/secp256k1/ecdsa/ecdsa.go b/ecc/secp256k1/ecdsa/ecdsa.go index 9405dfe0f9..73c5ca8f1d 100644 --- a/ecc/secp256k1/ecdsa/ecdsa.go +++ b/ecc/secp256k1/ecdsa/ecdsa.go @@ -26,7 +26,7 @@ const ( sizeFr = fr.Bytes sizeFrBits = fr.Bits sizeFp = fp.Bytes - sizePublicKey = 2 * sizeFp + sizePublicKey = secp256k1.SizeOfG1AffineUncompressed sizePrivateKey = sizeFr + sizePublicKey sizeSignature = 2 * sizeFr ) diff --git a/ecc/secp256k1/g1.go b/ecc/secp256k1/g1.go index 89e32572c9..c9ac075f2c 100644 --- a/ecc/secp256k1/g1.go +++ b/ecc/secp256k1/g1.go @@ -197,7 +197,7 @@ func (p *G1Affine) IsOnCurve() bool { if p.IsInfinity() { return true } - var left, right, tmp fp.Element + var left, right fp.Element left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) right.Add(&right, &bCurveCoeff) diff --git a/ecc/secp256k1/marshal.go b/ecc/secp256k1/marshal.go index 1c408ca00d..0cf84fa87f 100644 --- a/ecc/secp256k1/marshal.go +++ b/ecc/secp256k1/marshal.go @@ -17,21 +17,13 @@ import ( "github.com/consensys/gnark-crypto/parallel" ) -// To encode G1Affine and G2Affine points, we mask the most significant bits with these bits to specify without ambiguity -// metadata needed for point (de)compression -// we follow the BLS12-381 style encoding as specified in ZCash and now IETF -// see https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/11/ -// Appendix C. ZCash serialization format for BLS12_381 +// SEC1-style point prefix bytes for exact-byte field elements. const ( - mMask byte = 0b111 << 5 - mUncompressed byte = 0b000 << 5 - _ byte = 0b001 << 5 // invalid - mUncompressedInfinity byte = 0b010 << 5 - _ byte = 0b011 << 5 // invalid - mCompressedSmallest byte = 0b100 << 5 - mCompressedLargest byte = 0b101 << 5 - mCompressedInfinity byte = 0b110 << 5 - _ byte = 0b111 << 5 // invalid + mMask byte = 0xff + mCompressedInfinity byte = 0x00 + mCompressedSmallest byte = 0x02 + mCompressedLargest byte = 0x03 + mUncompressed byte = 0x04 ) var ( @@ -184,12 +176,6 @@ func (dec *Decoder) Decode(v any) (err error) { } nbBytes := SizeOfG1AffineCompressed - // 111, 011, 001 --> invalid mask - if isMaskInvalid(buf[0]) { - err = ErrInvalidEncoding - return - } - // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG1AffineUncompressed @@ -221,12 +207,6 @@ func (dec *Decoder) Decode(v any) (err error) { } nbBytes := SizeOfG1AffineCompressed - // 111, 011, 001 --> invalid mask - if isMaskInvalid(buf[0]) { - err = ErrInvalidEncoding - return - } - // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG1AffineUncompressed @@ -309,15 +289,8 @@ func (dec *Decoder) readUint64() (r uint64, err error) { return } -// isMaskInvalid returns true if the mask is invalid -func isMaskInvalid(msb byte) bool { - mData := msb & mMask - return ((mData == (0b111 << 5)) || (mData == (0b011 << 5)) || (mData == (0b001 << 5))) -} - func isCompressed(msb byte) bool { - mData := msb & mMask - return mData != mUncompressed && mData != mUncompressedInfinity + return msb != mUncompressed } // NewEncoder returns a binary encoder supporting curve secp256k1 objects @@ -383,7 +356,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("secp256k1 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -487,7 +460,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("secp256k1 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -498,7 +471,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + return errors.New("secp256k1 encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -602,7 +575,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("secp256k1 encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -656,10 +629,10 @@ func (enc *Encoder) writeUint32(a uint32) error { } // SizeOfG1AffineCompressed represents the size in bytes that a G1Affine need in binary form, compressed -const SizeOfG1AffineCompressed = 32 +const SizeOfG1AffineCompressed = 1 + fp.Bytes // SizeOfG1AffineUncompressed represents the size in bytes that a G1Affine need in binary form, uncompressed -const SizeOfG1AffineUncompressed = SizeOfG1AffineCompressed * 2 +const SizeOfG1AffineUncompressed = 1 + 2*fp.Bytes // Marshal converts p to a byte slice (without point compression) func (p *G1Affine) Marshal() []byte { @@ -673,15 +646,7 @@ func (p *G1Affine) Unmarshal(buf []byte) error { return err } -// Bytes returns binary representation of p -// will store X coordinate in regular form and a parity bit -// we follow the BLS12-381 style encoding as specified in ZCash and now IETF -// -// The most significant bit, when set, indicates that the point is in compressed form. Otherwise, the point is in uncompressed form. -// -// The second-most significant bit indicates that the point is at infinity. If this bit is set, the remaining bits of the group element's encoding should be set to zero. -// -// The third-most significant bit is set if (and only if) this point is in compressed form and it is not the point at infinity and its y-coordinate is the lexicographically largest of the two associated with the encoded x-coordinate. +// Bytes returns SEC1-style compressed binary representation of p. func (p *G1Affine) Bytes() (res [SizeOfG1AffineCompressed]byte) { // check if p is infinity point @@ -697,10 +662,8 @@ func (p *G1Affine) Bytes() (res [SizeOfG1AffineCompressed]byte) { msbMask = mCompressedLargest } - // we store X and mask the most significant word with our metadata mask - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - - res[0] |= msbMask + res[0] = msbMask + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X) return } @@ -712,19 +675,14 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity + res[0] = mUncompressed return } - // not compressed - // we store the Y coordinate - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[32:32+fp.Bytes]), p.Y) - - // we store X and mask the most significant word with our metadata mask - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - - res[0] |= mUncompressed + res[0] = mUncompressed + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X) + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1+fp.Bytes:1+2*fp.Bytes]), p.Y) return } @@ -748,16 +706,10 @@ func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { return 0, io.ErrShortBuffer } - // most significant byte - mData := buf[0] & mMask - - // 111, 011, 001 --> invalid mask - if isMaskInvalid(mData) { - return 0, ErrInvalidEncoding - } + mData := buf[0] // check buffer size - if (mData == mUncompressed) || (mData == mUncompressedInfinity) { + if mData == mUncompressed { if len(buf) < SizeOfG1AffineUncompressed { return 0, io.ErrShortBuffer } @@ -765,29 +717,21 @@ func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { // infinity encoded, we still check that the buffer is full of zeroes. if mData == mCompressedInfinity { - if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineCompressed]) { + if !isZeroed(0, buf[1:SizeOfG1AffineCompressed]) { return 0, ErrInvalidInfinityEncoding } p.X.SetZero() p.Y.SetZero() return SizeOfG1AffineCompressed, nil } - if mData == mUncompressedInfinity { - if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineUncompressed]) { - return 0, ErrInvalidInfinityEncoding - } - p.X.SetZero() - p.Y.SetZero() - return SizeOfG1AffineUncompressed, nil - } // uncompressed point if mData == mUncompressed { // read X and Y coordinates - if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { + if err := p.X.SetBytesCanonical(buf[1 : 1+fp.Bytes]); err != nil { return 0, err } - if err := p.Y.SetBytesCanonical(buf[fp.Bytes : fp.Bytes*2]); err != nil { + if err := p.Y.SetBytesCanonical(buf[1+fp.Bytes : 1+2*fp.Bytes]); err != nil { return 0, err } @@ -800,16 +744,12 @@ func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { } // we have a compressed coordinate - // we need to - // 1. copy the buffer (to keep this method thread safe) - // 2. we need to solve the curve equation to compute Y - - var bufX [fp.Bytes]byte - copy(bufX[:fp.Bytes], buf[:fp.Bytes]) - bufX[0] &= ^mMask + if mData != mCompressedSmallest && mData != mCompressedLargest { + return 0, ErrInvalidEncoding + } // read X coordinate - if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { + if err := p.X.SetBytesCanonical(buf[1 : 1+fp.Bytes]); err != nil { return 0, err } @@ -887,26 +827,23 @@ func (p *G1Affine) unsafeComputeY(subGroupCheck bool) error { // it sets X coordinate and uses Y for scratch space to store decompression metadata func (p *G1Affine) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, err error) { - // read the most significant byte - mData := buf[0] & mMask + mData := buf[0] if mData == mCompressedInfinity { isInfinity = true - if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineCompressed]) { + if !isZeroed(0, buf[1:SizeOfG1AffineCompressed]) { return isInfinity, ErrInvalidInfinityEncoding } p.X.SetZero() p.Y.SetZero() return isInfinity, nil } - - // we need to copy the input buffer (to keep this method thread safe) - var bufX [fp.Bytes]byte - copy(bufX[:fp.Bytes], buf[:fp.Bytes]) - bufX[0] &= ^mMask + if mData != mCompressedSmallest && mData != mCompressedLargest { + return false, ErrInvalidEncoding + } // read X coordinate - if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { + if err := p.X.SetBytesCanonical(buf[1 : 1+fp.Bytes]); err != nil { return false, err } // store mData in p.Y[0] diff --git a/ecc/secp256k1/marshal_test.go b/ecc/secp256k1/marshal_test.go index b4696e51e2..db076b44a3 100644 --- a/ecc/secp256k1/marshal_test.go +++ b/ecc/secp256k1/marshal_test.go @@ -7,7 +7,6 @@ package secp256k1 import ( "bytes" - crand "crypto/rand" "io" "math/big" "math/rand/v2" @@ -21,11 +20,6 @@ import ( "github.com/consensys/gnark-crypto/ecc/secp256k1/fr" ) -const ( - nbFuzzShort = 10 - nbFuzz = 100 -) - func TestEncoder(t *testing.T) { t.Parallel() // TODO need proper fuzz testing here @@ -190,26 +184,6 @@ func TestIsCompressed(t *testing.T) { } -func TestG1AffineInvalidBitMask(t *testing.T) { - t.Parallel() - var buf [SizeOfG1AffineCompressed]byte - crand.Read(buf[:]) - - var p G1Affine - buf[0] = 0b111 << 5 - if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { - t.Fatal("should error on invalid bit mask") - } - buf[0] = 0b011 << 5 - if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { - t.Fatal("should error on invalid bit mask") - } - buf[0] = 0b001 << 5 - if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { - t.Fatal("should error on invalid bit mask") - } -} - func TestG1AffineSerialization(t *testing.T) { t.Parallel() // test round trip serialization of infinity @@ -304,79 +278,3 @@ func TestG1AffineSerialization(t *testing.T) { } // define Gopters generators - -// GenFr generates an Fr element -func GenFr() gopter.Gen { - return func(genParams *gopter.GenParameters) *gopter.GenResult { - var elmt fr.Element - elmt.MustSetRandom() - - return gopter.NewGenResult(elmt, gopter.NoShrinker) - } -} - -// GenFp generates an Fp element -func GenFp() gopter.Gen { - return func(genParams *gopter.GenParameters) *gopter.GenResult { - var elmt fp.Element - elmt.MustSetRandom() - - return gopter.NewGenResult(elmt, gopter.NoShrinker) - } -} - -// GenE2 generates an fptower.E2 elmt -func GenE2() gopter.Gen { - return gopter.CombineGens( - GenFp(), - GenFp(), - ).Map(func(values []any) fptower.E2 { - return fptower.E2{A0: values[0].(fp.Element), A1: values[1].(fp.Element)} - }) -} - -// GenE4 generates an fptower.E4 elmt -func GenE4() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E4 { - return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} - }) -} - -// GenE6 generates an fptower.E6 elmt -func GenE6() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - GenE2(), - ).Map(func(values []any) fptower.E6 { - return fptower.E6{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2), B2: values[2].(fptower.E2)} - }) -} - -// GenE12 generates an fptower.E6 elmt -func GenE12() gopter.Gen { - return gopter.CombineGens( - GenE6(), - GenE6(), - ).Map(func(values []any) fptower.E12 { - return fptower.E12{C0: values[0].(fptower.E6), C1: values[1].(fptower.E6)} - }) -} - -// GenBigInt generates a big.Int -func GenBigInt() gopter.Gen { - return func(genParams *gopter.GenParameters) *gopter.GenResult { - var s big.Int - var b [fp.Bytes]byte - _, err := crand.Read(b[:]) - if err != nil { - panic(err) - } - s.SetBytes(b[:]) - genResult := gopter.NewGenResult(s, gopter.NoShrinker) - return genResult - } -} diff --git a/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index fcb26239aa..482c6e085e 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -225,118 +225,6 @@ func (z *E2) Exp(x E2, k *big.Int) *E2 { return z } -// ExpByCbrtHelperQ2Minus4Div9 is equivalent to z.Exp(x, 7001c71e3555555). -// It raises x to the (q²-4)/9 power over E2 using a shorter addition chain. -// -// uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain -func (z *E2) ExpByCbrtHelperQ2Minus4Div9(x E2) *E2 { - // addition chain: - // - // _10 = 2*1 - // _11 = 1 + _10 - // _110 = 2*_11 - // _111 = 1 + _110 - // _1110 = 2*_111 - // _11100 = 2*_1110 - // _111000 = 2*_11100 - // _1010100 = _11100 + _111000 - // _1010101 = 1 + _1010100 - // _1100011 = _1110 + _1010101 - // _1110000 = _11100 + _1010100 - // i35 = (_1110000 << 14 + _1100011 + _1110) << 7 + _1100011 - // i60 = ((i35 << 7 + _1100011) << 8 + _1010101) << 8 - // return (_1010101 + i60) << 8 + _1010101 - // - // Operations: 64 squares 10 multiplies - var t0, t1, t2, t3 E2 - - // Step 1: z = x^0x2 - z.Square(&x) - - // Step 2: z = x^0x3 - z.Mul(&x, z) - - // Step 3: z = x^0x6 - z.Square(z) - - // Step 4: z = x^0x7 - z.Mul(&x, z) - - // Step 5: t1 = x^0xe - t1.Square(z) - - // Step 6: t2 = x^0x1c - t2.Square(&t1) - - // Step 7: z = x^0x38 - z.Square(&t2) - - // Step 8: t3 = x^0x54 - t3.Mul(&t2, z) - - // Step 9: z = x^0x55 - z.Mul(&x, &t3) - - // Step 10: t0 = x^0x63 - t0.Mul(&t1, z) - - // Step 11: t2 = x^0x70 - t2.Mul(&t2, &t3) - - // Step 25: t2 = x^0x1c00000 - for range 14 { - t2.Square(&t2) - } - - // Step 26: t2 = x^0x1c00063 - t2.Mul(&t0, &t2) - - // Step 27: t1 = x^0x1c00071 - t1.Mul(&t1, &t2) - - // Step 34: t1 = x^0xe0003880 - for range 7 { - t1.Square(&t1) - } - - // Step 35: t1 = x^0xe00038e3 - t1.Mul(&t0, &t1) - - // Step 42: t1 = x^0x70001c7180 - for range 7 { - t1.Square(&t1) - } - - // Step 43: t0 = x^0x70001c71e3 - t0.Mul(&t0, &t1) - - // Step 51: t0 = x^0x70001c71e300 - for range 8 { - t0.Square(&t0) - } - - // Step 52: t0 = x^0x70001c71e355 - t0.Mul(z, &t0) - - // Step 60: t0 = x^0x70001c71e35500 - for range 8 { - t0.Square(&t0) - } - - // Step 61: t0 = x^0x70001c71e35555 - t0.Mul(z, &t0) - - // Step 69: t0 = x^0x70001c71e3555500 - for range 8 { - t0.Square(&t0) - } - - // Step 70: z = x^0x7001c71e3555555 - z.Mul(z, &t0) - - return z -} - // Sqrt sets z to the square root of and returns z // The function does not test whether the square root // exists or not, it's up to the caller to call diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index 7b7acbeee6..3a1fe6cb56 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -9,7 +9,6 @@ import ( "math/big" "math/bits" - "github.com/consensys/gnark-crypto/ecc" fr "github.com/consensys/gnark-crypto/field/koalabear" ) @@ -19,9 +18,6 @@ var ( cbrtE4Omega E4 cbrtE4Omega2 E4 cbrtE4Exponent big.Int - cbrtE4PhiCoeff E2 - cbrtE4GLVBasis ecc.Lattice - cbrtE4GLVSplit [2]big.Int ) var cbrtE4LucasExponent = [1]uint64{ @@ -39,21 +35,6 @@ func init() { cbrtE4Exponent.Div(&cbrtE4Exponent, big.NewInt(3)) three := new(big.Int).SetUint64(3) cbrtE4Exponent.ModInverse(three, &cbrtE4Exponent) - - var minusOne E2 - minusOne.A0.SetOne() - minusOne.A0.Neg(&minusOne.A0) - if cbrtE4PhiCoeff.Sqrt(&minusOne) == nil { - panic("koalabear/e4: failed to find Frobenius coefficient") - } - - var modulus, lambda, exponent big.Int - modulus.Exp(fr.Modulus(), big.NewInt(2), nil) - modulus.Add(&modulus, big.NewInt(1)) - lambda.Set(fr.Modulus()) - exponent.ModInverse(three, &modulus) - ecc.PrecomputeLattice(&modulus, &lambda, &cbrtE4GLVBasis) - cbrtE4GLVSplit = ecc.SplitScalar(&exponent, &cbrtE4GLVBasis) } // q + r'.r = 1, i.e., qInvNeg = - q⁻¹ mod r @@ -418,85 +399,6 @@ func (z *E4) ExpInt64(x E4, k int64) *E4 { return z } -// ExpByCbrtHelperQ4Minus16Div27 is equivalent to z.Exp(x, 9304aab8c7552f764c12f6978e38e3). -// It raises x to the (q^4-16)/27 power over E4 using a shorter addition chain. -// -// uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain -func (z *E4) ExpByCbrtHelperQ4Minus16Div27(x E4) *E4 { - var t0, t1, t2, t3, t4, t5, t6, t7, t8, t9 E4 - - t1.Square(&x) - t3.Square(&t1) - t6.Mul(&t1, &t3) - z.Mul(&t3, &t6) - t0.Mul(&x, z) - t5.Mul(&t3, z) - t4.Mul(&t3, &t5) - t2.Mul(&t6, &t4) - t7.Mul(&t6, &t2) - t8.Square(&t7) - t0.Mul(&t0, &t8) - t5.Mul(&t5, &t0) - t7.Mul(&t1, &t5) - t9.Mul(&t8, &t7) - t8.Mul(&t1, &t9) - t1.Mul(&t4, &t9) - t2.Mul(&t2, &t1) - t3.Mul(&t3, &t2) - t6.Mul(&t6, &t3) - t4.Mul(&t4, &t6) - z.Mul(z, &t4) - for range 13 { - t9.Square(&t9) - } - t8.Mul(&t8, &t9) - for range 8 { - t8.Square(&t8) - } - t7.Mul(&t7, &t8) - for range 11 { - t7.Square(&t7) - } - t6.Mul(&t6, &t7) - for range 8 { - t6.Square(&t6) - } - t5.Mul(&t5, &t6) - for range 10 { - t5.Square(&t5) - } - t5.Mul(&t2, &t5) - for range 8 { - t5.Square(&t5) - } - t4.Mul(&t4, &t5) - for range 10 { - t4.Square(&t4) - } - t3.Mul(&t3, &t4) - for range 10 { - t3.Square(&t3) - } - t2.Mul(&t2, &t3) - for range 8 { - t2.Square(&t2) - } - t1.Mul(&t1, &t2) - for range 8 { - t1.Square(&t1) - } - t1.Mul(z, &t1) - for range 7 { - t1.Square(&t1) - } - t0.Mul(&t0, &t1) - for range 11 { - t0.Square(&t0) - } - z.Mul(z, &t0) - return z -} - // Conjugate sets z to x conjugated and returns z func (z *E4) Conjugate(x *E4) *E4 { z.B0 = x.B0 @@ -594,10 +496,7 @@ func (z *E4) Cbrt(x *E4) *E4 { imY.Double(&x0x1) imY.Mul(&imY, &normInv) - var alpha E4 - alpha.B0.Set(&halfTau) - alpha.B1.Neg(&imY) - te, te1 := glvTraceE4(&alpha) + te, te1 := lucasV2E2Cbrt(&tau) var wa0, wa1 E2 wa0.Mul(&halfTau, &te) @@ -633,45 +532,6 @@ func (z *E4) Cbrt(x *E4) *E4 { return cbrtVerifyAndAdjustE4(z.Set(&y), x) } -func cbrtAndNormInverseE4(norm, x0sq, x1sq *E2) (m, normInv, deltaInv E2, ok bool) { - // Hamburg trick for E4 over E2, where |E2| ≡ 4 mod 9: - // with w = U^3 * norm and t = w^((q-4)/9), we have - // cbrt(w) = w * t^2 and w^(-1) = t^5 * cbrt(w)^2. - var x0x1, betaX0x1, U, U2, U3, w E2 - x0x1.Mul(x0sq, x1sq) - betaX0x1.MulByNonResidue(&x0x1) - U.Mul(&betaX0x1, norm) - U.Double(&U).Double(&U) - U.Double(&U).Double(&U) - U2.Square(&U) - U3.Mul(&U2, &U) - w.Mul(&U3, norm) - - var t, t2, t4, t5, cbrtW, cw2, wInv E2 - t.ExpByCbrtHelperQ2Minus4Div9(w) - t2.Square(&t) - cbrtW.Mul(&w, &t2) - cw2.Square(&cbrtW) - t4.Square(&t2) - t5.Mul(&t4, &t) - wInv.Mul(&t5, &cw2) - - var check, UInv E2 - UInv.Mul(&U2, norm).Mul(&UInv, &wInv) - m.Mul(&cbrtW, &UInv) - normInv.Mul(&U3, &wInv) - check.Square(&m).Mul(&check, &m) - if !check.Equal(norm) { - return m, normInv, deltaInv, false - } - - var norm2, norm3 E2 - norm2.Square(norm) - norm3.Mul(&norm2, norm) - deltaInv.Mul(&norm3, &UInv) - return m, normInv, deltaInv, true -} - func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { var check E4 check.Square(z).Mul(&check, z) @@ -722,93 +582,6 @@ func lucasV2E2Cbrt(alpha *E2) (E2, E2) { return te, te1 } -func (z *E4) phiCbrt(x *E4) *E4 { - z.B0.Conjugate(&x.B0) - z.B1.Conjugate(&x.B1).Mul(&z.B1, &cbrtE4PhiCoeff) - return z -} - -func expTorusGLVE4(z, x *E4) *E4 { - var res, q1, q2 E4 - q1.Set(x) - q2.phiCbrt(x) - res.SetOne() - - var k0, k1 big.Int - k0.Set(&cbrtE4GLVSplit[0]) - k1.Set(&cbrtE4GLVSplit[1]) - if k0.Sign() < 0 { - k0.Neg(&k0) - q1.Conjugate(&q1) - } - if k1.Sign() < 0 { - k1.Neg(&k1) - q2.Conjugate(&q2) - } - - const wnafWindow = 5 - naf0 := make([]int8, k0.BitLen()+wnafWindow+1) - naf1 := make([]int8, k1.BitLen()+wnafWindow+1) - len0 := ecc.WnafDecomposition(&k0, wnafWindow, naf0) - len1 := ecc.WnafDecomposition(&k1, wnafWindow, naf1) - maxLen := max(len0, len1) - if maxLen == 0 { - return z.SetOne() - } - - var table0, table1 [8]E4 - var q0Two, q1Two E4 - table0[0].Set(&q1) - table1[0].Set(&q2) - q0Two.Square(&q1) - q1Two.Square(&q2) - for i := 1; i < len(table0); i++ { - table0[i].Mul(&table0[i-1], &q0Two) - table1[i].Mul(&table1[i-1], &q1Two) - } - - for i := maxLen - 1; i >= 0; i-- { - res.Square(&res) - if i < len0 { - d := naf0[i] - if d != 0 { - if d > 0 { - res.Mul(&res, &table0[(d-1)/2]) - } else { - var inv E4 - inv.Conjugate(&table0[(-d-1)/2]) - res.Mul(&res, &inv) - } - } - } - if i < len1 { - d := naf1[i] - if d != 0 { - if d > 0 { - res.Mul(&res, &table1[(d-1)/2]) - } else { - var inv E4 - inv.Conjugate(&table1[(-d-1)/2]) - res.Mul(&res, &inv) - } - } - } - } - - return z.Set(&res) -} - -func glvTraceE4(alpha *E4) (E2, E2) { - var sigma, sigmaAlpha E4 - expTorusGLVE4(&sigma, alpha) - sigmaAlpha.Mul(&sigma, alpha) - - var te, te1 E2 - te.Double(&sigma.B0) - te1.Double(&sigmaAlpha.B0) - return te, te1 -} - // BatchInvertE4 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. // diff --git a/field/koalabear/extensions/e4_test.go b/field/koalabear/extensions/e4_test.go index eef84c1ab6..679e531852 100644 --- a/field/koalabear/extensions/e4_test.go +++ b/field/koalabear/extensions/e4_test.go @@ -1067,27 +1067,6 @@ func TestE4CbrtRejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E4 non-cube in 256 samples") } -func TestE4GLVTraceMatchesBinaryLucas(t *testing.T) { - for i := 0; i < 64; i++ { - var x, xInv, alpha E4 - x.MustSetRandom() - if x.IsZero() { - i-- - continue - } - xInv.Inverse(&x) - alpha.Conjugate(&x).Mul(&alpha, &xInv) - - var tau E2 - tau.Double(&alpha.B0) - - gotTe, gotTe1 := glvTraceE4(&alpha) - refTe, refTe1 := lucasV2E2Cbrt(&tau) - require.True(t, gotTe.Equal(&refTe)) - require.True(t, gotTe1.Equal(&refTe1)) - } -} - func BenchmarkE4Cbrt(b *testing.B) { var a, x E4 a.MustSetRandom() diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index 1ade4db5b5..5308357d63 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -9,7 +9,6 @@ import ( "math/big" "math/bits" - "github.com/consensys/gnark-crypto/ecc" fr "github.com/consensys/gnark-crypto/field/koalabear" ) @@ -19,9 +18,6 @@ var ( cbrtE8Omega E8 cbrtE8Omega2 E8 cbrtE8Exponent big.Int - cbrtE8PhiCoeff E4 - cbrtE8GLVBasis ecc.Lattice - cbrtE8GLVSplit [2]big.Int ) var cbrtE8LucasExponent = [2]uint64{ @@ -40,21 +36,6 @@ func init() { cbrtE8Exponent.Div(&cbrtE8Exponent, big.NewInt(3)) three := new(big.Int).SetUint64(3) cbrtE8Exponent.ModInverse(three, &cbrtE8Exponent) - - var minusOne E4 - minusOne.B0.A0.SetOne() - minusOne.B0.A0.Neg(&minusOne.B0.A0) - if cbrtE8PhiCoeff.Sqrt(&minusOne) == nil { - panic("koalabear/e8: failed to find Frobenius coefficient") - } - - var modulus, lambda, exponent big.Int - modulus.Exp(fr.Modulus(), big.NewInt(4), nil) - modulus.Add(&modulus, big.NewInt(1)) - lambda.Exp(fr.Modulus(), big.NewInt(2), nil) - exponent.ModInverse(three, &modulus) - ecc.PrecomputeLattice(&modulus, &lambda, &cbrtE8GLVBasis) - cbrtE8GLVSplit = ecc.SplitScalar(&exponent, &cbrtE8GLVBasis) } // E8 is a degree two finite field extension of E4. @@ -359,10 +340,7 @@ func (z *E8) Cbrt(x *E8) *E8 { imY.Double(&x0x1) imY.Mul(&imY, &normInv) - var alpha E8 - alpha.C0.Set(&halfTau) - alpha.C1.Neg(&imY) - te, te1 := glvTraceE8(&alpha) + te, te1 := lucasV2E4Cbrt(&tau) var wa0, wa1 E4 wa0.Mul(&halfTau, &te) @@ -398,57 +376,6 @@ func (z *E8) Cbrt(x *E8) *E8 { return cbrtVerifyAndAdjustE8(z.Set(&y), x) } -func cbrtAndNormInverseE8(norm, x0sq, x1sq *E4) (m, normInv, deltaInv E4, ok bool) { - var x0x1, betaX0x1, U, U2, U3, w E4 - x0x1.Mul(x0sq, x1sq) - betaX0x1.MulByNonResidue(&x0x1) - U.Mul(&betaX0x1, norm) - U.Double(&U).Double(&U) - U.Double(&U).Double(&U) - U2.Square(&U) - U3.Mul(&U2, &U) - w.Mul(&U3, norm) - - var t, t2, t3, t6, w2, cbrtW, c2, c4, c7, wInv E4 - t.ExpByCbrtHelperQ4Minus16Div27(w) - t2.Square(&t) - t3.Mul(&t2, &t) - t6.Square(&t3) - w2.Square(&w) - cbrtW.Mul(&w2, &t3) - c2.Square(&cbrtW) - c4.Square(&c2) - c7.Mul(&c4, &c2).Mul(&c7, &cbrtW) - wInv.Mul(&t6, &c7) - - var UInv, check E4 - UInv.Mul(&U2, norm).Mul(&UInv, &wInv) - m.Mul(&cbrtW, &UInv) - normInv.Mul(&U3, &wInv) - check.Square(&m).Mul(&check, &m) - if !check.Equal(norm) { - var alt E4 - alt.Mul(&m, &cbrtE4Omega) - check.Square(&alt).Mul(&check, &alt) - if check.Equal(norm) { - m.Set(&alt) - } else { - alt.Mul(&m, &cbrtE4Omega2) - check.Square(&alt).Mul(&check, &alt) - if !check.Equal(norm) { - return m, normInv, deltaInv, false - } - m.Set(&alt) - } - } - - var norm2, norm3 E4 - norm2.Square(norm) - norm3.Mul(&norm2, norm) - deltaInv.Mul(&norm3, &UInv) - return m, normInv, deltaInv, true -} - func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { var check, y E8 check.Square(z).Mul(&check, z) @@ -496,93 +423,6 @@ func lucasV2E4Cbrt(alpha *E4) (E4, E4) { return te, te1 } -func (z *E8) phiCbrt(x *E8) *E8 { - z.C0.Conjugate(&x.C0) - z.C1.Conjugate(&x.C1).Mul(&z.C1, &cbrtE8PhiCoeff) - return z -} - -func expTorusGLVE8(z, x *E8) *E8 { - var res, q1, q2 E8 - q1.Set(x) - q2.phiCbrt(x) - res.SetOne() - - var k0, k1 big.Int - k0.Set(&cbrtE8GLVSplit[0]) - k1.Set(&cbrtE8GLVSplit[1]) - if k0.Sign() < 0 { - k0.Neg(&k0) - q1.Conjugate(&q1) - } - if k1.Sign() < 0 { - k1.Neg(&k1) - q2.Conjugate(&q2) - } - - const wnafWindow = 5 - naf0 := make([]int8, k0.BitLen()+wnafWindow+1) - naf1 := make([]int8, k1.BitLen()+wnafWindow+1) - len0 := ecc.WnafDecomposition(&k0, wnafWindow, naf0) - len1 := ecc.WnafDecomposition(&k1, wnafWindow, naf1) - maxLen := max(len0, len1) - if maxLen == 0 { - return z.SetOne() - } - - var table0, table1 [8]E8 - var q0Two, q1Two E8 - table0[0].Set(&q1) - table1[0].Set(&q2) - q0Two.Square(&q1) - q1Two.Square(&q2) - for i := 1; i < len(table0); i++ { - table0[i].Mul(&table0[i-1], &q0Two) - table1[i].Mul(&table1[i-1], &q1Two) - } - - for i := maxLen - 1; i >= 0; i-- { - res.Square(&res) - if i < len0 { - d := naf0[i] - if d != 0 { - if d > 0 { - res.Mul(&res, &table0[(d-1)/2]) - } else { - var inv E8 - inv.Conjugate(&table0[(-d-1)/2]) - res.Mul(&res, &inv) - } - } - } - if i < len1 { - d := naf1[i] - if d != 0 { - if d > 0 { - res.Mul(&res, &table1[(d-1)/2]) - } else { - var inv E8 - inv.Conjugate(&table1[(-d-1)/2]) - res.Mul(&res, &inv) - } - } - } - } - - return z.Set(&res) -} - -func glvTraceE8(alpha *E8) (E4, E4) { - var sigma, sigmaAlpha E8 - expTorusGLVE8(&sigma, alpha) - sigmaAlpha.Mul(&sigma, alpha) - - var te, te1 E4 - te.Double(&sigma.C0) - te1.Double(&sigmaAlpha.C0) - return te, te1 -} - // BatchInvertE8 returns a new slice with every element in a inverted. func BatchInvertE8(a []E8) []E8 { res := make([]E8, len(a)) diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go index 1f15c59b3d..5a94367d09 100644 --- a/field/koalabear/extensions/e8_test.go +++ b/field/koalabear/extensions/e8_test.go @@ -197,27 +197,6 @@ func TestE8CbrtRejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E8 non-cube in 256 samples") } -func TestE8GLVTraceMatchesBinaryLucas(t *testing.T) { - for i := 0; i < 64; i++ { - var x, xInv, alpha E8 - x.MustSetRandom() - if x.IsZero() { - i-- - continue - } - xInv.Inverse(&x) - alpha.Conjugate(&x).Mul(&alpha, &xInv) - - var tau E4 - tau.Double(&alpha.C0) - - gotTe, gotTe1 := glvTraceE8(&alpha) - refTe, refTe1 := lucasV2E4Cbrt(&tau) - require.True(t, gotTe.Equal(&refTe)) - require.True(t, gotTe1.Equal(&refTe1)) - } -} - func BenchmarkE8Cbrt(b *testing.B) { var a, x E8 a.MustSetRandom() diff --git a/internal/generator/ecc/generate.go b/internal/generator/ecc/generate.go index c755330114..eba4279455 100644 --- a/internal/generator/ecc/generate.go +++ b/internal/generator/ecc/generate.go @@ -129,9 +129,13 @@ func Generate(conf config.Curve, baseDir string, gen *common.Generator) error { // marshal entries = []bavard.Entry{ - {File: filepath.Join(baseDir, "marshal.go"), Templates: []string{"marshal.go.tmpl"}}, {File: filepath.Join(baseDir, "marshal_test.go"), Templates: []string{"tests/marshal.go.tmpl"}}, } + // secp256k1 uses a hand-maintained SEC1-style prefix-byte marshal format: + // its base field has no spare top bits, so the generic bit-stealing template does not apply. + if conf.Name != config.SECP256K1.Name { + entries = append(entries, bavard.Entry{File: filepath.Join(baseDir, "marshal.go"), Templates: []string{"marshal.go.tmpl"}}) + } if err := eccGen.GenerateWithOptions(conf, packageName, "", "", bavardOpts, entries...); err != nil { return err diff --git a/internal/generator/ecc/template/marshal.go.tmpl b/internal/generator/ecc/template/marshal.go.tmpl index 0fd381212d..e02daed105 100644 --- a/internal/generator/ecc/template/marshal.go.tmpl +++ b/internal/generator/ecc/template/marshal.go.tmpl @@ -528,8 +528,8 @@ func isZeroed(firstByte byte, buf []byte) bool { return true } -{{template "encode" dict "Raw" ""}} -{{template "encode" dict "Raw" "Raw"}} +{{template "encode" dict "Raw" "" "all" .}} +{{template "encode" dict "Raw" "Raw" "all" .}} func (enc *Encoder) writeUint64Slice(t []uint64) (err error) { if err = enc.writeUint32(uint32(len(t))); err != nil { @@ -581,7 +581,7 @@ func (enc *Encoder) writeUint32(a uint32) error { func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New("{{.Name}} encoder: can't encode ") + return errors.New("{{.all.Name}} encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -615,7 +615,7 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - {{- if .HasG2 }} + {{- if .all.HasG2 }} case *G2Affine: buf := t.{{- $.Raw}}Bytes() written, err = enc.w.Write(buf[:]) @@ -689,7 +689,7 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { } } return nil - {{- if .HasG2 }} + {{- if .all.HasG2 }} case *[]G2Affine: return enc.encode{{- $.Raw}}(*t) case []G2Affine: @@ -715,7 +715,7 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New("{{.Name}} encoder: unsupported type") + return errors.New("{{.all.Name}} encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/internal/generator/ecc/template/point.go.tmpl b/internal/generator/ecc/template/point.go.tmpl index 52389e599c..7502942dd4 100644 --- a/internal/generator/ecc/template/point.go.tmpl +++ b/internal/generator/ecc/template/point.go.tmpl @@ -240,7 +240,11 @@ func (p *{{ $TAffine }}) IsOnCurve() bool { if p.IsInfinity() { return true } + {{- if and (eq .Name "kb8") (eq .PointName "g1") }} var left, right, tmp {{.CoordType}} + {{- else }} + var left, right {{.CoordType}} + {{- end }} left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) {{- if and (eq .Name "kb8") (eq .PointName "g1") }} diff --git a/internal/generator/ecc/template/tests/marshal.go.tmpl b/internal/generator/ecc/template/tests/marshal.go.tmpl index 181bfbbacf..54e0a8e93c 100644 --- a/internal/generator/ecc/template/tests/marshal.go.tmpl +++ b/internal/generator/ecc/template/tests/marshal.go.tmpl @@ -25,10 +25,12 @@ import ( {{- end }} ) +{{ if not (or (eq .Name "secp256k1") (eq .Name "secp256r1") (eq .Name "stark-curve")) }} const ( nbFuzzShort = 10 nbFuzz = 100 ) +{{ end }} func TestEncoder(t *testing.T) { t.Parallel() @@ -418,6 +420,7 @@ func Test{{ $.TAffine }}Serialization(t *testing.T) { // define Gopters generators +{{ if not (or (eq .Name "secp256k1") (eq .Name "secp256r1") (eq .Name "stark-curve")) }} // GenFr generates an Fr element func GenFr() gopter.Gen { @@ -443,6 +446,7 @@ func GenFp() gopter.Gen { // e2 e4 e12 e24 for bls24 // e2 e6 e12 else */}} +{{if or .HasG2 (eq .G1.CoordType "fptower.E2") (eq .G1.CoordType "fptower.E4") (eq .G1.CoordType "fptower.E8")}} {{if or (eq .Name "bw6-633") (eq .Name "bw6-761")}} // GenE3 generates an E3 elmt func GenE3() gopter.Gen { @@ -465,32 +469,34 @@ func GenFp() gopter.Gen { }) } {{ else }} - // GenE2 generates an fptower.E2 elmt - func GenE2() gopter.Gen { - return gopter.CombineGens( - GenFp(), - GenFp(), + // GenE2 generates an fptower.E2 elmt + func GenE2() gopter.Gen { + return gopter.CombineGens( + GenFp(), + GenFp(), ).Map(func(values []any) fptower.E2 { return fptower.E2{A0: values[0].(fp.Element), A1: values[1].(fp.Element)} }) } - // GenE4 generates an fptower.E4 elmt - func GenE4() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), + {{if or (eq .Name "kb8") (eq .Name "bls24-315") (eq .Name "bls24-317")}} + // GenE4 generates an fptower.E4 elmt + func GenE4() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), ).Map(func(values []any) fptower.E4 { return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} }) } + {{end}} {{if eq .Name "kb8"}} - // GenE8 generates an fptower.E8 elmt - func GenE8() gopter.Gen { - return gopter.CombineGens( - GenE4(), - GenE4(), + // GenE8 generates an fptower.E8 elmt + func GenE8() gopter.Gen { + return gopter.CombineGens( + GenE4(), + GenE4(), ).Map(func(values []any) fptower.E8 { return fptower.E8{C0: values[0].(fptower.E4), C1: values[1].(fptower.E4)} }) @@ -518,28 +524,29 @@ func GenFp() gopter.Gen { }) } {{ else }} - // GenE6 generates an fptower.E6 elmt - func GenE6() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - GenE2(), + // GenE6 generates an fptower.E6 elmt + func GenE6() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + GenE2(), ).Map(func(values []any) fptower.E6 { return fptower.E6{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2), B2: values[2].(fptower.E2)} }) } - // GenE12 generates an fptower.E6 elmt - func GenE12() gopter.Gen { - return gopter.CombineGens( - GenE6(), - GenE6(), + // GenE12 generates an fptower.E6 elmt + func GenE12() gopter.Gen { + return gopter.CombineGens( + GenE6(), + GenE6(), ).Map(func(values []any) fptower.E12 { return fptower.E12{C0: values[0].(fptower.E6), C1: values[1].(fptower.E6)} }) } {{ end }} {{ end }} +{{ end }} @@ -557,3 +564,4 @@ func GenBigInt() gopter.Gen { return genResult } } +{{ end }} diff --git a/internal/generator/ecdsa/template/ecdsa.go.tmpl b/internal/generator/ecdsa/template/ecdsa.go.tmpl index d17b9dceb4..adfb060f0f 100644 --- a/internal/generator/ecdsa/template/ecdsa.go.tmpl +++ b/internal/generator/ecdsa/template/ecdsa.go.tmpl @@ -21,7 +21,9 @@ const ( sizeFr = fr.Bytes sizeFrBits = fr.Bits sizeFp = fp.Bytes -{{- if or (eq .Name "secp256k1") (eq .Name "secp256r1")}} +{{- if eq .Name "secp256k1"}} + sizePublicKey = {{ .CurvePackage }}.SizeOfG1AffineUncompressed +{{- else if eq .Name "secp256r1"}} sizePublicKey = 2 * sizeFp {{- else}} sizePublicKey = {{ .CurvePackage }}.SizeOfG1AffineCompressed diff --git a/internal/generator/field/generator_extensions.go b/internal/generator/field/generator_extensions.go index 9f91747704..6a6e830394 100644 --- a/internal/generator/field/generator_extensions.go +++ b/internal/generator/field/generator_extensions.go @@ -19,11 +19,21 @@ func generateExtensions(F *config.Field, outputDir string) error { outputDir = filepath.Join(outputDir, "extensions") + isKoalaBear := F.Q[0] == 2130706433 + isBabyBear := F.Q[0] == 2013265921 + entries_ext2 := []bavard.Entry{ {File: filepath.Join(outputDir, "doc.go"), Templates: []string{"doc.go.tmpl"}}, {File: filepath.Join(outputDir, "utils.go"), Templates: []string{"utils.go.tmpl"}}, - {File: filepath.Join(outputDir, "e2.go"), Templates: []string{"e2.go.tmpl"}}, - {File: filepath.Join(outputDir, "e2_test.go"), Templates: []string{"e2_test.go.tmpl"}}, + } + // koalabear has a hand-maintained recursive cube-root stack in its extension files. + // The generic extension templates don't reproduce it yet, so generation only refreshes + // the shared support files and leaves e2/e4/e8 implementation files untouched. + if !isKoalaBear { + entries_ext2 = append(entries_ext2, + bavard.Entry{File: filepath.Join(outputDir, "e2.go"), Templates: []string{"e2.go.tmpl"}}, + bavard.Entry{File: filepath.Join(outputDir, "e2_test.go"), Templates: []string{"e2_test.go.tmpl"}}, + ) } type extensionsTemplateData struct { @@ -35,8 +45,6 @@ func generateExtensions(F *config.Field, outputDir string) error { IsBabyBear bool } - isKoalaBear := F.Q[0] == 2130706433 - isBabyBear := F.Q[0] == 2013265921 data := &extensionsTemplateData{ FF: F.PackageName, FieldPackagePath: fieldImportPath, @@ -54,9 +62,13 @@ func generateExtensions(F *config.Field, outputDir string) error { } if F.F31 { entries_ext4 := []bavard.Entry{ - {File: filepath.Join(outputDir, "e4.go"), Templates: []string{"e4.go.tmpl"}}, {File: filepath.Join(outputDir, "vector.go"), Templates: []string{"vector.go.tmpl"}}, - {File: filepath.Join(outputDir, "e4_test.go"), Templates: []string{"e4_test.go.tmpl"}}, + } + if !isKoalaBear { + entries_ext4 = append(entries_ext4, + bavard.Entry{File: filepath.Join(outputDir, "e4.go"), Templates: []string{"e4.go.tmpl"}}, + bavard.Entry{File: filepath.Join(outputDir, "e4_test.go"), Templates: []string{"e4_test.go.tmpl"}}, + ) } if isKoalaBear { @@ -68,17 +80,6 @@ func generateExtensions(F *config.Field, outputDir string) error { return err } - if isKoalaBear { - entriesExt8 := []bavard.Entry{ - {File: filepath.Join(outputDir, "e8.go"), Templates: []string{"e8.go.tmpl"}}, - {File: filepath.Join(outputDir, "e8_test.go"), Templates: []string{"e8_test.go.tmpl"}}, - } - - if err := g.Generate(data, "extensions", "", "extensions", entriesExt8...); err != nil { - return err - } - } - if isKoalaBear { // generate the assembly file; asmFile, err := os.Create(filepath.Join(outputDir, "e4_amd64.s")) diff --git a/internal/generator/field/generator_field.go b/internal/generator/field/generator_field.go index b1718e7beb..dbc2546ee3 100644 --- a/internal/generator/field/generator_field.go +++ b/internal/generator/field/generator_field.go @@ -153,7 +153,9 @@ func generateField(F *config.Field, outputDir, asmDirIncludePath, hashArm64, has g.Go(generate("vector_purego.go", []string{"element/vectoropspurego.go.tmpl"}, withBuildTag(pureGoVectorBuildTag))) - if F.UseAddChain { + // koalabear keeps custom fixed-exponent helpers alongside its recursive extension cube-root stack. + // The generic field template doesn't model those helpers yet, so generation must leave element_exp.go intact. + if F.UseAddChain && F.PackageName != "koalabear" { g.Go(generate("element_exp.go", []string{"element/fixedexp.go.tmpl"})) } diff --git a/internal/generator/main.go b/internal/generator/main.go index 447c653d93..ac4d19ce3d 100644 --- a/internal/generator/main.go +++ b/internal/generator/main.go @@ -107,7 +107,7 @@ func main() { curveDir := filepath.Join(baseDir, "ecc", conf.Name) - conf.FpUnusedBits = 64 - (conf.Fp.NbBits % 64) + conf.FpUnusedBits = (64 - (conf.Fp.NbBits % 64)) % 64 // Torus cbrt: compute betaInvNeg now that Fp config is available if conf.E2CbrtTorusEnabled && conf.E2CbrtTorusBeta != -1 { From e95cb0e4e63e95ff295ad33518bd469849e669e7 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 21 Apr 2026 18:47:26 -0400 Subject: [PATCH 12/33] refactor: make linter happy --- ecc/kb8/g1.go | 168 ------------------ internal/generator/ecc/template/point.go.tmpl | 15 +- 2 files changed, 10 insertions(+), 173 deletions(-) diff --git a/ecc/kb8/g1.go b/ecc/kb8/g1.go index 47daeadab1..3efdf6f735 100644 --- a/ecc/kb8/g1.go +++ b/ecc/kb8/g1.go @@ -401,30 +401,6 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { p.Y.Set(&y) p.Z.SetOne() return p - var XX, YY, YYYY, S, M, T fptower.E8 - XX.Square(&a.X) - YY.Square(&a.Y) - YYYY.Square(&YY) - S.Add(&a.X, &YY). - Square(&S). - Sub(&S, &XX). - Sub(&S, &YYYY). - Double(&S) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here - T.Square(&M). - Sub(&T, &S). - Sub(&T, &S) - p.X.Set(&T) - p.Y.Sub(&S, &T). - Mul(&p.Y, &M) - YYYY.Double(&YYYY). - Double(&YYYY). - Double(&YYYY) - p.Y.Sub(&p.Y, &YYYY) - p.Z.Double(&a.Y) - - return p } // AddMixed sets p to p+a in Jacobian coordinates, where a.Z = 1. @@ -499,30 +475,6 @@ func (p *G1Jac) DoubleAssign() *G1Jac { var a G1Affine a.FromJacobian(p) return p.DoubleMixed(&a) - var A, B, C, D, E, F, t fptower.E8 - A.Square(&p.X) - B.Square(&p.Y) - C.Square(&B) - D.Add(&p.X, &B). - Square(&D). - Sub(&D, &A). - Sub(&D, &C). - Double(&D) - E.Double(&A). - Add(&E, &A) - F.Square(&E) - t.Double(&D) - p.Z.Mul(&p.Y, &p.Z). - Double(&p.Z) - p.X.Sub(&F, &t) - p.Y.Sub(&D, &p.X). - Mul(&p.Y, &E) - t.Double(&C). - Double(&t). - Double(&t) - p.Y.Sub(&p.Y, &t) - - return p } // Triple sets p to [3]q in Jacobian coordinates for j=0 curves. @@ -533,64 +485,6 @@ func (p *G1Jac) Triple(q *G1Jac) *G1Jac { dbl.Double(q) p.Set(&dbl).AddAssign(q) return p - // Helper functions for multiplication by 3 and 4. - mulBy3 := func(v *fptower.E8) { - tmp := *v - v.Double(v) - v.Add(v, &tmp) - } - mulBy4 := func(v *fptower.E8) { - v.Double(v).Double(v) - } - - // --- Step 1: Compute initial terms from input q --- - var X3, Y2, XZ fptower.E8 - X3.Square(&q.X) // X3 = q.X^2 - Y2.Square(&q.Y) // Y2 = q.Y^2 - X3.Mul(&X3, &q.X) // X3 = q.X^3 - XZ.Mul(&q.X, &q.Z) // XZ = q.X * q.Z - - // --- Step 2: Compute the X-coordinate of an intermediate point τ --- - // Calculates Xτ = 4*q.Y^2 - 3*q.X^3. - // The variable p.Z is used for temporary storage and finalized in Step 6. - mulBy3(&X3) // X3 = 3*q.X^3 - mulBy4(&Y2) // Y2 = 4*q.Y^2 - var Xτ fptower.E8 - Xτ.Sub(&Y2, &X3) - p.Z.Mul(&Xτ, &XZ) // p.Z = Xτ * (q.X * q.Z) - - // --- Step 3: Compute the Y-coordinate of the intermediate point τ --- - // Calculates Yτ = q.Y * (9*q.X^3 - 8*q.Y^2). - // Reuses X3 and Y2 from previous steps. - mulBy3(&X3) // X3 = 9*q.X^3 - var Yτ fptower.E8 - Yτ.Double(&Y2) // Yτ = 8*q.Y^2 - Yτ.Sub(&X3, &Yτ).Mul(&Yτ, &q.Y) - - // --- Step 4: Compute powers of the intermediate point's coordinates --- - var Xτ2, Xτ3, Yτ2 fptower.E8 - Xτ2.Square(&Xτ) // Xτ2 = Xτ^2 - Xτ3.Mul(&Xτ2, &Xτ) // Xτ3 = Xτ^3 - Yτ2.Square(&Yτ) // Yτ2 = Yτ^2 - - // --- Step 5: Compute the final X and Y coordinates of the result [3]q --- - // This step re-applies the same transformation using (Xτ, Yτ) as input. - // p.X = 4*Yτ^2 - 3*Xτ^3 - mulBy3(&Xτ3) // Xτ3 = 3*Xτ^3 - mulBy4(&Yτ2) // Yτ2 = 4*Yτ^2 - p.X.Sub(&Yτ2, &Xτ3) - - // p.Y = Yτ * (9*Xτ^3 - 8*Yτ^2) - // Reuses Xτ3 and Yτ2 from the previous calculation. - mulBy3(&Xτ3) // Xτ3 = 9*Xτ^3 - Yτ2.Double(&Yτ2) // Yτ2 = 8*Yτ^2 - p.Y.Sub(&Xτ3, &Yτ2).Mul(&p.Y, &Yτ) - - // --- Step 6: Finalize the Z-coordinate --- - // p.Z = 3 * p.Z = 3 * Xτ * (q.X * q.Z) - mulBy3(&p.Z) - - return p } // ScalarMultiplication computes and returns p = [s]a @@ -922,27 +816,6 @@ func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { p.ZZ.Square(&res.Z) p.ZZZ.Mul(&p.ZZ, &res.Z) return p - var U, V, W, S, XX, M fptower.E8 - - U.Double(&q.Y) - V.Square(&U) - W.Mul(&U, &V) - S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here - U.Mul(&W, &q.Y) - - p.X.Square(&M). - Sub(&p.X, &S). - Sub(&p.X, &S) - p.Y.Sub(&S, &p.X). - Mul(&p.Y, &M). - Sub(&p.Y, &U) - p.ZZ.Mul(&V, &q.ZZ) - p.ZZZ.Mul(&W, &q.ZZZ) - - return p } // addMixed sets p to p+q in extended Jacobian coordinates, where a.ZZ=1. @@ -1079,27 +952,6 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { p.ZZ.Square(&res.Z) p.ZZZ.Mul(&p.ZZ, &res.Z) return p - var U, V, W, S, M, t fptower.E8 - - U.Double(&a.Y) - U.Neg(&U) - V.Square(&U) - W.Mul(&U, &V) - S.Mul(&a.X, &V) - t.Square(&a.X) - M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here - p.X.Square(&M) - t.Double(&S) - p.X.Sub(&p.X, &t) - t.Mul(&W, &a.Y) - p.Y.Sub(&S, &p.X). - Mul(&p.Y, &M). - Add(&p.Y, &t) - p.ZZ.Set(&V) - p.ZZZ.Set(&W) - - return p } // doubleMixed sets p to [2]a in Jacobian extended coordinates, where a.ZZ=1. @@ -1120,26 +972,6 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { p.ZZ.Square(&res.Z) p.ZZZ.Mul(&p.ZZ, &res.Z) return p - var U, V, W, S, M, t fptower.E8 - - U.Double(&a.Y) - V.Square(&U) - W.Mul(&U, &V) - S.Mul(&a.X, &V) - t.Square(&a.X) - M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here - p.X.Square(&M) - t.Double(&S) - p.X.Sub(&p.X, &t) - t.Mul(&W, &a.Y) - p.Y.Sub(&S, &p.X). - Mul(&p.Y, &M). - Sub(&p.Y, &t) - p.ZZ.Set(&V) - p.ZZZ.Set(&W) - - return p } // BatchJacobianToAffineG1 converts points in Jacobian coordinates to Affine coordinates diff --git a/internal/generator/ecc/template/point.go.tmpl b/internal/generator/ecc/template/point.go.tmpl index e7d717cfab..8441dd0d66 100644 --- a/internal/generator/ecc/template/point.go.tmpl +++ b/internal/generator/ecc/template/point.go.tmpl @@ -521,7 +521,7 @@ func (p *{{ $TJacobian }}) DoubleMixed(a *{{ $TAffine }}) *{{ $TJacobian }} { p.Y.Set(&y) p.Z.SetOne() return p - {{- end }} + {{- else }} var XX, YY, YYYY, S, M, T {{.CoordType}} XX.Square(&a.X) YY.Square(&a.Y) @@ -546,6 +546,7 @@ func (p *{{ $TJacobian }}) DoubleMixed(a *{{ $TAffine }}) *{{ $TJacobian }} { p.Z.Double(&a.Y) return p + {{- end }} } // AddMixed sets p to p+a in Jacobian coordinates, where a.Z = 1. @@ -621,7 +622,7 @@ func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { var a {{ $TAffine }} a.FromJacobian(p) return p.DoubleMixed(&a) - {{- end }} + {{- else }} var A, B, C, D, E, F, t {{.CoordType}} A.Square(&p.X) B.Square(&p.Y) @@ -646,6 +647,7 @@ func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { p.Y.Sub(&p.Y, &t) return p + {{- end }} } // Triple sets p to [3]q in Jacobian coordinates for j=0 curves. // @@ -656,7 +658,7 @@ func (p *{{ $TJacobian }}) Triple(q *{{ $TJacobian }}) *{{ $TJacobian }} { dbl.Double(q) p.Set(&dbl).AddAssign(q) return p - {{- end }} + {{- else }} // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *{{.CoordType}}) { {{- if eq .CoordType "fp.Element"}} @@ -719,6 +721,7 @@ func (p *{{ $TJacobian }}) Triple(q *{{ $TJacobian }}) *{{ $TJacobian }} { mulBy3(&p.Z) return p + {{- end }} } // ScalarMultiplication computes and returns p = [s]a @@ -2007,7 +2010,7 @@ func (p *{{ $TJacobianExtended }}) double(q *{{ $TJacobianExtended }}) *{{ $TJac p.ZZ.Square(&res.Z) p.ZZZ.Mul(&p.ZZ, &res.Z) return p - {{- end }} + {{- else }} var U, V, W, S, XX, M {{.CoordType}} U.Double(&q.Y) @@ -2029,6 +2032,7 @@ func (p *{{ $TJacobianExtended }}) double(q *{{ $TJacobianExtended }}) *{{ $TJac p.ZZZ.Mul(&W, &q.ZZZ) return p + {{- end }} } // addMixed sets p to p+q in extended Jacobian coordinates, where a.ZZ=1. @@ -2080,7 +2084,7 @@ func (p *{{ $TJacobianExtended }}) doubleMixed(a *{{ $TAffine }}) *{{ $TJacobian p.ZZ.Square(&res.Z) p.ZZZ.Mul(&p.ZZ, &res.Z) return p - {{- end }} + {{- else }} var U, V, W, S, M, t {{.all.CoordType}} U.Double(&a.Y) @@ -2108,6 +2112,7 @@ func (p *{{ $TJacobianExtended }}) doubleMixed(a *{{ $TAffine }}) *{{ $TJacobian p.ZZZ.Set(&W) return p + {{- end }} {{- end}} {{define "mAdd" }} From 4eac2c87943e6c83bcb6fe06d2d1c5c2cca0d480 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 22 Apr 2026 14:46:35 -0400 Subject: [PATCH 13/33] refactor: clean code generation --- ecc/bls12-377/g1.go | 24 +- ecc/bls12-377/g2.go | 24 +- ecc/bls12-377/marshal.go | 8 - ecc/bls12-377/marshal_test.go | 2 +- ecc/bls12-381/g1.go | 24 +- ecc/bls12-381/g2.go | 24 +- ecc/bls12-381/marshal.go | 8 - ecc/bls12-381/marshal_test.go | 2 +- ecc/bls24-315/g1.go | 24 +- ecc/bls24-315/g2.go | 24 +- ecc/bls24-315/marshal.go | 8 - ecc/bls24-315/marshal_test.go | 2 +- ecc/bls24-317/g1.go | 24 +- ecc/bls24-317/g2.go | 24 +- ecc/bls24-317/marshal.go | 8 - ecc/bls24-317/marshal_test.go | 2 +- ecc/bn254/g1.go | 26 +- ecc/bn254/g2.go | 24 +- ecc/bn254/marshal.go | 8 - ecc/bn254/marshal_test.go | 2 +- ecc/bw6-633/g1.go | 24 +- ecc/bw6-633/g2.go | 24 +- ecc/bw6-633/marshal.go | 8 - ecc/bw6-633/marshal_test.go | 2 +- ecc/bw6-761/g1.go | 24 +- ecc/bw6-761/g2.go | 24 +- ecc/bw6-761/marshal.go | 8 - ecc/bw6-761/marshal_test.go | 2 +- ecc/grumpkin/g1.go | 30 +- ecc/grumpkin/marshal.go | 10 +- ecc/grumpkin/marshal_test.go | 19 +- ecc/grumpkin/multiexp_test.go | 12 +- ecc/secp256k1/ecdsa/ecdsa.go | 2 +- ecc/secp256k1/g1.go | 30 +- ecc/secp256k1/marshal.go | 287 +-------- ecc/secp256k1/marshal_test.go | 71 +-- ecc/secp256r1/marshal.go | 573 +++++++++++++++++- ecc/secp256r1/marshal_test.go | 172 +++++- ecc/stark-curve/marshal.go | 436 +++++++++---- ecc/stark-curve/marshal_test.go | 85 ++- .../ecc/template/tests/point.go.tmpl | 12 +- .../generator/ecdsa/template/ecdsa.go.tmpl | 22 +- 42 files changed, 1430 insertions(+), 739 deletions(-) diff --git a/ecc/bls12-377/g1.go b/ecc/bls12-377/g1.go index 5a2d550bec..08f4c1e4ac 100644 --- a/ecc/bls12-377/g1.go +++ b/ecc/bls12-377/g1.go @@ -407,7 +407,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -499,7 +499,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -515,9 +515,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -1041,20 +1041,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1199,7 +1201,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1227,7 +1229,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls12-377/g2.go b/ecc/bls12-377/g2.go index 16552bb4eb..d824942c10 100644 --- a/ecc/bls12-377/g2.go +++ b/ecc/bls12-377/g2.go @@ -412,7 +412,7 @@ func (p *G2Jac) DoubleMixed(a *G2Affine) *G2Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -504,7 +504,7 @@ func (p *G2Jac) DoubleAssign() *G2Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -520,9 +520,9 @@ func (p *G2Jac) DoubleAssign() *G2Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G2Jac) Triple(q *G2Jac) *G2Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fptower.E2) { @@ -1075,20 +1075,22 @@ func (p *g2JacExtended) add(q *g2JacExtended) *g2JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g2JacExtended) double(q *g2JacExtended) *g2JacExtended { - var U, V, W, S, XX, M fptower.E2 + var U, V, W, S, M fptower.E2 U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fptower.E2 + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1233,7 +1235,7 @@ func (p *g2JacExtended) doubleNegMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1261,7 +1263,7 @@ func (p *g2JacExtended) doubleMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls12-377/marshal.go b/ecc/bls12-377/marshal.go index b9bb501151..6927134e18 100644 --- a/ecc/bls12-377/marshal.go +++ b/ecc/bls12-377/marshal.go @@ -859,9 +859,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -871,7 +869,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -890,7 +887,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -1122,9 +1118,7 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -1138,7 +1132,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // p.X.A1 | p.X.A0 fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X.A1) fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[48:48+fp.Bytes]), p.X.A0) - res[0] |= mUncompressed return @@ -1157,7 +1150,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { func (p *G2Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G2Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG2AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/bls12-377/marshal_test.go b/ecc/bls12-377/marshal_test.go index 84e710d59a..eae674db52 100644 --- a/ecc/bls12-377/marshal_test.go +++ b/ecc/bls12-377/marshal_test.go @@ -105,6 +105,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -164,7 +165,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine diff --git a/ecc/bls12-381/g1.go b/ecc/bls12-381/g1.go index 4c5bb4dd60..a9f29a7362 100644 --- a/ecc/bls12-381/g1.go +++ b/ecc/bls12-381/g1.go @@ -416,7 +416,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -508,7 +508,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -524,9 +524,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -1059,20 +1059,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1217,7 +1219,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1245,7 +1247,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls12-381/g2.go b/ecc/bls12-381/g2.go index f3b048ffaa..3f99749ecb 100644 --- a/ecc/bls12-381/g2.go +++ b/ecc/bls12-381/g2.go @@ -420,7 +420,7 @@ func (p *G2Jac) DoubleMixed(a *G2Affine) *G2Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -512,7 +512,7 @@ func (p *G2Jac) DoubleAssign() *G2Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -528,9 +528,9 @@ func (p *G2Jac) DoubleAssign() *G2Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G2Jac) Triple(q *G2Jac) *G2Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fptower.E2) { @@ -1091,20 +1091,22 @@ func (p *g2JacExtended) add(q *g2JacExtended) *g2JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g2JacExtended) double(q *g2JacExtended) *g2JacExtended { - var U, V, W, S, XX, M fptower.E2 + var U, V, W, S, M fptower.E2 U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fptower.E2 + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1249,7 +1251,7 @@ func (p *g2JacExtended) doubleNegMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1277,7 +1279,7 @@ func (p *g2JacExtended) doubleMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls12-381/marshal.go b/ecc/bls12-381/marshal.go index 885228ca7c..5690aefe7d 100644 --- a/ecc/bls12-381/marshal.go +++ b/ecc/bls12-381/marshal.go @@ -859,9 +859,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -871,7 +869,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -890,7 +887,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -1122,9 +1118,7 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -1138,7 +1132,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // p.X.A1 | p.X.A0 fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X.A1) fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[48:48+fp.Bytes]), p.X.A0) - res[0] |= mUncompressed return @@ -1157,7 +1150,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { func (p *G2Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G2Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG2AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/bls12-381/marshal_test.go b/ecc/bls12-381/marshal_test.go index ca519dd057..aca4a67f3f 100644 --- a/ecc/bls12-381/marshal_test.go +++ b/ecc/bls12-381/marshal_test.go @@ -105,6 +105,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -164,7 +165,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine diff --git a/ecc/bls24-315/g1.go b/ecc/bls24-315/g1.go index 5872bf92b5..59671459b5 100644 --- a/ecc/bls24-315/g1.go +++ b/ecc/bls24-315/g1.go @@ -407,7 +407,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -499,7 +499,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -515,9 +515,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -1014,20 +1014,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1172,7 +1174,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1200,7 +1202,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls24-315/g2.go b/ecc/bls24-315/g2.go index 06e71b8b09..440a443b8a 100644 --- a/ecc/bls24-315/g2.go +++ b/ecc/bls24-315/g2.go @@ -412,7 +412,7 @@ func (p *G2Jac) DoubleMixed(a *G2Affine) *G2Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -504,7 +504,7 @@ func (p *G2Jac) DoubleAssign() *G2Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -520,9 +520,9 @@ func (p *G2Jac) DoubleAssign() *G2Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G2Jac) Triple(q *G2Jac) *G2Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fptower.E4) { @@ -981,20 +981,22 @@ func (p *g2JacExtended) add(q *g2JacExtended) *g2JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g2JacExtended) double(q *g2JacExtended) *g2JacExtended { - var U, V, W, S, XX, M fptower.E4 + var U, V, W, S, M fptower.E4 U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fptower.E4 + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1139,7 +1141,7 @@ func (p *g2JacExtended) doubleNegMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1167,7 +1169,7 @@ func (p *g2JacExtended) doubleMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls24-315/marshal.go b/ecc/bls24-315/marshal.go index 55b26affb3..53ab6507e6 100644 --- a/ecc/bls24-315/marshal.go +++ b/ecc/bls24-315/marshal.go @@ -859,9 +859,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -871,7 +869,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -890,7 +887,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -1124,9 +1120,7 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -1144,7 +1138,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[40:40+fp.Bytes]), p.X.B1.A0) fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[80:80+fp.Bytes]), p.X.B0.A1) fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[120:120+fp.Bytes]), p.X.B0.A0) - res[0] |= mUncompressed return @@ -1163,7 +1156,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { func (p *G2Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G2Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG2AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/bls24-315/marshal_test.go b/ecc/bls24-315/marshal_test.go index c32faacf08..1b252f3998 100644 --- a/ecc/bls24-315/marshal_test.go +++ b/ecc/bls24-315/marshal_test.go @@ -105,6 +105,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -164,7 +165,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine diff --git a/ecc/bls24-317/g1.go b/ecc/bls24-317/g1.go index e7af58ba0d..0aab2f55e5 100644 --- a/ecc/bls24-317/g1.go +++ b/ecc/bls24-317/g1.go @@ -407,7 +407,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -499,7 +499,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -515,9 +515,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -1015,20 +1015,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1173,7 +1175,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1201,7 +1203,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls24-317/g2.go b/ecc/bls24-317/g2.go index a2565b729f..2a60cb9bb3 100644 --- a/ecc/bls24-317/g2.go +++ b/ecc/bls24-317/g2.go @@ -412,7 +412,7 @@ func (p *G2Jac) DoubleMixed(a *G2Affine) *G2Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -504,7 +504,7 @@ func (p *G2Jac) DoubleAssign() *G2Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -520,9 +520,9 @@ func (p *G2Jac) DoubleAssign() *G2Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G2Jac) Triple(q *G2Jac) *G2Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fptower.E4) { @@ -981,20 +981,22 @@ func (p *g2JacExtended) add(q *g2JacExtended) *g2JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g2JacExtended) double(q *g2JacExtended) *g2JacExtended { - var U, V, W, S, XX, M fptower.E4 + var U, V, W, S, M fptower.E4 U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fptower.E4 + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1139,7 +1141,7 @@ func (p *g2JacExtended) doubleNegMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1167,7 +1169,7 @@ func (p *g2JacExtended) doubleMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bls24-317/marshal.go b/ecc/bls24-317/marshal.go index c74dfd058e..9ab87d6bcc 100644 --- a/ecc/bls24-317/marshal.go +++ b/ecc/bls24-317/marshal.go @@ -859,9 +859,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -871,7 +869,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -890,7 +887,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -1124,9 +1120,7 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -1144,7 +1138,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[40:40+fp.Bytes]), p.X.B1.A0) fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[80:80+fp.Bytes]), p.X.B0.A1) fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[120:120+fp.Bytes]), p.X.B0.A0) - res[0] |= mUncompressed return @@ -1163,7 +1156,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { func (p *G2Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G2Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG2AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/bls24-317/marshal_test.go b/ecc/bls24-317/marshal_test.go index 218e6bda01..b7c05ef05a 100644 --- a/ecc/bls24-317/marshal_test.go +++ b/ecc/bls24-317/marshal_test.go @@ -105,6 +105,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -164,7 +165,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine diff --git a/ecc/bn254/g1.go b/ecc/bn254/g1.go index b8475c4dac..b9a2aded29 100644 --- a/ecc/bn254/g1.go +++ b/ecc/bn254/g1.go @@ -405,7 +405,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -497,7 +497,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -513,9 +513,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -636,9 +636,7 @@ func (p *G1Jac) IsOnCurve() bool { // the curve is of prime order i.e. E(𝔽p) is the full group // so we just check that the point is on the curve. func (p *G1Jac) IsInSubGroup() bool { - return p.IsOnCurve() - } // mulWindowed computes a double-and-add scalar multiplication p=[s]q in @@ -1041,20 +1039,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1199,7 +1199,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1227,7 +1227,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bn254/g2.go b/ecc/bn254/g2.go index badc36bf4f..34191ca67c 100644 --- a/ecc/bn254/g2.go +++ b/ecc/bn254/g2.go @@ -425,7 +425,7 @@ func (p *G2Jac) DoubleMixed(a *G2Affine) *G2Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -517,7 +517,7 @@ func (p *G2Jac) DoubleAssign() *G2Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -533,9 +533,9 @@ func (p *G2Jac) DoubleAssign() *G2Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G2Jac) Triple(q *G2Jac) *G2Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fptower.E2) { @@ -1140,20 +1140,22 @@ func (p *g2JacExtended) add(q *g2JacExtended) *g2JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g2JacExtended) double(q *g2JacExtended) *g2JacExtended { - var U, V, W, S, XX, M fptower.E2 + var U, V, W, S, M fptower.E2 U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fptower.E2 + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1298,7 +1300,7 @@ func (p *g2JacExtended) doubleNegMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1326,7 +1328,7 @@ func (p *g2JacExtended) doubleMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bn254/marshal.go b/ecc/bn254/marshal.go index 3c94ab4651..c3192a068c 100644 --- a/ecc/bn254/marshal.go +++ b/ecc/bn254/marshal.go @@ -826,9 +826,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressed - return } @@ -838,7 +836,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -857,7 +854,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -1078,9 +1074,7 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressed - return } @@ -1094,7 +1088,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // p.X.A1 | p.X.A0 fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X.A1) fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[32:32+fp.Bytes]), p.X.A0) - res[0] |= mUncompressed return @@ -1113,7 +1106,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { func (p *G2Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G2Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG2AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/bn254/marshal_test.go b/ecc/bn254/marshal_test.go index 53c079b254..d8ec00dff6 100644 --- a/ecc/bn254/marshal_test.go +++ b/ecc/bn254/marshal_test.go @@ -105,6 +105,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -164,7 +165,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine diff --git a/ecc/bw6-633/g1.go b/ecc/bw6-633/g1.go index 97fb84c9f9..914eb8c44e 100644 --- a/ecc/bw6-633/g1.go +++ b/ecc/bw6-633/g1.go @@ -407,7 +407,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -499,7 +499,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -515,9 +515,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -1037,20 +1037,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1195,7 +1197,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1223,7 +1225,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bw6-633/g2.go b/ecc/bw6-633/g2.go index d3504e15e3..4e8b974026 100644 --- a/ecc/bw6-633/g2.go +++ b/ecc/bw6-633/g2.go @@ -412,7 +412,7 @@ func (p *G2Jac) DoubleMixed(a *G2Affine) *G2Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -504,7 +504,7 @@ func (p *G2Jac) DoubleAssign() *G2Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -520,9 +520,9 @@ func (p *G2Jac) DoubleAssign() *G2Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G2Jac) Triple(q *G2Jac) *G2Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -969,20 +969,22 @@ func (p *g2JacExtended) add(q *g2JacExtended) *g2JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g2JacExtended) double(q *g2JacExtended) *g2JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1127,7 +1129,7 @@ func (p *g2JacExtended) doubleNegMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1155,7 +1157,7 @@ func (p *g2JacExtended) doubleMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bw6-633/marshal.go b/ecc/bw6-633/marshal.go index ad5b5703d3..8806812838 100644 --- a/ecc/bw6-633/marshal.go +++ b/ecc/bw6-633/marshal.go @@ -859,9 +859,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -871,7 +869,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -890,7 +887,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -1120,9 +1116,7 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -1132,7 +1126,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -1151,7 +1144,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { func (p *G2Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G2Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG2AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/bw6-633/marshal_test.go b/ecc/bw6-633/marshal_test.go index 2d40e85d9e..f8c8edf0d0 100644 --- a/ecc/bw6-633/marshal_test.go +++ b/ecc/bw6-633/marshal_test.go @@ -105,6 +105,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -164,7 +165,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine diff --git a/ecc/bw6-761/g1.go b/ecc/bw6-761/g1.go index 3b46dbf5da..2c50e42e96 100644 --- a/ecc/bw6-761/g1.go +++ b/ecc/bw6-761/g1.go @@ -407,7 +407,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -499,7 +499,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -515,9 +515,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -1078,20 +1078,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1236,7 +1238,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1264,7 +1266,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bw6-761/g2.go b/ecc/bw6-761/g2.go index 48d19303bf..57e0439217 100644 --- a/ecc/bw6-761/g2.go +++ b/ecc/bw6-761/g2.go @@ -412,7 +412,7 @@ func (p *G2Jac) DoubleMixed(a *G2Affine) *G2Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -504,7 +504,7 @@ func (p *G2Jac) DoubleAssign() *G2Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -520,9 +520,9 @@ func (p *G2Jac) DoubleAssign() *G2Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G2Jac) Triple(q *G2Jac) *G2Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -1007,20 +1007,22 @@ func (p *g2JacExtended) add(q *g2JacExtended) *g2JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g2JacExtended) double(q *g2JacExtended) *g2JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1165,7 +1167,7 @@ func (p *g2JacExtended) doubleNegMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1193,7 +1195,7 @@ func (p *g2JacExtended) doubleMixed(a *G2Affine) *g2JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/bw6-761/marshal.go b/ecc/bw6-761/marshal.go index f59e87307a..867d460e63 100644 --- a/ecc/bw6-761/marshal.go +++ b/ecc/bw6-761/marshal.go @@ -859,9 +859,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -871,7 +869,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -890,7 +887,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -1120,9 +1116,7 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return } @@ -1132,7 +1126,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -1151,7 +1144,6 @@ func (p *G2Affine) RawBytes() (res [SizeOfG2AffineUncompressed]byte) { func (p *G2Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G2Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG2AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/bw6-761/marshal_test.go b/ecc/bw6-761/marshal_test.go index 35e39dd869..04964c751b 100644 --- a/ecc/bw6-761/marshal_test.go +++ b/ecc/bw6-761/marshal_test.go @@ -105,6 +105,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -164,7 +165,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine diff --git a/ecc/grumpkin/g1.go b/ecc/grumpkin/g1.go index 3853afd8cd..dfb8b0b931 100644 --- a/ecc/grumpkin/g1.go +++ b/ecc/grumpkin/g1.go @@ -206,9 +206,7 @@ func (p *G1Affine) IsOnCurve() bool { // IsInSubGroup returns true if the affine point p is in the correct subgroup, false otherwise. func (p *G1Affine) IsInSubGroup() bool { - var _p G1Jac - _p.FromAffine(p) - return _p.IsInSubGroup() + return p.IsOnCurve() } // IsInSubGroupBatchG1 checks if a batch of points P_i are in G1. @@ -407,7 +405,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -499,7 +497,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -515,9 +513,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -637,9 +635,7 @@ func (p *G1Jac) IsOnCurve() bool { // the curve is of prime order i.e. E(𝔽p) is the full group // so we just check that the point is on the curve. func (p *G1Jac) IsInSubGroup() bool { - return p.IsOnCurve() - } // mulWindowed computes a double-and-add scalar multiplication p=[s]q in @@ -974,20 +970,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1132,7 +1130,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1160,7 +1158,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/grumpkin/marshal.go b/ecc/grumpkin/marshal.go index 6e0529b1ad..ecade4fe28 100644 --- a/ecc/grumpkin/marshal.go +++ b/ecc/grumpkin/marshal.go @@ -17,7 +17,7 @@ import ( "github.com/consensys/gnark-crypto/parallel" ) -// To encode G1Affine and G2Affine points, we mask the most significant bits with these bits to specify without ambiguity +// To encode G1Affine points, we mask the most significant bits with these bits to specify without ambiguity // metadata needed for point (de)compression // we have less than 3 bits available on the msw, so we can't follow BLS12-381 style encoding. // the difference is the case where a point is infinity and uncompressed is not flagged @@ -61,7 +61,7 @@ func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { } // Decode reads the binary encoding of v from the stream -// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, *[]G1Affine or *[]G2Affine +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *[]G1Affine func (dec *Decoder) Decode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { @@ -315,7 +315,7 @@ func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { } // Encode writes the binary encoding of v to the stream -// type must be uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, []G1Affine, []G2Affine, *[]G1Affine or *[]G2Affine +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, []G1Affine, *[]G1Affine func (enc *Encoder) Encode(v any) (err error) { if enc.raw { return enc.encodeRaw(v) @@ -690,9 +690,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressed - return } @@ -702,7 +700,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -721,7 +718,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer diff --git a/ecc/grumpkin/marshal_test.go b/ecc/grumpkin/marshal_test.go index 9fb5011104..0f6770d825 100644 --- a/ecc/grumpkin/marshal_test.go +++ b/ecc/grumpkin/marshal_test.go @@ -7,6 +7,7 @@ package grumpkin import ( "bytes" + crand "crypto/rand" "io" "math/big" "math/rand/v2" @@ -97,6 +98,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -153,10 +155,10 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine + g1 = g1GenAff { @@ -303,3 +305,18 @@ func GenFp() gopter.Gen { return gopter.NewGenResult(elmt, gopter.NoShrinker) } } + +// GenBigInt generates a big.Int +func GenBigInt() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var s big.Int + var b [fp.Bytes]byte + _, err := crand.Read(b[:]) + if err != nil { + panic(err) + } + s.SetBytes(b[:]) + genResult := gopter.NewGenResult(s, gopter.NoShrinker) + return genResult + } +} diff --git a/ecc/grumpkin/multiexp_test.go b/ecc/grumpkin/multiexp_test.go index 2814eab547..274787b5d6 100644 --- a/ecc/grumpkin/multiexp_test.go +++ b/ecc/grumpkin/multiexp_test.go @@ -271,12 +271,12 @@ func TestCrossMultiExpG1(t *testing.T) { } -// _innerMsmG1Reference always do ext jacobian with c == 15 +// _innerMsmG1Reference always do ext jacobian with c == 16 func _innerMsmG1Reference(p *G1Jac, points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) *G1Jac { // partition the scalars - digits, _ := partitionScalars(scalars, 15, config.NbTasks) + digits, _ := partitionScalars(scalars, 16, config.NbTasks) - nbChunks := computeNbChunks(15) + nbChunks := computeNbChunks(16) // for each chunk, spawn one go routine that'll loop through all the scalars in the // corresponding bit-window @@ -291,11 +291,11 @@ func _innerMsmG1Reference(p *G1Jac, points []G1Affine, scalars []fr.Element, con // the last chunk may be processed with a different method than the rest, as it could be smaller. n := len(points) for j := int(nbChunks - 1); j >= 0; j-- { - processChunk := processChunkG1Jacobian[bucketg1JacExtendedC15] - go processChunk(uint64(j), chChunks[j], 15, points, digits[j*n:(j+1)*n], nil) + processChunk := processChunkG1Jacobian[bucketg1JacExtendedC16] + go processChunk(uint64(j), chChunks[j], 16, points, digits[j*n:(j+1)*n], nil) } - return msmReduceChunkG1Affine(p, int(15), chChunks[:]) + return msmReduceChunkG1Affine(p, int(16), chChunks[:]) } func BenchmarkMultiExpG1(b *testing.B) { diff --git a/ecc/secp256k1/ecdsa/ecdsa.go b/ecc/secp256k1/ecdsa/ecdsa.go index 73c5ca8f1d..9405dfe0f9 100644 --- a/ecc/secp256k1/ecdsa/ecdsa.go +++ b/ecc/secp256k1/ecdsa/ecdsa.go @@ -26,7 +26,7 @@ const ( sizeFr = fr.Bytes sizeFrBits = fr.Bits sizeFp = fp.Bytes - sizePublicKey = secp256k1.SizeOfG1AffineUncompressed + sizePublicKey = 2 * sizeFp sizePrivateKey = sizeFr + sizePublicKey sizeSignature = 2 * sizeFr ) diff --git a/ecc/secp256k1/g1.go b/ecc/secp256k1/g1.go index c9ac075f2c..722b6c57b3 100644 --- a/ecc/secp256k1/g1.go +++ b/ecc/secp256k1/g1.go @@ -206,9 +206,7 @@ func (p *G1Affine) IsOnCurve() bool { // IsInSubGroup returns true if the affine point p is in the correct subgroup, false otherwise. func (p *G1Affine) IsInSubGroup() bool { - var _p G1Jac - _p.FromAffine(p) - return _p.IsInSubGroup() + return p.IsOnCurve() } // IsInSubGroupBatchG1 checks if a batch of points P_i are in G1. @@ -407,7 +405,7 @@ func (p *G1Jac) DoubleMixed(a *G1Affine) *G1Jac { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -499,7 +497,7 @@ func (p *G1Jac) DoubleAssign() *G1Jac { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -515,9 +513,9 @@ func (p *G1Jac) DoubleAssign() *G1Jac { return p } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +// Triple sets p to [3]q in Jacobian coordinates. // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) func (p *G1Jac) Triple(q *G1Jac) *G1Jac { // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *fp.Element) { @@ -637,9 +635,7 @@ func (p *G1Jac) IsOnCurve() bool { // the curve is of prime order i.e. E(𝔽p) is the full group // so we just check that the point is on the curve. func (p *G1Jac) IsInSubGroup() bool { - return p.IsOnCurve() - } // mulWindowed computes a double-and-add scalar multiplication p=[s]q in @@ -974,20 +970,22 @@ func (p *g1JacExtended) add(q *g1JacExtended) *g1JacExtended { // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *g1JacExtended) double(q *g1JacExtended) *g1JacExtended { - var U, V, W, S, XX, M fp.Element + var U, V, W, S, M fp.Element U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + { + var XX fp.Element + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + } U.Mul(&W, &q.Y) p.X.Square(&M). @@ -1132,7 +1130,7 @@ func (p *g1JacExtended) doubleNegMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -1160,7 +1158,7 @@ func (p *g1JacExtended) doubleMixed(a *G1Affine) *g1JacExtended { S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) diff --git a/ecc/secp256k1/marshal.go b/ecc/secp256k1/marshal.go index 0cf84fa87f..e9c77127f5 100644 --- a/ecc/secp256k1/marshal.go +++ b/ecc/secp256k1/marshal.go @@ -10,25 +10,9 @@ import ( "errors" "io" "reflect" - "sync/atomic" "github.com/consensys/gnark-crypto/ecc/secp256k1/fp" "github.com/consensys/gnark-crypto/ecc/secp256k1/fr" - "github.com/consensys/gnark-crypto/parallel" -) - -// SEC1-style point prefix bytes for exact-byte field elements. -const ( - mMask byte = 0xff - mCompressedInfinity byte = 0x00 - mCompressedSmallest byte = 0x02 - mCompressedLargest byte = 0x03 - mUncompressed byte = 0x04 -) - -var ( - ErrInvalidInfinityEncoding = errors.New("invalid infinity point encoding") - ErrInvalidEncoding = errors.New("invalid point encoding") ) // Encoder writes secp256k1 object values to an output stream @@ -58,7 +42,7 @@ func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { } // Decode reads the binary encoding of v from the stream -// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, *[]G1Affine or *[]G2Affine +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *[]G1Affine func (dec *Decoder) Decode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { @@ -168,25 +152,13 @@ func (dec *Decoder) Decode(v any) (err error) { } return case *G1Affine: - // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. - read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) + // no point compression + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineUncompressed]) dec.n += int64(read) if err != nil { return } - nbBytes := SizeOfG1AffineCompressed - - // most significant byte contains metadata - if !isCompressed(buf[0]) { - nbBytes = SizeOfG1AffineUncompressed - // we read more. - read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) - dec.n += int64(read) - if err != nil { - return - } - } - _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) + _, err = t.setBytes(buf[:SizeOfG1AffineUncompressed], dec.subGroupCheck) return case *[]G1Affine: sliceLen, err = dec.readUint32() @@ -196,54 +168,15 @@ func (dec *Decoder) Decode(v any) (err error) { if len(*t) != int(sliceLen) || *t == nil { *t = make([]G1Affine, sliceLen) } - compressed := make([]bool, sliceLen) for i := range len(*t) { - - // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. - read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineUncompressed]) dec.n += int64(read) if err != nil { return } - nbBytes := SizeOfG1AffineCompressed - - // most significant byte contains metadata - if !isCompressed(buf[0]) { - nbBytes = SizeOfG1AffineUncompressed - // we read more. - read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) - dec.n += int64(read) - if err != nil { - return - } - _, err = (*t)[i].setBytes(buf[:nbBytes], false) - if err != nil { - return - } - } else { - var r bool - if r, err = (*t)[i].unsafeSetCompressedBytes(buf[:nbBytes]); err != nil { - return - } - compressed[i] = !r - } - } - var nbErrs uint64 - parallel.Execute(len(compressed), func(start, end int) { - for i := start; i < end; i++ { - if compressed[i] { - if err := (*t)[i].unsafeComputeY(dec.subGroupCheck); err != nil { - atomic.AddUint64(&nbErrs, 1) - } - } else if dec.subGroupCheck { - if !(*t)[i].IsInSubGroup() { - atomic.AddUint64(&nbErrs, 1) - } - } + if _, err = (*t)[i].setBytes(buf[:SizeOfG1AffineUncompressed], dec.subGroupCheck); err != nil { + return } - }) - if nbErrs != 0 { - return errors.New("point decompression failed") } return nil @@ -289,10 +222,6 @@ func (dec *Decoder) readUint64() (r uint64, err error) { return } -func isCompressed(msb byte) bool { - return msb != mUncompressed -} - // NewEncoder returns a binary encoder supporting curve secp256k1 objects func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { // default settings @@ -311,7 +240,7 @@ func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { } // Encode writes the binary encoding of v to the stream -// type must be uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, []G1Affine, []G2Affine, *[]G1Affine or *[]G2Affine +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, []G1Affine, *[]G1Affine func (enc *Encoder) Encode(v any) (err error) { if enc.raw { return enc.encodeRaw(v) @@ -340,19 +269,6 @@ func NoSubgroupChecks() func(*Decoder) { } } -// isZeroed checks that the provided bytes are at 0 -func isZeroed(firstByte byte, buf []byte) bool { - if firstByte != 0 { - return false - } - for _, b := range buf { - if b != 0 { - return false - } - } - return true -} - func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { @@ -446,7 +362,7 @@ func (enc *Encoder) encode(v any) (err error) { } enc.n += 4 - var buf [SizeOfG1AffineCompressed]byte + var buf [SizeOfG1AffineUncompressed]byte for i := range len(t) { buf = t[i].Bytes() @@ -628,11 +544,8 @@ func (enc *Encoder) writeUint32(a uint32) error { return err } -// SizeOfG1AffineCompressed represents the size in bytes that a G1Affine need in binary form, compressed -const SizeOfG1AffineCompressed = 1 + fp.Bytes - // SizeOfG1AffineUncompressed represents the size in bytes that a G1Affine need in binary form, uncompressed -const SizeOfG1AffineUncompressed = 1 + 2*fp.Bytes +const SizeOfG1AffineUncompressed = 32 * 2 // Marshal converts p to a byte slice (without point compression) func (p *G1Affine) Marshal() []byte { @@ -646,26 +559,10 @@ func (p *G1Affine) Unmarshal(buf []byte) error { return err } -// Bytes returns SEC1-style compressed binary representation of p. -func (p *G1Affine) Bytes() (res [SizeOfG1AffineCompressed]byte) { - - // check if p is infinity point - if p.X.IsZero() && p.Y.IsZero() { - res[0] = mCompressedInfinity - return - } - - msbMask := mCompressedSmallest - // compressed, we need to know if Y is lexicographically bigger than -Y - // if p.Y ">" -p.Y - if p.Y.LexicographicallyLargest() { - msbMask = mCompressedLargest - } - - res[0] = msbMask - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X) - - return +// Bytes returns binary representation of p (stores X and Y coordinate) +// point compression is not available for this curve +func (p *G1Affine) Bytes() (res [SizeOfG1AffineUncompressed]byte) { + return p.RawBytes() } // RawBytes returns binary representation of p (stores X and Y coordinate) @@ -674,15 +571,15 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - - res[0] = mUncompressed - return } - res[0] = mUncompressed - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1+fp.Bytes:1+2*fp.Bytes]), p.Y) + // not compressed + // we store the Y coordinate + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[32:32+fp.Bytes]), p.Y) + + // we store X and mask the most significant word with our metadata mask + fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) return } @@ -701,154 +598,24 @@ func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } +// no point compression - setBytes only handles uncompressed format func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { - if len(buf) < SizeOfG1AffineCompressed { + if len(buf) < SizeOfG1AffineUncompressed { return 0, io.ErrShortBuffer } - mData := buf[0] - - // check buffer size - if mData == mUncompressed { - if len(buf) < SizeOfG1AffineUncompressed { - return 0, io.ErrShortBuffer - } - } - - // infinity encoded, we still check that the buffer is full of zeroes. - if mData == mCompressedInfinity { - if !isZeroed(0, buf[1:SizeOfG1AffineCompressed]) { - return 0, ErrInvalidInfinityEncoding - } - p.X.SetZero() - p.Y.SetZero() - return SizeOfG1AffineCompressed, nil - } - - // uncompressed point - if mData == mUncompressed { - // read X and Y coordinates - if err := p.X.SetBytesCanonical(buf[1 : 1+fp.Bytes]); err != nil { - return 0, err - } - if err := p.Y.SetBytesCanonical(buf[1+fp.Bytes : 1+2*fp.Bytes]); err != nil { - return 0, err - } - - // subgroup check - if subGroupCheck && !p.IsInSubGroup() { - return 0, errors.New("invalid point: subgroup check failed") - } - - return SizeOfG1AffineUncompressed, nil - } - - // we have a compressed coordinate - if mData != mCompressedSmallest && mData != mCompressedLargest { - return 0, ErrInvalidEncoding - } - - // read X coordinate - if err := p.X.SetBytesCanonical(buf[1 : 1+fp.Bytes]); err != nil { + // read X and Y coordinates + if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { return 0, err } - - var YSquared, Y fp.Element - - YSquared.Square(&p.X).Mul(&YSquared, &p.X) - YSquared.Add(&YSquared, &bCurveCoeff) - if Y.Sqrt(&YSquared) == nil { - return 0, errors.New("invalid compressed coordinate: square root doesn't exist") - } - - if Y.LexicographicallyLargest() { - // Y ">" -Y - if mData == mCompressedSmallest { - Y.Neg(&Y) - } - } else { - // Y "<=" -Y - if mData == mCompressedLargest { - Y.Neg(&Y) - } + if err := p.Y.SetBytesCanonical(buf[fp.Bytes : fp.Bytes*2]); err != nil { + return 0, err } - p.Y = Y - // subgroup check if subGroupCheck && !p.IsInSubGroup() { return 0, errors.New("invalid point: subgroup check failed") } - return SizeOfG1AffineCompressed, nil -} - -// unsafeComputeY called by Decoder when processing slices of compressed point in parallel (step 2) -// it computes the Y coordinate from the already set X coordinate and is compute intensive -func (p *G1Affine) unsafeComputeY(subGroupCheck bool) error { - // stored in unsafeSetCompressedBytes - - mData := byte(p.Y[0]) - - // we have a compressed coordinate, we need to solve the curve equation to compute Y - var YSquared, Y fp.Element - - YSquared.Square(&p.X).Mul(&YSquared, &p.X) - YSquared.Add(&YSquared, &bCurveCoeff) - if Y.Sqrt(&YSquared) == nil { - return errors.New("invalid compressed coordinate: square root doesn't exist") - } - - if Y.LexicographicallyLargest() { - // Y ">" -Y - if mData == mCompressedSmallest { - Y.Neg(&Y) - } - } else { - // Y "<=" -Y - if mData == mCompressedLargest { - Y.Neg(&Y) - } - } - - p.Y = Y - - // subgroup check - if subGroupCheck && !p.IsInSubGroup() { - return errors.New("invalid point: subgroup check failed") - } - - return nil -} - -// unsafeSetCompressedBytes is called by Decoder when processing slices of compressed point in parallel (step 1) -// assumes buf[:8] mask is set to compressed -// returns true if point is infinity and need no further processing -// it sets X coordinate and uses Y for scratch space to store decompression metadata -func (p *G1Affine) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, err error) { - - mData := buf[0] - - if mData == mCompressedInfinity { - isInfinity = true - if !isZeroed(0, buf[1:SizeOfG1AffineCompressed]) { - return isInfinity, ErrInvalidInfinityEncoding - } - p.X.SetZero() - p.Y.SetZero() - return isInfinity, nil - } - if mData != mCompressedSmallest && mData != mCompressedLargest { - return false, ErrInvalidEncoding - } - - // read X coordinate - if err := p.X.SetBytesCanonical(buf[1 : 1+fp.Bytes]); err != nil { - return false, err - } - // store mData in p.Y[0] - p.Y[0] = uint64(mData) - - // recomputing Y will be done asynchronously - return isInfinity, nil + return SizeOfG1AffineUncompressed, nil } diff --git a/ecc/secp256k1/marshal_test.go b/ecc/secp256k1/marshal_test.go index ae324b28f8..d132d1800f 100644 --- a/ecc/secp256k1/marshal_test.go +++ b/ecc/secp256k1/marshal_test.go @@ -7,6 +7,7 @@ package secp256k1 import ( "bytes" + crand "crypto/rand" "io" "math/big" "math/rand/v2" @@ -97,6 +98,7 @@ func TestEncoder(t *testing.T) { var outL [][]fr.Element var outM [][]uint64 var outN [][][]fr.Element + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { @@ -154,62 +156,10 @@ func TestEncoder(t *testing.T) { } -func TestIsCompressed(t *testing.T) { - t.Parallel() - var g1Inf, g1 G1Affine - g1 = g1GenAff - - { - b := g1Inf.Bytes() - if !isCompressed(b[0]) { - t.Fatal("g1Inf.Bytes() should be compressed") - } - } - - { - b := g1Inf.RawBytes() - if isCompressed(b[0]) { - t.Fatal("g1Inf.RawBytes() should be uncompressed") - } - } - - { - b := g1.Bytes() - if !isCompressed(b[0]) { - t.Fatal("g1.Bytes() should be compressed") - } - } - - { - b := g1.RawBytes() - if isCompressed(b[0]) { - t.Fatal("g1.RawBytes() should be uncompressed") - } - } - -} - func TestG1AffineSerialization(t *testing.T) { t.Parallel() // test round trip serialization of infinity { - // compressed - { - var p1, p2 G1Affine - p2.X.MustSetRandom() - p2.Y.MustSetRandom() - buf := p1.Bytes() - n, err := p2.SetBytes(buf[:]) - if err != nil { - t.Fatal(err) - } - if n != SizeOfG1AffineCompressed { - t.Fatal("invalid number of bytes consumed in buffer") - } - if !(p2.X.IsZero() && p2.Y.IsZero()) { // nolint QF1001 - t.Fatal("deserialization of uncompressed infinity point is not infinity") - } - } // uncompressed { @@ -271,7 +221,7 @@ func TestG1AffineSerialization(t *testing.T) { if err != nil { return false } - if n != SizeOfG1AffineCompressed { + if n != SizeOfG1AffineUncompressed { return false } return start.X.Equal(&end.X) && start.Y.Equal(&end.Y) @@ -303,3 +253,18 @@ func GenFp() gopter.Gen { return gopter.NewGenResult(elmt, gopter.NoShrinker) } } + +// GenBigInt generates a big.Int +func GenBigInt() gopter.Gen { + return func(genParams *gopter.GenParameters) *gopter.GenResult { + var s big.Int + var b [fp.Bytes]byte + _, err := crand.Read(b[:]) + if err != nil { + panic(err) + } + s.SetBytes(b[:]) + genResult := gopter.NewGenResult(s, gopter.NoShrinker) + return genResult + } +} diff --git a/ecc/secp256r1/marshal.go b/ecc/secp256r1/marshal.go index 843a8e37c5..31f732c903 100644 --- a/ecc/secp256r1/marshal.go +++ b/ecc/secp256r1/marshal.go @@ -1,26 +1,584 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. +// Code generated by consensys/gnark-crypto DO NOT EDIT + package secp256r1 import ( + "encoding/binary" "errors" "io" + "reflect" "github.com/consensys/gnark-crypto/ecc/secp256r1/fp" + "github.com/consensys/gnark-crypto/ecc/secp256r1/fr" ) +// Encoder writes secp256r1 object values to an output stream +type Encoder struct { + w io.Writer + n int64 // written bytes + raw bool // raw vs compressed encoding +} + +// Decoder reads secp256r1 object values from an inbound stream +type Decoder struct { + r io.Reader + n int64 // read bytes + subGroupCheck bool // default to true +} + +// NewDecoder returns a binary decoder supporting curve secp256r1 objects in both +// compressed and uncompressed (raw) forms +func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { + d := &Decoder{r: r, subGroupCheck: true} + + for _, o := range options { + o(d) + } + + return d +} + +// Decode reads the binary encoding of v from the stream +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *[]G1Affine +func (dec *Decoder) Decode(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { + return errors.New("secp256r1 decoder: unsupported type, need pointer") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + // in particular, careful attention must be given to usage of Bytes() method on Elements and Points + // that return an array (not a slice) of bytes. Using this is beneficial to minimize memory allocations + // in very large (de)serialization upstream in gnark. + // (but detrimental to code readability here) + + var read64 int64 + if vf, ok := v.(io.ReaderFrom); ok { + read64, err = vf.ReadFrom(dec.r) + dec.n += read64 + return + } + var buf [SizeOfG1AffineUncompressed]byte + var read int + var sliceLen uint32 + + switch t := v.(type) { + case *[][]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([][]uint64, sliceLen) + + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { + return + } + (*t)[i] = make([]uint64, sliceLen) + for j := range (*t)[i] { + if (*t)[i][j], err = dec.readUint64(); err != nil { + return + } + } + } + return + case *[]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([]uint64, sliceLen) + for i := range *t { + if (*t)[i], err = dec.readUint64(); err != nil { + return + } + } + return + case *fr.Element: + read, err = io.ReadFull(dec.r, buf[:fr.Bytes]) + dec.n += int64(read) + if err != nil { + return + } + err = t.SetBytesCanonical(buf[:fr.Bytes]) + return + case *fp.Element: + read, err = io.ReadFull(dec.r, buf[:fp.Bytes]) + dec.n += int64(read) + if err != nil { + return + } + err = t.SetBytesCanonical(buf[:fp.Bytes]) + return + case *[]fr.Element: + read64, err = (*fr.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[]fp.Element: + read64, err = (*fp.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len(*t) != int(sliceLen) { + *t = make([][]fr.Element, sliceLen) + } + for i := range *t { + read64, err = (*fr.Vector)(&(*t)[i]).ReadFrom(dec.r) + dec.n += read64 + } + return + case *[][][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len(*t) != int(sliceLen) { + *t = make([][][]fr.Element, sliceLen) + } + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { + return + } + if len((*t)[i]) != int(sliceLen) { + (*t)[i] = make([][]fr.Element, sliceLen) + } + for j := range (*t)[i] { + read64, err = (*fr.Vector)(&(*t)[i][j]).ReadFrom(dec.r) + dec.n += read64 + } + } + return + case *G1Affine: + // no point compression + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + _, err = t.setBytes(buf[:SizeOfG1AffineUncompressed], dec.subGroupCheck) + return + case *[]G1Affine: + sliceLen, err = dec.readUint32() + if err != nil { + return + } + if len(*t) != int(sliceLen) || *t == nil { + *t = make([]G1Affine, sliceLen) + } + for i := range len(*t) { + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + if _, err = (*t)[i].setBytes(buf[:SizeOfG1AffineUncompressed], dec.subGroupCheck); err != nil { + return + } + } + + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New("secp256r1 encoder: unsupported type") + } + err = binary.Read(dec.r, binary.BigEndian, t) + if err == nil { + dec.n += int64(n) + } + return + } +} + +// BytesRead return total bytes read from reader +func (dec *Decoder) BytesRead() int64 { + return dec.n +} + +func (dec *Decoder) readUint32() (r uint32, err error) { + var read int + var buf [4]byte + read, err = io.ReadFull(dec.r, buf[:4]) + dec.n += int64(read) + if err != nil { + return + } + r = binary.BigEndian.Uint32(buf[:4]) + return +} + +func (dec *Decoder) readUint64() (r uint64, err error) { + var read int + var buf [8]byte + read, err = io.ReadFull(dec.r, buf[:]) + dec.n += int64(read) + if err != nil { + return + } + r = binary.BigEndian.Uint64(buf[:]) + return +} + +// NewEncoder returns a binary encoder supporting curve secp256r1 objects +func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { + // default settings + enc := &Encoder{ + w: w, + n: 0, + raw: false, + } + + // handle options + for _, option := range options { + option(enc) + } + + return enc +} + +// Encode writes the binary encoding of v to the stream +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, []G1Affine, *[]G1Affine +func (enc *Encoder) Encode(v any) (err error) { + if enc.raw { + return enc.encodeRaw(v) + } + return enc.encode(v) +} + +// BytesWritten return total bytes written on writer +func (enc *Encoder) BytesWritten() int64 { + return enc.n +} + +// RawEncoding returns an option to use in NewEncoder(...) which sets raw encoding mode to true +// points will not be compressed using this option +func RawEncoding() func(*Encoder) { + return func(enc *Encoder) { + enc.raw = true + } +} + +// NoSubgroupChecks returns an option to use in NewDecoder(...) which disable subgroup checks on the points +// the decoder will read. Use with caution, as crafted points from an untrusted source can lead to crypto-attacks. +func NoSubgroupChecks() func(*Decoder) { + return func(dec *Decoder) { + dec.subGroupCheck = false + } +} + +func (enc *Encoder) encode(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New("secp256r1 encoder: can't encode ") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + + var written int + + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) + case *fr.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *fp.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *G1Affine: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: + // write slice length + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 + } + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { + return + } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } + } + return + case *[]G1Affine: + return enc.encode(*t) + case []G1Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG1AffineUncompressed]byte + + for i := range len(t) { + buf = t[i].Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New("secp256r1 encoder: unsupported type") + } + err = binary.Write(enc.w, binary.BigEndian, t) + enc.n += int64(n) + return + } +} + +func (enc *Encoder) encodeRaw(v any) (err error) { + rv := reflect.ValueOf(v) + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New("secp256r1 encoder: can't encode ") + } + + // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + + var written int + + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) + case *fr.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *fp.Element: + buf := t.Bytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case *G1Affine: + buf := t.RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: + // write slice length + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 + } + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { + return + } + enc.n += 4 + for i := range t { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { + return + } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } + } + return + case *[]G1Affine: + return enc.encodeRaw(*t) + case []G1Affine: + // write slice length + err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) + if err != nil { + return + } + enc.n += 4 + + var buf [SizeOfG1AffineUncompressed]byte + + for i := range len(t) { + buf = t[i].RawBytes() + written, err = enc.w.Write(buf[:]) + enc.n += int64(written) + if err != nil { + return + } + } + return nil + default: + n := binary.Size(t) + if n == -1 { + return errors.New("secp256r1 encoder: unsupported type") + } + err = binary.Write(enc.w, binary.BigEndian, t) + enc.n += int64(n) + return + } +} + +func (enc *Encoder) writeUint64Slice(t []uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint64(t[i]); err != nil { + return + } + } + return nil +} + +func (enc *Encoder) writeUint64SliceSlice(t [][]uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint32(uint32(len(t[i]))); err != nil { + return + } + for j := range t[i] { + if err = enc.writeUint64(t[i][j]); err != nil { + return + } + } + } + return nil +} + +func (enc *Encoder) writeUint64(a uint64) error { + var buff [64 / 8]byte + binary.BigEndian.PutUint64(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + +func (enc *Encoder) writeUint32(a uint32) error { + var buff [32 / 8]byte + binary.BigEndian.PutUint32(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + // SizeOfG1AffineUncompressed represents the size in bytes that a G1Affine need in binary form, uncompressed -const SizeOfG1AffineUncompressed = 64 +const SizeOfG1AffineUncompressed = 32 * 2 + +// Marshal converts p to a byte slice (without point compression) +func (p *G1Affine) Marshal() []byte { + b := p.RawBytes() + return b[:] +} + +// Unmarshal is an alias to SetBytes() +func (p *G1Affine) Unmarshal(buf []byte) error { + _, err := p.SetBytes(buf) + return err +} + +// Bytes returns binary representation of p (stores X and Y coordinate) +// point compression is not available for this curve +func (p *G1Affine) Bytes() (res [SizeOfG1AffineUncompressed]byte) { + return p.RawBytes() +} // RawBytes returns binary representation of p (stores X and Y coordinate) +// see Bytes() for a compressed representation func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { + // check if p is infinity point + if p.X.IsZero() && p.Y.IsZero() { + return + } + // not compressed // we store the Y coordinate fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[32:32+fp.Bytes]), p.Y) - // we store the X coordinate + // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) return @@ -28,22 +586,24 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // SetBytes sets p from binary representation in buf and returns number of consumed bytes // -// bytes in buf must match RawBytes() +// bytes in buf must match either RawBytes() or Bytes() output // // if buf is too short io.ErrShortBuffer is returned // +// if buf contains compressed representation (output from Bytes()) and we're unable to compute +// the Y coordinate (i.e the square root doesn't exist) this function returns an error +// // this check if the resulting point is on the curve and in the correct subgroup func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } -// we store both X and Y and there is no spare bit for flagging +// no point compression - setBytes only handles uncompressed format func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineUncompressed { return 0, io.ErrShortBuffer } - // uncompressed point // read X and Y coordinates if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { return 0, err @@ -58,5 +618,4 @@ func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { } return SizeOfG1AffineUncompressed, nil - } diff --git a/ecc/secp256r1/marshal_test.go b/ecc/secp256r1/marshal_test.go index 6630c47520..5b86264501 100644 --- a/ecc/secp256r1/marshal_test.go +++ b/ecc/secp256r1/marshal_test.go @@ -1,11 +1,17 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. +// Code generated by consensys/gnark-crypto DO NOT EDIT + package secp256r1 import ( + "bytes" crand "crypto/rand" + "io" "math/big" + "math/rand/v2" + "reflect" "testing" "github.com/leanovate/gopter" @@ -20,10 +26,141 @@ const ( nbFuzz = 100 ) +func TestEncoder(t *testing.T) { + t.Parallel() + // TODO need proper fuzz testing here + + var inA uint64 + var inB fr.Element + var inC fp.Element + var inD G1Affine + var inE G1Affine + var inG []G1Affine + var inI []fp.Element + var inJ []fr.Element + var inK fr.Vector + var inL [][]fr.Element + var inM [][]uint64 + var inN [][][]fr.Element + + // set values of inputs + inA = rand.Uint64() //#nosec G404 weak rng is fine here + inB.MustSetRandom() + inC.MustSetRandom() + inD.ScalarMultiplication(&g1GenAff, new(big.Int).SetUint64(rand.Uint64())) //#nosec G404 weak rng is fine here + // inE --> infinity + inG = make([]G1Affine, 2) + inG[1] = inD + inI = make([]fp.Element, 3) + inI[2] = inD.X + inJ = make([]fr.Element, 0) + inK = make(fr.Vector, 42) + inK[41].SetUint64(42) + inL = [][]fr.Element{inJ, inK} + inM = [][]uint64{{1, 2}, {4}, {}} + inN = make([][][]fr.Element, 4) + for i := range 4 { + inN[i] = make([][]fr.Element, i+2) + for j := range i + 2 { + inNIJ := make(fr.Vector, j+3) + inNIJ.MustSetRandom() + inN[i][j] = inNIJ + } + } + + // encode them, compressed and raw + var buf, bufRaw bytes.Buffer + enc := NewEncoder(&buf) + encRaw := NewEncoder(&bufRaw, RawEncoding()) + toEncode := []any{inA, &inB, &inC, &inD, &inE, inG, inI, inJ, inK, inL, inM, inN} + for _, v := range toEncode { + if err := enc.Encode(v); err != nil { + t.Fatal(err) + } + if err := encRaw.Encode(v); err != nil { + t.Fatal(err) + } + } + + testDecode := func(t *testing.T, r io.Reader, n int64) { + dec := NewDecoder(r) + var outA uint64 + var outB fr.Element + var outC fp.Element + var outD G1Affine + var outE G1Affine + outE.X.SetOne() + outE.Y.SetUint64(42) + var outG []G1Affine + var outI []fp.Element + var outJ []fr.Element + var outK fr.Vector + var outL [][]fr.Element + var outM [][]uint64 + var outN [][][]fr.Element + + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} + for _, v := range toDecode { + if err := dec.Decode(v); err != nil { + t.Fatal(err) + } + } + + // compare values + if inA != outA { + t.Fatal("didn't encode/decode uint64 value properly") + } + + if !inB.Equal(&outB) || !inC.Equal(&outC) { + t.Fatal("decode(encode(Element) failed") + } + if !inD.Equal(&outD) || !inE.Equal(&outE) { + t.Fatal("decode(encode(G1Affine) failed") + } + if len(inG) != len(outG) { + t.Fatal("decode(encode(slice(points))) failed") + } + for i := range len(inG) { + if !inG[i].Equal(&outG[i]) { + t.Fatal("decode(encode(slice(points))) failed") + } + } + if (len(inI) != len(outI)) || (len(inJ) != len(outJ)) { + t.Fatal("decode(encode(slice(elements))) failed") + } + for i := range len(inI) { + if !inI[i].Equal(&outI[i]) { + t.Fatal("decode(encode(slice(elements))) failed") + } + } + if !reflect.DeepEqual(inK, outK) { + t.Fatal("decode(encode(vector)) failed") + } + if !reflect.DeepEqual(inL, outL) { + t.Fatal("decode(encode(slice²(elements))) failed") + } + if !reflect.DeepEqual(inM, outM) { + t.Fatal("decode(encode(slice²(uint64))) failed") + } + if !reflect.DeepEqual(inN, outN) { + t.Fatal("decode(encode(slice^{3}(uint64))) failed") + } + if n != dec.BytesRead() { + t.Fatal("bytes read don't match bytes written") + } + } + + // decode them + testDecode(t, &buf, enc.BytesWritten()) + testDecode(t, &bufRaw, encRaw.BytesWritten()) + +} + func TestG1AffineSerialization(t *testing.T) { t.Parallel() // test round trip serialization of infinity { + // uncompressed { var p1, p2 G1Affine @@ -72,28 +209,52 @@ func TestG1AffineSerialization(t *testing.T) { GenFp(), )) + properties.Property("[G1] Affine SetBytes(Bytes()) should stay the same", prop.ForAll( + func(a fp.Element) bool { + var start, end G1Affine + var ab big.Int + a.BigInt(&ab) + start.ScalarMultiplication(&g1GenAff, &ab) + + buf := start.Bytes() + n, err := end.SetBytes(buf[:]) + if err != nil { + return false + } + if n != SizeOfG1AffineUncompressed { + return false + } + return start.X.Equal(&end.X) && start.Y.Equal(&end.Y) + }, + GenFp(), + )) + properties.TestingRun(t, gopter.ConsoleReporter(false)) } -// GenFr generates an Fr element. +// define Gopters generators + +// GenFr generates an Fr element func GenFr() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { var elmt fr.Element elmt.MustSetRandom() + return gopter.NewGenResult(elmt, gopter.NoShrinker) } } -// GenFp generates an Fp element. +// GenFp generates an Fp element func GenFp() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { var elmt fp.Element elmt.MustSetRandom() + return gopter.NewGenResult(elmt, gopter.NoShrinker) } } -// GenBigInt generates a big.Int. +// GenBigInt generates a big.Int func GenBigInt() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { var s big.Int @@ -103,6 +264,7 @@ func GenBigInt() gopter.Gen { panic(err) } s.SetBytes(b[:]) - return gopter.NewGenResult(s, gopter.NoShrinker) + genResult := gopter.NewGenResult(s, gopter.NoShrinker) + return genResult } } diff --git a/ecc/stark-curve/marshal.go b/ecc/stark-curve/marshal.go index 536aa78cc3..4d15f261c9 100644 --- a/ecc/stark-curve/marshal.go +++ b/ecc/stark-curve/marshal.go @@ -1,7 +1,7 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// FOO +// Code generated by consensys/gnark-crypto DO NOT EDIT package starkcurve @@ -19,14 +19,24 @@ import ( // To encode G1Affine points, we mask the most significant bits with these bits to specify without ambiguity // metadata needed for point (de)compression -// we have less than 3 bits available on the msw, so we can't follow BLS12-381 style encoding. -// the difference is the case where a point is infinity and uncompressed is not flagged +// we follow the BLS12-381 style encoding as specified in ZCash and now IETF +// see https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/11/ +// Appendix C. ZCash serialization format for BLS12_381 const ( - mMask byte = 0b11 << 6 - mUncompressed byte = 0b00 << 6 - mCompressedSmallest byte = 0b10 << 6 - mCompressedLargest byte = 0b11 << 6 - mCompressedInfinity byte = 0b01 << 6 + mMask byte = 0b111 << 5 + mUncompressed byte = 0b000 << 5 + _ byte = 0b001 << 5 // invalid + mUncompressedInfinity byte = 0b010 << 5 + _ byte = 0b011 << 5 // invalid + mCompressedSmallest byte = 0b100 << 5 + mCompressedLargest byte = 0b101 << 5 + mCompressedInfinity byte = 0b110 << 5 + _ byte = 0b111 << 5 // invalid +) + +var ( + ErrInvalidInfinityEncoding = errors.New("invalid infinity point encoding") + ErrInvalidEncoding = errors.New("invalid point encoding") ) // Encoder writes stark-curve object values to an output stream @@ -56,23 +66,59 @@ func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { } // Decode reads the binary encoding of v from the stream -// type must be *uint64, *fr.Element, *fp.Element, *G1Affine or *[]G1Affine +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *[]G1Affine func (dec *Decoder) Decode(v any) (err error) { rv := reflect.ValueOf(v) - if v == nil || rv.Kind() != reflect.Pointer || rv.IsNil() || !rv.Elem().CanSet() { + if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { return errors.New("stark-curve decoder: unsupported type, need pointer") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap // in particular, careful attention must be given to usage of Bytes() method on Elements and Points - // that return an array (not a slice) of bytes. Using this is beneficial to minimize memallocs + // that return an array (not a slice) of bytes. Using this is beneficial to minimize memory allocations // in very large (de)serialization upstream in gnark. - // (but detrimental to code visibility here) + // (but detrimental to code readability here) + var read64 int64 + if vf, ok := v.(io.ReaderFrom); ok { + read64, err = vf.ReadFrom(dec.r) + dec.n += read64 + return + } var buf [SizeOfG1AffineUncompressed]byte var read int + var sliceLen uint32 switch t := v.(type) { + case *[][]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([][]uint64, sliceLen) + + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { + return + } + (*t)[i] = make([]uint64, sliceLen) + for j := range (*t)[i] { + if (*t)[i][j], err = dec.readUint64(); err != nil { + return + } + } + } + return + case *[]uint64: + if sliceLen, err = dec.readUint32(); err != nil { + return + } + *t = make([]uint64, sliceLen) + for i := range *t { + if (*t)[i], err = dec.readUint64(); err != nil { + return + } + } + return case *fr.Element: read, err = io.ReadFull(dec.r, buf[:fr.Bytes]) dec.n += int64(read) @@ -90,44 +136,42 @@ func (dec *Decoder) Decode(v any) (err error) { err = t.SetBytesCanonical(buf[:fp.Bytes]) return case *[]fr.Element: - var sliceLen uint32 - sliceLen, err = dec.readUint32() - if err != nil { + read64, err = (*fr.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[]fp.Element: + read64, err = (*fp.Vector)(t).ReadFrom(dec.r) + dec.n += read64 + return + case *[][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { return } if len(*t) != int(sliceLen) { - *t = make([]fr.Element, sliceLen) + *t = make([][]fr.Element, sliceLen) } - - for i := 0; i < len(*t); i++ { - read, err = io.ReadFull(dec.r, buf[:fr.Bytes]) - dec.n += int64(read) - if err != nil { - return - } - if err = (*t)[i].SetBytesCanonical(buf[:fr.Bytes]); err != nil { - return - } + for i := range *t { + read64, err = (*fr.Vector)(&(*t)[i]).ReadFrom(dec.r) + dec.n += read64 } return - case *[]fp.Element: - var sliceLen uint32 - sliceLen, err = dec.readUint32() - if err != nil { + case *[][][]fr.Element: + if sliceLen, err = dec.readUint32(); err != nil { return } if len(*t) != int(sliceLen) { - *t = make([]fp.Element, sliceLen) + *t = make([][][]fr.Element, sliceLen) } - - for i := 0; i < len(*t); i++ { - read, err = io.ReadFull(dec.r, buf[:fp.Bytes]) - dec.n += int64(read) - if err != nil { + for i := range *t { + if sliceLen, err = dec.readUint32(); err != nil { return } - if err = (*t)[i].SetBytesCanonical(buf[:fp.Bytes]); err != nil { - return + if len((*t)[i]) != int(sliceLen) { + (*t)[i] = make([][]fr.Element, sliceLen) + } + for j := range (*t)[i] { + read64, err = (*fr.Vector)(&(*t)[i][j]).ReadFrom(dec.r) + dec.n += read64 } } return @@ -139,6 +183,13 @@ func (dec *Decoder) Decode(v any) (err error) { return } nbBytes := SizeOfG1AffineCompressed + + // 111, 011, 001 --> invalid mask + if isMaskInvalid(buf[0]) { + err = ErrInvalidEncoding + return + } + // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG1AffineUncompressed @@ -152,16 +203,15 @@ func (dec *Decoder) Decode(v any) (err error) { _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) return case *[]G1Affine: - var sliceLen uint32 sliceLen, err = dec.readUint32() if err != nil { return } - if len(*t) != int(sliceLen) { + if len(*t) != int(sliceLen) || *t == nil { *t = make([]G1Affine, sliceLen) } compressed := make([]bool, sliceLen) - for i := 0; i < len(*t); i++ { + for i := range len(*t) { // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) @@ -170,6 +220,13 @@ func (dec *Decoder) Decode(v any) (err error) { return } nbBytes := SizeOfG1AffineCompressed + + // 111, 011, 001 --> invalid mask + if isMaskInvalid(buf[0]) { + err = ErrInvalidEncoding + return + } + // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG1AffineUncompressed @@ -185,7 +242,7 @@ func (dec *Decoder) Decode(v any) (err error) { } } else { var r bool - if r, err = ((*t)[i].unsafeSetCompressedBytes(buf[:nbBytes])); err != nil { + if r, err = (*t)[i].unsafeSetCompressedBytes(buf[:nbBytes]); err != nil { return } compressed[i] = !r @@ -240,9 +297,27 @@ func (dec *Decoder) readUint32() (r uint32, err error) { return } +func (dec *Decoder) readUint64() (r uint64, err error) { + var read int + var buf [8]byte + read, err = io.ReadFull(dec.r, buf[:]) + dec.n += int64(read) + if err != nil { + return + } + r = binary.BigEndian.Uint64(buf[:]) + return +} + +// isMaskInvalid returns true if the mask is invalid +func isMaskInvalid(msb byte) bool { + mData := msb & mMask + return ((mData == (0b111 << 5)) || (mData == (0b011 << 5)) || (mData == (0b001 << 5))) +} + func isCompressed(msb byte) bool { mData := msb & mMask - return mData != mUncompressed + return mData != mUncompressed && mData != mUncompressedInfinity } // NewEncoder returns a binary encoder supporting curve stark-curve objects @@ -263,7 +338,7 @@ func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { } // Encode writes the binary encoding of v to the stream -// type must be uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, []G1Affine or []G2Affine +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, []G1Affine, *[]G1Affine func (enc *Encoder) Encode(v any) (err error) { if enc.raw { return enc.encodeRaw(v) @@ -292,16 +367,41 @@ func NoSubgroupChecks() func(*Decoder) { } } +// isZeroed checks that the provided bytes are at 0 +func isZeroed(firstByte byte, buf []byte) bool { + if firstByte != 0 { + return false + } + for _, b := range buf { + if b != 0 { + return false + } + } + return true +} + func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) - if v == nil || (rv.Kind() == reflect.Pointer && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New("stark-curve encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + var written int + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) case *fr.Element: buf := t.Bytes() written, err = enc.w.Write(buf[:]) @@ -317,41 +417,54 @@ func (enc *Encoder) encode(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { return } enc.n += 4 - var buf [fr.Bytes]byte for i := range t { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 } - return nil - case []fp.Element: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { return } enc.n += 4 - var buf [fp.Bytes]byte for i := range t { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { return } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } } - return nil - + return + case *[]G1Affine: + return enc.encode(*t) case []G1Affine: // write slice length err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) @@ -362,7 +475,7 @@ func (enc *Encoder) encode(v any) (err error) { var buf [SizeOfG1AffineCompressed]byte - for i := range t { + for i := range len(t) { buf = t[i].Bytes() written, err = enc.w.Write(buf[:]) enc.n += int64(written) @@ -374,7 +487,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("stark-curve encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -384,14 +497,26 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) - if v == nil || (rv.Kind() == reflect.Pointer && rv.IsNil()) { - return errors.New(" encoder: can't encode ") + if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { + return errors.New("stark-curve encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap + var written64 int64 + if vw, ok := v.(io.WriterTo); ok { + written64, err = vw.WriteTo(enc.w) + enc.n += written64 + return + } + var written int + switch t := v.(type) { + case []uint64: + return enc.writeUint64Slice(t) + case [][]uint64: + return enc.writeUint64SliceSlice(t) case *fr.Element: buf := t.Bytes() written, err = enc.w.Write(buf[:]) @@ -407,41 +532,54 @@ func (enc *Encoder) encodeRaw(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return + case fr.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return + case fp.Vector: + written64, err = t.WriteTo(enc.w) + enc.n += written64 + return case []fr.Element: + written64, err = (*fr.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case []fp.Element: + written64, err = (*fp.Vector)(&t).WriteTo(enc.w) + enc.n += written64 + return + case [][]fr.Element: // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { return } enc.n += 4 - var buf [fr.Bytes]byte for i := range t { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { - return - } + written64, err = (*fr.Vector)(&t[i]).WriteTo(enc.w) + enc.n += written64 } - return nil - case []fp.Element: - // write slice length - err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) - if err != nil { + return + case [][][]fr.Element: + // number of collections + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))); err != nil { return } enc.n += 4 - var buf [fp.Bytes]byte for i := range t { - buf = t[i].Bytes() - written, err = enc.w.Write(buf[:]) - enc.n += int64(written) - if err != nil { + // size of current collection + if err = binary.Write(enc.w, binary.BigEndian, uint32(len(t[i]))); err != nil { return } + enc.n += 4 + // write each vector of the current collection + for j := range t[i] { + written64, err = (*fr.Vector)(&t[i][j]).WriteTo(enc.w) + enc.n += written64 + } } - return nil - + return + case *[]G1Affine: + return enc.encodeRaw(*t) case []G1Affine: // write slice length err = binary.Write(enc.w, binary.BigEndian, uint32(len(t))) @@ -452,7 +590,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { var buf [SizeOfG1AffineUncompressed]byte - for i := range t { + for i := range len(t) { buf = t[i].RawBytes() written, err = enc.w.Write(buf[:]) enc.n += int64(written) @@ -464,7 +602,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New(" encoder: unsupported type") + return errors.New("stark-curve encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -472,6 +610,51 @@ func (enc *Encoder) encodeRaw(v any) (err error) { } } +func (enc *Encoder) writeUint64Slice(t []uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint64(t[i]); err != nil { + return + } + } + return nil +} + +func (enc *Encoder) writeUint64SliceSlice(t [][]uint64) (err error) { + if err = enc.writeUint32(uint32(len(t))); err != nil { + return + } + for i := range t { + if err = enc.writeUint32(uint32(len(t[i]))); err != nil { + return + } + for j := range t[i] { + if err = enc.writeUint64(t[i][j]); err != nil { + return + } + } + } + return nil +} + +func (enc *Encoder) writeUint64(a uint64) error { + var buff [64 / 8]byte + binary.BigEndian.PutUint64(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + +func (enc *Encoder) writeUint32(a uint32) error { + var buff [32 / 8]byte + binary.BigEndian.PutUint32(buff[:], a) + written, err := enc.w.Write(buff[:]) + enc.n += int64(written) + return err +} + // SizeOfG1AffineCompressed represents the size in bytes that a G1Affine need in binary form, compressed const SizeOfG1AffineCompressed = 32 @@ -492,15 +675,13 @@ func (p *G1Affine) Unmarshal(buf []byte) error { // Bytes returns binary representation of p // will store X coordinate in regular form and a parity bit -// as we have less than 3 bits available in our coordinate, we can't follow BLS12-381 style encoding (ZCash/IETF) +// we follow the BLS12-381 style encoding as specified in ZCash and now IETF +// +// The most significant bit, when set, indicates that the point is in compressed form. Otherwise, the point is in uncompressed form. // -// we use the 2 most significant bits instead +// The second-most significant bit indicates that the point is at infinity. If this bit is set, the remaining bits of the group element's encoding should be set to zero. // -// 00 -> uncompressed -// 10 -> compressed, use smallest lexicographically square root of Y^2 -// 11 -> compressed, use largest lexicographically square root of Y^2 -// 01 -> compressed infinity point -// the "uncompressed infinity point" will just have 00 (uncompressed) followed by zeroes (infinity = 0,0 in affine coordinates) +// The third-most significant bit is set if (and only if) this point is in compressed form and it is not the point at infinity and its y-coordinate is the lexicographically largest of the two associated with the encoded x-coordinate. func (p *G1Affine) Bytes() (res [SizeOfG1AffineCompressed]byte) { // check if p is infinity point @@ -530,9 +711,7 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - - res[0] = mUncompressed - + res[0] = mUncompressedInfinity return } @@ -542,7 +721,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { // we store X and mask the most significant word with our metadata mask fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[0:0+fp.Bytes]), p.X) - res[0] |= mUncompressed return @@ -561,7 +739,6 @@ func (p *G1Affine) RawBytes() (res [SizeOfG1AffineUncompressed]byte) { func (p *G1Affine) SetBytes(buf []byte) (int, error) { return p.setBytes(buf, true) } - func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOfG1AffineCompressed { return 0, io.ErrShortBuffer @@ -570,19 +747,35 @@ func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { // most significant byte mData := buf[0] & mMask + // 111, 011, 001 --> invalid mask + if isMaskInvalid(mData) { + return 0, ErrInvalidEncoding + } + // check buffer size - if mData == mUncompressed { + if (mData == mUncompressed) || (mData == mUncompressedInfinity) { if len(buf) < SizeOfG1AffineUncompressed { return 0, io.ErrShortBuffer } } - // if infinity is encoded in the metadata, we don't need to read the buffer + // infinity encoded, we still check that the buffer is full of zeroes. if mData == mCompressedInfinity { + if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineCompressed]) { + return 0, ErrInvalidInfinityEncoding + } p.X.SetZero() p.Y.SetZero() return SizeOfG1AffineCompressed, nil } + if mData == mUncompressedInfinity { + if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineUncompressed]) { + return 0, ErrInvalidInfinityEncoding + } + p.X.SetZero() + p.Y.SetZero() + return SizeOfG1AffineUncompressed, nil + } // uncompressed point if mData == mUncompressed { @@ -618,11 +811,13 @@ func (p *G1Affine) setBytes(buf []byte, subGroupCheck bool) (int, error) { var YSquared, Y fp.Element - // y^2=x^3+x+b YSquared.Square(&p.X).Mul(&YSquared, &p.X) - YSquared.Add(&YSquared, &p.X). - Add(&YSquared, &bCurveCoeff) - + { + var aX fp.Element + aX.Mul(&aCurveCoeff, &p.X) + YSquared.Add(&YSquared, &aX) + } + YSquared.Add(&YSquared, &bCurveCoeff) if Y.Sqrt(&YSquared) == nil { return 0, errors.New("invalid compressed coordinate: square root doesn't exist") } @@ -659,11 +854,13 @@ func (p *G1Affine) unsafeComputeY(subGroupCheck bool) error { // we have a compressed coordinate, we need to solve the curve equation to compute Y var YSquared, Y fp.Element - // y^2=x^3+x+b YSquared.Square(&p.X).Mul(&YSquared, &p.X) - YSquared.Add(&YSquared, &p.X). - Add(&YSquared, &bCurveCoeff) - + { + var aX fp.Element + aX.Mul(&aCurveCoeff, &p.X) + YSquared.Add(&YSquared, &aX) + } + YSquared.Add(&YSquared, &bCurveCoeff) if Y.Sqrt(&YSquared) == nil { return errors.New("invalid compressed coordinate: square root doesn't exist") } @@ -700,9 +897,12 @@ func (p *G1Affine) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, err er mData := buf[0] & mMask if mData == mCompressedInfinity { + isInfinity = true + if !isZeroed(buf[0] & ^mMask, buf[1:SizeOfG1AffineCompressed]) { + return isInfinity, ErrInvalidInfinityEncoding + } p.X.SetZero() p.Y.SetZero() - isInfinity = true return isInfinity, nil } diff --git a/ecc/stark-curve/marshal_test.go b/ecc/stark-curve/marshal_test.go index 574891ae9c..d0fd969a30 100644 --- a/ecc/stark-curve/marshal_test.go +++ b/ecc/stark-curve/marshal_test.go @@ -1,6 +1,8 @@ -// Copyright 2020-2025 Consensys Software Inc. +// Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. +// Code generated by consensys/gnark-crypto DO NOT EDIT + package starkcurve import ( @@ -8,7 +10,8 @@ import ( crand "crypto/rand" "io" "math/big" - "math/rand" + "math/rand/v2" + "reflect" "testing" "github.com/leanovate/gopter" @@ -35,6 +38,10 @@ func TestEncoder(t *testing.T) { var inG []G1Affine var inI []fp.Element var inJ []fr.Element + var inK fr.Vector + var inL [][]fr.Element + var inM [][]uint64 + var inN [][][]fr.Element // set values of inputs inA = rand.Uint64() //#nosec G404 weak rng is fine here @@ -47,12 +54,25 @@ func TestEncoder(t *testing.T) { inI = make([]fp.Element, 3) inI[2] = inD.X inJ = make([]fr.Element, 0) + inK = make(fr.Vector, 42) + inK[41].SetUint64(42) + inL = [][]fr.Element{inJ, inK} + inM = [][]uint64{{1, 2}, {4}, {}} + inN = make([][][]fr.Element, 4) + for i := range 4 { + inN[i] = make([][]fr.Element, i+2) + for j := range i + 2 { + inNIJ := make(fr.Vector, j+3) + inNIJ.MustSetRandom() + inN[i][j] = inNIJ + } + } // encode them, compressed and raw var buf, bufRaw bytes.Buffer enc := NewEncoder(&buf) encRaw := NewEncoder(&bufRaw, RawEncoding()) - toEncode := []any{inA, &inB, &inC, &inD, &inE, inG, inI, inJ} + toEncode := []any{inA, &inB, &inC, &inD, &inE, inG, inI, inJ, inK, inL, inM, inN} for _, v := range toEncode { if err := enc.Encode(v); err != nil { t.Fatal(err) @@ -74,8 +94,12 @@ func TestEncoder(t *testing.T) { var outG []G1Affine var outI []fp.Element var outJ []fr.Element + var outK fr.Vector + var outL [][]fr.Element + var outM [][]uint64 + var outN [][][]fr.Element - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ} + toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { t.Fatal(err) @@ -93,7 +117,10 @@ func TestEncoder(t *testing.T) { if !inD.Equal(&outD) || !inE.Equal(&outE) { t.Fatal("decode(encode(G1Affine) failed") } - for i := 0; i < len(inG); i++ { + if len(inG) != len(outG) { + t.Fatal("decode(encode(slice(points))) failed") + } + for i := range len(inG) { if !inG[i].Equal(&outG[i]) { t.Fatal("decode(encode(slice(points))) failed") } @@ -101,11 +128,23 @@ func TestEncoder(t *testing.T) { if (len(inI) != len(outI)) || (len(inJ) != len(outJ)) { t.Fatal("decode(encode(slice(elements))) failed") } - for i := 0; i < len(inI); i++ { + for i := range len(inI) { if !inI[i].Equal(&outI[i]) { t.Fatal("decode(encode(slice(elements))) failed") } } + if !reflect.DeepEqual(inK, outK) { + t.Fatal("decode(encode(vector)) failed") + } + if !reflect.DeepEqual(inL, outL) { + t.Fatal("decode(encode(slice²(elements))) failed") + } + if !reflect.DeepEqual(inM, outM) { + t.Fatal("decode(encode(slice²(uint64))) failed") + } + if !reflect.DeepEqual(inN, outN) { + t.Fatal("decode(encode(slice^{3}(uint64))) failed") + } if n != dec.BytesRead() { t.Fatal("bytes read don't match bytes written") } @@ -116,7 +155,6 @@ func TestEncoder(t *testing.T) { testDecode(t, &bufRaw, encRaw.BytesWritten()) } - func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine @@ -153,6 +191,26 @@ func TestIsCompressed(t *testing.T) { } +func TestG1AffineInvalidBitMask(t *testing.T) { + t.Parallel() + var buf [SizeOfG1AffineCompressed]byte + crand.Read(buf[:]) + + var p G1Affine + buf[0] = 0b111 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } + buf[0] = 0b011 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } + buf[0] = 0b001 << 5 + if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid bit mask") + } +} + func TestG1AffineSerialization(t *testing.T) { t.Parallel() // test round trip serialization of infinity @@ -246,25 +304,29 @@ func TestG1AffineSerialization(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } -// GenFr generates an Fr element. +// define Gopters generators + +// GenFr generates an Fr element func GenFr() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { var elmt fr.Element elmt.MustSetRandom() + return gopter.NewGenResult(elmt, gopter.NoShrinker) } } -// GenFp generates an Fp element. +// GenFp generates an Fp element func GenFp() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { var elmt fp.Element elmt.MustSetRandom() + return gopter.NewGenResult(elmt, gopter.NoShrinker) } } -// GenBigInt generates a big.Int. +// GenBigInt generates a big.Int func GenBigInt() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { var s big.Int @@ -274,6 +336,7 @@ func GenBigInt() gopter.Gen { panic(err) } s.SetBytes(b[:]) - return gopter.NewGenResult(s, gopter.NoShrinker) + genResult := gopter.NewGenResult(s, gopter.NoShrinker) + return genResult } } diff --git a/internal/generator/ecc/template/tests/point.go.tmpl b/internal/generator/ecc/template/tests/point.go.tmpl index cb13fd6058..ce16c09341 100644 --- a/internal/generator/ecc/template/tests/point.go.tmpl +++ b/internal/generator/ecc/template/tests/point.go.tmpl @@ -8,8 +8,6 @@ {{$fuzzer = "GenE2()"}} {{- else if eq .CoordType "fptower.E4" }} {{$fuzzer = "GenE4()"}} -{{- else if eq .CoordType "fptower.E8" }} - {{$fuzzer = "GenE8()"}} {{- end}} {{$c := .CMax}} @@ -21,7 +19,7 @@ import ( "math/rand/v2" crand "crypto/rand" - {{if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") (eq .CoordType "fptower.E8")}} + {{if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4")}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" {{else}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" @@ -355,8 +353,8 @@ func Test{{ toUpper .PointName }}Conversions(t *testing.T) { baseTableAff := BatchJacobianToAffineG1([]G1Jac{g1, g2}) return op1.Equal(&baseTableAff[0]) && op2.Equal(&baseTableAff[1]) }, - {{$fuzzer}}, - {{$fuzzer}}, + GenFp(), + GenFp(), )) {{- end }} @@ -579,7 +577,7 @@ func Test{{ $TAffine }}Ops(t *testing.T) { genScalar, )) - {{ if and (eq .PointName "g2") (or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") (eq .CoordType "fptower.E8"))}} + {{ if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4")}} properties.Property("[{{ toUpper .Name }}] psi should map points from E' to itself", prop.ForAll( func() bool { var a {{ $TJacobian }} @@ -1163,7 +1161,7 @@ func fuzz{{ $TJacobianExtended }}(p *{{ $TJacobianExtended }}, f {{ .CoordType}} return res } -{{- if or (not .GenerateMarshal) (eq .Name "secp256r1") (eq .Name "stark-curve")}} +{{- if not .GenerateMarshal}} const ( nbFuzzShort = 10 nbFuzz = 100 diff --git a/internal/generator/ecdsa/template/ecdsa.go.tmpl b/internal/generator/ecdsa/template/ecdsa.go.tmpl index adfb060f0f..2802daef44 100644 --- a/internal/generator/ecdsa/template/ecdsa.go.tmpl +++ b/internal/generator/ecdsa/template/ecdsa.go.tmpl @@ -4,7 +4,7 @@ import ( "crypto/rand" "crypto/sha512" "crypto/subtle" - {{- if or (eq .Name "secp256k1") (eq .Name "bn254") (eq .Name "stark-curve") (eq .Name "secp256r1") }} + {{- if .ECDSAKeyRecovery }} "errors" {{- end }} "hash" @@ -21,18 +21,16 @@ const ( sizeFr = fr.Bytes sizeFrBits = fr.Bits sizeFp = fp.Bytes -{{- if eq .Name "secp256k1"}} - sizePublicKey = {{ .CurvePackage }}.SizeOfG1AffineUncompressed -{{- else if eq .Name "secp256r1"}} - sizePublicKey = 2 * sizeFp -{{- else}} +{{- if .SupportsPointCompression}} sizePublicKey = {{ .CurvePackage }}.SizeOfG1AffineCompressed +{{- else}} + sizePublicKey = 2 * sizeFp {{- end}} sizePrivateKey = sizeFr + sizePublicKey sizeSignature = 2 * sizeFr ) -{{- if or (eq .Name "secp256k1") (eq .Name "bn254") (eq .Name "stark-curve") (eq .Name "secp256r1") }} +{{- if .ECDSAKeyRecovery }} var ( // ErrNoSqrtR is returned when x^3+ax+b is not a square in the field. This // is used for public key recovery and allows to detect if the signature is @@ -86,10 +84,10 @@ func GenerateKey(rand io.Reader) (*PrivateKey, error) { } - {{- if or (eq .Name "secp256k1") (eq .Name "stark-curve") (eq .Name "grumpkin") (eq .Name "secp256r1")}} - _, g := {{ .CurvePackage }}.Generators() - {{- else}} + {{- if .HasG2}} _, _, g, _ := {{ .CurvePackage }}.Generators() + {{- else}} + _, g := {{ .CurvePackage }}.Generators() {{- end}} privateKey := new(PrivateKey) @@ -113,7 +111,7 @@ func HashToInt(hash []byte) *big.Int { return ret } -{{- if or (eq .Name "secp256k1") (eq .Name "bn254") (eq .Name "stark-curve") (eq .Name "secp256r1") }} +{{- if .ECDSAKeyRecovery }} // recoverP recovers the value P (prover commitment) when creating a signature. // It uses the recovery information v and part of the decomposed signature r. It // is used internally for recovering the public key. @@ -235,7 +233,7 @@ func (privKey *PrivateKey) Public() signature.PublicKey { return &pub } -{{- if or (eq .Name "secp256k1") (eq .Name "bn254") (eq .Name "stark-curve") (eq .Name "secp256r1") }} +{{- if .ECDSAKeyRecovery }} // SignForRecover performs the ECDSA signature according to [SEC 1] Section 4.1.3 and returns public key recovery information. // // The argument hFunc defines the hash function for computing the hash of the From f5239392350287e7e57b0b1a957124acd097a7fa Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 22 Apr 2026 15:01:16 -0400 Subject: [PATCH 14/33] refactor: clean code generation --- internal/generator/config/kb8.go | 1 + internal/generator/ecc/generate.go | 45 +- .../generator/ecc/template/marshal.go.tmpl | 587 ++++++------------ .../generator/ecc/template/multiexp.go.tmpl | 18 +- .../ecc/template/multiexp_affine.go.tmpl | 4 +- .../ecc/template/multiexp_jacobian.go.tmpl | 2 +- internal/generator/ecc/template/point.go.tmpl | 248 ++++---- .../ecc/template/tests/marshal.go.tmpl | 151 ++--- .../ecc/template/tests/multiexp.go.tmpl | 10 +- internal/generator/main.go | 158 ++--- 10 files changed, 497 insertions(+), 727 deletions(-) diff --git a/internal/generator/config/kb8.go b/internal/generator/config/kb8.go index dcca022bae..9301bef100 100644 --- a/internal/generator/config/kb8.go +++ b/internal/generator/config/kb8.go @@ -6,6 +6,7 @@ var KB8 = Curve{ EnumID: "KB8", FpModulus: "2130706433", FrModulus: "424804331891979973455971894938199991839487883914575852667663156896715214921", + NoFieldSuite: true, G1: Point{ CoordType: "fptower.E8", CoordExtDegree: 8, diff --git a/internal/generator/ecc/generate.go b/internal/generator/ecc/generate.go index eba4279455..4e606feae8 100644 --- a/internal/generator/ecc/generate.go +++ b/internal/generator/ecc/generate.go @@ -1,7 +1,6 @@ package ecc import ( - "os" "path/filepath" "slices" "sort" @@ -26,11 +25,6 @@ func Generate(conf config.Curve, baseDir string, gen *common.Generator) error { {File: filepath.Join(baseDir, "g1.go"), Templates: []string{"point.go.tmpl"}}, {File: filepath.Join(baseDir, "g1_test.go"), Templates: []string{"tests/point.go.tmpl"}}, } - // if not secp256k1, generate the lagrange transform - if conf.Name != config.SECP256K1.Name || conf.Name != config.GRUMPKIN.Name || conf.Name != config.SECP256R1.Name { - os.Remove(filepath.Join(baseDir, "g1_lagrange.go")) - os.Remove(filepath.Join(baseDir, "g1_lagrange_test.go")) - } g1 := pconf{conf, conf.G1} if err := eccGen.Generate(g1, packageName, "", "", entries...); err != nil { @@ -128,32 +122,27 @@ func Generate(conf config.Curve, baseDir string, gen *common.Generator) error { } // marshal - entries = []bavard.Entry{ - {File: filepath.Join(baseDir, "marshal_test.go"), Templates: []string{"tests/marshal.go.tmpl"}}, - } - // secp256k1 uses a hand-maintained SEC1-style prefix-byte marshal format: - // its base field has no spare top bits, so the generic bit-stealing template does not apply. - if conf.Name != config.SECP256K1.Name { - entries = append(entries, bavard.Entry{File: filepath.Join(baseDir, "marshal.go"), Templates: []string{"marshal.go.tmpl"}}) - } - - if err := eccGen.GenerateWithOptions(conf, packageName, "", "", bavardOpts, entries...); err != nil { - return err - } + if conf.GenerateMarshal() { + entries = []bavard.Entry{ + {File: filepath.Join(baseDir, "marshal.go"), Templates: []string{"marshal.go.tmpl"}}, + {File: filepath.Join(baseDir, "marshal_test.go"), Templates: []string{"tests/marshal.go.tmpl"}}, + } - // No G2 for single-group curves. - if !conf.HasG2() { - return nil + if err := eccGen.GenerateWithOptions(conf, packageName, "", "", bavardOpts, entries...); err != nil { + return err + } } // G2 - entries = []bavard.Entry{ - {File: filepath.Join(baseDir, "g2.go"), Templates: []string{"point.go.tmpl"}}, - {File: filepath.Join(baseDir, "g2_test.go"), Templates: []string{"tests/point.go.tmpl"}}, - } - g2 := pconf{conf, conf.G2} - if err := eccGen.Generate(g2, packageName, "", "", entries...); err != nil { - return err + if conf.HasG2() { + entries = []bavard.Entry{ + {File: filepath.Join(baseDir, "g2.go"), Templates: []string{"point.go.tmpl"}}, + {File: filepath.Join(baseDir, "g2_test.go"), Templates: []string{"tests/point.go.tmpl"}}, + } + g2 := pconf{conf, conf.G2} + if err := eccGen.Generate(g2, packageName, "", "", entries...); err != nil { + return err + } } return nil diff --git a/internal/generator/ecc/template/marshal.go.tmpl b/internal/generator/ecc/template/marshal.go.tmpl index e02daed105..9ebaf0de45 100644 --- a/internal/generator/ecc/template/marshal.go.tmpl +++ b/internal/generator/ecc/template/marshal.go.tmpl @@ -12,29 +12,24 @@ import ( "reflect" "errors" "encoding/binary" + {{- if ge .FpUnusedBits 2}} "sync/atomic" + {{- end}} - {{- if .HasG2 }} + {{- if .HasG2}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" - {{- end }} + {{- end}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" + {{- if ge .FpUnusedBits 2}} "github.com/consensys/gnark-crypto/parallel" + {{- end}} ) -// To encode G1Affine and G2Affine points, we mask the most significant bits with these bits to specify without ambiguity +{{- if ge .FpUnusedBits 3}} +// To encode G1Affine {{- if .HasG2}} and G2Affine {{- end}} points, we mask the most significant bits with these bits to specify without ambiguity // metadata needed for point (de)compression -{{- if eq .Name "kb8"}} -const ( - mMask byte = 0xff - mUncompressed byte = 0x00 - mUncompressedInfinity byte = 0x01 - mCompressedSmallest byte = 0x02 - mCompressedLargest byte = 0x03 - mCompressedInfinity byte = 0x04 -) -{{- else if ge .FpUnusedBits 3}} // we follow the BLS12-381 style encoding as specified in ZCash and now IETF // see https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/11/ // Appendix C. ZCash serialization format for BLS12_381 @@ -49,7 +44,9 @@ const ( mCompressedInfinity byte = 0b110 << 5 _ byte = 0b111 << 5 // invalid ) -{{- else}} +{{- else if ge .FpUnusedBits 2}} +// To encode G1Affine {{- if .HasG2}} and G2Affine {{- end}} points, we mask the most significant bits with these bits to specify without ambiguity +// metadata needed for point (de)compression // we have less than 3 bits available on the msw, so we can't follow BLS12-381 style encoding. // the difference is the case where a point is infinity and uncompressed is not flagged const ( @@ -60,16 +57,19 @@ const ( mCompressedInfinity byte = 0b01 << 6 ) {{- end}} +{{- /* when FpUnusedBits < 2, no point compression is available; only uncompressed serialization is generated */}} -{{- if .HasG2 }} +{{- if .HasG2}} // SizeOfGT represents the size in bytes that a GT element need in binary form const SizeOfGT = fptower.SizeOfGT -{{- end }} +{{- end}} +{{- if ge .FpUnusedBits 2}} var ( ErrInvalidInfinityEncoding = errors.New("invalid infinity point encoding") ErrInvalidEncoding = errors.New("invalid point encoding") ) +{{- end}} // Encoder writes {{.Name}} object values to an output stream type Encoder struct { @@ -99,7 +99,7 @@ func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { // Decode reads the binary encoding of v from the stream -// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, *[]G1Affine or *[]G2Affine +// type must be *uint64, *fr.Element, *fp.Element, *G1Affine, {{- if .HasG2}} *G2Affine, {{- end}} *[]G1Affine {{- if .HasG2}} or *[]G2Affine {{- end}} func (dec *Decoder) Decode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { @@ -119,11 +119,11 @@ func (dec *Decoder) Decode(v any) (err error) { return } - {{- if .HasG2 }} + {{- if .HasG2}} var buf [SizeOfG2AffineUncompressed]byte - {{- else }} + {{- else}} var buf [SizeOfG1AffineUncompressed]byte - {{- end }} + {{- end}} var read int var sliceLen uint32 @@ -214,6 +214,7 @@ func (dec *Decoder) Decode(v any) (err error) { } return case *G1Affine: + {{- if ge .FpUnusedBits 2}} // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineCompressed]) dec.n += int64(read) @@ -230,10 +231,10 @@ func (dec *Decoder) Decode(v any) (err error) { } {{- end}} - // most significant byte contains metadata + // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG1AffineUncompressed - // we read more. + // we read more. read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) dec.n += int64(read) if err != nil { @@ -241,8 +242,17 @@ func (dec *Decoder) Decode(v any) (err error) { } } _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) - return - {{- if .HasG2 }} + {{- else}} + // no point compression + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + _, err = t.setBytes(buf[:SizeOfG1AffineUncompressed], dec.subGroupCheck) + {{- end}} + return + {{- if .HasG2}} case *G2Affine: // we start by reading compressed point size, if metadata tells us it is uncompressed, we read more. read, err = io.ReadFull(dec.r, buf[:SizeOfG2AffineCompressed]) @@ -260,10 +270,10 @@ func (dec *Decoder) Decode(v any) (err error) { } {{- end}} - // most significant byte contains metadata + // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG2AffineUncompressed - // we read more. + // we read more. read, err = io.ReadFull(dec.r, buf[SizeOfG2AffineCompressed:SizeOfG2AffineUncompressed]) dec.n += int64(read) if err != nil { @@ -271,8 +281,8 @@ func (dec *Decoder) Decode(v any) (err error) { } } _, err = t.setBytes(buf[:nbBytes], dec.subGroupCheck) - return - {{- end }} + return + {{- end}} case *[]G1Affine: sliceLen, err = dec.readUint32() if err != nil { @@ -281,6 +291,7 @@ func (dec *Decoder) Decode(v any) (err error) { if len(*t) != int(sliceLen) || *t == nil { *t = make([]G1Affine, sliceLen) } + {{- if ge .FpUnusedBits 2}} compressed := make([]bool, sliceLen) for i := range len(*t) { @@ -300,10 +311,10 @@ func (dec *Decoder) Decode(v any) (err error) { } {{- end}} - // most significant byte contains metadata + // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG1AffineUncompressed - // we read more. + // we read more. read, err = io.ReadFull(dec.r, buf[SizeOfG1AffineCompressed:SizeOfG1AffineUncompressed]) dec.n += int64(read) if err != nil { @@ -314,9 +325,9 @@ func (dec *Decoder) Decode(v any) (err error) { return } } else { - var r bool + var r bool if r, err = (*t)[i].unsafeSetCompressedBytes(buf[:nbBytes]); err != nil { - return + return } compressed[i] = !r } @@ -338,9 +349,21 @@ func (dec *Decoder) Decode(v any) (err error) { if nbErrs != 0 { return errors.New("point decompression failed") } - + {{- else}} + for i := range len(*t) { + read, err = io.ReadFull(dec.r, buf[:SizeOfG1AffineUncompressed]) + dec.n += int64(read) + if err != nil { + return + } + if _, err = (*t)[i].setBytes(buf[:SizeOfG1AffineUncompressed], dec.subGroupCheck); err != nil { + return + } + } + {{- end}} + return nil - {{- if .HasG2 }} + {{- if .HasG2}} case *[]G2Affine: sliceLen, err = dec.readUint32() if err != nil { @@ -369,10 +392,10 @@ func (dec *Decoder) Decode(v any) (err error) { } {{- end}} - // most significant byte contains metadata + // most significant byte contains metadata if !isCompressed(buf[0]) { nbBytes = SizeOfG2AffineUncompressed - // we read more. + // we read more. read, err = io.ReadFull(dec.r, buf[SizeOfG2AffineCompressed:SizeOfG2AffineUncompressed]) dec.n += int64(read) if err != nil { @@ -407,9 +430,9 @@ func (dec *Decoder) Decode(v any) (err error) { if nbErrs != 0 { return errors.New("point decompression failed") } - + return nil - {{- end }} + {{- end}} default: n := binary.Size(t) if n == -1 { @@ -452,6 +475,8 @@ func (dec *Decoder) readUint64() (r uint64, err error) { return } +{{- if ge .FpUnusedBits 2}} + {{ if ge .FpUnusedBits 3}} // isMaskInvalid returns true if the mask is invalid func isMaskInvalid(msb byte) bool { @@ -465,6 +490,8 @@ func isCompressed(msb byte) bool { return mData != mUncompressed{{- if ge .FpUnusedBits 3}}&& mData != mUncompressedInfinity {{- end}} } +{{- end}} + // NewEncoder returns a binary encoder supporting curve {{.Name}} objects func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { @@ -485,7 +512,7 @@ func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { // Encode writes the binary encoding of v to the stream -// type must be uint64, *fr.Element, *fp.Element, *G1Affine, *G2Affine, []G1Affine, []G2Affine, *[]G1Affine or *[]G2Affine +// type must be uint64, *fr.Element, *fp.Element, *G1Affine, {{- if .HasG2}} *G2Affine, {{- end}} []G1Affine, {{- if .HasG2}} []G2Affine, {{- end}} *[]G1Affine {{- if .HasG2}} or *[]G2Affine {{- end}} func (enc *Encoder) Encode(v any) (err error) { if enc.raw { return enc.encodeRaw(v) @@ -515,6 +542,7 @@ func NoSubgroupChecks() func(*Decoder) { } } +{{- if ge .FpUnusedBits 2}} // isZeroed checks that the provided bytes are at 0 func isZeroed(firstByte byte, buf []byte) bool { if firstByte != 0 { @@ -527,9 +555,10 @@ func isZeroed(firstByte byte, buf []byte) bool { } return true } +{{- end}} -{{template "encode" dict "Raw" "" "all" .}} -{{template "encode" dict "Raw" "Raw" "all" .}} +{{template "encode" dict "Raw" "" "HasG2" .HasG2 "SupportsPointCompression" .SupportsPointCompression "Name" .Name}} +{{template "encode" dict "Raw" "Raw" "HasG2" .HasG2 "SupportsPointCompression" .SupportsPointCompression "Name" .Name}} func (enc *Encoder) writeUint64Slice(t []uint64) (err error) { if err = enc.writeUint32(uint32(len(t))); err != nil { @@ -581,7 +610,7 @@ func (enc *Encoder) writeUint32(a uint32) error { func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New("{{.all.Name}} encoder: can't encode ") + return errors.New("{{.Name}} encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -615,13 +644,13 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - {{- if .all.HasG2 }} + {{- if .HasG2}} case *G2Affine: buf := t.{{- $.Raw}}Bytes() written, err = enc.w.Write(buf[:]) enc.n += int64(written) return - {{- end }} + {{- end}} case fr.Vector: written64, err = t.WriteTo(enc.w) enc.n += written64 @@ -678,7 +707,7 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { } enc.n += 4 - var buf [SizeOfG1Affine{{- if $.Raw}}Uncompressed{{- else}}Compressed{{- end}}]byte + var buf [SizeOfG1Affine{{- if or $.Raw (not $.SupportsPointCompression)}}Uncompressed{{- else}}Compressed{{- end}}]byte for i := range len(t) { buf = t[i].{{- $.Raw}}Bytes() @@ -689,7 +718,7 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { } } return nil - {{- if .all.HasG2 }} + {{- if .HasG2}} case *[]G2Affine: return enc.encode{{- $.Raw}}(*t) case []G2Affine: @@ -711,11 +740,11 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { } } return nil - {{- end }} + {{- end}} default: n := binary.Size(t) if n == -1 { - return errors.New("{{.all.Name}} encoder: unsupported type") + return errors.New("{{.Name}} encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -725,193 +754,32 @@ func (enc *Encoder) encode{{- $.Raw}}(v any) (err error) { {{end}} -{{- $sizeOfFp := .Fp.NbBytes}} +{{- $sizeOfFp := mul .Fp.NbWords 8}} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G1.CoordType "PointName" .G1.PointName "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange}} -{{- if .HasG2 }} +{{- if .HasG2}} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G2.CoordType "PointName" .G2.PointName "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange}} -{{- end }} +{{- end}} {{define "marshalpoint"}} -{{- if and (eq $.all.Name "kb8") (eq $.PointName "g1") }} - -// SizeOf{{ $.TAffine }}Compressed represents the size in bytes that a {{ $.TAffine }} need in binary form, compressed -const SizeOf{{ $.TAffine }}Compressed = 1 + {{ $.sizeOfFp }}*8 - -// SizeOf{{ $.TAffine }}Uncompressed represents the size in bytes that a {{ $.TAffine }} need in binary form, uncompressed -const SizeOf{{ $.TAffine }}Uncompressed = 1 + 2*({{ $.sizeOfFp }}*8) - -func (p *{{ $.TAffine }}) Marshal() ([]byte) { - b := p.RawBytes() - return b[:] -} - -func (p *{{ $.TAffine }}) Unmarshal(buf []byte) error { - _, err := p.SetBytes(buf) - return err -} - -func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Compressed]byte) { - if p.X.IsZero() && p.Y.IsZero() { - res[0] = mCompressedInfinity - return - } - res[0] = mCompressedSmallest - if p.Y.LexicographicallyLargest() { - res[0] = mCompressedLargest - } - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X.C1.B1.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[5:5+fp.Bytes]), p.X.C1.B1.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[9:9+fp.Bytes]), p.X.C1.B0.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[13:13+fp.Bytes]), p.X.C1.B0.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[17:17+fp.Bytes]), p.X.C0.B1.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[21:21+fp.Bytes]), p.X.C0.B1.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[25:25+fp.Bytes]), p.X.C0.B0.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[29:29+fp.Bytes]), p.X.C0.B0.A0) - return -} - -func (p *{{ $.TAffine }}) RawBytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byte) { - if p.X.IsZero() && p.Y.IsZero() { - res[0] = mUncompressedInfinity - return - } - res[0] = mUncompressed - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[33:33+fp.Bytes]), p.Y.C1.B1.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[37:37+fp.Bytes]), p.Y.C1.B1.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[41:41+fp.Bytes]), p.Y.C1.B0.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[45:45+fp.Bytes]), p.Y.C1.B0.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[49:49+fp.Bytes]), p.Y.C0.B1.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[53:53+fp.Bytes]), p.Y.C0.B1.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[57:57+fp.Bytes]), p.Y.C0.B0.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[61:61+fp.Bytes]), p.Y.C0.B0.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[1:1+fp.Bytes]), p.X.C1.B1.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[5:5+fp.Bytes]), p.X.C1.B1.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[9:9+fp.Bytes]), p.X.C1.B0.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[13:13+fp.Bytes]), p.X.C1.B0.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[17:17+fp.Bytes]), p.X.C0.B1.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[21:21+fp.Bytes]), p.X.C0.B1.A0) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[25:25+fp.Bytes]), p.X.C0.B0.A1) - fp.BigEndian.PutElement((*[fp.Bytes]byte)(res[29:29+fp.Bytes]), p.X.C0.B0.A0) - return -} - -func (p *{{ $.TAffine }}) SetBytes(buf []byte) (int, error) { - return p.setBytes(buf, true) -} - -func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) { - if len(buf) < SizeOf{{ $.TAffine }}Compressed { - return 0, io.ErrShortBuffer - } - mData := buf[0] - if isMaskInvalid(mData) { - return 0, ErrInvalidEncoding - } - if mData == mUncompressed || mData == mUncompressedInfinity { - if len(buf) < SizeOf{{ $.TAffine }}Uncompressed { - return 0, io.ErrShortBuffer - } - } - if mData == mCompressedInfinity { - if !isZeroed(0, buf[1:SizeOf{{ $.TAffine }}Compressed]) { - return 0, ErrInvalidInfinityEncoding - } - p.X.SetZero() - p.Y.SetZero() - return SizeOf{{ $.TAffine }}Compressed, nil - } - if mData == mUncompressedInfinity { - if !isZeroed(0, buf[1:SizeOf{{ $.TAffine }}Uncompressed]) { - return 0, ErrInvalidInfinityEncoding - } - p.X.SetZero() - p.Y.SetZero() - return SizeOf{{ $.TAffine }}Uncompressed, nil - } - if err := p.X.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*0 : 1+fp.Bytes*1]); err != nil { return 0, err } - if err := p.X.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*1 : 1+fp.Bytes*2]); err != nil { return 0, err } - if err := p.X.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*2 : 1+fp.Bytes*3]); err != nil { return 0, err } - if err := p.X.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*3 : 1+fp.Bytes*4]); err != nil { return 0, err } - if err := p.X.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*4 : 1+fp.Bytes*5]); err != nil { return 0, err } - if err := p.X.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*5 : 1+fp.Bytes*6]); err != nil { return 0, err } - if err := p.X.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*6 : 1+fp.Bytes*7]); err != nil { return 0, err } - if err := p.X.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*7 : 1+fp.Bytes*8]); err != nil { return 0, err } - if mData == mUncompressed { - if err := p.Y.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*8 : 1+fp.Bytes*9]); err != nil { return 0, err } - if err := p.Y.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*9 : 1+fp.Bytes*10]); err != nil { return 0, err } - if err := p.Y.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*10 : 1+fp.Bytes*11]); err != nil { return 0, err } - if err := p.Y.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*11 : 1+fp.Bytes*12]); err != nil { return 0, err } - if err := p.Y.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*12 : 1+fp.Bytes*13]); err != nil { return 0, err } - if err := p.Y.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*13 : 1+fp.Bytes*14]); err != nil { return 0, err } - if err := p.Y.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*14 : 1+fp.Bytes*15]); err != nil { return 0, err } - if err := p.Y.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*15 : 1+fp.Bytes*16]); err != nil { return 0, err } - if subGroupCheck && !p.IsInSubGroup() { return 0, errors.New("invalid point: subgroup check failed") } - return SizeOf{{ $.TAffine }}Uncompressed, nil - } - p.Y.C0.B0.A0[0] = uint32(mData) - if err := p.unsafeComputeY(subGroupCheck); err != nil { return 0, err } - return SizeOf{{ $.TAffine }}Compressed, nil -} - -func (p *{{ $.TAffine }}) unsafeComputeY(subGroupCheck bool) error { - mData := byte(p.Y.C0.B0.A0[0]) - var YSquared, Y, tmp {{$.CoordType}} - YSquared.Square(&p.X).Mul(&YSquared, &p.X) - tmp.Mul(&p.X, &aCurveCoeff) - YSquared.Add(&YSquared, &tmp).Add(&YSquared, &bCurveCoeff) - if YSquared.Legendre() == -1 { - return errors.New("invalid compressed coordinate: square root doesn't exist") - } - Y.Sqrt(&YSquared) - if Y.LexicographicallyLargest() { - if mData == mCompressedSmallest { - Y.Neg(&Y) - } - } else if mData == mCompressedLargest { - Y.Neg(&Y) - } - p.Y = Y - if subGroupCheck && !p.IsInSubGroup() { return errors.New("invalid point: subgroup check failed") } - return nil -} - -func (p *{{ $.TAffine }}) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, err error) { - mData := buf[0] - if mData == mCompressedInfinity { - isInfinity = true - if !isZeroed(0, buf[1:SizeOf{{ $.TAffine }}Compressed]) { - return isInfinity, ErrInvalidInfinityEncoding - } - p.X.SetZero() - p.Y.SetZero() - return isInfinity, nil - } - if err := p.X.C1.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*0 : 1+fp.Bytes*1]); err != nil { return false, err } - if err := p.X.C1.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*1 : 1+fp.Bytes*2]); err != nil { return false, err } - if err := p.X.C1.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*2 : 1+fp.Bytes*3]); err != nil { return false, err } - if err := p.X.C1.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*3 : 1+fp.Bytes*4]); err != nil { return false, err } - if err := p.X.C0.B1.A1.SetBytesCanonical(buf[1+fp.Bytes*4 : 1+fp.Bytes*5]); err != nil { return false, err } - if err := p.X.C0.B1.A0.SetBytesCanonical(buf[1+fp.Bytes*5 : 1+fp.Bytes*6]); err != nil { return false, err } - if err := p.X.C0.B0.A1.SetBytesCanonical(buf[1+fp.Bytes*6 : 1+fp.Bytes*7]); err != nil { return false, err } - if err := p.X.C0.B0.A0.SetBytesCanonical(buf[1+fp.Bytes*7 : 1+fp.Bytes*8]); err != nil { return false, err } - p.Y.C0.B0.A0[0] = uint32(mData) - return isInfinity, nil -} - -{{- else }} +{{- if ge .all.FpUnusedBits 2}} // SizeOf{{ $.TAffine }}Compressed represents the size in bytes that a {{ $.TAffine }} need in binary form, compressed -const SizeOf{{ $.TAffine }}Compressed = {{ $.sizeOfFp }} {{- if eq $.CoordType "fptower.E2"}} * 2 {{- end}} {{- if eq $.CoordType "fptower.E4"}} * 4 {{- end}} {{- if eq $.CoordType "fptower.E8"}} * 8 {{- end}} +const SizeOf{{ $.TAffine }}Compressed = {{ $.sizeOfFp }} {{- if eq $.CoordType "fptower.E2"}} * 2 {{- end}} {{- if eq $.CoordType "fptower.E4"}} * 4 {{- end}} +{{- end}} // SizeOf{{ $.TAffine }}Uncompressed represents the size in bytes that a {{ $.TAffine }} need in binary form, uncompressed +{{- if ge .all.FpUnusedBits 2}} const SizeOf{{ $.TAffine }}Uncompressed = SizeOf{{ $.TAffine }}Compressed * 2 +{{- else}} +const SizeOf{{ $.TAffine }}Uncompressed = {{ $.sizeOfFp }} {{- if eq $.CoordType "fptower.E2"}} * 2 {{- end}} {{- if eq $.CoordType "fptower.E4"}} * 4 {{- end}} * 2 +{{- end}} @@ -930,6 +798,7 @@ func (p *{{ $.TAffine }}) Unmarshal(buf []byte) error { +{{- if ge .all.FpUnusedBits 2}} // Bytes returns binary representation of p // will store X coordinate in regular form and a parity bit {{- if ge .all.FpUnusedBits 3}} @@ -962,8 +831,8 @@ func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Compressed]byte) { msbMask := mCompressedSmallest // compressed, we need to know if Y is lexicographically bigger than -Y - // if p.Y ">" -p.Y - if p.Y.LexicographicallyLargest() { + // if p.Y ">" -p.Y + if p.Y.LexicographicallyLargest() { msbMask = mCompressedLargest } @@ -973,7 +842,7 @@ func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Compressed]byte) { {{- $offset := $.sizeOfFp}} {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.A0"}} {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X.A1"}} - {{- else if eq $.CoordType "fptower.E4"}} + {{- else if eq $.CoordType "fptower.E4"}} // p.X.B1.A1 | p.X.B1.A0 | p.X.B0.A1 | p.X.B0.A0 {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X.B1.A1"}} {{- $offset := mul $.sizeOfFp 1}} @@ -982,23 +851,6 @@ func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Compressed]byte) { {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A1"}} {{- $offset := mul $.sizeOfFp 3}} {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A0"}} - {{- else if eq $.CoordType "fptower.E8"}} - // p.X.C1.B1.A1 | p.X.C1.B1.A0 | p.X.C1.B0.A1 | p.X.C1.B0.A0 | p.X.C0.B1.A1 | p.X.C0.B1.A0 | p.X.C0.B0.A1 | p.X.C0.B0.A0 - {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X.C1.B1.A1"}} - {{- $offset := mul $.sizeOfFp 1}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B1.A0"}} - {{- $offset := mul $.sizeOfFp 2}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A1"}} - {{- $offset := mul $.sizeOfFp 3}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A0"}} - {{- $offset := mul $.sizeOfFp 4}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A1"}} - {{- $offset := mul $.sizeOfFp 5}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A0"}} - {{- $offset := mul $.sizeOfFp 6}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A1"}} - {{- $offset := mul $.sizeOfFp 7}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A0"}} {{- else}} {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X"}} {{- end}} @@ -1007,6 +859,13 @@ func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Compressed]byte) { return } +{{- else}} +// Bytes returns binary representation of p (stores X and Y coordinate) +// point compression is not available for this curve +func (p *{{ $.TAffine }}) Bytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byte) { + return p.RawBytes() +} +{{- end}} // RawBytes returns binary representation of p (stores X and Y coordinate) @@ -1015,11 +874,11 @@ func (p *{{ $.TAffine }}) RawBytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byt // check if p is infinity point if p.X.IsZero() && p.Y.IsZero() { - {{if ge .all.FpUnusedBits 3}} - res[0] = mUncompressedInfinity - {{else}} - res[0] = mUncompressed - {{end}} + {{- if ge .all.FpUnusedBits 3}} + res[0] = mUncompressedInfinity + {{- else if ge .all.FpUnusedBits 2}} + res[0] = mUncompressed + {{- end}} return } @@ -1043,24 +902,6 @@ func (p *{{ $.TAffine }}) RawBytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byt {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.B0.A1"}} {{- $offset := mul $.sizeOfFp 7}} {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.B0.A0"}} - {{- else if eq $.CoordType "fptower.E8"}} - // p.Y.C1.B1.A1 | p.Y.C1.B1.A0 | p.Y.C1.B0.A1 | p.Y.C1.B0.A0 | p.Y.C0.B1.A1 | p.Y.C0.B1.A0 | p.Y.C0.B0.A1 | p.Y.C0.B0.A0 - {{- $offset := mul $.sizeOfFp 8}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B1.A1"}} - {{- $offset := mul $.sizeOfFp 9}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B1.A0"}} - {{- $offset := mul $.sizeOfFp 10}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B0.A1"}} - {{- $offset := mul $.sizeOfFp 11}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C1.B0.A0"}} - {{- $offset := mul $.sizeOfFp 12}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B1.A1"}} - {{- $offset := mul $.sizeOfFp 13}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B1.A0"}} - {{- $offset := mul $.sizeOfFp 14}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B0.A1"}} - {{- $offset := mul $.sizeOfFp 15}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.Y.C0.B0.A0"}} {{- else}} {{- template "putFp" dict "all" .all "OffSet" $.sizeOfFp "From" "p.Y"}} {{- end}} @@ -1080,30 +921,15 @@ func (p *{{ $.TAffine }}) RawBytes() (res [SizeOf{{ $.TAffine }}Uncompressed]byt {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A1"}} {{- $offset := mul $.sizeOfFp 3}} {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.B0.A0"}} - {{- else if eq $.CoordType "fptower.E8"}} - // p.X.C1.B1.A1 | p.X.C1.B1.A0 | p.X.C1.B0.A1 | p.X.C1.B0.A0 | p.X.C0.B1.A1 | p.X.C0.B1.A0 | p.X.C0.B0.A1 | p.X.C0.B0.A0 - {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X.C1.B1.A1"}} - {{- $offset := mul $.sizeOfFp 1}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B1.A0"}} - {{- $offset := mul $.sizeOfFp 2}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A1"}} - {{- $offset := mul $.sizeOfFp 3}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C1.B0.A0"}} - {{- $offset := mul $.sizeOfFp 4}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A1"}} - {{- $offset := mul $.sizeOfFp 5}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B1.A0"}} - {{- $offset := mul $.sizeOfFp 6}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A1"}} - {{- $offset := mul $.sizeOfFp 7}} - {{- template "putFp" dict "all" .all "OffSet" $offset "From" "p.X.C0.B0.A0"}} {{- else}} {{- template "putFp" dict "all" .all "OffSet" 0 "From" "p.X"}} {{- end}} + {{- if ge .all.FpUnusedBits 2}} res[0] |= mUncompressed + {{- end}} - return + return } @@ -1122,6 +948,7 @@ func (p *{{ $.TAffine }}) SetBytes(buf []byte) (int, error) { } +{{- if ge .all.FpUnusedBits 2}} func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) { if len(buf) < SizeOf{{ $.TAffine }}Compressed { return 0, io.ErrShortBuffer @@ -1136,14 +963,14 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) return 0, ErrInvalidEncoding } {{- end}} - + // check buffer size if (mData == mUncompressed) {{- if ge .all.FpUnusedBits 3}} || (mData == mUncompressedInfinity) {{- end}} { if len(buf) < SizeOf{{ $.TAffine }}Uncompressed { return 0, io.ErrShortBuffer } - } + } // infinity encoded, we still check that the buffer is full of zeroes. if (mData == mCompressedInfinity) { @@ -1155,7 +982,7 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) return SizeOf{{ $.TAffine }}Compressed, nil } - {{- if ge .all.FpUnusedBits 3}} + {{- if ge .all.FpUnusedBits 3}} if (mData == mUncompressedInfinity) { if !isZeroed(buf[0] & ^mMask, buf[1:SizeOf{{ $.TAffine }}Uncompressed]) { return 0, ErrInvalidInfinityEncoding @@ -1164,7 +991,7 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) p.Y.SetZero() return SizeOf{{ $.TAffine }}Uncompressed, nil } - {{- end}} + {{- end}} // uncompressed point if mData == mUncompressed { @@ -1184,7 +1011,7 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) if err := p.Y.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { return 0, err } - {{- else if eq $.CoordType "fptower.E4"}} + {{- else if eq $.CoordType "fptower.E4"}} // p.X.B1.A1 | p.X.B1.A0 | p.X.B0.A1 | p.X.B0.A0 if err := p.X.B1.A1.SetBytesCanonical(buf[fp.Bytes*0:fp.Bytes*1]); err != nil { return 0, err @@ -1211,55 +1038,6 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) if err := p.Y.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { return 0, err } - {{- else if eq $.CoordType "fptower.E8"}} - if err := p.X.C1.B1.A1.SetBytesCanonical(buf[fp.Bytes*0:fp.Bytes*1]); err != nil { - return 0, err - } - if err := p.X.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*1:fp.Bytes*2]); err != nil { - return 0, err - } - if err := p.X.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { - return 0, err - } - if err := p.X.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { - return 0, err - } - if err := p.X.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*4:fp.Bytes*5]); err != nil { - return 0, err - } - if err := p.X.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*5:fp.Bytes*6]); err != nil { - return 0, err - } - if err := p.X.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*6:fp.Bytes*7]); err != nil { - return 0, err - } - if err := p.X.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { - return 0, err - } - if err := p.Y.C1.B1.A1.SetBytesCanonical(buf[fp.Bytes*8:fp.Bytes*9]); err != nil { - return 0, err - } - if err := p.Y.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*9:fp.Bytes*10]); err != nil { - return 0, err - } - if err := p.Y.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*10:fp.Bytes*11]); err != nil { - return 0, err - } - if err := p.Y.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*11:fp.Bytes*12]); err != nil { - return 0, err - } - if err := p.Y.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*12:fp.Bytes*13]); err != nil { - return 0, err - } - if err := p.Y.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*13:fp.Bytes*14]); err != nil { - return 0, err - } - if err := p.Y.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*14:fp.Bytes*15]); err != nil { - return 0, err - } - if err := p.Y.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*15:fp.Bytes*16]); err != nil { - return 0, err - } {{- else}} if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { return 0, err @@ -1269,7 +1047,7 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) } {{- end}} - // subgroup check + // subgroup check if subGroupCheck && !p.IsInSubGroup() { return 0, errors.New("invalid point: subgroup check failed") } @@ -1309,31 +1087,6 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) if err := p.X.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { return 0, err } - {{- else if eq $.CoordType "fptower.E8"}} - if err := p.X.C1.B1.A1.SetBytesCanonical(bufX[fp.Bytes*0:fp.Bytes*1]); err != nil { - return 0, err - } - if err := p.X.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*1:fp.Bytes*2]); err != nil { - return 0, err - } - if err := p.X.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { - return 0, err - } - if err := p.X.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { - return 0, err - } - if err := p.X.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*4:fp.Bytes*5]); err != nil { - return 0, err - } - if err := p.X.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*5:fp.Bytes*6]); err != nil { - return 0, err - } - if err := p.X.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*6:fp.Bytes*7]); err != nil { - return 0, err - } - if err := p.X.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { - return 0, err - } {{- else}} if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { return 0, err @@ -1344,9 +1097,16 @@ func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) var YSquared, Y {{$.CoordType}} YSquared.Square(&p.X).Mul(&YSquared, &p.X) + {{- if and (eq .PointName "g1") (not .all.G1.IsZeroA)}} + { + var aX {{$.CoordType}} + aX.Mul(&aCurveCoeff, &p.X) + YSquared.Add(&YSquared, &aX) + } + {{- end}} YSquared.Add(&YSquared, &{{- if eq .PointName "g2"}}bTwistCurveCoeff{{- else}}bCurveCoeff{{- end}}) - {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4") (eq $.CoordType "fptower.E8")}} + {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4")}} if YSquared.Legendre() == -1 { return 0, errors.New("invalid compressed coordinate: square root doesn't exist") } @@ -1390,8 +1150,6 @@ func (p *{{ $.TAffine }}) unsafeComputeY(subGroupCheck bool) error { mData := byte(p.Y.A0[0]) {{ else if eq $.CoordType "fptower.E4"}} mData := byte(p.Y.B0.A0[0]) - {{ else if eq $.CoordType "fptower.E8"}} - mData := byte(p.Y.C0.B0.A0[0]) {{ else}} mData := byte(p.Y[0]) {{ end}} @@ -1399,18 +1157,18 @@ func (p *{{ $.TAffine }}) unsafeComputeY(subGroupCheck bool) error { // we have a compressed coordinate, we need to solve the curve equation to compute Y var YSquared, Y {{$.CoordType}} - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - var tmp {{$.CoordType}} - {{- end }} YSquared.Square(&p.X).Mul(&YSquared, &p.X) - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - tmp.Mul(&p.X, &aCurveCoeff) - YSquared.Add(&YSquared, &tmp) - {{- end }} + {{- if and (eq .PointName "g1") (not .all.G1.IsZeroA)}} + { + var aX {{$.CoordType}} + aX.Mul(&aCurveCoeff, &p.X) + YSquared.Add(&YSquared, &aX) + } + {{- end}} YSquared.Add(&YSquared, &{{- if eq .PointName "g2"}}bTwistCurveCoeff{{- else}}bCurveCoeff{{- end}}) - {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4") (eq $.CoordType "fptower.E8")}} + {{- if or (eq $.CoordType "fptower.E2") (eq $.CoordType "fptower.E4")}} if YSquared.Legendre() == -1 { return errors.New("invalid compressed coordinate: square root doesn't exist") } @@ -1497,46 +1255,81 @@ func (p *{{ $.TAffine }}) unsafeSetCompressedBytes(buf []byte) (isInfinity bool, // store mData in p.Y.B0.A0[0] p.Y.B0.A0[0] = uint64(mData) - {{- else if eq $.CoordType "fptower.E8"}} - if err := p.X.C1.B1.A1.SetBytesCanonical(bufX[fp.Bytes*0:fp.Bytes*1]); err != nil { + {{- else}} + if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { return false, err } - if err := p.X.C1.B1.A0.SetBytesCanonical(buf[fp.Bytes*1:fp.Bytes*2]); err != nil { - return false, err + // store mData in p.Y[0] + p.Y[0] = uint64(mData) + {{- end}} + + // recomputing Y will be done asynchronously + return isInfinity, nil +} + +{{- else}} +// no point compression - setBytes only handles uncompressed format +func (p *{{ $.TAffine }}) setBytes(buf []byte, subGroupCheck bool) (int, error) { + if len(buf) < SizeOf{{ $.TAffine }}Uncompressed { + return 0, io.ErrShortBuffer + } + + // read X and Y coordinates + {{- if eq $.CoordType "fptower.E2"}} + if err := p.X.A1.SetBytesCanonical(buf[:fp.Bytes]); err != nil { + return 0, err } - if err := p.X.C1.B0.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { - return false, err + if err := p.X.A0.SetBytesCanonical(buf[fp.Bytes:fp.Bytes*2]); err != nil { + return 0, err } - if err := p.X.C1.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { - return false, err + if err := p.Y.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { + return 0, err } - if err := p.X.C0.B1.A1.SetBytesCanonical(buf[fp.Bytes*4:fp.Bytes*5]); err != nil { - return false, err + if err := p.Y.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { + return 0, err } - if err := p.X.C0.B1.A0.SetBytesCanonical(buf[fp.Bytes*5:fp.Bytes*6]); err != nil { - return false, err + {{- else if eq $.CoordType "fptower.E4"}} + if err := p.X.B1.A1.SetBytesCanonical(buf[fp.Bytes*0:fp.Bytes*1]); err != nil { + return 0, err } - if err := p.X.C0.B0.A1.SetBytesCanonical(buf[fp.Bytes*6:fp.Bytes*7]); err != nil { - return false, err + if err := p.X.B1.A0.SetBytesCanonical(buf[fp.Bytes*1:fp.Bytes*2]); err != nil { + return 0, err } - if err := p.X.C0.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { - return false, err + if err := p.X.B0.A1.SetBytesCanonical(buf[fp.Bytes*2:fp.Bytes*3]); err != nil { + return 0, err + } + if err := p.X.B0.A0.SetBytesCanonical(buf[fp.Bytes*3:fp.Bytes*4]); err != nil { + return 0, err + } + if err := p.Y.B1.A1.SetBytesCanonical(buf[fp.Bytes*4:fp.Bytes*5]); err != nil { + return 0, err + } + if err := p.Y.B1.A0.SetBytesCanonical(buf[fp.Bytes*5:fp.Bytes*6]); err != nil { + return 0, err + } + if err := p.Y.B0.A1.SetBytesCanonical(buf[fp.Bytes*6:fp.Bytes*7]); err != nil { + return 0, err + } + if err := p.Y.B0.A0.SetBytesCanonical(buf[fp.Bytes*7:fp.Bytes*8]); err != nil { + return 0, err } - p.Y.C0.B0.A0[0] = {{- if eq $.all.Fp.NbBytes 4 }} uint32(mData) {{- else }} uint64(mData) {{- end }} {{- else}} - if err := p.X.SetBytesCanonical(bufX[:fp.Bytes]); err != nil { - return false, err + if err := p.X.SetBytesCanonical(buf[:fp.Bytes]); err != nil { + return 0, err + } + if err := p.Y.SetBytesCanonical(buf[fp.Bytes:fp.Bytes*2]); err != nil { + return 0, err } - // store mData in p.Y[0] - p.Y[0] = uint64(mData) {{- end}} - // recomputing Y will be done asynchronously - return isInfinity, nil -} - -{{- end }} + // subgroup check + if subGroupCheck && !p.IsInSubGroup() { + return 0, errors.New("invalid point: subgroup check failed") + } + return SizeOf{{ $.TAffine }}Uncompressed, nil +} +{{- end}} {{end}} diff --git a/internal/generator/ecc/template/multiexp.go.tmpl b/internal/generator/ecc/template/multiexp.go.tmpl index 3c0b3ef013..261471e5c1 100644 --- a/internal/generator/ecc/template/multiexp.go.tmpl +++ b/internal/generator/ecc/template/multiexp.go.tmpl @@ -16,15 +16,9 @@ import ( "runtime" ) -{{- if or (eq .Name "secp256k1") (eq .Name "secp256r1")}} -{{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 15}} -{{- else if eq .Name "grumpkin"}} -{{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 16}} -{{- else}} -{{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 16}} -{{- if .HasG2 }} -{{template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "cmax" 16}} -{{- end }} +{{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" .G1.CMax}} +{{- if .HasG2}} +{{template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "cmax" .G2.CMax}} {{- end}} @@ -182,11 +176,7 @@ func partitionScalars(scalars []fr.Element, c uint64, nbTasks int) ([]uint16, [ // for each chunk compute the statistics for chunkID := start; chunkID < end; chunkID++ { // indicates if a bucket is hit. - {{- if or (eq .Name "secp256k1") (eq .Name "secp256r1")}} - var b bitSetC15 - {{- else}} - var b bitSetC16 - {{- end}} + var b bitSetC{{.G1.CMax}} // digits for the chunk chunkDigits := digits[chunkID*len(scalars):(chunkID+1)*len(scalars)] diff --git a/internal/generator/ecc/template/multiexp_affine.go.tmpl b/internal/generator/ecc/template/multiexp_affine.go.tmpl index 086cf5a435..e5d42eb8ab 100644 --- a/internal/generator/ecc/template/multiexp_affine.go.tmpl +++ b/internal/generator/ecc/template/multiexp_affine.go.tmpl @@ -9,13 +9,13 @@ import ( "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" - {{- if and .HasG2 (ne .G1.CoordType .G2.CoordType) (ne .Name "secp256k1") (ne .Name "grumpkin") }} + {{- if and .HasG2 (ne .G1.CoordType .G2.CoordType) }} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" {{- end}} ) {{ template "multiexp" dict "CoordType" .G1.CoordType "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange}} -{{- if and .HasG2 (ne .Name "secp256k1") (ne .Name "grumpkin")}} +{{- if .HasG2}} {{ template "multiexp" dict "CoordType" .G2.CoordType "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange}} {{- end}} diff --git a/internal/generator/ecc/template/multiexp_jacobian.go.tmpl b/internal/generator/ecc/template/multiexp_jacobian.go.tmpl index d4e2ef4306..5b384dfbe8 100644 --- a/internal/generator/ecc/template/multiexp_jacobian.go.tmpl +++ b/internal/generator/ecc/template/multiexp_jacobian.go.tmpl @@ -9,7 +9,7 @@ {{ template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange }} -{{- if and .HasG2 (ne .Name "secp256k1") (ne .Name "grumpkin")}} +{{- if .HasG2}} {{ template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange }} {{- end}} diff --git a/internal/generator/ecc/template/point.go.tmpl b/internal/generator/ecc/template/point.go.tmpl index 8441dd0d66..f62f9cf5ff 100644 --- a/internal/generator/ecc/template/point.go.tmpl +++ b/internal/generator/ecc/template/point.go.tmpl @@ -17,7 +17,7 @@ import ( {{- end}} "github.com/consensys/gnark-crypto/parallel" "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" - {{- if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") (eq .CoordType "fptower.E8") }} + {{- if or (eq .CoordType "fptower.E2") (eq .CoordType "fptower.E4") }} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" {{else}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" @@ -240,17 +240,16 @@ func (p *{{ $TAffine }}) IsOnCurve() bool { if p.IsInfinity() { return true } - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - var left, right, tmp {{.CoordType}} - {{- else }} - var left, right {{.CoordType}} - {{- end }} + var left, right {{.CoordType}} left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - tmp.Mul(&p.X, &aCurveCoeff) - right.Add(&right, &tmp) - {{- end }} + {{- if not .IsZeroA}} + { + var aX {{.CoordType}} + aX.Mul(&p.X, &aCurveCoeff) + right.Add(&right, &aX) + } + {{- end}} {{- if eq .PointName "g1"}} right.Add(&right, &bCurveCoeff) {{- else}} @@ -261,10 +260,9 @@ func (p *{{ $TAffine }}) IsOnCurve() bool { // IsInSubGroup returns true if the affine point p is in the correct subgroup, false otherwise. func (p *{{ $TAffine }}) IsInSubGroup() bool { -{{- if eq .Name "bn254"}} - {{- if eq .PointName "g1"}} +{{- if and (not .CofactorCleaning) (eq .PointName "g1")}} return p.IsOnCurve() - {{- else if eq .PointName "g2"}} +{{- else if and (eq .Name "bn254") (eq .PointName "g2")}} if !p.IsOnCurve() { return false } @@ -281,7 +279,6 @@ func (p *{{ $TAffine }}) IsInSubGroup() bool { Double(&res) return res.Equal(&c) - {{- end}} {{- else if eq .Name "bls12-381"}} {{- if eq .PointName "g1"}} if !p.IsOnCurve() { @@ -503,25 +500,6 @@ func (p *{{ $TJacobian }}) SubAssign(q *{{ $TJacobian }}) *{{ $TJacobian }} { // https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-mdbl-2007-bl // ~Cost: 1M + 5S func (p *{{ $TJacobian }}) DoubleMixed(a *{{ $TAffine }}) *{{ $TJacobian }} { - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - if a.IsInfinity() || a.Y.IsZero() { - return p.Set(&g1Infinity) - } - var n, d, x, y, tmp {{.CoordType}} - n.Square(&a.X) - tmp.Set(&n) - n.Double(&n).Add(&n, &tmp).Add(&n, &aCurveCoeff) - d.Double(&a.Y).Inverse(&d) - n.Mul(&n, &d) - x.Square(&n) - tmp.Double(&a.X) - x.Sub(&x, &tmp) - y.Sub(&a.X, &x).Mul(&y, &n).Sub(&y, &a.Y) - p.X.Set(&x) - p.Y.Set(&y) - p.Z.SetOne() - return p - {{- else }} var XX, YY, YYYY, S, M, T {{.CoordType}} XX.Square(&a.X) YY.Square(&a.Y) @@ -532,7 +510,10 @@ func (p *{{ $TJacobian }}) DoubleMixed(a *{{ $TAffine }}) *{{ $TJacobian }} { Sub(&S, &YYYY). Double(&S) M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + Add(&M, &XX) // M = 3*XX + {{- if not .IsZeroA}} + M.Add(&M, &aCurveCoeff) // M = 3*XX + a (Z=1 for mixed doubling, so a*Z⁴ = a) + {{- end}} T.Square(&M). Sub(&T, &S). Sub(&T, &S) @@ -546,7 +527,6 @@ func (p *{{ $TJacobian }}) DoubleMixed(a *{{ $TAffine }}) *{{ $TJacobian }} { p.Z.Double(&a.Y) return p - {{- end }} } // AddMixed sets p to p+a in Jacobian coordinates, where a.Z = 1. @@ -611,18 +591,49 @@ func (p *{{ $TJacobian }}) Double(q *{{ $TJacobian }}) *{{ $TJacobian }} { } // DoubleAssign doubles p in Jacobian coordinates. +{{- if .IsNeg3A}} +// +// https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-3.html#doubling-dbl-2001-b +// ~Cost: 3M + 5S (using alpha = 3*(X-Z²)(X+Z²) when a=-3) +func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { + var delta, gamma, beta, alpha, tmp {{.CoordType}} + delta.Square(&p.Z) + gamma.Square(&p.Y) + beta.Mul(&p.X, &gamma) + // alpha = 3*(X+Z²)(X-Z²) = 3*X² - 3*Z⁴ = 3*X² + a*Z⁴ (since a=-3) + alpha.Add(&p.X, &delta) + tmp.Sub(&p.X, &delta) + {{- if eq .CoordType "fp.Element"}} + fp.MulBy3(&tmp) + {{- else}} + { + t := tmp + tmp.Double(&tmp).Add(&tmp, &t) + } + {{- end}} + alpha.Mul(&alpha, &tmp) + p.X.Square(&alpha) + tmp.Double(&beta).Double(&tmp).Double(&tmp) + p.X.Sub(&p.X, &tmp) + p.Z.Add(&p.Y, &p.Z). + Square(&p.Z). + Sub(&p.Z, &gamma). + Sub(&p.Z, &delta) + p.Y.Double(&beta). + Double(&p.Y). + Sub(&p.Y, &p.X). + Mul(&p.Y, &alpha) + tmp.Square(&gamma). + Double(&tmp).Double(&tmp).Double(&tmp) + p.Y.Sub(&p.Y, &tmp) + + return p +} +{{- else}} // // https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l // ~Cost: 2M + 5S func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - if p.Z.IsZero() || p.Y.IsZero() { - return p.Set(&g1Infinity) - } - var a {{ $TAffine }} - a.FromJacobian(p) - return p.DoubleMixed(&a) - {{- else }} var A, B, C, D, E, F, t {{.CoordType}} A.Square(&p.X) B.Square(&p.Y) @@ -633,7 +644,16 @@ func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { Sub(&D, &C). Double(&D) E.Double(&A). - Add(&E, &A) + Add(&E, &A) // E = 3*A = 3*X² + {{- if not .IsZeroA}} + { + var aZ4 {{.CoordType}} + aZ4.Square(&p.Z) + aZ4.Square(&aZ4). + Mul(&aZ4, &aCurveCoeff) + E.Add(&E, &aZ4) // E = 3*X² + a*Z⁴ + } + {{- end}} F.Square(&E) t.Double(&D) p.Z.Mul(&p.Y, &p.Z). @@ -647,18 +667,22 @@ func (p *{{ $TJacobian }}) DoubleAssign() *{{ $TJacobian }} { p.Y.Sub(&p.Y, &t) return p - {{- end }} } -// Triple sets p to [3]q in Jacobian coordinates for j=0 curves. +{{- end}} +// Triple sets p to [3]q in Jacobian coordinates. +{{- if .IsZeroA}} // -// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 +// https://eprint.iacr.org/2024/1906.pdf, Proposition 2.1 (optimized for j=0 curves) +{{- end}} func (p *{{ $TJacobian }}) Triple(q *{{ $TJacobian }}) *{{ $TJacobian }} { - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - var dbl {{ $TJacobian }} - dbl.Double(q) - p.Set(&dbl).AddAssign(q) +{{- if not .IsZeroA}} + // general case: double and add (use temp to handle p == q aliasing) + var tmp {{ $TJacobian }} + tmp.Double(q).AddAssign(q) + p.Set(&tmp) return p - {{- else }} +} +{{- else}} // Helper functions for multiplication by 3 and 4. mulBy3 := func(v *{{.CoordType}}) { {{- if eq .CoordType "fp.Element"}} @@ -721,8 +745,8 @@ func (p *{{ $TJacobian }}) Triple(q *{{ $TJacobian }}) *{{ $TJacobian }} { mulBy3(&p.Z) return p - {{- end }} } +{{- end}} // ScalarMultiplication computes and returns p = [s]a // where p and a are Jacobian points. @@ -798,12 +822,16 @@ func (p *{{ $TJacobian }}) IsOnCurve() bool { left.Square(&p.Y) right.Square(&p.X).Mul(&right, &p.X) ZZ.Square(&p.Z) - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - var Z4 {{.CoordType}} - Z4.Square(&ZZ) - tmp.Mul(&p.X, &Z4).Mul(&tmp, &aCurveCoeff) - right.Add(&right, &tmp) - {{- end }} + {{- if not .IsZeroA}} + { + // add a*X*Z⁴ term + var aXZ4 {{.CoordType}} + aXZ4.Mul(&ZZ, &ZZ). + Mul(&aXZ4, &p.X). + Mul(&aXZ4, &aCurveCoeff) + right.Add(&right, &aXZ4) + } + {{- end}} tmp.Square(&ZZ).Mul(&tmp, &ZZ) {{- if eq .PointName "g1"}} {{- if or (eq .Name "bls12-381") (eq .Name "bls24-317") (eq .Name "bw6-633")}} @@ -839,17 +867,14 @@ func (p *{{ $TJacobian }}) IsOnCurve() bool { -{{- if or (eq .Name "bn254") (eq .Name "secp256k1") (eq .Name "grumpkin") (eq .Name "kb8")}} - {{- if eq .PointName "g1"}} - // IsInSubGroup returns true if p is on the r-torsion, false otherwise. - // the curve is of prime order i.e. E(𝔽p) is the full group - // so we just check that the point is on the curve. - func (p *{{ $TJacobian }}) IsInSubGroup() bool { - - return p.IsOnCurve() - - } - {{else if eq .PointName "g2"}} +{{- if and (not .CofactorCleaning) (eq .PointName "g1")}} + // IsInSubGroup returns true if p is on the r-torsion, false otherwise. + // the curve is of prime order i.e. E(𝔽p) is the full group + // so we just check that the point is on the curve. + func (p *{{ $TJacobian }}) IsInSubGroup() bool { + return p.IsOnCurve() + } +{{- else if and (eq .Name "bn254") (eq .PointName "g2")}} // IsInSubGroup returns true if p is on the r-torsion, false otherwise. // https://eprint.iacr.org/2022/348.pdf, sec. 3 and 5.1 // [r]P == 0 <==> [x₀+1]P + ψ([x₀]P) + ψ²([x₀]P) = ψ³([2x₀]P) @@ -870,7 +895,6 @@ func (p *{{ $TJacobian }}) IsOnCurve() bool { return res.Equal(&c) } - {{- end}} {{else if eq .Name "bw6-761"}} // IsInSubGroup returns true if p is on the r-torsion, false otherwise. {{ if .GLV}} @@ -1150,7 +1174,7 @@ func (p *{{ $TJacobian }}) mulWindowedMixed(q *{{ $TAffine }}, s *big.Int) *{{ $ return p } -{{- if and (ne .Name "secp256k1") (ne .Name "grumpkin")}} +{{- if .HasG2}} // mulBySeed multiplies the point q by the seed xGen in Jacobian coordinates // using an optimized addition chain. func (p *{{ $TJacobian }}) mulBySeed(q *{{ $TJacobian }}) *{{ $TJacobian }} { @@ -1307,7 +1331,7 @@ func (p *{{ $TJacobian }}) mulBySeed(q *{{ $TJacobian }}) *{{ $TJacobian }} { // where w is a third root of unity. func (p *{{ $TJacobian }}) phi(q *{{ $TJacobian }}) *{{ $TJacobian }} { p.Set(q) - {{- if or (eq .CoordType "fptower.E2" ) (eq .CoordType "fptower.E4" ) (eq .CoordType "fptower.E8" )}} + {{- if or (eq .CoordType "fptower.E2" ) (eq .CoordType "fptower.E4" )}} p.X.MulByElement(&p.X, &thirdRootOne{{toUpper .PointName}}) {{- else}} p.X.Mul(&p.X, &thirdRootOne{{toUpper .PointName}}) @@ -1993,33 +2017,42 @@ func (p *{{ $TJacobianExtended }}) add(q *{{ $TJacobianExtended }}) *{{ $TJacobi // double sets p to [2]q in Jacobian extended coordinates. // // http://www.hyperelliptic.org/EFD/g1p/auto-shortw-xyzz.html#doubling-dbl-2008-s-1 -// ~Cost: 6M + 3S // // N.B.: since we consider any point on Z=0 as the point at infinity // this doubling formula works for infinity points as well. func (p *{{ $TJacobianExtended }}) double(q *{{ $TJacobianExtended }}) *{{ $TJacobianExtended }} { - {{- if and (eq .Name "kb8") (eq .PointName "g1") }} - var jq, res {{ $TJacobian }} - jq.fromJacExtended(q) - res.Double(&jq) - if res.Z.IsZero() { - return p.SetInfinity() - } - p.X.Set(&res.X) - p.Y.Set(&res.Y) - p.ZZ.Square(&res.Z) - p.ZZZ.Mul(&p.ZZ, &res.Z) - return p - {{- else }} - var U, V, W, S, XX, M {{.CoordType}} + var U, V, W, S, M {{.CoordType}} U.Double(&q.Y) V.Square(&U) W.Mul(&U, &V) S.Mul(&q.X, &V) - XX.Square(&q.X) - M.Double(&XX). - Add(&M, &XX) // -> + A, but A=0 here + {{- if .IsNeg3A}} + // M = 3*X² + a*ZZ² = 3*(X-ZZ)(X+ZZ) when a=-3 + { + var XZ {{.CoordType}} + M.Sub(&q.X, &q.ZZ) + XZ.Add(&q.X, &q.ZZ). + Mul(&XZ, &M) + M.Double(&XZ). + Add(&M, &XZ) + } + {{- else}} + { + var XX {{.CoordType}} + XX.Square(&q.X) + M.Double(&XX). + Add(&M, &XX) // M = 3*XX + {{- if not .IsZeroA}} + { + var aZZ2 {{.CoordType}} + aZZ2.Square(&q.ZZ). + Mul(&aZZ2, &aCurveCoeff) + M.Add(&M, &aZZ2) // M = 3*XX + a*ZZ² + } + {{- end}} + } + {{- end}} U.Mul(&W, &q.Y) p.X.Square(&M). @@ -2032,7 +2065,6 @@ func (p *{{ $TJacobianExtended }}) double(q *{{ $TJacobianExtended }}) *{{ $TJac p.ZZZ.Mul(&W, &q.ZZZ) return p - {{- end }} } // addMixed sets p to p+q in extended Jacobian coordinates, where a.ZZ=1. @@ -2068,23 +2100,6 @@ func (p *{{ $TJacobianExtended }}) doubleMixed(a *{{ $TAffine }}) *{{ $TJacobian } {{define "mDouble" }} - {{- if and (eq .all.Name "kb8") (eq .all.PointName "g1") }} - var aff G1Affine - aff.Set(a) - {{- if .negate}} - aff.Neg(&aff) - {{- end}} - var res G1Jac - res.FromAffine(&aff).DoubleAssign() - if res.Z.IsZero() { - return p.SetInfinity() - } - p.X.Set(&res.X) - p.Y.Set(&res.Y) - p.ZZ.Square(&res.Z) - p.ZZZ.Mul(&p.ZZ, &res.Z) - return p - {{- else }} var U, V, W, S, M, t {{.all.CoordType}} U.Double(&a.Y) @@ -2096,7 +2111,10 @@ func (p *{{ $TJacobianExtended }}) doubleMixed(a *{{ $TAffine }}) *{{ $TJacobian S.Mul(&a.X, &V) t.Square(&a.X) M.Double(&t). - Add(&M, &t) // -> + A, but A=0 here + Add(&M, &t) // M = 3*X² + {{- if not .all.IsZeroA}} + M.Add(&M, &aCurveCoeff) // M = 3*X² + a (ZZ=1 for mixed doubling) + {{- end}} p.X.Square(&M) t.Double(&S) p.X.Sub(&p.X, &t) @@ -2112,7 +2130,6 @@ func (p *{{ $TJacobianExtended }}) doubleMixed(a *{{ $TAffine }}) *{{ $TJacobian p.ZZZ.Set(&W) return p - {{- end }} {{- end}} {{define "mAdd" }} @@ -2221,12 +2238,7 @@ func (p *{{ $TProjective }}) FromAffine(a *{{ $TAffine }}) *{{ $TProjective }} { func BatchJacobianToAffine{{ toUpper .PointName }}(points []{{ $TJacobian }}) []{{ $TAffine }} { result := make([]{{ $TAffine }}, len(points)) zeroes := make([]bool, len(points)) - {{- if eq .CoordType "fp.Element" }} accumulator := fp.One() - {{- else }} - var accumulator {{ .CoordType }} - accumulator.SetOne() - {{- end }} // batch invert all points[].Z coordinates with Montgomery batch inversion trick // (stores points[].Z^-1 in result[i].X to avoid allocating a slice of fr.Elements) @@ -2239,7 +2251,7 @@ func BatchJacobianToAffine{{ toUpper .PointName }}(points []{{ $TJacobian }}) [] accumulator.Mul(&accumulator, &points[i].Z) } - var accInverse {{ .CoordType }} + var accInverse fp.Element accInverse.Inverse(&accumulator) for i := len(points) - 1; i >= 0; i-- { @@ -2258,7 +2270,7 @@ func BatchJacobianToAffine{{ toUpper .PointName }}(points []{{ $TJacobian }}) [] // do nothing, (X=0, Y=0) is infinity point in affine continue } - var a, b {{ .CoordType }} + var a, b fp.Element a = result[i].X b.Square(&a) result[i].X.Mul(&points[i].X, &b) diff --git a/internal/generator/ecc/template/tests/marshal.go.tmpl b/internal/generator/ecc/template/tests/marshal.go.tmpl index b07de843a8..dc4d80e895 100644 --- a/internal/generator/ecc/template/tests/marshal.go.tmpl +++ b/internal/generator/ecc/template/tests/marshal.go.tmpl @@ -20,9 +20,9 @@ import ( "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fr" "github.com/consensys/gnark-crypto/ecc/{{.Name}}/fp" - {{- if or .HasG2 (eq .G1.CoordType "fptower.E2") (eq .G1.CoordType "fptower.E4") (eq .G1.CoordType "fptower.E8") }} + {{- if .HasG2}} "github.com/consensys/gnark-crypto/ecc/{{.Name}}/internal/fptower" - {{- end }} + {{- end}} ) const ( @@ -39,13 +39,13 @@ func TestEncoder(t *testing.T) { var inC fp.Element var inD G1Affine var inE G1Affine - {{- if .HasG2 }} + {{- if .HasG2}} var inF G2Affine - {{- end }} + {{- end}} var inG []G1Affine - {{- if .HasG2 }} + {{- if .HasG2}} var inH []G2Affine - {{- end }} + {{- end}} var inI []fp.Element var inJ []fr.Element var inK fr.Vector @@ -59,24 +59,16 @@ func TestEncoder(t *testing.T) { inC.MustSetRandom() inD.ScalarMultiplication(&g1GenAff, new(big.Int).SetUint64(rand.Uint64())) //#nosec G404 weak rng is fine here // inE --> infinity - {{- if .HasG2 }} + {{- if .HasG2}} inF.ScalarMultiplication(&g2GenAff, new(big.Int).SetUint64(rand.Uint64())) //#nosec G404 weak rng is fine here - {{- end }} + {{- end}} inG = make([]G1Affine, 2) - {{- if .HasG2 }} + {{- if .HasG2}} inH = make([]G2Affine, 0) - {{- end }} + {{- end}} inG[1] = inD inI = make([]fp.Element, 3) - {{- if eq .G1.CoordType "fp.Element" }} inI[2] = inD.X - {{- else if eq .G1.CoordType "fptower.E2" }} - inI[2] = inD.X.A0 - {{- else if eq .G1.CoordType "fptower.E4" }} - inI[2] = inD.X.B0.A0 - {{- else if eq .G1.CoordType "fptower.E8" }} - inI[2] = inD.X.C0.B0.A0 - {{- end }} inJ = make([]fr.Element, 0) inK = make(fr.Vector, 42) inK[41].SetUint64(42) @@ -96,11 +88,7 @@ func TestEncoder(t *testing.T) { var buf, bufRaw bytes.Buffer enc := NewEncoder(&buf) encRaw := NewEncoder(&bufRaw, RawEncoding()) - {{- if .HasG2 }} - toEncode := []any{inA, &inB, &inC, &inD, &inE, &inF, inG, inH, inI, inJ, inK, inL, inM, inN} - {{- else }} - toEncode := []any{inA, &inB, &inC, &inD, &inE, inG, inI, inJ, inK, inL, inM, inN} - {{- end }} + toEncode := []any{inA, &inB, &inC, &inD, &inE, {{- if .HasG2}} &inF, {{- end}} inG, {{- if .HasG2}} inH, {{- end}} inI, inJ, inK, inL, inM, inN} for _, v := range toEncode { if err := enc.Encode(v); err != nil { t.Fatal(err) @@ -119,18 +107,14 @@ func TestEncoder(t *testing.T) { var outD G1Affine var outE G1Affine outE.X.SetOne() - {{- if eq .G1.CoordType "fp.Element" }} outE.Y.SetUint64(42) - {{- else }} - outE.Y.SetOne() - {{- end }} - {{- if .HasG2 }} + {{- if .HasG2}} var outF G2Affine - {{- end }} + {{- end}} var outG []G1Affine - {{- if .HasG2 }} + {{- if .HasG2}} var outH []G2Affine - {{- end }} + {{- end}} var outI []fp.Element var outJ []fr.Element var outK fr.Vector @@ -138,11 +122,7 @@ func TestEncoder(t *testing.T) { var outM [][]uint64 var outN [][][]fr.Element - {{- if .HasG2 }} - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outF, &outG, &outH, &outI, &outJ, &outK, &outL, &outM, &outN} - {{- else }} - toDecode := []any{&outA, &outB, &outC, &outD, &outE, &outG, &outI, &outJ, &outK, &outL, &outM, &outN} - {{- end }} + toDecode := []any{&outA, &outB, &outC, &outD, &outE, {{- if .HasG2}} &outF, {{- end}} &outG, {{- if .HasG2}} &outH, {{- end}} &outI, &outJ, &outK, &outL, &outM, &outN} for _, v := range toDecode { if err := dec.Decode(v); err != nil { t.Fatal(err) @@ -160,18 +140,14 @@ func TestEncoder(t *testing.T) { if !inD.Equal(&outD) || !inE.Equal(&outE) { t.Fatal("decode(encode(G1Affine) failed") } - {{- if .HasG2 }} + {{- if .HasG2}} if !inF.Equal(&outF) { t.Fatal("decode(encode(G2Affine) failed") } - if (len(inG) != len(outG)) || (len(inH) != len(outH)) { + {{- end}} + if (len(inG) != len(outG)) {{- if .HasG2}} || (len(inH) != len(outH)) {{- end}} { t.Fatal("decode(encode(slice(points))) failed") } - {{- else }} - if len(inG) != len(outG) { - t.Fatal("decode(encode(slice(points))) failed") - } - {{- end }} for i := range len(inG) { if !inG[i].Equal(&outG[i]) { t.Fatal("decode(encode(slice(points))) failed") @@ -211,17 +187,18 @@ func TestEncoder(t *testing.T) { +{{- if ge .FpUnusedBits 2}} func TestIsCompressed(t *testing.T) { t.Parallel() var g1Inf, g1 G1Affine - {{- if .HasG2 }} + {{- if .HasG2}} var g2Inf, g2 G2Affine + {{- end}} g1 = g1GenAff + {{- if .HasG2}} g2 = g2GenAff - {{- else }} - g1 = g1GenAff - {{- end }} + {{- end}} { b := g1Inf.Bytes() @@ -250,7 +227,10 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g1.RawBytes() should be uncompressed") } } - {{- if .HasG2 }} + + + + {{- if .HasG2}} { b := g2Inf.Bytes() if !isCompressed(b[0]) { @@ -278,17 +258,18 @@ func TestIsCompressed(t *testing.T) { t.Fatal("g2.RawBytes() should be uncompressed") } } - {{- end }} + {{- end}} } +{{- end}} -{{- $sizeOfFp := .Fp.NbBytes}} +{{- $sizeOfFp := mul .Fp.NbWords 8}} {{- $FpUnusedBits := .FpUnusedBits}} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G1.CoordType "PointName" .G1.PointName "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "FpUnusedBits" $FpUnusedBits}} -{{- if .HasG2 }} +{{- if .HasG2}} {{template "marshalpoint" dict "all" . "sizeOfFp" $sizeOfFp "CoordType" .G2.CoordType "PointName" .G2.PointName "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "FpUnusedBits" $FpUnusedBits}} -{{- end }} +{{- end}} {{define "marshalpoint"}} @@ -322,6 +303,7 @@ func Test{{ $.TAffine }}Serialization(t *testing.T) { t.Parallel() // test round trip serialization of infinity { + {{- if ge $.FpUnusedBits 2}} // compressed { var p1, p2 {{ $.TAffine }} @@ -339,6 +321,7 @@ func Test{{ $.TAffine }}Serialization(t *testing.T) { t.Fatal("deserialization of uncompressed infinity point is not infinity") } } + {{- end}} // uncompressed { @@ -403,7 +386,7 @@ func Test{{ $.TAffine }}Serialization(t *testing.T) { if err != nil { return false } - if n != SizeOf{{ $.TAffine }}Compressed { + if n != SizeOf{{ $.TAffine }}{{- if ge $.FpUnusedBits 2}}Compressed{{- else}}Uncompressed{{- end}} { return false } return start.X.Equal(&end.X) && start.Y.Equal(&end.Y) @@ -443,8 +426,9 @@ func GenFp() gopter.Gen { // e2 e4 e12 e24 for bls24 // e2 e6 e12 else */}} -{{if or .HasG2 (eq .G1.CoordType "fptower.E2") (eq .G1.CoordType "fptower.E4") (eq .G1.CoordType "fptower.E8")}} -{{if or (eq .Name "bw6-633") (eq .Name "bw6-761")}} +{{if not .HasG2}} +{{/* no extension field generators needed for curves without G2 */}} +{{else if eq .G2.CoordType "fp.Element"}} // GenE3 generates an E3 elmt func GenE3() gopter.Gen { return gopter.CombineGens( @@ -466,40 +450,27 @@ func GenFp() gopter.Gen { }) } {{ else }} - // GenE2 generates an fptower.E2 elmt - func GenE2() gopter.Gen { - return gopter.CombineGens( - GenFp(), - GenFp(), + // GenE2 generates an fptower.E2 elmt + func GenE2() gopter.Gen { + return gopter.CombineGens( + GenFp(), + GenFp(), ).Map(func(values []any) fptower.E2 { return fptower.E2{A0: values[0].(fp.Element), A1: values[1].(fp.Element)} }) } - {{if or (eq .Name "kb8") (eq .Name "bls24-315") (eq .Name "bls24-317")}} + {{if eq .G2.CoordType "fptower.E4"}} // GenE4 generates an fptower.E4 elmt func GenE4() gopter.Gen { return gopter.CombineGens( GenE2(), GenE2(), - ).Map(func(values []any) fptower.E4 { - return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} - }) - } - {{end}} - - {{if eq .Name "kb8"}} - // GenE8 generates an fptower.E8 elmt - func GenE8() gopter.Gen { - return gopter.CombineGens( - GenE4(), - GenE4(), - ).Map(func(values []any) fptower.E8 { - return fptower.E8{C0: values[0].(fptower.E4), C1: values[1].(fptower.E4)} - }) - } + ).Map(func(values []any) fptower.E4 { + return fptower.E4{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2)} + }) + } - {{else if or (eq .Name "bls24-315") (eq .Name "bls24-317")}} // GenE12 generates an fptower.E12 elmt func GenE12() gopter.Gen { return gopter.CombineGens( @@ -521,22 +492,22 @@ func GenFp() gopter.Gen { }) } {{ else }} - // GenE6 generates an fptower.E6 elmt - func GenE6() gopter.Gen { - return gopter.CombineGens( - GenE2(), - GenE2(), - GenE2(), + // GenE6 generates an fptower.E6 elmt + func GenE6() gopter.Gen { + return gopter.CombineGens( + GenE2(), + GenE2(), + GenE2(), ).Map(func(values []any) fptower.E6 { return fptower.E6{B0: values[0].(fptower.E2), B1: values[1].(fptower.E2), B2: values[2].(fptower.E2)} }) } - // GenE12 generates an fptower.E6 elmt - func GenE12() gopter.Gen { - return gopter.CombineGens( - GenE6(), - GenE6(), + // GenE12 generates an fptower.E6 elmt + func GenE12() gopter.Gen { + return gopter.CombineGens( + GenE6(), + GenE6(), ).Map(func(values []any) fptower.E12 { return fptower.E12{C0: values[0].(fptower.E6), C1: values[1].(fptower.E6)} }) @@ -545,6 +516,7 @@ func GenFp() gopter.Gen { {{ end }} + // GenBigInt generates a big.Int func GenBigInt() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { @@ -559,4 +531,3 @@ func GenBigInt() gopter.Gen { return genResult } } -{{ end }} diff --git a/internal/generator/ecc/template/tests/multiexp.go.tmpl b/internal/generator/ecc/template/tests/multiexp.go.tmpl index 341e76a226..665de10ea6 100644 --- a/internal/generator/ecc/template/tests/multiexp.go.tmpl +++ b/internal/generator/ecc/template/tests/multiexp.go.tmpl @@ -23,13 +23,9 @@ import ( ) -{{- if and (ne .Name "secp256k1") (ne .Name "grumpkin")}} -{{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 16}} -{{- if .HasG2 }} -{{template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "cmax" 16}} -{{- end }} -{{- else}} -{{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" 15}} +{{template "multiexp" dict "PointName" .G1.PointName "UPointName" (toUpper .G1.PointName) "TAffine" $G1TAffine "TJacobian" $G1TJacobian "TJacobianExtended" $G1TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G1.CRange "cmax" .G1.CMax}} +{{- if .HasG2}} +{{template "multiexp" dict "PointName" .G2.PointName "UPointName" (toUpper .G2.PointName) "TAffine" $G2TAffine "TJacobian" $G2TJacobian "TJacobianExtended" $G2TJacobianExtended "FrNbWords" .Fr.NbWords "CRange" .G2.CRange "cmax" .G2.CMax}} {{- end}} {{define "multiexp" }} diff --git a/internal/generator/main.go b/internal/generator/main.go index ac4d19ce3d..d7ff7dc8b7 100644 --- a/internal/generator/main.go +++ b/internal/generator/main.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "sync" "time" @@ -87,6 +88,17 @@ func main() { }(conf) } + // clean up previously generated files before regenerating. + // files with the "DO NOT EDIT" header are removed; hand-written files + // (without this header) are preserved. + for _, conf := range config.Curves { + if conf.Equal(config.KB8) { + continue + } + curveDir := filepath.Join(baseDir, "ecc", conf.Name) + cleanGeneratedFiles(curveDir) + } + for _, conf := range config.Curves { wg.Add(1) // for each curve, generate the needed files @@ -126,7 +138,7 @@ func main() { } // fp - if conf.Name != "kb8" { + if !conf.Equal(config.KB8) { outputDir := filepath.Join(curveDir, "fp") relAsmDir, err := filepath.Rel(outputDir, asmDirBuildPath) assertNoError(err) @@ -144,7 +156,7 @@ func main() { asmConfig := &fieldConfig.Assembly{BuildDir: asmDirBuildPath, IncludeDir: relAsmDir} frOpts := []field.Option{field.WithASM(asmConfig)} - if !(conf.Equal(config.SECP256R1) || conf.Equal(config.STARK_CURVE) || conf.Equal(config.SECP256K1) || conf.Equal(config.GRUMPKIN) || conf.Name == "kb8") { // nolint QF1001 + if conf.GenerateFFT() { frOpts = append(frOpts, field.WithFFT(fftConfig), field.WithIOP()) } if conf.Equal(config.BLS12_377) { @@ -154,87 +166,61 @@ func main() { assertNoError(field.GenerateFF(conf.Fr, outputDir, frOpts...)) } - // generate ecdsa - if conf.G1.CoordExtDegree == 1 { - assertNoError(ecdsa.Generate(conf, curveDir, gen)) - } - - if conf.Equal(config.STARK_CURVE) || conf.Equal(config.SECP256R1) { - return // TODO @yelhousni + // preserve the checked-in kb8 ECC package; the shared ECC generator remains + // master-neutral for existing curves, while kb8 keeps its hand-maintained + // field-wrapper, point, marshal, and multiexp files. + if conf.Equal(config.KB8) { + return } - // generate G1, G2, multiExp, ... - assertNoError(ecc.Generate(conf, curveDir, gen)) + // generate ecdsa + assertNoError(ecdsa.Generate(conf, curveDir, gen)) - if conf.Name == "kb8" { - return + // generate G1, G2, multiExp, marshal, ... + if conf.GenerateECC() { + assertNoError(ecc.Generate(conf, curveDir, gen)) } - if conf.Equal(config.SECP256K1) { - return - } + // field suite: mimc, polynomial, poseidon2, hash_to_field + if conf.GenerateFieldSuite() { + frInfo := fieldConfig.FieldDependency{ + FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + conf.Name + "/fr", + FieldPackageName: "fr", + ElementType: "fr.Element", + } - // generate mimc on fr - assertNoError(mimc.Generate(conf, filepath.Join(curveDir, "fr", "mimc"), gen)) + fpInfo := fieldConfig.FieldDependency{ + FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + conf.Name + "/fp", + FieldPackageName: "fp", + ElementType: "fp.Element", + } - // generate polynomial on fr - frInfo := fieldConfig.FieldDependency{ - FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + conf.Name + "/fr", - FieldPackageName: "fr", - ElementType: "fr.Element", + assertNoError(mimc.Generate(conf, filepath.Join(curveDir, "fr", "mimc"), gen)) + assertNoError(polynomial.Generate(frInfo, filepath.Join(curveDir, "fr", "polynomial"), true, gen)) + assertNoError(poseidon2.Generate(conf, filepath.Join(curveDir, "fr", "poseidon2"), gen)) + assertNoError(hash_to_field.Generate(frInfo, filepath.Join(curveDir, "fr", "hash_to_field"), gen)) + assertNoError(hash_to_field.Generate(fpInfo, filepath.Join(curveDir, "fp", "hash_to_field"), gen)) } - assertNoError(polynomial.Generate(frInfo, filepath.Join(curveDir, "fr", "polynomial"), true, gen)) - - // generate poseidon2 on fr - assertNoError(poseidon2.Generate(conf, filepath.Join(curveDir, "fr", "poseidon2"), gen)) - fpInfo := fieldConfig.FieldDependency{ - FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + conf.Name + "/fp", - FieldPackageName: "fp", - ElementType: "fp.Element", + // hash to curve (only if hash suite is configured and ECC is generated) + if conf.GenerateHashToCurve() && conf.GenerateECC() { + assertNoError(hash_to_curve.Generate(conf, curveDir, gen)) } - // generate wrapped hash-to-field for both fr and fp - assertNoError(hash_to_field.Generate(frInfo, filepath.Join(curveDir, "fr", "hash_to_field"), gen)) - assertNoError(hash_to_field.Generate(fpInfo, filepath.Join(curveDir, "fp", "hash_to_field"), gen)) - - // generate hash to curve for both G1 and G2 - assertNoError(hash_to_curve.Generate(conf, curveDir, gen)) - - if conf.Equal(config.GRUMPKIN) { - return + // pairing-dependent packages + if conf.GeneratePairingPackages() { + assertNoError(pedersen.Generate(conf, filepath.Join(curveDir, "fr", "pedersen"), gen)) + assertNoError(tower.Generate(conf, filepath.Join(curveDir, "internal", "fptower"), gen)) + assertNoError(pairing.Generate(conf, curveDir, gen)) + assertNoError(fri.Generate(conf, filepath.Join(curveDir, "fr", "fri"), gen)) + assertNoError(mpcsetup.Generate(conf, filepath.Join(curveDir, "mpcsetup"), gen)) + assertNoError(kzg.Generate(conf, filepath.Join(curveDir, "kzg"), gen)) + assertNoError(shplonk.Generate(conf, filepath.Join(curveDir, "shplonk"), gen)) + assertNoError(fflonk.Generate(conf, filepath.Join(curveDir, "fflonk"), gen)) + assertNoError(plookup.Generate(conf, filepath.Join(curveDir, "fr", "plookup"), gen)) + assertNoError(permutation.Generate(conf, filepath.Join(curveDir, "fr", "permutation"), gen)) } - // generate pedersen on fr - assertNoError(pedersen.Generate(conf, filepath.Join(curveDir, "fr", "pedersen"), gen)) - - // generate tower of extension - assertNoError(tower.Generate(conf, filepath.Join(curveDir, "internal", "fptower"), gen)) - - // generate pairing tests - assertNoError(pairing.Generate(conf, curveDir, gen)) - - // generate fri on fr - assertNoError(fri.Generate(conf, filepath.Join(curveDir, "fr", "fri"), gen)) - - // generate mpc setup tools - assertNoError(mpcsetup.Generate(conf, filepath.Join(curveDir, "mpcsetup"), gen)) - - // generate kzg on fr - assertNoError(kzg.Generate(conf, filepath.Join(curveDir, "kzg"), gen)) - - // generate shplonk on fr - assertNoError(shplonk.Generate(conf, filepath.Join(curveDir, "shplonk"), gen)) - - // generate fflonk on fr - assertNoError(fflonk.Generate(conf, filepath.Join(curveDir, "fflonk"), gen)) - - // generate plookup on fr - assertNoError(plookup.Generate(conf, filepath.Join(curveDir, "fr", "plookup"), gen)) - - // generate permutation on fr - assertNoError(permutation.Generate(conf, filepath.Join(curveDir, "fr", "permutation"), gen)) - }(conf) } @@ -310,3 +296,35 @@ func assertNoError(err error) { os.Exit(-1) } } + +// cleanGeneratedFiles removes all previously generated files (those with the +// "Code generated by consensys/gnark-crypto DO NOT EDIT" header) from dir and +// its subdirectories. This prevents stale generated files from persisting when +// the generation logic changes. +func cleanGeneratedFiles(dir string) { + const generatedHeader = "Code generated by consensys/gnark-crypto DO NOT EDIT" + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + if isGeneratedFile(path, generatedHeader) { + os.Remove(path) + } + return nil + }) +} + +func isGeneratedFile(path string, header string) bool { + ext := filepath.Ext(path) + if ext != ".go" && ext != ".s" { + return false + } + f, err := os.Open(path) + if err != nil { + return false + } + buf := make([]byte, 4096) + n, _ := f.Read(buf) + f.Close() + return n > 0 && strings.Contains(string(buf[:n]), header) +} From 64184278ddee5b2e78f30a85aef229539e537f09 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 22 Apr 2026 15:15:58 -0400 Subject: [PATCH 15/33] fix: Cbrt(0)=0 --- ecc/kb8/fr/element.go | 3 +++ internal/generator/field/template/element/cbrt.go.tmpl | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/ecc/kb8/fr/element.go b/ecc/kb8/fr/element.go index 4202285917..358e3b0bc5 100644 --- a/ecc/kb8/fr/element.go +++ b/ecc/kb8/fr/element.go @@ -1392,6 +1392,9 @@ func (z *Element) Cbrt(x *Element) *Element { for i := uint64(0); i < r-1; i++ { check.Cube(&check) } + if check.IsZero() { + return z.SetZero() + } if !check.IsOne() { // x is not a cubic residue return nil diff --git a/internal/generator/field/template/element/cbrt.go.tmpl b/internal/generator/field/template/element/cbrt.go.tmpl index 22271e21f8..8d9d85f11d 100644 --- a/internal/generator/field/template/element/cbrt.go.tmpl +++ b/internal/generator/field/template/element/cbrt.go.tmpl @@ -249,6 +249,9 @@ func (z *{{.ElementName}}) Cbrt(x *{{.ElementName}}) *{{.ElementName}} { // For e=2, check if x is a cubic residue: t^3 should equal 1 var check {{.ElementName}} check.Cube(&t) + if check.IsZero() { + return z.SetZero() + } if !check.IsOne() { return nil } @@ -325,6 +328,9 @@ func (z *{{.ElementName}}) Cbrt(x *{{.ElementName}}) *{{.ElementName}} { for i := uint64(0); i < r-1; i++ { check.Cube(&check) } + if check.IsZero() { + return z.SetZero() + } if !check.IsOne() { // x is not a cubic residue return nil From a985438800ee6713f41f882b54413f0de4a30c18 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 5 May 2026 13:11:05 -0400 Subject: [PATCH 16/33] refactor: precomputed constants in e16 cbrt --- ecc/kb8/multiset-hash/cardano.go | 73 ++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index f364fdc337..fbac97a45c 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -23,6 +23,14 @@ var ( e8Four extensions.E8 e8TwentySeven extensions.E8 e8NegThree extensions.E8 + + // Precomputed constants used by cardanoRoots. Computed once at init. + e8Inv2 extensions.E8 // 1/2 + e8Inv4 extensions.E8 // 1/4 + e8A3 extensions.E8 // (-3)^3 = -27 + e8Neg4A3 extensions.E8 // -4·(-3)^3 = 108 + e8NegHalf extensions.E8 // -1/2 + e8NegOne extensions.E8 // -1, equals (-3)^3 / 27 = a3/27 ) var e16LucasExponent = [4]uint64{ @@ -43,6 +51,13 @@ func init() { e8Beta.C1.SetOne() e8BetaInv.Inverse(&e8Beta) e8Omega = findPrimitiveCubeRoot() + + e8Inv2.Inverse(&e8Two) + e8Inv4.Inverse(&e8Four) + e8A3.Square(&e8NegThree).Mul(&e8A3, &e8NegThree) // -27 + e8Neg4A3.Mul(&e8A3, &e8Four).Neg(&e8Neg4A3) // 108 + e8NegHalf.Set(&e8Inv2).Neg(&e8NegHalf) // -1/2 + e8NegOne.Set(&e8One).Neg(&e8NegOne) // -1 = a3/27 } func depressedCubicRoot(c extensions.E8) (extensions.E8, bool) { @@ -57,24 +72,19 @@ func depressedCubicRoot(c extensions.E8) (extensions.E8, bool) { } func cardanoRoots(c extensions.E8) []extensions.E8 { - var a3, neg4a3, k27c2, delta extensions.E8 - a3.Square(&e8NegThree).Mul(&a3, &e8NegThree) - neg4a3.Mul(&a3, &e8Four).Neg(&neg4a3) + // delta = -4·a³ - 27·c² where a = -3, so -4·a³ = 108. + var k27c2, delta extensions.E8 k27c2.Square(&c).Mul(&k27c2, &e8TwentySeven) - delta.Sub(&neg4a3, &k27c2) + delta.Sub(&e8Neg4A3, &k27c2) - var inv2, inv4, inv27 extensions.E8 - inv2.Inverse(&e8Two) - inv4.Inverse(&e8Four) - inv27.Inverse(&e8TwentySeven) - - var discD, a3Over27 extensions.E8 - discD.Square(&c).Mul(&discD, &inv4) - a3Over27.Mul(&a3, &inv27) - discD.Add(&discD, &a3Over27) + // discD = c²/4 + a³/27 = c²/4 - 1 + var discD extensions.E8 + discD.Square(&c).Mul(&discD, &e8Inv4) + discD.Sub(&discD, &e8One) + // negCHalf = -c/2 = c · (-1/2) var negCHalf extensions.E8 - negCHalf.Mul(&c, &inv2).Neg(&negCHalf) + negCHalf.Mul(&c, &e8NegHalf) if delta.IsZero() { return repeatedRoots(c) @@ -109,12 +119,20 @@ func cardanoRootsBaseField(negCHalf, discD extensions.E8) []extensions.E8 { return nil } - var omega2 extensions.E8 - omega2.Square(&e8Omega) - - var invU, r0, r1, r2, t1, t2 extensions.E8 + // cbrtE8 already adjusts u via cbrtVerifyAndAdjustE8 to the principal cube + // root, so r0 = u + 1/u is the depressed-cubic root in the typical case. + // Compute and check r0 first; fall back to the ω-rotated forms only if r0 + // fails the verification. + var invU, r0 extensions.E8 invU.Inverse(&u) r0.Add(&u, &invU) + if isDepressedCubicRoot(&r0, &negCHalf) { + return []extensions.E8{r0} + } + + var omega2 extensions.E8 + omega2.Square(&e8Omega) + var r1, r2, t1, t2 extensions.E8 t1.Mul(&e8Omega, &u) t2.Mul(&omega2, &invU) r1.Add(&t1, &t2) @@ -122,12 +140,12 @@ func cardanoRootsBaseField(negCHalf, discD extensions.E8) []extensions.E8 { t2.Mul(&e8Omega, &invU) r2.Add(&t1, &t2) - return filterValidRoots(negCHalf, []extensions.E8{r0, r1, r2}) + return filterValidRoots(negCHalf, []extensions.E8{r1, r2}) } func cardanoRootsViaQuadraticExtension(negCHalf, discD extensions.E8) []extensions.E8 { var discOverBeta, sqrtDiscOverBeta extensions.E8 - discOverBeta.Div(&discD, &e8Beta) + discOverBeta.Mul(&discD, &e8BetaInv) if discOverBeta.Legendre() != 1 { return nil } @@ -143,11 +161,20 @@ func cardanoRootsViaQuadraticExtension(negCHalf, discD extensions.E8) []extensio return nil } + // Try zeta = 1 first (cube root canonicalized inside Cbrt usually picks + // this branch). Fall through to the rotated cases as backstops. + { + var inv, sum e16 + inv.Inverse(&u) + sum.Add(&u, &inv) + if sum.A1.IsZero() && isDepressedCubicRoot(&sum.A0, &negCHalf) { + return []extensions.E8{sum.A0} + } + } + var omega2 extensions.E8 omega2.Square(&e8Omega) - zetas := [3]extensions.E8{e8One, e8Omega, omega2} - - for _, zeta := range zetas { + for _, zeta := range [2]extensions.E8{e8Omega, omega2} { var cand, inv, sum e16 cand.MulByE8(&u, &zeta) inv.Inverse(&cand) From 96e5013d740fdba719237195f492b1e7b35be76c Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Tue, 5 May 2026 13:33:49 -0400 Subject: [PATCH 17/33] refactor: clean code and remove dead code --- ecc/kb8/multiset-hash/cardano.go | 521 ++----------------------------- 1 file changed, 25 insertions(+), 496 deletions(-) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/kb8/multiset-hash/cardano.go index fbac97a45c..f4eddc1d1d 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/kb8/multiset-hash/cardano.go @@ -23,14 +23,7 @@ var ( e8Four extensions.E8 e8TwentySeven extensions.E8 e8NegThree extensions.E8 - - // Precomputed constants used by cardanoRoots. Computed once at init. - e8Inv2 extensions.E8 // 1/2 - e8Inv4 extensions.E8 // 1/4 - e8A3 extensions.E8 // (-3)^3 = -27 - e8Neg4A3 extensions.E8 // -4·(-3)^3 = 108 - e8NegHalf extensions.E8 // -1/2 - e8NegOne extensions.E8 // -1, equals (-3)^3 / 27 = a3/27 + e8Neg4A3 extensions.E8 // 108 = -4·(-3)^3 ) var e16LucasExponent = [4]uint64{ @@ -52,12 +45,9 @@ func init() { e8BetaInv.Inverse(&e8Beta) e8Omega = findPrimitiveCubeRoot() - e8Inv2.Inverse(&e8Two) - e8Inv4.Inverse(&e8Four) - e8A3.Square(&e8NegThree).Mul(&e8A3, &e8NegThree) // -27 - e8Neg4A3.Mul(&e8A3, &e8Four).Neg(&e8Neg4A3) // 108 - e8NegHalf.Set(&e8Inv2).Neg(&e8NegHalf) // -1/2 - e8NegOne.Set(&e8One).Neg(&e8NegOne) // -1 = a3/27 + var a3 extensions.E8 + a3.Square(&e8NegThree).Mul(&a3, &e8NegThree) // (-3)^3 = -27 + e8Neg4A3.Mul(&a3, &e8Four).Neg(&e8Neg4A3) } func depressedCubicRoot(c extensions.E8) (extensions.E8, bool) { @@ -72,19 +62,27 @@ func depressedCubicRoot(c extensions.E8) (extensions.E8, bool) { } func cardanoRoots(c extensions.E8) []extensions.E8 { - // delta = -4·a³ - 27·c² where a = -3, so -4·a³ = 108. - var k27c2, delta extensions.E8 - k27c2.Square(&c).Mul(&k27c2, &e8TwentySeven) - delta.Sub(&e8Neg4A3, &k27c2) + // For x³ - 3x + c = 0: + // delta = -4a³ - 27c² = 108 - 27c² + // discD = c²/4 + a³/27 = c²/4 - 1 + // negCHalf = -c/2 + var c2 extensions.E8 + c2.Square(&c) + + var delta extensions.E8 + delta.Mul(&c2, &e8TwentySeven) + delta.Sub(&e8Neg4A3, &delta) - // discD = c²/4 + a³/27 = c²/4 - 1 var discD extensions.E8 - discD.Square(&c).Mul(&discD, &e8Inv4) + discD.Set(&c2) + discD.Halve() + discD.Halve() discD.Sub(&discD, &e8One) - // negCHalf = -c/2 = c · (-1/2) var negCHalf extensions.E8 - negCHalf.Mul(&c, &e8NegHalf) + negCHalf.Set(&c) + negCHalf.Halve() + negCHalf.Neg(&negCHalf) if delta.IsZero() { return repeatedRoots(c) @@ -119,10 +117,9 @@ func cardanoRootsBaseField(negCHalf, discD extensions.E8) []extensions.E8 { return nil } - // cbrtE8 already adjusts u via cbrtVerifyAndAdjustE8 to the principal cube - // root, so r0 = u + 1/u is the depressed-cubic root in the typical case. - // Compute and check r0 first; fall back to the ω-rotated forms only if r0 - // fails the verification. + // extensions.E8.Cbrt returns the principal cube root, so r0 = u + 1/u is + // the depressed-cubic root in the typical case. Fall back to the ω-rotated + // forms only if r0 fails the verification. var invU, r0 extensions.E8 invU.Inverse(&u) r0.Add(&u, &invU) @@ -161,8 +158,8 @@ func cardanoRootsViaQuadraticExtension(negCHalf, discD extensions.E8) []extensio return nil } - // Try zeta = 1 first (cube root canonicalized inside Cbrt usually picks - // this branch). Fall through to the rotated cases as backstops. + // e16.Cbrt canonicalizes its result, so zeta = 1 (i.e. cand = u) is the + // typical match. Try it first. { var inv, sum e16 inv.Inverse(&u) @@ -225,424 +222,6 @@ func cbrtE8(z, x *extensions.E8) *extensions.E8 { return z.Cbrt(x) } -func cbrtVerifyAndAdjustE8(z, x *extensions.E8) *extensions.E8 { - var check, omega2, y extensions.E8 - check.Square(z).Mul(&check, z) - if check.Equal(x) { - return z - } - - y.Mul(z, &e8Omega) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - omega2.Square(&e8Omega) - y.Mul(z, &omega2) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - return nil -} - -func expByKBE8Cbrt(z, x *extensions.E8) *extensions.E8 { - // expByKBCbrt computation is derived from the addition chain: - // - // _10 = 2*1 - // _100 = 2*_10 - // _110 = _10 + _100 - // _1000 = _10 + _110 - // _1010 = _10 + _1000 - // _1011 = 1 + _1010 - // _1100 = 1 + _1011 - // _10110 = _1010 + _1100 - // _11100 = _110 + _10110 - // _11110 = _10 + _11100 - // _11111 = 1 + _11110 - // _101011 = _1100 + _11111 - // _1000111 = _11100 + _101011 - // _1001011 = _100 + _1000111 - // _1010011 = _1000 + _1001011 - // _1010101 = _10 + _1010011 - // _1011001 = _100 + _1010101 - // _1110111 = _11110 + _1011001 - // _1111001 = _10 + _1110111 - // _10001111 = _10110 + _1111001 - // _10010101 = _110 + _10001111 - // _10011101 = _1000 + _10010101 - // _10100101 = _1000 + _10011101 - // _10101111 = _1010 + _10100101 - // _10110111 = _1000 + _10101111 - // _11000011 = _1100 + _10110111 - // _11001011 = _1000 + _11000011 - // _11001101 = _10 + _11001011 - // _11001111 = _10 + _11001101 - // _11010101 = _110 + _11001111 - // _11011101 = _1000 + _11010101 - // i49 = ((_11001101 + _11011101) << 7 + _10110111) << 8 + _1011001 - // i80 = ((i49 << 2 + 1) << 16 + _10011101) << 11 - // i100 = ((_1001011 + i80) << 9 + _1000111) << 8 + _1010011 - // i128 = ((i100 << 11 + _11001011) << 9 + _11001111) << 6 - // i154 = ((_11111 + i128) << 15 + _10100101) << 8 + _10010101 - // i182 = ((i154 << 9 + _10101111) << 8 + _1111001) << 9 - // i200 = ((_10010101 + i182) << 8 + _11011101) << 7 + _1110111 - // i228 = ((i200 << 9 + _11001101) << 8 + _11010101) << 9 - // i251 = ((_11000011 + i228) << 8 + _101011) << 12 + _11011101 - // i273 = ((_110 + i251) << 8 + _10001111) << 11 + _11010101 - // i296 = ((i273 << 8 + _1010101) << 8 + _1010101) << 5 - // return _1011 + i296 - // - // Operations: 239 squares 58 multiplies - // - // Generated by github.com/mmcloughlin/addchain v0.4.0. - - var ( - t0 extensions.E8 - t1 extensions.E8 - t2 extensions.E8 - t3 extensions.E8 - t4 extensions.E8 - t5 extensions.E8 - t6 extensions.E8 - t7 extensions.E8 - t8 extensions.E8 - t9 extensions.E8 - t10 extensions.E8 - t11 extensions.E8 - t12 extensions.E8 - t13 extensions.E8 - t14 extensions.E8 - t15 extensions.E8 - t16 extensions.E8 - t17 extensions.E8 - t18 extensions.E8 - t19 extensions.E8 - t20 extensions.E8 - t21 extensions.E8 - t22 extensions.E8 - t23 extensions.E8 - t24 extensions.E8 - ) - - t0.Square(x) - t1.Square(&t0) - t2.Mul(&t0, &t1) - t3.Mul(&t0, &t2) - t4.Mul(&t0, &t3) - t5.Mul(x, &t4) - t6.Mul(x, &t5) - t7.Mul(&t4, &t6) - t8.Mul(&t2, &t7) - t9.Mul(&t0, &t8) - t10.Mul(x, &t9) - t11.Mul(&t6, &t10) - t8.Mul(&t8, &t11) - t12.Mul(&t1, &t8) - t13.Mul(&t3, &t12) - t14.Mul(&t0, &t13) - t1.Mul(&t1, &t14) - t9.Mul(&t9, &t1) - t15.Mul(&t0, &t9) - t7.Mul(&t7, &t15) - t16.Mul(&t2, &t7) - t17.Mul(&t3, &t16) - t18.Mul(&t3, &t17) - t4.Mul(&t4, &t18) - t19.Mul(&t3, &t4) - t6.Mul(&t6, &t19) - t20.Mul(&t3, &t6) - t21.Mul(&t0, &t20) - t0.Mul(&t0, &t21) - t22.Mul(&t2, &t0) - t3.Mul(&t3, &t22) - t23.Mul(&t21, &t3) - for s := 0; s < 7; s++ { - t23.Square(&t23) - } - t19.Mul(&t19, &t23) - for s := 0; s < 8; s++ { - t19.Square(&t19) - } - t1.Mul(&t1, &t19) - for s := 0; s < 2; s++ { - t1.Square(&t1) - } - t24.Mul(x, &t1) - for s := 0; s < 16; s++ { - t24.Square(&t24) - } - t17.Mul(&t17, &t24) - for s := 0; s < 11; s++ { - t17.Square(&t17) - } - t12.Mul(&t12, &t17) - for s := 0; s < 9; s++ { - t12.Square(&t12) - } - t8.Mul(&t8, &t12) - for s := 0; s < 8; s++ { - t8.Square(&t8) - } - t13.Mul(&t13, &t8) - for s := 0; s < 11; s++ { - t13.Square(&t13) - } - t20.Mul(&t20, &t13) - for s := 0; s < 9; s++ { - t20.Square(&t20) - } - t0.Mul(&t0, &t20) - for s := 0; s < 6; s++ { - t0.Square(&t0) - } - t10.Mul(&t10, &t0) - for s := 0; s < 15; s++ { - t10.Square(&t10) - } - t18.Mul(&t18, &t10) - for s := 0; s < 8; s++ { - t18.Square(&t18) - } - t18.Mul(&t16, &t18) - for s := 0; s < 9; s++ { - t18.Square(&t18) - } - t4.Mul(&t4, &t18) - for s := 0; s < 8; s++ { - t4.Square(&t4) - } - t15.Mul(&t15, &t4) - for s := 0; s < 9; s++ { - t15.Square(&t15) - } - t16.Mul(&t16, &t15) - for s := 0; s < 8; s++ { - t16.Square(&t16) - } - t16.Mul(&t3, &t16) - for s := 0; s < 7; s++ { - t16.Square(&t16) - } - t9.Mul(&t9, &t16) - for s := 0; s < 9; s++ { - t9.Square(&t9) - } - t21.Mul(&t21, &t9) - for s := 0; s < 8; s++ { - t21.Square(&t21) - } - t21.Mul(&t22, &t21) - for s := 0; s < 9; s++ { - t21.Square(&t21) - } - t6.Mul(&t6, &t21) - for s := 0; s < 8; s++ { - t6.Square(&t6) - } - t11.Mul(&t11, &t6) - for s := 0; s < 12; s++ { - t11.Square(&t11) - } - t3.Mul(&t3, &t11) - t2.Mul(&t2, &t3) - for s := 0; s < 8; s++ { - t2.Square(&t2) - } - t7.Mul(&t7, &t2) - for s := 0; s < 11; s++ { - t7.Square(&t7) - } - t22.Mul(&t22, &t7) - for s := 0; s < 8; s++ { - t22.Square(&t22) - } - t22.Mul(&t14, &t22) - for s := 0; s < 8; s++ { - t22.Square(&t22) - } - t14.Mul(&t14, &t22) - for s := 0; s < 5; s++ { - t14.Square(&t14) - } - z.Mul(&t5, &t14) - - return z -} - -func expByKBE8Helper(z, x *extensions.E8) *extensions.E8 { - var ( - t0 extensions.E8 - t1 extensions.E8 - t2 extensions.E8 - t3 extensions.E8 - t4 extensions.E8 - t5 extensions.E8 - t6 extensions.E8 - t7 extensions.E8 - t8 extensions.E8 - t9 extensions.E8 - t10 extensions.E8 - t11 extensions.E8 - t12 extensions.E8 - t13 extensions.E8 - t14 extensions.E8 - t15 extensions.E8 - t16 extensions.E8 - t17 extensions.E8 - t18 extensions.E8 - t19 extensions.E8 - t20 extensions.E8 - t21 extensions.E8 - t22 extensions.E8 - ) - - t1.Square(x) - t9.Square(&t1) - z.Mul(x, &t9) - t3.Mul(x, z) - t4.Mul(&t1, &t3) - t7.Mul(&t1, &t4) - t6.Mul(&t1, &t7) - t2.Mul(&t7, &t6) - t0.Mul(&t3, &t2) - t8.Mul(&t1, &t0) - t13.Mul(x, &t8) - t5.Mul(&t6, &t13) - t17.Mul(&t0, &t5) - t18.Mul(&t9, &t17) - t16.Mul(&t4, &t18) - t0.Mul(&t1, &t16) - t20.Mul(&t9, &t0) - t8.Mul(&t8, &t20) - t10.Mul(&t1, &t8) - t2.Mul(&t2, &t10) - t9.Mul(&t3, &t2) - t19.Mul(&t4, &t9) - t12.Mul(&t4, &t19) - t11.Mul(&t7, &t12) - t21.Mul(&t4, &t11) - t6.Mul(&t6, &t21) - t15.Mul(&t4, &t6) - t7.Mul(&t1, &t15) - t14.Mul(&t1, &t7) - t1.Mul(&t3, &t14) - t4.Mul(&t4, &t1) - t22.Mul(&t7, &t4) - for range 7 { - t22.Square(&t22) - } - t21.Mul(&t21, &t22) - for range 8 { - t21.Square(&t21) - } - t20.Mul(&t20, &t21) - for range 2 { - t20.Square(&t20) - } - t20.Mul(x, &t20) - for range 16 { - t20.Square(&t20) - } - t19.Mul(&t19, &t20) - for range 11 { - t19.Square(&t19) - } - t18.Mul(&t18, &t19) - for range 9 { - t18.Square(&t18) - } - t17.Mul(&t17, &t18) - for range 8 { - t17.Square(&t17) - } - t16.Mul(&t16, &t17) - for range 11 { - t16.Square(&t16) - } - t15.Mul(&t15, &t16) - for range 9 { - t15.Square(&t15) - } - t14.Mul(&t14, &t15) - for range 6 { - t14.Square(&t14) - } - t13.Mul(&t13, &t14) - for range 15 { - t13.Square(&t13) - } - t12.Mul(&t12, &t13) - for range 8 { - t12.Square(&t12) - } - t12.Mul(&t9, &t12) - for range 9 { - t12.Square(&t12) - } - t11.Mul(&t11, &t12) - for range 8 { - t11.Square(&t11) - } - t10.Mul(&t10, &t11) - for range 9 { - t10.Square(&t10) - } - t9.Mul(&t9, &t10) - for range 8 { - t9.Square(&t9) - } - t9.Mul(&t4, &t9) - for range 7 { - t9.Square(&t9) - } - t8.Mul(&t8, &t9) - for range 9 { - t8.Square(&t8) - } - t7.Mul(&t7, &t8) - for range 8 { - t7.Square(&t7) - } - t7.Mul(&t1, &t7) - for range 9 { - t7.Square(&t7) - } - t6.Mul(&t6, &t7) - for range 8 { - t6.Square(&t6) - } - t5.Mul(&t5, &t6) - for range 12 { - t5.Square(&t5) - } - t4.Mul(&t4, &t5) - t3.Mul(&t3, &t4) - for range 8 { - t3.Square(&t3) - } - t2.Mul(&t2, &t3) - for range 11 { - t2.Square(&t2) - } - t1.Mul(&t1, &t2) - for range 8 { - t1.Square(&t1) - } - t1.Mul(&t0, &t1) - for range 8 { - t1.Square(&t1) - } - t0.Mul(&t0, &t1) - for range 4 { - t0.Square(&t0) - } - z.Mul(z, &t0) - return z -} - func lucasV2E8(alpha *extensions.E8) (extensions.E8, extensions.E8) { var v0, v1, two extensions.E8 two.C0.B0.A0.SetUint64(2) @@ -1305,53 +884,3 @@ func (z *e16) Cbrt(x *e16) *e16 { y.A1.Add(&t1, &t2).Mul(&y.A1, &mInv) return cbrtVerifyAndAdjustE16(z.Set(&y), x) } - -func cbrtAndNormInverseE16(norm, x0sq, x1sq *extensions.E8) (m, normInv, deltaInv extensions.E8, ok bool) { - var x0x1, betaX0x1, U, U2, U3, w extensions.E8 - x0x1.Mul(x0sq, x1sq) - betaX0x1.Mul(&x0x1, &e8Beta) - U.Mul(&betaX0x1, norm) - U.Double(&U).Double(&U) - U.Double(&U).Double(&U) - U2.Square(&U) - U3.Mul(&U2, &U) - w.Mul(&U3, norm) - - var t, t2, t4, t5, cbrtW, cw2, wInv extensions.E8 - expByKBE8Helper(&t, &w) - t2.Square(&t) - t4.Square(&t2) - t5.Mul(&t4, &t) - cbrtW.Mul(&w, &t2) - cw2.Square(&cbrtW) - wInv.Mul(&t5, &cw2) - - var UInv, check extensions.E8 - UInv.Mul(&U2, norm).Mul(&UInv, &wInv) - m.Mul(&cbrtW, &UInv) - normInv.Mul(&U3, &wInv) - check.Square(&m).Mul(&check, &m) - if !check.Equal(norm) { - var alt extensions.E8 - alt.Mul(&m, &e8Omega) - check.Square(&alt).Mul(&check, &alt) - if check.Equal(norm) { - m.Set(&alt) - } else { - var omega2 extensions.E8 - omega2.Square(&e8Omega) - alt.Mul(&m, &omega2) - check.Square(&alt).Mul(&check, &alt) - if !check.Equal(norm) { - return m, normInv, deltaInv, false - } - m.Set(&alt) - } - } - - var norm2, norm3 extensions.E8 - norm2.Square(norm) - norm3.Mul(&norm2, norm) - deltaInv.Mul(&norm3, &UInv) - return m, normInv, deltaInv, true -} From 569c9ef59c1317060b68aaa10f1f3863959f1699 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 7 May 2026 14:30:51 -0400 Subject: [PATCH 18/33] refactor: apply review suggestions --- ecc/kb8/fp/fp.go | 31 ++-- ecc/kb8/fr/cbrt_extra_test.go | 13 ++ ecc/kb8/fr/element.go | 144 +++++++++++++++++- ecc/kb8/multiset-hash/cardano_test.go | 13 ++ field/koalabear/extensions/e8_test.go | 6 + internal/generator/config/curve.go | 21 +++ internal/generator/config/kb8.go | 6 + internal/generator/fieldwrapper/generate.go | 19 +++ .../fieldwrapper/template/fp.go.tmpl | 40 +++++ .../fieldwrapper/template/templates.go | 6 + internal/generator/main.go | 19 +-- 11 files changed, 289 insertions(+), 29 deletions(-) create mode 100644 ecc/kb8/fr/cbrt_extra_test.go create mode 100644 internal/generator/fieldwrapper/generate.go create mode 100644 internal/generator/fieldwrapper/template/fp.go.tmpl create mode 100644 internal/generator/fieldwrapper/template/templates.go diff --git a/ecc/kb8/fp/fp.go b/ecc/kb8/fp/fp.go index d07d58d74d..cc13bc693d 100644 --- a/ecc/kb8/fp/fp.go +++ b/ecc/kb8/fp/fp.go @@ -1,42 +1,47 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + package fp import ( "math/big" - kb "github.com/consensys/gnark-crypto/field/koalabear" + koalabear "github.com/consensys/gnark-crypto/field/koalabear" ) -type Element = kb.Element -type Vector = kb.Vector +type Element = koalabear.Element +type Vector = koalabear.Vector const ( - Bits = kb.Bits - Bytes = kb.Bytes - Limbs = kb.Limbs + Bits = koalabear.Bits + Bytes = koalabear.Bytes + Limbs = koalabear.Limbs ) -var BigEndian = kb.BigEndian +var BigEndian = koalabear.BigEndian func Modulus() *big.Int { - return kb.Modulus() + return koalabear.Modulus() } func One() Element { - return kb.One() + return koalabear.One() } func BatchInvert(a []Element) []Element { - return kb.BatchInvert(a) + return koalabear.BatchInvert(a) } func Generator(m uint64) (Element, error) { - return kb.Generator(m) + return koalabear.Generator(m) } func Butterfly(a, b *Element) { - kb.Butterfly(a, b) + koalabear.Butterfly(a, b) } func MulBy3(x *Element) { - kb.MulBy3(x) + koalabear.MulBy3(x) } diff --git a/ecc/kb8/fr/cbrt_extra_test.go b/ecc/kb8/fr/cbrt_extra_test.go new file mode 100644 index 0000000000..57f1e024a9 --- /dev/null +++ b/ecc/kb8/fr/cbrt_extra_test.go @@ -0,0 +1,13 @@ +package fr + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestElementCbrtZero(t *testing.T) { + var zero, got Element + require.NotNil(t, got.Cbrt(&zero)) + require.True(t, got.IsZero()) +} diff --git a/ecc/kb8/fr/element.go b/ecc/kb8/fr/element.go index 358e3b0bc5..2db8cc2d15 100644 --- a/ecc/kb8/fr/element.go +++ b/ecc/kb8/fr/element.go @@ -745,8 +745,8 @@ func Hash(msg, dst []byte, count int) ([]Element, error) { // Exp z = xᵏ (mod q) func (z *Element) Exp(x Element, k *big.Int) *Element { - if k.IsUint64() && k.Uint64() == 0 { - return z.SetOne() + if k.IsUint64() { + return z.expUint64(x, k.Uint64()) } e := k @@ -761,14 +761,144 @@ func (z *Element) Exp(x Element, k *big.Int) *Element { defer pool.BigInt.Put(e) e.Neg(k) } + return z.expWindowed(x, e) +} - z.Set(&x) +// getBitUint extracts bit at position pos from a little-endian word slice. +func getBitUint(words []big.Word, pos int) uint { + return uint(words[pos/bits.UintSize]>>(uint(pos)%bits.UintSize)) & 1 +} - for i := e.BitLen() - 2; i >= 0; i-- { - z.Square(z) - if e.Bit(i) == 1 { - z.Mul(z, &x) +// getWindowUint extracts a window of windowSize bits starting at position pos (MSB) +// down to pos-windowSize+1 (LSB) from a little-endian word slice. +// windowSize must be between 1 and bits.UintSize. +func getWindowUint(words []big.Word, pos, windowSize int) uint { + low := pos - windowSize + 1 + wIdx := low / bits.UintSize + bIdx := uint(low) % bits.UintSize + + // extract from one word + win := uint(words[wIdx] >> bIdx) + + // if the window spans two words, include bits from the next word + if bIdx+uint(windowSize) > uint(bits.UintSize) { + win |= uint(words[wIdx+1]) << (uint(bits.UintSize) - bIdx) + } + + return win & ((1 << windowSize) - 1) +} + +// expWindowed computes z = xᵏ (mod q) using a 4-bit sliding window method. +// It accesses the exponent via big.Int.Bits() for direct word-level access. +func (z *Element) expWindowed(x Element, k *big.Int) *Element { + el := k.BitLen() + if el == 0 { + return z.SetOne() + } + if el == 1 { + z.Set(&x) + return z + } + + // precompute table: table[i] = x^(2i+1) for i = 0..7 + // i.e., odd powers x^1, x^3, x^5, ..., x^15 + const w = 4 // window size + var table [1 << (w - 1)]Element + var x2 Element + table[0].Set(&x) + x2.Square(&x) + for i := 1; i < len(table); i++ { + table[i].Mul(&table[i-1], &x2) + } + + words := k.Bits() + z.SetOne() + + for i := el - 1; i >= 0; { + if getBitUint(words, i) == 0 { + z.Square(z) + i-- + continue + } + // collect up to w bits starting from position i (MSB), ending at a 1-bit + windowSize := w + if i+1 < windowSize { + windowSize = i + 1 + } + winVal := getWindowUint(words, i, windowSize) + + // trim trailing zeros to get an odd lookup value + trailingZeros := bits.TrailingZeros(winVal) + winVal >>= trailingZeros + effectiveSize := windowSize - trailingZeros + + for j := 0; j < effectiveSize; j++ { + z.Square(z) + } + z.Mul(z, &table[(winVal-1)>>1]) + for j := 0; j < trailingZeros; j++ { + z.Square(z) + } + i -= windowSize + } + + return z +} + +// expUint64 computes z = xᵏ (mod q) for a uint64 exponent. +// Uses binary method for small exponents and 4-bit windowed method for larger ones. +func (z *Element) expUint64(x Element, k uint64) *Element { + if k == 0 { + return z.SetOne() + } + el := bits.Len64(k) + if el <= 8 { + // small exponent: binary method avoids precompute overhead + z.Set(&x) + for i := el - 2; i >= 0; i-- { + z.Square(z) + if (k>>i)&1 == 1 { + z.Mul(z, &x) + } + } + return z + } + + const w = 4 + var table [1 << (w - 1)]Element + var x2 Element + table[0].Set(&x) + x2.Square(&x) + for i := 1; i < len(table); i++ { + table[i].Mul(&table[i-1], &x2) + } + + z.SetOne() + + for i := el - 1; i >= 0; { + if (k>>i)&1 == 0 { + z.Square(z) + i-- + continue + } + windowSize := w + if i+1 < windowSize { + windowSize = i + 1 + } + winVal := uint((k >> (i - windowSize + 1)) & ((1 << windowSize) - 1)) + + trailingZeros := bits.TrailingZeros(winVal) + winVal >>= trailingZeros + effectiveSize := windowSize - trailingZeros + + for j := 0; j < effectiveSize; j++ { + z.Square(z) + } + z.Mul(z, &table[(winVal-1)>>1]) + for j := 0; j < trailingZeros; j++ { + z.Square(z) } + i -= windowSize } return z diff --git a/ecc/kb8/multiset-hash/cardano_test.go b/ecc/kb8/multiset-hash/cardano_test.go index 6ad75fc5d2..9222a777f8 100644 --- a/ecc/kb8/multiset-hash/cardano_test.go +++ b/ecc/kb8/multiset-hash/cardano_test.go @@ -29,6 +29,12 @@ func TestCbrtE8RejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E8 non-cube in 256 samples") } +func TestCbrtE8Zero(t *testing.T) { + var zero, got extensions.E8 + require.NotNil(t, cbrtE8(&got, &zero)) + require.True(t, got.IsZero()) +} + func TestE16CbrtOnCubicResidues(t *testing.T) { for i := 0; i < 128; i++ { var a, x, got, check e16 @@ -54,6 +60,13 @@ func TestE16CbrtRejectsNonResidues(t *testing.T) { t.Fatal("failed to find an E16 non-cube in 256 samples") } +func TestE16CbrtZero(t *testing.T) { + var zero, got e16 + require.NotNil(t, got.Cbrt(&zero)) + require.True(t, got.A0.IsZero()) + require.True(t, got.A1.IsZero()) +} + func TestDepressedCubicRootFindsValidRoot(t *testing.T) { for i := 0; i < 64; i++ { var x, x3, c, lhs extensions.E8 diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go index 5a94367d09..a547157479 100644 --- a/field/koalabear/extensions/e8_test.go +++ b/field/koalabear/extensions/e8_test.go @@ -175,6 +175,12 @@ func genE8() gopter.Gen { }) } +func TestE8CbrtZero(t *testing.T) { + var zero, got E8 + require.NotNil(t, got.Cbrt(&zero)) + require.True(t, got.IsZero()) +} + func TestE8CbrtOnCubicResidues(t *testing.T) { for i := 0; i < 128; i++ { var a, x, got, check E8 diff --git a/internal/generator/config/curve.go b/internal/generator/config/curve.go index e9d77c087f..aed534ee54 100644 --- a/internal/generator/config/curve.go +++ b/internal/generator/config/curve.go @@ -14,6 +14,7 @@ type Curve struct { EnumID string FpModulus string FrModulus string + ExistingFp ExistingFieldPackage Fp *config.Field Fr *config.Field @@ -35,6 +36,9 @@ type Curve struct { // Used for curves that only need field arithmetic and ECDSA (e.g., stark-curve, secp256r1). NoECC bool + // NoECDSA disables ECDSA package generation. + NoECDSA bool + // ECDSAKeyRecovery enables ECDSA public key recovery (SignForRecover, RecoverPublicKey). ECDSAKeyRecovery bool @@ -55,6 +59,11 @@ type Curve struct { E2CbrtTorusLucasTopBit int // bit length - 1 } +type ExistingFieldPackage struct { + PackagePath string + PackageName string +} + type TwistedEdwardsCurve struct { Name string Package string @@ -102,6 +111,18 @@ func (c Curve) GenerateECC() bool { return c.HasG1() && !c.NoECC } +func (c Curve) GenerateECDSA() bool { + return !c.NoECDSA +} + +func (c Curve) GenerateFp() bool { + return c.ExistingFp.PackagePath == "" +} + +func (c Curve) GenerateFpWrapper() bool { + return c.ExistingFp.PackagePath != "" +} + func (c Curve) GenerateFieldSuite() bool { return !c.NoFieldSuite } diff --git a/internal/generator/config/kb8.go b/internal/generator/config/kb8.go index 9301bef100..0b62e32665 100644 --- a/internal/generator/config/kb8.go +++ b/internal/generator/config/kb8.go @@ -7,6 +7,12 @@ var KB8 = Curve{ FpModulus: "2130706433", FrModulus: "424804331891979973455971894938199991839487883914575852667663156896715214921", NoFieldSuite: true, + NoECC: true, + NoECDSA: true, + ExistingFp: ExistingFieldPackage{ + PackagePath: "github.com/consensys/gnark-crypto/field/koalabear", + PackageName: "koalabear", + }, G1: Point{ CoordType: "fptower.E8", CoordExtDegree: 8, diff --git a/internal/generator/fieldwrapper/generate.go b/internal/generator/fieldwrapper/generate.go new file mode 100644 index 0000000000..8ceb6b87c9 --- /dev/null +++ b/internal/generator/fieldwrapper/generate.go @@ -0,0 +1,19 @@ +package fieldwrapper + +import ( + "path/filepath" + + "github.com/consensys/bavard" + "github.com/consensys/gnark-crypto/internal/generator/common" + "github.com/consensys/gnark-crypto/internal/generator/config" + "github.com/consensys/gnark-crypto/internal/generator/fieldwrapper/template" +) + +func Generate(conf config.Curve, baseDir string) error { + entry := bavard.Entry{ + File: filepath.Join(baseDir, "fp.go"), + Templates: []string{"fp.go.tmpl"}, + } + gen := common.NewDefaultGenerator(template.FS) + return gen.Generate(conf, "fp", "", "", entry) +} diff --git a/internal/generator/fieldwrapper/template/fp.go.tmpl b/internal/generator/fieldwrapper/template/fp.go.tmpl new file mode 100644 index 0000000000..c58167a697 --- /dev/null +++ b/internal/generator/fieldwrapper/template/fp.go.tmpl @@ -0,0 +1,40 @@ +import ( + "math/big" + + {{ .ExistingFp.PackageName }} "{{ .ExistingFp.PackagePath }}" +) + +type Element = {{ .ExistingFp.PackageName }}.Element +type Vector = {{ .ExistingFp.PackageName }}.Vector + +const ( + Bits = {{ .ExistingFp.PackageName }}.Bits + Bytes = {{ .ExistingFp.PackageName }}.Bytes + Limbs = {{ .ExistingFp.PackageName }}.Limbs +) + +var BigEndian = {{ .ExistingFp.PackageName }}.BigEndian + +func Modulus() *big.Int { + return {{ .ExistingFp.PackageName }}.Modulus() +} + +func One() Element { + return {{ .ExistingFp.PackageName }}.One() +} + +func BatchInvert(a []Element) []Element { + return {{ .ExistingFp.PackageName }}.BatchInvert(a) +} + +func Generator(m uint64) (Element, error) { + return {{ .ExistingFp.PackageName }}.Generator(m) +} + +func Butterfly(a, b *Element) { + {{ .ExistingFp.PackageName }}.Butterfly(a, b) +} + +func MulBy3(x *Element) { + {{ .ExistingFp.PackageName }}.MulBy3(x) +} diff --git a/internal/generator/fieldwrapper/template/templates.go b/internal/generator/fieldwrapper/template/templates.go new file mode 100644 index 0000000000..2e03795152 --- /dev/null +++ b/internal/generator/fieldwrapper/template/templates.go @@ -0,0 +1,6 @@ +package template + +import "embed" + +//go:embed *.tmpl +var FS embed.FS diff --git a/internal/generator/main.go b/internal/generator/main.go index d7ff7dc8b7..eee4ca4db7 100644 --- a/internal/generator/main.go +++ b/internal/generator/main.go @@ -24,6 +24,7 @@ import ( "github.com/consensys/gnark-crypto/internal/generator/fflonk" "github.com/consensys/gnark-crypto/internal/generator/field" fieldConfig "github.com/consensys/gnark-crypto/internal/generator/field/config" + "github.com/consensys/gnark-crypto/internal/generator/fieldwrapper" "github.com/consensys/gnark-crypto/internal/generator/fri" "github.com/consensys/gnark-crypto/internal/generator/hash_to_curve" "github.com/consensys/gnark-crypto/internal/generator/hash_to_field" @@ -93,6 +94,9 @@ func main() { // (without this header) are preserved. for _, conf := range config.Curves { if conf.Equal(config.KB8) { + curveDir := filepath.Join(baseDir, "ecc", conf.Name) + cleanGeneratedFiles(filepath.Join(curveDir, "fp")) + cleanGeneratedFiles(filepath.Join(curveDir, "fr")) continue } curveDir := filepath.Join(baseDir, "ecc", conf.Name) @@ -138,7 +142,9 @@ func main() { } // fp - if !conf.Equal(config.KB8) { + if conf.GenerateFpWrapper() { + assertNoError(fieldwrapper.Generate(conf, filepath.Join(curveDir, "fp"))) + } else if conf.GenerateFp() { outputDir := filepath.Join(curveDir, "fp") relAsmDir, err := filepath.Rel(outputDir, asmDirBuildPath) assertNoError(err) @@ -166,15 +172,10 @@ func main() { assertNoError(field.GenerateFF(conf.Fr, outputDir, frOpts...)) } - // preserve the checked-in kb8 ECC package; the shared ECC generator remains - // master-neutral for existing curves, while kb8 keeps its hand-maintained - // field-wrapper, point, marshal, and multiexp files. - if conf.Equal(config.KB8) { - return - } - // generate ecdsa - assertNoError(ecdsa.Generate(conf, curveDir, gen)) + if conf.GenerateECDSA() { + assertNoError(ecdsa.Generate(conf, curveDir, gen)) + } // generate G1, G2, multiExp, marshal, ... if conf.GenerateECC() { From 8cad06c9dd3bf14bd7aa853bd2a261af8a5a782f Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 18 May 2026 16:05:07 -0400 Subject: [PATCH 19/33] feat: add vector/pq multiset hash --- ecc/bn254/multiexp_glv.go | 1076 +++++++++++++++++ ecc/bn254/multiexp_glv_test.go | 176 +++ ecc/kb8/multiset-hash/doc.go | 29 +- .../vector_multiset_hash_linear.go | 140 +++ .../vector_multiset_hash_poseidon2.go | 179 +++ .../vector_multiset_hash_test.go | 483 ++++++++ 6 files changed, 2078 insertions(+), 5 deletions(-) create mode 100644 ecc/bn254/multiexp_glv.go create mode 100644 ecc/bn254/multiexp_glv_test.go create mode 100644 ecc/kb8/multiset-hash/vector_multiset_hash_linear.go create mode 100644 ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go create mode 100644 ecc/kb8/multiset-hash/vector_multiset_hash_test.go diff --git a/ecc/bn254/multiexp_glv.go b/ecc/bn254/multiexp_glv.go new file mode 100644 index 0000000000..2f0496d4ba --- /dev/null +++ b/ecc/bn254/multiexp_glv.go @@ -0,0 +1,1076 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bn254 + +import ( + "errors" + "math/bits" + "runtime" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bn254/fp" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/parallel" +) + +const ( + glvMSMG1Window = 16 + glvMSMG1Chunks = 8 + + glvMSMG1WideWindow = 19 + glvMSMG1WideChunks = 7 + glvMSMG1WideStripeBits = 12 + glvMSMG1WideStripeSize = 1 << glvMSMG1WideStripeBits + glvMSMG1WideStripeCount = 1 << (glvMSMG1WideWindow - 1 - glvMSMG1WideStripeBits) + glvMSMG1WideBatchSize = 640 +) + +type glvMSMG1Scalar struct { + k0 [glvMSMG1Chunks]int16 + k1 [glvMSMG1Chunks]int16 + extra0 int8 + extra1 int8 +} + +type glvMSMG1WideScalar struct { + k0 [glvMSMG1WideChunks]int32 + k1 [glvMSMG1WideChunks]int32 + extra0 int8 + extra1 int8 +} + +type glvMSMG1WideOp struct { + index uint32 + bucket uint32 + neg bool +} + +type batchOpG1AffineWide struct { + bucketID uint32 + point G1Affine +} + +var ( + glvMSMG1B1 = [6]uint64{ + 0x96ce4aece61f0339, + 0x2e3ff027efccd68a, + 0x8fa7d32d2fafba64, + 0x6eb9c714773a6ef2, + 0xd91d232ec7e0b3d7, + 0x0000000000000002, + } + glvMSMG1B2Abs = [7]uint64{ + 0xd073ced5f11aeea9, + 0x7abf2e6fc85f00fa, + 0x869375169b9bdffa, + 0xa5e38cfb5eaa26d9, + 0x7a7bd9d4391eb18d, + 0x4ccef014a773d2cf, + 0x0000000000000002, + } + glvMSMG1V11Abs = [2]uint64{0x8211bbeb7d4f1128, 0x6f4d8248eeb859fc} + glvMSMG1V20 = [2]uint64{0x0be4e1541221250b, 0x6f4d8248eeb859fd} +) + +const glvMSMG1V10 = uint64(0x89d3256894d213e3) + +// MultiExpGLV computes sum(scalars[i] * points[i]) using the BN254 G1 GLV +// endomorphism. It is an experimental opt-in MSM variant for large MSMs. +func (p *G1Affine) MultiExpGLV(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { + var _p G1Jac + if _, err := _p.MultiExpGLV(points, scalars, config); err != nil { + return nil, err + } + p.FromJacobian(&_p) + return p, nil +} + +// MultiExpGLV computes sum(scalars[i] * points[i]) using a deferred-phi GLV +// Pippenger variant. +// +// Each scalar is decomposed as k0 + lambda*k1. For each window we build two +// bucket tables over the original input points and apply phi only after the k1 +// bucket table has been reduced: +// +// sum k0_i*P_i + phi(sum k1_i*P_i) +// +// This avoids materializing phi(P_i) for every input point. +func (p *G1Jac) MultiExpGLV(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { + nbPoints := len(points) + if nbPoints != len(scalars) { + return nil, errors.New("len(points) != len(scalars)") + } + if config.NbTasks <= 0 { + config.NbTasks = runtime.NumCPU() * 2 + } else if config.NbTasks > 1024 { + return nil, errors.New("invalid config: config.NbTasks > 1024") + } + + digits := decomposeScalarsGLVG1(scalars, config.NbTasks) + return innerMSMGLVG1(p, points, digits), nil +} + +// MultiExpGLVWide computes sum(scalars[i] * points[i]) using the deferred-phi +// GLV MSM with 19-bit windows and a striped bucket backend. +func (p *G1Affine) MultiExpGLVWide(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { + var _p G1Jac + if _, err := _p.MultiExpGLVWide(points, scalars, config); err != nil { + return nil, err + } + p.FromJacobian(&_p) + return p, nil +} + +// MultiExpGLVWide computes sum(scalars[i] * points[i]) using 19-bit GLV +// component windows. The larger window reduces bucket insertions to 14N for +// BN254-sized scalars, while stripes avoid full 2^18 bucket-table allocation. +func (p *G1Jac) MultiExpGLVWide(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { + nbPoints := len(points) + if nbPoints != len(scalars) { + return nil, errors.New("len(points) != len(scalars)") + } + if nbPoints > 1<<32-1 { + return nil, errors.New("invalid input: len(points) > math.MaxUint32") + } + if config.NbTasks <= 0 { + config.NbTasks = runtime.NumCPU() * 2 + } else if config.NbTasks > 1024 { + return nil, errors.New("invalid config: config.NbTasks > 1024") + } + + digits := decomposeScalarsGLVG1Wide(scalars, config.NbTasks) + return innerMSMGLVG1Wide(p, points, digits), nil +} + +// MultiExpGLVWideDense computes sum(scalars[i] * points[i]) with the 19-bit +// deferred-phi GLV MSM and full dynamic bucket tables. It is intended for +// benchmarking the algorithmic tradeoff against the lower-memory striped path. +func (p *G1Affine) MultiExpGLVWideDense(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { + var _p G1Jac + if _, err := _p.MultiExpGLVWideDense(points, scalars, config); err != nil { + return nil, err + } + p.FromJacobian(&_p) + return p, nil +} + +// MultiExpGLVWideDense computes sum(scalars[i] * points[i]) with the 19-bit +// deferred-phi GLV MSM and full dynamic bucket tables. +func (p *G1Jac) MultiExpGLVWideDense(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { + nbPoints := len(points) + if nbPoints != len(scalars) { + return nil, errors.New("len(points) != len(scalars)") + } + if config.NbTasks <= 0 { + config.NbTasks = runtime.NumCPU() * 2 + } else if config.NbTasks > 1024 { + return nil, errors.New("invalid config: config.NbTasks > 1024") + } + + digits := decomposeScalarsGLVG1Wide(scalars, config.NbTasks) + return innerMSMGLVG1WideDense(p, points, digits), nil +} + +func decomposeScalarsGLVG1(scalars []fr.Element, nbTasks int) []glvMSMG1Scalar { + digits := make([]glvMSMG1Scalar, len(scalars)) + + parallel.Execute(len(scalars), func(start, end int) { + for i := start; i < end; i++ { + if scalars[i].IsZero() { + continue + } + + splitScalarGLVG1Fixed(scalars[i].Bits(), &digits[i]) + } + }, nbTasks) + + return digits +} + +func decomposeScalarsGLVG1Wide(scalars []fr.Element, nbTasks int) []glvMSMG1WideScalar { + digits := make([]glvMSMG1WideScalar, len(scalars)) + + parallel.Execute(len(scalars), func(start, end int) { + for i := start; i < end; i++ { + if scalars[i].IsZero() { + continue + } + + splitScalarGLVG1FixedWide(scalars[i].Bits(), &digits[i]) + } + }, nbTasks) + + return digits +} + +func splitScalarGLVG1Fixed(s [4]uint64, out *glvMSMG1Scalar) { + sign0, mag0, sign1, mag1 := splitScalarGLVG1FixedComponents(s) + recodeSignedGLVComponentG1(sign0, mag0, &out.k0, &out.extra0) + recodeSignedGLVComponentG1(sign1, mag1, &out.k1, &out.extra1) +} + +func splitScalarGLVG1FixedWide(s [4]uint64, out *glvMSMG1WideScalar) { + sign0, mag0, sign1, mag1 := splitScalarGLVG1FixedComponents(s) + recodeSignedGLVComponentG1Wide(sign0, mag0, &out.k0, &out.extra0) + recodeSignedGLVComponentG1Wide(sign1, mag1, &out.k1, &out.extra1) +} + +func splitScalarGLVG1FixedComponents(s [4]uint64) (int, [4]uint64, int, [4]uint64) { + c1 := mul4x6High512(s, glvMSMG1B1) + c2 := mul4x7High512(s, glvMSMG1B2Abs) + + var v0 [4]uint64 + add4(&v0, mul1x1(c1, glvMSMG1V10)) + add4(&v0, mul2x2(c2, glvMSMG1V20)) + + sign0, mag0 := subSigned4(s, v0) + + a := mul1x2(c1, glvMSMG1V11Abs) + b := mul2x1(c2, glvMSMG1V10) + sign1, mag1 := subSigned4(a, b) + + return sign0, mag0, sign1, mag1 +} + +func recodeSignedGLVComponentG1(sign int, mag [4]uint64, digits *[glvMSMG1Chunks]int16, extra *int8) { + const ( + base = int64(1 << glvMSMG1Window) + minDigit = -int64(1 << (glvMSMG1Window - 1)) + maxDigit = int64(1<<(glvMSMG1Window-1)) - 1 + ) + + if sign == 0 { + return + } + + var carry int64 + for i := range glvMSMG1Chunks { + di := int64(window16U256(mag, i)) + if sign < 0 { + di = -di + } + di += carry + carry = 0 + + if di > maxDigit { + di -= base + carry = 1 + } else if di < minDigit { + di += base + carry = -1 + } + + digits[i] = int16(di) + } + + *extra = int8(carry) +} + +func recodeSignedGLVComponentG1Wide(sign int, mag [4]uint64, digits *[glvMSMG1WideChunks]int32, extra *int8) { + const ( + base = int64(1 << glvMSMG1WideWindow) + minDigit = -int64(1 << (glvMSMG1WideWindow - 1)) + maxDigit = int64(1<<(glvMSMG1WideWindow-1)) - 1 + ) + + if sign == 0 { + return + } + + var carry int64 + for i := range glvMSMG1WideChunks { + di := int64(windowU256(mag, i*glvMSMG1WideWindow, glvMSMG1WideWindow)) + if sign < 0 { + di = -di + } + di += carry + carry = 0 + + if di > maxDigit { + di -= base + carry = 1 + } else if di < minDigit { + di += base + carry = -1 + } + + digits[i] = int32(di) + } + + *extra = int8(carry) +} + +func window16U256(words [4]uint64, chunk int) uint64 { + const mask = uint64(1<> (uint(chunk%4) * glvMSMG1Window)) & mask +} + +func windowU256(words [4]uint64, bitOffset, width int) uint64 { + mask := uint64(1<> shift + if shift != 0 && wordIndex+1 < len(words) && int(shift)+width > 64 { + value |= words[wordIndex+1] << (64 - shift) + } + return value & mask +} + +func mul4x6High512(a [4]uint64, b [6]uint64) uint64 { + var product [10]uint64 + for i := range a { + for j := range b { + addMul64(product[:], i+j, a[i], b[j]) + } + } + return product[8] +} + +func mul4x7High512(a [4]uint64, b [7]uint64) [2]uint64 { + var product [11]uint64 + for i := range a { + for j := range b { + addMul64(product[:], i+j, a[i], b[j]) + } + } + return [2]uint64{product[8], product[9]} +} + +func mul1x1(a, b uint64) (out [4]uint64) { + out[1], out[0] = bits.Mul64(a, b) + return out +} + +func mul1x2(a uint64, b [2]uint64) (out [4]uint64) { + hi, lo := bits.Mul64(a, b[0]) + out[0] = lo + carry := hi + hi, lo = bits.Mul64(a, b[1]) + out[1], carry = bits.Add64(lo, carry, 0) + out[2], _ = bits.Add64(hi, 0, carry) + return out +} + +func mul2x1(a [2]uint64, b uint64) (out [4]uint64) { + hi, lo := bits.Mul64(a[0], b) + out[0] = lo + carry := hi + hi, lo = bits.Mul64(a[1], b) + out[1], carry = bits.Add64(lo, carry, 0) + out[2], _ = bits.Add64(hi, 0, carry) + return out +} + +func mul2x2(a, b [2]uint64) (out [4]uint64) { + for i := range a { + for j := range b { + addMul64(out[:], i+j, a[i], b[j]) + } + } + return out +} + +func addMul64(out []uint64, index int, a, b uint64) { + hi, lo := bits.Mul64(a, b) + + var carry uint64 + out[index], carry = bits.Add64(out[index], lo, 0) + hi, hiCarry := bits.Add64(hi, 0, carry) + out[index+1], carry = bits.Add64(out[index+1], hi, 0) + carry += hiCarry + + for i := index + 2; carry != 0 && i < len(out); i++ { + out[i], carry = bits.Add64(out[i], 0, carry) + } +} + +func add4(z *[4]uint64, x [4]uint64) { + var carry uint64 + for i := range 4 { + z[i], carry = bits.Add64(z[i], x[i], carry) + } +} + +func subSigned4(a, b [4]uint64) (int, [4]uint64) { + switch cmp4(a, b) { + case 1: + return 1, sub4(a, b) + case -1: + return -1, sub4(b, a) + default: + return 0, [4]uint64{} + } +} + +func cmp4(a, b [4]uint64) int { + for i := 3; i >= 0; i-- { + if a[i] > b[i] { + return 1 + } + if a[i] < b[i] { + return -1 + } + } + return 0 +} + +func sub4(a, b [4]uint64) (out [4]uint64) { + var borrow uint64 + for i := range 4 { + out[i], borrow = bits.Sub64(a[i], b[i], borrow) + } + return out +} + +func innerMSMGLVG1(p *G1Jac, points []G1Affine, digits []glvMSMG1Scalar) *G1Jac { + chunks := make([]chan g1JacExtended, glvMSMG1Chunks+1) + for i := range chunks { + chunks[i] = make(chan g1JacExtended, 1) + } + + for chunk := range glvMSMG1Chunks { + go processChunkG1GLVBatchAffineC16(chunk, chunks[chunk], points, digits) + } + go processExtraChunkG1GLV(chunks[glvMSMG1Chunks], points, digits) + + return msmReduceChunkG1Affine(p, glvMSMG1Window, chunks) +} + +func innerMSMGLVG1Wide(p *G1Jac, points []G1Affine, digits []glvMSMG1WideScalar) *G1Jac { + chunks := make([]chan g1JacExtended, glvMSMG1WideChunks+1) + for i := range chunks { + chunks[i] = make(chan g1JacExtended, 1) + } + + for chunk := range glvMSMG1WideChunks { + go processChunkG1GLVWideStriped(chunk, chunks[chunk], points, digits) + } + go processExtraChunkG1GLVWide(chunks[glvMSMG1WideChunks], points, digits) + + return msmReduceChunkG1Affine(p, glvMSMG1WideWindow, chunks) +} + +func innerMSMGLVG1WideDense(p *G1Jac, points []G1Affine, digits []glvMSMG1WideScalar) *G1Jac { + chunks := make([]chan g1JacExtended, glvMSMG1WideChunks+1) + for i := range chunks { + chunks[i] = make(chan g1JacExtended, 1) + } + + for chunk := range glvMSMG1WideChunks { + go processChunkG1GLVWideDense(chunk, chunks[chunk], points, digits) + } + go processExtraChunkG1GLVWide(chunks[glvMSMG1WideChunks], points, digits) + + return msmReduceChunkG1Affine(p, glvMSMG1WideWindow, chunks) +} + +type g1GLVBatchAffineC16 struct { + buckets bucketG1AffineC16 + bucketsJE bucketg1JacExtendedC16 + bucketIds bitSetC16 + cptAdd int + R ppG1AffineC16 + P pG1AffineC16 + queue qG1AffineC16 + qID int +} + +func (ctx *g1GLVBatchAffineC16) init() { + for i := range len(ctx.bucketsJE) { + ctx.bucketsJE[i].SetInfinity() + } +} + +func (ctx *g1GLVBatchAffineC16) executeAndReset() { + if ctx.cptAdd == 0 { + return + } + batchAddG1Affine[pG1AffineC16, ppG1AffineC16, cG1AffineC16](&ctx.R, &ctx.P, ctx.cptAdd) + ctx.bucketIds = bitSetC16{} + ctx.cptAdd = 0 +} + +func (ctx *g1GLVBatchAffineC16) addFromQueue(op batchOpG1Affine) { + BK := &ctx.buckets[op.bucketID] + if BK.IsInfinity() { + BK.Set(&op.point) + return + } + if BK.X.Equal(&op.point.X) { + if BK.Y.Equal(&op.point.Y) { + ctx.bucketsJE[op.bucketID].addMixed(&op.point) + return + } + BK.SetInfinity() + return + } + + ctx.bucketIds[op.bucketID] = true + ctx.R[ctx.cptAdd] = BK + ctx.P[ctx.cptAdd] = op.point + ctx.cptAdd++ +} + +func (ctx *g1GLVBatchAffineC16) add(bucketID uint16, point *G1Affine, isAdd bool) { + BK := &ctx.buckets[bucketID] + if BK.IsInfinity() { + if isAdd { + BK.Set(point) + } else { + BK.Neg(point) + } + return + } + if BK.X.Equal(&point.X) { + if BK.Y.Equal(&point.Y) { + if isAdd { + ctx.bucketsJE[bucketID].addMixed(point) + } else { + BK.SetInfinity() + } + return + } + if isAdd { + BK.SetInfinity() + } else { + ctx.bucketsJE[bucketID].subMixed(point) + } + return + } + + ctx.bucketIds[bucketID] = true + ctx.R[ctx.cptAdd] = BK + if isAdd { + ctx.P[ctx.cptAdd].Set(point) + } else { + ctx.P[ctx.cptAdd].Neg(point) + } + ctx.cptAdd++ +} + +func (ctx *g1GLVBatchAffineC16) flushQueue() { + for i := range ctx.qID { + ctx.bucketsJE[ctx.queue[i].bucketID].addMixed(&ctx.queue[i].point) + } + ctx.qID = 0 +} + +func (ctx *g1GLVBatchAffineC16) processTopQueue() { + for i := ctx.qID - 1; i >= 0; i-- { + if ctx.bucketIds[ctx.queue[i].bucketID] { + return + } + ctx.addFromQueue(ctx.queue[i]) + ctx.qID-- + } +} + +func (ctx *g1GLVBatchAffineC16) addDigit(digit int16, point *G1Affine) { + if digit == 0 || point.IsInfinity() { + return + } + + d := int32(digit) + isAdd := d > 0 + if !isAdd { + d = -d + } + bucketID := uint16(d - 1) + + if ctx.bucketIds[bucketID] { + ctx.queue[ctx.qID].bucketID = bucketID + if isAdd { + ctx.queue[ctx.qID].point.Set(point) + } else { + ctx.queue[ctx.qID].point.Neg(point) + } + ctx.qID++ + if ctx.qID == len(ctx.queue)-1 { + ctx.flushQueue() + } + return + } + + ctx.add(bucketID, point, isAdd) + if ctx.cptAdd == len(ctx.P) { + ctx.executeAndReset() + ctx.processTopQueue() + } +} + +func (ctx *g1GLVBatchAffineC16) reduce(total *g1JacExtended) { + ctx.executeAndReset() + ctx.flushQueue() + + var runningSum g1JacExtended + runningSum.SetInfinity() + total.SetInfinity() + for k := len(ctx.buckets) - 1; k >= 0; k-- { + runningSum.addMixed(&ctx.buckets[k]) + if !ctx.bucketsJE[k].IsInfinity() { + runningSum.add(&ctx.bucketsJE[k]) + } + total.add(&runningSum) + } +} + +func processChunkG1GLVBatchAffineC16(chunk int, chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1Scalar) { + var bucket0, bucket1 g1GLVBatchAffineC16 + bucket0.init() + bucket1.init() + + for i := range points { + bucket0.addDigit(digits[i].k0[chunk], &points[i]) + bucket1.addDigit(digits[i].k1[chunk], &points[i]) + } + + var total0, total1 g1JacExtended + bucket0.reduce(&total0) + bucket1.reduce(&total1) + phiG1JacExtended(&total1) + total0.add(&total1) + chRes <- total0 +} + +func processExtraChunkG1GLV(chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1Scalar) { + var total0, total1 g1JacExtended + total0.SetInfinity() + total1.SetInfinity() + + for i := range points { + switch digits[i].extra0 { + case 1: + total0.addMixed(&points[i]) + case -1: + total0.subMixed(&points[i]) + } + switch digits[i].extra1 { + case 1: + total1.addMixed(&points[i]) + case -1: + total1.subMixed(&points[i]) + } + } + + phiG1JacExtended(&total1) + total0.add(&total1) + chRes <- total0 +} + +func phiG1JacExtended(p *g1JacExtended) { + if p.IsInfinity() { + return + } + p.X.Mul(&p.X, &thirdRootOneG1) +} + +type g1GLVStripeBatchAffine struct { + buckets []G1Affine + bucketsJE []g1JacExtended + bucketStamps []uint32 + stamp uint32 + touched []uint32 + touchedMarks []bool + cptAdd int + R []*G1Affine + P []G1Affine + queue []batchOpG1AffineWide + qID int + lambda []fp.Element + lambdain []fp.Element +} + +func newG1GLVBatchAffineDynamic(size int) g1GLVStripeBatchAffine { + return g1GLVStripeBatchAffine{ + buckets: make([]G1Affine, size), + bucketsJE: make([]g1JacExtended, size), + bucketStamps: make([]uint32, size), + stamp: 1, + touched: make([]uint32, 0, min(size, glvMSMG1WideStripeSize)), + touchedMarks: make([]bool, size), + R: make([]*G1Affine, glvMSMG1WideBatchSize), + P: make([]G1Affine, glvMSMG1WideBatchSize), + queue: make([]batchOpG1AffineWide, glvMSMG1WideBatchSize), + lambda: make([]fp.Element, glvMSMG1WideBatchSize), + lambdain: make([]fp.Element, glvMSMG1WideBatchSize), + } +} + +func newG1GLVStripeBatchAffine() g1GLVStripeBatchAffine { + return newG1GLVBatchAffineDynamic(glvMSMG1WideStripeSize) +} + +func (ctx *g1GLVStripeBatchAffine) resetBatch() { + ctx.stamp++ + if ctx.stamp == 0 { + clear(ctx.bucketStamps) + ctx.stamp = 1 + } + ctx.cptAdd = 0 +} + +func (ctx *g1GLVStripeBatchAffine) touch(bucketID uint32) { + if !ctx.touchedMarks[bucketID] { + ctx.touchedMarks[bucketID] = true + ctx.touched = append(ctx.touched, bucketID) + } +} + +func (ctx *g1GLVStripeBatchAffine) resetStripe() { + for _, bucketID := range ctx.touched { + ctx.buckets[bucketID].SetInfinity() + ctx.bucketsJE[bucketID] = g1JacExtended{} + ctx.touchedMarks[bucketID] = false + } + ctx.touched = ctx.touched[:0] + ctx.qID = 0 + ctx.resetBatch() +} + +func (ctx *g1GLVStripeBatchAffine) executeAndReset() { + if ctx.cptAdd == 0 { + return + } + batchAddG1AffineDynamic(ctx.R, ctx.P, ctx.lambda, ctx.lambdain, ctx.cptAdd) + ctx.resetBatch() +} + +func (ctx *g1GLVStripeBatchAffine) addFromQueue(op batchOpG1AffineWide) { + BK := &ctx.buckets[op.bucketID] + ctx.touch(op.bucketID) + if BK.IsInfinity() { + BK.Set(&op.point) + return + } + if BK.X.Equal(&op.point.X) { + if BK.Y.Equal(&op.point.Y) { + ctx.bucketsJE[op.bucketID].addMixed(&op.point) + return + } + BK.SetInfinity() + return + } + + ctx.bucketStamps[op.bucketID] = ctx.stamp + ctx.R[ctx.cptAdd] = BK + ctx.P[ctx.cptAdd] = op.point + ctx.cptAdd++ +} + +func (ctx *g1GLVStripeBatchAffine) add(bucketID uint32, point *G1Affine, isAdd bool) { + BK := &ctx.buckets[bucketID] + ctx.touch(bucketID) + if BK.IsInfinity() { + if isAdd { + BK.Set(point) + } else { + BK.Neg(point) + } + return + } + if BK.X.Equal(&point.X) { + if BK.Y.Equal(&point.Y) { + if isAdd { + ctx.bucketsJE[bucketID].addMixed(point) + } else { + BK.SetInfinity() + } + return + } + if isAdd { + BK.SetInfinity() + } else { + ctx.bucketsJE[bucketID].subMixed(point) + } + return + } + + ctx.bucketStamps[bucketID] = ctx.stamp + ctx.R[ctx.cptAdd] = BK + if isAdd { + ctx.P[ctx.cptAdd].Set(point) + } else { + ctx.P[ctx.cptAdd].Neg(point) + } + ctx.cptAdd++ +} + +func (ctx *g1GLVStripeBatchAffine) flushQueue() { + for i := range ctx.qID { + ctx.touch(ctx.queue[i].bucketID) + ctx.bucketsJE[ctx.queue[i].bucketID].addMixed(&ctx.queue[i].point) + } + ctx.qID = 0 +} + +func (ctx *g1GLVStripeBatchAffine) processTopQueue() { + for i := ctx.qID - 1; i >= 0; i-- { + if ctx.bucketStamps[ctx.queue[i].bucketID] == ctx.stamp { + return + } + ctx.addFromQueue(ctx.queue[i]) + ctx.qID-- + } +} + +func (ctx *g1GLVStripeBatchAffine) addOp(op glvMSMG1WideOp, points []G1Affine) { + point := &points[op.index] + if point.IsInfinity() { + return + } + + if ctx.bucketStamps[op.bucket] == ctx.stamp { + ctx.queue[ctx.qID].bucketID = op.bucket + if op.neg { + ctx.queue[ctx.qID].point.Neg(point) + } else { + ctx.queue[ctx.qID].point.Set(point) + } + ctx.qID++ + if ctx.qID == len(ctx.queue)-1 { + ctx.flushQueue() + } + return + } + + ctx.add(op.bucket, point, !op.neg) + if ctx.cptAdd == len(ctx.P) { + ctx.executeAndReset() + ctx.processTopQueue() + } +} + +func (ctx *g1GLVStripeBatchAffine) reduceInto(runningSum, total *g1JacExtended) { + ctx.executeAndReset() + ctx.flushQueue() + for k := len(ctx.buckets) - 1; k >= 0; k-- { + runningSum.addMixed(&ctx.buckets[k]) + if !ctx.bucketsJE[k].IsInfinity() { + runningSum.add(&ctx.bucketsJE[k]) + } + total.add(runningSum) + } + ctx.resetStripe() +} + +func (ctx *g1GLVStripeBatchAffine) reduce(total *g1JacExtended) { + var runningSum g1JacExtended + runningSum.SetInfinity() + total.SetInfinity() + ctx.reduceInto(&runningSum, total) +} + +func processChunkG1GLVWideStriped(chunk int, chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1WideScalar) { + var counts0, counts1 [glvMSMG1WideStripeCount]int + for i := range points { + if points[i].IsInfinity() { + continue + } + if d := digits[i].k0[chunk]; d != 0 { + counts0[stripeIDG1Wide(d)]++ + } + if d := digits[i].k1[chunk]; d != 0 { + counts1[stripeIDG1Wide(d)]++ + } + } + + offsets0, totalOps0 := prefixWideCountsG1(counts0) + offsets1, totalOps1 := prefixWideCountsG1(counts1) + ops0 := make([]glvMSMG1WideOp, totalOps0) + ops1 := make([]glvMSMG1WideOp, totalOps1) + cursor0, cursor1 := offsets0, offsets1 + + for i := range points { + if points[i].IsInfinity() { + continue + } + if d := digits[i].k0[chunk]; d != 0 { + stripe, bucket, neg := splitWideDigitG1(d) + ops0[cursor0[stripe]] = glvMSMG1WideOp{index: uint32(i), bucket: bucket, neg: neg} + cursor0[stripe]++ + } + if d := digits[i].k1[chunk]; d != 0 { + stripe, bucket, neg := splitWideDigitG1(d) + ops1[cursor1[stripe]] = glvMSMG1WideOp{index: uint32(i), bucket: bucket, neg: neg} + cursor1[stripe]++ + } + } + + total0 := reduceWideOpsG1(points, ops0, offsets0, counts0) + total1 := reduceWideOpsG1(points, ops1, offsets1, counts1) + phiG1JacExtended(&total1) + total0.add(&total1) + chRes <- total0 +} + +func processChunkG1GLVWideDense(chunk int, chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1WideScalar) { + bucket0 := newG1GLVBatchAffineDynamic(1 << (glvMSMG1WideWindow - 1)) + bucket1 := newG1GLVBatchAffineDynamic(1 << (glvMSMG1WideWindow - 1)) + + for i := range points { + if points[i].IsInfinity() { + continue + } + bucket0.addWideDigit(digits[i].k0[chunk], &points[i]) + bucket1.addWideDigit(digits[i].k1[chunk], &points[i]) + } + + var total0, total1 g1JacExtended + bucket0.reduce(&total0) + bucket1.reduce(&total1) + phiG1JacExtended(&total1) + total0.add(&total1) + chRes <- total0 +} + +func (ctx *g1GLVStripeBatchAffine) addWideDigit(digit int32, point *G1Affine) { + if digit == 0 { + return + } + neg := digit < 0 + if neg { + digit = -digit + } + bucketID := uint32(digit) - 1 + + if ctx.bucketStamps[bucketID] == ctx.stamp { + ctx.queue[ctx.qID].bucketID = bucketID + if neg { + ctx.queue[ctx.qID].point.Neg(point) + } else { + ctx.queue[ctx.qID].point.Set(point) + } + ctx.qID++ + if ctx.qID == len(ctx.queue)-1 { + ctx.flushQueue() + } + return + } + + ctx.add(bucketID, point, !neg) + if ctx.cptAdd == len(ctx.P) { + ctx.executeAndReset() + ctx.processTopQueue() + } +} + +func stripeIDG1Wide(digit int32) int { + if digit < 0 { + digit = -digit + } + return int((uint32(digit) - 1) >> glvMSMG1WideStripeBits) +} + +func splitWideDigitG1(digit int32) (int, uint32, bool) { + neg := digit < 0 + if neg { + digit = -digit + } + bucketID := uint32(digit) - 1 + stripe := int(bucketID >> glvMSMG1WideStripeBits) + bucket := bucketID & (glvMSMG1WideStripeSize - 1) + return stripe, bucket, neg +} + +func prefixWideCountsG1(counts [glvMSMG1WideStripeCount]int) ([glvMSMG1WideStripeCount]int, int) { + var offsets [glvMSMG1WideStripeCount]int + total := 0 + for i, count := range counts { + offsets[i] = total + total += count + } + return offsets, total +} + +func reduceWideOpsG1(points []G1Affine, ops []glvMSMG1WideOp, offsets, counts [glvMSMG1WideStripeCount]int) g1JacExtended { + ctx := newG1GLVStripeBatchAffine() + var runningSum, total g1JacExtended + runningSum.SetInfinity() + total.SetInfinity() + + for stripe := glvMSMG1WideStripeCount - 1; stripe >= 0; stripe-- { + if counts[stripe] == 0 { + if !runningSum.IsInfinity() { + for range glvMSMG1WideStripeSize { + total.add(&runningSum) + } + } + continue + } + + start := offsets[stripe] + end := start + counts[stripe] + for _, op := range ops[start:end] { + ctx.addOp(op, points) + } + ctx.reduceInto(&runningSum, &total) + } + + return total +} + +func processExtraChunkG1GLVWide(chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1WideScalar) { + var total0, total1 g1JacExtended + total0.SetInfinity() + total1.SetInfinity() + + for i := range points { + switch digits[i].extra0 { + case 1: + total0.addMixed(&points[i]) + case -1: + total0.subMixed(&points[i]) + } + switch digits[i].extra1 { + case 1: + total1.addMixed(&points[i]) + case -1: + total1.subMixed(&points[i]) + } + } + + phiG1JacExtended(&total1) + total0.add(&total1) + chRes <- total0 +} + +func batchAddG1AffineDynamic(R []*G1Affine, P []G1Affine, lambda, lambdain []fp.Element, batchSize int) { + for j := range batchSize { + lambdain[j].Sub(&P[j].X, &R[j].X) + } + + var accumulator fp.Element + lambda[0].SetOne() + accumulator.Set(&lambdain[0]) + + for i := 1; i < batchSize; i++ { + lambda[i] = accumulator + accumulator.Mul(&accumulator, &lambdain[i]) + } + + accumulator.Inverse(&accumulator) + + for i := batchSize - 1; i > 0; i-- { + lambda[i].Mul(&lambda[i], &accumulator) + accumulator.Mul(&accumulator, &lambdain[i]) + } + lambda[0].Set(&accumulator) + + var t fp.Element + var Q G1Affine + for j := range batchSize { + t.Sub(&P[j].Y, &R[j].Y) + lambda[j].Mul(&lambda[j], &t) + + Q.X.Square(&lambda[j]) + Q.X.Sub(&Q.X, &R[j].X) + Q.X.Sub(&Q.X, &P[j].X) + + t.Sub(&R[j].X, &Q.X) + Q.Y.Mul(&lambda[j], &t) + Q.Y.Sub(&Q.Y, &R[j].Y) + + R[j].Set(&Q) + } +} diff --git a/ecc/bn254/multiexp_glv_test.go b/ecc/bn254/multiexp_glv_test.go new file mode 100644 index 0000000000..54c75b68fe --- /dev/null +++ b/ecc/bn254/multiexp_glv_test.go @@ -0,0 +1,176 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package bn254 + +import ( + "fmt" + "math/big" + "math/bits" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +func TestMultiExpG1GLV(t *testing.T) { + sizes := []int{0, 1, 2, 7, 73, 512, 1 << 12} + + for _, size := range sizes { + t.Run(fmt.Sprintf("%d points", size), func(t *testing.T) { + points := make([]G1Affine, size) + scalars := make([]fr.Element, size) + + var g G1Jac + g.Set(&g1Gen) + for i := range points { + points[i].FromJacobian(&g) + g.AddAssign(&g1Gen) + } + fillBenchScalars(scalars) + if size > 7 { + points[3].SetInfinity() + scalars[5].SetZero() + } + + var expected, got, gotWide, gotWideDense G1Jac + if _, err := expected.MultiExp(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { + t.Fatal(err) + } + if _, err := got.MultiExpGLV(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { + t.Fatal(err) + } + if !expected.Equal(&got) { + t.Fatalf("GLV MSM mismatch for %d points", size) + } + if _, err := gotWide.MultiExpGLVWide(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { + t.Fatal(err) + } + if !expected.Equal(&gotWide) { + t.Fatalf("wide GLV MSM mismatch for %d points", size) + } + if _, err := gotWideDense.MultiExpGLVWideDense(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { + t.Fatal(err) + } + if !expected.Equal(&gotWideDense) { + t.Fatalf("wide dense GLV MSM mismatch for %d points", size) + } + }) + } +} + +func TestSplitScalarGLVG1Fixed(t *testing.T) { + scalars := make([]fr.Element, 1024) + fillBenchScalars(scalars) + scalars = append(scalars, fr.Element{}, fr.NewElement(1)) + + for i := range scalars { + var got glvMSMG1Scalar + splitScalarGLVG1Fixed(scalars[i].Bits(), &got) + + var scalar big.Int + scalars[i].BigInt(&scalar) + want := ecc.SplitScalar(&scalar, &glvBasis) + + gotK0 := reconstructGLVG1Component(got.k0, got.extra0) + gotK1 := reconstructGLVG1Component(got.k1, got.extra1) + if gotK0.Cmp(&want[0]) != 0 || gotK1.Cmp(&want[1]) != 0 { + t.Fatalf("split mismatch at index %d", i) + } + + var gotWide glvMSMG1WideScalar + splitScalarGLVG1FixedWide(scalars[i].Bits(), &gotWide) + gotWideK0 := reconstructGLVG1WideComponent(gotWide.k0, gotWide.extra0) + gotWideK1 := reconstructGLVG1WideComponent(gotWide.k1, gotWide.extra1) + if gotWideK0.Cmp(&want[0]) != 0 || gotWideK1.Cmp(&want[1]) != 0 { + t.Fatalf("wide split mismatch at index %d", i) + } + } +} + +func reconstructGLVG1Component(digits [glvMSMG1Chunks]int16, extra int8) big.Int { + var res, term big.Int + for i, digit := range digits { + if digit == 0 { + continue + } + term.SetInt64(int64(digit)) + term.Lsh(&term, uint(i*glvMSMG1Window)) + res.Add(&res, &term) + } + if extra != 0 { + term.SetInt64(int64(extra)) + term.Lsh(&term, glvMSMG1Chunks*glvMSMG1Window) + res.Add(&res, &term) + } + return res +} + +func reconstructGLVG1WideComponent(digits [glvMSMG1WideChunks]int32, extra int8) big.Int { + var res, term big.Int + for i, digit := range digits { + if digit == 0 { + continue + } + term.SetInt64(int64(digit)) + term.Lsh(&term, uint(i*glvMSMG1WideWindow)) + res.Add(&res, &term) + } + if extra != 0 { + term.SetInt64(int64(extra)) + term.Lsh(&term, glvMSMG1WideChunks*glvMSMG1WideWindow) + res.Add(&res, &term) + } + return res +} + +func BenchmarkMultiExpG1GLV(b *testing.B) { + const ( + pow = (bits.UintSize / 2) - (bits.UintSize / 8) + nbSamples = 1 << pow + ) + + var ( + samplePoints [nbSamples]G1Affine + sampleScalars [nbSamples]fr.Element + ) + + fillBenchScalars(sampleScalars[:]) + fillBenchBasesG1(samplePoints[:]) + + for i := 16; i <= pow; i += 2 { + using := 1 << i + + b.Run(fmt.Sprintf("baseline/%d points", using), func(b *testing.B) { + var testPoint G1Affine + b.ResetTimer() + for range b.N { + testPoint.MultiExp(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) + } + }) + + b.Run(fmt.Sprintf("glv/%d points", using), func(b *testing.B) { + var testPoint G1Affine + b.ResetTimer() + for range b.N { + testPoint.MultiExpGLV(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) + } + }) + + b.Run(fmt.Sprintf("glv-wide/%d points", using), func(b *testing.B) { + var testPoint G1Affine + b.ResetTimer() + for range b.N { + testPoint.MultiExpGLVWide(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) + } + }) + + b.Run(fmt.Sprintf("glv-wide-dense/%d points", using), func(b *testing.B) { + var testPoint G1Affine + b.ResetTimer() + for range b.N { + testPoint.MultiExpGLVWideDense(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) + } + }) + } +} diff --git a/ecc/kb8/multiset-hash/doc.go b/ecc/kb8/multiset-hash/doc.go index 95c1c15df2..c38c4d842b 100644 --- a/ecc/kb8/multiset-hash/doc.go +++ b/ecc/kb8/multiset-hash/doc.go @@ -1,7 +1,26 @@ -// Package multisethash implements the y-increment multiset hash over kb8. +// Package multisethash implements y-increment elliptic-curve multiset +// hashing (ECMSH) over kb8. // -// Messages are 16-bit values. Each message m is mapped by scanning k in -// [0, 256) and setting y = m*256 + k in the base subfield of Fp^8. The first -// resulting point (x, y) on kb8 is used as the image of the message, and -// multiset hashing is the additive group sum of those mapped points. +// The package exposes three variants: +// +// - Classical one-point ECMSH (Accumulator, Hash, Map): 16-bit messages. +// Each message m is mapped by scanning k in [0, 256) and setting +// y = m*256 + k in the base subfield of Fp^8. The first resulting point +// (x, y) on kb8 is used as the image. Security is ~124 classical bits +// (no post-quantum security). +// +// - Linear-separator vector ECMSH (LinearAccumulator, HashLinear, +// MapLinear): a digest of N = 23 curve points. Coordinate i uses +// y_i(m, k) = T*(m + i*M) + k with T = 128, M = 2^18. +// +// - Poseidon2-sponge vector ECMSH (Poseidon2Accumulator, HashPoseidon2, +// MapPoseidon2): a digest of N = 23 curve points. The N ordinates are +// derived by absorbing (domain tag, msg) into a width-16 Poseidon2 +// sponge (rate 8, 3 squeeze permutations) and range-reducing each +// output into [0, floor(p/(2T))) with T = 256. +// +// The two vector variants are post-quantum candidates: under Shor's +// algorithm a collision becomes a bounded modular linear relation, i.e. a +// SIS-shaped problem with modulus r ~ 2^248 and dimension N = 23 +// (5704 SIS-volume bits, matching the Linea KoalaBear LtHash baseline). package multisethash diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_linear.go b/ecc/kb8/multiset-hash/vector_multiset_hash_linear.go new file mode 100644 index 0000000000..5035bd3f9a --- /dev/null +++ b/ecc/kb8/multiset-hash/vector_multiset_hash_linear.go @@ -0,0 +1,140 @@ +package multisethash + +import ( + "errors" + "fmt" + + "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" +) + +// Linear-separator vector ECMSH (Section 4.3 "linear separator"). +// +// The digest is a vector of N = 23 ECMSH accumulators. Coordinate i uses +// the y-increment relation with per-coordinate slot s_i = msg + i*M: +// +// y_i(msg, k) = T * (msg + i*M) + k, k in [0, T) +// +// With T = 128 and M = 2^18, the encoded ordinates satisfy +// y_i < N*M*T = 23 * 2^18 * 128 < p/2, so the image is inverse-free. +const ( + linearN = 23 + linearT = 128 + linearM = 1 << 18 +) + +var errLinearMsgOutOfRange = fmt.Errorf("kb8 vector multiset hash: linear message must be < 2^18 (= %d)", linearM) + +// LinearAccumulator holds the N affine accumulator points for the +// linear-separator vector ECMSH. +type LinearAccumulator struct { + sum [linearN]kb8.G1Affine +} + +// NewLinearAccumulator returns a zero (all-infinity) LinearAccumulator. +func NewLinearAccumulator() LinearAccumulator { + var a LinearAccumulator + for i := range a.sum { + a.sum[i].SetInfinity() + } + return a +} + +// Insert maps msg to N curve points using the linear domain separator and +// adds each point to the corresponding accumulator coordinate. +func (a *LinearAccumulator) Insert(msg uint32) error { + pts, _, err := MapLinear(msg) + if err != nil { + return err + } + for i := range a.sum { + a.sum[i].Add(&a.sum[i], &pts[i]) + } + return nil +} + +// Remove maps msg to N curve points and subtracts each from the +// corresponding accumulator coordinate. +func (a *LinearAccumulator) Remove(msg uint32) error { + pts, _, err := MapLinear(msg) + if err != nil { + return err + } + var neg kb8.G1Affine + for i := range a.sum { + neg.Neg(&pts[i]) + a.sum[i].Add(&a.sum[i], &neg) + } + return nil +} + +// Digest returns the current vector of accumulator points. +func (a *LinearAccumulator) Digest() [linearN]kb8.G1Affine { + return a.sum +} + +// Reset clears the accumulator to the all-infinity state. +func (a *LinearAccumulator) Reset() { + for i := range a.sum { + a.sum[i].SetInfinity() + } +} + +// HashLinear returns the linear-separator vector ECMSH of msgs. +func HashLinear(msgs []uint32) ([linearN]kb8.G1Affine, error) { + acc := NewLinearAccumulator() + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + return [linearN]kb8.G1Affine{}, err + } + } + return acc.Digest(), nil +} + +// MapLinear deterministically maps msg to N curve points using the linear +// domain separator y_i(msg, k) = T*(msg + i*M) + k. It returns the N points +// and the offsets k_i in [0, T) that produced them. +func MapLinear(msg uint32) ([linearN]kb8.G1Affine, [linearN]uint8, error) { + var ( + pts [linearN]kb8.G1Affine + offsets [linearN]uint8 + ) + if uint64(msg) >= linearM { + return pts, offsets, errLinearMsgOutOfRange + } + _, b := kb8.CurveCoefficients() + for i := 0; i < linearN; i++ { + baseY := (uint64(msg) + uint64(i)*linearM) * linearT + p, k, err := mapAtBase(baseY, linearT, &b) + if err != nil { + return pts, offsets, err + } + pts[i] = p + offsets[i] = k + } + return pts, offsets, nil +} + +// mapAtBase scans k in [0, tweakBound) and returns the first curve point +// whose ordinate is y = baseY + k in the base subfield. baseY + tweakBound +// must remain strictly below p/2 to keep the image inverse-free. +func mapAtBase(baseY uint64, tweakBound uint64, b *extensions.E8) (kb8.G1Affine, uint8, error) { + for k := uint64(0); k < tweakBound; k++ { + var y, c, ySquared extensions.E8 + y.C0.B0.A0.SetUint64(baseY + k) + + ySquared.Square(&y) + c.Sub(b, &ySquared) + + x, ok := depressedCubicRoot(c) + if !ok { + continue + } + + p := kb8.G1Affine{X: x, Y: y} + if p.IsOnCurve() && p.IsInSubGroup() { + return p, uint8(k), nil + } + } + return kb8.G1Affine{}, 0, errors.New("kb8 vector multiset hash: failed to map message in tweak window") +} diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go b/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go new file mode 100644 index 0000000000..b6c1e32c88 --- /dev/null +++ b/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go @@ -0,0 +1,179 @@ +package multisethash + +import ( + "encoding/binary" + "math/big" + "sync" + + "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" +) + +// Poseidon2-sponge vector ECMSH (Section 4.3 "preferred concrete derivation"). +// +// The digest is a vector of N = 23 ECMSH accumulators. The N ordinates are +// derived by absorbing (domainTag, msg) into a width-16 Poseidon2 sponge with +// rate 8, then squeezing 3 permutations worth of output (24 elements, 23 used). +// Each squeezed element u is reduced into [0, floor(p / (2*T))), giving a slot +// s and ordinate y = T*s + k for some k in [0, T). With T = 256, every y stays +// below p/2 so the image is inverse-free. +const ( + pqN = 23 + pqT = 256 + pqWidth = 16 // Poseidon2 state width + pqSqueezeRate = 8 // koalabear elements consumed per permutation + pqPermutations = 3 // ceil(pqN / pqSqueezeRate) +) + +// pqDomainTag is the 8-byte ASCII domain separator absorbed before the +// message (paper §4.3, "ECMSH_PQ"). +var pqDomainTag = [8]byte{'E', 'C', 'M', 'S', 'H', '_', 'P', 'Q'} + +// pqReducerBound = floor(p / (2*T)) is the upper bound on the slot s +// extracted from each squeezed koalabear element. With p = 2^31 - 2^24 + 1 +// and T = 256, this is floor(2130706433 / 512) = 4161536. +var ( + pqReducerBound = func() *big.Int { + p := koalabear.Modulus() + denom := big.NewInt(2 * pqT) + return new(big.Int).Div(p, denom) + }() + + pqPermOnce sync.Once + pqPermImpl *poseidon2.Permutation +) + +// pqPerm returns the lazily-initialized width-16 Poseidon2 permutation +// shared across all map calls. +func pqPerm() *poseidon2.Permutation { + pqPermOnce.Do(func() { + pqPermImpl = poseidon2.NewPermutation(pqWidth, 6, 21) + }) + return pqPermImpl +} + +// Poseidon2Accumulator holds the N affine accumulator points for the +// Poseidon2-sponge vector ECMSH. +type Poseidon2Accumulator struct { + sum [pqN]kb8.G1Affine +} + +// NewPoseidon2Accumulator returns a zero (all-infinity) Poseidon2Accumulator. +func NewPoseidon2Accumulator() Poseidon2Accumulator { + var a Poseidon2Accumulator + for i := range a.sum { + a.sum[i].SetInfinity() + } + return a +} + +// Insert maps msg to N curve points via the Poseidon2 sponge domain separator +// and adds each point to the corresponding accumulator coordinate. +func (a *Poseidon2Accumulator) Insert(msg uint64) error { + pts, _, err := MapPoseidon2(msg) + if err != nil { + return err + } + for i := range a.sum { + a.sum[i].Add(&a.sum[i], &pts[i]) + } + return nil +} + +// Remove maps msg to N curve points and subtracts each from the corresponding +// accumulator coordinate. +func (a *Poseidon2Accumulator) Remove(msg uint64) error { + pts, _, err := MapPoseidon2(msg) + if err != nil { + return err + } + var neg kb8.G1Affine + for i := range a.sum { + neg.Neg(&pts[i]) + a.sum[i].Add(&a.sum[i], &neg) + } + return nil +} + +// Digest returns the current vector of accumulator points. +func (a *Poseidon2Accumulator) Digest() [pqN]kb8.G1Affine { + return a.sum +} + +// Reset clears the accumulator to the all-infinity state. +func (a *Poseidon2Accumulator) Reset() { + for i := range a.sum { + a.sum[i].SetInfinity() + } +} + +// HashPoseidon2 returns the Poseidon2-sponge vector ECMSH of msgs. +func HashPoseidon2(msgs []uint64) ([pqN]kb8.G1Affine, error) { + acc := NewPoseidon2Accumulator() + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + return [pqN]kb8.G1Affine{}, err + } + } + return acc.Digest(), nil +} + +// MapPoseidon2 deterministically maps msg to N curve points using a +// Poseidon2 sponge over the koalabear field. It returns the N points and +// the per-coordinate tweak offsets k_i in [0, T) that produced them. +func MapPoseidon2(msg uint64) ([pqN]kb8.G1Affine, [pqN]uint8, error) { + var ( + pts [pqN]kb8.G1Affine + offsets [pqN]uint8 + ) + + squeezed, err := squeezePoseidon2(msg) + if err != nil { + return pts, offsets, err + } + + _, b := kb8.CurveCoefficients() + var tmp big.Int + for i := 0; i < pqN; i++ { + squeezed[i].BigInt(&tmp) + tmp.Mod(&tmp, pqReducerBound) + baseY := tmp.Uint64() * pqT + + p, k, err := mapAtBase(baseY, pqT, &b) + if err != nil { + return pts, offsets, err + } + pts[i] = p + offsets[i] = k + } + return pts, offsets, nil +} + +// squeezePoseidon2 absorbs the domain tag and msg into a width-16 sponge with +// rate 8 and returns the first pqPermutations * pqSqueezeRate squeezed +// koalabear elements. +func squeezePoseidon2(msg uint64) ([pqPermutations * pqSqueezeRate]koalabear.Element, error) { + var ( + state [pqWidth]koalabear.Element + squeezed [pqPermutations * pqSqueezeRate]koalabear.Element + ) + + // Absorb (domainTag, msg) into the rate part (state[0:pqSqueezeRate]). + // The 8-byte tag is split into two 32-bit big-endian halves and the + // 64-bit msg into two more halves, filling state[0..3]. state[4..7] + // and the capacity state[8..15] stay zero. + state[0].SetUint64(uint64(binary.BigEndian.Uint32(pqDomainTag[0:4]))) + state[1].SetUint64(uint64(binary.BigEndian.Uint32(pqDomainTag[4:8]))) + state[2].SetUint64(msg & 0xFFFFFFFF) + state[3].SetUint64(msg >> 32) + + perm := pqPerm() + for i := 0; i < pqPermutations; i++ { + if err := perm.Permutation(state[:]); err != nil { + return squeezed, err + } + copy(squeezed[i*pqSqueezeRate:(i+1)*pqSqueezeRate], state[:pqSqueezeRate]) + } + return squeezed, nil +} diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_test.go b/ecc/kb8/multiset-hash/vector_multiset_hash_test.go new file mode 100644 index 0000000000..0efd47191c --- /dev/null +++ b/ecc/kb8/multiset-hash/vector_multiset_hash_test.go @@ -0,0 +1,483 @@ +package multisethash + +import ( + "math" + "math/big" + "testing" + + "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/stretchr/testify/require" +) + +// halfModulus returns floor(p/2), the threshold below which the encoded +// ordinate must remain to keep the map-to-curve image inverse-free. +func halfModulus() *big.Int { + p := koalabear.Modulus() + return new(big.Int).Rsh(p, 1) +} + +func sampleLinearMessages(n int) []uint32 { + res := make([]uint32, n) + var x uint32 = 1 + for i := range res { + x = 1664525*x + 1013904223 + res[i] = x % linearM + } + return res +} + +func sampleVectorMessages64(n int) []uint64 { + res := make([]uint64, n) + var x uint64 = 1 + for i := range res { + x = 6364136223846793005*x + 1442695040888963407 + res[i] = x + } + return res +} + +// ----- Linear separator ----- + +func TestMapLinearDeterministic(t *testing.T) { + for _, msg := range sampleLinearMessages(16) { + p1, o1, err := MapLinear(msg) + require.NoError(t, err) + p2, o2, err := MapLinear(msg) + require.NoError(t, err) + require.Equal(t, o1, o2) + for i := range p1 { + require.True(t, p1[i].Equal(&p2[i])) + require.True(t, p1[i].IsOnCurve()) + require.True(t, p1[i].IsInSubGroup()) + require.False(t, p1[i].IsInfinity()) + require.Less(t, int(o1[i]), linearT) + } + } +} + +func TestMapLinearInverseFree(t *testing.T) { + half := halfModulus() + var y big.Int + for _, msg := range sampleLinearMessages(32) { + pts, _, err := MapLinear(msg) + require.NoError(t, err) + for i := range pts { + pts[i].Y.C0.B0.A0.BigInt(&y) + require.Negative(t, y.Cmp(half), "linear y_%d out of inverse-free domain for msg=%d", i, msg) + } + } +} + +func TestLinearRejectsOutOfRange(t *testing.T) { + _, _, err := MapLinear(linearM) + require.Error(t, err) + _, _, err = MapLinear(linearM - 1) + require.NoError(t, err) + acc := NewLinearAccumulator() + require.Error(t, acc.Insert(linearM)) + require.NoError(t, acc.Insert(linearM-1)) +} + +func TestLinearHashPermutationInvariant(t *testing.T) { + msgs := sampleLinearMessages(8) + got1, err := HashLinear(msgs) + require.NoError(t, err) + + permuted := append([]uint32(nil), msgs...) + permuted[0], permuted[5] = permuted[5], permuted[0] + permuted[1], permuted[7] = permuted[7], permuted[1] + + got2, err := HashLinear(permuted) + require.NoError(t, err) + for i := range got1 { + require.True(t, got1[i].Equal(&got2[i])) + } +} + +func TestLinearInsertRemoveCancellation(t *testing.T) { + msgs := sampleLinearMessages(6) + acc := NewLinearAccumulator() + for _, msg := range msgs { + require.NoError(t, acc.Insert(msg)) + } + require.NoError(t, acc.Remove(msgs[2])) + require.NoError(t, acc.Remove(msgs[4])) + + acc2 := NewLinearAccumulator() + require.NoError(t, acc2.Insert(msgs[0])) + require.NoError(t, acc2.Insert(msgs[1])) + require.NoError(t, acc2.Insert(msgs[3])) + require.NoError(t, acc2.Insert(msgs[5])) + + d1 := acc.Digest() + d2 := acc2.Digest() + for i := range d1 { + require.True(t, d1[i].Equal(&d2[i])) + } +} + +func TestLinearDuplicatesMatter(t *testing.T) { + msg := sampleLinearMessages(1)[0] + single, err := HashLinear([]uint32{msg}) + require.NoError(t, err) + double, err := HashLinear([]uint32{msg, msg}) + require.NoError(t, err) + var differs bool + for i := range single { + if !single[i].Equal(&double[i]) { + differs = true + break + } + } + require.True(t, differs, "doubling msg must change the linear digest") +} + +func TestEmptyLinearHashIsInfinity(t *testing.T) { + got, err := HashLinear(nil) + require.NoError(t, err) + for i := range got { + require.True(t, got[i].IsInfinity()) + } +} + +// ----- Poseidon2 sponge separator ----- + +func TestMapPoseidon2Deterministic(t *testing.T) { + for _, msg := range sampleVectorMessages64(16) { + p1, o1, err := MapPoseidon2(msg) + require.NoError(t, err) + p2, o2, err := MapPoseidon2(msg) + require.NoError(t, err) + require.Equal(t, o1, o2) + for i := range p1 { + require.True(t, p1[i].Equal(&p2[i])) + require.True(t, p1[i].IsOnCurve()) + require.True(t, p1[i].IsInSubGroup()) + require.False(t, p1[i].IsInfinity()) + require.Less(t, int(o1[i]), pqT) + } + } +} + +func TestMapPoseidon2InverseFree(t *testing.T) { + half := halfModulus() + var y big.Int + for _, msg := range sampleVectorMessages64(32) { + pts, _, err := MapPoseidon2(msg) + require.NoError(t, err) + for i := range pts { + pts[i].Y.C0.B0.A0.BigInt(&y) + require.Negative(t, y.Cmp(half), "poseidon2 y_%d out of inverse-free domain for msg=%d", i, msg) + } + } +} + +func TestPoseidon2HashPermutationInvariant(t *testing.T) { + msgs := sampleVectorMessages64(8) + got1, err := HashPoseidon2(msgs) + require.NoError(t, err) + + permuted := append([]uint64(nil), msgs...) + permuted[0], permuted[5] = permuted[5], permuted[0] + permuted[1], permuted[7] = permuted[7], permuted[1] + + got2, err := HashPoseidon2(permuted) + require.NoError(t, err) + for i := range got1 { + require.True(t, got1[i].Equal(&got2[i])) + } +} + +func TestPoseidon2InsertRemoveCancellation(t *testing.T) { + msgs := sampleVectorMessages64(6) + acc := NewPoseidon2Accumulator() + for _, msg := range msgs { + require.NoError(t, acc.Insert(msg)) + } + require.NoError(t, acc.Remove(msgs[2])) + require.NoError(t, acc.Remove(msgs[4])) + + acc2 := NewPoseidon2Accumulator() + require.NoError(t, acc2.Insert(msgs[0])) + require.NoError(t, acc2.Insert(msgs[1])) + require.NoError(t, acc2.Insert(msgs[3])) + require.NoError(t, acc2.Insert(msgs[5])) + + d1 := acc.Digest() + d2 := acc2.Digest() + for i := range d1 { + require.True(t, d1[i].Equal(&d2[i])) + } +} + +func TestPoseidon2DuplicatesMatter(t *testing.T) { + msg := sampleVectorMessages64(1)[0] + single, err := HashPoseidon2([]uint64{msg}) + require.NoError(t, err) + double, err := HashPoseidon2([]uint64{msg, msg}) + require.NoError(t, err) + var differs bool + for i := range single { + if !single[i].Equal(&double[i]) { + differs = true + break + } + } + require.True(t, differs, "doubling msg must change the poseidon2 digest") +} + +func TestEmptyPoseidon2HashIsInfinity(t *testing.T) { + got, err := HashPoseidon2(nil) + require.NoError(t, err) + for i := range got { + require.True(t, got[i].IsInfinity()) + } +} + +// ----- Hash <-> Accumulator equivalence ----- + +func TestLinearHashMatchesAccumulator(t *testing.T) { + msgs := sampleLinearMessages(10) + got, err := HashLinear(msgs) + require.NoError(t, err) + + acc := NewLinearAccumulator() + for _, msg := range msgs { + require.NoError(t, acc.Insert(msg)) + } + digest := acc.Digest() + for i := range got { + require.True(t, got[i].Equal(&digest[i])) + } +} + +func TestPoseidon2HashMatchesAccumulator(t *testing.T) { + msgs := sampleVectorMessages64(10) + got, err := HashPoseidon2(msgs) + require.NoError(t, err) + + acc := NewPoseidon2Accumulator() + for _, msg := range msgs { + require.NoError(t, acc.Insert(msg)) + } + digest := acc.Digest() + for i := range got { + require.True(t, got[i].Equal(&digest[i])) + } +} + +// ----- Homomorphic additivity: Hash(A ∪ B) = Hash(A) + Hash(B) ----- + +func TestLinearHomomorphicAdditivity(t *testing.T) { + msgs := sampleLinearMessages(12) + mid := len(msgs) / 2 + a, b := msgs[:mid], msgs[mid:] + + full, err := HashLinear(msgs) + require.NoError(t, err) + dA, err := HashLinear(a) + require.NoError(t, err) + dB, err := HashLinear(b) + require.NoError(t, err) + + for i := range full { + var sum kb8.G1Affine + sum.Add(&dA[i], &dB[i]) + require.True(t, sum.Equal(&full[i]), + "linear: Hash(A∪B)[%d] must equal Hash(A)+Hash(B)", i) + } +} + +func TestPoseidon2HomomorphicAdditivity(t *testing.T) { + msgs := sampleVectorMessages64(12) + mid := len(msgs) / 2 + a, b := msgs[:mid], msgs[mid:] + + full, err := HashPoseidon2(msgs) + require.NoError(t, err) + dA, err := HashPoseidon2(a) + require.NoError(t, err) + dB, err := HashPoseidon2(b) + require.NoError(t, err) + + for i := range full { + var sum kb8.G1Affine + sum.Add(&dA[i], &dB[i]) + require.True(t, sum.Equal(&full[i]), + "poseidon2: Hash(A∪B)[%d] must equal Hash(A)+Hash(B)", i) + } +} + +// ----- Distinct messages produce distinct digests ----- + +func TestLinearDistinctMessagesDiffer(t *testing.T) { + msgs := sampleLinearMessages(8) + seen := make(map[string]uint32) + for _, msg := range msgs { + digest, err := HashLinear([]uint32{msg}) + require.NoError(t, err) + // digest the first coordinate's bytes as a fingerprint; full + // equality is exercised by the deterministic test. + buf := digest[0].Bytes() + key := string(buf[:]) + if prev, ok := seen[key]; ok { + require.Equal(t, prev, msg, + "distinct linear msgs %d and %d collide on coord 0", prev, msg) + } + seen[key] = msg + } +} + +func TestPoseidon2DistinctMessagesDiffer(t *testing.T) { + msgs := sampleVectorMessages64(8) + seen := make(map[string]uint64) + for _, msg := range msgs { + digest, err := HashPoseidon2([]uint64{msg}) + require.NoError(t, err) + buf := digest[0].Bytes() + key := string(buf[:]) + if prev, ok := seen[key]; ok { + require.Equal(t, prev, msg, + "distinct poseidon2 msgs %d and %d collide on coord 0", prev, msg) + } + seen[key] = msg + } +} + +// ----- Boundary messages ----- + +func TestLinearBoundaryMessages(t *testing.T) { + for _, msg := range []uint32{0, linearM - 1} { + pts, _, err := MapLinear(msg) + require.NoError(t, err, "MapLinear must succeed for boundary msg=%d", msg) + for i := range pts { + require.True(t, pts[i].IsOnCurve()) + require.True(t, pts[i].IsInSubGroup()) + } + } +} + +func TestPoseidon2BoundaryMessages(t *testing.T) { + for _, msg := range []uint64{0, math.MaxUint64} { + pts, _, err := MapPoseidon2(msg) + require.NoError(t, err, "MapPoseidon2 must succeed for boundary msg=%d", msg) + for i := range pts { + require.True(t, pts[i].IsOnCurve()) + require.True(t, pts[i].IsInSubGroup()) + } + } +} + +// ----- Linear per-coordinate slot range ----- + +func TestLinearPerCoordinateSlotRange(t *testing.T) { + // Coordinate i must encode y in [T*i*M, T*(i+1)*M). This catches an + // off-by-one or index swap in the linear separator. + var y big.Int + for _, msg := range sampleLinearMessages(8) { + pts, _, err := MapLinear(msg) + require.NoError(t, err) + for i := range pts { + pts[i].Y.C0.B0.A0.BigInt(&y) + loBound := uint64(linearT) * uint64(i) * uint64(linearM) + hiBound := uint64(linearT) * (uint64(i) + 1) * uint64(linearM) + require.GreaterOrEqual(t, y.Cmp(new(big.Int).SetUint64(loBound)), 0, + "coord %d y must be >= %d (msg=%d)", i, loBound, msg) + require.Negative(t, y.Cmp(new(big.Int).SetUint64(hiBound)), + "coord %d y must be < %d (msg=%d)", i, hiBound, msg) + } + } +} + +// ----- Reset ----- + +func TestLinearReset(t *testing.T) { + acc := NewLinearAccumulator() + for _, msg := range sampleLinearMessages(4) { + require.NoError(t, acc.Insert(msg)) + } + acc.Reset() + digest := acc.Digest() + for i := range digest { + require.True(t, digest[i].IsInfinity(), + "linear Reset must clear coord %d", i) + } +} + +func TestPoseidon2Reset(t *testing.T) { + acc := NewPoseidon2Accumulator() + for _, msg := range sampleVectorMessages64(4) { + require.NoError(t, acc.Insert(msg)) + } + acc.Reset() + digest := acc.Digest() + for i := range digest { + require.True(t, digest[i].IsInfinity(), + "poseidon2 Reset must clear coord %d", i) + } +} + +// ----- Cross-variant sanity ----- + +func TestLinearAndPoseidon2DigestsDiffer(t *testing.T) { + // The two variants build their digests over different domain separators, + // so even at "msg=0" their per-coordinate accumulators should not match + // point-by-point. + linDigest, err := HashLinear([]uint32{0}) + require.NoError(t, err) + pqDigest, err := HashPoseidon2([]uint64{0}) + require.NoError(t, err) + + var anyDiff bool + for i := range linDigest { + var l, p kb8.G1Affine = linDigest[i], pqDigest[i] + if !l.Equal(&p) { + anyDiff = true + break + } + } + require.True(t, anyDiff) +} + +// ----- Benchmarks ----- + +func BenchmarkMapLinear(b *testing.B) { + msg := sampleLinearMessages(1)[0] + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, _, err := MapLinear(msg); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkMapPoseidon2(b *testing.B) { + msg := sampleVectorMessages64(1)[0] + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, _, err := MapPoseidon2(msg); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkHashLinear256(b *testing.B) { + msgs := sampleLinearMessages(256) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := HashLinear(msgs); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkHashPoseidon2_256(b *testing.B) { + msgs := sampleVectorMessages64(256) + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := HashPoseidon2(msgs); err != nil { + b.Fatal(err) + } + } +} From edf8428bf405c13ac121dd92e9d5b3b8fb1896be Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 18 May 2026 16:09:12 -0400 Subject: [PATCH 20/33] chore: go generate --- ecc/kb8/fr/element_amd64.s | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/kb8/fr/element_amd64.s b/ecc/kb8/fr/element_amd64.s index 7c1e38abd2..464c982518 100644 --- a/ecc/kb8/fr/element_amd64.s +++ b/ecc/kb8/fr/element_amd64.s @@ -5,6 +5,6 @@ // Code generated by consensys/gnark-crypto DO NOT EDIT -// We include the hash to force the Go compiler to recompile: 4712025900192836046 +// We include the hash to force the Go compiler to recompile: 10523172047764019734 #include "../../../field/asm/element_4w/element_4w_amd64.s" From d29036cd98f0fb8b827ce04cc391b7f7a539df1e Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 18 May 2026 16:49:51 -0400 Subject: [PATCH 21/33] refactor: exports and inlines --- .../vector_multiset_hash_poseidon2.go | 26 +++++++++++++++++++ field/koalabear/extensions/e2.go | 7 ++++- field/koalabear/extensions/e4.go | 6 ++--- field/koalabear/extensions/e8.go | 12 ++++----- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go b/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go index b6c1e32c88..d8b50e7195 100644 --- a/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go +++ b/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go @@ -150,6 +150,32 @@ func MapPoseidon2(msg uint64) ([pqN]kb8.G1Affine, [pqN]uint8, error) { return pts, offsets, nil } +// MapAtSlot is a public helper used by the gnark in-circuit Poseidon2 vector +// ECMSH gadget. Given a slot s = u mod ⌊p/(2T)⌋ (already range-reduced by the +// caller — typically the in-circuit code after a Poseidon2 squeeze), it scans +// k in [0, pqT) and returns the first kb8 curve point whose ordinate is +// y = pqT*s + k in the base subfield. The slot must satisfy +// pqT*s + (pqT-1) < p/2 to preserve inverse-freeness; this is automatic when +// s < ⌊p/(2T)⌋. +func MapAtSlot(slot uint64) (kb8.G1Affine, uint8, error) { + _, b := kb8.CurveCoefficients() + return mapAtBase(slot*pqT, pqT, &b) +} + +// PqReducerBound returns ⌊p/(2T)⌋, the upper bound on the slot s produced by +// the Poseidon2 sponge range-reduction. Exported for the gnark in-circuit +// range-reduction constraint. +func PqReducerBound() *big.Int { + return new(big.Int).Set(pqReducerBound) +} + +// PqDomainTag returns the 8-byte ASCII domain separator absorbed before the +// message by MapPoseidon2 (paper §4.3, "ECMSH_PQ"). Exported so the in-circuit +// sponge can absorb the same bytes. +func PqDomainTag() [8]byte { + return pqDomainTag +} + // squeezePoseidon2 absorbs the domain tag and msg into a width-16 sponge with // rate 8 and returns the first pqPermutations * pqSqueezeRate squeezed // koalabear elements. diff --git a/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index 5eb2cc9e3f..4ebba767dd 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -31,7 +31,12 @@ func init() { cbrtFpThreeInv.Inverse(&cbrtFpThree) cbrtE2One.SetOne() - cbrtE2NRInv.MulByNonResidueInv(&cbrtE2One) + // cbrtE2NRInv = cbrtE2One * u^{-1} = (0, 1/3) since u² = 3 in the koalabear + // quadratic non-residue convention. Inlined to avoid depending on the removed + // E2.MulByNonResidueInv method (master renamed it; the cbrt stack still needs + // this exact value). + cbrtE2NRInv.A0.SetZero() + cbrtE2NRInv.A1.Set(&cbrtFpThreeInv) var sqrtMinusThree E2 sqrtMinusThree.A0.Neg(&cbrtFpThree) sqrtMinusThree.Sqrt(&sqrtMinusThree) diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index 4fab949d56..7f47f24d6e 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -477,7 +477,7 @@ func (z *E4) Cbrt(x *E4) *E4 { var x0sq, x1sq, betaX1sq, norm E2 x0sq.Square(&x.B0) x1sq.Square(&x.B1) - betaX1sq.MulByNonResidue(&x1sq) + betaX1sq.MulByQuadraticNonResidue(&x1sq) norm.Sub(&x0sq, &betaX1sq) var m, normInv E2 @@ -514,7 +514,7 @@ func (z *E4) Cbrt(x *E4) *E4 { var gamma0, gamma1 E2 gamma0.Mul(&wa1, &k) - gamma0.MulByNonResidue(&gamma0) + gamma0.MulByQuadraticNonResidue(&gamma0) gamma1.Mul(&wa0, &k) var mInv E2 @@ -524,7 +524,7 @@ func (z *E4) Cbrt(x *E4) *E4 { var t1, t2 E2 t1.Mul(&x.B0, &gamma0) t2.Mul(&x.B1, &gamma1) - t2.MulByNonResidue(&t2) + t2.MulByQuadraticNonResidue(&t2) y.B0.Sub(&t1, &t2).Mul(&y.B0, &mInv) t1.Mul(&x.B1, &gamma0) t2.Mul(&x.B0, &gamma1) diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index 5308357d63..e221831604 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -180,7 +180,7 @@ func (z *E8) Mul(x, y *E8) *E8 { Sub(&c, &a). Sub(&c, &b) z.C1.Set(&c) - b.MulByNonResidue(&b) + b.MulByQuadraticNonResidue(&b) z.C0.Add(&a, &b) return z } @@ -194,7 +194,7 @@ func (z *E8) Square(x *E8) *E8 { func (z *E8) Inverse(x *E8) *E8 { var t0, t1 E4 t0.Square(&x.C0) - t1.Square(&x.C1).MulByNonResidue(&t1) + t1.Square(&x.C1).MulByQuadraticNonResidue(&t1) t0.Sub(&t0, &t1).Inverse(&t0) z.C0.Mul(&x.C0, &t0) z.C1.Mul(&x.C1, &t0).Neg(&z.C1) @@ -269,7 +269,7 @@ func (z *E8) Halve() { // norm sets x to the norm of z. func (z *E8) norm(x *E4) { var tmp E4 - tmp.Square(&z.C1).MulByNonResidue(&tmp) + tmp.Square(&z.C1).MulByQuadraticNonResidue(&tmp) x.Square(&z.C0).Sub(x, &tmp) } @@ -321,7 +321,7 @@ func (z *E8) Cbrt(x *E8) *E8 { var x0sq, x1sq, betaX1sq, norm E4 x0sq.Square(&x.C0) x1sq.Square(&x.C1) - betaX1sq.MulByNonResidue(&x1sq) + betaX1sq.MulByQuadraticNonResidue(&x1sq) norm.Sub(&x0sq, &betaX1sq) var m, normInv E4 @@ -358,7 +358,7 @@ func (z *E8) Cbrt(x *E8) *E8 { var gamma0, gamma1 E4 gamma0.Mul(&wa1, &k) - gamma0.MulByNonResidue(&gamma0) + gamma0.MulByQuadraticNonResidue(&gamma0) gamma1.Mul(&wa0, &k) var mInv E4 @@ -368,7 +368,7 @@ func (z *E8) Cbrt(x *E8) *E8 { var t1, t2 E4 t1.Mul(&x.C0, &gamma0) t2.Mul(&x.C1, &gamma1) - t2.MulByNonResidue(&t2) + t2.MulByQuadraticNonResidue(&t2) y.C0.Sub(&t1, &t2).Mul(&y.C0, &mInv) t1.Mul(&x.C1, &gamma0) t2.Mul(&x.C0, &gamma1) From 913fcc2d84464c73a28f67666723f86db212a36b Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 18 May 2026 16:57:55 -0400 Subject: [PATCH 22/33] fix: make linter happy --- ecc/kb8/multiset-hash/vector_multiset_hash_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_test.go b/ecc/kb8/multiset-hash/vector_multiset_hash_test.go index 0efd47191c..5100220eb0 100644 --- a/ecc/kb8/multiset-hash/vector_multiset_hash_test.go +++ b/ecc/kb8/multiset-hash/vector_multiset_hash_test.go @@ -431,7 +431,7 @@ func TestLinearAndPoseidon2DigestsDiffer(t *testing.T) { var anyDiff bool for i := range linDigest { - var l, p kb8.G1Affine = linDigest[i], pqDigest[i] + l, p := linDigest[i], pqDigest[i] if !l.Equal(&p) { anyDiff = true break From fe90751e98a7bfdfb30dee318590111b69db0f75 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 18 May 2026 21:04:06 -0400 Subject: [PATCH 23/33] fix(bn254): make MaxUint32 check compile on 32-bit GOARCH The `1<<32-1` literal in MultiExpGLVWide's nbPoints guard overflows the 32-bit int on GOARCH=386, breaking the CI `go test -json` step that runs on ./ecc/bn254/... under 32-bit. The build failure emits a JSON event with empty Package, which makes gotestfmt v2.5.0 panic with "BUG: Empty package name encountered" and fails the test job. Cast through uint64 and compare against math.MaxUint32 so the constant compiles cleanly under both 32- and 64-bit int. Co-Authored-By: Claude Opus 4.7 (1M context) --- ecc/bn254/multiexp_glv.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ecc/bn254/multiexp_glv.go b/ecc/bn254/multiexp_glv.go index 2f0496d4ba..a4fab23855 100644 --- a/ecc/bn254/multiexp_glv.go +++ b/ecc/bn254/multiexp_glv.go @@ -5,6 +5,7 @@ package bn254 import ( "errors" + "math" "math/bits" "runtime" @@ -130,7 +131,7 @@ func (p *G1Jac) MultiExpGLVWide(points []G1Affine, scalars []fr.Element, config if nbPoints != len(scalars) { return nil, errors.New("len(points) != len(scalars)") } - if nbPoints > 1<<32-1 { + if uint64(nbPoints) > math.MaxUint32 { return nil, errors.New("invalid input: len(points) > math.MaxUint32") } if config.NbTasks <= 0 { From f973383271583679702384deee5ce09c3694027d Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 21 May 2026 14:07:51 -0400 Subject: [PATCH 24/33] refactor: address Ivo review --- ecc/bn254/multiexp_glv.go | 1077 ----------------- ecc/bn254/multiexp_glv_test.go | 176 --- ecc/kb8/fp/doc.go | 11 + ecc/kb8/fp/fp.go | 15 +- ecc/kb8/g1.go | 2 - ecc/kb8/g1_test.go | 2 - ecc/kb8/marshal.go | 2 - ecc/kb8/marshal_test.go | 2 - ecc/kb8/multiexp.go | 2 - ecc/kb8/multiexp_affine.go | 2 - ecc/kb8/multiexp_jacobian.go | 2 - ecc/kb8/multiexp_test.go | 2 - field/koalabear/element_exp.go | 47 +- field/koalabear/extensions/e2.go | 190 --- field/koalabear/extensions/e2_cbrt.go | 190 +++ field/koalabear/extensions/e2_cbrt_test.go | 45 + field/koalabear/extensions/e2_test.go | 39 - field/koalabear/extensions/e4.go | 155 --- field/koalabear/extensions/e4_cbrt.go | 149 +++ field/koalabear/extensions/e4_cbrt_test.go | 45 + field/koalabear/extensions/e4_test.go | 35 - field/koalabear/extensions/e8.go | 161 +-- field/koalabear/extensions/e8_cbrt.go | 146 +++ field/koalabear/extensions/e8_cbrt_test.go | 51 + field/koalabear/extensions/e8_test.go | 42 - internal/generator/addchain/e1c71c7 | Bin 0 -> 209 bytes internal/generator/config/curve.go | 8 +- internal/generator/config/fields.go | 12 +- .../generator/field/config/field_config.go | 34 +- .../generator/field/generator_extensions.go | 21 +- internal/generator/field/generator_field.go | 4 +- .../field/template/element/fixedexp.go.tmpl | 9 +- .../field/template/extensions/e2.go.tmpl | 3 + .../field/template/extensions/e8.go.tmpl | 14 +- internal/generator/fieldwrapper/generate.go | 8 +- .../fieldwrapper/template/doc.go.tmpl | 6 + .../fieldwrapper/template/fp.go.tmpl | 13 + internal/generator/main.go | 8 +- 38 files changed, 786 insertions(+), 1944 deletions(-) delete mode 100644 ecc/bn254/multiexp_glv.go delete mode 100644 ecc/bn254/multiexp_glv_test.go create mode 100644 ecc/kb8/fp/doc.go create mode 100644 field/koalabear/extensions/e2_cbrt.go create mode 100644 field/koalabear/extensions/e2_cbrt_test.go create mode 100644 field/koalabear/extensions/e4_cbrt.go create mode 100644 field/koalabear/extensions/e4_cbrt_test.go create mode 100644 field/koalabear/extensions/e8_cbrt.go create mode 100644 field/koalabear/extensions/e8_cbrt_test.go create mode 100644 internal/generator/addchain/e1c71c7 create mode 100644 internal/generator/fieldwrapper/template/doc.go.tmpl diff --git a/ecc/bn254/multiexp_glv.go b/ecc/bn254/multiexp_glv.go deleted file mode 100644 index a4fab23855..0000000000 --- a/ecc/bn254/multiexp_glv.go +++ /dev/null @@ -1,1077 +0,0 @@ -// Copyright 2020-2026 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -package bn254 - -import ( - "errors" - "math" - "math/bits" - "runtime" - - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/bn254/fp" - "github.com/consensys/gnark-crypto/ecc/bn254/fr" - "github.com/consensys/gnark-crypto/parallel" -) - -const ( - glvMSMG1Window = 16 - glvMSMG1Chunks = 8 - - glvMSMG1WideWindow = 19 - glvMSMG1WideChunks = 7 - glvMSMG1WideStripeBits = 12 - glvMSMG1WideStripeSize = 1 << glvMSMG1WideStripeBits - glvMSMG1WideStripeCount = 1 << (glvMSMG1WideWindow - 1 - glvMSMG1WideStripeBits) - glvMSMG1WideBatchSize = 640 -) - -type glvMSMG1Scalar struct { - k0 [glvMSMG1Chunks]int16 - k1 [glvMSMG1Chunks]int16 - extra0 int8 - extra1 int8 -} - -type glvMSMG1WideScalar struct { - k0 [glvMSMG1WideChunks]int32 - k1 [glvMSMG1WideChunks]int32 - extra0 int8 - extra1 int8 -} - -type glvMSMG1WideOp struct { - index uint32 - bucket uint32 - neg bool -} - -type batchOpG1AffineWide struct { - bucketID uint32 - point G1Affine -} - -var ( - glvMSMG1B1 = [6]uint64{ - 0x96ce4aece61f0339, - 0x2e3ff027efccd68a, - 0x8fa7d32d2fafba64, - 0x6eb9c714773a6ef2, - 0xd91d232ec7e0b3d7, - 0x0000000000000002, - } - glvMSMG1B2Abs = [7]uint64{ - 0xd073ced5f11aeea9, - 0x7abf2e6fc85f00fa, - 0x869375169b9bdffa, - 0xa5e38cfb5eaa26d9, - 0x7a7bd9d4391eb18d, - 0x4ccef014a773d2cf, - 0x0000000000000002, - } - glvMSMG1V11Abs = [2]uint64{0x8211bbeb7d4f1128, 0x6f4d8248eeb859fc} - glvMSMG1V20 = [2]uint64{0x0be4e1541221250b, 0x6f4d8248eeb859fd} -) - -const glvMSMG1V10 = uint64(0x89d3256894d213e3) - -// MultiExpGLV computes sum(scalars[i] * points[i]) using the BN254 G1 GLV -// endomorphism. It is an experimental opt-in MSM variant for large MSMs. -func (p *G1Affine) MultiExpGLV(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { - var _p G1Jac - if _, err := _p.MultiExpGLV(points, scalars, config); err != nil { - return nil, err - } - p.FromJacobian(&_p) - return p, nil -} - -// MultiExpGLV computes sum(scalars[i] * points[i]) using a deferred-phi GLV -// Pippenger variant. -// -// Each scalar is decomposed as k0 + lambda*k1. For each window we build two -// bucket tables over the original input points and apply phi only after the k1 -// bucket table has been reduced: -// -// sum k0_i*P_i + phi(sum k1_i*P_i) -// -// This avoids materializing phi(P_i) for every input point. -func (p *G1Jac) MultiExpGLV(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { - nbPoints := len(points) - if nbPoints != len(scalars) { - return nil, errors.New("len(points) != len(scalars)") - } - if config.NbTasks <= 0 { - config.NbTasks = runtime.NumCPU() * 2 - } else if config.NbTasks > 1024 { - return nil, errors.New("invalid config: config.NbTasks > 1024") - } - - digits := decomposeScalarsGLVG1(scalars, config.NbTasks) - return innerMSMGLVG1(p, points, digits), nil -} - -// MultiExpGLVWide computes sum(scalars[i] * points[i]) using the deferred-phi -// GLV MSM with 19-bit windows and a striped bucket backend. -func (p *G1Affine) MultiExpGLVWide(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { - var _p G1Jac - if _, err := _p.MultiExpGLVWide(points, scalars, config); err != nil { - return nil, err - } - p.FromJacobian(&_p) - return p, nil -} - -// MultiExpGLVWide computes sum(scalars[i] * points[i]) using 19-bit GLV -// component windows. The larger window reduces bucket insertions to 14N for -// BN254-sized scalars, while stripes avoid full 2^18 bucket-table allocation. -func (p *G1Jac) MultiExpGLVWide(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { - nbPoints := len(points) - if nbPoints != len(scalars) { - return nil, errors.New("len(points) != len(scalars)") - } - if uint64(nbPoints) > math.MaxUint32 { - return nil, errors.New("invalid input: len(points) > math.MaxUint32") - } - if config.NbTasks <= 0 { - config.NbTasks = runtime.NumCPU() * 2 - } else if config.NbTasks > 1024 { - return nil, errors.New("invalid config: config.NbTasks > 1024") - } - - digits := decomposeScalarsGLVG1Wide(scalars, config.NbTasks) - return innerMSMGLVG1Wide(p, points, digits), nil -} - -// MultiExpGLVWideDense computes sum(scalars[i] * points[i]) with the 19-bit -// deferred-phi GLV MSM and full dynamic bucket tables. It is intended for -// benchmarking the algorithmic tradeoff against the lower-memory striped path. -func (p *G1Affine) MultiExpGLVWideDense(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Affine, error) { - var _p G1Jac - if _, err := _p.MultiExpGLVWideDense(points, scalars, config); err != nil { - return nil, err - } - p.FromJacobian(&_p) - return p, nil -} - -// MultiExpGLVWideDense computes sum(scalars[i] * points[i]) with the 19-bit -// deferred-phi GLV MSM and full dynamic bucket tables. -func (p *G1Jac) MultiExpGLVWideDense(points []G1Affine, scalars []fr.Element, config ecc.MultiExpConfig) (*G1Jac, error) { - nbPoints := len(points) - if nbPoints != len(scalars) { - return nil, errors.New("len(points) != len(scalars)") - } - if config.NbTasks <= 0 { - config.NbTasks = runtime.NumCPU() * 2 - } else if config.NbTasks > 1024 { - return nil, errors.New("invalid config: config.NbTasks > 1024") - } - - digits := decomposeScalarsGLVG1Wide(scalars, config.NbTasks) - return innerMSMGLVG1WideDense(p, points, digits), nil -} - -func decomposeScalarsGLVG1(scalars []fr.Element, nbTasks int) []glvMSMG1Scalar { - digits := make([]glvMSMG1Scalar, len(scalars)) - - parallel.Execute(len(scalars), func(start, end int) { - for i := start; i < end; i++ { - if scalars[i].IsZero() { - continue - } - - splitScalarGLVG1Fixed(scalars[i].Bits(), &digits[i]) - } - }, nbTasks) - - return digits -} - -func decomposeScalarsGLVG1Wide(scalars []fr.Element, nbTasks int) []glvMSMG1WideScalar { - digits := make([]glvMSMG1WideScalar, len(scalars)) - - parallel.Execute(len(scalars), func(start, end int) { - for i := start; i < end; i++ { - if scalars[i].IsZero() { - continue - } - - splitScalarGLVG1FixedWide(scalars[i].Bits(), &digits[i]) - } - }, nbTasks) - - return digits -} - -func splitScalarGLVG1Fixed(s [4]uint64, out *glvMSMG1Scalar) { - sign0, mag0, sign1, mag1 := splitScalarGLVG1FixedComponents(s) - recodeSignedGLVComponentG1(sign0, mag0, &out.k0, &out.extra0) - recodeSignedGLVComponentG1(sign1, mag1, &out.k1, &out.extra1) -} - -func splitScalarGLVG1FixedWide(s [4]uint64, out *glvMSMG1WideScalar) { - sign0, mag0, sign1, mag1 := splitScalarGLVG1FixedComponents(s) - recodeSignedGLVComponentG1Wide(sign0, mag0, &out.k0, &out.extra0) - recodeSignedGLVComponentG1Wide(sign1, mag1, &out.k1, &out.extra1) -} - -func splitScalarGLVG1FixedComponents(s [4]uint64) (int, [4]uint64, int, [4]uint64) { - c1 := mul4x6High512(s, glvMSMG1B1) - c2 := mul4x7High512(s, glvMSMG1B2Abs) - - var v0 [4]uint64 - add4(&v0, mul1x1(c1, glvMSMG1V10)) - add4(&v0, mul2x2(c2, glvMSMG1V20)) - - sign0, mag0 := subSigned4(s, v0) - - a := mul1x2(c1, glvMSMG1V11Abs) - b := mul2x1(c2, glvMSMG1V10) - sign1, mag1 := subSigned4(a, b) - - return sign0, mag0, sign1, mag1 -} - -func recodeSignedGLVComponentG1(sign int, mag [4]uint64, digits *[glvMSMG1Chunks]int16, extra *int8) { - const ( - base = int64(1 << glvMSMG1Window) - minDigit = -int64(1 << (glvMSMG1Window - 1)) - maxDigit = int64(1<<(glvMSMG1Window-1)) - 1 - ) - - if sign == 0 { - return - } - - var carry int64 - for i := range glvMSMG1Chunks { - di := int64(window16U256(mag, i)) - if sign < 0 { - di = -di - } - di += carry - carry = 0 - - if di > maxDigit { - di -= base - carry = 1 - } else if di < minDigit { - di += base - carry = -1 - } - - digits[i] = int16(di) - } - - *extra = int8(carry) -} - -func recodeSignedGLVComponentG1Wide(sign int, mag [4]uint64, digits *[glvMSMG1WideChunks]int32, extra *int8) { - const ( - base = int64(1 << glvMSMG1WideWindow) - minDigit = -int64(1 << (glvMSMG1WideWindow - 1)) - maxDigit = int64(1<<(glvMSMG1WideWindow-1)) - 1 - ) - - if sign == 0 { - return - } - - var carry int64 - for i := range glvMSMG1WideChunks { - di := int64(windowU256(mag, i*glvMSMG1WideWindow, glvMSMG1WideWindow)) - if sign < 0 { - di = -di - } - di += carry - carry = 0 - - if di > maxDigit { - di -= base - carry = 1 - } else if di < minDigit { - di += base - carry = -1 - } - - digits[i] = int32(di) - } - - *extra = int8(carry) -} - -func window16U256(words [4]uint64, chunk int) uint64 { - const mask = uint64(1<> (uint(chunk%4) * glvMSMG1Window)) & mask -} - -func windowU256(words [4]uint64, bitOffset, width int) uint64 { - mask := uint64(1<> shift - if shift != 0 && wordIndex+1 < len(words) && int(shift)+width > 64 { - value |= words[wordIndex+1] << (64 - shift) - } - return value & mask -} - -func mul4x6High512(a [4]uint64, b [6]uint64) uint64 { - var product [10]uint64 - for i := range a { - for j := range b { - addMul64(product[:], i+j, a[i], b[j]) - } - } - return product[8] -} - -func mul4x7High512(a [4]uint64, b [7]uint64) [2]uint64 { - var product [11]uint64 - for i := range a { - for j := range b { - addMul64(product[:], i+j, a[i], b[j]) - } - } - return [2]uint64{product[8], product[9]} -} - -func mul1x1(a, b uint64) (out [4]uint64) { - out[1], out[0] = bits.Mul64(a, b) - return out -} - -func mul1x2(a uint64, b [2]uint64) (out [4]uint64) { - hi, lo := bits.Mul64(a, b[0]) - out[0] = lo - carry := hi - hi, lo = bits.Mul64(a, b[1]) - out[1], carry = bits.Add64(lo, carry, 0) - out[2], _ = bits.Add64(hi, 0, carry) - return out -} - -func mul2x1(a [2]uint64, b uint64) (out [4]uint64) { - hi, lo := bits.Mul64(a[0], b) - out[0] = lo - carry := hi - hi, lo = bits.Mul64(a[1], b) - out[1], carry = bits.Add64(lo, carry, 0) - out[2], _ = bits.Add64(hi, 0, carry) - return out -} - -func mul2x2(a, b [2]uint64) (out [4]uint64) { - for i := range a { - for j := range b { - addMul64(out[:], i+j, a[i], b[j]) - } - } - return out -} - -func addMul64(out []uint64, index int, a, b uint64) { - hi, lo := bits.Mul64(a, b) - - var carry uint64 - out[index], carry = bits.Add64(out[index], lo, 0) - hi, hiCarry := bits.Add64(hi, 0, carry) - out[index+1], carry = bits.Add64(out[index+1], hi, 0) - carry += hiCarry - - for i := index + 2; carry != 0 && i < len(out); i++ { - out[i], carry = bits.Add64(out[i], 0, carry) - } -} - -func add4(z *[4]uint64, x [4]uint64) { - var carry uint64 - for i := range 4 { - z[i], carry = bits.Add64(z[i], x[i], carry) - } -} - -func subSigned4(a, b [4]uint64) (int, [4]uint64) { - switch cmp4(a, b) { - case 1: - return 1, sub4(a, b) - case -1: - return -1, sub4(b, a) - default: - return 0, [4]uint64{} - } -} - -func cmp4(a, b [4]uint64) int { - for i := 3; i >= 0; i-- { - if a[i] > b[i] { - return 1 - } - if a[i] < b[i] { - return -1 - } - } - return 0 -} - -func sub4(a, b [4]uint64) (out [4]uint64) { - var borrow uint64 - for i := range 4 { - out[i], borrow = bits.Sub64(a[i], b[i], borrow) - } - return out -} - -func innerMSMGLVG1(p *G1Jac, points []G1Affine, digits []glvMSMG1Scalar) *G1Jac { - chunks := make([]chan g1JacExtended, glvMSMG1Chunks+1) - for i := range chunks { - chunks[i] = make(chan g1JacExtended, 1) - } - - for chunk := range glvMSMG1Chunks { - go processChunkG1GLVBatchAffineC16(chunk, chunks[chunk], points, digits) - } - go processExtraChunkG1GLV(chunks[glvMSMG1Chunks], points, digits) - - return msmReduceChunkG1Affine(p, glvMSMG1Window, chunks) -} - -func innerMSMGLVG1Wide(p *G1Jac, points []G1Affine, digits []glvMSMG1WideScalar) *G1Jac { - chunks := make([]chan g1JacExtended, glvMSMG1WideChunks+1) - for i := range chunks { - chunks[i] = make(chan g1JacExtended, 1) - } - - for chunk := range glvMSMG1WideChunks { - go processChunkG1GLVWideStriped(chunk, chunks[chunk], points, digits) - } - go processExtraChunkG1GLVWide(chunks[glvMSMG1WideChunks], points, digits) - - return msmReduceChunkG1Affine(p, glvMSMG1WideWindow, chunks) -} - -func innerMSMGLVG1WideDense(p *G1Jac, points []G1Affine, digits []glvMSMG1WideScalar) *G1Jac { - chunks := make([]chan g1JacExtended, glvMSMG1WideChunks+1) - for i := range chunks { - chunks[i] = make(chan g1JacExtended, 1) - } - - for chunk := range glvMSMG1WideChunks { - go processChunkG1GLVWideDense(chunk, chunks[chunk], points, digits) - } - go processExtraChunkG1GLVWide(chunks[glvMSMG1WideChunks], points, digits) - - return msmReduceChunkG1Affine(p, glvMSMG1WideWindow, chunks) -} - -type g1GLVBatchAffineC16 struct { - buckets bucketG1AffineC16 - bucketsJE bucketg1JacExtendedC16 - bucketIds bitSetC16 - cptAdd int - R ppG1AffineC16 - P pG1AffineC16 - queue qG1AffineC16 - qID int -} - -func (ctx *g1GLVBatchAffineC16) init() { - for i := range len(ctx.bucketsJE) { - ctx.bucketsJE[i].SetInfinity() - } -} - -func (ctx *g1GLVBatchAffineC16) executeAndReset() { - if ctx.cptAdd == 0 { - return - } - batchAddG1Affine[pG1AffineC16, ppG1AffineC16, cG1AffineC16](&ctx.R, &ctx.P, ctx.cptAdd) - ctx.bucketIds = bitSetC16{} - ctx.cptAdd = 0 -} - -func (ctx *g1GLVBatchAffineC16) addFromQueue(op batchOpG1Affine) { - BK := &ctx.buckets[op.bucketID] - if BK.IsInfinity() { - BK.Set(&op.point) - return - } - if BK.X.Equal(&op.point.X) { - if BK.Y.Equal(&op.point.Y) { - ctx.bucketsJE[op.bucketID].addMixed(&op.point) - return - } - BK.SetInfinity() - return - } - - ctx.bucketIds[op.bucketID] = true - ctx.R[ctx.cptAdd] = BK - ctx.P[ctx.cptAdd] = op.point - ctx.cptAdd++ -} - -func (ctx *g1GLVBatchAffineC16) add(bucketID uint16, point *G1Affine, isAdd bool) { - BK := &ctx.buckets[bucketID] - if BK.IsInfinity() { - if isAdd { - BK.Set(point) - } else { - BK.Neg(point) - } - return - } - if BK.X.Equal(&point.X) { - if BK.Y.Equal(&point.Y) { - if isAdd { - ctx.bucketsJE[bucketID].addMixed(point) - } else { - BK.SetInfinity() - } - return - } - if isAdd { - BK.SetInfinity() - } else { - ctx.bucketsJE[bucketID].subMixed(point) - } - return - } - - ctx.bucketIds[bucketID] = true - ctx.R[ctx.cptAdd] = BK - if isAdd { - ctx.P[ctx.cptAdd].Set(point) - } else { - ctx.P[ctx.cptAdd].Neg(point) - } - ctx.cptAdd++ -} - -func (ctx *g1GLVBatchAffineC16) flushQueue() { - for i := range ctx.qID { - ctx.bucketsJE[ctx.queue[i].bucketID].addMixed(&ctx.queue[i].point) - } - ctx.qID = 0 -} - -func (ctx *g1GLVBatchAffineC16) processTopQueue() { - for i := ctx.qID - 1; i >= 0; i-- { - if ctx.bucketIds[ctx.queue[i].bucketID] { - return - } - ctx.addFromQueue(ctx.queue[i]) - ctx.qID-- - } -} - -func (ctx *g1GLVBatchAffineC16) addDigit(digit int16, point *G1Affine) { - if digit == 0 || point.IsInfinity() { - return - } - - d := int32(digit) - isAdd := d > 0 - if !isAdd { - d = -d - } - bucketID := uint16(d - 1) - - if ctx.bucketIds[bucketID] { - ctx.queue[ctx.qID].bucketID = bucketID - if isAdd { - ctx.queue[ctx.qID].point.Set(point) - } else { - ctx.queue[ctx.qID].point.Neg(point) - } - ctx.qID++ - if ctx.qID == len(ctx.queue)-1 { - ctx.flushQueue() - } - return - } - - ctx.add(bucketID, point, isAdd) - if ctx.cptAdd == len(ctx.P) { - ctx.executeAndReset() - ctx.processTopQueue() - } -} - -func (ctx *g1GLVBatchAffineC16) reduce(total *g1JacExtended) { - ctx.executeAndReset() - ctx.flushQueue() - - var runningSum g1JacExtended - runningSum.SetInfinity() - total.SetInfinity() - for k := len(ctx.buckets) - 1; k >= 0; k-- { - runningSum.addMixed(&ctx.buckets[k]) - if !ctx.bucketsJE[k].IsInfinity() { - runningSum.add(&ctx.bucketsJE[k]) - } - total.add(&runningSum) - } -} - -func processChunkG1GLVBatchAffineC16(chunk int, chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1Scalar) { - var bucket0, bucket1 g1GLVBatchAffineC16 - bucket0.init() - bucket1.init() - - for i := range points { - bucket0.addDigit(digits[i].k0[chunk], &points[i]) - bucket1.addDigit(digits[i].k1[chunk], &points[i]) - } - - var total0, total1 g1JacExtended - bucket0.reduce(&total0) - bucket1.reduce(&total1) - phiG1JacExtended(&total1) - total0.add(&total1) - chRes <- total0 -} - -func processExtraChunkG1GLV(chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1Scalar) { - var total0, total1 g1JacExtended - total0.SetInfinity() - total1.SetInfinity() - - for i := range points { - switch digits[i].extra0 { - case 1: - total0.addMixed(&points[i]) - case -1: - total0.subMixed(&points[i]) - } - switch digits[i].extra1 { - case 1: - total1.addMixed(&points[i]) - case -1: - total1.subMixed(&points[i]) - } - } - - phiG1JacExtended(&total1) - total0.add(&total1) - chRes <- total0 -} - -func phiG1JacExtended(p *g1JacExtended) { - if p.IsInfinity() { - return - } - p.X.Mul(&p.X, &thirdRootOneG1) -} - -type g1GLVStripeBatchAffine struct { - buckets []G1Affine - bucketsJE []g1JacExtended - bucketStamps []uint32 - stamp uint32 - touched []uint32 - touchedMarks []bool - cptAdd int - R []*G1Affine - P []G1Affine - queue []batchOpG1AffineWide - qID int - lambda []fp.Element - lambdain []fp.Element -} - -func newG1GLVBatchAffineDynamic(size int) g1GLVStripeBatchAffine { - return g1GLVStripeBatchAffine{ - buckets: make([]G1Affine, size), - bucketsJE: make([]g1JacExtended, size), - bucketStamps: make([]uint32, size), - stamp: 1, - touched: make([]uint32, 0, min(size, glvMSMG1WideStripeSize)), - touchedMarks: make([]bool, size), - R: make([]*G1Affine, glvMSMG1WideBatchSize), - P: make([]G1Affine, glvMSMG1WideBatchSize), - queue: make([]batchOpG1AffineWide, glvMSMG1WideBatchSize), - lambda: make([]fp.Element, glvMSMG1WideBatchSize), - lambdain: make([]fp.Element, glvMSMG1WideBatchSize), - } -} - -func newG1GLVStripeBatchAffine() g1GLVStripeBatchAffine { - return newG1GLVBatchAffineDynamic(glvMSMG1WideStripeSize) -} - -func (ctx *g1GLVStripeBatchAffine) resetBatch() { - ctx.stamp++ - if ctx.stamp == 0 { - clear(ctx.bucketStamps) - ctx.stamp = 1 - } - ctx.cptAdd = 0 -} - -func (ctx *g1GLVStripeBatchAffine) touch(bucketID uint32) { - if !ctx.touchedMarks[bucketID] { - ctx.touchedMarks[bucketID] = true - ctx.touched = append(ctx.touched, bucketID) - } -} - -func (ctx *g1GLVStripeBatchAffine) resetStripe() { - for _, bucketID := range ctx.touched { - ctx.buckets[bucketID].SetInfinity() - ctx.bucketsJE[bucketID] = g1JacExtended{} - ctx.touchedMarks[bucketID] = false - } - ctx.touched = ctx.touched[:0] - ctx.qID = 0 - ctx.resetBatch() -} - -func (ctx *g1GLVStripeBatchAffine) executeAndReset() { - if ctx.cptAdd == 0 { - return - } - batchAddG1AffineDynamic(ctx.R, ctx.P, ctx.lambda, ctx.lambdain, ctx.cptAdd) - ctx.resetBatch() -} - -func (ctx *g1GLVStripeBatchAffine) addFromQueue(op batchOpG1AffineWide) { - BK := &ctx.buckets[op.bucketID] - ctx.touch(op.bucketID) - if BK.IsInfinity() { - BK.Set(&op.point) - return - } - if BK.X.Equal(&op.point.X) { - if BK.Y.Equal(&op.point.Y) { - ctx.bucketsJE[op.bucketID].addMixed(&op.point) - return - } - BK.SetInfinity() - return - } - - ctx.bucketStamps[op.bucketID] = ctx.stamp - ctx.R[ctx.cptAdd] = BK - ctx.P[ctx.cptAdd] = op.point - ctx.cptAdd++ -} - -func (ctx *g1GLVStripeBatchAffine) add(bucketID uint32, point *G1Affine, isAdd bool) { - BK := &ctx.buckets[bucketID] - ctx.touch(bucketID) - if BK.IsInfinity() { - if isAdd { - BK.Set(point) - } else { - BK.Neg(point) - } - return - } - if BK.X.Equal(&point.X) { - if BK.Y.Equal(&point.Y) { - if isAdd { - ctx.bucketsJE[bucketID].addMixed(point) - } else { - BK.SetInfinity() - } - return - } - if isAdd { - BK.SetInfinity() - } else { - ctx.bucketsJE[bucketID].subMixed(point) - } - return - } - - ctx.bucketStamps[bucketID] = ctx.stamp - ctx.R[ctx.cptAdd] = BK - if isAdd { - ctx.P[ctx.cptAdd].Set(point) - } else { - ctx.P[ctx.cptAdd].Neg(point) - } - ctx.cptAdd++ -} - -func (ctx *g1GLVStripeBatchAffine) flushQueue() { - for i := range ctx.qID { - ctx.touch(ctx.queue[i].bucketID) - ctx.bucketsJE[ctx.queue[i].bucketID].addMixed(&ctx.queue[i].point) - } - ctx.qID = 0 -} - -func (ctx *g1GLVStripeBatchAffine) processTopQueue() { - for i := ctx.qID - 1; i >= 0; i-- { - if ctx.bucketStamps[ctx.queue[i].bucketID] == ctx.stamp { - return - } - ctx.addFromQueue(ctx.queue[i]) - ctx.qID-- - } -} - -func (ctx *g1GLVStripeBatchAffine) addOp(op glvMSMG1WideOp, points []G1Affine) { - point := &points[op.index] - if point.IsInfinity() { - return - } - - if ctx.bucketStamps[op.bucket] == ctx.stamp { - ctx.queue[ctx.qID].bucketID = op.bucket - if op.neg { - ctx.queue[ctx.qID].point.Neg(point) - } else { - ctx.queue[ctx.qID].point.Set(point) - } - ctx.qID++ - if ctx.qID == len(ctx.queue)-1 { - ctx.flushQueue() - } - return - } - - ctx.add(op.bucket, point, !op.neg) - if ctx.cptAdd == len(ctx.P) { - ctx.executeAndReset() - ctx.processTopQueue() - } -} - -func (ctx *g1GLVStripeBatchAffine) reduceInto(runningSum, total *g1JacExtended) { - ctx.executeAndReset() - ctx.flushQueue() - for k := len(ctx.buckets) - 1; k >= 0; k-- { - runningSum.addMixed(&ctx.buckets[k]) - if !ctx.bucketsJE[k].IsInfinity() { - runningSum.add(&ctx.bucketsJE[k]) - } - total.add(runningSum) - } - ctx.resetStripe() -} - -func (ctx *g1GLVStripeBatchAffine) reduce(total *g1JacExtended) { - var runningSum g1JacExtended - runningSum.SetInfinity() - total.SetInfinity() - ctx.reduceInto(&runningSum, total) -} - -func processChunkG1GLVWideStriped(chunk int, chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1WideScalar) { - var counts0, counts1 [glvMSMG1WideStripeCount]int - for i := range points { - if points[i].IsInfinity() { - continue - } - if d := digits[i].k0[chunk]; d != 0 { - counts0[stripeIDG1Wide(d)]++ - } - if d := digits[i].k1[chunk]; d != 0 { - counts1[stripeIDG1Wide(d)]++ - } - } - - offsets0, totalOps0 := prefixWideCountsG1(counts0) - offsets1, totalOps1 := prefixWideCountsG1(counts1) - ops0 := make([]glvMSMG1WideOp, totalOps0) - ops1 := make([]glvMSMG1WideOp, totalOps1) - cursor0, cursor1 := offsets0, offsets1 - - for i := range points { - if points[i].IsInfinity() { - continue - } - if d := digits[i].k0[chunk]; d != 0 { - stripe, bucket, neg := splitWideDigitG1(d) - ops0[cursor0[stripe]] = glvMSMG1WideOp{index: uint32(i), bucket: bucket, neg: neg} - cursor0[stripe]++ - } - if d := digits[i].k1[chunk]; d != 0 { - stripe, bucket, neg := splitWideDigitG1(d) - ops1[cursor1[stripe]] = glvMSMG1WideOp{index: uint32(i), bucket: bucket, neg: neg} - cursor1[stripe]++ - } - } - - total0 := reduceWideOpsG1(points, ops0, offsets0, counts0) - total1 := reduceWideOpsG1(points, ops1, offsets1, counts1) - phiG1JacExtended(&total1) - total0.add(&total1) - chRes <- total0 -} - -func processChunkG1GLVWideDense(chunk int, chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1WideScalar) { - bucket0 := newG1GLVBatchAffineDynamic(1 << (glvMSMG1WideWindow - 1)) - bucket1 := newG1GLVBatchAffineDynamic(1 << (glvMSMG1WideWindow - 1)) - - for i := range points { - if points[i].IsInfinity() { - continue - } - bucket0.addWideDigit(digits[i].k0[chunk], &points[i]) - bucket1.addWideDigit(digits[i].k1[chunk], &points[i]) - } - - var total0, total1 g1JacExtended - bucket0.reduce(&total0) - bucket1.reduce(&total1) - phiG1JacExtended(&total1) - total0.add(&total1) - chRes <- total0 -} - -func (ctx *g1GLVStripeBatchAffine) addWideDigit(digit int32, point *G1Affine) { - if digit == 0 { - return - } - neg := digit < 0 - if neg { - digit = -digit - } - bucketID := uint32(digit) - 1 - - if ctx.bucketStamps[bucketID] == ctx.stamp { - ctx.queue[ctx.qID].bucketID = bucketID - if neg { - ctx.queue[ctx.qID].point.Neg(point) - } else { - ctx.queue[ctx.qID].point.Set(point) - } - ctx.qID++ - if ctx.qID == len(ctx.queue)-1 { - ctx.flushQueue() - } - return - } - - ctx.add(bucketID, point, !neg) - if ctx.cptAdd == len(ctx.P) { - ctx.executeAndReset() - ctx.processTopQueue() - } -} - -func stripeIDG1Wide(digit int32) int { - if digit < 0 { - digit = -digit - } - return int((uint32(digit) - 1) >> glvMSMG1WideStripeBits) -} - -func splitWideDigitG1(digit int32) (int, uint32, bool) { - neg := digit < 0 - if neg { - digit = -digit - } - bucketID := uint32(digit) - 1 - stripe := int(bucketID >> glvMSMG1WideStripeBits) - bucket := bucketID & (glvMSMG1WideStripeSize - 1) - return stripe, bucket, neg -} - -func prefixWideCountsG1(counts [glvMSMG1WideStripeCount]int) ([glvMSMG1WideStripeCount]int, int) { - var offsets [glvMSMG1WideStripeCount]int - total := 0 - for i, count := range counts { - offsets[i] = total - total += count - } - return offsets, total -} - -func reduceWideOpsG1(points []G1Affine, ops []glvMSMG1WideOp, offsets, counts [glvMSMG1WideStripeCount]int) g1JacExtended { - ctx := newG1GLVStripeBatchAffine() - var runningSum, total g1JacExtended - runningSum.SetInfinity() - total.SetInfinity() - - for stripe := glvMSMG1WideStripeCount - 1; stripe >= 0; stripe-- { - if counts[stripe] == 0 { - if !runningSum.IsInfinity() { - for range glvMSMG1WideStripeSize { - total.add(&runningSum) - } - } - continue - } - - start := offsets[stripe] - end := start + counts[stripe] - for _, op := range ops[start:end] { - ctx.addOp(op, points) - } - ctx.reduceInto(&runningSum, &total) - } - - return total -} - -func processExtraChunkG1GLVWide(chRes chan<- g1JacExtended, points []G1Affine, digits []glvMSMG1WideScalar) { - var total0, total1 g1JacExtended - total0.SetInfinity() - total1.SetInfinity() - - for i := range points { - switch digits[i].extra0 { - case 1: - total0.addMixed(&points[i]) - case -1: - total0.subMixed(&points[i]) - } - switch digits[i].extra1 { - case 1: - total1.addMixed(&points[i]) - case -1: - total1.subMixed(&points[i]) - } - } - - phiG1JacExtended(&total1) - total0.add(&total1) - chRes <- total0 -} - -func batchAddG1AffineDynamic(R []*G1Affine, P []G1Affine, lambda, lambdain []fp.Element, batchSize int) { - for j := range batchSize { - lambdain[j].Sub(&P[j].X, &R[j].X) - } - - var accumulator fp.Element - lambda[0].SetOne() - accumulator.Set(&lambdain[0]) - - for i := 1; i < batchSize; i++ { - lambda[i] = accumulator - accumulator.Mul(&accumulator, &lambdain[i]) - } - - accumulator.Inverse(&accumulator) - - for i := batchSize - 1; i > 0; i-- { - lambda[i].Mul(&lambda[i], &accumulator) - accumulator.Mul(&accumulator, &lambdain[i]) - } - lambda[0].Set(&accumulator) - - var t fp.Element - var Q G1Affine - for j := range batchSize { - t.Sub(&P[j].Y, &R[j].Y) - lambda[j].Mul(&lambda[j], &t) - - Q.X.Square(&lambda[j]) - Q.X.Sub(&Q.X, &R[j].X) - Q.X.Sub(&Q.X, &P[j].X) - - t.Sub(&R[j].X, &Q.X) - Q.Y.Mul(&lambda[j], &t) - Q.Y.Sub(&Q.Y, &R[j].Y) - - R[j].Set(&Q) - } -} diff --git a/ecc/bn254/multiexp_glv_test.go b/ecc/bn254/multiexp_glv_test.go deleted file mode 100644 index 54c75b68fe..0000000000 --- a/ecc/bn254/multiexp_glv_test.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2020-2026 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -package bn254 - -import ( - "fmt" - "math/big" - "math/bits" - "testing" - - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/bn254/fr" -) - -func TestMultiExpG1GLV(t *testing.T) { - sizes := []int{0, 1, 2, 7, 73, 512, 1 << 12} - - for _, size := range sizes { - t.Run(fmt.Sprintf("%d points", size), func(t *testing.T) { - points := make([]G1Affine, size) - scalars := make([]fr.Element, size) - - var g G1Jac - g.Set(&g1Gen) - for i := range points { - points[i].FromJacobian(&g) - g.AddAssign(&g1Gen) - } - fillBenchScalars(scalars) - if size > 7 { - points[3].SetInfinity() - scalars[5].SetZero() - } - - var expected, got, gotWide, gotWideDense G1Jac - if _, err := expected.MultiExp(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { - t.Fatal(err) - } - if _, err := got.MultiExpGLV(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { - t.Fatal(err) - } - if !expected.Equal(&got) { - t.Fatalf("GLV MSM mismatch for %d points", size) - } - if _, err := gotWide.MultiExpGLVWide(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { - t.Fatal(err) - } - if !expected.Equal(&gotWide) { - t.Fatalf("wide GLV MSM mismatch for %d points", size) - } - if _, err := gotWideDense.MultiExpGLVWideDense(points, scalars, ecc.MultiExpConfig{NbTasks: 2}); err != nil { - t.Fatal(err) - } - if !expected.Equal(&gotWideDense) { - t.Fatalf("wide dense GLV MSM mismatch for %d points", size) - } - }) - } -} - -func TestSplitScalarGLVG1Fixed(t *testing.T) { - scalars := make([]fr.Element, 1024) - fillBenchScalars(scalars) - scalars = append(scalars, fr.Element{}, fr.NewElement(1)) - - for i := range scalars { - var got glvMSMG1Scalar - splitScalarGLVG1Fixed(scalars[i].Bits(), &got) - - var scalar big.Int - scalars[i].BigInt(&scalar) - want := ecc.SplitScalar(&scalar, &glvBasis) - - gotK0 := reconstructGLVG1Component(got.k0, got.extra0) - gotK1 := reconstructGLVG1Component(got.k1, got.extra1) - if gotK0.Cmp(&want[0]) != 0 || gotK1.Cmp(&want[1]) != 0 { - t.Fatalf("split mismatch at index %d", i) - } - - var gotWide glvMSMG1WideScalar - splitScalarGLVG1FixedWide(scalars[i].Bits(), &gotWide) - gotWideK0 := reconstructGLVG1WideComponent(gotWide.k0, gotWide.extra0) - gotWideK1 := reconstructGLVG1WideComponent(gotWide.k1, gotWide.extra1) - if gotWideK0.Cmp(&want[0]) != 0 || gotWideK1.Cmp(&want[1]) != 0 { - t.Fatalf("wide split mismatch at index %d", i) - } - } -} - -func reconstructGLVG1Component(digits [glvMSMG1Chunks]int16, extra int8) big.Int { - var res, term big.Int - for i, digit := range digits { - if digit == 0 { - continue - } - term.SetInt64(int64(digit)) - term.Lsh(&term, uint(i*glvMSMG1Window)) - res.Add(&res, &term) - } - if extra != 0 { - term.SetInt64(int64(extra)) - term.Lsh(&term, glvMSMG1Chunks*glvMSMG1Window) - res.Add(&res, &term) - } - return res -} - -func reconstructGLVG1WideComponent(digits [glvMSMG1WideChunks]int32, extra int8) big.Int { - var res, term big.Int - for i, digit := range digits { - if digit == 0 { - continue - } - term.SetInt64(int64(digit)) - term.Lsh(&term, uint(i*glvMSMG1WideWindow)) - res.Add(&res, &term) - } - if extra != 0 { - term.SetInt64(int64(extra)) - term.Lsh(&term, glvMSMG1WideChunks*glvMSMG1WideWindow) - res.Add(&res, &term) - } - return res -} - -func BenchmarkMultiExpG1GLV(b *testing.B) { - const ( - pow = (bits.UintSize / 2) - (bits.UintSize / 8) - nbSamples = 1 << pow - ) - - var ( - samplePoints [nbSamples]G1Affine - sampleScalars [nbSamples]fr.Element - ) - - fillBenchScalars(sampleScalars[:]) - fillBenchBasesG1(samplePoints[:]) - - for i := 16; i <= pow; i += 2 { - using := 1 << i - - b.Run(fmt.Sprintf("baseline/%d points", using), func(b *testing.B) { - var testPoint G1Affine - b.ResetTimer() - for range b.N { - testPoint.MultiExp(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) - } - }) - - b.Run(fmt.Sprintf("glv/%d points", using), func(b *testing.B) { - var testPoint G1Affine - b.ResetTimer() - for range b.N { - testPoint.MultiExpGLV(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) - } - }) - - b.Run(fmt.Sprintf("glv-wide/%d points", using), func(b *testing.B) { - var testPoint G1Affine - b.ResetTimer() - for range b.N { - testPoint.MultiExpGLVWide(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) - } - }) - - b.Run(fmt.Sprintf("glv-wide-dense/%d points", using), func(b *testing.B) { - var testPoint G1Affine - b.ResetTimer() - for range b.N { - testPoint.MultiExpGLVWideDense(samplePoints[:using], sampleScalars[:using], ecc.MultiExpConfig{}) - } - }) - } -} diff --git a/ecc/kb8/fp/doc.go b/ecc/kb8/fp/doc.go new file mode 100644 index 0000000000..93b14330bf --- /dev/null +++ b/ecc/kb8/fp/doc.go @@ -0,0 +1,11 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +// Package fp aliases github.com/consensys/gnark-crypto/field/koalabear for curves whose base field is +// implemented in an existing top-level field package. +// +// It exists so packages under ecc/kb8 can use the same local fp import +// shape as curves with generated base fields. +package fp diff --git a/ecc/kb8/fp/fp.go b/ecc/kb8/fp/fp.go index cc13bc693d..fea51da117 100644 --- a/ecc/kb8/fp/fp.go +++ b/ecc/kb8/fp/fp.go @@ -11,37 +11,50 @@ import ( koalabear "github.com/consensys/gnark-crypto/field/koalabear" ) +// Element is the base field element type for this curve. type Element = koalabear.Element + +// Vector is a slice of base field elements. type Vector = koalabear.Vector const ( - Bits = koalabear.Bits + // Bits is the bit length of the field modulus. + Bits = koalabear.Bits + // Bytes is the byte length needed to encode one field element. Bytes = koalabear.Bytes + // Limbs is the number of machine words used by one field element. Limbs = koalabear.Limbs ) +// BigEndian is the big-endian byte order implementation. var BigEndian = koalabear.BigEndian +// Modulus returns a copy of the field modulus. func Modulus() *big.Int { return koalabear.Modulus() } +// One returns the field element one. func One() Element { return koalabear.One() } +// BatchInvert returns a new slice with every non-zero element inverted. func BatchInvert(a []Element) []Element { return koalabear.BatchInvert(a) } +// Generator returns a field generator for the requested multiplicative subgroup. func Generator(m uint64) (Element, error) { return koalabear.Generator(m) } +// Butterfly computes the in-place butterfly operation on a and b. func Butterfly(a, b *Element) { koalabear.Butterfly(a, b) } +// MulBy3 multiplies x by 3 in place. func MulBy3(x *Element) { koalabear.MulBy3(x) } diff --git a/ecc/kb8/g1.go b/ecc/kb8/g1.go index 3efdf6f735..13657aaec3 100644 --- a/ecc/kb8/g1.go +++ b/ecc/kb8/g1.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 import ( diff --git a/ecc/kb8/g1_test.go b/ecc/kb8/g1_test.go index 312892229e..50fe725d0d 100644 --- a/ecc/kb8/g1_test.go +++ b/ecc/kb8/g1_test.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 import ( diff --git a/ecc/kb8/marshal.go b/ecc/kb8/marshal.go index 4931920ed7..187729ab81 100644 --- a/ecc/kb8/marshal.go +++ b/ecc/kb8/marshal.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 import ( diff --git a/ecc/kb8/marshal_test.go b/ecc/kb8/marshal_test.go index 7a2612c691..6845aec2df 100644 --- a/ecc/kb8/marshal_test.go +++ b/ecc/kb8/marshal_test.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 import ( diff --git a/ecc/kb8/multiexp.go b/ecc/kb8/multiexp.go index 6baf9f7964..4dc10b8d43 100644 --- a/ecc/kb8/multiexp.go +++ b/ecc/kb8/multiexp.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 import ( diff --git a/ecc/kb8/multiexp_affine.go b/ecc/kb8/multiexp_affine.go index 914123bb98..d8a59e11c7 100644 --- a/ecc/kb8/multiexp_affine.go +++ b/ecc/kb8/multiexp_affine.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 import "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" diff --git a/ecc/kb8/multiexp_jacobian.go b/ecc/kb8/multiexp_jacobian.go index 5c21c342f2..fb4bf80082 100644 --- a/ecc/kb8/multiexp_jacobian.go +++ b/ecc/kb8/multiexp_jacobian.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 func processChunkG1Jacobian[B ibg1JacExtended](chunk uint64, diff --git a/ecc/kb8/multiexp_test.go b/ecc/kb8/multiexp_test.go index 8fdcb56f1b..5a577074a2 100644 --- a/ecc/kb8/multiexp_test.go +++ b/ecc/kb8/multiexp_test.go @@ -1,8 +1,6 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Code generated by consensys/gnark-crypto DO NOT EDIT - package kb8 import ( diff --git a/field/koalabear/element_exp.go b/field/koalabear/element_exp.go index 256225a975..58c0b0c2bd 100644 --- a/field/koalabear/element_exp.go +++ b/field/koalabear/element_exp.go @@ -131,7 +131,8 @@ func (z *Element) ExpByCbrt2q1o3(x Element) *Element { } // ExpByCbrtHelperQMinus2Div9 is equivalent to z.Exp(x, e1c71c7). -// It raises x to the (q-2)/9 power using a shorter addition chain. +// It raises x to the (q-2)/9 power using an addition chain. +// This helper is used by extension-field cube-root computations. // // uses github.com/mmcloughlin/addchain v0.4.0 to generate a shorter addition chain func (z *Element) ExpByCbrtHelperQMinus2Div9(x Element) *Element { @@ -145,48 +146,48 @@ func (z *Element) ExpByCbrtHelperQMinus2Div9(x Element) *Element { // i11 = _111 + i10 // return ((i10 + i11) << 6 + _111) << 12 + i11 // - // Operations: 30 squares 6 multiplies - var t0, t1, t2 Element + // Operations: 26 squares 6 multiplies + var t0, t1 Element // Step 1: z = x^0x2 z.Square(&x) - // Step 2: t0 = x^0x3 - t0.Mul(&x, z) + // Step 2: z = x^0x3 + z.Mul(&x, z) - // Step 3: t1 = x^0x6 - t1.Square(&t0) + // Step 3: z = x^0x6 + z.Square(z) // Step 4: t0 = x^0x7 - t0.Mul(&x, &t1) + t0.Mul(&x, z) - // Step 10: t2 = x^0x1c0 - t2.Set(&t0) - for range 6 { - t2.Square(&t2) + // Step 10: t1 = x^0x1c0 + t1.Square(&t0) + for s := 1; s < 6; s++ { + t1.Square(&t1) } - // Step 11: t1 = x^0x1c7 - t1.Mul(&t0, &t2) + // Step 11: z = x^0x1c7 + z.Mul(&t0, &t1) - // Step 12: z = x^0x387 - z.Mul(&t2, &t1) + // Step 12: t1 = x^0x387 + t1.Mul(&t1, z) - // Step 18: z = x^0xe1c0 + // Step 18: t1 = x^0xe1c0 for range 6 { - z.Square(z) + t1.Square(&t1) } - // Step 19: z = x^0xe1c7 - z.Mul(z, &t0) + // Step 19: t0 = x^0xe1c7 + t0.Mul(&t0, &t1) - // Step 31: z = x^0xe1c7000 + // Step 31: t0 = x^0xe1c7000 for range 12 { - z.Square(z) + t0.Square(&t0) } // Step 32: z = x^0xe1c71c7 - z.Mul(z, &t1) + z.Mul(z, &t0) return z } diff --git a/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index 4ebba767dd..3b80405fc3 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -11,41 +11,6 @@ import ( fr "github.com/consensys/gnark-crypto/field/koalabear" ) -var ( - cbrtFpOne fr.Element - cbrtFpTwo fr.Element - cbrtFpThree fr.Element - cbrtFpThreeInv fr.Element - cbrtE2One E2 - cbrtE2Omega E2 - cbrtE2Omega2 E2 - cbrtE2NRInv E2 -) - -var cbrtE2MuLucasExponent uint64 = 473490319 - -func init() { - cbrtFpOne.SetOne() - cbrtFpTwo.SetUint64(2) - cbrtFpThree.SetUint64(3) - cbrtFpThreeInv.Inverse(&cbrtFpThree) - - cbrtE2One.SetOne() - // cbrtE2NRInv = cbrtE2One * u^{-1} = (0, 1/3) since u² = 3 in the koalabear - // quadratic non-residue convention. Inlined to avoid depending on the removed - // E2.MulByNonResidueInv method (master renamed it; the cbrt stack still needs - // this exact value). - cbrtE2NRInv.A0.SetZero() - cbrtE2NRInv.A1.Set(&cbrtFpThreeInv) - var sqrtMinusThree E2 - sqrtMinusThree.A0.Neg(&cbrtFpThree) - sqrtMinusThree.Sqrt(&sqrtMinusThree) - cbrtE2Omega.Sub(&sqrtMinusThree, &cbrtE2One) - cbrtE2Omega.A0.Halve() - cbrtE2Omega.A1.Halve() - cbrtE2Omega2.Square(&cbrtE2Omega) -} - // E2 is a degree two finite field extension of fr.Element type E2 struct { A0, A1 fr.Element @@ -254,161 +219,6 @@ func (z *E2) Sqrt(x *E2) *E2 { return z } -// Cbrt sets z to the cube root of x and returns z. -// It returns nil if x is not a cubic residue. -func (z *E2) Cbrt(x *E2) *E2 { - if x.A1.IsZero() { - z.A0.Cbrt(&x.A0) - z.A1.SetZero() - return z - } - - if x.A0.IsZero() { - z.A0.SetZero() - z.A1.Mul(&x.A1, &cbrtFpThreeInv) - z.A1.Cbrt(&z.A1) - return cbrtVerifyE2(z, x) - } - - var x0sq, x1sq, betaX1sq, norm fr.Element - x0sq.Square(&x.A0) - x1sq.Square(&x.A1) - betaX1sq.Mul(&x1sq, &cbrtFpThree) - norm.Sub(&x0sq, &betaX1sq) - - m, normInv, deltaInv, ok := cbrtAndNormInverseE2(&norm, &x0sq, &x1sq) - if !ok { - return nil - } - - var halfTau, tau fr.Element - halfTau.Add(&x0sq, &betaX1sq) - halfTau.Mul(&halfTau, &normInv) - tau.Double(&halfTau) - - Te, Te1 := lucasV2Fp(&tau) - - var x0x1, imY fr.Element - x0x1.Mul(&x.A0, &x.A1) - imY.Double(&x0x1).Neg(&imY) - imY.Mul(&imY, &normInv) - - var WA0, WA1 fr.Element - WA0.Mul(&halfTau, &Te) - WA0.Sub(&Te1, &WA0) - WA1.Mul(&imY, &Te) - - var sIm, k fr.Element - sIm.Double(&imY) - k.Mul(&sIm, &deltaInv) - - var gamma0, gamma1 fr.Element - gamma0.Mul(&WA1, &k).Mul(&gamma0, &cbrtFpThree) - gamma1.Mul(&WA0, &k) - - var mInv fr.Element - mInv.Square(&m).Mul(&mInv, &normInv) - - var t1, t2 fr.Element - t1.Mul(&x.A0, &gamma0) - t2.Mul(&x.A1, &gamma1).Mul(&t2, &cbrtFpThree) - z.A0.Sub(&t1, &t2).Mul(&z.A0, &mInv) - t1.Mul(&x.A1, &gamma0) - t2.Mul(&x.A0, &gamma1) - z.A1.Sub(&t1, &t2).Mul(&z.A1, &mInv) - if out := cbrtVerifyE2(z, x); out != nil { - return out - } - - var sigma fr.Element - sigma.Set(&Te) - var d0, d1, d0d1, d0d1Inv fr.Element - d0.Sub(&sigma, &cbrtFpOne) - d0.Mul(&m, &d0) - d1.Add(&sigma, &cbrtFpOne) - d1.Mul(&m, &d1) - d0d1.Mul(&d0, &d1) - if d0d1.IsZero() { - return nil - } - d0d1Inv.Inverse(&d0d1) - - z.A0.Mul(&d1, &d0d1Inv).Mul(&z.A0, &x.A0) - z.A1.Mul(&d0, &d0d1Inv).Mul(&z.A1, &x.A1) - return cbrtVerifyE2(z, x) -} - -func cbrtAndNormInverseE2(norm, x0sq, x1sq *fr.Element) (m, normInv, deltaInv fr.Element, ok bool) { - // Hamburg trick for KoalaBear (p ≡ 2 mod 9): - // with w = U^3 * norm and t = w^((p-2)/9), we have - // w^(-1) = t^9 and cbrt(w) = w * t^6. - var U, x0x1, U2, U3, w fr.Element - x0x1.Mul(x0sq, x1sq) - U.Mul(&x0x1, norm) - U.Double(&U).Double(&U) - U.Double(&U) - U.Mul(&U, &cbrtFpThree) - U2.Square(&U) - U3.Mul(&U2, &U) - w.Mul(&U3, norm) - - var t, t2, t4, t6, t8, t9 fr.Element - t.ExpByCbrtHelperQMinus2Div9(w) - t2.Square(&t) - t4.Square(&t2) - t6.Mul(&t4, &t2) - t8.Square(&t4) - t9.Mul(&t8, &t) - - var cbrtW, UInv fr.Element - cbrtW.Mul(&w, &t6) - UInv.Mul(&U2, norm).Mul(&UInv, &t9) - m.Mul(&cbrtW, &UInv) - normInv.Mul(&U3, &t9) - - var check fr.Element - check.Square(&m).Mul(&check, &m) - if !check.Equal(norm) { - return m, normInv, deltaInv, false - } - - var norm2, norm3 fr.Element - norm2.Square(norm) - norm3.Mul(&norm2, norm) - deltaInv.Mul(&norm3, &UInv) - return m, normInv, deltaInv, true -} - -func cbrtVerifyE2(z, x *E2) *E2 { - var check E2 - check.Square(z).Mul(&check, z) - if !check.Equal(x) { - return nil - } - return z -} - -func lucasV2Fp(alpha *fr.Element) (fr.Element, fr.Element) { - var v0, v1, prod fr.Element - v0.Set(alpha) - v1.Square(alpha).Sub(&v1, &cbrtFpTwo) - for i := 27; i >= 1; i-- { - bit := (cbrtE2MuLucasExponent >> uint(i)) & 1 - prod.Mul(&v0, &v1).Sub(&prod, alpha) - if bit == 0 { - v1.Set(&prod) - v0.Square(&v0).Sub(&v0, &cbrtFpTwo) - } else { - v0.Set(&prod) - v1.Square(&v1).Sub(&v1, &cbrtFpTwo) - } - } - var te, te1 fr.Element - te.Mul(&v0, &v1).Sub(&te, alpha) - te1.Square(&v1).Sub(&te1, &cbrtFpTwo) - return te, te1 -} - // BatchInvertE2 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. // diff --git a/field/koalabear/extensions/e2_cbrt.go b/field/koalabear/extensions/e2_cbrt.go new file mode 100644 index 0000000000..2bc25b2546 --- /dev/null +++ b/field/koalabear/extensions/e2_cbrt.go @@ -0,0 +1,190 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package extensions + +import fr "github.com/consensys/gnark-crypto/field/koalabear" + +var ( + cbrtFpOne fr.Element + cbrtFpTwo fr.Element + cbrtFpThree fr.Element + cbrtFpThreeInv fr.Element + cbrtE2One E2 + cbrtE2Omega E2 + cbrtE2Omega2 E2 + cbrtE2NRInv E2 +) + +const cbrtE2MuLucasExponent uint64 = 473490319 + +func init() { + cbrtFpOne.SetOne() + cbrtFpTwo.SetUint64(2) + cbrtFpThree.SetUint64(3) + cbrtFpThreeInv.Inverse(&cbrtFpThree) + + cbrtE2One.SetOne() + cbrtE2NRInv.A0.SetZero() + cbrtE2NRInv.A1.Set(&cbrtFpThreeInv) + + var sqrtMinusThree E2 + sqrtMinusThree.A0.Neg(&cbrtFpThree) + sqrtMinusThree.Sqrt(&sqrtMinusThree) + cbrtE2Omega.Sub(&sqrtMinusThree, &cbrtE2One) + cbrtE2Omega.A0.Halve() + cbrtE2Omega.A1.Halve() + cbrtE2Omega2.Square(&cbrtE2Omega) +} + +// Cbrt sets z to the cube root of x and returns z. +// It returns nil if x is not a cubic residue. +func (z *E2) Cbrt(x *E2) *E2 { + if x.A1.IsZero() { + z.A0.Cbrt(&x.A0) + z.A1.SetZero() + return z + } + + if x.A0.IsZero() { + z.A0.SetZero() + z.A1.Mul(&x.A1, &cbrtFpThreeInv) + z.A1.Cbrt(&z.A1) + return cbrtVerifyE2(z, x) + } + + var x0sq, x1sq, betaX1sq, norm fr.Element + x0sq.Square(&x.A0) + x1sq.Square(&x.A1) + betaX1sq.Mul(&x1sq, &cbrtFpThree) + norm.Sub(&x0sq, &betaX1sq) + + m, normInv, deltaInv, ok := cbrtAndNormInverseE2(&norm, &x0sq, &x1sq) + if !ok { + return nil + } + + var halfTau, tau fr.Element + halfTau.Add(&x0sq, &betaX1sq) + halfTau.Mul(&halfTau, &normInv) + tau.Double(&halfTau) + + te, te1 := lucasV2Fp(&tau) + + var x0x1, imY fr.Element + x0x1.Mul(&x.A0, &x.A1) + imY.Double(&x0x1).Neg(&imY) + imY.Mul(&imY, &normInv) + + var wa0, wa1 fr.Element + wa0.Mul(&halfTau, &te) + wa0.Sub(&te1, &wa0) + wa1.Mul(&imY, &te) + + var sIm, k fr.Element + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) + + var gamma0, gamma1 fr.Element + gamma0.Mul(&wa1, &k).Mul(&gamma0, &cbrtFpThree) + gamma1.Mul(&wa0, &k) + + var mInv fr.Element + mInv.Square(&m).Mul(&mInv, &normInv) + + var t1, t2 fr.Element + t1.Mul(&x.A0, &gamma0) + t2.Mul(&x.A1, &gamma1).Mul(&t2, &cbrtFpThree) + z.A0.Sub(&t1, &t2).Mul(&z.A0, &mInv) + t1.Mul(&x.A1, &gamma0) + t2.Mul(&x.A0, &gamma1) + z.A1.Sub(&t1, &t2).Mul(&z.A1, &mInv) + if out := cbrtVerifyE2(z, x); out != nil { + return out + } + + var sigma fr.Element + sigma.Set(&te) + var d0, d1, d0d1, d0d1Inv fr.Element + d0.Sub(&sigma, &cbrtFpOne) + d0.Mul(&m, &d0) + d1.Add(&sigma, &cbrtFpOne) + d1.Mul(&m, &d1) + d0d1.Mul(&d0, &d1) + if d0d1.IsZero() { + return nil + } + d0d1Inv.Inverse(&d0d1) + + z.A0.Mul(&d1, &d0d1Inv).Mul(&z.A0, &x.A0) + z.A1.Mul(&d0, &d0d1Inv).Mul(&z.A1, &x.A1) + return cbrtVerifyE2(z, x) +} + +func cbrtAndNormInverseE2(norm, x0sq, x1sq *fr.Element) (m, normInv, deltaInv fr.Element, ok bool) { + var U, x0x1, U2, U3, w fr.Element + x0x1.Mul(x0sq, x1sq) + U.Mul(&x0x1, norm) + U.Double(&U).Double(&U) + U.Double(&U) + U.Mul(&U, &cbrtFpThree) + U2.Square(&U) + U3.Mul(&U2, &U) + w.Mul(&U3, norm) + + var t, t2, t4, t6, t8, t9 fr.Element + t.ExpByCbrtHelperQMinus2Div9(w) + t2.Square(&t) + t4.Square(&t2) + t6.Mul(&t4, &t2) + t8.Square(&t4) + t9.Mul(&t8, &t) + + var cbrtW, UInv fr.Element + cbrtW.Mul(&w, &t6) + UInv.Mul(&U2, norm).Mul(&UInv, &t9) + m.Mul(&cbrtW, &UInv) + normInv.Mul(&U3, &t9) + + var check fr.Element + check.Square(&m).Mul(&check, &m) + if !check.Equal(norm) { + return m, normInv, deltaInv, false + } + + var norm2, norm3 fr.Element + norm2.Square(norm) + norm3.Mul(&norm2, norm) + deltaInv.Mul(&norm3, &UInv) + return m, normInv, deltaInv, true +} + +func cbrtVerifyE2(z, x *E2) *E2 { + var check E2 + check.Square(z).Mul(&check, z) + if !check.Equal(x) { + return nil + } + return z +} + +func lucasV2Fp(alpha *fr.Element) (fr.Element, fr.Element) { + var v0, v1, prod fr.Element + v0.Set(alpha) + v1.Square(alpha).Sub(&v1, &cbrtFpTwo) + for i := 27; i >= 1; i-- { + bit := (cbrtE2MuLucasExponent >> uint(i)) & 1 + prod.Mul(&v0, &v1).Sub(&prod, alpha) + if bit == 0 { + v1.Set(&prod) + v0.Square(&v0).Sub(&v0, &cbrtFpTwo) + } else { + v0.Set(&prod) + v1.Square(&v1).Sub(&v1, &cbrtFpTwo) + } + } + var te, te1 fr.Element + te.Mul(&v0, &v1).Sub(&te, alpha) + te1.Square(&v1).Sub(&te1, &cbrtFpTwo) + return te, te1 +} diff --git a/field/koalabear/extensions/e2_cbrt_test.go b/field/koalabear/extensions/e2_cbrt_test.go new file mode 100644 index 0000000000..fe2d039159 --- /dev/null +++ b/field/koalabear/extensions/e2_cbrt_test.go @@ -0,0 +1,45 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package extensions + +import "testing" + +func TestE2CbrtOnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check E2 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + if got.Cbrt(&x) == nil { + t.Fatal("expected cubic residue to have a cube root") + } + check.Square(&got).Mul(&check, &got) + if !check.Equal(&x) { + t.Fatal("returned cube root does not verify") + } + } +} + +func TestE2CbrtRejectsNonResidues(t *testing.T) { + var x, got E2 + for i := 0; i < 256; i++ { + x.MustSetRandom() + if got.Cbrt(&x) == nil { + return + } + } + t.Fatal("failed to find an E2 non-cube in 256 samples") +} + +func BenchmarkE2Cbrt(b *testing.B) { + var a, x E2 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + var z E2 + b.ResetTimer() + for i := 0; i < b.N; i++ { + if z.Cbrt(&x) == nil { + b.Fatal("expected cubic residue to have a cube root") + } + } +} diff --git a/field/koalabear/extensions/e2_test.go b/field/koalabear/extensions/e2_test.go index 58cc492000..df35d04717 100644 --- a/field/koalabear/extensions/e2_test.go +++ b/field/koalabear/extensions/e2_test.go @@ -533,42 +533,3 @@ func genE2() gopter.Gen { return E2{A0: values[0].(fr.Element), A1: values[1].(fr.Element)} }) } - -func TestE2CbrtOnCubicResidues(t *testing.T) { - for i := 0; i < 128; i++ { - var a, x, got, check E2 - a.MustSetRandom() - x.Square(&a).Mul(&x, &a) - if got.Cbrt(&x) == nil { - t.Fatal("expected cubic residue to have a cube root") - } - check.Square(&got).Mul(&check, &got) - if !check.Equal(&x) { - t.Fatal("returned cube root does not verify") - } - } -} - -func TestE2CbrtRejectsNonResidues(t *testing.T) { - var x, got E2 - for i := 0; i < 256; i++ { - x.MustSetRandom() - if got.Cbrt(&x) == nil { - return - } - } - t.Fatal("failed to find an E2 non-cube in 256 samples") -} - -func BenchmarkE2Cbrt(b *testing.B) { - var a, x E2 - a.MustSetRandom() - x.Square(&a).Mul(&x, &a) - var z E2 - b.ResetTimer() - for i := 0; i < b.N; i++ { - if z.Cbrt(&x) == nil { - b.Fatal("expected cubic residue to have a cube root") - } - } -} diff --git a/field/koalabear/extensions/e4.go b/field/koalabear/extensions/e4.go index 7f47f24d6e..6f7e8729a2 100644 --- a/field/koalabear/extensions/e4.go +++ b/field/koalabear/extensions/e4.go @@ -12,31 +12,6 @@ import ( fr "github.com/consensys/gnark-crypto/field/koalabear" ) -var ( - cbrtE4One E4 - cbrtE4NRInv E4 - cbrtE4Omega E4 - cbrtE4Omega2 E4 - cbrtE4Exponent big.Int -) - -var cbrtE4LucasExponent = [1]uint64{ - 1513303301209194497, -} - -func init() { - cbrtE4One.SetOne() - cbrtE4NRInv.B1.SetOne() - cbrtE4NRInv.Inverse(&cbrtE4NRInv) - cbrtE4Omega.B0 = cbrtE2Omega - cbrtE4Omega2.Square(&cbrtE4Omega) - cbrtE4Exponent.Exp(fr.Modulus(), big.NewInt(4), nil) - cbrtE4Exponent.Sub(&cbrtE4Exponent, big.NewInt(1)) - cbrtE4Exponent.Div(&cbrtE4Exponent, big.NewInt(3)) - three := new(big.Int).SetUint64(3) - cbrtE4Exponent.ModInverse(three, &cbrtE4Exponent) -} - // q + r'.r = 1, i.e., qInvNeg = - q⁻¹ mod r // used for Montgomery reduction const qInvNeg = 2130706431 @@ -452,136 +427,6 @@ func (z *E4) Sqrt(x *E4) *E4 { return z } -// Cbrt sets z to the cube root of x and returns z. -// It returns nil if x is not a cubic residue. -func (z *E4) Cbrt(x *E4) *E4 { - if x.B1.IsZero() { - if z.B0.Cbrt(&x.B0) == nil { - return nil - } - z.B1.SetZero() - return z - } - - if x.B0.IsZero() { - var y E4 - var x1OverNR E2 - x1OverNR.Mul(&x.B1, &cbrtE2NRInv) - if y.B1.Cbrt(&x1OverNR) == nil { - return nil - } - y.B0.SetZero() - return cbrtVerifyAndAdjustE4(z.Set(&y), x) - } - - var x0sq, x1sq, betaX1sq, norm E2 - x0sq.Square(&x.B0) - x1sq.Square(&x.B1) - betaX1sq.MulByQuadraticNonResidue(&x1sq) - norm.Sub(&x0sq, &betaX1sq) - - var m, normInv E2 - if m.Cbrt(&norm) == nil { - return nil - } - normInv.Inverse(&norm) - - var halfTau, tau E2 - halfTau.Add(&x0sq, &betaX1sq) - halfTau.Mul(&halfTau, &normInv) - tau.Double(&halfTau) - - var x0x1, imY E2 - x0x1.Mul(&x.B0, &x.B1) - imY.Double(&x0x1) - imY.Mul(&imY, &normInv) - - te, te1 := lucasV2E2Cbrt(&tau) - - var wa0, wa1 E2 - wa0.Mul(&halfTau, &te) - wa0.Sub(&te1, &wa0) - wa1.Mul(&imY, &te) - - var delta, deltaInv, sIm, k E2 - delta.Square(&tau).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One) - if delta.IsZero() { - return nil - } - deltaInv.Inverse(&delta) - sIm.Double(&imY) - k.Mul(&sIm, &deltaInv) - - var gamma0, gamma1 E2 - gamma0.Mul(&wa1, &k) - gamma0.MulByQuadraticNonResidue(&gamma0) - gamma1.Mul(&wa0, &k) - - var mInv E2 - mInv.Square(&m).Mul(&mInv, &normInv) - - var y E4 - var t1, t2 E2 - t1.Mul(&x.B0, &gamma0) - t2.Mul(&x.B1, &gamma1) - t2.MulByQuadraticNonResidue(&t2) - y.B0.Sub(&t1, &t2).Mul(&y.B0, &mInv) - t1.Mul(&x.B1, &gamma0) - t2.Mul(&x.B0, &gamma1) - y.B1.Sub(&t1, &t2).Mul(&y.B1, &mInv) - return cbrtVerifyAndAdjustE4(z.Set(&y), x) -} - -func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { - var check E4 - check.Square(z).Mul(&check, z) - if check.Equal(x) { - return z - } - - var y E4 - y.B0.Mul(&z.B0, &cbrtE2Omega) - y.B1.Mul(&z.B1, &cbrtE2Omega) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - y.B0.Mul(&z.B0, &cbrtE2Omega2) - y.B1.Mul(&z.B1, &cbrtE2Omega2) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - return nil -} - -func lucasV2E2Cbrt(alpha *E2) (E2, E2) { - var v0, v1, prod E2 - var two E2 - two.A0.SetUint64(2) - v0.Set(alpha) - v1.Square(alpha).Sub(&v1, &two) - for i := 59; i >= 1; i-- { - bit := (cbrtE4LucasExponent[0] >> uint(i)) & 1 - if bit == 0 { - prod.Mul(&v0, &v1).Sub(&prod, alpha) - v1.Set(&prod) - v0.Square(&v0).Sub(&v0, &two) - } else { - prod.Mul(&v0, &v1).Sub(&prod, alpha) - v0.Set(&prod) - v1.Square(&v1).Sub(&v1, &two) - } - } - - var te, te1 E2 - te.Mul(&v0, &v1).Sub(&te, alpha) - te1.Square(&v1).Sub(&te1, &two) - return te, te1 -} - // BatchInvertE4 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. // diff --git a/field/koalabear/extensions/e4_cbrt.go b/field/koalabear/extensions/e4_cbrt.go new file mode 100644 index 0000000000..5c8d143718 --- /dev/null +++ b/field/koalabear/extensions/e4_cbrt.go @@ -0,0 +1,149 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package extensions + +var ( + cbrtE4One E4 + cbrtE4NRInv E4 +) + +var cbrtE4LucasExponent = [1]uint64{ + 1513303301209194497, +} + +func init() { + cbrtE4One.SetOne() + cbrtE4NRInv.B1.SetOne() + cbrtE4NRInv.Inverse(&cbrtE4NRInv) +} + +// Cbrt sets z to the cube root of x and returns z. +// It returns nil if x is not a cubic residue. +func (z *E4) Cbrt(x *E4) *E4 { + if x.B1.IsZero() { + if z.B0.Cbrt(&x.B0) == nil { + return nil + } + z.B1.SetZero() + return z + } + + if x.B0.IsZero() { + var y E4 + var x1OverNR E2 + x1OverNR.Mul(&x.B1, &cbrtE2NRInv) + if y.B1.Cbrt(&x1OverNR) == nil { + return nil + } + y.B0.SetZero() + return cbrtVerifyAndAdjustE4(z.Set(&y), x) + } + + var x0sq, x1sq, betaX1sq, norm E2 + x0sq.Square(&x.B0) + x1sq.Square(&x.B1) + betaX1sq.MulByQuadraticNonResidue(&x1sq) + norm.Sub(&x0sq, &betaX1sq) + + var m, normInv E2 + if m.Cbrt(&norm) == nil { + return nil + } + normInv.Inverse(&norm) + + var halfTau, tau E2 + halfTau.Add(&x0sq, &betaX1sq) + halfTau.Mul(&halfTau, &normInv) + tau.Double(&halfTau) + + var x0x1, imY E2 + x0x1.Mul(&x.B0, &x.B1) + imY.Double(&x0x1) + imY.Mul(&imY, &normInv) + + te, te1 := lucasV2E2Cbrt(&tau) + + var wa0, wa1 E2 + wa0.Mul(&halfTau, &te) + wa0.Sub(&te1, &wa0) + wa1.Mul(&imY, &te) + + var delta, deltaInv, sIm, k E2 + delta.Square(&tau).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One).Sub(&delta, &cbrtE2One) + if delta.IsZero() { + return nil + } + deltaInv.Inverse(&delta) + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) + + var gamma0, gamma1 E2 + gamma0.Mul(&wa1, &k) + gamma0.MulByQuadraticNonResidue(&gamma0) + gamma1.Mul(&wa0, &k) + + var mInv E2 + mInv.Square(&m).Mul(&mInv, &normInv) + + var y E4 + var t1, t2 E2 + t1.Mul(&x.B0, &gamma0) + t2.Mul(&x.B1, &gamma1) + t2.MulByQuadraticNonResidue(&t2) + y.B0.Sub(&t1, &t2).Mul(&y.B0, &mInv) + t1.Mul(&x.B1, &gamma0) + t2.Mul(&x.B0, &gamma1) + y.B1.Sub(&t1, &t2).Mul(&y.B1, &mInv) + return cbrtVerifyAndAdjustE4(z.Set(&y), x) +} + +func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { + var check E4 + check.Square(z).Mul(&check, z) + if check.Equal(x) { + return z + } + + var y E4 + y.B0.Mul(&z.B0, &cbrtE2Omega) + y.B1.Mul(&z.B1, &cbrtE2Omega) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + y.B0.Mul(&z.B0, &cbrtE2Omega2) + y.B1.Mul(&z.B1, &cbrtE2Omega2) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + return nil +} + +func lucasV2E2Cbrt(alpha *E2) (E2, E2) { + var v0, v1, prod E2 + var two E2 + two.A0.SetUint64(2) + v0.Set(alpha) + v1.Square(alpha).Sub(&v1, &two) + for i := 59; i >= 1; i-- { + bit := (cbrtE4LucasExponent[0] >> uint(i)) & 1 + if bit == 0 { + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v1.Set(&prod) + v0.Square(&v0).Sub(&v0, &two) + } else { + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v0.Set(&prod) + v1.Square(&v1).Sub(&v1, &two) + } + } + + var te, te1 E2 + te.Mul(&v0, &v1).Sub(&te, alpha) + te1.Square(&v1).Sub(&te1, &two) + return te, te1 +} diff --git a/field/koalabear/extensions/e4_cbrt_test.go b/field/koalabear/extensions/e4_cbrt_test.go new file mode 100644 index 0000000000..51835ec513 --- /dev/null +++ b/field/koalabear/extensions/e4_cbrt_test.go @@ -0,0 +1,45 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package extensions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestE4CbrtOnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check E4 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + require.NotNil(t, got.Cbrt(&x)) + check.Square(&got).Mul(&check, &got) + require.True(t, check.Equal(&x)) + } +} + +func TestE4CbrtRejectsNonResidues(t *testing.T) { + var x, got E4 + for i := 0; i < 256; i++ { + x.MustSetRandom() + if got.Cbrt(&x) == nil { + return + } + } + t.Fatal("failed to find an E4 non-cube in 256 samples") +} + +func BenchmarkE4Cbrt(b *testing.B) { + var a, x E4 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + var z E4 + b.ResetTimer() + for i := 0; i < b.N; i++ { + if z.Cbrt(&x) == nil { + b.Fatal("expected cubic residue to have a cube root") + } + } +} diff --git a/field/koalabear/extensions/e4_test.go b/field/koalabear/extensions/e4_test.go index f0d554ecc5..fe658295a3 100644 --- a/field/koalabear/extensions/e4_test.go +++ b/field/koalabear/extensions/e4_test.go @@ -1025,38 +1025,3 @@ func genFrVector(size int) gopter.Gen { return gopter.NewGenResult(v, gopter.NoShrinker) } } - -func TestE4CbrtOnCubicResidues(t *testing.T) { - for i := 0; i < 128; i++ { - var a, x, got, check E4 - a.MustSetRandom() - x.Square(&a).Mul(&x, &a) - require.NotNil(t, got.Cbrt(&x)) - check.Square(&got).Mul(&check, &got) - require.True(t, check.Equal(&x)) - } -} - -func TestE4CbrtRejectsNonResidues(t *testing.T) { - var x, got E4 - for i := 0; i < 256; i++ { - x.MustSetRandom() - if got.Cbrt(&x) == nil { - return - } - } - t.Fatal("failed to find an E4 non-cube in 256 samples") -} - -func BenchmarkE4Cbrt(b *testing.B) { - var a, x E4 - a.MustSetRandom() - x.Square(&a).Mul(&x, &a) - var z E4 - b.ResetTimer() - for i := 0; i < b.N; i++ { - if z.Cbrt(&x) == nil { - b.Fatal("expected cubic residue to have a cube root") - } - } -} diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index e221831604..63634506ac 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -12,32 +12,6 @@ import ( fr "github.com/consensys/gnark-crypto/field/koalabear" ) -var ( - cbrtE8One E8 - cbrtE8NRInv E8 - cbrtE8Omega E8 - cbrtE8Omega2 E8 - cbrtE8Exponent big.Int -) - -var cbrtE8LucasExponent = [2]uint64{ - 2930905110336765953, - 372437575807401643, -} - -func init() { - cbrtE8One.SetOne() - cbrtE8NRInv.C1.SetOne() - cbrtE8NRInv.Inverse(&cbrtE8NRInv) - cbrtE8Omega.C0.B0 = cbrtE2Omega - cbrtE8Omega2.Square(&cbrtE8Omega) - cbrtE8Exponent.Exp(fr.Modulus(), big.NewInt(8), nil) - cbrtE8Exponent.Sub(&cbrtE8Exponent, big.NewInt(1)) - cbrtE8Exponent.Div(&cbrtE8Exponent, big.NewInt(3)) - three := new(big.Int).SetUint64(3) - cbrtE8Exponent.ModInverse(three, &cbrtE8Exponent) -} - // E8 is a degree two finite field extension of E4. type E8 struct { C0, C1 E4 @@ -187,7 +161,13 @@ func (z *E8) Mul(x, y *E8) *E8 { // Square sets z=x*x in E8 and returns z. func (z *E8) Square(x *E8) *E8 { - return z.Mul(x, x) + var a, b, c E4 + a.Square(&x.C0) + b.Square(&x.C1).MulByQuadraticNonResidue(&b) + c.Mul(&x.C0, &x.C1).Double(&c) + z.C0.Add(&a, &b) + z.C1.Set(&c) + return z } // Inverse sets z to the inverse of x in E8 and returns z. @@ -296,133 +276,6 @@ func (z *E8) Sqrt(x *E8) *E8 { return z } -// Cbrt sets z to the cube root of x and returns z. -// It returns nil if x is not a cubic residue. -func (z *E8) Cbrt(x *E8) *E8 { - if x.C1.IsZero() { - if z.C0.Cbrt(&x.C0) == nil { - return nil - } - z.C1.SetZero() - return z - } - - if x.C0.IsZero() { - var y E8 - var x1OverNR E4 - x1OverNR.Mul(&x.C1, &cbrtE4NRInv) - if y.C1.Cbrt(&x1OverNR) == nil { - return nil - } - y.C0.SetZero() - return cbrtVerifyAndAdjustE8(z.Set(&y), x) - } - - var x0sq, x1sq, betaX1sq, norm E4 - x0sq.Square(&x.C0) - x1sq.Square(&x.C1) - betaX1sq.MulByQuadraticNonResidue(&x1sq) - norm.Sub(&x0sq, &betaX1sq) - - var m, normInv E4 - if m.Cbrt(&norm) == nil { - return nil - } - normInv.Inverse(&norm) - - var halfTau, tau E4 - halfTau.Add(&x0sq, &betaX1sq) - halfTau.Mul(&halfTau, &normInv) - tau.Double(&halfTau) - - var x0x1, imY E4 - x0x1.Mul(&x.C0, &x.C1) - imY.Double(&x0x1) - imY.Mul(&imY, &normInv) - - te, te1 := lucasV2E4Cbrt(&tau) - - var wa0, wa1 E4 - wa0.Mul(&halfTau, &te) - wa0.Sub(&te1, &wa0) - wa1.Mul(&imY, &te) - - var delta, deltaInv, sIm, k E4 - delta.Square(&tau).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One) - if delta.IsZero() { - return nil - } - deltaInv.Inverse(&delta) - sIm.Double(&imY) - k.Mul(&sIm, &deltaInv) - - var gamma0, gamma1 E4 - gamma0.Mul(&wa1, &k) - gamma0.MulByQuadraticNonResidue(&gamma0) - gamma1.Mul(&wa0, &k) - - var mInv E4 - mInv.Square(&m).Mul(&mInv, &normInv) - - var y E8 - var t1, t2 E4 - t1.Mul(&x.C0, &gamma0) - t2.Mul(&x.C1, &gamma1) - t2.MulByQuadraticNonResidue(&t2) - y.C0.Sub(&t1, &t2).Mul(&y.C0, &mInv) - t1.Mul(&x.C1, &gamma0) - t2.Mul(&x.C0, &gamma1) - y.C1.Sub(&t1, &t2).Mul(&y.C1, &mInv) - return cbrtVerifyAndAdjustE8(z.Set(&y), x) -} - -func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { - var check, y E8 - check.Square(z).Mul(&check, z) - if check.Equal(x) { - return z - } - - y.Mul(z, &cbrtE8Omega) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - y.Mul(z, &cbrtE8Omega2) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - return nil -} - -func lucasV2E4Cbrt(alpha *E4) (E4, E4) { - var v0, v1, prod E4 - var two E4 - two.B0.A0.SetUint64(2) - v0.Set(alpha) - v1.Square(alpha).Sub(&v1, &two) - for i := 121; i >= 1; i-- { - bit := (cbrtE8LucasExponent[i/64] >> uint(i%64)) & 1 - if bit == 0 { - prod.Mul(&v0, &v1).Sub(&prod, alpha) - v1.Set(&prod) - v0.Square(&v0).Sub(&v0, &two) - } else { - prod.Mul(&v0, &v1).Sub(&prod, alpha) - v0.Set(&prod) - v1.Square(&v1).Sub(&v1, &two) - } - } - - var te, te1 E4 - te.Mul(&v0, &v1).Sub(&te, alpha) - te1.Square(&v1).Sub(&te1, &two) - return te, te1 -} - // BatchInvertE8 returns a new slice with every element in a inverted. func BatchInvertE8(a []E8) []E8 { res := make([]E8, len(a)) diff --git a/field/koalabear/extensions/e8_cbrt.go b/field/koalabear/extensions/e8_cbrt.go new file mode 100644 index 0000000000..7313182ee0 --- /dev/null +++ b/field/koalabear/extensions/e8_cbrt.go @@ -0,0 +1,146 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package extensions + +var ( + cbrtE8Omega E8 + cbrtE8Omega2 E8 +) + +var cbrtE8LucasExponent = [2]uint64{ + 2930905110336765953, + 372437575807401643, +} + +func init() { + cbrtE8Omega.C0.B0 = cbrtE2Omega + cbrtE8Omega2.Square(&cbrtE8Omega) +} + +// Cbrt sets z to the cube root of x and returns z. +// It returns nil if x is not a cubic residue. +func (z *E8) Cbrt(x *E8) *E8 { + if x.C1.IsZero() { + if z.C0.Cbrt(&x.C0) == nil { + return nil + } + z.C1.SetZero() + return z + } + + if x.C0.IsZero() { + var y E8 + var x1OverNR E4 + x1OverNR.Mul(&x.C1, &cbrtE4NRInv) + if y.C1.Cbrt(&x1OverNR) == nil { + return nil + } + y.C0.SetZero() + return cbrtVerifyAndAdjustE8(z.Set(&y), x) + } + + var x0sq, x1sq, betaX1sq, norm E4 + x0sq.Square(&x.C0) + x1sq.Square(&x.C1) + betaX1sq.MulByQuadraticNonResidue(&x1sq) + norm.Sub(&x0sq, &betaX1sq) + + var m, normInv E4 + if m.Cbrt(&norm) == nil { + return nil + } + normInv.Inverse(&norm) + + var halfTau, tau E4 + halfTau.Add(&x0sq, &betaX1sq) + halfTau.Mul(&halfTau, &normInv) + tau.Double(&halfTau) + + var x0x1, imY E4 + x0x1.Mul(&x.C0, &x.C1) + imY.Double(&x0x1) + imY.Mul(&imY, &normInv) + + te, te1 := lucasV2E4Cbrt(&tau) + + var wa0, wa1 E4 + wa0.Mul(&halfTau, &te) + wa0.Sub(&te1, &wa0) + wa1.Mul(&imY, &te) + + var delta, deltaInv, sIm, k E4 + delta.Square(&tau).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One).Sub(&delta, &cbrtE4One) + if delta.IsZero() { + return nil + } + deltaInv.Inverse(&delta) + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) + + var gamma0, gamma1 E4 + gamma0.Mul(&wa1, &k) + gamma0.MulByQuadraticNonResidue(&gamma0) + gamma1.Mul(&wa0, &k) + + var mInv E4 + mInv.Square(&m).Mul(&mInv, &normInv) + + var y E8 + var t1, t2 E4 + t1.Mul(&x.C0, &gamma0) + t2.Mul(&x.C1, &gamma1) + t2.MulByQuadraticNonResidue(&t2) + y.C0.Sub(&t1, &t2).Mul(&y.C0, &mInv) + t1.Mul(&x.C1, &gamma0) + t2.Mul(&x.C0, &gamma1) + y.C1.Sub(&t1, &t2).Mul(&y.C1, &mInv) + return cbrtVerifyAndAdjustE8(z.Set(&y), x) +} + +func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { + var check, y E8 + check.Square(z).Mul(&check, z) + if check.Equal(x) { + return z + } + + y.Mul(z, &cbrtE8Omega) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + y.Mul(z, &cbrtE8Omega2) + check.Square(&y).Mul(&check, &y) + if check.Equal(x) { + return z.Set(&y) + } + + return nil +} + +func lucasV2E4Cbrt(alpha *E4) (E4, E4) { + var v0, v1, prod E4 + var two E4 + two.B0.A0.SetUint64(2) + v0.Set(alpha) + v1.Square(alpha).Sub(&v1, &two) + for i := 121; i >= 1; i-- { + bit := (cbrtE8LucasExponent[i/64] >> uint(i%64)) & 1 + if bit == 0 { + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v1.Set(&prod) + v0.Square(&v0).Sub(&v0, &two) + } else { + prod.Mul(&v0, &v1).Sub(&prod, alpha) + v0.Set(&prod) + v1.Square(&v1).Sub(&v1, &two) + } + } + + var te, te1 E4 + te.Mul(&v0, &v1).Sub(&te, alpha) + te1.Square(&v1).Sub(&te1, &two) + return te, te1 +} diff --git a/field/koalabear/extensions/e8_cbrt_test.go b/field/koalabear/extensions/e8_cbrt_test.go new file mode 100644 index 0000000000..917f4c2ee1 --- /dev/null +++ b/field/koalabear/extensions/e8_cbrt_test.go @@ -0,0 +1,51 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package extensions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestE8CbrtZero(t *testing.T) { + var zero, got E8 + require.NotNil(t, got.Cbrt(&zero)) + require.True(t, got.IsZero()) +} + +func TestE8CbrtOnCubicResidues(t *testing.T) { + for i := 0; i < 128; i++ { + var a, x, got, check E8 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + require.NotNil(t, got.Cbrt(&x)) + check.Square(&got).Mul(&check, &got) + require.True(t, check.Equal(&x)) + } +} + +func TestE8CbrtRejectsNonResidues(t *testing.T) { + var x, got E8 + for i := 0; i < 256; i++ { + x.MustSetRandom() + if got.Cbrt(&x) == nil { + return + } + } + t.Fatal("failed to find an E8 non-cube in 256 samples") +} + +func BenchmarkE8Cbrt(b *testing.B) { + var a, x E8 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + var z E8 + b.ResetTimer() + for i := 0; i < b.N; i++ { + if z.Cbrt(&x) == nil { + b.Fatal("expected cubic residue to have a cube root") + } + } +} diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go index a547157479..6dfd0fe6f8 100644 --- a/field/koalabear/extensions/e8_test.go +++ b/field/koalabear/extensions/e8_test.go @@ -11,7 +11,6 @@ import ( "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" - "github.com/stretchr/testify/require" ) func TestE8ReceiverIsOperand(t *testing.T) { @@ -174,44 +173,3 @@ func genE8() gopter.Gen { return E8{C0: values[0].(E4), C1: values[1].(E4)} }) } - -func TestE8CbrtZero(t *testing.T) { - var zero, got E8 - require.NotNil(t, got.Cbrt(&zero)) - require.True(t, got.IsZero()) -} - -func TestE8CbrtOnCubicResidues(t *testing.T) { - for i := 0; i < 128; i++ { - var a, x, got, check E8 - a.MustSetRandom() - x.Square(&a).Mul(&x, &a) - require.NotNil(t, got.Cbrt(&x)) - check.Square(&got).Mul(&check, &got) - require.True(t, check.Equal(&x)) - } -} - -func TestE8CbrtRejectsNonResidues(t *testing.T) { - var x, got E8 - for i := 0; i < 256; i++ { - x.MustSetRandom() - if got.Cbrt(&x) == nil { - return - } - } - t.Fatal("failed to find an E8 non-cube in 256 samples") -} - -func BenchmarkE8Cbrt(b *testing.B) { - var a, x E8 - a.MustSetRandom() - x.Square(&a).Mul(&x, &a) - var z E8 - b.ResetTimer() - for i := 0; i < b.N; i++ { - if z.Cbrt(&x) == nil { - b.Fatal("expected cubic residue to have a cube root") - } - } -} diff --git a/internal/generator/addchain/e1c71c7 b/internal/generator/addchain/e1c71c7 new file mode 100644 index 0000000000000000000000000000000000000000..ce5a969337c3014dd8bbc6c8fd54f49f8708aee1 GIT binary patch literal 209 zcmWm4%?bfw0EOZA{RmBxq)AMgBuSE_Ns>JqZo$e**weF z`n=rCznH&a@TzDyJ<4JM4pz})k&yoil@bD0iy_poVFM#0Y}&+@Eo|Gyj-B%DV%HdZ z_ONds6B8Ud#MBf=j!HMii4&YU#hEjlJI93!T)M=SD_pzA%nUbfaO)O#?l5-GAI*Rj A-~a#s literal 0 HcmV?d00001 diff --git a/internal/generator/config/curve.go b/internal/generator/config/curve.go index aed534ee54..c7bcdce646 100644 --- a/internal/generator/config/curve.go +++ b/internal/generator/config/curve.go @@ -14,7 +14,10 @@ type Curve struct { EnumID string FpModulus string FrModulus string - ExistingFp ExistingFieldPackage + // ExistingFp points to an already generated field package to alias as ecc//fp. + // Set it when a curve is defined over a top-level field package rather than a + // curve-local generated base field. + ExistingFp ExistingFieldPackage Fp *config.Field Fr *config.Field @@ -59,8 +62,11 @@ type Curve struct { E2CbrtTorusLucasTopBit int // bit length - 1 } +// ExistingFieldPackage describes an existing field package reused as a curve base field. type ExistingFieldPackage struct { + // PackagePath is the full Go import path of the existing field package. PackagePath string + // PackageName is the local package identifier used for imports in generated aliases. PackageName string } diff --git a/internal/generator/config/fields.go b/internal/generator/config/fields.go index f7a37fb35e..7534f25ff3 100644 --- a/internal/generator/config/fields.go +++ b/internal/generator/config/fields.go @@ -3,6 +3,12 @@ package config type Field struct { Name string Modulus string + + // GenerateExtensionE8 enables generation of field/extensions/e8.go. + GenerateExtensionE8 bool + + // CustomExtensionCbrt keeps extension Cbrt methods in hand-maintained files. + CustomExtensionCbrt bool } var Fields []Field @@ -17,8 +23,10 @@ func init() { Modulus: "0xFFFFFFFF00000001", }) addField(Field{ - Name: "koalabear", - Modulus: "0x7f000001", + Name: "koalabear", + Modulus: "0x7f000001", + GenerateExtensionE8: true, + CustomExtensionCbrt: true, }) addField(Field{ Name: "babybear", diff --git a/internal/generator/field/config/field_config.go b/internal/generator/field/config/field_config.go index 07fe87b87d..d9602357f4 100644 --- a/internal/generator/field/config/field_config.go +++ b/internal/generator/field/config/field_config.go @@ -70,6 +70,7 @@ type Field struct { // Cbrt pre computes CbrtQ2Mod3 bool // q ≡ 2 (mod 3) + CbrtQ2Mod9 bool // q ≡ 2 (mod 9), use (q-2)/9 helper for extension Cbrt CbrtQ1Mod3 bool // q ≡ 1 (mod 3), need special handling CbrtQ7Mod9 bool // q ≡ 7 (mod 9), use (q+2)/9 exponent CbrtQ4Mod9 bool // q ≡ 4 (mod 9), use (2q+1)/9 exponent @@ -82,6 +83,7 @@ type Field struct { Cbrt2QPlus1Div9 string // (2q+1)/9 for q ≡ 4 (mod 9) Cbrt2QPlus7Div27 string // (2q+7)/27 for q ≡ 10 (mod 27) CbrtQPlus8Div27 string // (q+8)/27 for q ≡ 19 (mod 27) + CbrtQMinus2Div9 string // (q-2)/9 for q ≡ 2 (mod 9) CbrtSPlus1Div3 string // (CbrtS+1)/3 for q ≡ 1 (mod 3) with CbrtS ≡ 2 (mod 3) CbrtSMinus1Div3 string // (CbrtS-1)/3 for q ≡ 1 (mod 3) with CbrtS ≡ 1 (mod 3) CbrtG []uint64 // NonCubicResidue ^ CbrtS (montgomery form) -- primitive 3^CbrtE root of unity (ζ) @@ -94,6 +96,7 @@ type Field struct { Cbrt2QPlus1Div9Data *addchain.AddChainData Cbrt2QPlus7Div27Data *addchain.AddChainData CbrtQPlus8Div27Data *addchain.AddChainData + CbrtQMinus2Div9Data *addchain.AddChainData CbrtSPlus1Div3Data *addchain.AddChainData CbrtSMinus1Div3Data *addchain.AddChainData @@ -101,6 +104,10 @@ type Field struct { CbrtTorusHelperData *addchain.AddChainData CbrtTorusHelperName string // e.g. "CbrtHelperQMinus19Div27" + // Extension generation options. + GenerateExtensionE8 bool + CustomExtensionCbrt bool + Word Word // 32 iff Q < 2^32, else 64 F31 bool // 31 bits field @@ -356,9 +363,14 @@ func NewFieldConfig(packageName, elementName, modulus string, useAddChain bool) // Cbrt pre computes // Check if q ≡ 1 (mod 3) or q ≡ 2 (mod 3) + three := big.NewInt(3) + nine := big.NewInt(9) + twentySeven := big.NewInt(27) var qMod3 big.Int - qMod3.SetUint64(3) - qMod3.Mod(&bModulus, &qMod3) + qMod3.Mod(&bModulus, three) + var qMod9, qMod27 big.Int + qMod9.Mod(&bModulus, nine) + qMod27.Mod(&bModulus, twentySeven) if qMod3.Cmp(new(big.Int).SetUint64(2)) == 0 { // q ≡ 2 (mod 3) @@ -372,6 +384,17 @@ func NewFieldConfig(packageName, elementName, modulus string, useAddChain bool) if F.UseAddChain { F.CbrtQ2Mod3ExponentData = addchain.GetAddChain(&cbrtExponent) } + if qMod9.Cmp(big.NewInt(2)) == 0 { + // q ≡ 2 (mod 9): helper used by extension-field cube-root computations. + F.CbrtQ2Mod9 = true + var helperExponent big.Int + helperExponent.Sub(&bModulus, big.NewInt(2)) + helperExponent.Div(&helperExponent, nine) + F.CbrtQMinus2Div9 = helperExponent.Text(16) + if F.UseAddChain { + F.CbrtQMinus2Div9Data = addchain.GetAddChain(&helperExponent) + } + } } else { // q ≡ 1 (mod 3) // use Tonelli-Shanks variant for cube roots @@ -383,8 +406,6 @@ func NewFieldConfig(packageName, elementName, modulus string, useAddChain bool) s.Sub(&bModulus, &one) // Count the power of 3 in q-1 - three := big.NewInt(3) - nine := big.NewInt(9) e := uint64(0) var remainder big.Int for { @@ -400,11 +421,6 @@ func NewFieldConfig(packageName, elementName, modulus string, useAddChain bool) // Check q mod 9 and q mod 27 for optimized exponentiation // Reference: Lemma 3 of https://eprint.iacr.org/2021/1446.pdf - var qMod9, qMod27 big.Int - qMod9.Mod(&bModulus, nine) - twentySeven := big.NewInt(27) - qMod27.Mod(&bModulus, twentySeven) - if e == 1 && qMod9.Cmp(big.NewInt(7)) == 0 { // q ≡ 7 (mod 9): use cbrt(x) = x^((q+2)/9) F.CbrtQ7Mod9 = true diff --git a/internal/generator/field/generator_extensions.go b/internal/generator/field/generator_extensions.go index 7a75c24af5..c8ad380b66 100644 --- a/internal/generator/field/generator_extensions.go +++ b/internal/generator/field/generator_extensions.go @@ -27,12 +27,13 @@ func generateExtensions(F *config.Field, outputDir string) error { } type extensionsTemplateData struct { - FF string - FieldPackagePath string - F31 bool - Q, QInvNeg uint64 - IsKoalaBear bool - IsBabyBear bool + FF string + FieldPackagePath string + F31 bool + Q, QInvNeg uint64 + IsKoalaBear bool + IsBabyBear bool + CustomExtensionCbrt bool // QuadraticNonResidue is α, where E2 = Fr[u]/(u²-α). QuadraticNonResidue uint64 } @@ -52,6 +53,7 @@ func generateExtensions(F *config.Field, outputDir string) error { F31: F.F31, IsKoalaBear: isKoalaBear, IsBabyBear: isBabyBear, + CustomExtensionCbrt: F.CustomExtensionCbrt, Q: F.Q[0], QInvNeg: F.QInverse[0], QuadraticNonResidue: quadraticNonResidue, @@ -71,6 +73,13 @@ func generateExtensions(F *config.Field, outputDir string) error { {File: filepath.Join(outputDir, "e6_test.go"), Templates: []string{"e6_test.go.tmpl"}}, } + if F.GenerateExtensionE8 { + entries_ext4 = append(entries_ext4, + bavard.Entry{File: filepath.Join(outputDir, "e8.go"), Templates: []string{"e8.go.tmpl"}}, + bavard.Entry{File: filepath.Join(outputDir, "e8_test.go"), Templates: []string{"e8_test.go.tmpl"}}, + ) + } + if isKoalaBear { entries_ext4 = append(entries_ext4, bavard.Entry{File: filepath.Join(outputDir, "e4_amd64.go"), Templates: []string{"e4.amd64.go.tmpl"}, BuildTag: "!purego"}) entries_ext4 = append(entries_ext4, bavard.Entry{File: filepath.Join(outputDir, "e4_purego.go"), Templates: []string{"e4.purego.go.tmpl"}, BuildTag: "purego || (!amd64)"}) diff --git a/internal/generator/field/generator_field.go b/internal/generator/field/generator_field.go index dbc2546ee3..b1718e7beb 100644 --- a/internal/generator/field/generator_field.go +++ b/internal/generator/field/generator_field.go @@ -153,9 +153,7 @@ func generateField(F *config.Field, outputDir, asmDirIncludePath, hashArm64, has g.Go(generate("vector_purego.go", []string{"element/vectoropspurego.go.tmpl"}, withBuildTag(pureGoVectorBuildTag))) - // koalabear keeps custom fixed-exponent helpers alongside its recursive extension cube-root stack. - // The generic field template doesn't model those helpers yet, so generation must leave element_exp.go intact. - if F.UseAddChain && F.PackageName != "koalabear" { + if F.UseAddChain { g.Go(generate("element_exp.go", []string{"element/fixedexp.go.tmpl"})) } diff --git a/internal/generator/field/template/element/fixedexp.go.tmpl b/internal/generator/field/template/element/fixedexp.go.tmpl index 1a871d5621..fa4e279fed 100644 --- a/internal/generator/field/template/element/fixedexp.go.tmpl +++ b/internal/generator/field/template/element/fixedexp.go.tmpl @@ -29,6 +29,10 @@ {{- end}} {{- end}} +{{- if .CbrtQMinus2Div9Data}} + {{expByAddChain "CbrtHelperQMinus2Div9" .CbrtQMinus2Div9Data .ElementName}} +{{- end}} + {{- if .CbrtTorusHelperData}} {{expByAddChain .CbrtTorusHelperName .CbrtTorusHelperData .ElementName}} {{- end}} @@ -62,6 +66,9 @@ {{- else if eq .name "CbrtQPlus8Div27"}} // It raises x to the (q+8)/27 power using a shorter addition chain. // This is used when q ≡ 19 (mod 27) for efficient cube root computation. +{{- else if eq .name "CbrtHelperQMinus2Div9"}} +// It raises x to the (q-2)/9 power using an addition chain. +// This helper is used by extension-field cube-root computations. {{- else if eq .name "CbrtHelperQMinus7Div9"}} // It raises x to the (q-7)/9 power using an addition chain. // This helper is used by cbrtAndNormInverse to share exponentiation between @@ -122,4 +129,4 @@ func (z *{{.eName}}) ExpBy{{$.name}}(x {{.eName}}) *{{.eName}} { return z } -{{end}} \ No newline at end of file +{{end}} diff --git a/internal/generator/field/template/extensions/e2.go.tmpl b/internal/generator/field/template/extensions/e2.go.tmpl index a68b7a708c..6b6abe59ed 100644 --- a/internal/generator/field/template/extensions/e2.go.tmpl +++ b/internal/generator/field/template/extensions/e2.go.tmpl @@ -212,6 +212,8 @@ func (z *E2) Sqrt(x *E2) *E2 { return z } +{{- if not .CustomExtensionCbrt}} + // Cbrt sets z to the cube root of x and returns z // The function does not test whether the cube root // exists or not, it's up to the caller to verify. @@ -235,6 +237,7 @@ func (z *E2) Cbrt(x *E2) *E2 { return z } +{{- end}} // BatchInvertE2 returns a new slice with every element in a inverted. // It uses Montgomery batch inversion trick. diff --git a/internal/generator/field/template/extensions/e8.go.tmpl b/internal/generator/field/template/extensions/e8.go.tmpl index 94a050a86f..38fe76a743 100644 --- a/internal/generator/field/template/extensions/e8.go.tmpl +++ b/internal/generator/field/template/extensions/e8.go.tmpl @@ -147,21 +147,27 @@ func (z *E8) Mul(x, y *E8) *E8 { Sub(&c, &a). Sub(&c, &b) z.C1.Set(&c) - b.MulByNonResidue(&b) + b.MulByQuadraticNonResidue(&b) z.C0.Add(&a, &b) return z } // Square sets z=x*x in E8 and returns z. func (z *E8) Square(x *E8) *E8 { - return z.Mul(x, x) + var a, b, c E4 + a.Square(&x.C0) + b.Square(&x.C1).MulByQuadraticNonResidue(&b) + c.Mul(&x.C0, &x.C1).Double(&c) + z.C0.Add(&a, &b) + z.C1.Set(&c) + return z } // Inverse sets z to the inverse of x in E8 and returns z. func (z *E8) Inverse(x *E8) *E8 { var t0, t1 E4 t0.Square(&x.C0) - t1.Square(&x.C1).MulByNonResidue(&t1) + t1.Square(&x.C1).MulByQuadraticNonResidue(&t1) t0.Sub(&t0, &t1).Inverse(&t0) z.C0.Mul(&x.C0, &t0) z.C1.Mul(&x.C1, &t0).Neg(&z.C1) @@ -236,7 +242,7 @@ func (z *E8) Halve() { // norm sets x to the norm of z. func (z *E8) norm(x *E4) { var tmp E4 - tmp.Square(&z.C1).MulByNonResidue(&tmp) + tmp.Square(&z.C1).MulByQuadraticNonResidue(&tmp) x.Square(&z.C0).Sub(x, &tmp) } diff --git a/internal/generator/fieldwrapper/generate.go b/internal/generator/fieldwrapper/generate.go index 8ceb6b87c9..3a637dfa08 100644 --- a/internal/generator/fieldwrapper/generate.go +++ b/internal/generator/fieldwrapper/generate.go @@ -10,10 +10,10 @@ import ( ) func Generate(conf config.Curve, baseDir string) error { - entry := bavard.Entry{ - File: filepath.Join(baseDir, "fp.go"), - Templates: []string{"fp.go.tmpl"}, + entries := []bavard.Entry{ + {File: filepath.Join(baseDir, "doc.go"), Templates: []string{"doc.go.tmpl"}}, + {File: filepath.Join(baseDir, "fp.go"), Templates: []string{"fp.go.tmpl"}}, } gen := common.NewDefaultGenerator(template.FS) - return gen.Generate(conf, "fp", "", "", entry) + return gen.Generate(conf, "fp", "", "", entries...) } diff --git a/internal/generator/fieldwrapper/template/doc.go.tmpl b/internal/generator/fieldwrapper/template/doc.go.tmpl new file mode 100644 index 0000000000..f63b4afe39 --- /dev/null +++ b/internal/generator/fieldwrapper/template/doc.go.tmpl @@ -0,0 +1,6 @@ +// Package fp aliases {{ .ExistingFp.PackagePath }} for curves whose base field is +// implemented in an existing top-level field package. +// +// It exists so packages under ecc/{{ .Name }} can use the same local fp import +// shape as curves with generated base fields. +package fp diff --git a/internal/generator/fieldwrapper/template/fp.go.tmpl b/internal/generator/fieldwrapper/template/fp.go.tmpl index c58167a697..03032021ec 100644 --- a/internal/generator/fieldwrapper/template/fp.go.tmpl +++ b/internal/generator/fieldwrapper/template/fp.go.tmpl @@ -4,37 +4,50 @@ import ( {{ .ExistingFp.PackageName }} "{{ .ExistingFp.PackagePath }}" ) +// Element is the base field element type for this curve. type Element = {{ .ExistingFp.PackageName }}.Element + +// Vector is a slice of base field elements. type Vector = {{ .ExistingFp.PackageName }}.Vector const ( + // Bits is the bit length of the field modulus. Bits = {{ .ExistingFp.PackageName }}.Bits + // Bytes is the byte length needed to encode one field element. Bytes = {{ .ExistingFp.PackageName }}.Bytes + // Limbs is the number of machine words used by one field element. Limbs = {{ .ExistingFp.PackageName }}.Limbs ) +// BigEndian is the big-endian byte order implementation. var BigEndian = {{ .ExistingFp.PackageName }}.BigEndian +// Modulus returns a copy of the field modulus. func Modulus() *big.Int { return {{ .ExistingFp.PackageName }}.Modulus() } +// One returns the field element one. func One() Element { return {{ .ExistingFp.PackageName }}.One() } +// BatchInvert returns a new slice with every non-zero element inverted. func BatchInvert(a []Element) []Element { return {{ .ExistingFp.PackageName }}.BatchInvert(a) } +// Generator returns a field generator for the requested multiplicative subgroup. func Generator(m uint64) (Element, error) { return {{ .ExistingFp.PackageName }}.Generator(m) } +// Butterfly computes the in-place butterfly operation on a and b. func Butterfly(a, b *Element) { {{ .ExistingFp.PackageName }}.Butterfly(a, b) } +// MulBy3 multiplies x by 3 in place. func MulBy3(x *Element) { {{ .ExistingFp.PackageName }}.MulBy3(x) } diff --git a/internal/generator/main.go b/internal/generator/main.go index eee4ca4db7..fc3c550f18 100644 --- a/internal/generator/main.go +++ b/internal/generator/main.go @@ -74,6 +74,8 @@ func main() { defer wg.Done() fc, err := fieldConfig.NewFieldConfig(f.Name, "Element", f.Modulus, true) assertNoError(err) + fc.GenerateExtensionE8 = f.GenerateExtensionE8 + fc.CustomExtensionCbrt = f.CustomExtensionCbrt outputDir := filepath.Join(baseDir, "field", f.Name) relAsmDir, err := filepath.Rel(outputDir, asmDirBuildPath) assertNoError(err) @@ -93,12 +95,6 @@ func main() { // files with the "DO NOT EDIT" header are removed; hand-written files // (without this header) are preserved. for _, conf := range config.Curves { - if conf.Equal(config.KB8) { - curveDir := filepath.Join(baseDir, "ecc", conf.Name) - cleanGeneratedFiles(filepath.Join(curveDir, "fp")) - cleanGeneratedFiles(filepath.Join(curveDir, "fr")) - continue - } curveDir := filepath.Join(baseDir, "ecc", conf.Name) cleanGeneratedFiles(curveDir) } From 52d6828271a22c91c3877443e568e9807de4ef7d Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 21 May 2026 14:48:46 -0400 Subject: [PATCH 25/33] perf: optimize Fp8 arithmetic --- field/koalabear/extensions/e8.go | 164 ++++++++++++++---- field/koalabear/extensions/e8_test.go | 80 +++++++++ .../field/template/extensions/e8.go.tmpl | 164 ++++++++++++++---- .../field/template/extensions/e8_test.go.tmpl | 80 +++++++++ 4 files changed, 430 insertions(+), 58 deletions(-) diff --git a/field/koalabear/extensions/e8.go b/field/koalabear/extensions/e8.go index 63634506ac..c9205ce337 100644 --- a/field/koalabear/extensions/e8.go +++ b/field/koalabear/extensions/e8.go @@ -81,36 +81,66 @@ func (z *E8) Lift(v *fr.Element) *E8 { // MulByElement multiplies an element in E8 by an element in fr. func (z *E8) MulByElement(x *E8, y *fr.Element) *E8 { - z.C0.MulByElement(&x.C0, y) - z.C1.MulByElement(&x.C1, y) + z.C0.B0.A0.Mul(&x.C0.B0.A0, y) + z.C0.B0.A1.Mul(&x.C0.B0.A1, y) + z.C0.B1.A0.Mul(&x.C0.B1.A0, y) + z.C0.B1.A1.Mul(&x.C0.B1.A1, y) + z.C1.B0.A0.Mul(&x.C1.B0.A0, y) + z.C1.B0.A1.Mul(&x.C1.B0.A1, y) + z.C1.B1.A0.Mul(&x.C1.B1.A0, y) + z.C1.B1.A1.Mul(&x.C1.B1.A1, y) return z } // Add sets z=x+y in E8 and returns z. func (z *E8) Add(x, y *E8) *E8 { - z.C0.Add(&x.C0, &y.C0) - z.C1.Add(&x.C1, &y.C1) + z.C0.B0.A0.Add(&x.C0.B0.A0, &y.C0.B0.A0) + z.C0.B0.A1.Add(&x.C0.B0.A1, &y.C0.B0.A1) + z.C0.B1.A0.Add(&x.C0.B1.A0, &y.C0.B1.A0) + z.C0.B1.A1.Add(&x.C0.B1.A1, &y.C0.B1.A1) + z.C1.B0.A0.Add(&x.C1.B0.A0, &y.C1.B0.A0) + z.C1.B0.A1.Add(&x.C1.B0.A1, &y.C1.B0.A1) + z.C1.B1.A0.Add(&x.C1.B1.A0, &y.C1.B1.A0) + z.C1.B1.A1.Add(&x.C1.B1.A1, &y.C1.B1.A1) return z } // Sub sets z=x-y in E8 and returns z. func (z *E8) Sub(x, y *E8) *E8 { - z.C0.Sub(&x.C0, &y.C0) - z.C1.Sub(&x.C1, &y.C1) + z.C0.B0.A0.Sub(&x.C0.B0.A0, &y.C0.B0.A0) + z.C0.B0.A1.Sub(&x.C0.B0.A1, &y.C0.B0.A1) + z.C0.B1.A0.Sub(&x.C0.B1.A0, &y.C0.B1.A0) + z.C0.B1.A1.Sub(&x.C0.B1.A1, &y.C0.B1.A1) + z.C1.B0.A0.Sub(&x.C1.B0.A0, &y.C1.B0.A0) + z.C1.B0.A1.Sub(&x.C1.B0.A1, &y.C1.B0.A1) + z.C1.B1.A0.Sub(&x.C1.B1.A0, &y.C1.B1.A0) + z.C1.B1.A1.Sub(&x.C1.B1.A1, &y.C1.B1.A1) return z } // Double sets z=2*x and returns z. func (z *E8) Double(x *E8) *E8 { - z.C0.Double(&x.C0) - z.C1.Double(&x.C1) + z.C0.B0.A0.Double(&x.C0.B0.A0) + z.C0.B0.A1.Double(&x.C0.B0.A1) + z.C0.B1.A0.Double(&x.C0.B1.A0) + z.C0.B1.A1.Double(&x.C0.B1.A1) + z.C1.B0.A0.Double(&x.C1.B0.A0) + z.C1.B0.A1.Double(&x.C1.B0.A1) + z.C1.B1.A0.Double(&x.C1.B1.A0) + z.C1.B1.A1.Double(&x.C1.B1.A1) return z } // Neg negates an E8 element. func (z *E8) Neg(x *E8) *E8 { - z.C0.Neg(&x.C0) - z.C1.Neg(&x.C1) + z.C0.B0.A0.Neg(&x.C0.B0.A0) + z.C0.B0.A1.Neg(&x.C0.B0.A1) + z.C0.B1.A0.Neg(&x.C0.B1.A0) + z.C0.B1.A1.Neg(&x.C0.B1.A1) + z.C1.B0.A0.Neg(&x.C1.B0.A0) + z.C1.B0.A1.Neg(&x.C1.B0.A1) + z.C1.B1.A0.Neg(&x.C1.B1.A0) + z.C1.B1.A1.Neg(&x.C1.B1.A1) return z } @@ -145,28 +175,97 @@ func (z *E8) IsOne() bool { // Mul sets z=x*y in E8 and returns z. func (z *E8) Mul(x, y *E8) *E8 { - var a, b, c, d E4 - a.Mul(&x.C0, &y.C0) - b.Mul(&x.C1, &y.C1) - c.Add(&x.C0, &x.C1) - d.Add(&y.C0, &y.C1) - c.Mul(&c, &d). - Sub(&c, &a). - Sub(&c, &b) - z.C1.Set(&c) - b.MulByQuadraticNonResidue(&b) - z.C0.Add(&a, &b) + a0 := uint64(x.C0.B0.A0[0]) + a1 := uint64(x.C1.B0.A0[0]) + a2 := uint64(x.C0.B1.A0[0]) + a3 := uint64(x.C1.B1.A0[0]) + a4 := uint64(x.C0.B0.A1[0]) + a5 := uint64(x.C1.B0.A1[0]) + a6 := uint64(x.C0.B1.A1[0]) + a7 := uint64(x.C1.B1.A1[0]) + + b0 := uint64(y.C0.B0.A0[0]) + b1 := uint64(y.C1.B0.A0[0]) + b2 := uint64(y.C0.B1.A0[0]) + b3 := uint64(y.C1.B1.A0[0]) + b4 := uint64(y.C0.B0.A1[0]) + b5 := uint64(y.C1.B0.A1[0]) + b6 := uint64(y.C0.B1.A1[0]) + b7 := uint64(y.C1.B1.A1[0]) + + z.C0.B0.A0[0] = reduceSmall(montReduceLazy(a0*b0) + + 3*(montReduceLazy(a1*b7+a2*b6)+ + montReduceLazy(a3*b5+a4*b4)+ + montReduceLazy(a5*b3+a6*b2)+ + montReduceLazy(a7*b1))) + z.C1.B0.A0[0] = reduceSmall(montReduceLazy(a0*b1+a1*b0) + + 3*(montReduceLazy(a2*b7+a3*b6)+ + montReduceLazy(a4*b5+a5*b4)+ + montReduceLazy(a6*b3+a7*b2))) + z.C0.B1.A0[0] = reduceSmall(montReduceLazy(a0*b2+a1*b1) + + montReduceLazy(a2*b0) + + 3*(montReduceLazy(a3*b7+a4*b6)+ + montReduceLazy(a5*b5+a6*b4)+ + montReduceLazy(a7*b3))) + z.C1.B1.A0[0] = reduceSmall(montReduceLazy(a0*b3+a1*b2) + + montReduceLazy(a2*b1+a3*b0) + + 3*(montReduceLazy(a4*b7+a5*b6)+ + montReduceLazy(a6*b5+a7*b4))) + z.C0.B0.A1[0] = reduceSmall(montReduceLazy(a0*b4+a1*b3) + + montReduceLazy(a2*b2+a3*b1) + + montReduceLazy(a4*b0) + + 3*(montReduceLazy(a5*b7+a6*b6)+ + montReduceLazy(a7*b5))) + z.C1.B0.A1[0] = reduceSmall(montReduceLazy(a0*b5+a1*b4) + + montReduceLazy(a2*b3+a3*b2) + + montReduceLazy(a4*b1+a5*b0) + + 3*montReduceLazy(a6*b7+a7*b6)) + z.C0.B1.A1[0] = reduceSmall(montReduceLazy(a0*b6+a1*b5) + + montReduceLazy(a2*b4+a3*b3) + + montReduceLazy(a4*b2+a5*b1) + + montReduceLazy(a6*b0) + + 3*montReduceLazy(a7*b7)) + z.C1.B1.A1[0] = reduceSmall(montReduceLazy(a0*b7+a1*b6) + + montReduceLazy(a2*b5+a3*b4) + + montReduceLazy(a4*b3+a5*b2) + + montReduceLazy(a6*b1+a7*b0)) return z } // Square sets z=x*x in E8 and returns z. func (z *E8) Square(x *E8) *E8 { - var a, b, c E4 - a.Square(&x.C0) - b.Square(&x.C1).MulByQuadraticNonResidue(&b) - c.Mul(&x.C0, &x.C1).Double(&c) - z.C0.Add(&a, &b) - z.C1.Set(&c) + a0 := uint64(x.C0.B0.A0[0]) + a1 := uint64(x.C1.B0.A0[0]) + a2 := uint64(x.C0.B1.A0[0]) + a3 := uint64(x.C1.B1.A0[0]) + a4 := uint64(x.C0.B0.A1[0]) + a5 := uint64(x.C1.B0.A1[0]) + a6 := uint64(x.C0.B1.A1[0]) + a7 := uint64(x.C1.B1.A1[0]) + + z.C0.B0.A0[0] = reduceSmall(montReduceLazy(a0*a0) + + 3*montReduceLazy(a4*a4) + + 2*3*(montReduceLazy(a1*a7+a2*a6)+montReduceLazy(a3*a5))) + z.C1.B0.A0[0] = reduceSmall(2 * (montReduceLazy(a0*a1) + + 3*(montReduceLazy(a2*a7+a3*a6)+montReduceLazy(a4*a5)))) + z.C0.B1.A0[0] = reduceSmall(2*montReduceLazy(a0*a2) + + montReduceLazy(a1*a1) + + 2*3*montReduceLazy(a3*a7+a4*a6) + + 3*montReduceLazy(a5*a5)) + z.C1.B1.A0[0] = reduceSmall(2 * (montReduceLazy(a0*a3+a1*a2) + + 3*montReduceLazy(a4*a7+a5*a6))) + z.C0.B0.A1[0] = reduceSmall(2*montReduceLazy(a0*a4+a1*a3) + + montReduceLazy(a2*a2) + + 2*3*montReduceLazy(a5*a7) + + 3*montReduceLazy(a6*a6)) + z.C1.B0.A1[0] = reduceSmall(2 * (montReduceLazy(a0*a5+a1*a4) + + montReduceLazy(a2*a3) + + 3*montReduceLazy(a6*a7))) + z.C0.B1.A1[0] = reduceSmall(2*(montReduceLazy(a0*a6+a1*a5)+montReduceLazy(a2*a4)) + + montReduceLazy(a3*a3) + + 3*montReduceLazy(a7*a7)) + z.C1.B1.A1[0] = reduceSmall(2 * (montReduceLazy(a0*a7+a1*a6) + + montReduceLazy(a2*a5+a3*a4))) return z } @@ -177,7 +276,11 @@ func (z *E8) Inverse(x *E8) *E8 { t1.Square(&x.C1).MulByQuadraticNonResidue(&t1) t0.Sub(&t0, &t1).Inverse(&t0) z.C0.Mul(&x.C0, &t0) - z.C1.Mul(&x.C1, &t0).Neg(&z.C1) + z.C1.Mul(&x.C1, &t0) + z.C1.B0.A0.Neg(&z.C1.B0.A0) + z.C1.B0.A1.Neg(&z.C1.B0.A1) + z.C1.B1.A0.Neg(&z.C1.B1.A0) + z.C1.B1.A1.Neg(&z.C1.B1.A1) return z } @@ -236,7 +339,10 @@ func (z *E8) ExpInt64(x E8, k int64) *E8 { // Conjugate sets z to x conjugated and returns z. func (z *E8) Conjugate(x *E8) *E8 { z.C0 = x.C0 - z.C1.Neg(&x.C1) + z.C1.B0.A0.Neg(&x.C1.B0.A0) + z.C1.B0.A1.Neg(&x.C1.B0.A1) + z.C1.B1.A0.Neg(&x.C1.B1.A0) + z.C1.B1.A1.Neg(&x.C1.B1.A1) return z } diff --git a/field/koalabear/extensions/e8_test.go b/field/koalabear/extensions/e8_test.go index 6dfd0fe6f8..f740ce5f0a 100644 --- a/field/koalabear/extensions/e8_test.go +++ b/field/koalabear/extensions/e8_test.go @@ -9,6 +9,8 @@ import ( "math/big" "testing" + fr "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" ) @@ -173,3 +175,81 @@ func genE8() gopter.Gen { return E8{C0: values[0].(E4), C1: values[1].(E4)} }) } + +func BenchmarkE8Add(b *testing.B) { + var a, c E8 + a.MustSetRandom() + c.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Add(&a, &c) + } +} + +func BenchmarkE8Sub(b *testing.B) { + var a, c E8 + a.MustSetRandom() + c.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Sub(&a, &c) + } +} + +func BenchmarkE8Mul(b *testing.B) { + var a, c E8 + a.MustSetRandom() + c.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Mul(&a, &c) + } +} + +func BenchmarkE8MulByElement(b *testing.B) { + var a E8 + var c fr.Element + a.MustSetRandom() + _, _ = c.SetRandom() + b.ResetTimer() + for range b.N { + a.MulByElement(&a, &c) + } +} + +func BenchmarkE8Square(b *testing.B) { + var a E8 + a.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Square(&a) + } +} + +func BenchmarkE8Sqrt(b *testing.B) { + var a, c E8 + a.MustSetRandom() + a.Square(&a) + b.ResetTimer() + for range b.N { + c.Sqrt(&a) + } +} + +func BenchmarkE8Inverse(b *testing.B) { + var a E8 + a.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Inverse(&a) + } +} + +func BenchmarkE8Conjugate(b *testing.B) { + var a E8 + a.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Conjugate(&a) + } +} diff --git a/internal/generator/field/template/extensions/e8.go.tmpl b/internal/generator/field/template/extensions/e8.go.tmpl index 38fe76a743..e4c38ee434 100644 --- a/internal/generator/field/template/extensions/e8.go.tmpl +++ b/internal/generator/field/template/extensions/e8.go.tmpl @@ -74,36 +74,66 @@ func (z *E8) Lift(v *fr.Element) *E8 { // MulByElement multiplies an element in E8 by an element in fr. func (z *E8) MulByElement(x *E8, y *fr.Element) *E8 { - z.C0.MulByElement(&x.C0, y) - z.C1.MulByElement(&x.C1, y) + z.C0.B0.A0.Mul(&x.C0.B0.A0, y) + z.C0.B0.A1.Mul(&x.C0.B0.A1, y) + z.C0.B1.A0.Mul(&x.C0.B1.A0, y) + z.C0.B1.A1.Mul(&x.C0.B1.A1, y) + z.C1.B0.A0.Mul(&x.C1.B0.A0, y) + z.C1.B0.A1.Mul(&x.C1.B0.A1, y) + z.C1.B1.A0.Mul(&x.C1.B1.A0, y) + z.C1.B1.A1.Mul(&x.C1.B1.A1, y) return z } // Add sets z=x+y in E8 and returns z. func (z *E8) Add(x, y *E8) *E8 { - z.C0.Add(&x.C0, &y.C0) - z.C1.Add(&x.C1, &y.C1) + z.C0.B0.A0.Add(&x.C0.B0.A0, &y.C0.B0.A0) + z.C0.B0.A1.Add(&x.C0.B0.A1, &y.C0.B0.A1) + z.C0.B1.A0.Add(&x.C0.B1.A0, &y.C0.B1.A0) + z.C0.B1.A1.Add(&x.C0.B1.A1, &y.C0.B1.A1) + z.C1.B0.A0.Add(&x.C1.B0.A0, &y.C1.B0.A0) + z.C1.B0.A1.Add(&x.C1.B0.A1, &y.C1.B0.A1) + z.C1.B1.A0.Add(&x.C1.B1.A0, &y.C1.B1.A0) + z.C1.B1.A1.Add(&x.C1.B1.A1, &y.C1.B1.A1) return z } // Sub sets z=x-y in E8 and returns z. func (z *E8) Sub(x, y *E8) *E8 { - z.C0.Sub(&x.C0, &y.C0) - z.C1.Sub(&x.C1, &y.C1) + z.C0.B0.A0.Sub(&x.C0.B0.A0, &y.C0.B0.A0) + z.C0.B0.A1.Sub(&x.C0.B0.A1, &y.C0.B0.A1) + z.C0.B1.A0.Sub(&x.C0.B1.A0, &y.C0.B1.A0) + z.C0.B1.A1.Sub(&x.C0.B1.A1, &y.C0.B1.A1) + z.C1.B0.A0.Sub(&x.C1.B0.A0, &y.C1.B0.A0) + z.C1.B0.A1.Sub(&x.C1.B0.A1, &y.C1.B0.A1) + z.C1.B1.A0.Sub(&x.C1.B1.A0, &y.C1.B1.A0) + z.C1.B1.A1.Sub(&x.C1.B1.A1, &y.C1.B1.A1) return z } // Double sets z=2*x and returns z. func (z *E8) Double(x *E8) *E8 { - z.C0.Double(&x.C0) - z.C1.Double(&x.C1) + z.C0.B0.A0.Double(&x.C0.B0.A0) + z.C0.B0.A1.Double(&x.C0.B0.A1) + z.C0.B1.A0.Double(&x.C0.B1.A0) + z.C0.B1.A1.Double(&x.C0.B1.A1) + z.C1.B0.A0.Double(&x.C1.B0.A0) + z.C1.B0.A1.Double(&x.C1.B0.A1) + z.C1.B1.A0.Double(&x.C1.B1.A0) + z.C1.B1.A1.Double(&x.C1.B1.A1) return z } // Neg negates an E8 element. func (z *E8) Neg(x *E8) *E8 { - z.C0.Neg(&x.C0) - z.C1.Neg(&x.C1) + z.C0.B0.A0.Neg(&x.C0.B0.A0) + z.C0.B0.A1.Neg(&x.C0.B0.A1) + z.C0.B1.A0.Neg(&x.C0.B1.A0) + z.C0.B1.A1.Neg(&x.C0.B1.A1) + z.C1.B0.A0.Neg(&x.C1.B0.A0) + z.C1.B0.A1.Neg(&x.C1.B0.A1) + z.C1.B1.A0.Neg(&x.C1.B1.A0) + z.C1.B1.A1.Neg(&x.C1.B1.A1) return z } @@ -138,28 +168,97 @@ func (z *E8) IsOne() bool { // Mul sets z=x*y in E8 and returns z. func (z *E8) Mul(x, y *E8) *E8 { - var a, b, c, d E4 - a.Mul(&x.C0, &y.C0) - b.Mul(&x.C1, &y.C1) - c.Add(&x.C0, &x.C1) - d.Add(&y.C0, &y.C1) - c.Mul(&c, &d). - Sub(&c, &a). - Sub(&c, &b) - z.C1.Set(&c) - b.MulByQuadraticNonResidue(&b) - z.C0.Add(&a, &b) + a0 := uint64(x.C0.B0.A0[0]) + a1 := uint64(x.C1.B0.A0[0]) + a2 := uint64(x.C0.B1.A0[0]) + a3 := uint64(x.C1.B1.A0[0]) + a4 := uint64(x.C0.B0.A1[0]) + a5 := uint64(x.C1.B0.A1[0]) + a6 := uint64(x.C0.B1.A1[0]) + a7 := uint64(x.C1.B1.A1[0]) + + b0 := uint64(y.C0.B0.A0[0]) + b1 := uint64(y.C1.B0.A0[0]) + b2 := uint64(y.C0.B1.A0[0]) + b3 := uint64(y.C1.B1.A0[0]) + b4 := uint64(y.C0.B0.A1[0]) + b5 := uint64(y.C1.B0.A1[0]) + b6 := uint64(y.C0.B1.A1[0]) + b7 := uint64(y.C1.B1.A1[0]) + + z.C0.B0.A0[0] = reduceSmall(montReduceLazy(a0*b0) + + {{.QuadraticNonResidue}}*(montReduceLazy(a1*b7+a2*b6)+ + montReduceLazy(a3*b5+a4*b4)+ + montReduceLazy(a5*b3+a6*b2)+ + montReduceLazy(a7*b1))) + z.C1.B0.A0[0] = reduceSmall(montReduceLazy(a0*b1+a1*b0) + + {{.QuadraticNonResidue}}*(montReduceLazy(a2*b7+a3*b6)+ + montReduceLazy(a4*b5+a5*b4)+ + montReduceLazy(a6*b3+a7*b2))) + z.C0.B1.A0[0] = reduceSmall(montReduceLazy(a0*b2+a1*b1) + + montReduceLazy(a2*b0) + + {{.QuadraticNonResidue}}*(montReduceLazy(a3*b7+a4*b6)+ + montReduceLazy(a5*b5+a6*b4)+ + montReduceLazy(a7*b3))) + z.C1.B1.A0[0] = reduceSmall(montReduceLazy(a0*b3+a1*b2) + + montReduceLazy(a2*b1+a3*b0) + + {{.QuadraticNonResidue}}*(montReduceLazy(a4*b7+a5*b6)+ + montReduceLazy(a6*b5+a7*b4))) + z.C0.B0.A1[0] = reduceSmall(montReduceLazy(a0*b4+a1*b3) + + montReduceLazy(a2*b2+a3*b1) + + montReduceLazy(a4*b0) + + {{.QuadraticNonResidue}}*(montReduceLazy(a5*b7+a6*b6)+ + montReduceLazy(a7*b5))) + z.C1.B0.A1[0] = reduceSmall(montReduceLazy(a0*b5+a1*b4) + + montReduceLazy(a2*b3+a3*b2) + + montReduceLazy(a4*b1+a5*b0) + + {{.QuadraticNonResidue}}*montReduceLazy(a6*b7+a7*b6)) + z.C0.B1.A1[0] = reduceSmall(montReduceLazy(a0*b6+a1*b5) + + montReduceLazy(a2*b4+a3*b3) + + montReduceLazy(a4*b2+a5*b1) + + montReduceLazy(a6*b0) + + {{.QuadraticNonResidue}}*montReduceLazy(a7*b7)) + z.C1.B1.A1[0] = reduceSmall(montReduceLazy(a0*b7+a1*b6) + + montReduceLazy(a2*b5+a3*b4) + + montReduceLazy(a4*b3+a5*b2) + + montReduceLazy(a6*b1+a7*b0)) return z } // Square sets z=x*x in E8 and returns z. func (z *E8) Square(x *E8) *E8 { - var a, b, c E4 - a.Square(&x.C0) - b.Square(&x.C1).MulByQuadraticNonResidue(&b) - c.Mul(&x.C0, &x.C1).Double(&c) - z.C0.Add(&a, &b) - z.C1.Set(&c) + a0 := uint64(x.C0.B0.A0[0]) + a1 := uint64(x.C1.B0.A0[0]) + a2 := uint64(x.C0.B1.A0[0]) + a3 := uint64(x.C1.B1.A0[0]) + a4 := uint64(x.C0.B0.A1[0]) + a5 := uint64(x.C1.B0.A1[0]) + a6 := uint64(x.C0.B1.A1[0]) + a7 := uint64(x.C1.B1.A1[0]) + + z.C0.B0.A0[0] = reduceSmall(montReduceLazy(a0*a0) + + {{.QuadraticNonResidue}}*montReduceLazy(a4*a4) + + 2*{{.QuadraticNonResidue}}*(montReduceLazy(a1*a7+a2*a6)+montReduceLazy(a3*a5))) + z.C1.B0.A0[0] = reduceSmall(2 * (montReduceLazy(a0*a1) + + {{.QuadraticNonResidue}}*(montReduceLazy(a2*a7+a3*a6)+montReduceLazy(a4*a5)))) + z.C0.B1.A0[0] = reduceSmall(2*montReduceLazy(a0*a2) + + montReduceLazy(a1*a1) + + 2*{{.QuadraticNonResidue}}*montReduceLazy(a3*a7+a4*a6) + + {{.QuadraticNonResidue}}*montReduceLazy(a5*a5)) + z.C1.B1.A0[0] = reduceSmall(2 * (montReduceLazy(a0*a3+a1*a2) + + {{.QuadraticNonResidue}}*montReduceLazy(a4*a7+a5*a6))) + z.C0.B0.A1[0] = reduceSmall(2*montReduceLazy(a0*a4+a1*a3) + + montReduceLazy(a2*a2) + + 2*{{.QuadraticNonResidue}}*montReduceLazy(a5*a7) + + {{.QuadraticNonResidue}}*montReduceLazy(a6*a6)) + z.C1.B0.A1[0] = reduceSmall(2 * (montReduceLazy(a0*a5+a1*a4) + + montReduceLazy(a2*a3) + + {{.QuadraticNonResidue}}*montReduceLazy(a6*a7))) + z.C0.B1.A1[0] = reduceSmall(2*(montReduceLazy(a0*a6+a1*a5)+montReduceLazy(a2*a4)) + + montReduceLazy(a3*a3) + + {{.QuadraticNonResidue}}*montReduceLazy(a7*a7)) + z.C1.B1.A1[0] = reduceSmall(2 * (montReduceLazy(a0*a7+a1*a6) + + montReduceLazy(a2*a5+a3*a4))) return z } @@ -170,7 +269,11 @@ func (z *E8) Inverse(x *E8) *E8 { t1.Square(&x.C1).MulByQuadraticNonResidue(&t1) t0.Sub(&t0, &t1).Inverse(&t0) z.C0.Mul(&x.C0, &t0) - z.C1.Mul(&x.C1, &t0).Neg(&z.C1) + z.C1.Mul(&x.C1, &t0) + z.C1.B0.A0.Neg(&z.C1.B0.A0) + z.C1.B0.A1.Neg(&z.C1.B0.A1) + z.C1.B1.A0.Neg(&z.C1.B1.A0) + z.C1.B1.A1.Neg(&z.C1.B1.A1) return z } @@ -229,7 +332,10 @@ func (z *E8) ExpInt64(x E8, k int64) *E8 { // Conjugate sets z to x conjugated and returns z. func (z *E8) Conjugate(x *E8) *E8 { z.C0 = x.C0 - z.C1.Neg(&x.C1) + z.C1.B0.A0.Neg(&x.C1.B0.A0) + z.C1.B0.A1.Neg(&x.C1.B0.A1) + z.C1.B1.A0.Neg(&x.C1.B1.A0) + z.C1.B1.A1.Neg(&x.C1.B1.A1) return z } diff --git a/internal/generator/field/template/extensions/e8_test.go.tmpl b/internal/generator/field/template/extensions/e8_test.go.tmpl index 573f38e080..2645953036 100644 --- a/internal/generator/field/template/extensions/e8_test.go.tmpl +++ b/internal/generator/field/template/extensions/e8_test.go.tmpl @@ -2,6 +2,8 @@ import ( "math/big" "testing" + fr "{{ .FieldPackagePath }}" + "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" ) @@ -166,3 +168,81 @@ func genE8() gopter.Gen { return E8{C0: values[0].(E4), C1: values[1].(E4)} }) } + +func BenchmarkE8Add(b *testing.B) { + var a, c E8 + a.MustSetRandom() + c.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Add(&a, &c) + } +} + +func BenchmarkE8Sub(b *testing.B) { + var a, c E8 + a.MustSetRandom() + c.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Sub(&a, &c) + } +} + +func BenchmarkE8Mul(b *testing.B) { + var a, c E8 + a.MustSetRandom() + c.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Mul(&a, &c) + } +} + +func BenchmarkE8MulByElement(b *testing.B) { + var a E8 + var c fr.Element + a.MustSetRandom() + _, _ = c.SetRandom() + b.ResetTimer() + for range b.N { + a.MulByElement(&a, &c) + } +} + +func BenchmarkE8Square(b *testing.B) { + var a E8 + a.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Square(&a) + } +} + +func BenchmarkE8Sqrt(b *testing.B) { + var a, c E8 + a.MustSetRandom() + a.Square(&a) + b.ResetTimer() + for range b.N { + c.Sqrt(&a) + } +} + +func BenchmarkE8Inverse(b *testing.B) { + var a E8 + a.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Inverse(&a) + } +} + +func BenchmarkE8Conjugate(b *testing.B) { + var a E8 + a.MustSetRandom() + b.ResetTimer() + for range b.N { + a.Conjugate(&a) + } +} From e10b3369dc3b3df311ffed0fb17ed5f50ca83bb8 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 21 May 2026 15:06:27 -0400 Subject: [PATCH 26/33] fix: encoding and overflow slot --- ecc/kb8/marshal.go | 8 ++++++-- ecc/kb8/marshal_test.go | 8 ++++++++ .../vector_multiset_hash_poseidon2.go | 6 ++++++ .../multiset-hash/vector_multiset_hash_test.go | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/ecc/kb8/marshal.go b/ecc/kb8/marshal.go index 187729ab81..2a1c2f6d48 100644 --- a/ecc/kb8/marshal.go +++ b/ecc/kb8/marshal.go @@ -304,8 +304,12 @@ func (dec *Decoder) readUint64() (r uint64, err error) { // isMaskInvalid returns true if the mask is invalid func isMaskInvalid(msb byte) bool { - mData := msb & mMask - return ((mData == (0b111 << 5)) || (mData == (0b011 << 5)) || (mData == (0b001 << 5))) + switch msb & mMask { + case mUncompressed, mUncompressedInfinity, mCompressedSmallest, mCompressedLargest, mCompressedInfinity: + return false + default: + return true + } } func isCompressed(msb byte) bool { diff --git a/ecc/kb8/marshal_test.go b/ecc/kb8/marshal_test.go index 6845aec2df..3f086f510d 100644 --- a/ecc/kb8/marshal_test.go +++ b/ecc/kb8/marshal_test.go @@ -207,6 +207,14 @@ func TestG1AffineInvalidBitMask(t *testing.T) { if _, err := p.SetBytes(buf[:]); err != ErrInvalidEncoding { t.Fatal("should error on invalid bit mask") } + + valid := g1GenAff.Bytes() + for _, invalidTag := range []byte{0x05, 0x06, 0xff} { + valid[0] = invalidTag + if _, err := p.SetBytes(valid[:]); err != ErrInvalidEncoding { + t.Fatal("should error on invalid encoding tag") + } + } } func TestG1AffineSerialization(t *testing.T) { diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go b/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go index d8b50e7195..a95d995105 100644 --- a/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go +++ b/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go @@ -2,6 +2,7 @@ package multisethash import ( "encoding/binary" + "errors" "math/big" "sync" @@ -34,6 +35,8 @@ var pqDomainTag = [8]byte{'E', 'C', 'M', 'S', 'H', '_', 'P', 'Q'} // extracted from each squeezed koalabear element. With p = 2^31 - 2^24 + 1 // and T = 256, this is floor(2130706433 / 512) = 4161536. var ( + errPqSlotOutOfRange = errors.New("kb8 vector multiset hash: Poseidon2 slot out of range") + pqReducerBound = func() *big.Int { p := koalabear.Modulus() denom := big.NewInt(2 * pqT) @@ -158,6 +161,9 @@ func MapPoseidon2(msg uint64) ([pqN]kb8.G1Affine, [pqN]uint8, error) { // pqT*s + (pqT-1) < p/2 to preserve inverse-freeness; this is automatic when // s < ⌊p/(2T)⌋. func MapAtSlot(slot uint64) (kb8.G1Affine, uint8, error) { + if slot >= pqReducerBound.Uint64() { + return kb8.G1Affine{}, 0, errPqSlotOutOfRange + } _, b := kb8.CurveCoefficients() return mapAtBase(slot*pqT, pqT, &b) } diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_test.go b/ecc/kb8/multiset-hash/vector_multiset_hash_test.go index 5100220eb0..43fc1f3b75 100644 --- a/ecc/kb8/multiset-hash/vector_multiset_hash_test.go +++ b/ecc/kb8/multiset-hash/vector_multiset_hash_test.go @@ -369,6 +369,22 @@ func TestPoseidon2BoundaryMessages(t *testing.T) { } } +func TestMapAtSlotRejectsOutOfRange(t *testing.T) { + bound := pqReducerBound.Uint64() + + p, _, err := MapAtSlot(bound - 1) + require.NoError(t, err) + var y big.Int + p.Y.C0.B0.A0.BigInt(&y) + require.Negative(t, y.Cmp(halfModulus())) + + _, _, err = MapAtSlot(bound) + require.ErrorIs(t, err, errPqSlotOutOfRange) + + _, _, err = MapAtSlot(math.MaxUint64/pqT + 1) + require.ErrorIs(t, err, errPqSlotOutOfRange) +} + // ----- Linear per-coordinate slot range ----- func TestLinearPerCoordinateSlotRange(t *testing.T) { From 61d70d0c555f47a0f5c3e67aced45e573e7080dd Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 21 May 2026 17:52:47 -0400 Subject: [PATCH 27/33] refactor: rename kb8 to octobear --- ecc/ecc.go | 4 +- ecc/ecc_field.go | 10 ++-- ecc/{kb8 => octobear}/fp/doc.go | 2 +- ecc/{kb8 => octobear}/fp/fp.go | 0 ecc/{kb8 => octobear}/fr/arith.go | 0 ecc/{kb8 => octobear}/fr/cbrt_extra_test.go | 0 ecc/{kb8 => octobear}/fr/doc.go | 0 ecc/{kb8 => octobear}/fr/element.go | 0 ecc/{kb8 => octobear}/fr/element_amd64.go | 0 ecc/{kb8 => octobear}/fr/element_amd64.s | 0 ecc/{kb8 => octobear}/fr/element_arm64.go | 0 ecc/{kb8 => octobear}/fr/element_arm64.s | 0 ecc/{kb8 => octobear}/fr/element_exp.go | 0 ecc/{kb8 => octobear}/fr/element_purego.go | 0 ecc/{kb8 => octobear}/fr/element_test.go | 0 ecc/{kb8 => octobear}/fr/vector.go | 0 ecc/{kb8 => octobear}/fr/vector_amd64.go | 0 ecc/{kb8 => octobear}/fr/vector_purego.go | 0 ecc/{kb8 => octobear}/fr/vector_test.go | 0 ecc/{kb8 => octobear}/g1.go | 6 +- ecc/{kb8 => octobear}/g1_test.go | 58 +++++++++---------- .../internal/fptower/fptower.go | 0 ecc/{kb8 => octobear}/marshal.go | 28 ++++----- ecc/{kb8 => octobear}/marshal_test.go | 8 +-- ecc/{kb8 => octobear}/multiexp.go | 4 +- ecc/{kb8 => octobear}/multiexp_affine.go | 4 +- ecc/{kb8 => octobear}/multiexp_jacobian.go | 2 +- ecc/{kb8 => octobear}/multiexp_test.go | 4 +- .../multiset-hash/cardano.go | 4 +- .../multiset-hash/cardano_test.go | 0 ecc/{kb8 => octobear}/multiset-hash/doc.go | 4 +- .../multiset-hash/multiset_hash.go | 28 ++++----- .../multiset-hash/multiset_hash_test.go | 4 +- .../vector_multiset_hash_linear.go | 26 ++++----- .../vector_multiset_hash_poseidon2.go | 28 ++++----- .../vector_multiset_hash_test.go | 6 +- ecc/{kb8/kb8.go => octobear/octobear.go} | 14 ++--- .../generator/config/{kb8.go => octobear.go} | 10 ++-- 38 files changed, 127 insertions(+), 127 deletions(-) rename ecc/{kb8 => octobear}/fp/doc.go (84%) rename ecc/{kb8 => octobear}/fp/fp.go (100%) rename ecc/{kb8 => octobear}/fr/arith.go (100%) rename ecc/{kb8 => octobear}/fr/cbrt_extra_test.go (100%) rename ecc/{kb8 => octobear}/fr/doc.go (100%) rename ecc/{kb8 => octobear}/fr/element.go (100%) rename ecc/{kb8 => octobear}/fr/element_amd64.go (100%) rename ecc/{kb8 => octobear}/fr/element_amd64.s (100%) rename ecc/{kb8 => octobear}/fr/element_arm64.go (100%) rename ecc/{kb8 => octobear}/fr/element_arm64.s (100%) rename ecc/{kb8 => octobear}/fr/element_exp.go (100%) rename ecc/{kb8 => octobear}/fr/element_purego.go (100%) rename ecc/{kb8 => octobear}/fr/element_test.go (100%) rename ecc/{kb8 => octobear}/fr/vector.go (100%) rename ecc/{kb8 => octobear}/fr/vector_amd64.go (100%) rename ecc/{kb8 => octobear}/fr/vector_purego.go (100%) rename ecc/{kb8 => octobear}/fr/vector_test.go (100%) rename ecc/{kb8 => octobear}/g1.go (99%) rename ecc/{kb8 => octobear}/g1_test.go (84%) rename ecc/{kb8 => octobear}/internal/fptower/fptower.go (100%) rename ecc/{kb8 => octobear}/marshal.go (96%) rename ecc/{kb8 => octobear}/marshal_test.go (97%) rename ecc/{kb8 => octobear}/multiexp.go (99%) rename ecc/{kb8 => octobear}/multiexp_affine.go (99%) rename ecc/{kb8 => octobear}/multiexp_jacobian.go (99%) rename ecc/{kb8 => octobear}/multiexp_test.go (99%) rename ecc/{kb8 => octobear}/multiset-hash/cardano.go (99%) rename ecc/{kb8 => octobear}/multiset-hash/cardano_test.go (100%) rename ecc/{kb8 => octobear}/multiset-hash/doc.go (91%) rename ecc/{kb8 => octobear}/multiset-hash/multiset_hash.go (65%) rename ecc/{kb8 => octobear}/multiset-hash/multiset_hash_test.go (97%) rename ecc/{kb8 => octobear}/multiset-hash/vector_multiset_hash_linear.go (80%) rename ecc/{kb8 => octobear}/multiset-hash/vector_multiset_hash_poseidon2.go (88%) rename ecc/{kb8 => octobear}/multiset-hash/vector_multiset_hash_test.go (99%) rename ecc/{kb8/kb8.go => octobear/octobear.go} (83%) rename internal/generator/config/{kb8.go => octobear.go} (82%) diff --git a/ecc/ecc.go b/ecc/ecc.go index 5b59f46a21..8f576dcd27 100644 --- a/ecc/ecc.go +++ b/ecc/ecc.go @@ -1,7 +1,7 @@ // Copyright 2020-2025 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Package ecc provides bls12-381, bls12-377, bn254, bw6-761, bls24-315, bls24-317, bw6-633, secp256k1, secp256r1, grumpkin, stark-curve and kb8 elliptic curves implementations. +// Package ecc provides bls12-381, bls12-377, bn254, bw6-761, bls24-315, bls24-317, bw6-633, secp256k1, secp256r1, grumpkin, stark-curve and octobear elliptic curves implementations. // // Also // @@ -32,7 +32,7 @@ const ( SECP256K1 GRUMPKIN SECP256R1 - KB8 + OCTOBEAR ) // MultiExpConfig enables to set optional configuration attribute to a call to MultiExp diff --git a/ecc/ecc_field.go b/ecc/ecc_field.go index a3bf8e85e0..c14c2ed2ac 100644 --- a/ecc/ecc_field.go +++ b/ecc/ecc_field.go @@ -20,7 +20,7 @@ var mID = map[string]ID{ "bw6_633": BW6_633, "bw6_761": BW6_761, "grumpkin": GRUMPKIN, - "kb8": KB8, + "octobear": OCTOBEAR, "secp256k1": SECP256K1, "secp256r1": SECP256R1, "stark_curve": STARK_CURVE, @@ -46,7 +46,7 @@ func (id ID) ScalarField() *big.Int { f.SetString("258664426012969094010652733694893533536393512754914660539884262666720468348340822774968888139573360124440321458177", 10) case GRUMPKIN: f.SetString("21888242871839275222246405745257275088696311157297823662689037894645226208583", 10) - case KB8: + case OCTOBEAR: f.SetString("424804331891979973455971894938199991839487883914575852667663156896715214921", 10) case SECP256K1: f.SetString("115792089237316195423570985008687907852837564279074904382605163141518161494337", 10) @@ -80,7 +80,7 @@ func (id ID) BaseField() *big.Int { f.SetString("6891450384315732539396789682275657542479668912536150109513790160209623422243491736087683183289411687640864567753786613451161759120554247759349511699125301598951605099378508850372543631423596795951899700429969112842764913119068299", 10) case GRUMPKIN: f.SetString("21888242871839275222246405745257275088548364400416034343698204186575808495617", 10) - case KB8: + case OCTOBEAR: f.SetString("2130706433", 10) case SECP256K1: f.SetString("115792089237316195423570985008687907853269984665640564039457584007908834671663", 10) @@ -113,8 +113,8 @@ func (id ID) String() string { return "bw6_761" case GRUMPKIN: return "grumpkin" - case KB8: - return "kb8" + case OCTOBEAR: + return "octobear" case SECP256K1: return "secp256k1" case SECP256R1: diff --git a/ecc/kb8/fp/doc.go b/ecc/octobear/fp/doc.go similarity index 84% rename from ecc/kb8/fp/doc.go rename to ecc/octobear/fp/doc.go index 93b14330bf..bf26bec2ad 100644 --- a/ecc/kb8/fp/doc.go +++ b/ecc/octobear/fp/doc.go @@ -6,6 +6,6 @@ // Package fp aliases github.com/consensys/gnark-crypto/field/koalabear for curves whose base field is // implemented in an existing top-level field package. // -// It exists so packages under ecc/kb8 can use the same local fp import +// It exists so packages under ecc/octobear can use the same local fp import // shape as curves with generated base fields. package fp diff --git a/ecc/kb8/fp/fp.go b/ecc/octobear/fp/fp.go similarity index 100% rename from ecc/kb8/fp/fp.go rename to ecc/octobear/fp/fp.go diff --git a/ecc/kb8/fr/arith.go b/ecc/octobear/fr/arith.go similarity index 100% rename from ecc/kb8/fr/arith.go rename to ecc/octobear/fr/arith.go diff --git a/ecc/kb8/fr/cbrt_extra_test.go b/ecc/octobear/fr/cbrt_extra_test.go similarity index 100% rename from ecc/kb8/fr/cbrt_extra_test.go rename to ecc/octobear/fr/cbrt_extra_test.go diff --git a/ecc/kb8/fr/doc.go b/ecc/octobear/fr/doc.go similarity index 100% rename from ecc/kb8/fr/doc.go rename to ecc/octobear/fr/doc.go diff --git a/ecc/kb8/fr/element.go b/ecc/octobear/fr/element.go similarity index 100% rename from ecc/kb8/fr/element.go rename to ecc/octobear/fr/element.go diff --git a/ecc/kb8/fr/element_amd64.go b/ecc/octobear/fr/element_amd64.go similarity index 100% rename from ecc/kb8/fr/element_amd64.go rename to ecc/octobear/fr/element_amd64.go diff --git a/ecc/kb8/fr/element_amd64.s b/ecc/octobear/fr/element_amd64.s similarity index 100% rename from ecc/kb8/fr/element_amd64.s rename to ecc/octobear/fr/element_amd64.s diff --git a/ecc/kb8/fr/element_arm64.go b/ecc/octobear/fr/element_arm64.go similarity index 100% rename from ecc/kb8/fr/element_arm64.go rename to ecc/octobear/fr/element_arm64.go diff --git a/ecc/kb8/fr/element_arm64.s b/ecc/octobear/fr/element_arm64.s similarity index 100% rename from ecc/kb8/fr/element_arm64.s rename to ecc/octobear/fr/element_arm64.s diff --git a/ecc/kb8/fr/element_exp.go b/ecc/octobear/fr/element_exp.go similarity index 100% rename from ecc/kb8/fr/element_exp.go rename to ecc/octobear/fr/element_exp.go diff --git a/ecc/kb8/fr/element_purego.go b/ecc/octobear/fr/element_purego.go similarity index 100% rename from ecc/kb8/fr/element_purego.go rename to ecc/octobear/fr/element_purego.go diff --git a/ecc/kb8/fr/element_test.go b/ecc/octobear/fr/element_test.go similarity index 100% rename from ecc/kb8/fr/element_test.go rename to ecc/octobear/fr/element_test.go diff --git a/ecc/kb8/fr/vector.go b/ecc/octobear/fr/vector.go similarity index 100% rename from ecc/kb8/fr/vector.go rename to ecc/octobear/fr/vector.go diff --git a/ecc/kb8/fr/vector_amd64.go b/ecc/octobear/fr/vector_amd64.go similarity index 100% rename from ecc/kb8/fr/vector_amd64.go rename to ecc/octobear/fr/vector_amd64.go diff --git a/ecc/kb8/fr/vector_purego.go b/ecc/octobear/fr/vector_purego.go similarity index 100% rename from ecc/kb8/fr/vector_purego.go rename to ecc/octobear/fr/vector_purego.go diff --git a/ecc/kb8/fr/vector_test.go b/ecc/octobear/fr/vector_test.go similarity index 100% rename from ecc/kb8/fr/vector_test.go rename to ecc/octobear/fr/vector_test.go diff --git a/ecc/kb8/g1.go b/ecc/octobear/g1.go similarity index 99% rename from ecc/kb8/g1.go rename to ecc/octobear/g1.go index 13657aaec3..458c176ebb 100644 --- a/ecc/kb8/g1.go +++ b/ecc/octobear/g1.go @@ -1,7 +1,7 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear import ( "crypto/rand" @@ -10,8 +10,8 @@ import ( "sync/atomic" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/kb8/fr" - "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + "github.com/consensys/gnark-crypto/ecc/octobear/fr" + "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" "github.com/consensys/gnark-crypto/parallel" ) diff --git a/ecc/kb8/g1_test.go b/ecc/octobear/g1_test.go similarity index 84% rename from ecc/kb8/g1_test.go rename to ecc/octobear/g1_test.go index 50fe725d0d..745638df80 100644 --- a/ecc/kb8/g1_test.go +++ b/ecc/octobear/g1_test.go @@ -1,7 +1,7 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear import ( crand "crypto/rand" @@ -10,9 +10,9 @@ import ( "math/rand/v2" "testing" - "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" - "github.com/consensys/gnark-crypto/ecc/kb8/fr" + "github.com/consensys/gnark-crypto/ecc/octobear/fr" "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" @@ -29,7 +29,7 @@ func TestIsOnG1(t *testing.T) { properties := gopter.NewProperties(parameters) - properties.Property("[KB8] g1Gen (affine) should be on the curve", prop.ForAll( + properties.Property("[OCTOBEAR] g1Gen (affine) should be on the curve", prop.ForAll( func(a fptower.E8) bool { var op1, op2 G1Affine op1.FromJacobian(&g1Gen) @@ -40,7 +40,7 @@ func TestIsOnG1(t *testing.T) { GenE8(), )) - properties.Property("[KB8] g1Gen (Jacobian) should be on the curve", prop.ForAll( + properties.Property("[OCTOBEAR] g1Gen (Jacobian) should be on the curve", prop.ForAll( func(a fptower.E8) bool { var op1, op2, op3 G1Jac op1.Set(&g1Gen) @@ -53,7 +53,7 @@ func TestIsOnG1(t *testing.T) { GenE8(), )) - properties.Property("[KB8] IsInSubGroup and MulBy subgroup order should be the same", prop.ForAll( + properties.Property("[OCTOBEAR] IsInSubGroup and MulBy subgroup order should be the same", prop.ForAll( func(a fptower.E8) bool { var op1, op2 G1Jac op1 = fuzzG1Jac(&g1Gen, a) @@ -81,7 +81,7 @@ func TestIsInSubGroupBatchG1(t *testing.T) { // number of points to test const nbSamples = 100 - properties.Property("[KB8] IsInSubGroupBatchG1 test should pass with high probability", prop.ForAll( + properties.Property("[OCTOBEAR] IsInSubGroupBatchG1 test should pass with high probability", prop.ForAll( func(mixer fr.Element) bool { // mixer ensures that all the words of a frElement are set var sampleScalars [nbSamples]fr.Element @@ -113,7 +113,7 @@ func TestG1Conversions(t *testing.T) { properties := gopter.NewProperties(parameters) - properties.Property("[KB8] Affine representation should be independent of the Jacobian representative", prop.ForAll( + properties.Property("[OCTOBEAR] Affine representation should be independent of the Jacobian representative", prop.ForAll( func(a fptower.E8) bool { g := fuzzG1Jac(&g1Gen, a) var op1 G1Affine @@ -123,7 +123,7 @@ func TestG1Conversions(t *testing.T) { GenE8(), )) - properties.Property("[KB8] Affine representation should be independent of a Extended Jacobian representative", prop.ForAll( + properties.Property("[OCTOBEAR] Affine representation should be independent of a Extended Jacobian representative", prop.ForAll( func(a fptower.E8) bool { var g g1JacExtended g.X.Set(&g1Gen.X) @@ -139,7 +139,7 @@ func TestG1Conversions(t *testing.T) { GenE8(), )) - properties.Property("[KB8] Jacobian representation should be the same as the affine representative", prop.ForAll( + properties.Property("[OCTOBEAR] Jacobian representation should be the same as the affine representative", prop.ForAll( func(a fptower.E8) bool { var g G1Jac var op1 G1Affine @@ -156,7 +156,7 @@ func TestG1Conversions(t *testing.T) { GenE8(), )) - properties.Property("[KB8] Converting affine symbol for infinity to Jacobian should output correct infinity in Jacobian", prop.ForAll( + properties.Property("[OCTOBEAR] Converting affine symbol for infinity to Jacobian should output correct infinity in Jacobian", prop.ForAll( func() bool { var g G1Affine g.X.SetZero() @@ -169,7 +169,7 @@ func TestG1Conversions(t *testing.T) { }, )) - properties.Property("[KB8] Converting infinity in extended Jacobian to affine should output infinity symbol in Affine", prop.ForAll( + properties.Property("[OCTOBEAR] Converting infinity in extended Jacobian to affine should output infinity symbol in Affine", prop.ForAll( func() bool { var g G1Affine var op1 g1JacExtended @@ -181,7 +181,7 @@ func TestG1Conversions(t *testing.T) { }, )) - properties.Property("[KB8] Converting infinity in extended Jacobian to Jacobian should output infinity in Jacobian", prop.ForAll( + properties.Property("[OCTOBEAR] Converting infinity in extended Jacobian to Jacobian should output infinity in Jacobian", prop.ForAll( func() bool { var g G1Jac var op1 g1JacExtended @@ -194,7 +194,7 @@ func TestG1Conversions(t *testing.T) { }, )) - properties.Property("[KB8] [Jacobian] Two representatives of the same class should be equal", prop.ForAll( + properties.Property("[OCTOBEAR] [Jacobian] Two representatives of the same class should be equal", prop.ForAll( func(a, b fptower.E8) bool { op1 := fuzzG1Jac(&g1Gen, a) op2 := fuzzG1Jac(&g1Gen, b) @@ -203,7 +203,7 @@ func TestG1Conversions(t *testing.T) { GenE8(), GenE8(), )) - properties.Property("[KB8] BatchJacobianToAffineG1 and FromJacobian should output the same result", prop.ForAll( + properties.Property("[OCTOBEAR] BatchJacobianToAffineG1 and FromJacobian should output the same result", prop.ForAll( func(a, b fptower.E8) bool { g1 := fuzzG1Jac(&g1Gen, a) g2 := fuzzG1Jac(&g1Gen, b) @@ -229,7 +229,7 @@ func TestG1AffineOps(t *testing.T) { genScalar := GenFr() - properties.Property("[KB8] Add(P,-P) should return the point at infinity", prop.ForAll( + properties.Property("[OCTOBEAR] Add(P,-P) should return the point at infinity", prop.ForAll( func(s fr.Element) bool { var op1, op2 G1Affine var sInt big.Int @@ -245,7 +245,7 @@ func TestG1AffineOps(t *testing.T) { GenFr(), )) - properties.Property("[KB8] Add(P,0) and Add(0,P) should return P", prop.ForAll( + properties.Property("[OCTOBEAR] Add(P,0) and Add(0,P) should return P", prop.ForAll( func(s fr.Element) bool { var op1, op2 G1Affine var sInt big.Int @@ -262,7 +262,7 @@ func TestG1AffineOps(t *testing.T) { GenFr(), )) - properties.Property("[KB8] Add should call double when adding the same point", prop.ForAll( + properties.Property("[OCTOBEAR] Add should call double when adding the same point", prop.ForAll( func(s fr.Element) bool { var op1, op2 G1Affine var sInt big.Int @@ -278,7 +278,7 @@ func TestG1AffineOps(t *testing.T) { GenFr(), )) - properties.Property("[KB8] [2]G = double(G) + G - G", prop.ForAll( + properties.Property("[OCTOBEAR] [2]G = double(G) + G - G", prop.ForAll( func(s fr.Element) bool { var sInt big.Int g := g1GenAff @@ -294,7 +294,7 @@ func TestG1AffineOps(t *testing.T) { GenFr(), )) - properties.Property("[KB8] [-s]G = -[s]G", prop.ForAll( + properties.Property("[OCTOBEAR] [-s]G = -[s]G", prop.ForAll( func(s fr.Element) bool { g := g1GenAff var gj G1Jac @@ -326,7 +326,7 @@ func TestG1AffineOps(t *testing.T) { GenFr(), )) - properties.Property("[KB8] [Jacobian] Add should call double when adding the same point", prop.ForAll( + properties.Property("[OCTOBEAR] [Jacobian] Add should call double when adding the same point", prop.ForAll( func(a, b fptower.E8) bool { fop1 := fuzzG1Jac(&g1Gen, a) fop2 := fuzzG1Jac(&g1Gen, b) @@ -339,7 +339,7 @@ func TestG1AffineOps(t *testing.T) { GenE8(), )) - properties.Property("[KB8] [Jacobian] Adding the opposite of a point to itself should output inf", prop.ForAll( + properties.Property("[OCTOBEAR] [Jacobian] Adding the opposite of a point to itself should output inf", prop.ForAll( func(a, b fptower.E8) bool { fop1 := fuzzG1Jac(&g1Gen, a) fop2 := fuzzG1Jac(&g1Gen, b) @@ -351,7 +351,7 @@ func TestG1AffineOps(t *testing.T) { GenE8(), )) - properties.Property("[KB8] [Jacobian] Adding the inf to a point should not modify the point", prop.ForAll( + properties.Property("[OCTOBEAR] [Jacobian] Adding the inf to a point should not modify the point", prop.ForAll( func(a fptower.E8) bool { fop1 := fuzzG1Jac(&g1Gen, a) fop1.AddAssign(&g1Infinity) @@ -363,7 +363,7 @@ func TestG1AffineOps(t *testing.T) { GenE8(), )) - properties.Property("[KB8] [Jacobian Extended] addMixed (-G) should equal subMixed(G)", prop.ForAll( + properties.Property("[OCTOBEAR] [Jacobian Extended] addMixed (-G) should equal subMixed(G)", prop.ForAll( func(a fptower.E8) bool { fop1 := fuzzG1Jac(&g1Gen, a) var p1, p1Neg G1Affine @@ -382,7 +382,7 @@ func TestG1AffineOps(t *testing.T) { GenE8(), )) - properties.Property("[KB8] [Jacobian Extended] doubleMixed (-G) should equal doubleNegMixed(G)", prop.ForAll( + properties.Property("[OCTOBEAR] [Jacobian Extended] doubleMixed (-G) should equal doubleNegMixed(G)", prop.ForAll( func(a fptower.E8) bool { fop1 := fuzzG1Jac(&g1Gen, a) var p1, p1Neg G1Affine @@ -401,7 +401,7 @@ func TestG1AffineOps(t *testing.T) { GenE8(), )) - properties.Property("[KB8] [Jacobian] Addmix the negation to itself should output 0", prop.ForAll( + properties.Property("[OCTOBEAR] [Jacobian] Addmix the negation to itself should output 0", prop.ForAll( func(a fptower.E8) bool { fop1 := fuzzG1Jac(&g1Gen, a) fop1.Neg(&fop1) @@ -413,7 +413,7 @@ func TestG1AffineOps(t *testing.T) { GenE8(), )) - properties.Property("[KB8] scalar multiplication (double and add) should depend only on the scalar mod r", prop.ForAll( + properties.Property("[OCTOBEAR] scalar multiplication (double and add) should depend only on the scalar mod r", prop.ForAll( func(s fr.Element) bool { r := fr.Modulus() @@ -436,7 +436,7 @@ func TestG1AffineOps(t *testing.T) { genScalar, )) - properties.Property("[KB8] JointScalarMultiplicationBase and ScalarMultiplication should output the same results", prop.ForAll( + properties.Property("[OCTOBEAR] JointScalarMultiplicationBase and ScalarMultiplication should output the same results", prop.ForAll( func(s1, s2 fr.Element) bool { var op1, op2, temp G1Jac @@ -472,7 +472,7 @@ func TestG1BatchScalarMultiplication(t *testing.T) { // size of the multiExps const nbSamples = 10 - properties.Property("[KB8] BatchScalarMultiplication should be consistent with individual scalar multiplications", prop.ForAll( + properties.Property("[OCTOBEAR] BatchScalarMultiplication should be consistent with individual scalar multiplications", prop.ForAll( func(mixer fr.Element) bool { // mixer ensures that all the words of a fpElement are set var sampleScalars [nbSamples]fr.Element diff --git a/ecc/kb8/internal/fptower/fptower.go b/ecc/octobear/internal/fptower/fptower.go similarity index 100% rename from ecc/kb8/internal/fptower/fptower.go rename to ecc/octobear/internal/fptower/fptower.go diff --git a/ecc/kb8/marshal.go b/ecc/octobear/marshal.go similarity index 96% rename from ecc/kb8/marshal.go rename to ecc/octobear/marshal.go index 2a1c2f6d48..104c1b8fab 100644 --- a/ecc/kb8/marshal.go +++ b/ecc/octobear/marshal.go @@ -1,7 +1,7 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear import ( "encoding/binary" @@ -10,9 +10,9 @@ import ( "reflect" "sync/atomic" - "github.com/consensys/gnark-crypto/ecc/kb8/fp" - "github.com/consensys/gnark-crypto/ecc/kb8/fr" - "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + "github.com/consensys/gnark-crypto/ecc/octobear/fp" + "github.com/consensys/gnark-crypto/ecc/octobear/fr" + "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" "github.com/consensys/gnark-crypto/parallel" ) @@ -32,21 +32,21 @@ var ( ErrInvalidEncoding = errors.New("invalid point encoding") ) -// Encoder writes kb8 object values to an output stream +// Encoder writes octobear object values to an output stream type Encoder struct { w io.Writer n int64 // written bytes raw bool // raw vs compressed encoding } -// Decoder reads kb8 object values from an inbound stream +// Decoder reads octobear object values from an inbound stream type Decoder struct { r io.Reader n int64 // read bytes subGroupCheck bool // default to true } -// NewDecoder returns a binary decoder supporting curve kb8 objects in both +// NewDecoder returns a binary decoder supporting curve octobear objects in both // compressed and uncompressed (raw) forms func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { d := &Decoder{r: r, subGroupCheck: true} @@ -63,7 +63,7 @@ func NewDecoder(r io.Reader, options ...func(*Decoder)) *Decoder { func (dec *Decoder) Decode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || rv.Kind() != reflect.Ptr || rv.IsNil() || !rv.Elem().CanSet() { - return errors.New("kb8 decoder: unsupported type, need pointer") + return errors.New("octobear decoder: unsupported type, need pointer") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -263,7 +263,7 @@ func (dec *Decoder) Decode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New("kb8 encoder: unsupported type") + return errors.New("octobear encoder: unsupported type") } err = binary.Read(dec.r, binary.BigEndian, t) if err == nil { @@ -317,7 +317,7 @@ func isCompressed(msb byte) bool { return mData != mUncompressed && mData != mUncompressedInfinity } -// NewEncoder returns a binary encoder supporting curve kb8 objects +// NewEncoder returns a binary encoder supporting curve octobear objects func NewEncoder(w io.Writer, options ...func(*Encoder)) *Encoder { // default settings enc := &Encoder{ @@ -380,7 +380,7 @@ func isZeroed(firstByte byte, buf []byte) bool { func (enc *Encoder) encode(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New("kb8 encoder: can't encode ") + return errors.New("octobear encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -484,7 +484,7 @@ func (enc *Encoder) encode(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New("kb8 encoder: unsupported type") + return errors.New("octobear encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) @@ -495,7 +495,7 @@ func (enc *Encoder) encode(v any) (err error) { func (enc *Encoder) encodeRaw(v any) (err error) { rv := reflect.ValueOf(v) if v == nil || (rv.Kind() == reflect.Ptr && rv.IsNil()) { - return errors.New("kb8 encoder: can't encode ") + return errors.New("octobear encoder: can't encode ") } // implementation note: code is a bit verbose (abusing code generation), but minimize allocations on the heap @@ -599,7 +599,7 @@ func (enc *Encoder) encodeRaw(v any) (err error) { default: n := binary.Size(t) if n == -1 { - return errors.New("kb8 encoder: unsupported type") + return errors.New("octobear encoder: unsupported type") } err = binary.Write(enc.w, binary.BigEndian, t) enc.n += int64(n) diff --git a/ecc/kb8/marshal_test.go b/ecc/octobear/marshal_test.go similarity index 97% rename from ecc/kb8/marshal_test.go rename to ecc/octobear/marshal_test.go index 3f086f510d..6ebeace289 100644 --- a/ecc/kb8/marshal_test.go +++ b/ecc/octobear/marshal_test.go @@ -1,7 +1,7 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear import ( "bytes" @@ -15,9 +15,9 @@ import ( "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" - "github.com/consensys/gnark-crypto/ecc/kb8/fp" - "github.com/consensys/gnark-crypto/ecc/kb8/fr" - "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + "github.com/consensys/gnark-crypto/ecc/octobear/fp" + "github.com/consensys/gnark-crypto/ecc/octobear/fr" + "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" ) const ( diff --git a/ecc/kb8/multiexp.go b/ecc/octobear/multiexp.go similarity index 99% rename from ecc/kb8/multiexp.go rename to ecc/octobear/multiexp.go index 4dc10b8d43..7682c8aca4 100644 --- a/ecc/kb8/multiexp.go +++ b/ecc/octobear/multiexp.go @@ -1,7 +1,7 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear import ( "errors" @@ -9,7 +9,7 @@ import ( "runtime" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/kb8/fr" + "github.com/consensys/gnark-crypto/ecc/octobear/fr" "github.com/consensys/gnark-crypto/parallel" ) diff --git a/ecc/kb8/multiexp_affine.go b/ecc/octobear/multiexp_affine.go similarity index 99% rename from ecc/kb8/multiexp_affine.go rename to ecc/octobear/multiexp_affine.go index d8a59e11c7..2c97d513f0 100644 --- a/ecc/kb8/multiexp_affine.go +++ b/ecc/octobear/multiexp_affine.go @@ -1,9 +1,9 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear -import "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" +import "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" type batchOpG1Affine struct { bucketID uint16 diff --git a/ecc/kb8/multiexp_jacobian.go b/ecc/octobear/multiexp_jacobian.go similarity index 99% rename from ecc/kb8/multiexp_jacobian.go rename to ecc/octobear/multiexp_jacobian.go index fb4bf80082..201d8e2235 100644 --- a/ecc/kb8/multiexp_jacobian.go +++ b/ecc/octobear/multiexp_jacobian.go @@ -1,7 +1,7 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear func processChunkG1Jacobian[B ibg1JacExtended](chunk uint64, chRes chan<- g1JacExtended, diff --git a/ecc/kb8/multiexp_test.go b/ecc/octobear/multiexp_test.go similarity index 99% rename from ecc/kb8/multiexp_test.go rename to ecc/octobear/multiexp_test.go index 5a577074a2..86a1f27435 100644 --- a/ecc/kb8/multiexp_test.go +++ b/ecc/octobear/multiexp_test.go @@ -1,7 +1,7 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -package kb8 +package octobear import ( "fmt" @@ -13,7 +13,7 @@ import ( "testing" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/kb8/fr" + "github.com/consensys/gnark-crypto/ecc/octobear/fr" "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" ) diff --git a/ecc/kb8/multiset-hash/cardano.go b/ecc/octobear/multiset-hash/cardano.go similarity index 99% rename from ecc/kb8/multiset-hash/cardano.go rename to ecc/octobear/multiset-hash/cardano.go index f4eddc1d1d..fad000a943 100644 --- a/ecc/kb8/multiset-hash/cardano.go +++ b/ecc/octobear/multiset-hash/cardano.go @@ -8,7 +8,7 @@ import ( "github.com/consensys/gnark-crypto/field/koalabear/extensions" ) -// Cardano solver for the depressed cubic x^3 - 3x + c = 0 over kb8 Fp^8. +// Cardano solver for the depressed cubic x^3 - 3x + c = 0 over octobear Fp^8. // This mirrors the structure of the secp256r1 Cardano solver in PR #831: // repeated-root case, square-discriminant case over the base field, and a // quadratic-extension fallback when the discriminant is not a square. @@ -260,7 +260,7 @@ func findPrimitiveCubeRoot() extensions.E8 { return w } } - panic("kb8 multiset hash: failed to find primitive cube root in Fp^8") + panic("octobear multiset hash: failed to find primitive cube root in Fp^8") } func e8SearchCandidates() []extensions.E8 { diff --git a/ecc/kb8/multiset-hash/cardano_test.go b/ecc/octobear/multiset-hash/cardano_test.go similarity index 100% rename from ecc/kb8/multiset-hash/cardano_test.go rename to ecc/octobear/multiset-hash/cardano_test.go diff --git a/ecc/kb8/multiset-hash/doc.go b/ecc/octobear/multiset-hash/doc.go similarity index 91% rename from ecc/kb8/multiset-hash/doc.go rename to ecc/octobear/multiset-hash/doc.go index c38c4d842b..bc6eb36c4a 100644 --- a/ecc/kb8/multiset-hash/doc.go +++ b/ecc/octobear/multiset-hash/doc.go @@ -1,12 +1,12 @@ // Package multisethash implements y-increment elliptic-curve multiset -// hashing (ECMSH) over kb8. +// hashing (ECMSH) over octobear. // // The package exposes three variants: // // - Classical one-point ECMSH (Accumulator, Hash, Map): 16-bit messages. // Each message m is mapped by scanning k in [0, 256) and setting // y = m*256 + k in the base subfield of Fp^8. The first resulting point -// (x, y) on kb8 is used as the image. Security is ~124 classical bits +// (x, y) on octobear is used as the image. Security is ~124 classical bits // (no post-quantum security). // // - Linear-separator vector ECMSH (LinearAccumulator, HashLinear, diff --git a/ecc/kb8/multiset-hash/multiset_hash.go b/ecc/octobear/multiset-hash/multiset_hash.go similarity index 65% rename from ecc/kb8/multiset-hash/multiset_hash.go rename to ecc/octobear/multiset-hash/multiset_hash.go index e506768494..894dc804de 100644 --- a/ecc/kb8/multiset-hash/multiset_hash.go +++ b/ecc/octobear/multiset-hash/multiset_hash.go @@ -3,17 +3,17 @@ package multisethash import ( "errors" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" ) const tweakBound = 256 -var errMapFailure = errors.New("kb8 multiset hash: failed to map message after 256 y-increments") +var errMapFailure = errors.New("octobear multiset hash: failed to map message after 256 y-increments") // Accumulator stores an additive multiset hash state in affine coordinates. type Accumulator struct { - sum kb8.G1Affine + sum octobear.G1Affine } // NewAccumulator returns a zero accumulator. @@ -23,7 +23,7 @@ func NewAccumulator() Accumulator { return a } -// Insert maps msg to kb8 and adds it to the accumulator. +// Insert maps msg to octobear and adds it to the accumulator. func (a *Accumulator) Insert(msg uint16) error { p, _, err := Map(msg) if err != nil { @@ -33,7 +33,7 @@ func (a *Accumulator) Insert(msg uint16) error { return nil } -// Remove maps msg to kb8 and subtracts it from the accumulator. +// Remove maps msg to octobear and subtracts it from the accumulator. func (a *Accumulator) Remove(msg uint16) error { p, _, err := Map(msg) if err != nil { @@ -45,7 +45,7 @@ func (a *Accumulator) Remove(msg uint16) error { } // Digest returns the current accumulator state in affine coordinates. -func (a *Accumulator) Digest() kb8.G1Affine { +func (a *Accumulator) Digest() octobear.G1Affine { return a.sum } @@ -55,21 +55,21 @@ func (a *Accumulator) Reset() { } // Hash returns the multiset hash of msgs. -func Hash(msgs []uint16) (kb8.G1Affine, error) { +func Hash(msgs []uint16) (octobear.G1Affine, error) { acc := NewAccumulator() for _, msg := range msgs { if err := acc.Insert(msg); err != nil { - return kb8.G1Affine{}, err + return octobear.G1Affine{}, err } } return acc.Digest(), nil } -// Map deterministically maps msg to a point on kb8 using the y-increment method. +// Map deterministically maps msg to a point on octobear using the y-increment method. // It returns the mapped point and the first offset k in [0, 255] such that -// y = msg*256 + k yields a point (x, y) on kb8. -func Map(msg uint16) (kb8.G1Affine, uint8, error) { - _, b := kb8.CurveCoefficients() +// y = msg*256 + k yields a point (x, y) on octobear. +func Map(msg uint16) (octobear.G1Affine, uint8, error) { + _, b := octobear.CurveCoefficients() baseY := uint64(msg) * tweakBound for k := uint16(0); k < tweakBound; k++ { @@ -85,11 +85,11 @@ func Map(msg uint16) (kb8.G1Affine, uint8, error) { continue } - p := kb8.G1Affine{X: x, Y: y} + p := octobear.G1Affine{X: x, Y: y} if p.IsOnCurve() && p.IsInSubGroup() { return p, uint8(k), nil } } - return kb8.G1Affine{}, 0, errMapFailure + return octobear.G1Affine{}, 0, errMapFailure } diff --git a/ecc/kb8/multiset-hash/multiset_hash_test.go b/ecc/octobear/multiset-hash/multiset_hash_test.go similarity index 97% rename from ecc/kb8/multiset-hash/multiset_hash_test.go rename to ecc/octobear/multiset-hash/multiset_hash_test.go index 226afaa222..5b4cba8847 100644 --- a/ecc/kb8/multiset-hash/multiset_hash_test.go +++ b/ecc/octobear/multiset-hash/multiset_hash_test.go @@ -3,7 +3,7 @@ package multisethash import ( "testing" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" "github.com/stretchr/testify/require" ) @@ -112,7 +112,7 @@ func TestDigestRoundTrip(t *testing.T) { got, err := Hash(msgs) require.NoError(t, err) buf := got.Bytes() - var dec kb8.G1Affine + var dec octobear.G1Affine _, err = dec.SetBytes(buf[:]) require.NoError(t, err) require.True(t, dec.Equal(&got)) diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_linear.go b/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go similarity index 80% rename from ecc/kb8/multiset-hash/vector_multiset_hash_linear.go rename to ecc/octobear/multiset-hash/vector_multiset_hash_linear.go index 5035bd3f9a..32c4d10985 100644 --- a/ecc/kb8/multiset-hash/vector_multiset_hash_linear.go +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark-crypto/field/koalabear/extensions" ) @@ -23,12 +23,12 @@ const ( linearM = 1 << 18 ) -var errLinearMsgOutOfRange = fmt.Errorf("kb8 vector multiset hash: linear message must be < 2^18 (= %d)", linearM) +var errLinearMsgOutOfRange = fmt.Errorf("octobear vector multiset hash: linear message must be < 2^18 (= %d)", linearM) // LinearAccumulator holds the N affine accumulator points for the // linear-separator vector ECMSH. type LinearAccumulator struct { - sum [linearN]kb8.G1Affine + sum [linearN]octobear.G1Affine } // NewLinearAccumulator returns a zero (all-infinity) LinearAccumulator. @@ -60,7 +60,7 @@ func (a *LinearAccumulator) Remove(msg uint32) error { if err != nil { return err } - var neg kb8.G1Affine + var neg octobear.G1Affine for i := range a.sum { neg.Neg(&pts[i]) a.sum[i].Add(&a.sum[i], &neg) @@ -69,7 +69,7 @@ func (a *LinearAccumulator) Remove(msg uint32) error { } // Digest returns the current vector of accumulator points. -func (a *LinearAccumulator) Digest() [linearN]kb8.G1Affine { +func (a *LinearAccumulator) Digest() [linearN]octobear.G1Affine { return a.sum } @@ -81,11 +81,11 @@ func (a *LinearAccumulator) Reset() { } // HashLinear returns the linear-separator vector ECMSH of msgs. -func HashLinear(msgs []uint32) ([linearN]kb8.G1Affine, error) { +func HashLinear(msgs []uint32) ([linearN]octobear.G1Affine, error) { acc := NewLinearAccumulator() for _, msg := range msgs { if err := acc.Insert(msg); err != nil { - return [linearN]kb8.G1Affine{}, err + return [linearN]octobear.G1Affine{}, err } } return acc.Digest(), nil @@ -94,15 +94,15 @@ func HashLinear(msgs []uint32) ([linearN]kb8.G1Affine, error) { // MapLinear deterministically maps msg to N curve points using the linear // domain separator y_i(msg, k) = T*(msg + i*M) + k. It returns the N points // and the offsets k_i in [0, T) that produced them. -func MapLinear(msg uint32) ([linearN]kb8.G1Affine, [linearN]uint8, error) { +func MapLinear(msg uint32) ([linearN]octobear.G1Affine, [linearN]uint8, error) { var ( - pts [linearN]kb8.G1Affine + pts [linearN]octobear.G1Affine offsets [linearN]uint8 ) if uint64(msg) >= linearM { return pts, offsets, errLinearMsgOutOfRange } - _, b := kb8.CurveCoefficients() + _, b := octobear.CurveCoefficients() for i := 0; i < linearN; i++ { baseY := (uint64(msg) + uint64(i)*linearM) * linearT p, k, err := mapAtBase(baseY, linearT, &b) @@ -118,7 +118,7 @@ func MapLinear(msg uint32) ([linearN]kb8.G1Affine, [linearN]uint8, error) { // mapAtBase scans k in [0, tweakBound) and returns the first curve point // whose ordinate is y = baseY + k in the base subfield. baseY + tweakBound // must remain strictly below p/2 to keep the image inverse-free. -func mapAtBase(baseY uint64, tweakBound uint64, b *extensions.E8) (kb8.G1Affine, uint8, error) { +func mapAtBase(baseY uint64, tweakBound uint64, b *extensions.E8) (octobear.G1Affine, uint8, error) { for k := uint64(0); k < tweakBound; k++ { var y, c, ySquared extensions.E8 y.C0.B0.A0.SetUint64(baseY + k) @@ -131,10 +131,10 @@ func mapAtBase(baseY uint64, tweakBound uint64, b *extensions.E8) (kb8.G1Affine, continue } - p := kb8.G1Affine{X: x, Y: y} + p := octobear.G1Affine{X: x, Y: y} if p.IsOnCurve() && p.IsInSubGroup() { return p, uint8(k), nil } } - return kb8.G1Affine{}, 0, errors.New("kb8 vector multiset hash: failed to map message in tweak window") + return octobear.G1Affine{}, 0, errors.New("octobear vector multiset hash: failed to map message in tweak window") } diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go similarity index 88% rename from ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go rename to ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go index a95d995105..1eca624552 100644 --- a/ecc/kb8/multiset-hash/vector_multiset_hash_poseidon2.go +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go @@ -6,7 +6,7 @@ import ( "math/big" "sync" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark-crypto/field/koalabear/poseidon2" ) @@ -35,7 +35,7 @@ var pqDomainTag = [8]byte{'E', 'C', 'M', 'S', 'H', '_', 'P', 'Q'} // extracted from each squeezed koalabear element. With p = 2^31 - 2^24 + 1 // and T = 256, this is floor(2130706433 / 512) = 4161536. var ( - errPqSlotOutOfRange = errors.New("kb8 vector multiset hash: Poseidon2 slot out of range") + errPqSlotOutOfRange = errors.New("octobear vector multiset hash: Poseidon2 slot out of range") pqReducerBound = func() *big.Int { p := koalabear.Modulus() @@ -59,7 +59,7 @@ func pqPerm() *poseidon2.Permutation { // Poseidon2Accumulator holds the N affine accumulator points for the // Poseidon2-sponge vector ECMSH. type Poseidon2Accumulator struct { - sum [pqN]kb8.G1Affine + sum [pqN]octobear.G1Affine } // NewPoseidon2Accumulator returns a zero (all-infinity) Poseidon2Accumulator. @@ -91,7 +91,7 @@ func (a *Poseidon2Accumulator) Remove(msg uint64) error { if err != nil { return err } - var neg kb8.G1Affine + var neg octobear.G1Affine for i := range a.sum { neg.Neg(&pts[i]) a.sum[i].Add(&a.sum[i], &neg) @@ -100,7 +100,7 @@ func (a *Poseidon2Accumulator) Remove(msg uint64) error { } // Digest returns the current vector of accumulator points. -func (a *Poseidon2Accumulator) Digest() [pqN]kb8.G1Affine { +func (a *Poseidon2Accumulator) Digest() [pqN]octobear.G1Affine { return a.sum } @@ -112,11 +112,11 @@ func (a *Poseidon2Accumulator) Reset() { } // HashPoseidon2 returns the Poseidon2-sponge vector ECMSH of msgs. -func HashPoseidon2(msgs []uint64) ([pqN]kb8.G1Affine, error) { +func HashPoseidon2(msgs []uint64) ([pqN]octobear.G1Affine, error) { acc := NewPoseidon2Accumulator() for _, msg := range msgs { if err := acc.Insert(msg); err != nil { - return [pqN]kb8.G1Affine{}, err + return [pqN]octobear.G1Affine{}, err } } return acc.Digest(), nil @@ -125,9 +125,9 @@ func HashPoseidon2(msgs []uint64) ([pqN]kb8.G1Affine, error) { // MapPoseidon2 deterministically maps msg to N curve points using a // Poseidon2 sponge over the koalabear field. It returns the N points and // the per-coordinate tweak offsets k_i in [0, T) that produced them. -func MapPoseidon2(msg uint64) ([pqN]kb8.G1Affine, [pqN]uint8, error) { +func MapPoseidon2(msg uint64) ([pqN]octobear.G1Affine, [pqN]uint8, error) { var ( - pts [pqN]kb8.G1Affine + pts [pqN]octobear.G1Affine offsets [pqN]uint8 ) @@ -136,7 +136,7 @@ func MapPoseidon2(msg uint64) ([pqN]kb8.G1Affine, [pqN]uint8, error) { return pts, offsets, err } - _, b := kb8.CurveCoefficients() + _, b := octobear.CurveCoefficients() var tmp big.Int for i := 0; i < pqN; i++ { squeezed[i].BigInt(&tmp) @@ -156,15 +156,15 @@ func MapPoseidon2(msg uint64) ([pqN]kb8.G1Affine, [pqN]uint8, error) { // MapAtSlot is a public helper used by the gnark in-circuit Poseidon2 vector // ECMSH gadget. Given a slot s = u mod ⌊p/(2T)⌋ (already range-reduced by the // caller — typically the in-circuit code after a Poseidon2 squeeze), it scans -// k in [0, pqT) and returns the first kb8 curve point whose ordinate is +// k in [0, pqT) and returns the first octobear curve point whose ordinate is // y = pqT*s + k in the base subfield. The slot must satisfy // pqT*s + (pqT-1) < p/2 to preserve inverse-freeness; this is automatic when // s < ⌊p/(2T)⌋. -func MapAtSlot(slot uint64) (kb8.G1Affine, uint8, error) { +func MapAtSlot(slot uint64) (octobear.G1Affine, uint8, error) { if slot >= pqReducerBound.Uint64() { - return kb8.G1Affine{}, 0, errPqSlotOutOfRange + return octobear.G1Affine{}, 0, errPqSlotOutOfRange } - _, b := kb8.CurveCoefficients() + _, b := octobear.CurveCoefficients() return mapAtBase(slot*pqT, pqT, &b) } diff --git a/ecc/kb8/multiset-hash/vector_multiset_hash_test.go b/ecc/octobear/multiset-hash/vector_multiset_hash_test.go similarity index 99% rename from ecc/kb8/multiset-hash/vector_multiset_hash_test.go rename to ecc/octobear/multiset-hash/vector_multiset_hash_test.go index 43fc1f3b75..ad2ecd6e80 100644 --- a/ecc/kb8/multiset-hash/vector_multiset_hash_test.go +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_test.go @@ -5,7 +5,7 @@ import ( "math/big" "testing" - "github.com/consensys/gnark-crypto/ecc/kb8" + "github.com/consensys/gnark-crypto/ecc/octobear" "github.com/consensys/gnark-crypto/field/koalabear" "github.com/stretchr/testify/require" ) @@ -282,7 +282,7 @@ func TestLinearHomomorphicAdditivity(t *testing.T) { require.NoError(t, err) for i := range full { - var sum kb8.G1Affine + var sum octobear.G1Affine sum.Add(&dA[i], &dB[i]) require.True(t, sum.Equal(&full[i]), "linear: Hash(A∪B)[%d] must equal Hash(A)+Hash(B)", i) @@ -302,7 +302,7 @@ func TestPoseidon2HomomorphicAdditivity(t *testing.T) { require.NoError(t, err) for i := range full { - var sum kb8.G1Affine + var sum octobear.G1Affine sum.Add(&dA[i], &dB[i]) require.True(t, sum.Equal(&full[i]), "poseidon2: Hash(A∪B)[%d] must equal Hash(A)+Hash(B)", i) diff --git a/ecc/kb8/kb8.go b/ecc/octobear/octobear.go similarity index 83% rename from ecc/kb8/kb8.go rename to ecc/octobear/octobear.go index 6ae9ac158e..cc614548b6 100644 --- a/ecc/kb8/kb8.go +++ b/ecc/octobear/octobear.go @@ -1,12 +1,12 @@ // Copyright 2020-2026 Consensys Software Inc. // Licensed under the Apache License, Version 2.0. See the LICENSE file for details. -// Package kb8 for efficient elliptic curve implementation for kb8 (koalabear-8). +// Package octobear for efficient elliptic curve implementation for octobear (koalabear-8). // This curve is intended for circuit operations defined over the KoalaBear field. // In particular, it is used for multiset-hash constructions appearing in zkVM // memory arguments. // -// kb8: A curve over 𝔽p⁸ with +// octobear: A curve over 𝔽p⁸ with // // 𝔽p: p=2130706433 = 2³¹-2²⁴+1 // 𝔽r: r=424804331891979973455971894938199991839487883914575852667663156896715214921 @@ -21,17 +21,17 @@ // This code has not been audited and is provided as-is. In particular, there // is no security guarantee such as constant time implementation or side-channel // attack resistance. -package kb8 +package octobear import ( "math/big" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/kb8/internal/fptower" + "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" ) -// ID kb8 ID. -const ID = ecc.KB8 +// ID octobear ID. +const ID = ecc.OCTOBEAR // aCurveCoeff and bCurveCoeff are the coefficients of Y²=X³+ax+b. var aCurveCoeff, bCurveCoeff fptower.E8 @@ -43,7 +43,7 @@ var ( g1Infinity G1Jac ) -// xGen is only used by the generic mulBySeed helper. kb8 has no seed-based endomorphism, +// xGen is only used by the generic mulBySeed helper. octobear has no seed-based endomorphism, // so keep it as the identity scalar. var xGen big.Int diff --git a/internal/generator/config/kb8.go b/internal/generator/config/octobear.go similarity index 82% rename from internal/generator/config/kb8.go rename to internal/generator/config/octobear.go index 0b62e32665..7cc6ee05af 100644 --- a/internal/generator/config/kb8.go +++ b/internal/generator/config/octobear.go @@ -1,9 +1,9 @@ package config -var KB8 = Curve{ - Name: "kb8", - CurvePackage: "kb8", - EnumID: "KB8", +var OCTOBEAR = Curve{ + Name: "octobear", + CurvePackage: "octobear", + EnumID: "OCTOBEAR", FpModulus: "2130706433", FrModulus: "424804331891979973455971894938199991839487883914575852667663156896715214921", NoFieldSuite: true, @@ -25,5 +25,5 @@ var KB8 = Curve{ } func init() { - addCurve(&KB8) + addCurve(&OCTOBEAR) } From 8d7eba492bae3229b1b82f9b940c3bb9713cf077 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 21 May 2026 18:08:52 -0400 Subject: [PATCH 28/33] ci: whitelist octobear spelling --- .golangci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 189eacb587..7cdf077cd0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,9 @@ linters: excludes: - G115 - G602 + misspell: + ignore-rules: + - octobear exclusions: generated: disable presets: From cd2bfba10616a7959b51b045bdc644eb8feb6c04 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 27 May 2026 08:47:41 -0400 Subject: [PATCH 29/33] fix: cbrt receiver and vPoseidon2-encoding injectiveness --- .../vector_multiset_hash_poseidon2.go | 18 ++++++-- .../vector_multiset_hash_test.go | 26 +++++++++++ field/koalabear/extensions/e2_cbrt.go | 33 ++++++++----- field/koalabear/extensions/e2_cbrt_test.go | 46 +++++++++++++++++++ field/koalabear/extensions/e4_cbrt.go | 17 +++++-- field/koalabear/extensions/e4_cbrt_test.go | 12 +++++ field/koalabear/extensions/e8_cbrt.go | 17 +++++-- field/koalabear/extensions/e8_cbrt_test.go | 12 +++++ 8 files changed, 157 insertions(+), 24 deletions(-) diff --git a/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go index 1eca624552..cf57f24fe3 100644 --- a/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go @@ -192,13 +192,21 @@ func squeezePoseidon2(msg uint64) ([pqPermutations * pqSqueezeRate]koalabear.Ele ) // Absorb (domainTag, msg) into the rate part (state[0:pqSqueezeRate]). - // The 8-byte tag is split into two 32-bit big-endian halves and the - // 64-bit msg into two more halves, filling state[0..3]. state[4..7] - // and the capacity state[8..15] stay zero. + // The 8-byte tag occupies state[0..1] as two 32-bit big-endian halves + // (each < p, so SetUint64 is injective on the tag's domain). The 64-bit + // msg is split into four 16-bit big-endian chunks across state[2..5]; + // each chunk is < 2^16 < p, so this encoding is injective for the full + // uint64 domain. state[6..7] and the capacity state[8..15] stay zero. + // + // Note: a 32-bit-half encoding would not be injective here because + // koalabear has p = 2^31 - 2^24 + 1 < 2^32, so e.g. msg = 0 and msg = p + // would absorb the same field elements (mod p) and collide. state[0].SetUint64(uint64(binary.BigEndian.Uint32(pqDomainTag[0:4]))) state[1].SetUint64(uint64(binary.BigEndian.Uint32(pqDomainTag[4:8]))) - state[2].SetUint64(msg & 0xFFFFFFFF) - state[3].SetUint64(msg >> 32) + state[2].SetUint64(uint64(uint16(msg >> 48))) + state[3].SetUint64(uint64(uint16(msg >> 32))) + state[4].SetUint64(uint64(uint16(msg >> 16))) + state[5].SetUint64(uint64(uint16(msg))) perm := pqPerm() for i := 0; i < pqPermutations; i++ { diff --git a/ecc/octobear/multiset-hash/vector_multiset_hash_test.go b/ecc/octobear/multiset-hash/vector_multiset_hash_test.go index ad2ecd6e80..c8d6710043 100644 --- a/ecc/octobear/multiset-hash/vector_multiset_hash_test.go +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_test.go @@ -358,6 +358,32 @@ func TestLinearBoundaryMessages(t *testing.T) { } } +// Regression test for the non-injective uint64 encoding bug: previously +// squeezePoseidon2 absorbed msg as two 32-bit halves via SetUint64, but with +// koalabear's p = 2^31 - 2^24 + 1 < 2^32 the low-half reduction collapsed +// msg = 0 and msg = p into the same sponge input, yielding a trivial +// single-element multiset collision. The encoding is now four 16-bit chunks +// (each < 2^16 < p), so distinct uint64 values must produce distinct digests. +func TestPoseidon2EncodingInjectiveAcrossModulus(t *testing.T) { + p := koalabear.Modulus().Uint64() + msgs := []uint64{0, p, p + 1, 1 << 32, 2 * p, math.MaxUint64} + seen := make(map[string]uint64, len(msgs)) + for _, msg := range msgs { + digest, err := HashPoseidon2([]uint64{msg}) + require.NoError(t, err) + var buf []byte + for i := range digest { + b := digest[i].Bytes() + buf = append(buf, b[:]...) + } + key := string(buf) + if prev, ok := seen[key]; ok { + t.Fatalf("Poseidon2 digest collision: msg=%d and msg=%d produce the same digest", prev, msg) + } + seen[key] = msg + } +} + func TestPoseidon2BoundaryMessages(t *testing.T) { for _, msg := range []uint64{0, math.MaxUint64} { pts, _, err := MapPoseidon2(msg) diff --git a/field/koalabear/extensions/e2_cbrt.go b/field/koalabear/extensions/e2_cbrt.go index 2bc25b2546..195b790a53 100644 --- a/field/koalabear/extensions/e2_cbrt.go +++ b/field/koalabear/extensions/e2_cbrt.go @@ -39,6 +39,9 @@ func init() { // Cbrt sets z to the cube root of x and returns z. // It returns nil if x is not a cubic residue. +// +// The candidate is computed into a local E2 and only written to z at the +// end, so x.Cbrt(&x) is safe (does not corrupt x mid-computation). func (z *E2) Cbrt(x *E2) *E2 { if x.A1.IsZero() { z.A0.Cbrt(&x.A0) @@ -46,11 +49,16 @@ func (z *E2) Cbrt(x *E2) *E2 { return z } + var y E2 + if x.A0.IsZero() { - z.A0.SetZero() - z.A1.Mul(&x.A1, &cbrtFpThreeInv) - z.A1.Cbrt(&z.A1) - return cbrtVerifyE2(z, x) + y.A0.SetZero() + y.A1.Mul(&x.A1, &cbrtFpThreeInv) + y.A1.Cbrt(&y.A1) + if cbrtVerifyE2(&y, x) == nil { + return nil + } + return z.Set(&y) } var x0sq, x1sq, betaX1sq, norm fr.Element @@ -95,12 +103,12 @@ func (z *E2) Cbrt(x *E2) *E2 { var t1, t2 fr.Element t1.Mul(&x.A0, &gamma0) t2.Mul(&x.A1, &gamma1).Mul(&t2, &cbrtFpThree) - z.A0.Sub(&t1, &t2).Mul(&z.A0, &mInv) + y.A0.Sub(&t1, &t2).Mul(&y.A0, &mInv) t1.Mul(&x.A1, &gamma0) t2.Mul(&x.A0, &gamma1) - z.A1.Sub(&t1, &t2).Mul(&z.A1, &mInv) - if out := cbrtVerifyE2(z, x); out != nil { - return out + y.A1.Sub(&t1, &t2).Mul(&y.A1, &mInv) + if cbrtVerifyE2(&y, x) != nil { + return z.Set(&y) } var sigma fr.Element @@ -116,9 +124,12 @@ func (z *E2) Cbrt(x *E2) *E2 { } d0d1Inv.Inverse(&d0d1) - z.A0.Mul(&d1, &d0d1Inv).Mul(&z.A0, &x.A0) - z.A1.Mul(&d0, &d0d1Inv).Mul(&z.A1, &x.A1) - return cbrtVerifyE2(z, x) + y.A0.Mul(&d1, &d0d1Inv).Mul(&y.A0, &x.A0) + y.A1.Mul(&d0, &d0d1Inv).Mul(&y.A1, &x.A1) + if cbrtVerifyE2(&y, x) == nil { + return nil + } + return z.Set(&y) } func cbrtAndNormInverseE2(norm, x0sq, x1sq *fr.Element) (m, normInv, deltaInv fr.Element, ok bool) { diff --git a/field/koalabear/extensions/e2_cbrt_test.go b/field/koalabear/extensions/e2_cbrt_test.go index fe2d039159..5453345fe1 100644 --- a/field/koalabear/extensions/e2_cbrt_test.go +++ b/field/koalabear/extensions/e2_cbrt_test.go @@ -20,6 +20,52 @@ func TestE2CbrtOnCubicResidues(t *testing.T) { } } +// Regression test for the receiver-aliasing bug where z.A0 was written +// before x.A0 was read on the main path (and similarly in the x.A0==0 +// branch, where the verify-against-x step ran after z had been overwritten). +// Ensures z == x is safe for E2/E4/E8 cube roots. +func TestE2CbrtReceiverSafeAlias(t *testing.T) { + for i := 0; i < 64; i++ { + var a, x, expected, inPlace, check E2 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + + if expected.Cbrt(&x) == nil { + t.Fatal("expected cubic residue to have a cube root") + } + + inPlace.Set(&x) + if inPlace.Cbrt(&inPlace) == nil { + t.Fatal("in-place Cbrt returned nil on a cubic residue") + } + check.Square(&inPlace).Mul(&check, &inPlace) + if !check.Equal(&x) { + t.Fatal("in-place Cbrt does not verify against original x") + } + } +} + +// Exercises the x.A0 == 0 branch (purely imaginary) under z == x aliasing. +func TestE2CbrtReceiverSafeAliasPureImaginary(t *testing.T) { + for i := 0; i < 64; i++ { + var a, x, check E2 + a.MustSetRandom() + a.A0.SetZero() + x.Square(&a).Mul(&x, &a) + if x.IsZero() { + continue + } + orig := x + if x.Cbrt(&x) == nil { + continue + } + check.Square(&x).Mul(&check, &x) + if !check.Equal(&orig) { + t.Fatal("in-place Cbrt does not verify on x with A0==0") + } + } +} + func TestE2CbrtRejectsNonResidues(t *testing.T) { var x, got E2 for i := 0; i < 256; i++ { diff --git a/field/koalabear/extensions/e4_cbrt.go b/field/koalabear/extensions/e4_cbrt.go index 5c8d143718..f5291446f8 100644 --- a/field/koalabear/extensions/e4_cbrt.go +++ b/field/koalabear/extensions/e4_cbrt.go @@ -20,6 +20,9 @@ func init() { // Cbrt sets z to the cube root of x and returns z. // It returns nil if x is not a cubic residue. +// +// The candidate is computed into a local E4 and only written to z at the +// end, so x.Cbrt(&x) is safe (does not corrupt x mid-computation). func (z *E4) Cbrt(x *E4) *E4 { if x.B1.IsZero() { if z.B0.Cbrt(&x.B0) == nil { @@ -29,15 +32,19 @@ func (z *E4) Cbrt(x *E4) *E4 { return z } + var y E4 + if x.B0.IsZero() { - var y E4 var x1OverNR E2 x1OverNR.Mul(&x.B1, &cbrtE2NRInv) if y.B1.Cbrt(&x1OverNR) == nil { return nil } y.B0.SetZero() - return cbrtVerifyAndAdjustE4(z.Set(&y), x) + if cbrtVerifyAndAdjustE4(&y, x) == nil { + return nil + } + return z.Set(&y) } var x0sq, x1sq, betaX1sq, norm E2 @@ -86,7 +93,6 @@ func (z *E4) Cbrt(x *E4) *E4 { var mInv E2 mInv.Square(&m).Mul(&mInv, &normInv) - var y E4 var t1, t2 E2 t1.Mul(&x.B0, &gamma0) t2.Mul(&x.B1, &gamma1) @@ -95,7 +101,10 @@ func (z *E4) Cbrt(x *E4) *E4 { t1.Mul(&x.B1, &gamma0) t2.Mul(&x.B0, &gamma1) y.B1.Sub(&t1, &t2).Mul(&y.B1, &mInv) - return cbrtVerifyAndAdjustE4(z.Set(&y), x) + if cbrtVerifyAndAdjustE4(&y, x) == nil { + return nil + } + return z.Set(&y) } func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { diff --git a/field/koalabear/extensions/e4_cbrt_test.go b/field/koalabear/extensions/e4_cbrt_test.go index 51835ec513..a3df351f01 100644 --- a/field/koalabear/extensions/e4_cbrt_test.go +++ b/field/koalabear/extensions/e4_cbrt_test.go @@ -20,6 +20,18 @@ func TestE4CbrtOnCubicResidues(t *testing.T) { } } +func TestE4CbrtReceiverSafeAlias(t *testing.T) { + for i := 0; i < 32; i++ { + var a, x, inPlace, check E4 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + inPlace.Set(&x) + require.NotNil(t, inPlace.Cbrt(&inPlace)) + check.Square(&inPlace).Mul(&check, &inPlace) + require.True(t, check.Equal(&x), "E4 in-place Cbrt must satisfy z^3 == x") + } +} + func TestE4CbrtRejectsNonResidues(t *testing.T) { var x, got E4 for i := 0; i < 256; i++ { diff --git a/field/koalabear/extensions/e8_cbrt.go b/field/koalabear/extensions/e8_cbrt.go index 7313182ee0..2b4c50e787 100644 --- a/field/koalabear/extensions/e8_cbrt.go +++ b/field/koalabear/extensions/e8_cbrt.go @@ -20,6 +20,9 @@ func init() { // Cbrt sets z to the cube root of x and returns z. // It returns nil if x is not a cubic residue. +// +// The candidate is computed into a local E8 and only written to z at the +// end, so x.Cbrt(&x) is safe (does not corrupt x mid-computation). func (z *E8) Cbrt(x *E8) *E8 { if x.C1.IsZero() { if z.C0.Cbrt(&x.C0) == nil { @@ -29,15 +32,19 @@ func (z *E8) Cbrt(x *E8) *E8 { return z } + var y E8 + if x.C0.IsZero() { - var y E8 var x1OverNR E4 x1OverNR.Mul(&x.C1, &cbrtE4NRInv) if y.C1.Cbrt(&x1OverNR) == nil { return nil } y.C0.SetZero() - return cbrtVerifyAndAdjustE8(z.Set(&y), x) + if cbrtVerifyAndAdjustE8(&y, x) == nil { + return nil + } + return z.Set(&y) } var x0sq, x1sq, betaX1sq, norm E4 @@ -86,7 +93,6 @@ func (z *E8) Cbrt(x *E8) *E8 { var mInv E4 mInv.Square(&m).Mul(&mInv, &normInv) - var y E8 var t1, t2 E4 t1.Mul(&x.C0, &gamma0) t2.Mul(&x.C1, &gamma1) @@ -95,7 +101,10 @@ func (z *E8) Cbrt(x *E8) *E8 { t1.Mul(&x.C1, &gamma0) t2.Mul(&x.C0, &gamma1) y.C1.Sub(&t1, &t2).Mul(&y.C1, &mInv) - return cbrtVerifyAndAdjustE8(z.Set(&y), x) + if cbrtVerifyAndAdjustE8(&y, x) == nil { + return nil + } + return z.Set(&y) } func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { diff --git a/field/koalabear/extensions/e8_cbrt_test.go b/field/koalabear/extensions/e8_cbrt_test.go index 917f4c2ee1..07993e7e16 100644 --- a/field/koalabear/extensions/e8_cbrt_test.go +++ b/field/koalabear/extensions/e8_cbrt_test.go @@ -26,6 +26,18 @@ func TestE8CbrtOnCubicResidues(t *testing.T) { } } +func TestE8CbrtReceiverSafeAlias(t *testing.T) { + for i := 0; i < 32; i++ { + var a, x, inPlace, check E8 + a.MustSetRandom() + x.Square(&a).Mul(&x, &a) + inPlace.Set(&x) + require.NotNil(t, inPlace.Cbrt(&inPlace)) + check.Square(&inPlace).Mul(&check, &inPlace) + require.True(t, check.Equal(&x), "E8 in-place Cbrt must satisfy z^3 == x") + } +} + func TestE8CbrtRejectsNonResidues(t *testing.T) { var x, got E8 for i := 0; i < 256; i++ { From aae27cf830dfd819671d00fba4e11c458f5651f0 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Wed, 27 May 2026 10:28:22 -0400 Subject: [PATCH 30/33] refactor: dead code cleanup --- field/koalabear/extensions/e2_cbrt.go | 10 ------- field/koalabear/extensions/e4_cbrt.go | 34 +++++++++-------------- field/koalabear/extensions/e8_cbrt.go | 39 +++++++-------------------- 3 files changed, 21 insertions(+), 62 deletions(-) diff --git a/field/koalabear/extensions/e2_cbrt.go b/field/koalabear/extensions/e2_cbrt.go index 195b790a53..ad87d749bc 100644 --- a/field/koalabear/extensions/e2_cbrt.go +++ b/field/koalabear/extensions/e2_cbrt.go @@ -11,8 +11,6 @@ var ( cbrtFpThree fr.Element cbrtFpThreeInv fr.Element cbrtE2One E2 - cbrtE2Omega E2 - cbrtE2Omega2 E2 cbrtE2NRInv E2 ) @@ -27,14 +25,6 @@ func init() { cbrtE2One.SetOne() cbrtE2NRInv.A0.SetZero() cbrtE2NRInv.A1.Set(&cbrtFpThreeInv) - - var sqrtMinusThree E2 - sqrtMinusThree.A0.Neg(&cbrtFpThree) - sqrtMinusThree.Sqrt(&sqrtMinusThree) - cbrtE2Omega.Sub(&sqrtMinusThree, &cbrtE2One) - cbrtE2Omega.A0.Halve() - cbrtE2Omega.A1.Halve() - cbrtE2Omega2.Square(&cbrtE2Omega) } // Cbrt sets z to the cube root of x and returns z. diff --git a/field/koalabear/extensions/e4_cbrt.go b/field/koalabear/extensions/e4_cbrt.go index f5291446f8..93f0e9184b 100644 --- a/field/koalabear/extensions/e4_cbrt.go +++ b/field/koalabear/extensions/e4_cbrt.go @@ -41,7 +41,7 @@ func (z *E4) Cbrt(x *E4) *E4 { return nil } y.B0.SetZero() - if cbrtVerifyAndAdjustE4(&y, x) == nil { + if cbrtVerifyE4(&y, x) == nil { return nil } return z.Set(&y) @@ -101,35 +101,25 @@ func (z *E4) Cbrt(x *E4) *E4 { t1.Mul(&x.B1, &gamma0) t2.Mul(&x.B0, &gamma1) y.B1.Sub(&t1, &t2).Mul(&y.B1, &mInv) - if cbrtVerifyAndAdjustE4(&y, x) == nil { + if cbrtVerifyE4(&y, x) == nil { return nil } return z.Set(&y) } -func cbrtVerifyAndAdjustE4(z, x *E4) *E4 { +// cbrtVerifyE4 returns z iff z³ == x, else nil. +// +// The E4.Cbrt construction always returns a true cube root on the first try +// when x is a cubic residue (verified empirically over many random cubes), so +// no cube-root-of-unity rotation is needed — the verify is just a safety +// check against non-residue inputs. +func cbrtVerifyE4(z, x *E4) *E4 { var check E4 check.Square(z).Mul(&check, z) - if check.Equal(x) { - return z - } - - var y E4 - y.B0.Mul(&z.B0, &cbrtE2Omega) - y.B1.Mul(&z.B1, &cbrtE2Omega) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - y.B0.Mul(&z.B0, &cbrtE2Omega2) - y.B1.Mul(&z.B1, &cbrtE2Omega2) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) + if !check.Equal(x) { + return nil } - - return nil + return z } func lucasV2E2Cbrt(alpha *E2) (E2, E2) { diff --git a/field/koalabear/extensions/e8_cbrt.go b/field/koalabear/extensions/e8_cbrt.go index 2b4c50e787..8c50da07bc 100644 --- a/field/koalabear/extensions/e8_cbrt.go +++ b/field/koalabear/extensions/e8_cbrt.go @@ -3,21 +3,11 @@ package extensions -var ( - cbrtE8Omega E8 - cbrtE8Omega2 E8 -) - var cbrtE8LucasExponent = [2]uint64{ 2930905110336765953, 372437575807401643, } -func init() { - cbrtE8Omega.C0.B0 = cbrtE2Omega - cbrtE8Omega2.Square(&cbrtE8Omega) -} - // Cbrt sets z to the cube root of x and returns z. // It returns nil if x is not a cubic residue. // @@ -41,7 +31,7 @@ func (z *E8) Cbrt(x *E8) *E8 { return nil } y.C0.SetZero() - if cbrtVerifyAndAdjustE8(&y, x) == nil { + if cbrtVerifyE8(&y, x) == nil { return nil } return z.Set(&y) @@ -101,32 +91,21 @@ func (z *E8) Cbrt(x *E8) *E8 { t1.Mul(&x.C1, &gamma0) t2.Mul(&x.C0, &gamma1) y.C1.Sub(&t1, &t2).Mul(&y.C1, &mInv) - if cbrtVerifyAndAdjustE8(&y, x) == nil { + if cbrtVerifyE8(&y, x) == nil { return nil } return z.Set(&y) } -func cbrtVerifyAndAdjustE8(z, x *E8) *E8 { - var check, y E8 +// cbrtVerifyE8 returns z iff z³ == x, else nil. See cbrtVerifyE4 for why no +// cube-root-of-unity adjustment is needed. +func cbrtVerifyE8(z, x *E8) *E8 { + var check E8 check.Square(z).Mul(&check, z) - if check.Equal(x) { - return z - } - - y.Mul(z, &cbrtE8Omega) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) - } - - y.Mul(z, &cbrtE8Omega2) - check.Square(&y).Mul(&check, &y) - if check.Equal(x) { - return z.Set(&y) + if !check.Equal(x) { + return nil } - - return nil + return z } func lucasV2E4Cbrt(alpha *E4) (E4, E4) { From d3f2a0262d0312ffb72abc0f183980c00ff3fd0d Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 28 May 2026 15:41:27 -0400 Subject: [PATCH 31/33] refactor: address Ivo's review --- ecc/octobear/multiset-hash/cardano_test.go | 127 ++++++++++++++++++ ecc/octobear/multiset-hash/multiset_hash.go | 26 +--- .../vector_multiset_hash_linear.go | 11 +- 3 files changed, 134 insertions(+), 30 deletions(-) diff --git a/ecc/octobear/multiset-hash/cardano_test.go b/ecc/octobear/multiset-hash/cardano_test.go index 9222a777f8..bdd3fcc03f 100644 --- a/ecc/octobear/multiset-hash/cardano_test.go +++ b/ecc/octobear/multiset-hash/cardano_test.go @@ -81,3 +81,130 @@ func TestDepressedCubicRootFindsValidRoot(t *testing.T) { require.True(t, lhs.IsZero()) } } + +// cardanoDelta returns the dispatch discriminant delta = 108 - 27·c² used by +// cardanoRoots to choose a branch (zero → repeatedRoots, non-square → +// quadratic-extension branch, square → base-field branch). +func cardanoDelta(c *extensions.E8) extensions.E8 { + var c2, delta extensions.E8 + c2.Square(c) + delta.Mul(&c2, &e8TwentySeven) + delta.Sub(&e8Neg4A3, &delta) + return delta +} + +func checkCubicRoots(t *testing.T, c extensions.E8, roots []extensions.E8) { + t.Helper() + require.NotEmpty(t, roots, "cardanoRoots returned no roots") + for _, x := range roots { + var lhs, x3 extensions.E8 + x3.Square(&x).Mul(&x3, &x) + lhs.Set(&x3) + lhs.Sub(&lhs, &x).Sub(&lhs, &x).Sub(&lhs, &x).Add(&lhs, &c) + require.True(t, lhs.IsZero(), "root does not satisfy x^3 - 3x + c = 0") + } +} + +// TestCardanoRepeatedRootBranch exercises the delta = 0 branch with c = 2: +// x^3 - 3x + 2 = (x-1)^2 (x+2), so the dispatcher must hit repeatedRoots. +func TestCardanoRepeatedRootBranch(t *testing.T) { + var c extensions.E8 + c.C0.B0.A0.SetUint64(2) + + delta := cardanoDelta(&c) + require.True(t, delta.IsZero(), "c = 2 must drive delta to zero") + + roots := cardanoRoots(c) + checkCubicRoots(t, c, roots) + + var one, negTwo extensions.E8 + one.SetOne() + negTwo.SetOne().Double(&negTwo).Neg(&negTwo) + var foundOne, foundNegTwo bool + for _, x := range roots { + if x.Equal(&one) { + foundOne = true + } + if x.Equal(&negTwo) { + foundNegTwo = true + } + } + require.True(t, foundOne, "repeated-root branch must produce x = 1") + require.True(t, foundNegTwo, "repeated-root branch must produce x = -2") +} + +// TestCardanoBaseFieldBranch exercises the square-delta path. We build c from +// a known root x in the prime subfield: c = 3x - x^3 guarantees that x solves +// x^3 - 3x + c = 0, and any c whose components all lie in Fp produces a delta +// that is a square in E8 (since [E8 : Fp] = 8 is even, every element of Fp is +// a square in E8). Whether Cardano can recover roots through E8 depends on +// whether the cube root needed by the formula lies in E8, so we search across +// x values until the dispatcher returns a non-empty set including x. +func TestCardanoBaseFieldBranch(t *testing.T) { + for n := uint64(3); n < 10_000; n++ { + var x extensions.E8 + x.C0.B0.A0.SetUint64(n) + + var c, x3 extensions.E8 + x3.Square(&x).Mul(&x3, &x) + c.Double(&x).Add(&c, &x).Sub(&c, &x3) + + delta := cardanoDelta(&c) + if delta.IsZero() { + continue + } + require.Equal(t, 1, delta.Legendre(), "c in Fp must give a square delta in E8") + + roots := cardanoRoots(c) + if len(roots) == 0 { + continue + } + checkCubicRoots(t, c, roots) + var found bool + for _, r := range roots { + if r.Equal(&x) { + found = true + break + } + } + if !found { + continue + } + return + } + t.Fatal("could not find a base-field-branch witness in [3, 10000)") +} + +// TestCardanoQuadraticExtensionBranch exercises the non-square-delta path. +// To force delta to be a non-square in E8, c must have a non-trivial +// extension component (any element of the prime subfield is a square in E8). +// We construct x with both a base and an extension component, compute +// c = 3x - x^3, and search for one whose delta is a non-square. +func TestCardanoQuadraticExtensionBranch(t *testing.T) { + for n := uint64(1); n < 4096; n++ { + var x extensions.E8 + x.C0.B0.A0.SetUint64(n) + x.C1.B0.A0.SetUint64(1) + + var c, x3 extensions.E8 + x3.Square(&x).Mul(&x3, &x) + c.Double(&x).Add(&c, &x).Sub(&c, &x3) + + delta := cardanoDelta(&c) + if delta.IsZero() || delta.Legendre() != -1 { + continue + } + roots := cardanoRoots(c) + checkCubicRoots(t, c, roots) + var found bool + for _, r := range roots { + if r.Equal(&x) { + found = true + break + } + } + require.True(t, found, "extension branch must recover x at n = %d", n) + return + } + t.Fatal("could not find a quadratic-extension-branch witness in [1, 4096)") +} diff --git a/ecc/octobear/multiset-hash/multiset_hash.go b/ecc/octobear/multiset-hash/multiset_hash.go index 894dc804de..5e280ab1f3 100644 --- a/ecc/octobear/multiset-hash/multiset_hash.go +++ b/ecc/octobear/multiset-hash/multiset_hash.go @@ -4,12 +4,11 @@ import ( "errors" "github.com/consensys/gnark-crypto/ecc/octobear" - "github.com/consensys/gnark-crypto/field/koalabear/extensions" ) const tweakBound = 256 -var errMapFailure = errors.New("octobear multiset hash: failed to map message after 256 y-increments") +var errMapFailure = errors.New("octobear multiset hash: failed to map message in tweak window") // Accumulator stores an additive multiset hash state in affine coordinates. type Accumulator struct { @@ -70,26 +69,5 @@ func Hash(msgs []uint16) (octobear.G1Affine, error) { // y = msg*256 + k yields a point (x, y) on octobear. func Map(msg uint16) (octobear.G1Affine, uint8, error) { _, b := octobear.CurveCoefficients() - baseY := uint64(msg) * tweakBound - - for k := uint16(0); k < tweakBound; k++ { - var y, c, ySquared extensions.E8 - y.SetZero() - y.C0.B0.A0.SetUint64(baseY + uint64(k)) - - ySquared.Square(&y) - c.Sub(&b, &ySquared) - - x, ok := depressedCubicRoot(c) - if !ok { - continue - } - - p := octobear.G1Affine{X: x, Y: y} - if p.IsOnCurve() && p.IsInSubGroup() { - return p, uint8(k), nil - } - } - - return octobear.G1Affine{}, 0, errMapFailure + return mapAtBase(uint64(msg)*tweakBound, tweakBound, &b) } diff --git a/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go b/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go index 32c4d10985..6014ef2ad4 100644 --- a/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go @@ -1,7 +1,6 @@ package multisethash import ( - "errors" "fmt" "github.com/consensys/gnark-crypto/ecc/octobear" @@ -115,11 +114,11 @@ func MapLinear(msg uint32) ([linearN]octobear.G1Affine, [linearN]uint8, error) { return pts, offsets, nil } -// mapAtBase scans k in [0, tweakBound) and returns the first curve point -// whose ordinate is y = baseY + k in the base subfield. baseY + tweakBound +// mapAtBase scans k in [0, bound) and returns the first curve point +// whose ordinate is y = baseY + k in the base subfield. baseY + bound // must remain strictly below p/2 to keep the image inverse-free. -func mapAtBase(baseY uint64, tweakBound uint64, b *extensions.E8) (octobear.G1Affine, uint8, error) { - for k := uint64(0); k < tweakBound; k++ { +func mapAtBase(baseY uint64, bound uint64, b *extensions.E8) (octobear.G1Affine, uint8, error) { + for k := uint64(0); k < bound; k++ { var y, c, ySquared extensions.E8 y.C0.B0.A0.SetUint64(baseY + k) @@ -136,5 +135,5 @@ func mapAtBase(baseY uint64, tweakBound uint64, b *extensions.E8) (octobear.G1Af return p, uint8(k), nil } } - return octobear.G1Affine{}, 0, errors.New("octobear vector multiset hash: failed to map message in tweak window") + return octobear.G1Affine{}, 0, errMapFailure } From 5aa8cf48169ec7fc2250c49e764e6aab1faa3e2e Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Thu, 28 May 2026 17:40:20 -0400 Subject: [PATCH 32/33] docs: clean refs --- ecc/octobear/multiset-hash/doc.go | 3 ++- ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ecc/octobear/multiset-hash/doc.go b/ecc/octobear/multiset-hash/doc.go index bc6eb36c4a..4942ae966d 100644 --- a/ecc/octobear/multiset-hash/doc.go +++ b/ecc/octobear/multiset-hash/doc.go @@ -7,7 +7,8 @@ // Each message m is mapped by scanning k in [0, 256) and setting // y = m*256 + k in the base subfield of Fp^8. The first resulting point // (x, y) on octobear is used as the image. Security is ~124 classical bits -// (no post-quantum security). +// (no post-quantum security). It mainly follows "Elliptic Curve Multiset Hash" +// by Maitin-Shepard, Tibouchi and Aranha (https://arxiv.org/abs/1601.06502). // // - Linear-separator vector ECMSH (LinearAccumulator, HashLinear, // MapLinear): a digest of N = 23 curve points. Coordinate i uses diff --git a/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go index cf57f24fe3..328eb5dfaf 100644 --- a/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go @@ -28,7 +28,7 @@ const ( ) // pqDomainTag is the 8-byte ASCII domain separator absorbed before the -// message (paper §4.3, "ECMSH_PQ"). +// message. var pqDomainTag = [8]byte{'E', 'C', 'M', 'S', 'H', '_', 'P', 'Q'} // pqReducerBound = floor(p / (2*T)) is the upper bound on the slot s @@ -176,7 +176,7 @@ func PqReducerBound() *big.Int { } // PqDomainTag returns the 8-byte ASCII domain separator absorbed before the -// message by MapPoseidon2 (paper §4.3, "ECMSH_PQ"). Exported so the in-circuit +// message by MapPoseidon2. Exported so the in-circuit // sponge can absorb the same bytes. func PqDomainTag() [8]byte { return pqDomainTag From f6b0b478eda65300e89ed275840ae7b127cc0f80 Mon Sep 17 00:00:00 2001 From: Youssef El Housni Date: Mon, 1 Jun 2026 15:21:28 -0400 Subject: [PATCH 33/33] docs: q=2mod3 gives unique cbrt --- ecc/stark-curve/fp/element.go | 4 ++-- ecc/stark-curve/fp/element_test.go | 18 +++++------------- field/koalabear/element.go | 4 ++-- field/koalabear/element_test.go | 18 +++++------------- .../field/template/element/cbrt.go.tmpl | 5 +++++ .../field/template/element/test.go.tmpl | 14 ++++++++++++++ 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/ecc/stark-curve/fp/element.go b/ecc/stark-curve/fp/element.go index 1d199503a9..c15d9c12de 100644 --- a/ecc/stark-curve/fp/element.go +++ b/ecc/stark-curve/fp/element.go @@ -1657,8 +1657,8 @@ func (z *Element) SqrtTonelliShanks(x *Element) *Element { } // 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/ecc/stark-curve/fp/element_test.go b/ecc/stark-curve/fp/element_test.go index 2eeb5d733a..50423973dd 100644 --- a/ecc/stark-curve/fp/element_test.go +++ b/ecc/stark-curve/fp/element_test.go @@ -1561,11 +1561,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()) @@ -1578,9 +1575,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) @@ -1609,11 +1604,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/field/koalabear/element.go b/field/koalabear/element.go index d821f07ce4..0a567983e1 100644 --- a/field/koalabear/element.go +++ b/field/koalabear/element.go @@ -953,8 +953,8 @@ func (z *Element) Sqrt(x *Element) *Element { } // 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/field/koalabear/element_test.go b/field/koalabear/element_test.go index 8fbb89d10d..fd912b9189 100644 --- a/field/koalabear/element_test.go +++ b/field/koalabear/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/internal/generator/field/template/element/cbrt.go.tmpl b/internal/generator/field/template/element/cbrt.go.tmpl index 8d9d85f11d..16054e545f 100644 --- a/internal/generator/field/template/element/cbrt.go.tmpl +++ b/internal/generator/field/template/element/cbrt.go.tmpl @@ -24,8 +24,13 @@ func init() { {{- end }} // Cbrt z = ∛x (mod q) +{{- if .CbrtQ2Mod3}} +// Since q ≡ 2 (mod 3), cubing is a bijection on Fq, so every input has a unique +// cube root and Cbrt always returns z. +{{- else}} // if the cube root doesn't exist (x is not a cube mod q) // Cbrt leaves z unchanged and returns nil +{{- end}} func (z *{{.ElementName}}) Cbrt(x *{{.ElementName}}) *{{.ElementName}} { {{- if .CbrtQ2Mod3}} // q ≡ 2 (mod 3) diff --git a/internal/generator/field/template/element/test.go.tmpl b/internal/generator/field/template/element/test.go.tmpl index da5756776b..0f7a8ea45c 100644 --- a/internal/generator/field/template/element/test.go.tmpl +++ b/internal/generator/field/template/element/test.go.tmpl @@ -1002,11 +1002,16 @@ func Test{{toTitle .all.ElementName}}{{.Op}}(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 {{.all.ElementName}} + {{- if .all.CbrtQ2Mod3}} + // q ≡ 2 (mod 3): every element has a unique cube root, Cbrt never returns nil. + c.{{.Op}}(&a.element) + {{- else}} result := c.{{.Op}}(&a.element) if result == nil { // a is not a cubic residue, this is valid return true } + {{- end}} var cube, e big.Int c.BigInt(&e) cube.Exp(&e, big.NewInt(3), Modulus()) @@ -1038,9 +1043,13 @@ func Test{{toTitle .all.ElementName}}{{.Op}}(t *testing.T) { // b = a³ is guaranteed to be a cubic residue var b, c {{.all.ElementName}} b.Square(&a.element).Mul(&b, &a.element) + {{- if not .all.CbrtQ2Mod3}} if c.Cbrt(&b) == nil { return false } + {{- else}} + c.Cbrt(&b) + {{- end}} var check {{.all.ElementName}} check.Square(&c).Mul(&check, &c) return check.Equal(&b) @@ -1101,11 +1110,16 @@ func Test{{toTitle .all.ElementName}}{{.Op}}(t *testing.T) { {{- if eq .Op "Cbrt"}} // verify that c^3 == a (since there's no big.Int.ModCbrt) // Cbrt returns nil if the element is not a cubic residue + {{- if .all.CbrtQ2Mod3}} + // q ≡ 2 (mod 3): every element has a unique cube root, Cbrt never returns nil. + c.{{.Op}}(&a) + {{- else}} result := c.{{.Op}}(&a) if result == nil { // a is not a cubic residue, this is valid, continue continue } + {{- end}} var cube, e big.Int c.BigInt(&e) cube.Exp(&e, big.NewInt(3), Modulus())