diff --git a/ecc/bls12-377/fr/poseidon2/doc.go b/ecc/bls12-377/fr/poseidon2/doc.go new file mode 100644 index 0000000000..c2f3d4688e --- /dev/null +++ b/ecc/bls12-377/fr/poseidon2/doc.go @@ -0,0 +1,17 @@ +// Copyright 2020-2025 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 poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 diff --git a/ecc/bls12-377/fr/poseidon2/gkrgates/gkrgates.go b/ecc/bls12-377/fr/poseidon2/gkrgates/gkrgates.go new file mode 100644 index 0000000000..9785856185 --- /dev/null +++ b/ecc/bls12-377/fr/poseidon2/gkrgates/gkrgates.go @@ -0,0 +1,193 @@ +// Package gkrgates implements the Poseidon2 permutation gate for GKR +// +// This implementation is based on the [poseidon2] package, but exposes the +// primitives as gates for inclusion in GKR circuits. + +// TODO(@Tabaie @ThomasPiellard) generify once Poseidon2 parameters are known for all curves +package gkrgates + +import ( + "fmt" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/gkr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2" +) + +// The GKR gates needed for proving Poseidon2 permutations + +// extKeySBoxGate applies the external matrix mul, then adds the round key, then applies the sBox +// because of its symmetry, we don't need to define distinct x1 and x2 versions of it +type extKeySBoxGate struct { + roundKey fr.Element +} + +func (g *extKeySBoxGate) Evaluate(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("expected 2 inputs") + } + + x[0]. + Double(&x[0]). + Add(&x[0], &x[1]). + Add(&x[0], &g.roundKey) + return sBox2(x[0]) +} + +func (g *extKeySBoxGate) Degree() int { + return poseidon2.DegreeSBox() +} + +// for x1, the partial round gates are identical to full round gates +// for x2, the partial round gates are just a linear combination +// TODO @Tabaie eliminate the x2 partial round gates and have the x1 gates depend on i - rf/2 or so previous x1's + +// extGate2 applies the external matrix mul, outputting the second element of the result +type extGate2 struct{} + +func (extGate2) Evaluate(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("expected 2 inputs") + } + x[1]. + Double(&x[1]). + Add(&x[1], &x[0]) + return x[1] +} + +func (g extGate2) Degree() int { + return 1 +} + +// intGate2 applies the internal matrix mul, returning the second element +type intGate2 struct { +} + +func (g intGate2) Evaluate(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("expected 2 inputs") + } + x[0].Add(&x[0], &x[1]) + x[1]. + Double(&x[1]). + Add(&x[1], &x[0]) + return x[1] +} + +func (g intGate2) Degree() int { + return 1 +} + +// intKeySBoxGateFr applies the second row of internal matrix mul, then adds the round key, then applies the sBox +type intKeySBoxGate2 struct { + roundKey fr.Element +} + +func (g *intKeySBoxGate2) Evaluate(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("expected 2 inputs") + } + x[0].Add(&x[0], &x[1]) + x[1]. + Double(&x[1]). + Add(&x[1], &x[0]). + Add(&x[1], &g.roundKey) + + return sBox2(x[1]) +} + +func (g *intKeySBoxGate2) Degree() int { + return poseidon2.DegreeSBox() +} + +type extGate struct{} + +func (g extGate) Evaluate(x ...fr.Element) fr.Element { + if len(x) != 2 { + panic("expected 2 inputs") + } + x[0]. + Double(&x[0]). + Add(&x[0], &x[1]) + return x[0] +} + +func (g extGate) Degree() int { + return 1 +} + +// sBox2 is Hash.sBox for t=2 +func sBox2(x fr.Element) fr.Element { + var y fr.Element + y.Square(&x).Square(&y).Square(&y).Square(&y).Mul(&x, &y) + return y +} + +var initOnce sync.Once + +// RegisterGkrGates registers the Poseidon2 permutation gates for GKR +func RegisterGkrGates() { + initOnce.Do( + func() { + p := poseidon2.NewDefaultParameters() + halfRf := p.NbFullRounds / 2 + + gateNameX := func(i int) string { + return fmt.Sprintf("x-round=%d%s", i, p.String()) + } + gateNameY := func(i int) string { + return fmt.Sprintf("y-round=%d%s", i, p.String()) + } + + fullRound := func(i int) { + gkr.Gates[gateNameX(i)] = &extKeySBoxGate{ + roundKey: p.RoundKeys[i][0], + } + + gkr.Gates[gateNameY(i)] = &extKeySBoxGate{ + roundKey: p.RoundKeys[i][1], + } + } + + for i := range halfRf { + fullRound(i) + } + + { // i = halfRf: first partial round + i := halfRf + gkr.Gates[gateNameX(i)] = &extKeySBoxGate{ + roundKey: p.RoundKeys[i][0], + } + + gkr.Gates[gateNameY(i)] = extGate2{} + } + + for i := halfRf + 1; i < halfRf+p.NbPartialRounds; i++ { + gkr.Gates[gateNameX(i)] = &extKeySBoxGate{ // for x1, intKeySBox is identical to extKeySBox + roundKey: p.RoundKeys[i][0], + } + + gkr.Gates[gateNameY(i)] = intGate2{} + + } + + { + i := halfRf + p.NbPartialRounds + gkr.Gates[gateNameX(i)] = &extKeySBoxGate{ + roundKey: p.RoundKeys[i][0], + } + + gkr.Gates[gateNameY(i)] = &intKeySBoxGate2{ + roundKey: p.RoundKeys[i][1], + } + } + + for i := halfRf + p.NbPartialRounds + 1; i < p.NbPartialRounds+p.NbFullRounds; i++ { + fullRound(i) + } + + gkr.Gates[gateNameY(p.NbPartialRounds+p.NbFullRounds)] = extGate{} + }, + ) +} diff --git a/ecc/bls12-377/fr/poseidon2/hash.go b/ecc/bls12-377/fr/poseidon2/hash.go new file mode 100644 index 0000000000..c08cfcdb62 --- /dev/null +++ b/ecc/bls12-377/fr/poseidon2/hash.go @@ -0,0 +1,31 @@ +package poseidon2 + +import ( + "hash" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + gnarkHash "github.com/consensys/gnark-crypto/hash" +) + +// NewMerkleDamgardHasher returns a Poseidon2 hasher using the Merkle-Damgard +// construction with the default parameters. +func NewMerkleDamgardHasher() gnarkHash.StateStorer { + // TODO @Tabaie @ThomasPiellard Generify once Poseidon2 parameters are known for all curves + return gnarkHash.NewMerkleDamgardHasher( + &Permutation{params: NewDefaultParameters()}, make([]byte, fr.Bytes)) +} + +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// The default parameters are: +// - width: 2 +// - nbFullRounds: 6 +// - nbPartialRounds: 26 +func NewDefaultParameters() *Parameters { + return NewParameters(2, 6, 26) +} + +func init() { + gnarkHash.RegisterHash(gnarkHash.POSEIDON2_BLS12_377, func() hash.Hash { + return NewMerkleDamgardHasher() + }) +} diff --git a/ecc/bls12-377/fr/poseidon2/poseidon2.go b/ecc/bls12-377/fr/poseidon2/poseidon2.go index e5d7599e59..a4f8ef3970 100644 --- a/ecc/bls12-377/fr/poseidon2/poseidon2.go +++ b/ecc/bls12-377/fr/poseidon2/poseidon2.go @@ -7,55 +7,73 @@ package poseidon2 import ( "errors" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "fmt" + "golang.org/x/crypto/sha3" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" ) var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + d = 17 +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-BLS12_377[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -64,37 +82,65 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf / 2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp + rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) @@ -116,7 +162,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -135,39 +181,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -176,69 +222,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -246,3 +298,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/ecc/bls12-377/fr/poseidon2/poseidon2_test.go b/ecc/bls12-377/fr/poseidon2/poseidon2_test.go index c123dee64d..a62c7af9a3 100644 --- a/ecc/bls12-377/fr/poseidon2/poseidon2_test.go +++ b/ecc/bls12-377/fr/poseidon2/poseidon2_test.go @@ -12,6 +12,7 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -34,7 +35,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -55,7 +56,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom() diff --git a/ecc/bls12-381/fr/poseidon2/doc.go b/ecc/bls12-381/fr/poseidon2/doc.go new file mode 100644 index 0000000000..c2f3d4688e --- /dev/null +++ b/ecc/bls12-381/fr/poseidon2/doc.go @@ -0,0 +1,17 @@ +// Copyright 2020-2025 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 poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 diff --git a/ecc/bls12-381/fr/poseidon2/poseidon2.go b/ecc/bls12-381/fr/poseidon2/poseidon2.go index 68678b7636..cc4229c02c 100644 --- a/ecc/bls12-381/fr/poseidon2/poseidon2.go +++ b/ecc/bls12-381/fr/poseidon2/poseidon2.go @@ -7,55 +7,73 @@ package poseidon2 import ( "errors" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "fmt" + "golang.org/x/crypto/sha3" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" ) var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + d = 5 +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-BLS12_381[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -64,37 +82,65 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf / 2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp + rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) @@ -114,7 +160,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -133,39 +179,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -174,69 +220,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -244,3 +296,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/ecc/bls12-381/fr/poseidon2/poseidon2_test.go b/ecc/bls12-381/fr/poseidon2/poseidon2_test.go index 4851c090ee..5f38fe725b 100644 --- a/ecc/bls12-381/fr/poseidon2/poseidon2_test.go +++ b/ecc/bls12-381/fr/poseidon2/poseidon2_test.go @@ -12,6 +12,7 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -34,7 +35,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -55,7 +56,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom() diff --git a/ecc/bls24-315/fr/poseidon2/doc.go b/ecc/bls24-315/fr/poseidon2/doc.go new file mode 100644 index 0000000000..c2f3d4688e --- /dev/null +++ b/ecc/bls24-315/fr/poseidon2/doc.go @@ -0,0 +1,17 @@ +// Copyright 2020-2025 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 poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 diff --git a/ecc/bls24-315/fr/poseidon2/poseidon2.go b/ecc/bls24-315/fr/poseidon2/poseidon2.go index 9428276aa0..a6f5485363 100644 --- a/ecc/bls24-315/fr/poseidon2/poseidon2.go +++ b/ecc/bls24-315/fr/poseidon2/poseidon2.go @@ -7,55 +7,73 @@ package poseidon2 import ( "errors" - "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "fmt" + "golang.org/x/crypto/sha3" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" ) var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + d = 5 +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-BLS24_315[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -64,37 +82,65 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf / 2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp + rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) @@ -114,7 +160,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -133,39 +179,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -174,69 +220,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -244,3 +296,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/ecc/bls24-315/fr/poseidon2/poseidon2_test.go b/ecc/bls24-315/fr/poseidon2/poseidon2_test.go index 41da3d5728..37d3b61052 100644 --- a/ecc/bls24-315/fr/poseidon2/poseidon2_test.go +++ b/ecc/bls24-315/fr/poseidon2/poseidon2_test.go @@ -12,6 +12,7 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -34,7 +35,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -55,7 +56,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom() diff --git a/ecc/bls24-317/fr/poseidon2/doc.go b/ecc/bls24-317/fr/poseidon2/doc.go new file mode 100644 index 0000000000..c2f3d4688e --- /dev/null +++ b/ecc/bls24-317/fr/poseidon2/doc.go @@ -0,0 +1,17 @@ +// Copyright 2020-2025 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 poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 diff --git a/ecc/bls24-317/fr/poseidon2/poseidon2.go b/ecc/bls24-317/fr/poseidon2/poseidon2.go index 2cb42d683d..bdb6b72a64 100644 --- a/ecc/bls24-317/fr/poseidon2/poseidon2.go +++ b/ecc/bls24-317/fr/poseidon2/poseidon2.go @@ -7,55 +7,73 @@ package poseidon2 import ( "errors" - "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "fmt" + "golang.org/x/crypto/sha3" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" ) var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + d = 7 +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-BLS24_317[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -64,37 +82,65 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf / 2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp + rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) @@ -115,7 +161,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -134,39 +180,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -175,69 +221,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -245,3 +297,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/ecc/bls24-317/fr/poseidon2/poseidon2_test.go b/ecc/bls24-317/fr/poseidon2/poseidon2_test.go index eb107b7a64..5e1e8082c3 100644 --- a/ecc/bls24-317/fr/poseidon2/poseidon2_test.go +++ b/ecc/bls24-317/fr/poseidon2/poseidon2_test.go @@ -12,6 +12,7 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -34,7 +35,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -55,7 +56,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom() diff --git a/ecc/bn254/fr/poseidon2/doc.go b/ecc/bn254/fr/poseidon2/doc.go new file mode 100644 index 0000000000..c2f3d4688e --- /dev/null +++ b/ecc/bn254/fr/poseidon2/doc.go @@ -0,0 +1,17 @@ +// Copyright 2020-2025 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 poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 diff --git a/ecc/bn254/fr/poseidon2/poseidon2.go b/ecc/bn254/fr/poseidon2/poseidon2.go index a4a3ffd4bf..e75614ff00 100644 --- a/ecc/bn254/fr/poseidon2/poseidon2.go +++ b/ecc/bn254/fr/poseidon2/poseidon2.go @@ -7,55 +7,73 @@ package poseidon2 import ( "errors" - "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "fmt" + "golang.org/x/crypto/sha3" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" ) var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + d = 5 +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-BN254[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -64,37 +82,65 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf / 2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp + rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) @@ -114,7 +160,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -133,39 +179,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -174,69 +220,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -244,3 +296,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/ecc/bn254/fr/poseidon2/poseidon2_test.go b/ecc/bn254/fr/poseidon2/poseidon2_test.go index 712f04accf..2f056ef0b9 100644 --- a/ecc/bn254/fr/poseidon2/poseidon2_test.go +++ b/ecc/bn254/fr/poseidon2/poseidon2_test.go @@ -12,6 +12,7 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -34,7 +35,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -55,7 +56,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom() diff --git a/ecc/bw6-633/fr/poseidon2/doc.go b/ecc/bw6-633/fr/poseidon2/doc.go new file mode 100644 index 0000000000..c2f3d4688e --- /dev/null +++ b/ecc/bw6-633/fr/poseidon2/doc.go @@ -0,0 +1,17 @@ +// Copyright 2020-2025 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 poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 diff --git a/ecc/bw6-633/fr/poseidon2/poseidon2.go b/ecc/bw6-633/fr/poseidon2/poseidon2.go index 8402333265..4d432e3aa3 100644 --- a/ecc/bw6-633/fr/poseidon2/poseidon2.go +++ b/ecc/bw6-633/fr/poseidon2/poseidon2.go @@ -7,55 +7,73 @@ package poseidon2 import ( "errors" - "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "fmt" + "golang.org/x/crypto/sha3" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" ) var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + d = 5 +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-BW6_633[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -64,37 +82,65 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf / 2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp + rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) @@ -114,7 +160,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -133,39 +179,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -174,69 +220,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -244,3 +296,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/ecc/bw6-633/fr/poseidon2/poseidon2_test.go b/ecc/bw6-633/fr/poseidon2/poseidon2_test.go index 3effc4fc52..c7a2ddfd97 100644 --- a/ecc/bw6-633/fr/poseidon2/poseidon2_test.go +++ b/ecc/bw6-633/fr/poseidon2/poseidon2_test.go @@ -12,6 +12,7 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -34,7 +35,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -55,7 +56,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom() diff --git a/ecc/bw6-761/fr/poseidon2/doc.go b/ecc/bw6-761/fr/poseidon2/doc.go new file mode 100644 index 0000000000..c2f3d4688e --- /dev/null +++ b/ecc/bw6-761/fr/poseidon2/doc.go @@ -0,0 +1,17 @@ +// Copyright 2020-2025 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 poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 diff --git a/ecc/bw6-761/fr/poseidon2/poseidon2.go b/ecc/bw6-761/fr/poseidon2/poseidon2.go index 6e2021471d..0f70e03d75 100644 --- a/ecc/bw6-761/fr/poseidon2/poseidon2.go +++ b/ecc/bw6-761/fr/poseidon2/poseidon2.go @@ -7,55 +7,73 @@ package poseidon2 import ( "errors" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "fmt" + "golang.org/x/crypto/sha3" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" ) var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + d = 5 +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-BW6_761[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -64,37 +82,65 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf / 2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp + rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) @@ -114,7 +160,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -133,39 +179,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -174,69 +220,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -244,3 +296,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/ecc/bw6-761/fr/poseidon2/poseidon2_test.go b/ecc/bw6-761/fr/poseidon2/poseidon2_test.go index f615ff8dfc..59e8706952 100644 --- a/ecc/bw6-761/fr/poseidon2/poseidon2_test.go +++ b/ecc/bw6-761/fr/poseidon2/poseidon2_test.go @@ -12,6 +12,7 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -34,7 +35,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -55,7 +56,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom() diff --git a/hash/all/allhashes.go b/hash/all/allhashes.go index cb7b5947e3..a06636e056 100644 --- a/hash/all/allhashes.go +++ b/hash/all/allhashes.go @@ -2,6 +2,7 @@ package all import ( _ "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/mimc" + _ "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2" _ "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/mimc" _ "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/mimc" _ "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/mimc" diff --git a/hash/doc.go b/hash/doc.go index b1ed8c91c3..04d4fdfe55 100644 --- a/hash/doc.go +++ b/hash/doc.go @@ -1,14 +1,18 @@ -// Package hash provides MiMC hash function defined over implemented curves +// Package hash provides algebraic hash function defined over implemented curves // -// This package is kept for backwards compatibility. The recommended way to -// initialize hash function is to directly use the constructors in the -// corresponding packages (e.g. ecc/bn254/fr/mimc). Using the direct -// constructors allows to apply options for altering the hash function behavior -// (endianness, input splicing etc.) and returns more specific types with -// additional methods. +// This package is kept for backwards compatibility for initializing hash +// functions directly. The recommended way to initialize hash function is to +// directly use the constructors in the corresponding packages (e.g. +// ecc/bn254/fr/mimc). Using the direct constructors allows to apply options for +// altering the hash function behavior (endianness, input splicing etc.) and +// returns more specific types with additional methods. // // See [Importing hash functions] below for more information. // +// This package also provides a construction for a generic hash function from +// the compression primitive. See the interface [Compressor] and the +// corresponding initialization function [NewMerkleDamgardHasher]. +// // # Importing hash functions // // The package follows registration pattern for importing hash functions. To @@ -58,7 +62,7 @@ // The MiMC hash function is defined over a field. The input to the hash // function is a byte slice. The byte slice is interpreted as a sequence of // field elements. Due to this interpretation, the input byte slice length must -// be multiple of the field modulus size. And every secuence of byte slice for a +// be multiple of the field modulus size. And every sequence of byte slice for a // single field element must be strictly less than the field modulus. // // See open issues: diff --git a/hash/hashes.go b/hash/hashes.go index 897a8f11fe..4f433c86d9 100644 --- a/hash/hashes.go +++ b/hash/hashes.go @@ -38,19 +38,22 @@ const ( MIMC_BLS24_317 // MIMC_BW6_633 is the MiMC hash function for the BW6-633 curve. MIMC_BW6_633 + // POSEIDON2_BLS12_377 is the Poseidon2 hash function for the BLS12-377 curve. + POSEIDON2_BLS12_377 maxHash ) // size of digests in bytes var digestSize = []uint8{ - MIMC_BN254: 32, - MIMC_BLS12_381: 48, - MIMC_BLS12_377: 48, - MIMC_BW6_761: 96, - MIMC_BLS24_315: 48, - MIMC_BLS24_317: 48, - MIMC_BW6_633: 80, + MIMC_BN254: 32, + MIMC_BLS12_381: 48, + MIMC_BLS12_377: 48, + MIMC_BW6_761: 96, + MIMC_BLS24_315: 48, + MIMC_BLS24_317: 48, + MIMC_BW6_633: 80, + POSEIDON2_BLS12_377: 48, } // New initializes the hash function. This is a convenience function which does @@ -87,6 +90,8 @@ func (m Hash) String() string { return "MIMC_BLS24_317" case MIMC_BW6_633: return "MIMC_BW6_633" + case POSEIDON2_BLS12_377: + return "POSEIDON2_BLS12_377" default: return "unknown hash function" } diff --git a/hash/interface.go b/hash/interface.go index 5b2c3063b4..34422ede6e 100644 --- a/hash/interface.go +++ b/hash/interface.go @@ -14,3 +14,21 @@ type StateStorer interface { // state retrieved using [StateStorer.State] method. SetState(state []byte) error } + +// Compressor is a 2-1 one-way function. It takes two inputs and compresses +// them into one output. The inputs and outputs are all of the same size, which +// is the block size. See [BlockSize]. +// +// NB! This is lossy compression, meaning that the output is not guaranteed to +// be unique for different inputs. The output is guaranteed to be the same for +// the same inputs. +// +// The Compressor is used in the Merkle-Damgard construction to build a hash +// function. +type Compressor interface { + // Compress compresses the two inputs into one output. All the inputs and + // outputs are of the same size, which is the block size. See [BlockSize]. + Compress(left []byte, right []byte) (compressed []byte, err error) + // BlockSize returns the blocks size. + BlockSize() int +} diff --git a/hash/merkle-damgard.go b/hash/merkle-damgard.go new file mode 100644 index 0000000000..2ee1215eae --- /dev/null +++ b/hash/merkle-damgard.go @@ -0,0 +1,73 @@ +package hash + +type merkleDamgardHasher struct { + state []byte + iv []byte + f Compressor +} + +// Write implements hash.Write +func (h *merkleDamgardHasher) Write(p []byte) (n int, err error) { + blockSize := h.f.BlockSize() + for len(p) != 0 { + if len(p) < blockSize { + p = append(make([]byte, blockSize-len(p), blockSize), p...) + } + if h.state, err = h.f.Compress(h.state, p[:blockSize]); err != nil { + return + } + n += blockSize + p = p[blockSize:] + } + return +} + +func (h *merkleDamgardHasher) Sum(b []byte) []byte { + if _, err := h.Write(b); err != nil { + panic(err) + } + return h.state +} + +func (h *merkleDamgardHasher) Reset() { + h.state = h.iv +} + +func (h *merkleDamgardHasher) Size() int { + return h.f.BlockSize() +} + +func (h *merkleDamgardHasher) BlockSize() int { + return h.f.BlockSize() +} + +func (h *merkleDamgardHasher) State() []byte { + return h.state +} + +func (h *merkleDamgardHasher) SetState(state []byte) error { + h.state = state + return nil +} + +// NewMerkleDamgardHasher transforms a 2-1 one-way compression function into a +// hash function using a Merkle-Damgard construction. The resulting hash +// function has a block size equal to the block size of compression function. +// +// NB! The construction does not perform explicit padding on the input data. The +// last block of input data is zero-padded to full block size. This means that +// the construction is not collision resistant for generic data as the digest of +// input and input concatenated with zeros (up to the same number of total +// blocks) is same. For collision resistance the caller should perform explicit +// padding on the input data. +// +// The value initialState is provided as initial input to the compression +// function. Its preimage should not be known and thus it should be generated +// using a deterministic method. +func NewMerkleDamgardHasher(f Compressor, initialState []byte) StateStorer { + return &merkleDamgardHasher{ + state: initialState, + iv: initialState, + f: f, + } +} diff --git a/internal/generator/crypto/hash/poseidon2/generate.go b/internal/generator/crypto/hash/poseidon2/generate.go index 2c05bb08eb..0267fe2d83 100644 --- a/internal/generator/crypto/hash/poseidon2/generate.go +++ b/internal/generator/crypto/hash/poseidon2/generate.go @@ -11,6 +11,7 @@ func Generate(conf config.Curve, baseDir string, bgen *bavard.BatchGenerator) er conf.Package = "poseidon2" entries := []bavard.Entry{ + {File: filepath.Join(baseDir, "doc.go"), Templates: []string{"doc.go.tmpl"}}, {File: filepath.Join(baseDir, "poseidon2.go"), Templates: []string{"poseidon2.go.tmpl"}}, {File: filepath.Join(baseDir, "poseidon2_test.go"), Templates: []string{"poseidon2.test.go.tmpl"}}, } diff --git a/internal/generator/crypto/hash/poseidon2/template/doc.go.tmpl b/internal/generator/crypto/hash/poseidon2/template/doc.go.tmpl new file mode 100644 index 0000000000..3897419232 --- /dev/null +++ b/internal/generator/crypto/hash/poseidon2/template/doc.go.tmpl @@ -0,0 +1,12 @@ +// Package poseidon2 implements the Poseidon2 permutation +// +// Poseidon2 permutation is a cryptographic permutation for algebraic hashes. +// See the [original paper] by Grassi, Khovratovich and Schofnegger for the full details. +// +// This implementation is based on the [reference implementation] from +// HorizenLabs. See the [specifications] for parameter choices. +// +// [reference implementation]: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs +// [specifications]: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf +// [original paper]: https://eprint.iacr.org/2023/323.pdf +package poseidon2 \ No newline at end of file diff --git a/internal/generator/crypto/hash/poseidon2/template/poseidon2.go.tmpl b/internal/generator/crypto/hash/poseidon2/template/poseidon2.go.tmpl index 013c8a8e40..7134941c45 100644 --- a/internal/generator/crypto/hash/poseidon2/template/poseidon2.go.tmpl +++ b/internal/generator/crypto/hash/poseidon2/template/poseidon2.go.tmpl @@ -1,6 +1,9 @@ import ( "errors" + "fmt" + "golang.org/x/crypto/sha3" + "github.com/consensys/gnark-crypto/ecc/{{ .Name }}/fr" ) @@ -8,47 +11,68 @@ var ( ErrInvalidSizebuffer = errors.New("the size of the input should match the size of the hash buffer") ) -// reference implementation: https://github.com/HorizenLabs/poseidon2/blob/main/plain_implementations/src/poseidon2/poseidon2.rs -// specifications: https://github.com/argumentcomputer/neptune/blob/main/spec/poseidon_spec.pdf -// origina paper: https://eprint.iacr.org/2023/323.pdf +const ( + // d is the degree of the sBox + {{- if eq .Name "bls12-377" }} + d = 17 + {{ else if eq .Name "bls24-317" }} + d = 7 + {{ else }} + d = 5 + {{ end -}} +) -// parameters describing the poseidon2 implementation -type parameters struct { +// DegreeSBox returns the degree of the sBox function used in the Poseidon2 +// permutation. +func DegreeSBox() int { + return d +} + +// Parameters describing the Poseidon2 implementation. Use [NewParameters] or +// [NewParametersWithSeed] to initialize a new set of parameters to +// deterministically precompute the round keys. +type Parameters struct { // len(preimage)+len(digest)=len(preimage)+ceil(log(2*/r)) - t int + Width int // number of full rounds (even number) - rF int + NbFullRounds int // number of partial rounds - rP int + NbPartialRounds int - // diagonal elements of the internal matrices, minus one - diagInternalMatrices []fr.Element - - // round keys - roundKeys [][]fr.Element + // derived round keys from the parameter seed and curve ID + RoundKeys [][]fr.Element } -// Hash stores the buffer of the poseidon2 permutation and provides poseidon2 permutation -// methods on the buffer -type Hash struct { +// NewParameters returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the seed which is a digest of the parameters and curve ID. +func NewParameters(width, nbFullRounds, nbPartialRounds int) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + seed := p.String() + p.initRC(seed) + return &p +} - // parameters describing the - params parameters +// NewParametersWithSeed returns a new set of parameters for the Poseidon2 permutation. +// After creating the parameters, the round keys are initialized deterministically +// from the given seed. +func NewParametersWithSeed(width, nbFullRounds, nbPartialRounds int, seed string) *Parameters { + p := Parameters{Width: width, NbFullRounds: nbFullRounds, NbPartialRounds: nbPartialRounds} + p.initRC(seed) + return &p } -// NewHash returns a new hash instance allowing to apply the poseidon2 permutation -func NewHash(t, rf, rp int, seed string) Hash { - params := parameters{t: t, rF: rf, rP: rp} - params.roundKeys = InitRC(seed, rf, rp, t) - res := Hash{params: params} - return res +// String returns a string representation of the parameters. It is unique for +// specific parameters and curve. +func (p *Parameters) String() string { + return fmt.Sprintf("Poseidon2-{{.EnumID}}[t=%d,rF=%d,rP=%d,d=%d]", p.Width, p.NbFullRounds, p.NbPartialRounds, d) } -// InitRC initiate round keys. Only one entry is non zero for the internal +// initRC initiate round keys. Only one entry is non zero for the internal // rounds, cf https://eprint.iacr.org/2023/323.pdf page 9 -func InitRC(seed string, rf, rp, t int) [][]fr.Element { +func (p *Parameters) initRC(seed string) { bseed := ([]byte)(seed) hash := sha3.NewLegacyKeccak256() @@ -57,37 +81,66 @@ func InitRC(seed string, rf, rp, t int) [][]fr.Element { hash.Reset() _, _ = hash.Write(rnd) - roundKeys := make([][]fr.Element, rf+rp) - for i := 0; i < rf/2; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + roundKeys := make([][]fr.Element, p.NbFullRounds+p.NbPartialRounds) + for i := 0; i < p.NbFullRounds/2; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - for i := rf/2; i < rp+rf/2; i++ { + for i := p.NbFullRounds / 2; i < p.NbPartialRounds+p.NbFullRounds/2; i++ { roundKeys[i] = make([]fr.Element, 1) rnd = hash.Sum(nil) roundKeys[i][0].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } - for i := rp+rf/2; i < rp+rf; i++ { - roundKeys[i] = make([]fr.Element, t) - for j := 0; j < t; j++ { + for i := p.NbPartialRounds + p.NbFullRounds/2; i < p.NbPartialRounds+p.NbFullRounds; i++ { + roundKeys[i] = make([]fr.Element, p.Width) + for j := 0; j < p.Width; j++ { rnd = hash.Sum(nil) roundKeys[i][j].SetBytes(rnd) hash.Reset() _, _ = hash.Write(rnd) } } - return roundKeys + p.RoundKeys = roundKeys +} + +// Permutation stores the buffer of the Poseidon2 permutation and provides +// Poseidon2 permutation methods on the buffer +type Permutation struct { + // parameters describing the instance + params *Parameters +} + +// NewPermutation returns a new Poseidon2 permutation instance. +func NewPermutation(t, rf, rp int) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParameters(t, rf, rp) + res := &Permutation{params: params} + return res +} + +// NewPermutationWithSeed returns a new Poseidon2 permutation instance with a +// given seed. +func NewPermutationWithSeed(t, rf, rp int, seed string) *Permutation { + if t < 2 || t > 3 { + panic("only t=2,3 is supported") + } + params := NewParametersWithSeed(t, rf, rp, seed) + res := &Permutation{params: params} + return res } + // sBox applies the sBox on buffer[index] -func (h *Hash) sBox(index int, input []fr.Element) { +func (h *Permutation) sBox(index int, input []fr.Element) { var tmp fr.Element tmp.Set(&input[index]) {{ if eq .Name "bls12-377" }} @@ -120,7 +173,7 @@ func (h *Hash) sBox(index int, input []fr.Element) { // (1 1 4 6) // on chunks of 4 elemts on each part of the buffer // see https://eprint.iacr.org/2023/323.pdf appendix B for the addition chain -func (h *Hash) matMulM4InPlace(s []fr.Element) { +func (h *Permutation) matMulM4InPlace(s []fr.Element) { c := len(s) / 4 for i := 0; i < c; i++ { var t0, t1, t2, t3, t4, t5, t6, t7 fr.Element @@ -139,39 +192,39 @@ func (h *Hash) matMulM4InPlace(s []fr.Element) { } } -// when t=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) -// see https://eprint.iacr.org/2023/323.pdf page 15, case t=2,3 +// when T=2,3 the buffer is multiplied by circ(2,1) and circ(2,1,1) +// see https://eprint.iacr.org/2023/323.pdf page 15, case T=2,3 // -// when t=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) +// when T=0[4], the buffer is multiplied by circ(2M4,M4,..,M4) // see https://eprint.iacr.org/2023/323.pdf -func (h *Hash) matMulExternalInPlace(input []fr.Element) { +func (h *Permutation) matMulExternalInPlace(input []fr.Element) { - if h.params.t == 2 { + if h.params.Width == 2 { var tmp fr.Element tmp.Add(&input[0], &input[1]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) - } else if h.params.t == 3 { + } else if h.params.Width == 3 { var tmp fr.Element tmp.Add(&input[0], &input[1]). Add(&tmp, &input[2]) input[0].Add(&tmp, &input[0]) input[1].Add(&tmp, &input[1]) input[2].Add(&tmp, &input[2]) - } else if h.params.t == 4 { + } else if h.params.Width == 4 { h.matMulM4InPlace(input) } else { // at this stage t is supposed to be a multiple of 4 // the MDS matrix is circ(2M4,M4,..,M4) h.matMulM4InPlace(input) tmp := make([]fr.Element, 4) - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { tmp[0].Add(&tmp[0], &input[4*i]) tmp[1].Add(&tmp[1], &input[4*i+1]) tmp[2].Add(&tmp[2], &input[4*i+2]) tmp[3].Add(&tmp[3], &input[4*i+3]) } - for i := 0; i < h.params.t/4; i++ { + for i := 0; i < h.params.Width/4; i++ { input[4*i].Add(&input[4*i], &tmp[0]) input[4*i+1].Add(&input[4*i], &tmp[1]) input[4*i+2].Add(&input[4*i], &tmp[2]) @@ -180,69 +233,75 @@ func (h *Hash) matMulExternalInPlace(input []fr.Element) { } } -// when t=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] +// when T=2,3 the matrix are respectibely [[2,1][1,3]] and [[2,1,1][1,2,1][1,1,3]] // otherwise the matrix is filled with ones except on the diagonal, -func (h *Hash) matMulInternalInPlace(input []fr.Element) { - if h.params.t == 2 { +func (h *Permutation) matMulInternalInPlace(input []fr.Element) { + switch h.params.Width { + case 2: var sum fr.Element sum.Add(&input[0], &input[1]) input[0].Add(&input[0], &sum) input[1].Double(&input[1]).Add(&input[1], &sum) - } else if h.params.t == 3 { + case 3: var sum fr.Element sum.Add(&input[0], &input[1]).Add(&sum, &input[2]) input[0].Add(&input[0], &sum) input[1].Add(&input[1], &sum) input[2].Double(&input[2]).Add(&input[2], &sum) - } else { - var sum fr.Element - sum.Set(&input[0]) - for i := 1; i < h.params.t; i++ { - sum.Add(&sum, &input[i]) - } - for i := 0; i < h.params.t; i++ { - input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). - Add(&input[i], &sum) - } + default: + // var sum fr.Element + // sum.Set(&input[0]) + // for i := 1; i < h.params.t; i++ { + // sum.Add(&sum, &input[i]) + // } + // for i := 0; i < h.params.t; i++ { + // input[i].Mul(&input[i], &h.params.diagInternalMatrices[i]). + // Add(&input[i], &sum) + // } + panic("only T=2,3 is supported") } } // addRoundKeyInPlace adds the round-th key to the buffer -func (h *Hash) addRoundKeyInPlace(round int, input []fr.Element) { - for i := 0; i < len(h.params.roundKeys[round]); i++ { - input[i].Add(&input[i], &h.params.roundKeys[round][i]) +func (h *Permutation) addRoundKeyInPlace(round int, input []fr.Element) { + for i := 0; i < len(h.params.RoundKeys[round]); i++ { + input[i].Add(&input[i], &h.params.RoundKeys[round][i]) } } +func (h *Permutation) BlockSize() int { + return fr.Bytes +} + // Permutation applies the permutation on input, and stores the result in input. -func (h *Hash) Permutation(input []fr.Element) error { - if len(input) != h.params.t { +func (h *Permutation) Permutation(input []fr.Element) error { + if len(input) != h.params.Width { return ErrInvalidSizebuffer } // external matrix multiplication, cf https://eprint.iacr.org/2023/323.pdf page 14 (part 6) h.matMulExternalInPlace(input) - rf := h.params.rF / 2 + rf := h.params.NbFullRounds / 2 for i := 0; i < rf; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) } - for i := rf; i < rf+h.params.rP; i++ { + for i := rf; i < rf+h.params.NbPartialRounds; i++ { // one round = matMulInternal(sBox_sparse(addRoundKey)) h.addRoundKeyInPlace(i, input) h.sBox(0, input) h.matMulInternalInPlace(input) } - for i := rf + h.params.rP; i < h.params.rF+h.params.rP; i++ { + for i := rf + h.params.NbPartialRounds; i < h.params.NbFullRounds+h.params.NbPartialRounds; i++ { // one round = matMulExternal(sBox_Full(addRoundKey)) h.addRoundKeyInPlace(i, input) - for j := 0; j < h.params.t; j++ { + for j := 0; j < h.params.Width; j++ { h.sBox(j, input) } h.matMulExternalInPlace(input) @@ -250,3 +309,25 @@ func (h *Hash) Permutation(input []fr.Element) error { return nil } + +// Compress applies the permutation on left and right and returns the right lane +// of the result. Panics if the permutation instance is not initialized with a +// width of 2. +func (h *Permutation) Compress(left []byte, right []byte) ([]byte, error) { + if h.params.Width != 2 { + return nil, errors.New("need a 2-1 function") + } + var x [2]fr.Element + + if err := x[0].SetBytesCanonical(left); err != nil { + return nil, err + } + if err := x[1].SetBytesCanonical(right); err != nil { + return nil, err + } + if err := h.Permutation(x[:]); err != nil { + return nil, err + } + res := x[1].Bytes() + return res[:], nil +} diff --git a/internal/generator/crypto/hash/poseidon2/template/poseidon2.test.go.tmpl b/internal/generator/crypto/hash/poseidon2/template/poseidon2.test.go.tmpl index 809fc11c67..601aef3988 100644 --- a/internal/generator/crypto/hash/poseidon2/template/poseidon2.test.go.tmpl +++ b/internal/generator/crypto/hash/poseidon2/template/poseidon2.test.go.tmpl @@ -5,6 +5,8 @@ import ( ) func TestExternalMatrix(t *testing.T) { + t.Skip("skipping test - it is initialized for width=4 for which we don't have the diagonal matrix") + var expected [4][4]fr.Element expected[0][0].SetUint64(5) @@ -27,7 +29,7 @@ func TestExternalMatrix(t *testing.T) { expected[3][2].SetUint64(7) expected[3][3].SetUint64(6) - h := NewHash(4, 8, 56, "seed") + h := NewPermutation(4, 8, 56) var tmp [4]fr.Element for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { @@ -48,7 +50,7 @@ func TestExternalMatrix(t *testing.T) { } func BenchmarkPoseidon2(b *testing.B) { - h := NewHash(3, 8, 56, "seed") + h := NewPermutation(3, 8, 56) var tmp [3]fr.Element tmp[0].SetRandom() tmp[1].SetRandom()