Skip to content

Commit b962c16

Browse files
Copilotjulienrbrt
andcommitted
Add recovery history retention pruning
Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com>
1 parent f4f90cd commit b962c16

File tree

14 files changed

+136
-4
lines changed

14 files changed

+136
-4
lines changed

apps/evm/cmd/rollback.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func NewRollbackCmd() *cobra.Command {
7373
if err != nil {
7474
cmd.Printf("Warning: failed to create engine client, skipping EL rollback: %v\n", err)
7575
} else {
76+
engineClient.SetExecMetaRetention(nodeConfig.Node.StateHistoryRetention)
7677
if err := engineClient.Rollback(goCtx, height); err != nil {
7778
return fmt.Errorf("failed to rollback execution layer: %w", err)
7879
}

apps/evm/cmd/run.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ var RunCmd = &cobra.Command{
6969

7070
// Attach logger to the EVM engine client if available
7171
if ec, ok := executor.(*evm.EngineClient); ok {
72+
ec.SetExecMetaRetention(nodeConfig.Node.StateHistoryRetention)
7273
ec.SetLogger(logger.With().Str("module", "engine_client").Logger())
7374
}
7475

apps/evm/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,7 @@ replace (
222222
google.golang.org/genproto/googleapis/api => google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9
223223
google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9
224224
)
225+
226+
replace github.com/evstack/ev-node => ../../
227+
228+
replace github.com/evstack/ev-node/execution/evm => ../../execution/evm

execution/evm/execution.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,15 @@ func (c *EngineClient) SetLogger(l zerolog.Logger) {
270270
c.logger = l
271271
}
272272

273+
// SetExecMetaRetention configures how many recent execution metadata entries are retained.
274+
// A value of 0 keeps all entries.
275+
func (c *EngineClient) SetExecMetaRetention(limit uint64) {
276+
if c.store == nil {
277+
return
278+
}
279+
c.store.SetExecMetaRetention(limit)
280+
}
281+
273282
// InitChain initializes the blockchain with the given genesis parameters
274283
func (c *EngineClient) InitChain(ctx context.Context, genesisTime time.Time, initialHeight uint64, chainID string) ([]byte, error) {
275284
if initialHeight != 1 {

execution/evm/store.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,21 @@ func (em *ExecMeta) FromProto(other *pb.ExecMeta) error {
8585
// EVMStore wraps a ds.Batching datastore with a prefix for EVM execution data.
8686
// This keeps EVM-specific data isolated from other ev-node data.
8787
type EVMStore struct {
88-
db ds.Batching
88+
db ds.Batching
89+
execMetaRetention uint64
8990
}
9091

9192
// NewEVMStore creates a new EVMStore wrapping the given datastore.
9293
func NewEVMStore(db ds.Batching) *EVMStore {
9394
return &EVMStore{db: db}
9495
}
9596

97+
// SetExecMetaRetention sets the number of recent exec meta entries to keep.
98+
// A value of 0 keeps all exec meta history.
99+
func (s *EVMStore) SetExecMetaRetention(limit uint64) {
100+
s.execMetaRetention = limit
101+
}
102+
96103
// execMetaKey returns the datastore key for ExecMeta at a given height.
97104
func execMetaKey(height uint64) ds.Key {
98105
heightBytes := make([]byte, 8)
@@ -137,6 +144,13 @@ func (s *EVMStore) SaveExecMeta(ctx context.Context, meta *ExecMeta) error {
137144
return fmt.Errorf("failed to save exec meta: %w", err)
138145
}
139146

147+
if s.execMetaRetention > 0 && meta.Height > s.execMetaRetention {
148+
pruneHeight := meta.Height - s.execMetaRetention
149+
if err := s.db.Delete(ctx, execMetaKey(pruneHeight)); err != nil && !errors.Is(err, ds.ErrNotFound) {
150+
return fmt.Errorf("failed to prune exec meta at height %d: %w", pruneHeight, err)
151+
}
152+
}
153+
140154
return nil
141155
}
142156

execution/evm/store_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package evm
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
ds "github.com/ipfs/go-datastore"
8+
dssync "github.com/ipfs/go-datastore/sync"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestSaveExecMetaPrunesHistory(t *testing.T) {
13+
t.Parallel()
14+
15+
store := NewEVMStore(dssync.MutexWrap(ds.NewMapDatastore()))
16+
store.SetExecMetaRetention(2)
17+
18+
ctx := context.Background()
19+
for height := uint64(1); height <= 3; height++ {
20+
require.NoError(t, store.SaveExecMeta(ctx, &ExecMeta{
21+
Height: height,
22+
Stage: ExecStageStarted,
23+
}))
24+
}
25+
26+
meta, err := store.GetExecMeta(ctx, 1)
27+
require.NoError(t, err)
28+
require.Nil(t, meta)
29+
30+
meta, err = store.GetExecMeta(ctx, 2)
31+
require.NoError(t, err)
32+
require.NotNil(t, meta)
33+
require.Equal(t, uint64(2), meta.Height)
34+
35+
meta, err = store.GetExecMeta(ctx, 3)
36+
require.NoError(t, err)
37+
require.NotNil(t, meta)
38+
require.Equal(t, uint64(3), meta.Height)
39+
}

node/full.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ func newFullNode(
8383

8484
mainKV := store.NewEvNodeKVStore(database)
8585
baseStore := store.New(mainKV)
86+
if defaultStore, ok := baseStore.(*store.DefaultStore); ok {
87+
defaultStore.SetStateHistoryRetention(nodeConfig.Node.StateHistoryRetention)
88+
}
8689

8790
// Wrap with cached store for LRU caching of headers and block data
8891
cachedStore, err := store.NewCachedStore(baseStore)

node/light.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ func newLightNode(
5050

5151
componentLogger := logger.With().Str("component", "HeaderSyncService").Logger()
5252
baseStore := store.New(database)
53+
if defaultStore, ok := baseStore.(*store.DefaultStore); ok {
54+
defaultStore.SetStateHistoryRetention(conf.Node.StateHistoryRetention)
55+
}
5356

5457
// Wrap with cached store for LRU caching of headers
5558
cachedStore, err := store.NewCachedStore(baseStore)

pkg/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const (
5151
FlagReadinessMaxBlocksBehind = FlagPrefixEvnode + "node.readiness_max_blocks_behind"
5252
// FlagScrapeInterval is a flag for specifying the reaper scrape interval
5353
FlagScrapeInterval = FlagPrefixEvnode + "node.scrape_interval"
54+
// FlagStateHistoryRetention is a flag for specifying how much state/exec metadata history to keep
55+
FlagStateHistoryRetention = FlagPrefixEvnode + "node.state_history_retention"
5456
// FlagClearCache is a flag for clearing the cache
5557
FlagClearCache = FlagPrefixEvnode + "clear_cache"
5658

@@ -257,6 +259,7 @@ type NodeConfig struct {
257259
LazyMode bool `mapstructure:"lazy_mode" yaml:"lazy_mode" comment:"Enables lazy aggregation mode, where blocks are only produced when transactions are available or after LazyBlockTime. Optimizes resources by avoiding empty block creation during periods of inactivity."`
258260
LazyBlockInterval DurationWrapper `mapstructure:"lazy_block_interval" yaml:"lazy_block_interval" comment:"Maximum interval between blocks in lazy aggregation mode (LazyAggregator). Ensures blocks are produced periodically even without transactions to keep the chain active. Generally larger than BlockTime."`
259261
ScrapeInterval DurationWrapper `mapstructure:"scrape_interval" yaml:"scrape_interval" comment:"Interval at which the reaper polls the execution layer for new transactions. Lower values reduce transaction detection latency but increase RPC load. Examples: \"250ms\", \"500ms\", \"1s\"."`
262+
StateHistoryRetention uint64 `mapstructure:"state_history_retention" yaml:"state_history_retention" comment:"Number of recent heights to keep state and execution metadata for recovery (0 keeps all)."`
260263

261264
// Readiness / health configuration
262265
ReadinessWindowSeconds uint64 `mapstructure:"readiness_window_seconds" yaml:"readiness_window_seconds" comment:"Time window in seconds used to calculate ReadinessMaxBlocksBehind based on block time. Default: 15 seconds."`
@@ -436,6 +439,7 @@ func AddFlags(cmd *cobra.Command) {
436439
cmd.Flags().Uint64(FlagReadinessWindowSeconds, def.Node.ReadinessWindowSeconds, "time window in seconds for calculating readiness threshold based on block time (default: 15s)")
437440
cmd.Flags().Uint64(FlagReadinessMaxBlocksBehind, def.Node.ReadinessMaxBlocksBehind, "how many blocks behind best-known head the node can be and still be considered ready (0 = must be at head)")
438441
cmd.Flags().Duration(FlagScrapeInterval, def.Node.ScrapeInterval.Duration, "interval at which the reaper polls the execution layer for new transactions")
442+
cmd.Flags().Uint64(FlagStateHistoryRetention, def.Node.StateHistoryRetention, "number of recent heights to keep state and execution metadata for recovery (0 keeps all)")
439443

440444
// Data Availability configuration flags
441445
cmd.Flags().String(FlagDAAddress, def.DA.Address, "DA address (host:port)")

pkg/config/config_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func TestDefaultConfig(t *testing.T) {
3333
assert.Equal(t, uint64(0), def.Node.MaxPendingHeadersAndData)
3434
assert.Equal(t, false, def.Node.LazyMode)
3535
assert.Equal(t, 60*time.Second, def.Node.LazyBlockInterval.Duration)
36+
assert.Equal(t, uint64(5000), def.Node.StateHistoryRetention)
3637
assert.Equal(t, "file", def.Signer.SignerType)
3738
assert.Equal(t, "config", def.Signer.SignerPath)
3839
assert.Equal(t, "127.0.0.1:7331", def.RPC.Address)
@@ -64,6 +65,7 @@ func TestAddFlags(t *testing.T) {
6465
assertFlagValue(t, flags, FlagReadinessWindowSeconds, DefaultConfig().Node.ReadinessWindowSeconds)
6566
assertFlagValue(t, flags, FlagReadinessMaxBlocksBehind, DefaultConfig().Node.ReadinessMaxBlocksBehind)
6667
assertFlagValue(t, flags, FlagScrapeInterval, DefaultConfig().Node.ScrapeInterval)
68+
assertFlagValue(t, flags, FlagStateHistoryRetention, DefaultConfig().Node.StateHistoryRetention)
6769

6870
// DA flags
6971
assertFlagValue(t, flags, FlagDAAddress, DefaultConfig().DA.Address)
@@ -112,7 +114,7 @@ func TestAddFlags(t *testing.T) {
112114
assertFlagValue(t, flags, FlagRPCEnableDAVisualization, DefaultConfig().RPC.EnableDAVisualization)
113115

114116
// Count the number of flags we're explicitly checking
115-
expectedFlagCount := 63 // Update this number if you add more flag checks above
117+
expectedFlagCount := 64 // Update this number if you add more flag checks above
116118

117119
// Get the actual number of flags (both regular and persistent)
118120
actualFlagCount := 0

0 commit comments

Comments
 (0)