From 5e3c6705f320c97d42cd1da6573a49aac7804a63 Mon Sep 17 00:00:00 2001 From: Tabaie Date: Mon, 27 Oct 2025 12:44:29 -0500 Subject: [PATCH 1/3] feat: `SumMerkleDamgardDynamicLength` with tests --- std/hash/hash.go | 15 ++++++++++ std/hash/poseidon2/poseidon2_test.go | 43 ++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/std/hash/hash.go b/std/hash/hash.go index ab1f1ffdeb..9d1216364a 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -6,6 +6,7 @@ package hash import ( "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/lookup/logderivlookup" "github.com/consensys/gnark/std/math/uints" ) @@ -136,3 +137,17 @@ func (h *merkleDamgardHasher) Write(data ...frontend.Variable) { func (h *merkleDamgardHasher) Sum() frontend.Variable { return h.state } + +// SumMerkleDamgardDynamicLength returns the hash of the data slice, truncated at the given length. +func SumMerkleDamgardDynamicLength(api frontend.API, f Compressor, initialState frontend.Variable, length frontend.Variable, data []frontend.Variable) frontend.Variable { + resT := logderivlookup.New(api) + state := initialState + + resT.Insert(state) + for _, v := range data { + state = f.Compress(state, v) + resT.Insert(state) + } + + return resT.Lookup(length)[0] +} diff --git a/std/hash/poseidon2/poseidon2_test.go b/std/hash/poseidon2/poseidon2_test.go index 1ce1d46fef..41a27794d0 100644 --- a/std/hash/poseidon2/poseidon2_test.go +++ b/std/hash/poseidon2/poseidon2_test.go @@ -1,41 +1,68 @@ package poseidon2 import ( + "fmt" "testing" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + gcPoseidon2 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/hash" + "github.com/consensys/gnark/std/permutation/poseidon2" "github.com/consensys/gnark/test" ) type Poseidon2Circuit struct { Input []frontend.Variable - Expected frontend.Variable `gnark:",public"` + Expected []frontend.Variable `gnark:",public"` // Expected[i] = H(Input[:i+1]) } func (c *Poseidon2Circuit) Define(api frontend.API) error { + if len(c.Input) != len(c.Expected) { + return fmt.Errorf("length mismatch") + } hsh, err := NewMerkleDamgardHasher(api) if err != nil { return err } - hsh.Write(c.Input...) - api.AssertIsEqual(hsh.Sum(), c.Expected) + + compressor, err := poseidon2.NewPoseidon2(api) + if err != nil { + return err + } + + for i := range c.Input { + hsh.Write(c.Input[i]) + api.AssertIsEqual(c.Expected[i], hsh.Sum()) + api.AssertIsEqual(c.Expected[i], hash.SumMerkleDamgardDynamicLength(api, compressor, 0, i+1, c.Input)) + } + return nil } func TestPoseidon2Hash(t *testing.T) { assert := test.NewAssert(t) + var buf [fr.Bytes]byte const nbInputs = 5 // prepare expected output - h := poseidon2.NewMerkleDamgardHasher() + h := gcPoseidon2.NewMerkleDamgardHasher() + expected := make([]frontend.Variable, nbInputs) circInput := make([]frontend.Variable, nbInputs) for i := range nbInputs { - _, err := h.Write([]byte{byte(i)}) + buf[fr.Bytes-1] = byte(i) + _, err := h.Write(buf[:]) assert.NoError(err) circInput[i] = i + expected[i] = h.Sum(nil) } - res := h.Sum(nil) - assert.CheckCircuit(&Poseidon2Circuit{Input: make([]frontend.Variable, nbInputs)}, test.WithValidAssignment(&Poseidon2Circuit{Input: circInput, Expected: res}), test.WithCurves(ecc.BLS12_377)) // we have parametrized currently only for BLS12-377 + assert.CheckCircuit( + &Poseidon2Circuit{ + Input: make([]frontend.Variable, nbInputs), + Expected: make([]frontend.Variable, nbInputs), + }, test.WithValidAssignment(&Poseidon2Circuit{ + Input: circInput, + Expected: expected, + }), test.WithCurves(ecc.BLS12_377)) // we have parametrized currently only for BLS12-377 } From 1ae9c914a77296237e3825438311e4bc6e459d99 Mon Sep 17 00:00:00 2001 From: Tabaie Date: Mon, 27 Oct 2025 13:04:19 -0500 Subject: [PATCH 2/3] docs: incorporate copilot suggestions --- std/hash/hash.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/std/hash/hash.go b/std/hash/hash.go index 9d1216364a..46f46e1a60 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -113,8 +113,11 @@ type merkleDamgardHasher struct { api frontend.API } -// NewMerkleDamgardHasher transforms a 2-1 one-way function into a hash -// initialState is a value whose preimage is not known +// NewMerkleDamgardHasher range-extends a 2-1 one-way hash compression function into a hash by way of the Merkle-Damgård construction. +// Parameters: +// - api: constraint builder +// - f: 2-1 hash compression (one-way) function +// - initialState: the initialization vector (IV) in the Merkle-Damgård chain. It must be a value whose preimage is not known. func NewMerkleDamgardHasher(api frontend.API, f Compressor, initialState frontend.Variable) FieldHasher { return &merkleDamgardHasher{ state: initialState, @@ -138,7 +141,14 @@ func (h *merkleDamgardHasher) Sum() frontend.Variable { return h.state } -// SumMerkleDamgardDynamicLength returns the hash of the data slice, truncated at the given length. +// SumMerkleDamgardDynamicLength computes the Merkle-Damgård hash of the input data, truncated at the given length. +// Parameters: +// - api: constraint builder +// - f: 2-1 hash compression (one-way) function +// - initialState: the initialization vector (IV) in the Merkle-Damgård chain. It must be a value whose preimage is not known. +// - length: length of the prefix of data to be hashed. The verifier will not accept a value outside the range {0, 1, ..., len(data)}. +// The gnark prover will refuse to attempt to generate such an unsuccessful proof. +// - data: the values a prefix of which is to be hashed. func SumMerkleDamgardDynamicLength(api frontend.API, f Compressor, initialState frontend.Variable, length frontend.Variable, data []frontend.Variable) frontend.Variable { resT := logderivlookup.New(api) state := initialState From c9dd1c3ae68f18e2da229088833fa624a98518cc Mon Sep 17 00:00:00 2001 From: Tabaie Date: Mon, 27 Oct 2025 14:52:36 -0500 Subject: [PATCH 3/3] style: run goimports --- std/hash/hash.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/std/hash/hash.go b/std/hash/hash.go index 46f46e1a60..63951c37b7 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -115,9 +115,9 @@ type merkleDamgardHasher struct { // NewMerkleDamgardHasher range-extends a 2-1 one-way hash compression function into a hash by way of the Merkle-Damgård construction. // Parameters: -// - api: constraint builder -// - f: 2-1 hash compression (one-way) function -// - initialState: the initialization vector (IV) in the Merkle-Damgård chain. It must be a value whose preimage is not known. +// - api: constraint builder +// - f: 2-1 hash compression (one-way) function +// - initialState: the initialization vector (IV) in the Merkle-Damgård chain. It must be a value whose preimage is not known. func NewMerkleDamgardHasher(api frontend.API, f Compressor, initialState frontend.Variable) FieldHasher { return &merkleDamgardHasher{ state: initialState, @@ -143,12 +143,12 @@ func (h *merkleDamgardHasher) Sum() frontend.Variable { // SumMerkleDamgardDynamicLength computes the Merkle-Damgård hash of the input data, truncated at the given length. // Parameters: -// - api: constraint builder -// - f: 2-1 hash compression (one-way) function -// - initialState: the initialization vector (IV) in the Merkle-Damgård chain. It must be a value whose preimage is not known. -// - length: length of the prefix of data to be hashed. The verifier will not accept a value outside the range {0, 1, ..., len(data)}. -// The gnark prover will refuse to attempt to generate such an unsuccessful proof. -// - data: the values a prefix of which is to be hashed. +// - api: constraint builder +// - f: 2-1 hash compression (one-way) function +// - initialState: the initialization vector (IV) in the Merkle-Damgård chain. It must be a value whose preimage is not known. +// - length: length of the prefix of data to be hashed. The verifier will not accept a value outside the range {0, 1, ..., len(data)}. +// The gnark prover will refuse to attempt to generate such an unsuccessful proof. +// - data: the values a prefix of which is to be hashed. func SumMerkleDamgardDynamicLength(api frontend.API, f Compressor, initialState frontend.Variable, length frontend.Variable, data []frontend.Variable) frontend.Variable { resT := logderivlookup.New(api) state := initialState