Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions curve/ecgfp5/affine_point.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ecgfp5

import (
g "github.com/elliottech/poseidon_crypto/field/goldilocks"
gFp5 "github.com/elliottech/poseidon_crypto/field/goldilocks_quintic_extension"
)

Expand Down Expand Up @@ -46,18 +47,19 @@ func Lookup(win []AffinePoint, k int32) AffinePoint {
m := km1 - uint32(i) //nolint:gosec
c_1 := (m | (^m + 1)) >> 31
c := uint64(c_1) - 1
if c != 0 {
x = win[i].x
u = win[i].u
for limb := 0; limb < len(x); limb++ {
x[limb] |= g.GoldilocksField(uint64(win[i].x[limb]) & c)
u[limb] |= g.GoldilocksField(uint64(win[i].u[limb]) & c)
}

}

// If k < 0, then we must negate the point.
c := uint64(sign) | (uint64(sign) << 32)

if c != 0 {
u = gFp5.Neg(u)
c := uint64(0) - uint64(sign&1)
negativeU := gFp5.Neg(u)
for limb := 0; limb < len(u); limb++ {
u[limb] = g.GoldilocksField(
(uint64(u[limb]) &^ c) | (uint64(negativeU[limb]) & c),
)
}

return AffinePoint{x, u}
Expand Down
71 changes: 71 additions & 0 deletions curve/ecgfp5/curve_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package ecgfp5

import (
"encoding/binary"
"math/big"
"math/rand"
"testing"

g "github.com/elliottech/poseidon_crypto/field/goldilocks"
Expand Down Expand Up @@ -364,6 +367,18 @@ func TestToAffineAndLookup(t *testing.T) {

// Test lookup
win := BatchToAffine(tab1)
for k := -len(win); k <= len(win); k++ {
got := Lookup(win, int32(k))
want := LookupVarTime(win, int32(k))
if got != want {
t.Fatalf("Lookup(%d) = %v, want exact limbs %v", k, got, want)
}
}
for _, k := range []int32{-int32(len(win)) - 1, int32(len(win)) + 1, 72} {
if got := Lookup(win, k); got != AFFINE_NEUTRAL {
t.Fatalf("Lookup(%d) = %v, want neutral", k, got)
}
}
p1Affine := Lookup(win, 72)

if !gFp5.Equals(p1Affine.x, gFp5.FP5_ZERO) {
Expand Down Expand Up @@ -462,6 +477,62 @@ func TestScalarMul(t *testing.T) {
}
}

func TestMulGMatchesGenericMultiplication(t *testing.T) {
scalars := []ECgFp5Scalar{
ZERO,
ONE,
TWO,
NEG_ONE,
}
appendIfCanonical := func(value *big.Int) {
if value.Sign() >= 0 && value.Cmp(ORDER) < 0 {
scalars = append(scalars, FromNonCanonicalBigInt(value))
}
}
appendIfCanonical(new(big.Int).Sub(ORDER, big.NewInt(2)))
for bit := 0; bit <= 318; bit += generatorWindow {
boundary := new(big.Int).Lsh(big.NewInt(1), uint(bit))
appendIfCanonical(new(big.Int).Sub(new(big.Int).Set(boundary), big.NewInt(1)))
appendIfCanonical(boundary)
appendIfCanonical(new(big.Int).Add(new(big.Int).Set(boundary), big.NewInt(1)))
}
appendIfCanonical(new(big.Int).Lsh(big.NewInt(1), 318))
appendIfCanonical(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 319), big.NewInt(1)))

rng := rand.New(rand.NewSource(1)) //nolint:gosec // deterministic test corpus
var encoded [40]byte
for range 2_000 {
for offset := 0; offset < len(encoded); offset += 8 {
binary.LittleEndian.PutUint64(encoded[offset:], rng.Uint64())
}
scalars = append(scalars, ScalarElementFromLittleEndianBytes(encoded[:]))
}

for _, scalar := range scalars {
got := MulG(scalar).Encode()
want := GENERATOR_ECgFp5Point.Mul(scalar).Encode()
if got != want {
t.Fatalf("fixed-generator encoding differs for scalar %v: got %v, want %v", scalar, got, want)
}
}
}

func BenchmarkGeneratorMul(b *testing.B) {
scalar := SampleScalar()

b.Run("generic", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = GENERATOR_ECgFp5Point.Mul(scalar)
}
})

b.Run("fixed", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = MulG(scalar)
}
})
}

