From 5e3c6705f320c97d42cd1da6573a49aac7804a63 Mon Sep 17 00:00:00 2001 From: Tabaie Date: Mon, 27 Oct 2025 12:44:29 -0500 Subject: [PATCH 1/7] 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/7] 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/7] 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 From ae54e28e37cbe1300a13a92caf8731ea50faef91 Mon Sep 17 00:00:00 2001 From: Tabaie Date: Tue, 13 Jan 2026 14:18:05 -0600 Subject: [PATCH 4/7] feat: SumWithLength --- std/hash/hash.go | 65 +++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/std/hash/hash.go b/std/hash/hash.go index 35e9c054f2..64f03e0193 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -109,10 +109,11 @@ type Compressor interface { } type merkleDamgardHasher struct { - state frontend.Variable - iv frontend.Variable - f Compressor - api frontend.API + state []frontend.Variable // state after being updated with each written element + stateTable logderivlookup.Table // stateTable always contains a prefix of h.state + stateTableLen int + f Compressor + api frontend.API } // NewMerkleDamgardHasher range-extends a 2-1 one-way hash compression function into a hash by way of the Merkle-Damgård construction. @@ -122,59 +123,51 @@ type merkleDamgardHasher struct { // - 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) StateStorer { return &merkleDamgardHasher{ - state: initialState, - iv: initialState, + state: []frontend.Variable{initialState}, f: f, api: api, } } func (h *merkleDamgardHasher) Reset() { - h.state = h.iv + h.state = h.state[:1] + h.stateTableLen = 0 + h.stateTable = nil } func (h *merkleDamgardHasher) Write(data ...frontend.Variable) { for _, d := range data { - h.state = h.f.Compress(h.state, d) + h.state = append(h.state, h.f.Compress(h.state[len(h.state)-1], d)) } } func (h *merkleDamgardHasher) Sum() frontend.Variable { - return h.state + return h.state[len(h.state)-1] +} + +// SumWithLength computes the Merkle-Damgård hash of the input data, truncated at the given length. +// Parameters: +// - 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. +func (h *merkleDamgardHasher) SumWithLength(length frontend.Variable) frontend.Variable { + if h.stateTable == nil { + h.stateTable = logderivlookup.New(h.api) + } + for h.stateTableLen < len(h.state) { + h.stateTable.Insert(h.state[h.stateTableLen]) + h.stateTableLen++ + } + return h.stateTable.Lookup(length)[0] } func (h *merkleDamgardHasher) State() []frontend.Variable { - return []frontend.Variable{h.state} + return []frontend.Variable{h.state[len(h.state)-1]} } func (h *merkleDamgardHasher) SetState(state []frontend.Variable) error { - if h.state != h.iv { - return fmt.Errorf("the hasher is not in an initial state; reset before attempting to set the state") - } if len(state) != 1 { - return fmt.Errorf("expected one state variable, got %d", len(state)) + return fmt.Errorf("the hasher is not in an initial state; reset before attempting to set the state") } - h.state = state[0] + h.state = append(h.state, state[0]) return nil } - -// 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 - - resT.Insert(state) - for _, v := range data { - state = f.Compress(state, v) - resT.Insert(state) - } - - return resT.Lookup(length)[0] -} From fad6bbbba737046d9206288d0802026b179fdbbd Mon Sep 17 00:00:00 2001 From: Tabaie Date: Tue, 13 Jan 2026 14:26:58 -0600 Subject: [PATCH 5/7] fix: test --- std/hash/hash.go | 7 +++++++ std/hash/poseidon2/poseidon2_test.go | 8 +++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/std/hash/hash.go b/std/hash/hash.go index 64f03e0193..3554e38f23 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -27,6 +27,13 @@ type FieldHasher interface { Reset() } +// DynamicLengthFieldHasher can compute hashes of lengths unkown at compile time. +type DynamicLengthFieldHasher interface { + FieldHasher + // SumWithLength computes the hash of the first l inputs written into the hash. + SumWithLength(l frontend.Variable) frontend.Variable +} + // StateStorer allows to store and retrieve the state of a hash function. type StateStorer interface { FieldHasher diff --git a/std/hash/poseidon2/poseidon2_test.go b/std/hash/poseidon2/poseidon2_test.go index d2c0e9c62c..791ff7b1e4 100644 --- a/std/hash/poseidon2/poseidon2_test.go +++ b/std/hash/poseidon2/poseidon2_test.go @@ -11,7 +11,6 @@ import ( "github.com/consensys/gnark/std/hash" "github.com/consensys/gnark/std/hash/poseidon2" gkr_poseidon2 "github.com/consensys/gnark/std/hash/poseidon2/gkr-poseidon2" - permutation "github.com/consensys/gnark/std/permutation/poseidon2/gkr-poseidon2" "github.com/consensys/gnark/test" ) @@ -28,8 +27,9 @@ func (c *poseidon2Circuit) Define(api frontend.API) error { if err != nil { return err } + varlen := hsh.(hash.DynamicLengthFieldHasher) - compressor, err := permutation.NewCompressor(api) + hsh, err = poseidon2.New(api) if err != nil { return err } @@ -39,12 +39,14 @@ func (c *poseidon2Circuit) Define(api frontend.API) error { return err } + varlen.Write(c.Input...) + for i := range c.Input { hsh.Write(c.Input[i]) api.AssertIsEqual(c.Expected[i], hsh.Sum()) gkr.Write(c.Input[i]) api.AssertIsEqual(c.Expected[i], gkr.Sum()) - api.AssertIsEqual(c.Expected[i], hash.SumMerkleDamgardDynamicLength(api, compressor, 0, i+1, c.Input)) + api.AssertIsEqual(c.Expected[i], varlen.SumWithLength(i+1)) } return nil } From e899bb54883a6187774bf45d1982ecd27172ebad Mon Sep 17 00:00:00 2001 From: Tabaie Date: Tue, 13 Jan 2026 14:35:25 -0600 Subject: [PATCH 6/7] fix: unknown --- std/hash/hash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/std/hash/hash.go b/std/hash/hash.go index 3554e38f23..c86cdbeee6 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -27,7 +27,7 @@ type FieldHasher interface { Reset() } -// DynamicLengthFieldHasher can compute hashes of lengths unkown at compile time. +// DynamicLengthFieldHasher can compute hashes of lengths unknown at compile time. type DynamicLengthFieldHasher interface { FieldHasher // SumWithLength computes the hash of the first l inputs written into the hash. From f58211dd6e95861c1429cdfe95e5b3719af1a99d Mon Sep 17 00:00:00 2001 From: Tabaie Date: Tue, 13 Jan 2026 14:38:09 -0600 Subject: [PATCH 7/7] revert: add state length check back --- std/hash/hash.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/std/hash/hash.go b/std/hash/hash.go index c86cdbeee6..cd27fbed95 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -173,6 +173,9 @@ func (h *merkleDamgardHasher) State() []frontend.Variable { func (h *merkleDamgardHasher) SetState(state []frontend.Variable) error { if len(state) != 1 { + return fmt.Errorf("expected one state variable, got %d", len(state)) + } + if len(h.state) != 1 { return fmt.Errorf("the hasher is not in an initial state; reset before attempting to set the state") } h.state = append(h.state, state[0])