Skip to content

Commit 62d2222

Browse files
authored
perf(hash): alloc-free hashes and empty-data cache (#3728)
* perf(hash): alloc-free digest and empty-data cache * perf(hash): stream array call sites * docs(hash): clarify cached permutation input comment * test(crypto): lock in PoseidonDigest alloc count * refactor(class): scope sierra entry-point index to the loop * docs(hash): note FFI speedup * refactor(class): rename h to hasher
1 parent 1216dc1 commit 62d2222

5 files changed

Lines changed: 122 additions & 78 deletions

File tree

core/class.go

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,9 @@ func (c *SierraClass) Version() uint64 {
118118
}
119119

120120
func (c *SierraClass) Hash() (felt.Felt, error) {
121-
externalEntryPointsHash := crypto.PoseidonArray(
122-
flattenSierraEntryPoints(c.EntryPoints.External)...,
123-
)
124-
l1HandlerEntryPointsHash := crypto.PoseidonArray(
125-
flattenSierraEntryPoints(c.EntryPoints.L1Handler)...,
126-
)
127-
constructorHash := crypto.PoseidonArray(
128-
flattenSierraEntryPoints(c.EntryPoints.Constructor)...,
129-
)
121+
externalEntryPointsHash := sierraEntryPointsHash(c.EntryPoints.External)
122+
l1HandlerEntryPointsHash := sierraEntryPointsHash(c.EntryPoints.L1Handler)
123+
constructorHash := sierraEntryPointsHash(c.EntryPoints.Constructor)
130124
return crypto.PoseidonArray(
131125
felt.NewFromBytes[felt.Felt]([]byte("CONTRACT_CLASS_V"+c.SemanticVersion)),
132126
&externalEntryPointsHash,
@@ -216,20 +210,20 @@ var compiledClassV1Prefix = felt.NewFromBytes[felt.Felt]([]byte("COMPILED_CLASS_
216210

217211
// Hash computes the class hash using the specified hash version
218212
func (c *CasmClass) Hash(version CasmHashVersion) felt.Felt {
219-
h := NewCasmHasher(version)
213+
hasher := NewCasmHasher(version)
220214

221215
var bytecodeHash felt.Felt
222216
if len(c.BytecodeSegmentLengths.Children) == 0 {
223-
bytecodeHash = h.HashArray(c.Bytecode...)
217+
bytecodeHash = hasher.HashArray(c.Bytecode...)
224218
} else {
225-
bytecodeHash = SegmentedBytecodeHash(c.Bytecode, c.BytecodeSegmentLengths.Children, h)
219+
bytecodeHash = SegmentedBytecodeHash(c.Bytecode, c.BytecodeSegmentLengths.Children, hasher)
226220
}
227221

228-
externalEntryPointsHash := h.HashArray(flattenCompiledEntryPoints(c.External, h)...)
229-
l1HandlerEntryPointsHash := h.HashArray(flattenCompiledEntryPoints(c.L1Handler, h)...)
230-
constructorHash := h.HashArray(flattenCompiledEntryPoints(c.Constructor, h)...)
222+
externalEntryPointsHash := compiledEntryPointsHash(c.External, hasher)
223+
l1HandlerEntryPointsHash := compiledEntryPointsHash(c.L1Handler, hasher)
224+
constructorHash := compiledEntryPointsHash(c.Constructor, hasher)
231225

232-
return h.HashArray(
226+
return hasher.HashArray(
233227
compiledClassV1Prefix,
234228
&externalEntryPointsHash,
235229
&l1HandlerEntryPointsHash,
@@ -241,13 +235,13 @@ func (c *CasmClass) Hash(version CasmHashVersion) felt.Felt {
241235
func SegmentedBytecodeHash(
242236
bytecode []*felt.Felt,
243237
segmentLengths []SegmentLengths,
244-
h Hasher,
238+
hasher Hasher,
245239
) felt.Felt {
246240
var startingOffset uint64
247241
var digestSegment func(segments []SegmentLengths) (uint64, felt.Felt)
248242
digestSegment = func(segments []SegmentLengths) (uint64, felt.Felt) {
249243
var totalLength uint64
250-
digest := h.NewDigest()
244+
digest := hasher.NewDigest()
251245

252246
for _, segment := range segments {
253247
var curSegmentLength uint64
@@ -256,7 +250,7 @@ func SegmentedBytecodeHash(
256250
if len(segment.Children) == 0 {
257251
curSegmentLength = segment.Length
258252
segmentBytecode := bytecode[startingOffset : startingOffset+segment.Length]
259-
curSegmentHash = h.HashArray(segmentBytecode...)
253+
curSegmentHash = hasher.HashArray(segmentBytecode...)
260254
} else {
261255
curSegmentLength, curSegmentHash = digestSegment(segment.Children)
262256
}
@@ -277,33 +271,40 @@ func SegmentedBytecodeHash(
277271
return hash
278272
}
279273

280-
func flattenSierraEntryPoints(entryPoints []SierraEntryPoint) []*felt.Felt {
281-
result := make([]*felt.Felt, len(entryPoints)*2)
282-
for i, entryPoint := range entryPoints {
283-
// It is important that Selector is first because the order
284-
// influences the class hash.
285-
result[2*i] = entryPoint.Selector
286-
result[2*i+1] = felt.NewFromUint64[felt.Felt](entryPoint.Index)
274+
// Order matters (selector then index): it influences the class hash.
275+
func sierraEntryPointsHash(entryPoints []SierraEntryPoint) felt.Felt {
276+
var digest crypto.PoseidonDigest
277+
for _, ep := range entryPoints {
278+
var index felt.Felt
279+
index.SetUint64(ep.Index)
280+
digest.Update(ep.Selector, &index)
287281
}
288-
return result
289-
}
290-
291-
func flattenCompiledEntryPoints(entryPoints []CasmEntryPoint, h Hasher) []*felt.Felt {
292-
result := make([]*felt.Felt, len(entryPoints)*3)
293-
for i, entryPoint := range entryPoints {
294-
// It is important that Selector is first, then Offset is second because the order
295-
// influences the class hash.
296-
result[3*i] = entryPoint.Selector
297-
result[3*i+1] = felt.NewFromUint64[felt.Felt](entryPoint.Offset)
298-
builtins := make([]*felt.Felt, len(entryPoint.Builtins))
299-
for idx, buil := range entryPoint.Builtins {
300-
builtins[idx] = felt.NewFromBytes[felt.Felt]([]byte(buil))
301-
}
302-
builtinsHash := h.HashArray(builtins...)
303-
result[3*i+2] = &builtinsHash
282+
return digest.Finish()
283+
}
284+
285+
// Order matters (selector, offset, builtins hash): it influences the class hash.
286+
func compiledEntryPointsHash(entryPoints []CasmEntryPoint, hasher Hasher) felt.Felt {
287+
digest := hasher.NewDigest()
288+
// Passing &offset and &builtinsHash to digest.Update (an interface call)
289+
// makes the compiler heap-allocate them. Declaring them once and reusing
290+
// them keeps that to a single allocation instead of one per entry point.
291+
var offset, builtinsHash felt.Felt
292+
for _, ep := range entryPoints {
293+
offset.SetUint64(ep.Offset)
294+
builtinsHash = compiledBuiltinsHash(ep.Builtins, hasher)
295+
digest.Update(ep.Selector, &offset, &builtinsHash)
304296
}
297+
return digest.Finish()
298+
}
305299

306-
return result
300+
func compiledBuiltinsHash(builtins []string, hasher Hasher) felt.Felt {
301+
digest := hasher.NewDigest()
302+
var b felt.Felt
303+
for i := range builtins {
304+
b.SetBytes([]byte(builtins[i]))
305+
digest.Update(&b)
306+
}
307+
return digest.Finish()
307308
}
308309

309310
func VerifyClassHashes(classes map[felt.Felt]ClassDefinition) error {

core/crypto/poseidon_hash.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,30 @@ const (
1212
totalRounds = fullRounds + partialRounds
1313
)
1414

15+
// Caches the {0, 1, 0} input (state for hashing a single zero felt).
16+
// emptyDataOutput is computed in initializePoseidon.
17+
var (
18+
emptyDataInput = [3]felt.Felt{{}, felt.One, {}}
19+
emptyDataOutput [3]felt.Felt
20+
)
21+
1522
// HadesPermutation applies the Hades permutation to state in place.
16-
// The hashing steps are intentionally inlined for performance reasons.
1723
func HadesPermutation(state *[3]felt.Felt) {
18-
initialiseRoundKeys.Do(setRoundKeys)
24+
poseidonInit.Do(initializePoseidon)
25+
if *state == emptyDataInput {
26+
*state = emptyDataOutput
27+
return
28+
}
29+
hadesPermutationRounds(state)
30+
}
1931

32+
// hadesPermutationRounds runs the Hades rounds in place.
33+
// The hashing steps are intentionally inlined for performance reasons.
34+
// The sparse-MDS optimization was measured ~53% slower here (our MDS is mul-free), don't try it.
35+
//
36+
// The only remaining way to speed up Poseidon is FFI, see the discussion:
37+
// https://github.com/NethermindEth/juno/pull/3731
38+
func hadesPermutationRounds(state *[3]felt.Felt) {
2039
var squared, stateSum, triple felt.Felt
2140
for i := range totalRounds {
2241
full := (i < fullRounds/2) || (totalRounds-i <= fullRounds/2)
@@ -80,10 +99,17 @@ func PoseidonArray(elems ...*felt.Felt) felt.Felt {
8099
}
81100

82101
var (
83-
initialiseRoundKeys sync.Once
84-
roundKeys [totalRounds][3]felt.Felt
102+
poseidonInit sync.Once
103+
roundKeys [totalRounds][3]felt.Felt
85104
)
86105

106+
// initializePoseidon loads the round keys and precomputes the empty-data output.
107+
func initializePoseidon() {
108+
setRoundKeys()
109+
emptyDataOutput = emptyDataInput
110+
hadesPermutationRounds(&emptyDataOutput)
111+
}
112+
87113
func setRoundKeys() {
88114
var err error
89115
for round, keysStr := range roundKeysSpec {
@@ -103,28 +129,30 @@ var _ Digest = (*PoseidonDigest)(nil)
103129

104130
type PoseidonDigest struct {
105131
state [3]felt.Felt
106-
lastElem *felt.Felt
132+
lastElem felt.Felt
133+
hasLast bool
107134
}
108135

109136
func (d *PoseidonDigest) Update(elems ...*felt.Felt) Digest {
110137
for idx := range elems {
111-
if d.lastElem == nil {
112-
d.lastElem = new(felt.Felt).Set(elems[idx])
138+
if !d.hasLast {
139+
d.lastElem = *elems[idx]
140+
d.hasLast = true
113141
} else {
114-
d.state[0].Add(&d.state[0], d.lastElem)
142+
d.state[0].Add(&d.state[0], &d.lastElem)
115143
d.state[1].Add(&d.state[1], elems[idx])
116144
HadesPermutation(&d.state)
117-
d.lastElem = nil
145+
d.hasLast = false
118146
}
119147
}
120148
return d
121149
}
122150

123151
func (d *PoseidonDigest) Finish() felt.Felt {
124-
if d.lastElem == nil {
152+
if !d.hasLast {
125153
d.state[0].Add(&d.state[0], &felt.One)
126154
} else {
127-
d.state[0].Add(&d.state[0], d.lastElem)
155+
d.state[0].Add(&d.state[0], &d.lastElem)
128156
d.state[1].Add(&d.state[1], &felt.One)
129157
}
130158
HadesPermutation(&d.state)

core/crypto/poseidon_hash_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,19 @@ func BenchmarkPoseidon(b *testing.B) {
8585
}
8686
benchHashR = f
8787
}
88+
89+
// BenchmarkPoseidonDigest locks in the allocation count of the streaming
90+
// digest path (Update + Finish), which struct/array hashing relies on.
91+
// 40 felts exercises ~20 Hades permutations.
92+
func BenchmarkPoseidonDigest(b *testing.B) {
93+
elems := genRandomFelts(b, 40)
94+
95+
var f felt.Felt
96+
b.ReportAllocs()
97+
for b.Loop() {
98+
var digest crypto.PoseidonDigest
99+
digest.Update(elems...)
100+
f = digest.Finish()
101+
}
102+
benchHashR = f
103+
}

core/receipt.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,18 @@ func (r *TransactionReceipt) hash() felt.Felt {
5454
}
5555

5656
func messagesSentHash(messages []*L2ToL1Message) felt.Felt {
57-
chain := []*felt.Felt{
58-
felt.NewFromUint64[felt.Felt](uint64(len(messages))),
59-
}
57+
var digest crypto.PoseidonDigest
58+
var count, msgTo, payloadSize felt.Felt
59+
count.SetUint64(uint64(len(messages)))
60+
digest.Update(&count)
6061
for _, msg := range messages {
61-
msgTo := felt.FromBytes[felt.Felt](msg.To.Bytes())
62-
payloadSize := felt.FromUint64[felt.Felt](uint64(len(msg.Payload)))
63-
chain = append(chain, msg.From, &msgTo, &payloadSize)
64-
chain = append(chain, msg.Payload...)
62+
msgTo.SetBytes(msg.To.Bytes())
63+
payloadSize.SetUint64(uint64(len(msg.Payload)))
64+
digest.Update(msg.From, &msgTo, &payloadSize)
65+
digest.Update(msg.Payload...)
6566
}
6667

67-
return crypto.PoseidonArray(chain...)
68+
return digest.Finish()
6869
}
6970

7071
func receiptCommitment(receipts []*TransactionReceipt, backend TempTrieBackend) (felt.Felt, error) {

core/transaction.go

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,8 @@ func invokeTransactionHash(i *InvokeTransaction, n *networks.Network) (felt.Felt
480480
calldataHash := crypto.PoseidonArray(i.CallData...)
481481
daMode := felt.FromUint64[felt.Felt](dataAvailabilityMode(i.FeeDAMode, i.NonceDAMode))
482482

483-
hashElems := []*felt.Felt{
483+
var digest crypto.PoseidonDigest
484+
digest.Update(
484485
invokeFelt,
485486
i.Version.AsFelt(),
486487
i.SenderAddress,
@@ -491,19 +492,18 @@ func invokeTransactionHash(i *InvokeTransaction, n *networks.Network) (felt.Felt
491492
&daMode,
492493
&accountDeploymentDataHash,
493494
&calldataHash,
494-
}
495+
)
495496

496497
if len(i.ProofFacts) > 0 {
497-
proofFacts := make([]*felt.Felt, len(i.ProofFacts))
498-
for i, proofFact := range i.ProofFacts {
499-
proofFacts[i] = &proofFact
498+
var proofFactsDigest crypto.PoseidonDigest
499+
for j := range i.ProofFacts {
500+
proofFactsDigest.Update(&i.ProofFacts[j])
500501
}
501-
502-
proofFactsHash := crypto.PoseidonArray(proofFacts...)
503-
hashElems = append(hashElems, &proofFactsHash)
502+
proofFactsHash := proofFactsDigest.Finish()
503+
digest.Update(&proofFactsHash)
504504
}
505505

506-
return crypto.PoseidonArray(hashElems...), nil
506+
return digest.Finish(), nil
507507
default:
508508
return felt.Felt{}, errInvalidTransactionVersion(i, i.Version)
509509
}
@@ -513,19 +513,17 @@ func tipAndResourcesHash(tip uint64, resourceBounds map[Resource]ResourceBounds)
513513
l1Bounds := felt.FromBytes[felt.Felt](resourceBounds[ResourceL1Gas].Bytes(ResourceL1Gas))
514514
l2Bounds := felt.FromBytes[felt.Felt](resourceBounds[ResourceL2Gas].Bytes(ResourceL2Gas))
515515
tipFelt := felt.FromUint64[felt.Felt](tip)
516-
elems := []*felt.Felt{
517-
&tipFelt,
518-
&l1Bounds,
519-
&l2Bounds,
520-
}
516+
517+
var digest crypto.PoseidonDigest
518+
digest.Update(&tipFelt, &l1Bounds, &l2Bounds)
521519

522520
// l1_data_gas resource bounds were added in 0.13.4
523521
if bounds, ok := resourceBounds[ResourceL1DataGas]; ok && bounds.MaxPricePerUnit != nil {
524522
l1DataBounds := felt.FromBytes[felt.Felt](bounds.Bytes(ResourceL1DataGas))
525-
elems = append(elems, &l1DataBounds)
523+
digest.Update(&l1DataBounds)
526524
}
527525

528-
return crypto.PoseidonArray(elems...)
526+
return digest.Finish()
529527
}
530528

531529
func dataAvailabilityMode(feeDAMode, nonceDAMode DataAvailabilityMode) uint64 {

0 commit comments

Comments
 (0)