func testVectors() [8]gFp5.Element {
// P0 is neutral of G.
// P1 is a random point in G (encoded as w1)
Expand Down
64 changes: 62 additions & 2 deletions curve/ecgfp5/point.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ecgfp5

import (
"fmt"
"sync"

g "github.com/elliottech/poseidon_crypto/field/goldilocks"
gFp5 "github.com/elliottech/poseidon_crypto/field/goldilocks_quintic_extension"
Expand Down Expand Up @@ -447,8 +448,16 @@ func (p ECgFp5Point) AddAffine(rhs AffinePoint) ECgFp5Point {
}

const (
WINDOW = 5
WIN_SIZE = 1 << (WINDOW - 1)
WINDOW = 5
WIN_SIZE = 1 << (WINDOW - 1)
generatorWindow = 6
generatorWindowSize = 1 << (generatorWindow - 1)
generatorScalarDigits = (319 + generatorWindow) / generatorWindow
)

var (
generatorFixedWindowOnce sync.Once
generatorFixedWindowAffine []AffinePoint
)

// Convert points to affine coordinates.
Expand Down Expand Up @@ -556,6 +565,57 @@ func (r ECgFp5Point) Mul(s ECgFp5Scalar) ECgFp5Point {
return p
}

func makeGeneratorFixedWindowAffine() []AffinePoint {
points := make([]ECgFp5Point, generatorScalarDigits*generatorWindowSize)
base := GENERATOR_ECgFp5Point
for position := 0; position < generatorScalarDigits; position++ {
offset := position * generatorWindowSize
points[offset] = base
for i := 1; i < generatorWindowSize; i++ {
if i&1 == 0 {
points[offset+i] = points[offset+i-1].Add(base)
} else {
points[offset+i] = points[offset+(i>>1)].Double()
}
}
if position+1 < generatorScalarDigits {
base.SetMDouble(generatorWindow)
}
}
return BatchToAffine(points)
}

func getGeneratorFixedWindowAffine() []AffinePoint {
generatorFixedWindowOnce.Do(func() {
generatorFixedWindowAffine = makeGeneratorFixedWindowAffine()
})
return generatorFixedWindowAffine
}

// WarmGeneratorTable builds the fixed-generator table ahead of a
// latency-sensitive multiplication. The table is process-global and is built
// at most once.
func WarmGeneratorTable() {
_ = getGeneratorFixedWindowAffine()
}

// MulG multiplies the fixed curve generator by a scalar. Its position-weighted
// affine table removes both per-call window construction and all point
// doublings from the multiplication path.
func MulG(s ECgFp5Scalar) ECgFp5Point {
var digits [generatorScalarDigits]int32
s.RecodeSigned(digits[:], generatorWindow)
table := getGeneratorFixedWindowAffine()

p := Lookup(table[:generatorWindowSize], digits[0]).ToPoint()
for position := 1; position < len(digits); position++ {
offset := position * generatorWindowSize
p = p.AddAffine(Lookup(table[offset:offset+generatorWindowSize], digits[position]))
}

return p
}

// Returns G*scalarA + b*scalarB
func MulAddG(b ECgFp5Point, scalarA, scalarB ECgFp5Scalar) ECgFp5Point {
winA := GeneratorWindowAffine
Expand Down
44 changes: 34 additions & 10 deletions curve/ecgfp5/scalar_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import (
type ECgFp5Scalar [5]uint64

func (s ECgFp5Scalar) IsCanonical() bool {
return ToNonCanonicalBigInt(s).Cmp(ORDER) < 0
_, borrow := s.SubInner(N)
return borrow != 0
}

var (
Expand Down Expand Up @@ -70,11 +71,26 @@ func (s ECgFp5Scalar) SplitTo4BitLimbs() [80]uint8 {
}

func SampleScalar() ECgFp5Scalar {
rng, err := cryptorand.Int(cryptorand.Reader, ORDER)
if err != nil {
panic("failed to read random bytes into buffer")
var encoded [40]byte
for {
if _, err := cryptorand.Read(encoded[:]); err != nil {
panic("failed to read random bytes into buffer")
}
// The scalar order has a 319-bit bit length. Masking the excess bit
// before rejection sampling gives the same uniform distribution as
// crypto/rand.Int without constructing big.Int values.
encoded[39] &= 0x7f
candidate := ECgFp5Scalar{
binary.LittleEndian.Uint64(encoded[0:8]),
binary.LittleEndian.Uint64(encoded[8:16]),
binary.LittleEndian.Uint64(encoded[16:24]),
binary.LittleEndian.Uint64(encoded[24:32]),
binary.LittleEndian.Uint64(encoded[32:40]),
}
if candidate.IsCanonical() {
return candidate
}
}
return FromNonCanonicalBigInt(rng)
}

var (
Expand Down Expand Up @@ -254,13 +270,21 @@ func (s ECgFp5Scalar) MontyMul(rhs ECgFp5Scalar) ECgFp5Scalar {
}

func FromGfp5(fp5 gFp5.Element) ECgFp5Scalar {
result := new(big.Int)
for i := 4; i >= 0; i-- {
result.Lsh(result, 64)
result.Or(result, new(big.Int).SetUint64(fp5[i].ToCanonicalUint64()))
result := ECgFp5Scalar{
fp5[0].ToCanonicalUint64(),
fp5[1].ToCanonicalUint64(),
fp5[2].ToCanonicalUint64(),
fp5[3].ToCanonicalUint64(),
fp5[4].ToCanonicalUint64(),
}

return FromNonCanonicalBigInt(result)
// A five-limb Goldilocks value is below 2^320, while the scalar
// modulus is greater than 2^320/3. Therefore at most two conditional
// subtractions are required to reduce the value modulo N.
reduced, borrow := result.SubInner(N)
result = Select(borrow, reduced, result)
reduced, borrow = result.SubInner(N)
return Select(borrow, reduced, result)
}

// Warn: This won't work in 32-bit systems!
Expand Down
63 changes: 63 additions & 0 deletions curve/ecgfp5/scalar_field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,51 @@ func TestScalarElementFromLittleEndianBytesReduces(t *testing.T) {
}
}

func TestFromGfp5MatchesBigIntReduction(t *testing.T) {
values := []gFp5.Element{
{},
{1, 2, 3, 4, 5},
{g.GoldilocksField(g.ORDER - 1), g.GoldilocksField(g.ORDER - 1), g.GoldilocksField(g.ORDER - 1), g.GoldilocksField(g.ORDER - 1), g.GoldilocksField(g.ORDER - 1)},
{g.GoldilocksField(^uint64(0)), g.GoldilocksField(^uint64(0)), g.GoldilocksField(^uint64(0)), g.GoldilocksField(^uint64(0)), g.GoldilocksField(^uint64(0))},
}
for range 256 {
values = append(values, gFp5.Sample())
}

for _, value := range values {
reference := new(big.Int)
for i := 4; i >= 0; i-- {
reference.Lsh(reference, 64)
reference.Or(reference, new(big.Int).SetUint64(value[i].ToCanonicalUint64()))
}
want := FromNonCanonicalBigInt(reference)
if got := FromGfp5(value); !got.Equals(want) {
t.Fatalf("FromGfp5(%v) = %v, want %v", value, got, want)
}
}
}

func TestScalarOrderBitLength(t *testing.T) {
if got := ORDER.BitLen(); got != 319 {
t.Fatalf("scalar order bit length = %d, SampleScalar assumes 319", got)
}
}

func BenchmarkFromGfp5(b *testing.B) {
value := gFp5.Sample()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = FromGfp5(value)
}
}

func BenchmarkSampleScalar(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = SampleScalar()
}
}

func FuzzSerdes(f *testing.F) {
f.Add([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40})
f.Add(ORDER.Bytes())
Expand Down Expand Up @@ -314,6 +359,24 @@ func TestRecodeSigned(t *testing.T) {
}
}

func TestGeneratorWindowRecodeReconstructsLargestScalar(t *testing.T) {
scalar := FromNonCanonicalBigInt(new(big.Int).Sub(ORDER, big.NewInt(1)))
var digits [generatorScalarDigits]int32
scalar.RecodeSigned(digits[:], generatorWindow)

reconstructed := new(big.Int)
for position, digit := range digits {
term := new(big.Int).Lsh(big.NewInt(int64(digit)), uint(position*generatorWindow))
reconstructed.Add(reconstructed, term)
}
if want := new(big.Int).Sub(ORDER, big.NewInt(1)); reconstructed.Cmp(want) != 0 {
t.Fatalf("window recoding reconstructed %v, want %v", reconstructed, want)
}
if digits[len(digits)-1] < 0 {
t.Fatalf("top recoded digit = %d, want non-negative", digits[len(digits)-1])
}
}

func TestFromQuinticExtension(t *testing.T) {
scalar := FromGfp5(gFp5.Element{g.NegOneF(), g.NegOneF(), g.NegOneF(), g.NegOneF(), g.NegOneF()})

Expand Down
9 changes: 8 additions & 1 deletion hash/poseidon2_goldilocks_plonky2/poseidon2.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ func HashOutFromUint64Array(arr [4]uint64) HashOut {
}

func HashToQuinticExtension(m []g.GoldilocksField) gFp5.Element {
return gFp5.FromPlonky2GoldilocksField(HashNToMNoPad(m, 5))
var perm [WIDTH]g.GoldilocksField
for i := 0; i < len(m); i += RATE {
for j := 0; j < RATE && i+j < len(m); j++ {
perm[j] = m[i+j]
}
Permute(&perm)
}
return gFp5.Element{perm[0], perm[1], perm[2], perm[3], perm[4]}
}

type Poseidon2 struct{}
Expand Down
Loading