From 5509d38f00016df16fe07244ea6a147a73f3a9f4 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:32:55 +0100 Subject: [PATCH 01/28] basic prunning --- block/internal/executing/executor.go | 17 ++++++ pkg/config/config.go | 11 ++++ pkg/config/defaults.go | 3 ++ pkg/store/keys.go | 6 +++ pkg/store/store.go | 79 ++++++++++++++++++++++++++++ pkg/store/store_test.go | 51 ++++++++++++++++++ pkg/store/types.go | 27 +++++++++- 7 files changed, 193 insertions(+), 1 deletion(-) diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index bf1b44b6cb..341c5ec289 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -546,6 +546,23 @@ 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 { + if newHeight%e.config.Node.PruningInterval == 0 { + // Compute the prune floor: all heights <= targetHeight are candidates + // for pruning of header/data/signature/index entries. + if newHeight > e.config.Node.PruningKeepRecent { + targetHeight := newHeight - e.config.Node.PruningKeepRecent + if err := e.store.PruneBlocks(e.ctx, targetHeight); err != nil { + e.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") + } + } + } + } + // broadcast header and data to P2P network g, broadcastCtx := errgroup.WithContext(e.ctx) g.Go(func() error { diff --git a/pkg/config/config.go b/pkg/config/config.go index e03a277ce8..36354a9090 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -261,6 +261,13 @@ type NodeConfig struct { // Readiness / health configuration 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."` ReadinessMaxBlocksBehind uint64 `mapstructure:"readiness_max_blocks_behind" yaml:"readiness_max_blocks_behind" comment:"How many blocks behind best-known head the node can be and still be considered ready. 0 means must be exactly at head."` + + // Pruning configuration + // When enabled, the node will periodically prune old block data (headers, data, + // signatures, and hash index) from the local store while keeping recent history. + PruningEnabled bool `mapstructure:"pruning_enabled" yaml:"pruning_enabled" comment:"Enable height-based pruning of stored block data. When disabled, all blocks are kept (archive mode)."` + PruningKeepRecent uint64 `mapstructure:"pruning_keep_recent" yaml:"pruning_keep_recent" comment:"Number of most recent blocks to retain. Older blocks will have their header/data/signature removed from the local store. 0 means keep all blocks."` + PruningInterval uint64 `mapstructure:"pruning_interval" yaml:"pruning_interval" comment:"Run pruning every N blocks. Must be >= 1 when pruning is enabled."` } // LogConfig contains all logging configuration parameters @@ -436,6 +443,10 @@ func AddFlags(cmd *cobra.Command) { cmd.Flags().Uint64(FlagReadinessWindowSeconds, def.Node.ReadinessWindowSeconds, "time window in seconds for calculating readiness threshold based on block time (default: 15s)") 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)") cmd.Flags().Duration(FlagScrapeInterval, def.Node.ScrapeInterval.Duration, "interval at which the reaper polls the execution layer for new transactions") + // Pruning configuration flags + cmd.Flags().Bool(FlagPrefixEvnode+"node.pruning_enabled", def.Node.PruningEnabled, "enable height-based pruning of stored block data (headers, data, signatures, index)") + cmd.Flags().Uint64(FlagPrefixEvnode+"node.pruning_keep_recent", def.Node.PruningKeepRecent, "number of most recent blocks to retain when pruning is enabled (0 = keep all)") + cmd.Flags().Uint64(FlagPrefixEvnode+"node.pruning_interval", def.Node.PruningInterval, "run pruning every N blocks (must be >= 1 when pruning is enabled)") // Data Availability configuration flags cmd.Flags().String(FlagDAAddress, def.DA.Address, "DA address (host:port)") diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 0de2f4bc27..ee2cbfbeec 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -69,6 +69,9 @@ func DefaultConfig() Config { ReadinessWindowSeconds: defaultReadinessWindowSeconds, ReadinessMaxBlocksBehind: calculateReadinessMaxBlocksBehind(defaultBlockTime.Duration, defaultReadinessWindowSeconds), ScrapeInterval: DurationWrapper{1 * time.Second}, + PruningEnabled: false, + PruningKeepRecent: 0, + PruningInterval: 0, }, DA: DAConfig{ Address: "http://localhost:7980", diff --git a/pkg/store/keys.go b/pkg/store/keys.go index dd989c0e82..ff96fd8955 100644 --- a/pkg/store/keys.go +++ b/pkg/store/keys.go @@ -25,6 +25,12 @@ const ( // LastSubmittedHeaderHeightKey is the key used for persisting the last submitted header height in store. LastSubmittedHeaderHeightKey = "last-submitted-header-height" + // LastPrunedBlockHeightKey is the metadata key used for persisting the last + // pruned block height in the store. All block data (header, data, + // signature, and hash index) for heights <= this value are considered + // pruned and may be missing from the store. + LastPrunedBlockHeightKey = "last-pruned-block-height" + headerPrefix = "h" dataPrefix = "d" signaturePrefix = "c" diff --git a/pkg/store/store.go b/pkg/store/store.go index eafa47ae75..a050cbfe83 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -332,6 +332,85 @@ func (s *DefaultStore) Rollback(ctx context.Context, height uint64, aggregator b return nil } +// PruneBlocks removes block data (header, data, signature, and hash index) +// up to and including the given height from the store. It does not modify +// the current chain height or any state snapshots. +// +// This method is intended for long-term storage reduction and is safe to +// call repeatedly with the same or increasing heights. +func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error { + batch, err := s.db.Batch(ctx) + if err != nil { + return fmt.Errorf("failed to create a new batch for pruning: %w", err) + } + + // Track the last successfully pruned height so we can resume across restarts. + var lastPruned uint64 + meta, err := s.GetMetadata(ctx, LastPrunedBlockHeightKey) + if err != nil { + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to get last pruned height: %w", err) + } + } else if len(meta) == heightLength { + lastPruned, err = decodeHeight(meta) + if err != nil { + return fmt.Errorf("failed to decode last pruned height: %w", err) + } + } + + // Nothing new to prune. + if height <= lastPruned { + return nil + } + + // Delete block data for heights in (lastPruned, height]. + for h := lastPruned + 1; h <= height; h++ { + // Get header blob to compute the hash index key. If header is already + // missing (e.g. due to previous partial pruning), just skip this height. + headerBlob, err := s.db.Get(ctx, ds.NewKey(getHeaderKey(h))) + if err != nil { + if errors.Is(err, ds.ErrNotFound) { + continue + } + return fmt.Errorf("failed to get header at height %d during pruning: %w", h, err) + } + + if err := batch.Delete(ctx, ds.NewKey(getHeaderKey(h))); err != nil { + return fmt.Errorf("failed to delete header at height %d during pruning: %w", h, err) + } + + if err := batch.Delete(ctx, ds.NewKey(getDataKey(h))); err != nil { + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete data at height %d during pruning: %w", h, err) + } + } + + if err := batch.Delete(ctx, ds.NewKey(getSignatureKey(h))); err != nil { + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete signature at height %d during pruning: %w", h, err) + } + } + + headerHash := sha256.Sum256(headerBlob) + if err := batch.Delete(ctx, ds.NewKey(getIndexKey(headerHash[:]))); err != nil { + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete index for height %d during pruning: %w", h, err) + } + } + } + + // Persist the updated last pruned height. + if err := batch.Put(ctx, ds.NewKey(getMetaKey(LastPrunedBlockHeightKey)), encodeHeight(height)); err != nil { + return fmt.Errorf("failed to update last pruned height: %w", err) + } + + if err := batch.Commit(ctx); err != nil { + return fmt.Errorf("failed to commit pruning batch: %w", err) + } + + return nil +} + const heightLength = 8 func encodeHeight(height uint64) []byte { diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go index 6a09db465e..0d47f73058 100644 --- a/pkg/store/store_test.go +++ b/pkg/store/store_test.go @@ -789,6 +789,57 @@ func TestRollback(t *testing.T) { require.Equal(rollbackToHeight, state.LastBlockHeight) } +func TestPruneBlocks_RemovesOldBlockDataOnly(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ds, err := NewTestInMemoryKVStore() + require.NoError(t, err) + + s := New(ds).(*DefaultStore) + + // create and store a few blocks with headers, data, signatures and state + batch, err := s.NewBatch(ctx) + require.NoError(t, err) + + var lastState types.State + for h := uint64(1); h <= 5; h++ { + header := &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{Height: h}}} + data := &types.Data{} + sig := types.Signature([]byte{byte(h)}) + + require.NoError(t, batch.SaveBlockData(header, data, &sig)) + + // fake state snapshot per height + lastState = types.State{LastBlockHeight: h} + require.NoError(t, batch.UpdateState(lastState)) + } + require.NoError(t, batch.SetHeight(5)) + require.NoError(t, batch.Commit()) + + // prune everything up to height 3 + require.NoError(t, s.PruneBlocks(ctx, 3)) + + // old block data should be gone + for h := uint64(1); h <= 3; h++ { + _, _, err := s.GetBlockData(ctx, h) + assert.Error(t, err, "expected block data at height %d to be pruned", h) + } + + // recent block data should remain + for h := uint64(4); h <= 5; h++ { + _, _, err := s.GetBlockData(ctx, h) + assert.NoError(t, err, "expected block data at height %d to be kept", h) + } + + // state snapshots are not pruned by PruneBlocks + for h := uint64(1); h <= 5; h++ { + st, err := s.GetStateAtHeight(ctx, h) + assert.NoError(t, err, "expected state at height %d to remain", h) + assert.Equal(t, h, st.LastBlockHeight) + } +} + // TestRollbackToSameHeight verifies that rollback to same height is a no-op func TestRollbackToSameHeight(t *testing.T) { t.Parallel() diff --git a/pkg/store/types.go b/pkg/store/types.go index 13b62e2f1e..1162d0075e 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -30,10 +30,24 @@ type Batch interface { } // Store is minimal interface for storing and retrieving blocks, commits and state. +// +// It is composed from three concerns: +// - Reader: read access to blocks, state, and metadata +// - Rollback: consensus rollback logic (used for chain reorgs / recovery) +// - Pruner: long-term height-based pruning of historical block data type Store interface { - Rollback Reader Metadata + Rollback + Pruner + + // SetMetadata saves arbitrary value in the store. + // + // This method enables evolve to safely persist any information. + SetMetadata(ctx context.Context, key string, value []byte) error + + // DeleteMetadata removes a metadata key from the store. + DeleteMetadata(ctx context.Context, key string) error // Close safely closes underlying data storage, to ensure that data is actually saved. Close() error @@ -91,3 +105,14 @@ type Rollback interface { // Aggregator is used to determine if the rollback is performed on the aggregator node. Rollback(ctx context.Context, height uint64, aggregator bool) error } + +// Pruner provides long-term, height-based pruning of historical block data. +// +// Implementations SHOULD be idempotent and safe to call multiple times for +// the same or increasing target heights. +type Pruner interface { + // PruneBlocks removes block data (header, data, signature, and hash index) + // up to and including the given height from the store, without modifying + // state snapshots or the current chain height. + PruneBlocks(ctx context.Context, height uint64) error +} From 95a387684f4ee31fb60ea89248bfd8092c7e8c59 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:10:28 +0100 Subject: [PATCH 02/28] prune metadata from ev-node store --- pkg/store/store.go | 12 ++++++++++++ pkg/store/store_test.go | 30 +++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/pkg/store/store.go b/pkg/store/store.go index a050cbfe83..8f045762d7 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -391,6 +391,18 @@ func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error { } } + // Delete per-height DA metadata associated with this height, if any. + if err := batch.Delete(ctx, ds.NewKey(getMetaKey(GetHeightToDAHeightHeaderKey(h)))); err != nil { + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete header DA height metadata at height %d during pruning: %w", h, err) + } + } + if err := batch.Delete(ctx, ds.NewKey(getMetaKey(GetHeightToDAHeightDataKey(h)))); err != nil { + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete data DA height metadata at height %d during pruning: %w", h, err) + } + } + headerHash := sha256.Sum256(headerBlob) if err := batch.Delete(ctx, ds.NewKey(getIndexKey(headerHash[:]))); err != nil { if !errors.Is(err, ds.ErrNotFound) { diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go index 0d47f73058..f1b9a131cc 100644 --- a/pkg/store/store_test.go +++ b/pkg/store/store_test.go @@ -798,7 +798,7 @@ func TestPruneBlocks_RemovesOldBlockDataOnly(t *testing.T) { s := New(ds).(*DefaultStore) - // create and store a few blocks with headers, data, signatures and state + // create and store a few blocks with headers, data, signatures, state, and per-height DA metadata batch, err := s.NewBatch(ctx) require.NoError(t, err) @@ -813,6 +813,14 @@ func TestPruneBlocks_RemovesOldBlockDataOnly(t *testing.T) { // fake state snapshot per height lastState = types.State{LastBlockHeight: h} require.NoError(t, batch.UpdateState(lastState)) + + // store fake DA metadata per height + hDaKey := GetHeightToDAHeightHeaderKey(h) + dDaKey := GetHeightToDAHeightDataKey(h) + bz := make([]byte, 8) + binary.LittleEndian.PutUint64(bz, h+100) // arbitrary DA height + require.NoError(t, s.SetMetadata(ctx, hDaKey, bz)) + require.NoError(t, s.SetMetadata(ctx, dDaKey, bz)) } require.NoError(t, batch.SetHeight(5)) require.NoError(t, batch.Commit()) @@ -838,6 +846,26 @@ func TestPruneBlocks_RemovesOldBlockDataOnly(t *testing.T) { assert.NoError(t, err, "expected state at height %d to remain", h) assert.Equal(t, h, st.LastBlockHeight) } + + // per-height DA metadata for pruned heights should be gone + for h := uint64(1); h <= 3; h++ { + hDaKey := GetHeightToDAHeightHeaderKey(h) + dDaKey := GetHeightToDAHeightDataKey(h) + _, err := s.GetMetadata(ctx, hDaKey) + assert.Error(t, err, "expected header DA metadata at height %d to be pruned", h) + _, err = s.GetMetadata(ctx, dDaKey) + assert.Error(t, err, "expected data DA metadata at height %d to be pruned", h) + } + + // per-height DA metadata for unpruned heights should remain + for h := uint64(4); h <= 5; h++ { + hDaKey := GetHeightToDAHeightHeaderKey(h) + dDaKey := GetHeightToDAHeightDataKey(h) + _, err := s.GetMetadata(ctx, hDaKey) + assert.NoError(t, err, "expected header DA metadata at height %d to remain", h) + _, err = s.GetMetadata(ctx, dDaKey) + assert.NoError(t, err, "expected data DA metadata at height %d to remain", h) + } } // TestRollbackToSameHeight verifies that rollback to same height is a no-op From b8ca7fb8e971f35f2bb0aa84076c892ed3185684 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:27:04 +0100 Subject: [PATCH 03/28] wiring prunning config to go-header --- pkg/sync/sync_service.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/sync/sync_service.go b/pkg/sync/sync_service.go index 8567e79764..c9e26fd31e 100644 --- a/pkg/sync/sync_service.go +++ b/pkg/sync/sync_service.go @@ -189,11 +189,21 @@ func (syncService *SyncService[H]) Start(ctx context.Context) error { } // create syncer, must be before initFromP2PWithRetry which calls startSyncer. + syncOpts := []goheadersync.Option{goheadersync.WithBlockTime(syncService.conf.Node.BlockTime.Duration)} + // Map ev-node pruning configuration to go-header's pruning window: we approximate + // "keep N recent heights" as "retain headers for N * blockTime". + if syncService.conf.Node.PruningEnabled && syncService.conf.Node.PruningKeepRecent > 0 { + pruningWindow := syncService.conf.Node.BlockTime.Duration * time.Duration(syncService.conf.Node.PruningKeepRecent) + // Only set a pruning window if the computed duration is positive. + if pruningWindow > 0 { + syncOpts = append(syncOpts, goheadersync.WithPruningWindow(pruningWindow)) + } + } if syncService.syncer, err = newSyncer( syncService.ex, syncService.store, syncService.sub, - []goheadersync.Option{goheadersync.WithBlockTime(syncService.conf.Node.BlockTime.Duration)}, + syncOpts, ); err != nil { return fmt.Errorf("failed to create syncer: %w", err) } @@ -467,7 +477,10 @@ func newSyncer[H header.Header[H]]( opts = append(opts, goheadersync.WithMetrics(), +<<<<<<< HEAD goheadersync.WithPruningWindow(ninetyNineYears), // pruning window not relevant, because of the store wrapper. +======= +>>>>>>> 3b9a0f70 (wiring prunning config to go-header) goheadersync.WithTrustingPeriod(ninetyNineYears), ) return goheadersync.NewSyncer(ex, store, sub, opts...) From d09b8033a51314e27559e7a8f7be5b78749d64a3 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:42:55 +0100 Subject: [PATCH 04/28] prune evm exec store --- block/internal/executing/executor.go | 11 ++++ core/execution/execution.go | 13 ++++ execution/evm/execution.go | 17 +++++ execution/evm/store.go | 53 +++++++++++++++ execution/evm/store_test.go | 99 ++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+) create mode 100644 execution/evm/store_test.go diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index 341c5ec289..426afdcf06 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -559,6 +559,17 @@ func (e *Executor) ProduceBlock(ctx context.Context) error { if err := e.store.PruneBlocks(e.ctx, targetHeight); err != nil { e.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") } + + // If the execution client exposes execution-metadata pruning, + // prune ExecMeta using the same target height. This keeps EVM + // execution metadata aligned with ev-node's block store pruning + // while remaining a no-op for execution environments that don't + // implement ExecMetaPruner (e.g. ABCI-based executors). + if pruner, ok := e.exec.(coreexecutor.ExecMetaPruner); ok { + if err := pruner.PruneExecMeta(e.ctx, targetHeight); err != nil { + e.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") + } + } } } } diff --git a/core/execution/execution.go b/core/execution/execution.go index 276da369e9..1f85e2068a 100644 --- a/core/execution/execution.go +++ b/core/execution/execution.go @@ -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 +} diff --git a/execution/evm/execution.go b/execution/evm/execution.go index 7a0a9dfb5a..5263539429 100644 --- a/execution/evm/execution.go +++ b/execution/evm/execution.go @@ -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 @@ -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 diff --git a/execution/evm/store.go b/execution/evm/store.go index af731ee7c6..51a3056b66 100644 --- a/execution/evm/store.go +++ b/execution/evm/store.go @@ -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" @@ -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)) diff --git a/execution/evm/store_test.go b/execution/evm/store_test.go new file mode 100644 index 0000000000..64389701f9 --- /dev/null +++ b/execution/evm/store_test.go @@ -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) +} From c329f769a82c62177458119b680d37e7b2f6b904 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:45:24 +0100 Subject: [PATCH 05/28] add parameters validation --- pkg/config/config.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/config/config.go b/pkg/config/config.go index 36354a9090..7a14aaae0a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -383,6 +383,21 @@ func (c *Config) Validate() error { if err := c.Raft.Validate(); err != nil { return err } + + // Validate pruning configuration + if c.Node.PruningEnabled { + // When pruning is enabled, pruning_interval must be >= 1 + if c.Node.PruningInterval == 0 { + return fmt.Errorf("pruning_interval must be >= 1 when pruning is enabled") + } + + // When pruning is enabled, keeping 0 blocks is contradictory; use pruning_enabled=false + // for archive mode instead. + if c.Node.PruningKeepRecent == 0 { + return fmt.Errorf("pruning_keep_recent must be > 0 when pruning is enabled; use pruning_enabled=false to keep all blocks") + } + } + return nil } From 9ae2f6a0bd0aba19bdc8d77ddcdb5a2284c737b8 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:47:32 +0100 Subject: [PATCH 06/28] make error handling consistent --- pkg/store/store.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/store/store.go b/pkg/store/store.go index 8f045762d7..d981400f07 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -376,7 +376,9 @@ func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error { } if err := batch.Delete(ctx, ds.NewKey(getHeaderKey(h))); err != nil { - return fmt.Errorf("failed to delete header at height %d during pruning: %w", h, err) + if !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete header at height %d during pruning: %w", h, err) + } } if err := batch.Delete(ctx, ds.NewKey(getDataKey(h))); err != nil { From 907f28caea0e90d53648912e2cab64b96d50c9e7 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:19:26 +0100 Subject: [PATCH 07/28] add method to tracedstore to respect interface --- pkg/store/tracing.go | 16 ++++++++++++++++ pkg/store/tracing_test.go | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/pkg/store/tracing.go b/pkg/store/tracing.go index 33a21f93ad..42d686d612 100644 --- a/pkg/store/tracing.go +++ b/pkg/store/tracing.go @@ -247,6 +247,22 @@ func (t *tracedStore) Rollback(ctx context.Context, height uint64, aggregator bo return nil } +func (t *tracedStore) PruneBlocks(ctx context.Context, height uint64) error { + ctx, span := t.tracer.Start(ctx, "Store.PruneBlocks", + trace.WithAttributes(attribute.Int64("height", int64(height))), + ) + defer span.End() + + err := t.inner.PruneBlocks(ctx, height) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + + return nil +} + func (t *tracedStore) Close() error { return t.inner.Close() } diff --git a/pkg/store/tracing_test.go b/pkg/store/tracing_test.go index 0c0b967605..5477a66985 100644 --- a/pkg/store/tracing_test.go +++ b/pkg/store/tracing_test.go @@ -124,6 +124,12 @@ func (m *tracingMockStore) Rollback(ctx context.Context, height uint64, aggregat return nil } +func (m *tracingMockStore) PruneBlocks(ctx context.Context, height uint64) error { + // For tracing tests we don't need pruning behavior; just satisfy the Store + // interface. Specific pruning behavior is tested separately in store_test.go. + return nil +} + func (m *tracingMockStore) Close() error { return nil } From b022f10d8d64e6d2bc5b0c99e1ffb858a1ae75e8 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:42:23 +0100 Subject: [PATCH 08/28] add prune block to mockstore --- test/mocks/store.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/mocks/store.go b/test/mocks/store.go index 353fa0a1bb..efd1939940 100644 --- a/test/mocks/store.go +++ b/test/mocks/store.go @@ -39,6 +39,14 @@ func (_m *MockStore) EXPECT() *MockStore_Expecter { return &MockStore_Expecter{mock: &_m.Mock} } +// PruneBlocks provides a mock implementation for the Store's pruning method. +// Tests using MockStore currently do not exercise pruning behavior, so this +// method simply satisfies the interface and can be extended with expectations +// later if needed. +func (_mock *MockStore) PruneBlocks(ctx context.Context, height uint64) error { + return nil +} + // Close provides a mock function for the type MockStore func (_mock *MockStore) Close() error { ret := _mock.Called() From 99d586b1db8e82648c445aa15b6369ae6e4608cc Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:49:29 +0100 Subject: [PATCH 09/28] fix helper for flag for consistency --- pkg/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 7a14aaae0a..f42bfb666b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -266,7 +266,7 @@ type NodeConfig struct { // When enabled, the node will periodically prune old block data (headers, data, // signatures, and hash index) from the local store while keeping recent history. PruningEnabled bool `mapstructure:"pruning_enabled" yaml:"pruning_enabled" comment:"Enable height-based pruning of stored block data. When disabled, all blocks are kept (archive mode)."` - PruningKeepRecent uint64 `mapstructure:"pruning_keep_recent" yaml:"pruning_keep_recent" comment:"Number of most recent blocks to retain. Older blocks will have their header/data/signature removed from the local store. 0 means keep all blocks."` + PruningKeepRecent uint64 `mapstructure:"pruning_keep_recent" yaml:"pruning_keep_recent" comment:"Number of most recent blocks to retain when pruning is enabled. Must be > 0 when pruning is enabled; set pruning_enabled=false to keep all blocks (archive mode)."` PruningInterval uint64 `mapstructure:"pruning_interval" yaml:"pruning_interval" comment:"Run pruning every N blocks. Must be >= 1 when pruning is enabled."` } @@ -460,7 +460,7 @@ func AddFlags(cmd *cobra.Command) { cmd.Flags().Duration(FlagScrapeInterval, def.Node.ScrapeInterval.Duration, "interval at which the reaper polls the execution layer for new transactions") // Pruning configuration flags cmd.Flags().Bool(FlagPrefixEvnode+"node.pruning_enabled", def.Node.PruningEnabled, "enable height-based pruning of stored block data (headers, data, signatures, index)") - cmd.Flags().Uint64(FlagPrefixEvnode+"node.pruning_keep_recent", def.Node.PruningKeepRecent, "number of most recent blocks to retain when pruning is enabled (0 = keep all)") + cmd.Flags().Uint64(FlagPrefixEvnode+"node.pruning_keep_recent", def.Node.PruningKeepRecent, "number of most recent blocks to retain when pruning is enabled (must be > 0; disable pruning to keep all blocks)") cmd.Flags().Uint64(FlagPrefixEvnode+"node.pruning_interval", def.Node.PruningInterval, "run pruning every N blocks (must be >= 1 when pruning is enabled)") // Data Availability configuration flags From f798b18d4e87c3729b67a8613d776a35a39dcc88 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:16:17 +0100 Subject: [PATCH 10/28] add replace statement for local packages --- apps/evm/go.mod | 1 + apps/evm/go.sum | 2 -- apps/grpc/go.mod | 1 + apps/grpc/go.sum | 2 -- apps/testapp/go.mod | 5 ++++- apps/testapp/go.sum | 2 -- execution/evm/go.mod | 5 +++++ execution/evm/go.sum | 4 ---- go.mod | 3 +++ go.sum | 2 -- 10 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/evm/go.mod b/apps/evm/go.mod index b4a4d2a3b4..4e373eb123 100644 --- a/apps/evm/go.mod +++ b/apps/evm/go.mod @@ -4,6 +4,7 @@ go 1.25.6 replace ( github.com/evstack/ev-node => ../../ + github.com/evstack/ev-node/core => ../../core github.com/evstack/ev-node/execution/evm => ../../execution/evm ) diff --git a/apps/evm/go.sum b/apps/evm/go.sum index 5ea0339d44..58c363be55 100644 --- a/apps/evm/go.sum +++ b/apps/evm/go.sum @@ -411,8 +411,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= diff --git a/apps/grpc/go.mod b/apps/grpc/go.mod index 9a14cf9cfb..31ecc287fd 100644 --- a/apps/grpc/go.mod +++ b/apps/grpc/go.mod @@ -4,6 +4,7 @@ go 1.25.6 replace ( github.com/evstack/ev-node => ../../ + github.com/evstack/ev-node/core => ../../core github.com/evstack/ev-node/execution/grpc => ../../execution/grpc ) diff --git a/apps/grpc/go.sum b/apps/grpc/go.sum index fb175ac5cd..d1fd875f09 100644 --- a/apps/grpc/go.sum +++ b/apps/grpc/go.sum @@ -367,8 +367,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/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= diff --git a/apps/testapp/go.mod b/apps/testapp/go.mod index d68a967d63..c67b20897d 100644 --- a/apps/testapp/go.mod +++ b/apps/testapp/go.mod @@ -2,7 +2,10 @@ module github.com/evstack/ev-node/apps/testapp go 1.25.6 -replace github.com/evstack/ev-node => ../../ +replace ( + github.com/evstack/ev-node => ../../. + github.com/evstack/ev-node/core => ../../core +) require ( github.com/evstack/ev-node v1.0.0-rc.3 diff --git a/apps/testapp/go.sum b/apps/testapp/go.sum index fb175ac5cd..d1fd875f09 100644 --- a/apps/testapp/go.sum +++ b/apps/testapp/go.sum @@ -367,8 +367,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/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= diff --git a/execution/evm/go.mod b/execution/evm/go.mod index ecd188489f..7d46e86573 100644 --- a/execution/evm/go.mod +++ b/execution/evm/go.mod @@ -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 +) diff --git a/execution/evm/go.sum b/execution/evm/go.sum index f00f1139ab..3294e40da9 100644 --- a/execution/evm/go.sum +++ b/execution/evm/go.sum @@ -78,10 +78,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.3 h1:hphJBI0b1TgGN9wajB1twouMVMjhyHXXrS9QaG1XwvQ= -github.com/evstack/ev-node v1.0.0-rc.3/go.mod h1:5Cf3SauhgIV+seQKBJavv3f8ZZw+YTnH5DRJcI4Ooj0= -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= diff --git a/go.mod b/go.mod index d07ea7a7e8..8f10b14e53 100644 --- a/go.mod +++ b/go.mod @@ -195,3 +195,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 diff --git a/go.sum b/go.sum index fb175ac5cd..d1fd875f09 100644 --- a/go.sum +++ b/go.sum @@ -367,8 +367,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/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= From 70e4901c12b75466407b5d8aa74b7d592ea27be3 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:41:18 +0100 Subject: [PATCH 11/28] flags --- pkg/config/config_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1834e1b405..9189aa57e5 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -112,7 +112,7 @@ func TestAddFlags(t *testing.T) { assertFlagValue(t, flags, FlagRPCEnableDAVisualization, DefaultConfig().RPC.EnableDAVisualization) // Count the number of flags we're explicitly checking - expectedFlagCount := 63 // Update this number if you add more flag checks above + expectedFlagCount := 66 // Update this number if you add more flag checks above // Get the actual number of flags (both regular and persistent) actualFlagCount := 0 From 9315c6c022b6dba9bf5e25a103f93678e92faa89 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:03:17 +0100 Subject: [PATCH 12/28] add safetey mechanism for pruning only da included blocks --- block/internal/executing/executor.go | 49 +++++++++++++++++++--------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index 426afdcf06..89b9c4f1f4 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -3,6 +3,7 @@ package executing import ( "bytes" "context" + "encoding/binary" "errors" "fmt" "reflect" @@ -551,23 +552,41 @@ func (e *Executor) ProduceBlock(ctx context.Context) error { // 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 { - if newHeight%e.config.Node.PruningInterval == 0 { - // Compute the prune floor: all heights <= targetHeight are candidates - // for pruning of header/data/signature/index entries. - if newHeight > e.config.Node.PruningKeepRecent { - targetHeight := newHeight - e.config.Node.PruningKeepRecent - if err := e.store.PruneBlocks(e.ctx, targetHeight); err != nil { - e.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") + 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 the execution client exposes execution-metadata pruning, - // prune ExecMeta using the same target height. This keeps EVM - // execution metadata aligned with ev-node's block store pruning - // while remaining a no-op for execution environments that don't - // implement ExecMetaPruner (e.g. ABCI-based executors). - if pruner, ok := e.exec.(coreexecutor.ExecMetaPruner); ok { - if err := pruner.PruneExecMeta(e.ctx, targetHeight); err != nil { - e.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") + 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) + } } } } From 23af6b75384a0873138bbe6373bc09381c41117d Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:12:43 +0100 Subject: [PATCH 13/28] fix rebase --- pkg/sync/sync_service.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/sync/sync_service.go b/pkg/sync/sync_service.go index c9e26fd31e..d4a5f0e818 100644 --- a/pkg/sync/sync_service.go +++ b/pkg/sync/sync_service.go @@ -477,10 +477,7 @@ func newSyncer[H header.Header[H]]( opts = append(opts, goheadersync.WithMetrics(), -<<<<<<< HEAD goheadersync.WithPruningWindow(ninetyNineYears), // pruning window not relevant, because of the store wrapper. -======= ->>>>>>> 3b9a0f70 (wiring prunning config to go-header) goheadersync.WithTrustingPeriod(ninetyNineYears), ) return goheadersync.NewSyncer(ex, store, sub, opts...) From f250b646bcb4f44237ae91b15b6b353b4b597ee2 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:39:34 +0100 Subject: [PATCH 14/28] remove useless check --- execution/evm/execution.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/execution/evm/execution.go b/execution/evm/execution.go index 5263539429..370887cfd7 100644 --- a/execution/evm/execution.go +++ b/execution/evm/execution.go @@ -275,10 +275,6 @@ func NewEngineExecutionClient( // 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) } From 135c925d66c513b4c161c7e49c7655e9b17c2c0f Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:40:19 +0100 Subject: [PATCH 15/28] use lastprunedheight in Tail() to optimize --- pkg/store/store_adapter.go | 57 +++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/pkg/store/store_adapter.go b/pkg/store/store_adapter.go index 08743c6eb9..2a751749a3 100644 --- a/pkg/store/store_adapter.go +++ b/pkg/store/store_adapter.go @@ -52,6 +52,12 @@ type EntityWithDAHint[H any] interface { DAHint() uint64 } +// lastPrunedHeightGetter is an optional interface that store getters can +// implement to expose the last pruned block height. +type lastPrunedHeightGetter interface { + LastPrunedHeight(ctx context.Context) (uint64, bool) +} + // heightSub provides a mechanism for waiting on a specific height to be stored. // This is critical for go-header syncer which expects GetByHeight to block until // the requested height is available. @@ -400,8 +406,19 @@ func (a *StoreAdapter[H]) Tail(ctx context.Context) (H, error) { height = h } - // Try genesisInitialHeight first (most common case - no pruning) - item, err := a.getter.GetByHeight(ctx, a.genesisInitialHeight) + // Determine the first candidate tail height. By default, this is the + // genesis initial height, but if pruning metadata is available we can + // skip directly past fully-pruned ranges. + startHeight := a.genesisInitialHeight + if getter, ok := any(a.getter).(lastPrunedHeightGetter); ok { + if lastPruned, ok := getter.LastPrunedHeight(ctx); ok { + if lastPruned < ^uint64(0) { + startHeight = lastPruned + 1 + } + } + } + + item, err := a.getter.GetByHeight(ctx, startHeight) if err == nil { return item, nil } @@ -411,8 +428,8 @@ func (a *StoreAdapter[H]) Tail(ctx context.Context) (H, error) { return pendingItem, nil } - // Walk up from genesisInitialHeight to find the first available item (pruning case) - for h := a.genesisInitialHeight + 1; h <= height; h++ { + // Walk up from startHeight to find the first available item + for h := startHeight + 1; h <= height; h++ { item, err = a.getter.GetByHeight(ctx, h) if err == nil { return item, nil @@ -804,6 +821,22 @@ func (g *HeaderStoreGetter) HasAt(ctx context.Context, height uint64) bool { return err == nil } +// LastPrunedHeight implements lastPrunedHeightGetter for HeaderStoreGetter by +// reading the pruning metadata from the underlying store. +func (g *HeaderStoreGetter) LastPrunedHeight(ctx context.Context) (uint64, bool) { + meta, err := g.store.GetMetadata(ctx, LastPrunedBlockHeightKey) + if err != nil || len(meta) != heightLength { + return 0, false + } + + height, err := decodeHeight(meta) + if err != nil { + return 0, false + } + + return height, true +} + // DataStoreGetter implements StoreGetter for *types.Data. type DataStoreGetter struct { store Store @@ -874,6 +907,22 @@ func (g *DataStoreGetter) HasAt(ctx context.Context, height uint64) bool { return err == nil } +// LastPrunedHeight implements lastPrunedHeightGetter for DataStoreGetter by +// reading the pruning metadata from the underlying store. +func (g *DataStoreGetter) LastPrunedHeight(ctx context.Context) (uint64, bool) { + meta, err := g.store.GetMetadata(ctx, LastPrunedBlockHeightKey) + if err != nil || len(meta) != heightLength { + return 0, false + } + + height, err := decodeHeight(meta) + if err != nil { + return 0, false + } + + return height, true +} + // Type aliases for convenience type HeaderStoreAdapter = StoreAdapter[*types.P2PSignedHeader] type DataStoreAdapter = StoreAdapter[*types.P2PData] From c1168c9affbe74728b6714ce30600e3a38b5176c Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:52:32 +0100 Subject: [PATCH 16/28] move pruning from executor to dainclusionloop --- block/internal/executing/executor.go | 47 -------------------------- block/internal/submitting/submitter.go | 33 ++++++++++++++++++ 2 files changed, 33 insertions(+), 47 deletions(-) diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index 89b9c4f1f4..bf1b44b6cb 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -3,7 +3,6 @@ package executing import ( "bytes" "context" - "encoding/binary" "errors" "fmt" "reflect" @@ -547,52 +546,6 @@ 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(e.ctx) g.Go(func() error { diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 1c5b034c19..3b1cdd0d2d 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -360,6 +360,39 @@ func (s *Submitter) processDAInclusionLoop() { s.logger.Error().Err(err).Uint64("height", nextHeight).Msg("failed to persist DA included height") } + // Run height-based pruning if enabled. + if s.config.Node.PruningEnabled && s.config.Node.PruningKeepRecent > 0 && s.config.Node.PruningInterval > 0 { + // Trigger pruning only when we reach the configured interval. + if currentDAIncluded%s.config.Node.PruningInterval == 0 { + // We must make sure not to prune blocks that have not yet been included in DA. + daIncludedHeight := s.GetDAIncludedHeight() + + storeHeight, err := s.store.Height(s.ctx) + if err != nil { + s.logger.Error().Err(err).Msg("failed to get store height for pruning") + break + } + + upperBound := min(storeHeight, daIncludedHeight) + if upperBound <= s.config.Node.PruningKeepRecent { + // Not enough fully included blocks to prune while respecting keep-recent. + break + } + + targetHeight := upperBound - s.config.Node.PruningKeepRecent + + if err := s.store.PruneBlocks(s.ctx, targetHeight); err != nil { + s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") + } + + if pruner, ok := s.exec.(coreexecutor.ExecMetaPruner); ok { + if err := pruner.PruneExecMeta(s.ctx, targetHeight); err != nil { + s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") + } + } + } + } + // Delete height cache for that height // This can only be performed after the height has been persisted to store s.cache.DeleteHeight(nextHeight) From 155ed5c8792d68869dda27e731ea8135df2dbe10 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:52:48 +0100 Subject: [PATCH 17/28] don't prune go-header store --- pkg/sync/sync_service.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pkg/sync/sync_service.go b/pkg/sync/sync_service.go index d4a5f0e818..8567e79764 100644 --- a/pkg/sync/sync_service.go +++ b/pkg/sync/sync_service.go @@ -189,21 +189,11 @@ func (syncService *SyncService[H]) Start(ctx context.Context) error { } // create syncer, must be before initFromP2PWithRetry which calls startSyncer. - syncOpts := []goheadersync.Option{goheadersync.WithBlockTime(syncService.conf.Node.BlockTime.Duration)} - // Map ev-node pruning configuration to go-header's pruning window: we approximate - // "keep N recent heights" as "retain headers for N * blockTime". - if syncService.conf.Node.PruningEnabled && syncService.conf.Node.PruningKeepRecent > 0 { - pruningWindow := syncService.conf.Node.BlockTime.Duration * time.Duration(syncService.conf.Node.PruningKeepRecent) - // Only set a pruning window if the computed duration is positive. - if pruningWindow > 0 { - syncOpts = append(syncOpts, goheadersync.WithPruningWindow(pruningWindow)) - } - } if syncService.syncer, err = newSyncer( syncService.ex, syncService.store, syncService.sub, - syncOpts, + []goheadersync.Option{goheadersync.WithBlockTime(syncService.conf.Node.BlockTime.Duration)}, ); err != nil { return fmt.Errorf("failed to create syncer: %w", err) } From 52c005e8fa66cec85e142b6d066f8540b42ba7a6 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:08:46 +0100 Subject: [PATCH 18/28] update tail function --- pkg/store/store_adapter.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/pkg/store/store_adapter.go b/pkg/store/store_adapter.go index 2a751749a3..d593576f98 100644 --- a/pkg/store/store_adapter.go +++ b/pkg/store/store_adapter.go @@ -410,7 +410,7 @@ func (a *StoreAdapter[H]) Tail(ctx context.Context) (H, error) { // genesis initial height, but if pruning metadata is available we can // skip directly past fully-pruned ranges. startHeight := a.genesisInitialHeight - if getter, ok := any(a.getter).(lastPrunedHeightGetter); ok { + if getter, ok := a.getter.(lastPrunedHeightGetter); ok { if lastPruned, ok := getter.LastPrunedHeight(ctx); ok { if lastPruned < ^uint64(0) { startHeight = lastPruned + 1 @@ -821,22 +821,6 @@ func (g *HeaderStoreGetter) HasAt(ctx context.Context, height uint64) bool { return err == nil } -// LastPrunedHeight implements lastPrunedHeightGetter for HeaderStoreGetter by -// reading the pruning metadata from the underlying store. -func (g *HeaderStoreGetter) LastPrunedHeight(ctx context.Context) (uint64, bool) { - meta, err := g.store.GetMetadata(ctx, LastPrunedBlockHeightKey) - if err != nil || len(meta) != heightLength { - return 0, false - } - - height, err := decodeHeight(meta) - if err != nil { - return 0, false - } - - return height, true -} - // DataStoreGetter implements StoreGetter for *types.Data. type DataStoreGetter struct { store Store From 9e046c4e5c8e8949383eaf52691f861e0848f3a4 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:13:18 +0100 Subject: [PATCH 19/28] rename execmetapruner to execpruner --- block/internal/submitting/submitter.go | 4 ++-- core/execution/execution.go | 8 ++++---- execution/evm/execution.go | 8 ++++---- execution/evm/store.go | 4 ++-- execution/evm/store_test.go | 12 ++++++------ 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 3b1cdd0d2d..28f6252435 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -385,8 +385,8 @@ func (s *Submitter) processDAInclusionLoop() { s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") } - if pruner, ok := s.exec.(coreexecutor.ExecMetaPruner); ok { - if err := pruner.PruneExecMeta(s.ctx, targetHeight); err != nil { + if pruner, ok := s.exec.(coreexecutor.ExecPruner); ok { + if err := pruner.PruneExec(s.ctx, targetHeight); err != nil { s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") } } diff --git a/core/execution/execution.go b/core/execution/execution.go index 1f85e2068a..4eb8482f2c 100644 --- a/core/execution/execution.go +++ b/core/execution/execution.go @@ -162,15 +162,15 @@ type Rollbackable interface { Rollback(ctx context.Context, targetHeight uint64) error } -// ExecMetaPruner is an optional interface that execution clients can implement +// ExecPruner 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 +type ExecPruner interface { + // PruneExec 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 + PruneExec(ctx context.Context, height uint64) error } diff --git a/execution/evm/execution.go b/execution/evm/execution.go index 370887cfd7..1c988b67e3 100644 --- a/execution/evm/execution.go +++ b/execution/evm/execution.go @@ -69,7 +69,7 @@ var _ execution.Rollbackable = (*EngineClient)(nil) // 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) +var _ execution.ExecPruner = (*EngineClient)(nil) // validatePayloadStatus checks the payload status and returns appropriate errors. // It implements the Engine API specification's status handling: @@ -271,11 +271,11 @@ func NewEngineExecutionClient( }, nil } -// PruneExecMeta implements execution.ExecMetaPruner by delegating to the +// PruneExec implements execution.ExecPruner 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 { - return c.store.PruneExecMeta(ctx, height) +func (c *EngineClient) PruneExec(ctx context.Context, height uint64) error { + return c.store.PruneExec(ctx, height) } // SetLogger allows callers to attach a structured logger. diff --git a/execution/evm/store.go b/execution/evm/store.go index 51a3056b66..21fe5008ae 100644 --- a/execution/evm/store.go +++ b/execution/evm/store.go @@ -145,10 +145,10 @@ func (s *EVMStore) SaveExecMeta(ctx context.Context, meta *ExecMeta) error { return nil } -// PruneExecMeta removes ExecMeta entries up to and including the given height. +// PruneExec 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 { +func (s *EVMStore) PruneExec(ctx context.Context, height uint64) error { // Load last pruned height, if any. var lastPruned uint64 data, err := s.db.Get(ctx, ds.NewKey(lastPrunedExecMetaKey)) diff --git a/execution/evm/store_test.go b/execution/evm/store_test.go index 64389701f9..d3067fc1a6 100644 --- a/execution/evm/store_test.go +++ b/execution/evm/store_test.go @@ -17,7 +17,7 @@ func newTestDatastore(t *testing.T) ds.Batching { return dssync.MutexWrap(ds.NewMapDatastore()) } -func TestPruneExecMeta_PrunesUpToTargetHeight(t *testing.T) { +func TestPruneExec_PrunesUpToTargetHeight(t *testing.T) { t.Parallel() ctx := context.Background() @@ -39,7 +39,7 @@ func TestPruneExecMeta_PrunesUpToTargetHeight(t *testing.T) { } // Prune up to height 3 - require.NoError(t, store.PruneExecMeta(ctx, 3)) + require.NoError(t, store.PruneExec(ctx, 3)) // Heights 1..3 should be gone for h := uint64(1); h <= 3; h++ { @@ -56,10 +56,10 @@ func TestPruneExecMeta_PrunesUpToTargetHeight(t *testing.T) { } // Re-pruning with the same height should be a no-op - require.NoError(t, store.PruneExecMeta(ctx, 3)) + require.NoError(t, store.PruneExec(ctx, 3)) } -func TestPruneExecMeta_TracksLastPrunedHeight(t *testing.T) { +func TestPruneExec_TracksLastPrunedHeight(t *testing.T) { t.Parallel() ctx := context.Background() @@ -73,10 +73,10 @@ func TestPruneExecMeta_TracksLastPrunedHeight(t *testing.T) { } // First prune up to 2 - require.NoError(t, store.PruneExecMeta(ctx, 2)) + require.NoError(t, store.PruneExec(ctx, 2)) // Then prune up to 4; heights 3..4 should be deleted in this run - require.NoError(t, store.PruneExecMeta(ctx, 4)) + require.NoError(t, store.PruneExec(ctx, 4)) // Verify all heights 1..4 are gone, 5 remains for h := uint64(1); h <= 4; h++ { From 3c1641b5a8e42aec65daf510cd47b87717dfe300 Mon Sep 17 00:00:00 2001 From: Pierrick <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:40:24 +0100 Subject: [PATCH 20/28] Update core/execution/execution.go Co-authored-by: julienrbrt --- core/execution/execution.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/execution/execution.go b/core/execution/execution.go index 4eb8482f2c..f3ebe1da91 100644 --- a/core/execution/execution.go +++ b/core/execution/execution.go @@ -163,10 +163,7 @@ type Rollbackable interface { } // ExecPruner 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. +// to support height-based pruning of their execution metadata. type ExecPruner interface { // PruneExec should delete execution metadata for all heights up to and // including the given height. Implementations should be idempotent and track From 2566dae489545b6f65fdceaac7ce850f6660272b Mon Sep 17 00:00:00 2001 From: Pierrick <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:41:30 +0100 Subject: [PATCH 21/28] Update execution/evm/execution.go Co-authored-by: julienrbrt --- execution/evm/execution.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/execution/evm/execution.go b/execution/evm/execution.go index 1c988b67e3..399f7c91ee 100644 --- a/execution/evm/execution.go +++ b/execution/evm/execution.go @@ -66,9 +66,7 @@ var _ execution.HeightProvider = (*EngineClient)(nil) 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. +// ev-node's height-based pruning. var _ execution.ExecPruner = (*EngineClient)(nil) // validatePayloadStatus checks the payload status and returns appropriate errors. From 535cf1ce343a5b80ad4bb95d12f644def1faeb81 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:29:40 +0100 Subject: [PATCH 22/28] trigger pruning every ticker --- block/internal/submitting/submitter.go | 65 ++++++++++++++------------ 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 28f6252435..886f1ac0f9 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -360,42 +360,47 @@ func (s *Submitter) processDAInclusionLoop() { s.logger.Error().Err(err).Uint64("height", nextHeight).Msg("failed to persist DA included height") } - // Run height-based pruning if enabled. - if s.config.Node.PruningEnabled && s.config.Node.PruningKeepRecent > 0 && s.config.Node.PruningInterval > 0 { - // Trigger pruning only when we reach the configured interval. - if currentDAIncluded%s.config.Node.PruningInterval == 0 { - // We must make sure not to prune blocks that have not yet been included in DA. - daIncludedHeight := s.GetDAIncludedHeight() - - storeHeight, err := s.store.Height(s.ctx) - if err != nil { - s.logger.Error().Err(err).Msg("failed to get store height for pruning") - break - } + // Delete height cache for that height + // This can only be performed after the height has been persisted to store + s.cache.DeleteHeight(nextHeight) + } - upperBound := min(storeHeight, daIncludedHeight) - if upperBound <= s.config.Node.PruningKeepRecent { - // Not enough fully included blocks to prune while respecting keep-recent. - break - } + // Run height-based pruning if enabled. + if s.config.Node.PruningEnabled && s.config.Node.PruningKeepRecent > 0 && s.config.Node.PruningInterval > 0 { + currentDAIncluded = s.GetDAIncludedHeight() - targetHeight := upperBound - s.config.Node.PruningKeepRecent + var lastPruned uint64 + if bz, err := s.store.GetMetadata(s.ctx, store.LastPrunedBlockHeightKey); err == nil && len(bz) == 8 { + lastPruned = binary.LittleEndian.Uint64(bz) + } - if err := s.store.PruneBlocks(s.ctx, targetHeight); err != nil { - s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") - } + storeHeight, err := s.store.Height(s.ctx) + if err != nil { + s.logger.Error().Err(err).Msg("failed to get store height for pruning") + continue + } + if storeHeight <= lastPruned+uint64(s.config.Node.PruningInterval) { + continue + } - if pruner, ok := s.exec.(coreexecutor.ExecPruner); ok { - if err := pruner.PruneExec(s.ctx, targetHeight); err != nil { - s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") - } - } - } + // Never prune blocks that are not DA included + upperBound := min(storeHeight, currentDAIncluded) + if upperBound <= s.config.Node.PruningKeepRecent { + // Not enough fully included blocks to prune + continue } - // Delete height cache for that height - // This can only be performed after the height has been persisted to store - s.cache.DeleteHeight(nextHeight) + targetHeight := upperBound - s.config.Node.PruningKeepRecent + + if err := s.store.PruneBlocks(s.ctx, targetHeight); err != nil { + s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") + } + + if pruner, ok := s.exec.(coreexecutor.ExecPruner); ok { + if err := pruner.PruneExec(s.ctx, targetHeight); err != nil { + s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") + } + } } } } From 4ee40d636a593c217dc54e6b02fa6ca12c0d9d95 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:25:10 +0100 Subject: [PATCH 23/28] add store to store adapter --- block/internal/submitting/submitter.go | 2 +- pkg/store/store_adapter.go | 41 ++++++++------------------ 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 886f1ac0f9..6b1a2971f9 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -379,7 +379,7 @@ func (s *Submitter) processDAInclusionLoop() { s.logger.Error().Err(err).Msg("failed to get store height for pruning") continue } - if storeHeight <= lastPruned+uint64(s.config.Node.PruningInterval) { + if storeHeight <= lastPruned+s.config.Node.PruningInterval { continue } diff --git a/pkg/store/store_adapter.go b/pkg/store/store_adapter.go index d593576f98..479d7d6915 100644 --- a/pkg/store/store_adapter.go +++ b/pkg/store/store_adapter.go @@ -52,12 +52,6 @@ type EntityWithDAHint[H any] interface { DAHint() uint64 } -// lastPrunedHeightGetter is an optional interface that store getters can -// implement to expose the last pruned block height. -type lastPrunedHeightGetter interface { - LastPrunedHeight(ctx context.Context) (uint64, bool) -} - // heightSub provides a mechanism for waiting on a specific height to be stored. // This is critical for go-header syncer which expects GetByHeight to block until // the requested height is available. @@ -292,6 +286,7 @@ func (c *pendingCache[H]) recalcMaxHeight() { // a block, it writes to the underlying store, and subsequent reads will come from the store. type StoreAdapter[H EntityWithDAHint[H]] struct { getter StoreGetter[H] + store Store genesisInitialHeight uint64 // heightSub tracks the current height and allows waiting for specific heights. @@ -410,10 +405,12 @@ func (a *StoreAdapter[H]) Tail(ctx context.Context) (H, error) { // genesis initial height, but if pruning metadata is available we can // skip directly past fully-pruned ranges. startHeight := a.genesisInitialHeight - if getter, ok := a.getter.(lastPrunedHeightGetter); ok { - if lastPruned, ok := getter.LastPrunedHeight(ctx); ok { - if lastPruned < ^uint64(0) { - startHeight = lastPruned + 1 + if a.store != nil { + if meta, err := a.store.GetMetadata(ctx, LastPrunedBlockHeightKey); err == nil && len(meta) == heightLength { + if lastPruned, err := decodeHeight(meta); err == nil { + if candidate := lastPruned + 1; candidate > startHeight { + startHeight = candidate + } } } } @@ -891,22 +888,6 @@ func (g *DataStoreGetter) HasAt(ctx context.Context, height uint64) bool { return err == nil } -// LastPrunedHeight implements lastPrunedHeightGetter for DataStoreGetter by -// reading the pruning metadata from the underlying store. -func (g *DataStoreGetter) LastPrunedHeight(ctx context.Context) (uint64, bool) { - meta, err := g.store.GetMetadata(ctx, LastPrunedBlockHeightKey) - if err != nil || len(meta) != heightLength { - return 0, false - } - - height, err := decodeHeight(meta) - if err != nil { - return 0, false - } - - return height, true -} - // Type aliases for convenience type HeaderStoreAdapter = StoreAdapter[*types.P2PSignedHeader] type DataStoreAdapter = StoreAdapter[*types.P2PData] @@ -914,11 +895,15 @@ type DataStoreAdapter = StoreAdapter[*types.P2PData] // NewHeaderStoreAdapter creates a new StoreAdapter for headers. // The genesis is used to determine the initial height for efficient Tail lookups. func NewHeaderStoreAdapter(store Store, gen genesis.Genesis) *HeaderStoreAdapter { - return NewStoreAdapter(NewHeaderStoreGetter(store), gen) + adapter := NewStoreAdapter(NewHeaderStoreGetter(store), gen) + adapter.store = store + return adapter } // NewDataStoreAdapter creates a new StoreAdapter for data. // The genesis is used to determine the initial height for efficient Tail lookups. func NewDataStoreAdapter(store Store, gen genesis.Genesis) *DataStoreAdapter { - return NewStoreAdapter(NewDataStoreGetter(store), gen) + adapter := NewStoreAdapter(NewDataStoreGetter(store), gen) + adapter.store = store + return adapter } From a4434d1ce40ee2bb7f3739e15cb0fe19b3f9a477 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:57:28 +0100 Subject: [PATCH 24/28] invalidate cache store data after pruning --- pkg/store/cached_store.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/store/cached_store.go b/pkg/store/cached_store.go index e59d4e28c2..3fdabc9e5f 100644 --- a/pkg/store/cached_store.go +++ b/pkg/store/cached_store.go @@ -157,6 +157,19 @@ func (cs *CachedStore) Rollback(ctx context.Context, height uint64, aggregator b return nil } +// PruneBlocks wraps the underlying store's PruneBlocks and invalidates caches. +// After pruning historical block data from disk, any cached entries for pruned +// heights must not be served, so we conservatively clear the entire cache. +func (cs *CachedStore) PruneBlocks(ctx context.Context, height uint64) error { + if err := cs.Store.PruneBlocks(ctx, height); err != nil { + return err + } + + // Invalidate cache for pruned heights + cs.InvalidateRange(1, height) + return nil +} + // Close closes the underlying store. func (cs *CachedStore) Close() error { cs.ClearCache() From 86eb1a207a9e6bdef6994407e81dc20a3d849fe3 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:21:19 +0100 Subject: [PATCH 25/28] nit --- pkg/store/store_adapter.go | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/pkg/store/store_adapter.go b/pkg/store/store_adapter.go index 479d7d6915..7aa08e6a2c 100644 --- a/pkg/store/store_adapter.go +++ b/pkg/store/store_adapter.go @@ -305,9 +305,9 @@ type StoreAdapter[H EntityWithDAHint[H]] struct { onDeleteFn func(context.Context, uint64) error } -// NewStoreAdapter creates a new StoreAdapter wrapping the given store getter. +// NewStoreAdapter creates a new StoreAdapter wrapping the given store getter and backing Store. // The genesis is used to determine the initial height for efficient Tail lookups. -func NewStoreAdapter[H EntityWithDAHint[H]](getter StoreGetter[H], gen genesis.Genesis) *StoreAdapter[H] { +func NewStoreAdapter[H EntityWithDAHint[H]](getter StoreGetter[H], store Store, gen genesis.Genesis) *StoreAdapter[H] { // Get actual current height from store (0 if empty) var storeHeight uint64 if h, err := getter.Height(context.Background()); err == nil { @@ -316,6 +316,7 @@ func NewStoreAdapter[H EntityWithDAHint[H]](getter StoreGetter[H], gen genesis.G adapter := &StoreAdapter[H]{ getter: getter, + store: store, genesisInitialHeight: max(gen.InitialHeight, 1), pending: newPendingCache[H](), heightSub: newHeightSub(storeHeight), @@ -387,7 +388,6 @@ func (a *StoreAdapter[H]) Head(ctx context.Context, _ ...header.HeadOption[H]) ( // Tail returns the lowest item in the store. // For ev-node, this is typically the genesis/initial height. // If pruning has occurred, it walks up from initialHeight to find the first available item. -// TODO(@julienrbrt): Optimize this when pruning is enabled. func (a *StoreAdapter[H]) Tail(ctx context.Context) (H, error) { var zero H @@ -405,12 +405,10 @@ func (a *StoreAdapter[H]) Tail(ctx context.Context) (H, error) { // genesis initial height, but if pruning metadata is available we can // skip directly past fully-pruned ranges. startHeight := a.genesisInitialHeight - if a.store != nil { - if meta, err := a.store.GetMetadata(ctx, LastPrunedBlockHeightKey); err == nil && len(meta) == heightLength { - if lastPruned, err := decodeHeight(meta); err == nil { - if candidate := lastPruned + 1; candidate > startHeight { - startHeight = candidate - } + if meta, err := a.store.GetMetadata(ctx, LastPrunedBlockHeightKey); err == nil && len(meta) == heightLength { + if lastPruned, err := decodeHeight(meta); err == nil { + if candidate := lastPruned + 1; candidate > startHeight { + startHeight = candidate } } } @@ -895,15 +893,11 @@ type DataStoreAdapter = StoreAdapter[*types.P2PData] // NewHeaderStoreAdapter creates a new StoreAdapter for headers. // The genesis is used to determine the initial height for efficient Tail lookups. func NewHeaderStoreAdapter(store Store, gen genesis.Genesis) *HeaderStoreAdapter { - adapter := NewStoreAdapter(NewHeaderStoreGetter(store), gen) - adapter.store = store - return adapter + return NewStoreAdapter(NewHeaderStoreGetter(store), store, gen) } // NewDataStoreAdapter creates a new StoreAdapter for data. // The genesis is used to determine the initial height for efficient Tail lookups. func NewDataStoreAdapter(store Store, gen genesis.Genesis) *DataStoreAdapter { - adapter := NewStoreAdapter(NewDataStoreGetter(store), gen) - adapter.store = store - return adapter + return NewStoreAdapter(NewDataStoreGetter(store), store, gen) } From ca26acdd1aefe7b220eacb75e0ef6eeb8553b3d0 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:01:05 +0100 Subject: [PATCH 26/28] move pruning to it's own function --- block/internal/submitting/submitter.go | 66 ++++++++++++++------------ 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 6b1a2971f9..6a40bd818e 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -366,42 +366,48 @@ func (s *Submitter) processDAInclusionLoop() { } // Run height-based pruning if enabled. - if s.config.Node.PruningEnabled && s.config.Node.PruningKeepRecent > 0 && s.config.Node.PruningInterval > 0 { - currentDAIncluded = s.GetDAIncludedHeight() + s.pruneBlocks() + } + } +} - var lastPruned uint64 - if bz, err := s.store.GetMetadata(s.ctx, store.LastPrunedBlockHeightKey); err == nil && len(bz) == 8 { - lastPruned = binary.LittleEndian.Uint64(bz) - } +func (s *Submitter) pruneBlocks() { + if !s.config.Node.PruningEnabled || s.config.Node.PruningKeepRecent == 0 || s.config.Node.PruningInterval == 0 { + return + } - storeHeight, err := s.store.Height(s.ctx) - if err != nil { - s.logger.Error().Err(err).Msg("failed to get store height for pruning") - continue - } - if storeHeight <= lastPruned+s.config.Node.PruningInterval { - continue - } + currentDAIncluded := s.GetDAIncludedHeight() - // Never prune blocks that are not DA included - upperBound := min(storeHeight, currentDAIncluded) - if upperBound <= s.config.Node.PruningKeepRecent { - // Not enough fully included blocks to prune - continue - } + var lastPruned uint64 + if bz, err := s.store.GetMetadata(s.ctx, store.LastPrunedBlockHeightKey); err == nil && len(bz) == 8 { + lastPruned = binary.LittleEndian.Uint64(bz) + } - targetHeight := upperBound - s.config.Node.PruningKeepRecent + storeHeight, err := s.store.Height(s.ctx) + if err != nil { + s.logger.Error().Err(err).Msg("failed to get store height for pruning") + return + } + if storeHeight <= lastPruned+s.config.Node.PruningInterval { + return + } - if err := s.store.PruneBlocks(s.ctx, targetHeight); err != nil { - s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") - } + // Never prune blocks that are not DA included + upperBound := min(storeHeight, currentDAIncluded) + if upperBound <= s.config.Node.PruningKeepRecent { + // Not enough fully included blocks to prune + return + } - if pruner, ok := s.exec.(coreexecutor.ExecPruner); ok { - if err := pruner.PruneExec(s.ctx, targetHeight); err != nil { - s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") - } - } - } + targetHeight := upperBound - s.config.Node.PruningKeepRecent + + if err := s.store.PruneBlocks(s.ctx, targetHeight); err != nil { + s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data") + } + + if pruner, ok := s.exec.(coreexecutor.ExecPruner); ok { + if err := pruner.PruneExec(s.ctx, targetHeight); err != nil { + s.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune execution metadata") } } } From b1a6a0999d7a62f4d0b6d0f4121c4a6ad8cfab0c Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:05:45 +0100 Subject: [PATCH 27/28] update config.md file --- docs/learn/config.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/learn/config.md b/docs/learn/config.md index ba900a1630..fea509fa5d 100644 --- a/docs/learn/config.md +++ b/docs/learn/config.md @@ -17,6 +17,7 @@ This document provides a comprehensive reference for all configuration options a - [Maximum Pending Blocks](#maximum-pending-blocks) - [Lazy Mode (Lazy Aggregator)](#lazy-mode-lazy-aggregator) - [Lazy Block Interval](#lazy-block-interval) + - [Pruning (Height-Based Pruning)](#pruning-height-based-pruning) - [Data Availability Configuration (`da`)](#data-availability-configuration-da) - [DA Service Address](#da-service-address) - [DA Authentication Token](#da-authentication-token) @@ -279,6 +280,40 @@ _Example:_ `--rollkit.node.lazy_block_interval 1m` _Default:_ `"30s"` _Constant:_ `FlagLazyBlockTime` +### Pruning (Height-Based Pruning) + +**Description:** +Controls height-based pruning of stored block data (headers, data, signatures, and index) from the local store. When pruning is enabled, the node periodically deletes old blocks while keeping a recent window of history. When disabled, the node keeps all blocks (archive mode). + +**YAML:** + +```yaml +node: + pruning_enabled: true + pruning_keep_recent: 100000 + pruning_interval: 1000 +``` + +**Command-line Flags:** + +- `--evnode.node.pruning_enabled` (boolean) + - _Description:_ Enable height-based pruning of stored block data. When disabled, all blocks are kept (archive mode). +- `--evnode.node.pruning_keep_recent ` + - _Description:_ Number of most recent blocks to retain when pruning is enabled. Must be > 0 when pruning is enabled; set `pruning_enabled=false` to keep all blocks. +- `--evnode.node.pruning_interval ` + - _Description:_ Run pruning every N blocks. Must be >= 1 when pruning is enabled. + +_Defaults:_ + +```yaml +node: + pruning_enabled: false + pruning_keep_recent: 0 + pruning_interval: 0 +``` + +_Constants:_ `FlagNodePruningEnabled`, `FlagNodePruningKeepRecent`, `FlagNodePruningInterval` + ## Data Availability Configuration (`da`) Parameters for connecting and interacting with the Data Availability (DA) layer, which Evolve uses to publish block data. From fec4fb443d206dfa256424cb40f1d61a44ddd676 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:50:25 +0100 Subject: [PATCH 28/28] remove ai comments --- pkg/config/config.go | 4 ---- pkg/store/cached_store.go | 5 ++--- pkg/store/types.go | 16 ---------------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index f42bfb666b..b964ac7bf2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -386,13 +386,9 @@ func (c *Config) Validate() error { // Validate pruning configuration if c.Node.PruningEnabled { - // When pruning is enabled, pruning_interval must be >= 1 if c.Node.PruningInterval == 0 { return fmt.Errorf("pruning_interval must be >= 1 when pruning is enabled") } - - // When pruning is enabled, keeping 0 blocks is contradictory; use pruning_enabled=false - // for archive mode instead. if c.Node.PruningKeepRecent == 0 { return fmt.Errorf("pruning_keep_recent must be > 0 when pruning is enabled; use pruning_enabled=false to keep all blocks") } diff --git a/pkg/store/cached_store.go b/pkg/store/cached_store.go index 3fdabc9e5f..8a5cca5b38 100644 --- a/pkg/store/cached_store.go +++ b/pkg/store/cached_store.go @@ -157,9 +157,8 @@ func (cs *CachedStore) Rollback(ctx context.Context, height uint64, aggregator b return nil } -// PruneBlocks wraps the underlying store's PruneBlocks and invalidates caches. -// After pruning historical block data from disk, any cached entries for pruned -// heights must not be served, so we conservatively clear the entire cache. +// PruneBlocks wraps the underlying store's PruneBlocks and invalidates caches +// up to the heigh that we purne func (cs *CachedStore) PruneBlocks(ctx context.Context, height uint64) error { if err := cs.Store.PruneBlocks(ctx, height); err != nil { return err diff --git a/pkg/store/types.go b/pkg/store/types.go index 1162d0075e..a5635feec1 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -30,25 +30,12 @@ type Batch interface { } // Store is minimal interface for storing and retrieving blocks, commits and state. -// -// It is composed from three concerns: -// - Reader: read access to blocks, state, and metadata -// - Rollback: consensus rollback logic (used for chain reorgs / recovery) -// - Pruner: long-term height-based pruning of historical block data type Store interface { Reader Metadata Rollback Pruner - // SetMetadata saves arbitrary value in the store. - // - // This method enables evolve to safely persist any information. - SetMetadata(ctx context.Context, key string, value []byte) error - - // DeleteMetadata removes a metadata key from the store. - DeleteMetadata(ctx context.Context, key string) error - // Close safely closes underlying data storage, to ensure that data is actually saved. Close() error @@ -107,9 +94,6 @@ type Rollback interface { } // Pruner provides long-term, height-based pruning of historical block data. -// -// Implementations SHOULD be idempotent and safe to call multiple times for -// the same or increasing target heights. type Pruner interface { // PruneBlocks removes block data (header, data, signature, and hash index) // up to and including the given height from the store, without modifying