Skip to content
1 change: 1 addition & 0 deletions apps/evm/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.0

replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/core => ../../core
github.com/evstack/ev-node/execution/evm => ../../execution/evm
)

Expand Down
2 changes: 0 additions & 2 deletions apps/evm/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,6 @@ github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9i
github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
Expand Down
1 change: 1 addition & 0 deletions apps/grpc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.0

replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/core => ../../core
github.com/evstack/ev-node/execution/grpc => ../../execution/grpc
)

Expand Down
2 changes: 0 additions & 2 deletions apps/grpc/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
Expand Down
5 changes: 5 additions & 0 deletions apps/testapp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ module github.com/evstack/ev-node/apps/testapp

go 1.25.0

replace (
github.com/evstack/ev-node => ../../.
github.com/evstack/ev-node/core => ../../core
)

require (
github.com/evstack/ev-node v1.0.0-rc.1
github.com/evstack/ev-node/core v1.0.0-rc.1
Expand Down
4 changes: 0 additions & 4 deletions apps/testapp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node v1.0.0-rc.1 h1:MO7DT3y1X4WK7pTgl/867NroqhXJ/oe2NbmvMr3jqq8=
github.com/evstack/ev-node v1.0.0-rc.1/go.mod h1:JtbvY2r6k6ZhGYMeDNZk7cx6ALj3d0f6dVyyJmJHBd4=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
Expand Down
47 changes: 47 additions & 0 deletions block/internal/executing/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package executing
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -546,6 +547,52 @@ func (e *Executor) ProduceBlock(ctx context.Context) error {
// Update in-memory state after successful commit
e.setLastState(newState)

// Run height-based pruning of stored block data if enabled. This is a
// best-effort background maintenance step and should not cause block
// production to fail, but it does run in the critical path and may add
// some latency when large ranges are pruned.
if e.config.Node.PruningEnabled && e.config.Node.PruningKeepRecent > 0 && e.config.Node.PruningInterval > 0 {
interval := e.config.Node.PruningInterval
// Only attempt pruning when we're exactly at an interval boundary.
if newHeight%interval == 0 && newHeight > e.config.Node.PruningKeepRecent {
targetHeight := newHeight - e.config.Node.PruningKeepRecent

// Determine the DA-included floor for pruning, so we never prune
// beyond what has been confirmed in DA.
var daIncludedHeight uint64
meta, err := e.store.GetMetadata(e.ctx, store.DAIncludedHeightKey)
if err == nil && len(meta) == 8 {
daIncludedHeight = binary.LittleEndian.Uint64(meta)
}

// If nothing is known to be DA-included yet, skip pruning.
if daIncludedHeight == 0 {
// Nothing known to be DA-included yet; skip pruning.
} else {
if targetHeight > daIncludedHeight {
targetHeight = daIncludedHeight
}

if targetHeight > 0 {
if err := e.store.PruneBlocks(e.ctx, targetHeight); err != nil {
return fmt.Errorf("failed to prune old block data: %w", err)
}

// If the execution client exposes execution-metadata pruning,
// prune ExecMeta using the same target height. This keeps
// execution-layer metadata aligned with
// ev-node's block store pruning while remaining a no-op for
// execution environments that don't implement ExecMetaPruner yet.
if pruner, ok := e.exec.(coreexecutor.ExecMetaPruner); ok {
if err := pruner.PruneExecMeta(e.ctx, targetHeight); err != nil {
return fmt.Errorf("failed to prune execution metadata: %w", err)
}
}
}
}
}
}

// broadcast header and data to P2P network
g, broadcastCtx := errgroup.WithContext(ctx)
g.Go(func() error { return e.headerBroadcaster.WriteToStoreAndBroadcast(broadcastCtx, header) })
Expand Down
13 changes: 13 additions & 0 deletions core/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,16 @@ type Rollbackable interface {
// Rollback resets the execution layer head to the specified height.
Rollback(ctx context.Context, targetHeight uint64) error
}

// ExecMetaPruner is an optional interface that execution clients can implement
// to support height-based pruning of their execution metadata. This is used by
// EVM-based execution clients to keep ExecMeta consistent with ev-node's
// pruning window while remaining a no-op for execution environments that
// don't persist per-height metadata in ev-node's datastore.
type ExecMetaPruner interface {
// PruneExecMeta should delete execution metadata for all heights up to and
// including the given height. Implementations should be idempotent and track
// their own progress so that repeated calls with the same or decreasing
// heights are cheap no-ops.
PruneExecMeta(ctx context.Context, height uint64) error
}
17 changes: 17 additions & 0 deletions execution/evm/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ var _ execution.HeightProvider = (*EngineClient)(nil)
// Ensure EngineClient implements the execution.Rollbackable interface
var _ execution.Rollbackable = (*EngineClient)(nil)

// Ensure EngineClient implements optional pruning interface when used with
// ev-node's height-based pruning. This enables coordinated pruning of EVM
// ExecMeta alongside ev-node's own block data pruning, while remaining a
// no-op for non-EVM execution environments.
var _ execution.ExecMetaPruner = (*EngineClient)(nil)

// validatePayloadStatus checks the payload status and returns appropriate errors.
// It implements the Engine API specification's status handling:
// - VALID: Operation succeeded, return nil
Expand Down Expand Up @@ -265,6 +271,17 @@ func NewEngineExecutionClient(
}, nil
}

// PruneExecMeta implements execution.ExecMetaPruner by delegating to the
// underlying EVMStore. It is safe to call this multiple times with the same
// or increasing heights; the store tracks its own last-pruned height.
func (c *EngineClient) PruneExecMeta(ctx context.Context, height uint64) error {
if c.store == nil {
return nil
}

return c.store.PruneExecMeta(ctx, height)
}

// SetLogger allows callers to attach a structured logger.
func (c *EngineClient) SetLogger(l zerolog.Logger) {
c.logger = l
Expand Down
5 changes: 5 additions & 0 deletions execution/evm/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,8 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)

replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/core => ../../core
)
4 changes: 0 additions & 4 deletions execution/evm/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9i
github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/evstack/ev-node v1.0.0-rc.1 h1:MO7DT3y1X4WK7pTgl/867NroqhXJ/oe2NbmvMr3jqq8=
github.com/evstack/ev-node v1.0.0-rc.1/go.mod h1:JtbvY2r6k6ZhGYMeDNZk7cx6ALj3d0f6dVyyJmJHBd4=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
Expand Down
53 changes: 53 additions & 0 deletions execution/evm/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import (
// Store prefix for execution/evm data - keeps it isolated from other ev-node data
const evmStorePrefix = "evm/"

// lastPrunedExecMetaKey is the datastore key used to track the highest
// execution height for which ExecMeta has been pruned. All ExecMeta entries
// for heights <= this value are considered pruned.
const lastPrunedExecMetaKey = evmStorePrefix + "last-pruned-execmeta-height"

// ExecMeta stages
const (
ExecStageStarted = "started"
Expand Down Expand Up @@ -140,6 +145,54 @@ func (s *EVMStore) SaveExecMeta(ctx context.Context, meta *ExecMeta) error {
return nil
}

// PruneExecMeta removes ExecMeta entries up to and including the given height.
// It is safe to call this multiple times with the same or increasing heights;
// previously pruned ranges will be skipped based on the last-pruned marker.
func (s *EVMStore) PruneExecMeta(ctx context.Context, height uint64) error {
// Load last pruned height, if any.
var lastPruned uint64
data, err := s.db.Get(ctx, ds.NewKey(lastPrunedExecMetaKey))
if err != nil {
if !errors.Is(err, ds.ErrNotFound) {
return fmt.Errorf("failed to get last pruned execmeta height: %w", err)
}
} else if len(data) == 8 {
lastPruned = binary.BigEndian.Uint64(data)
}

// Nothing new to prune.
if height <= lastPruned {
return nil
}

batch, err := s.db.Batch(ctx)
if err != nil {
return fmt.Errorf("failed to create batch for execmeta pruning: %w", err)
}

for h := lastPruned + 1; h <= height; h++ {
key := execMetaKey(h)
if err := batch.Delete(ctx, key); err != nil {
if !errors.Is(err, ds.ErrNotFound) {
return fmt.Errorf("failed to delete exec meta at height %d: %w", h, err)
}
}
}

// Persist updated last pruned height.
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, height)
if err := batch.Put(ctx, ds.NewKey(lastPrunedExecMetaKey), buf); err != nil {
return fmt.Errorf("failed to update last pruned execmeta height: %w", err)
}

if err := batch.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit execmeta pruning batch: %w", err)
}

return nil
}

