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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions block/internal/executing/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"sync/atomic"
"time"
"unsafe"

"github.com/ipfs/go-datastore"
"github.com/libp2p/go-libp2p/core/crypto"
Expand Down Expand Up @@ -792,14 +793,12 @@ func (e *Executor) CreateBlock(ctx context.Context, height uint64, batchData *Ba
func (e *Executor) ApplyBlock(ctx context.Context, header types.Header, data *types.Data) (types.State, error) {
currentState := e.getLastState()

// Convert Txs to [][]byte for the execution client.
// types.Tx is []byte, so this is a type conversion, not a copy.
// Reinterpret Txs ([][]byte via type aliases) as [][]byte without allocation.
// types.Tx = []byte, so types.Txs = []Tx has identical memory layout to [][]byte.
// Using unsafe.Slice/unsafe.SliceData avoids the heap allocation of make([][]byte, n).
var rawTxs [][]byte
if n := len(data.Txs); n > 0 {
rawTxs = make([][]byte, n)
for i, tx := range data.Txs {
rawTxs[i] = []byte(tx)
}
rawTxs = unsafe.Slice((*[]byte)(unsafe.SliceData(data.Txs)), n)
}

// Execute transactions
Expand Down
13 changes: 4 additions & 9 deletions pkg/store/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"

ds "github.com/ipfs/go-datastore"
"google.golang.org/protobuf/proto"

"github.com/evstack/ev-node/types"
)
Expand Down Expand Up @@ -84,18 +83,14 @@ func (b *DefaultBatch) SaveBlockDataFromBytes(header *types.SignedHeader, header
return nil
}

// UpdateState updates the state in the batch
// UpdateState updates the state in the batch.
// Uses pooled State.MarshalBinary to reduce per-block allocations.
func (b *DefaultBatch) UpdateState(state types.State) error {
// Save the state at the height specified in the state itself
height := state.LastBlockHeight

pbState, err := state.ToProto()
data, err := state.MarshalBinary()
if err != nil {
return fmt.Errorf("failed to convert type state to protobuf type: %w", err)
}
data, err := proto.Marshal(pbState)
if err != nil {
return fmt.Errorf("failed to marshal state to protobuf: %w", err)
return fmt.Errorf("failed to marshal state: %w", err)
}

return b.batch.Put(b.ctx, ds.RawKey(getStateAtHeightKey(height)), data)
Expand Down
42 changes: 42 additions & 0 deletions types/hash_memo_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package types

import (
"testing"
)

// BenchmarkHeaderHash_NoMemo measures the cost of the old 3× call pattern with no
// memoization: each call re-marshals every field via ToProto → proto.Marshal → sha256.
func BenchmarkHeaderHash_NoMemo(b *testing.B) {
h := GetRandomHeader("bench-chain", GetRandomBytes(32))
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = h.Hash()
_ = h.Hash()
_ = h.Hash()
}
}

// BenchmarkHeaderHash_Memoized measures the cost of the same 3× call pattern after
// explicit memoization: first call pays full cost, subsequent two are cache hits.
func BenchmarkHeaderHash_Memoized(b *testing.B) {
h := GetRandomHeader("bench-chain", GetRandomBytes(32))
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
h.InvalidateHash()
_ = h.MemoizeHash() // compute and store
_ = h.Hash() // cache hit
_ = h.Hash() // cache hit
}
}

// BenchmarkHeaderHash_Single is a baseline: cost of one Hash() call with a cold cache.
func BenchmarkHeaderHash_Single(b *testing.B) {
h := GetRandomHeader("bench-chain", GetRandomBytes(32))
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = h.Hash()
}
}
37 changes: 29 additions & 8 deletions types/hashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@ import (
"crypto/sha256"
"errors"
"hash"
"sync"
"unsafe"

"google.golang.org/protobuf/proto"

pb "github.com/evstack/ev-node/types/pb/evnode/v1"
)

var (
leafPrefix = []byte{0}

// sha256Pool reuses sha256 Hash instances to avoid per-block allocation.
// sha256.New() allocates ~213 bytes (216B on 64-bit) per call. Pooling
// eliminates this allocation entirely in the hot path.
sha256Pool = sync.Pool{
New: func() interface{} {
return sha256.New()
},
}
)

// HashSlim returns the SHA256 hash of the header using the slim (current) binary encoding.
Expand Down Expand Up @@ -105,17 +120,23 @@ func (d *Data) Hash() Hash {
// Ignoring the marshal error for now to satisfy the go-header interface
// Later on the usage of Hash should be replaced with DA commitment
dBytes, _ := d.MarshalBinary()
return leafHashOpt(sha256.New(), dBytes)
s := sha256Pool.Get().(hash.Hash)
defer sha256Pool.Put(s)
return leafHashOpt(s, dBytes)
}

// DACommitment returns the DA commitment of the Data excluding the Metadata
// DACommitment returns the DA commitment of the Data excluding the Metadata.
// Avoids allocating a pruned Data struct and the [][]byte intermediate slice
// by serializing only the txs field directly to a protobuf message.
func (d *Data) DACommitment() Hash {
// Prune the Data to only include the Txs
prunedData := &Data{
Txs: d.Txs,
}
dBytes, _ := prunedData.MarshalBinary()
return leafHashOpt(sha256.New(), dBytes)
// pb.Data{Metadata: nil, Txs: ...} produces the same wire format as
// Data{Txs: d.Txs}.MarshalBinary() but without the intermediate Data
// wrapper allocation or the txsToByteSlices [][]byte copy.
pbData := pb.Data{Txs: unsafe.Slice((*[]byte)(unsafe.SliceData(d.Txs)), len(d.Txs))}
dBytes, _ := proto.Marshal(&pbData)
s := sha256Pool.Get().(hash.Hash)
defer sha256Pool.Put(s)
return leafHashOpt(s, dBytes)
}

func leafHashOpt(s hash.Hash, leaf []byte) []byte {
Expand Down
Loading
Loading