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: diff --git a/ecc/ecc.go b/ecc/ecc.go index 3fe3bd314c..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 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 octobear elliptic curves implementations. // // Also // @@ -32,6 +32,7 @@ const ( SECP256K1 GRUMPKIN SECP256R1 + 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 bf774fd4c1..c14c2ed2ac 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, + "octobear": OCTOBEAR, "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 OCTOBEAR: + 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 OCTOBEAR: + 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 OCTOBEAR: + return "octobear" case SECP256K1: return "secp256k1" case SECP256R1: diff --git a/ecc/octobear/fp/doc.go b/ecc/octobear/fp/doc.go new file mode 100644 index 0000000000..bf26bec2ad --- /dev/null +++ b/ecc/octobear/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/octobear can use the same local fp import +// shape as curves with generated base fields. +package fp diff --git a/ecc/octobear/fp/fp.go b/ecc/octobear/fp/fp.go new file mode 100644 index 0000000000..fea51da117 --- /dev/null +++ b/ecc/octobear/fp/fp.go @@ -0,0 +1,60 @@ +// 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" + + 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 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/octobear/fr/arith.go b/ecc/octobear/fr/arith.go new file mode 100644 index 0000000000..9aa0f3c4db --- /dev/null +++ b/ecc/octobear/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/octobear/fr/cbrt_extra_test.go b/ecc/octobear/fr/cbrt_extra_test.go new file mode 100644 index 0000000000..57f1e024a9 --- /dev/null +++ b/ecc/octobear/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/octobear/fr/doc.go b/ecc/octobear/fr/doc.go new file mode 100644 index 0000000000..0632b23e19 --- /dev/null +++ b/ecc/octobear/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/octobear/fr/element.go b/ecc/octobear/fr/element.go new file mode 100644 index 0000000000..2db8cc2d15 --- /dev/null +++ b/ecc/octobear/fr/element.go @@ -0,0 +1,1982 @@ +// 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() { + return z.expUint64(x, k.Uint64()) + } + + 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) + } + return z.expWindowed(x, e) +} + +// 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 +} + +// 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 +} + +// 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.IsZero() { + return z.SetZero() + } + 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/octobear/fr/element_amd64.go b/ecc/octobear/fr/element_amd64.go new file mode 100644 index 0000000000..c2f1e3691d --- /dev/null +++ b/ecc/octobear/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/octobear/fr/element_amd64.s b/ecc/octobear/fr/element_amd64.s new file mode 100644 index 0000000000..464c982518 --- /dev/null +++ b/ecc/octobear/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: 10523172047764019734 +#include "../../../field/asm/element_4w/element_4w_amd64.s" + diff --git a/ecc/octobear/fr/element_arm64.go b/ecc/octobear/fr/element_arm64.go new file mode 100644 index 0000000000..d4417ed826 --- /dev/null +++ b/ecc/octobear/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/octobear/fr/element_arm64.s b/ecc/octobear/fr/element_arm64.s new file mode 100644 index 0000000000..2cbfacdc7a --- /dev/null +++ b/ecc/octobear/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/octobear/fr/element_exp.go b/ecc/octobear/fr/element_exp.go new file mode 100644 index 0000000000..bcc5bc6d47 --- /dev/null +++ b/ecc/octobear/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/octobear/fr/element_purego.go b/ecc/octobear/fr/element_purego.go new file mode 100644 index 0000000000..799ff4497f --- /dev/null +++ b/ecc/octobear/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/octobear/fr/element_test.go b/ecc/octobear/fr/element_test.go new file mode 100644 index 0000000000..52f4ce3459 --- /dev/null +++ b/ecc/octobear/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/octobear/fr/vector.go b/ecc/octobear/fr/vector.go new file mode 100644 index 0000000000..c920dc1e17 --- /dev/null +++ b/ecc/octobear/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/octobear/fr/vector_amd64.go b/ecc/octobear/fr/vector_amd64.go new file mode 100644 index 0000000000..076550760c --- /dev/null +++ b/ecc/octobear/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/octobear/fr/vector_purego.go b/ecc/octobear/fr/vector_purego.go new file mode 100644 index 0000000000..b24444a6d8 --- /dev/null +++ b/ecc/octobear/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/octobear/fr/vector_test.go b/ecc/octobear/fr/vector_test.go new file mode 100644 index 0000000000..3856fa79f8 --- /dev/null +++ b/ecc/octobear/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< 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 +} + +// 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 +} + +// 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 +} + +// 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/octobear/g1_test.go b/ecc/octobear/g1_test.go new file mode 100644 index 0000000000..745638df80 --- /dev/null +++ b/ecc/octobear/g1_test.go @@ -0,0 +1,827 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package octobear + +import ( + crand "crypto/rand" + "fmt" + "math/big" + "math/rand/v2" + "testing" + + "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" + + "github.com/consensys/gnark-crypto/ecc/octobear/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("[OCTOBEAR] 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("[OCTOBEAR] 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("[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) + _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("[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 + + 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("[OCTOBEAR] 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("[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) + 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("[OCTOBEAR] 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("[OCTOBEAR] 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("[OCTOBEAR] 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("[OCTOBEAR] 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("[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) + return op1.Equal(&op2) + }, + GenE8(), + GenE8(), + )) + 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) + 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("[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 + g := g1GenAff + s.BigInt(&sInt) + op1.ScalarMultiplication(&g, &sInt) + op2.Neg(&op1) + + op1.Add(&op1, &op2) + return op1.IsInfinity() + + }, + GenFr(), + )) + + 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 + 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("[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 + g := g1GenAff + s.BigInt(&sInt) + op1.ScalarMultiplication(&g, &sInt) + + op2.Double(&op1) + op1.Add(&op1, &op1) + return op1.Equal(&op2) + + }, + GenFr(), + )) + + properties.Property("[OCTOBEAR] [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("[OCTOBEAR] [-s]G = -[s]G", prop.ForAll( + func(s fr.Element) bool { + g := g1GenAff + var gj G1Jac + gj.FromAffine(&g) + 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("[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) + var op1, op2 G1Jac + op1.Set(&fop1).AddAssign(&fop2) + op2.Double(&fop2) + return op1.Equal(&op2) + }, + GenE8(), + GenE8(), + )) + + 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) + fop2.Neg(&fop2) + fop1.AddAssign(&fop2) + return fop1.Equal(&g1Infinity) + }, + GenE8(), + GenE8(), + )) + + 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) + var op2 G1Jac + op2.Set(&g1Infinity) + op2.AddAssign(&g1Gen) + return fop1.Equal(&g1Gen) && op2.Equal(&g1Gen) + }, + GenE8(), + )) + + 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 + 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("[OCTOBEAR] [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("[OCTOBEAR] [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("[OCTOBEAR] 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("[OCTOBEAR] 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("[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 + + 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/octobear/internal/fptower/fptower.go b/ecc/octobear/internal/fptower/fptower.go new file mode 100644 index 0000000000..2b2a023c2e --- /dev/null +++ b/ecc/octobear/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/octobear/marshal.go b/ecc/octobear/marshal.go new file mode 100644 index 0000000000..104c1b8fab --- /dev/null +++ b/ecc/octobear/marshal.go @@ -0,0 +1,871 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package octobear + +import ( + "encoding/binary" + "errors" + "io" + "reflect" + "sync/atomic" + + "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" +) + +// 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 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 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 octobear 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("octobear 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("octobear 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 { + switch msb & mMask { + case mUncompressed, mUncompressedInfinity, mCompressedSmallest, mCompressedLargest, mCompressedInfinity: + return false + default: + return true + } +} + +func isCompressed(msb byte) bool { + mData := msb & mMask + return mData != mUncompressed && mData != mUncompressedInfinity +} + +// NewEncoder returns a binary encoder supporting curve octobear 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("octobear 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("octobear 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("octobear 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("octobear 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/octobear/marshal_test.go b/ecc/octobear/marshal_test.go new file mode 100644 index 0000000000..6ebeace289 --- /dev/null +++ b/ecc/octobear/marshal_test.go @@ -0,0 +1,378 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package octobear + +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/octobear/fp" + "github.com/consensys/gnark-crypto/ecc/octobear/fr" + "github.com/consensys/gnark-crypto/ecc/octobear/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") + } + + 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) { + 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/octobear/multiexp.go b/ecc/octobear/multiexp.go new file mode 100644 index 0000000000..7682c8aca4 --- /dev/null +++ b/ecc/octobear/multiexp.go @@ -0,0 +1,530 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package octobear + +import ( + "errors" + "math" + "runtime" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/octobear/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/octobear/multiexp_affine.go b/ecc/octobear/multiexp_affine.go new file mode 100644 index 0000000000..2c97d513f0 --- /dev/null +++ b/ecc/octobear/multiexp_affine.go @@ -0,0 +1,367 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package octobear + +import "github.com/consensys/gnark-crypto/ecc/octobear/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/octobear/multiexp_jacobian.go b/ecc/octobear/multiexp_jacobian.go new file mode 100644 index 0000000000..201d8e2235 --- /dev/null +++ b/ecc/octobear/multiexp_jacobian.go @@ -0,0 +1,95 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package octobear + +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/octobear/multiexp_test.go b/ecc/octobear/multiexp_test.go new file mode 100644 index 0000000000..86a1f27435 --- /dev/null +++ b/ecc/octobear/multiexp_test.go @@ -0,0 +1,440 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package octobear + +import ( + "fmt" + "math/big" + "math/bits" + "math/rand/v2" + "runtime" + "sync" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/octobear/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/octobear/multiset-hash/cardano.go b/ecc/octobear/multiset-hash/cardano.go new file mode 100644 index 0000000000..fad000a943 --- /dev/null +++ b/ecc/octobear/multiset-hash/cardano.go @@ -0,0 +1,886 @@ +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 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. + +var ( + e8Omega extensions.E8 + e8Beta extensions.E8 + e8BetaInv extensions.E8 + e8One extensions.E8 + e8Two extensions.E8 + e8Three extensions.E8 + e8Four extensions.E8 + e8TwentySeven extensions.E8 + e8NegThree extensions.E8 + e8Neg4A3 extensions.E8 // 108 = -4·(-3)^3 +) + +var e16LucasExponent = [4]uint64{ + 10958008504694079489, + 259792827419799556, + 15418438666690820192, + 45116822996742594, +} + +func init() { + 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.C1.SetOne() + e8BetaInv.Inverse(&e8Beta) + e8Omega = findPrimitiveCubeRoot() + + 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) { + 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 { + // 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) + + var discD extensions.E8 + discD.Set(&c2) + discD.Halve() + discD.Halve() + discD.Sub(&discD, &e8One) + + var negCHalf extensions.E8 + negCHalf.Set(&c) + negCHalf.Halve() + negCHalf.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 + } + + // 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) + 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) + t1.Mul(&omega2, &u) + t2.Mul(&e8Omega, &invU) + r2.Add(&t1, &t2) + + return filterValidRoots(negCHalf, []extensions.E8{r1, r2}) +} + +func cardanoRootsViaQuadraticExtension(negCHalf, discD extensions.E8) []extensions.E8 { + var discOverBeta, sqrtDiscOverBeta extensions.E8 + discOverBeta.Mul(&discD, &e8BetaInv) + 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 + } + + // 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) + sum.Add(&u, &inv) + if sum.A1.IsZero() && isDepressedCubicRoot(&sum.A0, &negCHalf) { + return []extensions.E8{sum.A0} + } + } + + var omega2 extensions.E8 + omega2.Square(&e8Omega) + for _, zeta := range [2]extensions.E8{e8Omega, omega2} { + 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 { + return z.Cbrt(x) +} + +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) + 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("octobear multiset hash: failed to find primitive cube root 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 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) + 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) + 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) 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) + 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) 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 { + 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) + + 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) + 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 + } + deltaInv.Inverse(&delta) + sIm.Double(&imY) + k.Mul(&sIm, &deltaInv) + + 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) +} diff --git a/ecc/octobear/multiset-hash/cardano_test.go b/ecc/octobear/multiset-hash/cardano_test.go new file mode 100644 index 0000000000..bdd3fcc03f --- /dev/null +++ b/ecc/octobear/multiset-hash/cardano_test.go @@ -0,0 +1,210 @@ +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 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 + 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 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 + 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()) + } +} + +// 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/doc.go b/ecc/octobear/multiset-hash/doc.go new file mode 100644 index 0000000000..4942ae966d --- /dev/null +++ b/ecc/octobear/multiset-hash/doc.go @@ -0,0 +1,27 @@ +// Package multisethash implements y-increment elliptic-curve multiset +// 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 octobear is used as the image. Security is ~124 classical bits +// (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 +// 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/octobear/multiset-hash/multiset_hash.go b/ecc/octobear/multiset-hash/multiset_hash.go new file mode 100644 index 0000000000..5e280ab1f3 --- /dev/null +++ b/ecc/octobear/multiset-hash/multiset_hash.go @@ -0,0 +1,73 @@ +package multisethash + +import ( + "errors" + + "github.com/consensys/gnark-crypto/ecc/octobear" +) + +const tweakBound = 256 + +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 { + sum octobear.G1Affine +} + +// NewAccumulator returns a zero accumulator. +func NewAccumulator() Accumulator { + var a Accumulator + a.sum.SetInfinity() + return a +} + +// 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 { + return err + } + a.sum.Add(&a.sum, &p) + return nil +} + +// 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 { + return err + } + p.Neg(&p) + a.sum.Add(&a.sum, &p) + return nil +} + +// Digest returns the current accumulator state in affine coordinates. +func (a *Accumulator) Digest() octobear.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 []uint16) (octobear.G1Affine, error) { + acc := NewAccumulator() + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + return octobear.G1Affine{}, err + } + } + return acc.Digest(), nil +} + +// 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 octobear. +func Map(msg uint16) (octobear.G1Affine, uint8, error) { + _, b := octobear.CurveCoefficients() + return mapAtBase(uint64(msg)*tweakBound, tweakBound, &b) +} diff --git a/ecc/octobear/multiset-hash/multiset_hash_test.go b/ecc/octobear/multiset-hash/multiset_hash_test.go new file mode 100644 index 0000000000..5b4cba8847 --- /dev/null +++ b/ecc/octobear/multiset-hash/multiset_hash_test.go @@ -0,0 +1,168 @@ +package multisethash + +import ( + "testing" + + "github.com/consensys/gnark-crypto/ecc/octobear" + "github.com/consensys/gnark-crypto/field/koalabear/extensions" + "github.com/stretchr/testify/require" +) + +func sampleMessages(n int) []uint16 { + res := make([]uint16, n) + var x uint32 = 1 + for i := range res { + 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) { + for _, msg := range sampleMessages(32) { + p1, o1, err := Map(msg) + require.NoError(t, err) + 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 := sampleMessages(8) + got1, err := Hash(msgs) + require.NoError(t, err) + + permuted := append([]uint16(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 TestInsertRemove(t *testing.T) { + msgs := sampleMessages(6) + acc := NewAccumulator() + 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])) + + 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 := sampleMessages(10) + got, err := Hash(msgs) + require.NoError(t, err) + + acc := NewAccumulator() + for _, msg := range msgs { + require.NoError(t, acc.Insert(msg)) + } + digest := acc.Digest() + require.True(t, got.Equal(&digest)) +} + +func TestDuplicatesMatter(t *testing.T) { + msg := sampleMessages(1)[0] + single, err := Hash([]uint16{msg}) + require.NoError(t, err) + + 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 octobear.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) { + msg := sampleMessages(1)[0] + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, err := Map(msg) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkAccumulatorInsert(b *testing.B) { + msgs := sampleMessages(256) + b.ResetTimer() + for i := 0; i < b.N; i++ { + acc := NewAccumulator() + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + b.Fatal(err) + } + } + _ = acc.Digest() + } +} + +func BenchmarkHash256(b *testing.B) { + msgs := sampleMessages(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") + } + } +} diff --git a/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go b/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go new file mode 100644 index 0000000000..6014ef2ad4 --- /dev/null +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_linear.go @@ -0,0 +1,139 @@ +package multisethash + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc/octobear" + "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("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]octobear.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 octobear.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]octobear.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]octobear.G1Affine, error) { + acc := NewLinearAccumulator() + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + return [linearN]octobear.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]octobear.G1Affine, [linearN]uint8, error) { + var ( + pts [linearN]octobear.G1Affine + offsets [linearN]uint8 + ) + if uint64(msg) >= linearM { + return pts, offsets, errLinearMsgOutOfRange + } + _, b := octobear.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, 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, 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) + + 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 +} diff --git a/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go new file mode 100644 index 0000000000..328eb5dfaf --- /dev/null +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_poseidon2.go @@ -0,0 +1,219 @@ +package multisethash + +import ( + "encoding/binary" + "errors" + "math/big" + "sync" + + "github.com/consensys/gnark-crypto/ecc/octobear" + "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. +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 ( + errPqSlotOutOfRange = errors.New("octobear vector multiset hash: Poseidon2 slot out of range") + + 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]octobear.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 octobear.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]octobear.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]octobear.G1Affine, error) { + acc := NewPoseidon2Accumulator() + for _, msg := range msgs { + if err := acc.Insert(msg); err != nil { + return [pqN]octobear.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]octobear.G1Affine, [pqN]uint8, error) { + var ( + pts [pqN]octobear.G1Affine + offsets [pqN]uint8 + ) + + squeezed, err := squeezePoseidon2(msg) + if err != nil { + return pts, offsets, err + } + + _, b := octobear.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 +} + +// 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 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) (octobear.G1Affine, uint8, error) { + if slot >= pqReducerBound.Uint64() { + return octobear.G1Affine{}, 0, errPqSlotOutOfRange + } + _, b := octobear.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. 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. +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 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(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++ { + 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/octobear/multiset-hash/vector_multiset_hash_test.go b/ecc/octobear/multiset-hash/vector_multiset_hash_test.go new file mode 100644 index 0000000000..c8d6710043 --- /dev/null +++ b/ecc/octobear/multiset-hash/vector_multiset_hash_test.go @@ -0,0 +1,525 @@ +package multisethash + +import ( + "math" + "math/big" + "testing" + + "github.com/consensys/gnark-crypto/ecc/octobear" + "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 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) + } +} + +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 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) + } +} + +// ----- 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()) + } + } +} + +// 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) + 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()) + } + } +} + +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) { + // 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 { + l, p := 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) + } + } +} diff --git a/ecc/octobear/octobear.go b/ecc/octobear/octobear.go new file mode 100644 index 0000000000..cc614548b6 --- /dev/null +++ b/ecc/octobear/octobear.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 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. +// +// octobear: 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 octobear + +import ( + "math/big" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/octobear/internal/fptower" +) + +// ID octobear ID. +const ID = ecc.OCTOBEAR + +// 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. octobear 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/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_exp.go b/field/koalabear/element_exp.go index ae81160fb4..58c0b0c2bd 100644 --- a/field/koalabear/element_exp.go +++ b/field/koalabear/element_exp.go @@ -129,3 +129,65 @@ 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 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 { + // 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: 26 squares 6 multiplies + var t0, t1 Element + + // 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: t0 = x^0x7 + t0.Mul(&x, z) + + // Step 10: t1 = x^0x1c0 + t1.Square(&t0) + for s := 1; s < 6; s++ { + t1.Square(&t1) + } + + // Step 11: z = x^0x1c7 + z.Mul(&t0, &t1) + + // Step 12: t1 = x^0x387 + t1.Mul(&t1, z) + + // Step 18: t1 = x^0xe1c0 + for range 6 { + t1.Square(&t1) + } + + // Step 19: t0 = x^0xe1c7 + t0.Mul(&t0, &t1) + + // Step 31: t0 = x^0xe1c7000 + for range 12 { + t0.Square(&t0) + } + + // Step 32: z = x^0xe1c71c7 + z.Mul(z, &t0) + + return z +} 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/field/koalabear/extensions/e2.go b/field/koalabear/extensions/e2.go index 7f3d442222..088586d9ea 100644 --- a/field/koalabear/extensions/e2.go +++ b/field/koalabear/extensions/e2.go @@ -239,30 +239,6 @@ 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. -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) - - return z -} - // 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..ad87d749bc --- /dev/null +++ b/field/koalabear/extensions/e2_cbrt.go @@ -0,0 +1,191 @@ +// 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 + 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) +} + +// 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) + z.A1.SetZero() + return z + } + + var y E2 + + if x.A0.IsZero() { + 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 + 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) + y.A0.Sub(&t1, &t2).Mul(&y.A0, &mInv) + t1.Mul(&x.A1, &gamma0) + t2.Mul(&x.A0, &gamma1) + y.A1.Sub(&t1, &t2).Mul(&y.A1, &mInv) + if cbrtVerifyE2(&y, x) != nil { + return z.Set(&y) + } + + 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) + + 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) { + 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..5453345fe1 --- /dev/null +++ b/field/koalabear/extensions/e2_cbrt_test.go @@ -0,0 +1,91 @@ +// 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") + } + } +} + +// 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++ { + 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_cbrt.go b/field/koalabear/extensions/e4_cbrt.go new file mode 100644 index 0000000000..93f0e9184b --- /dev/null +++ b/field/koalabear/extensions/e4_cbrt.go @@ -0,0 +1,148 @@ +// 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. +// +// 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 { + return nil + } + z.B1.SetZero() + return z + } + + var y E4 + + if x.B0.IsZero() { + var x1OverNR E2 + x1OverNR.Mul(&x.B1, &cbrtE2NRInv) + if y.B1.Cbrt(&x1OverNR) == nil { + return nil + } + y.B0.SetZero() + if cbrtVerifyE4(&y, x) == nil { + return nil + } + return z.Set(&y) + } + + 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 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) + if cbrtVerifyE4(&y, x) == nil { + return nil + } + return z.Set(&y) +} + +// 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 nil + } + return z +} + +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..a3df351f01 --- /dev/null +++ b/field/koalabear/extensions/e4_cbrt_test.go @@ -0,0 +1,57 @@ +// 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 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++ { + 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 new file mode 100644 index 0000000000..c9205ce337 --- /dev/null +++ b/field/koalabear/extensions/e8.go @@ -0,0 +1,423 @@ +// 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.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.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.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.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.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 +} + +// 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 { + 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 { + 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 +} + +// 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).MulByQuadraticNonResidue(&t1) + t0.Sub(&t0, &t1).Inverse(&t0) + z.C0.Mul(&x.C0, &t0) + 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 +} + +// 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.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 +} + +// 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).MulByQuadraticNonResidue(&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_cbrt.go b/field/koalabear/extensions/e8_cbrt.go new file mode 100644 index 0000000000..8c50da07bc --- /dev/null +++ b/field/koalabear/extensions/e8_cbrt.go @@ -0,0 +1,134 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package extensions + +var cbrtE8LucasExponent = [2]uint64{ + 2930905110336765953, + 372437575807401643, +} + +// 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 { + return nil + } + z.C1.SetZero() + return z + } + + var y E8 + + if x.C0.IsZero() { + var x1OverNR E4 + x1OverNR.Mul(&x.C1, &cbrtE4NRInv) + if y.C1.Cbrt(&x1OverNR) == nil { + return nil + } + y.C0.SetZero() + if cbrtVerifyE8(&y, x) == nil { + return nil + } + return z.Set(&y) + } + + 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 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) + if cbrtVerifyE8(&y, x) == nil { + return nil + } + return z.Set(&y) +} + +// 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 nil + } + return z +} + +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..07993e7e16 --- /dev/null +++ b/field/koalabear/extensions/e8_cbrt_test.go @@ -0,0 +1,63 @@ +// 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 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++ { + 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 new file mode 100644 index 0000000000..f740ce5f0a --- /dev/null +++ b/field/koalabear/extensions/e8_test.go @@ -0,0 +1,255 @@ +// 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" + + fr "github.com/consensys/gnark-crypto/field/koalabear" + + "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)} + }) +} + +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/addchain/2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 b/internal/generator/addchain/2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 new file mode 100644 index 0000000000..d8f91cde03 Binary files /dev/null and b/internal/generator/addchain/2f7e13d55e0ebe7849ddb6318e827b3e7005eb24e2bcbdf5ee9b8ad55ac73 differ diff --git a/internal/generator/addchain/78372234161552207af9352d70ba47f60b8efb355dedc0d683f9b76c0dc824 b/internal/generator/addchain/78372234161552207af9352d70ba47f60b8efb355dedc0d683f9b76c0dc824 new file mode 100644 index 0000000000..c99f599c54 Binary files /dev/null and b/internal/generator/addchain/78372234161552207af9352d70ba47f60b8efb355dedc0d683f9b76c0dc824 differ diff --git a/internal/generator/addchain/e1c71c7 b/internal/generator/addchain/e1c71c7 new file mode 100644 index 0000000000..ce5a969337 Binary files /dev/null and b/internal/generator/addchain/e1c71c7 differ diff --git a/internal/generator/addchain/f06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b904 b/internal/generator/addchain/f06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b904 new file mode 100644 index 0000000000..244393d6b3 Binary files /dev/null and b/internal/generator/addchain/f06e44682c2aa440f5f26a5ae1748fec171df66abbdb81ad07f36ed81b904 differ diff --git a/internal/generator/config/curve.go b/internal/generator/config/curve.go index e9d77c087f..c7bcdce646 100644 --- a/internal/generator/config/curve.go +++ b/internal/generator/config/curve.go @@ -14,6 +14,10 @@ type Curve struct { EnumID string FpModulus string FrModulus string + // 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 @@ -35,6 +39,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 +62,14 @@ 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 +} + type TwistedEdwardsCurve struct { Name string Package string @@ -102,6 +117,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/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/config/octobear.go b/internal/generator/config/octobear.go new file mode 100644 index 0000000000..7cc6ee05af --- /dev/null +++ b/internal/generator/config/octobear.go @@ -0,0 +1,29 @@ +package config + +var OCTOBEAR = Curve{ + Name: "octobear", + CurvePackage: "octobear", + EnumID: "OCTOBEAR", + 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, + CoordExtRoot: 3, + PointName: "g1", + GLV: false, + CofactorCleaning: false, + CRange: defaultCRange(), + }, +} + +func init() { + addCurve(&OCTOBEAR) +} diff --git a/internal/generator/field/config/field_config.go b/internal/generator/field/config/field_config.go index 8382624917..451164f384 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 if qMod3.Cmp(big.NewInt(1)) == 0 { // 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/template/element/cbrt.go.tmpl b/internal/generator/field/template/element/cbrt.go.tmpl index 22271e21f8..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) @@ -249,6 +254,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 +333,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 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/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()) diff --git a/internal/generator/field/template/extensions/e2.go.tmpl b/internal/generator/field/template/extensions/e2.go.tmpl index 9ed9fee4b3..7573bec3db 100644 --- a/internal/generator/field/template/extensions/e2.go.tmpl +++ b/internal/generator/field/template/extensions/e2.go.tmpl @@ -238,6 +238,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. @@ -261,6 +263,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 new file mode 100644 index 0000000000..e4c38ee434 --- /dev/null +++ b/internal/generator/field/template/extensions/e8.go.tmpl @@ -0,0 +1,416 @@ +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.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.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.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.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.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 +} + +// 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 { + 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 { + 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 +} + +// 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).MulByQuadraticNonResidue(&t1) + t0.Sub(&t0, &t1).Inverse(&t0) + z.C0.Mul(&x.C0, &t0) + 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 +} + +// 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.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 +} + +// 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).MulByQuadraticNonResidue(&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..2645953036 --- /dev/null +++ b/internal/generator/field/template/extensions/e8_test.go.tmpl @@ -0,0 +1,248 @@ +import ( + "math/big" + "testing" + + fr "{{ .FieldPackagePath }}" + + "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)} + }) +} + +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/fieldwrapper/generate.go b/internal/generator/fieldwrapper/generate.go new file mode 100644 index 0000000000..3a637dfa08 --- /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 { + 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", "", "", 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 new file mode 100644 index 0000000000..03032021ec --- /dev/null +++ b/internal/generator/fieldwrapper/template/fp.go.tmpl @@ -0,0 +1,53 @@ +import ( + "math/big" + + {{ .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/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 c6f3ac94a4..fc3c550f18 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" @@ -73,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) @@ -135,7 +138,9 @@ func main() { } // fp - { + 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) @@ -164,7 +169,9 @@ func main() { } // 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() {