// Sync ensures all pending writes are flushed to disk.
func (s *EVMStore) Sync(ctx context.Context) error {
return s.db.Sync(ctx, ds.NewKey(evmStorePrefix))
Expand Down
99 changes: 99 additions & 0 deletions execution/evm/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package evm

import (
"context"
"encoding/binary"
"testing"

ds "github.com/ipfs/go-datastore"
dssync "github.com/ipfs/go-datastore/sync"
"github.com/stretchr/testify/require"
)

// newTestDatastore creates an in-memory datastore for testing.
func newTestDatastore(t *testing.T) ds.Batching {
t.Helper()
// Wrap the in-memory MapDatastore to satisfy the Batching interface.
return dssync.MutexWrap(ds.NewMapDatastore())
}

func TestPruneExecMeta_PrunesUpToTargetHeight(t *testing.T) {
t.Parallel()

ctx := context.Background()
db := newTestDatastore(t)
store := NewEVMStore(db)

// Seed ExecMeta entries at heights 1..5
for h := uint64(1); h <= 5; h++ {
meta := &ExecMeta{Height: h}
require.NoError(t, store.SaveExecMeta(ctx, meta))
}

// Sanity: all heights should be present
for h := uint64(1); h <= 5; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.NotNil(t, meta)
require.Equal(t, h, meta.Height)
}

// Prune up to height 3
require.NoError(t, store.PruneExecMeta(ctx, 3))

// Heights 1..3 should be gone
for h := uint64(1); h <= 3; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.Nil(t, meta)
}

// Heights 4..5 should remain
for h := uint64(4); h <= 5; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.NotNil(t, meta)
}

// Re-pruning with the same height should be a no-op
require.NoError(t, store.PruneExecMeta(ctx, 3))
}

func TestPruneExecMeta_TracksLastPrunedHeight(t *testing.T) {
t.Parallel()

ctx := context.Background()
db := newTestDatastore(t)
store := NewEVMStore(db)

// Seed ExecMeta entries at heights 1..5
for h := uint64(1); h <= 5; h++ {
meta := &ExecMeta{Height: h}
require.NoError(t, store.SaveExecMeta(ctx, meta))
}

// First prune up to 2
require.NoError(t, store.PruneExecMeta(ctx, 2))

// Then prune up to 4; heights 3..4 should be deleted in this run
require.NoError(t, store.PruneExecMeta(ctx, 4))

// Verify all heights 1..4 are gone, 5 remains
for h := uint64(1); h <= 4; h++ {
meta, err := store.GetExecMeta(ctx, h)
require.NoError(t, err)
require.Nil(t, meta)
}

meta, err := store.GetExecMeta(ctx, 5)
require.NoError(t, err)
require.NotNil(t, meta)
require.Equal(t, uint64(5), meta.Height)

// Ensure last-pruned marker is set to 4
raw, err := db.Get(ctx, ds.NewKey(lastPrunedExecMetaKey))
require.NoError(t, err)
require.Len(t, raw, 8)
last := binary.BigEndian.Uint64(raw)
require.Equal(t, uint64(4), last)
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,6 @@ replace (
google.golang.org/genproto/googleapis/api => google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9
google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9
)

// use local core module during development/CI
replace github.com/evstack/ev-node/core => ./core
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
Expand Down
Loading
Loading