From af02a2e8eaf863154022b96074c80f70b87f5496 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Mon, 12 Oct 2026 17:10:56 +0200 Subject: [PATCH 1/2] fix(pruner): prune state snapshots in all mode and stop pruning from falling behind - Pruning mode "all" deleted block data but never the per-height state snapshots (/s/), so state grew without bound. Block pruning now also prunes states, keeping the state at the last pruned height so the oldest retained block can still be replayed. - Add Store.PruneStates, which deletes states and records LastPrunedStateHeightKey in a single batch (mirrors PruneBlocks), instead of one write per height. - Metadata mode no longer jumps its state marker to the last pruned block height, which skipped states that were never deleted. - Metadata mode keeps the state for the oldest retained block (previously it deleted it, leaving keep_recent-1 replayable blocks). - Replace the hard 10k per-tick cap with a budget of 4x blocks per interval (min 10k), executed in store batches of at most 10k heights. With the cap, pruning fell behind permanently whenever more than 10k blocks were produced per interval. --- block/internal/pruner/pruner.go | 121 +++++++++++++++------------ block/internal/pruner/pruner_test.go | 117 +++++++++++++++++++++++--- pkg/store/store.go | 69 ++++++++++++--- pkg/store/store_test.go | 46 ++++++++++ pkg/store/tracing.go | 15 ++++ pkg/store/tracing_test.go | 4 + pkg/store/types.go | 3 + test/mocks/store.go | 57 +++++++++++++ 8 files changed, 359 insertions(+), 73 deletions(-) diff --git a/block/internal/pruner/pruner.go b/block/internal/pruner/pruner.go index e525ca8a6a..f78fae02b4 100644 --- a/block/internal/pruner/pruner.go +++ b/block/internal/pruner/pruner.go @@ -139,44 +139,66 @@ func (p *Pruner) pruneBlocks() error { targetHeight := upperBound - p.cfg.KeepRecent - // Get the last pruned height to determine batch size lastPruned, err := p.getLastPrunedBlockHeight(p.ctx) if err != nil { return fmt.Errorf("failed to get last pruned block height: %w", err) } - // prune in batches to avoid overwhelming the system - batchSize := p.calculateBatchSize() - batchEnd := min(lastPruned+batchSize, targetHeight) + // Bound the work done per tick; a backlog drains over several ticks. + blocksEnd := max(min(lastPruned+p.pruneBudget(), targetHeight), lastPruned) + if err := p.pruneInChunks(lastPruned, blocksEnd, p.store.PruneBlocks); err != nil { + p.logger.Error().Err(err).Uint64("target_height", blocksEnd).Msg("failed to prune old block data") + return err + } + + if blocksEnd == 0 { + return nil + } - if err := p.store.PruneBlocks(p.ctx, batchEnd); err != nil { - p.logger.Error().Err(err).Uint64("target_height", batchEnd).Msg("failed to prune old block data") + // PruneBlocks leaves per-height state snapshots in place. Keep the state at + // blocksEnd: replaying the first retained block (blocksEnd+1) needs it. + if _, err := p.pruneStates(blocksEnd - 1); err != nil { return err } if p.execPruner != nil { - if err := p.execPruner.PruneExec(p.ctx, batchEnd); err != nil && !errors.Is(err, ds.ErrNotFound) { + if err := p.execPruner.PruneExec(p.ctx, blocksEnd); err != nil && !errors.Is(err, ds.ErrNotFound) { return err } } - p.logger.Debug().Uint64("pruned_up_to_height", batchEnd).Bool("da_enabled", p.daEnabled).Msg("pruned blocks up to height") + p.logger.Debug().Uint64("pruned_up_to_height", blocksEnd).Bool("da_enabled", p.daEnabled).Msg("pruned blocks up to height") return nil } -// calculateBatchSize returns the appropriate batch size for pruning operations. -// The batch size is based on the pruning interval and block time to ensure reasonable progress -// without overwhelming the node. -func (p *Pruner) calculateBatchSize() uint64 { - // Calculate batch size based on pruning interval and block time. - // We use 4x the blocks produced during one pruning interval as the batch size. - // This ensures we catch up at 3x the block production rate when there's a backlog. - // Example: With 100ms blocks and 15min interval: 15*60/0.1 = 9000 blocks/interval - // - Batch size: 36,000 blocks (prunes 36k, chain grows 9k = net 27k catch-up per interval) +// maxPruneChunk bounds the number of heights pruned in a single store batch, +// keeping individual write transactions small. +const maxPruneChunk uint64 = 10_000 + +// pruneBudget returns the maximum number of heights to prune per tick. +// It is 4x the blocks produced during one pruning interval, so a backlog drains +// at ~3x the block production rate and pruning never falls behind the chain. +// Example: 100ms blocks and a 15m interval produce 9,000 blocks per interval, +// giving a budget of 36,000 heights (net catch-up of 27,000 per interval). +func (p *Pruner) pruneBudget() uint64 { blocksPerInterval := uint64(p.cfg.Interval.Duration / p.blockTime) + return max(blocksPerInterval*4, maxPruneChunk) +} - // Ensure reasonable minimum - return min(blocksPerInterval*4, 10000) +// pruneInChunks calls prune for successive heights in (from, to], at most +// maxPruneChunk heights per call. The loop is bounded by (to-from)/maxPruneChunk. +func (p *Pruner) pruneInChunks(from, to uint64, prune func(context.Context, uint64) error) error { + for from < to { + if err := p.ctx.Err(); err != nil { + return err + } + end := min(from+maxPruneChunk, to) + if err := prune(p.ctx, end); err != nil { + return err + } + from = end + } + return nil } // pruneMetadata prunes old state and execution metadata entries based on the configured retention depth. @@ -188,47 +210,46 @@ func (p *Pruner) pruneMetadata() error { return err } - if height <= p.cfg.KeepRecent { + // Blocks in (target, height] stay replayable; the oldest one, target+1, + // needs the state at target, so only states below target are pruned. + if height <= p.cfg.KeepRecent+1 { return nil } + target := height - p.cfg.KeepRecent - lastPrunedState, err := p.getLastPrunedStateHeight(p.ctx) - if err != nil { - return fmt.Errorf("failed to get last pruned state height: %w", err) - } - - if lastPrunedBlock, err := p.getLastPrunedBlockHeight(p.ctx); err == nil && lastPrunedBlock > lastPrunedState { - lastPrunedState = lastPrunedBlock + statesEnd, err := p.pruneStates(target - 1) + if err != nil || statesEnd == 0 { + return err } - target := height - p.cfg.KeepRecent - if target <= lastPrunedState { - return nil + if p.execPruner != nil { + if err := p.execPruner.PruneExec(p.ctx, statesEnd); err != nil && !errors.Is(err, ds.ErrNotFound) { + return err + } } - batchSize := p.calculateBatchSize() - - // prune in batches to avoid overwhelming the system - batchEnd := min(lastPrunedState+batchSize, target) + return nil +} - for h := lastPrunedState + 1; h <= batchEnd; h++ { - if err := p.store.DeleteStateAtHeight(p.ctx, h); err != nil && !errors.Is(err, ds.ErrNotFound) { - return err - } +// pruneStates deletes per-height state snapshots in (lastPrunedState, upTo], +// bounded by pruneBudget per call. It returns the last pruned state height. +func (p *Pruner) pruneStates(upTo uint64) (uint64, error) { + lastPrunedState, err := p.getLastPrunedStateHeight(p.ctx) + if err != nil { + return 0, fmt.Errorf("failed to get last pruned state height: %w", err) } - if p.execPruner != nil { - if err := p.execPruner.PruneExec(p.ctx, batchEnd); err != nil && !errors.Is(err, ds.ErrNotFound) { - return err - } + if upTo <= lastPrunedState { + return lastPrunedState, nil } - if err := p.setLastPrunedStateHeight(p.ctx, batchEnd); err != nil { - return fmt.Errorf("failed to set last pruned block height: %w", err) + statesEnd := min(lastPrunedState+p.pruneBudget(), upTo) + if err := p.pruneInChunks(lastPrunedState, statesEnd, p.store.PruneStates); err != nil { + return 0, fmt.Errorf("failed to prune state snapshots: %w", err) } - p.logger.Debug().Uint64("pruned_to", batchEnd).Msg("pruned state height metadata up to height") - return nil + p.logger.Debug().Uint64("pruned_to", statesEnd).Msg("pruned state snapshots up to height") + return statesEnd, nil } // getLastPrunedBlockHeight returns the height of the last block that was pruned using PruneBlocks. @@ -251,7 +272,7 @@ func (p *Pruner) getLastPrunedBlockHeight(ctx context.Context) (uint64, error) { return lastPrunedBlockHeight, nil } -// getLastPrunedStateHeight returns the height of the last state that was pruned using DeleteStateAtHeight. +// getLastPrunedStateHeight returns the height of the last state that was pruned using PruneStates. func (p *Pruner) getLastPrunedStateHeight(ctx context.Context) (uint64, error) { lastPrunedStateHeightBz, err := p.store.GetMetadata(ctx, store.LastPrunedStateHeightKey) if errors.Is(err, ds.ErrNotFound) { @@ -270,9 +291,3 @@ func (p *Pruner) getLastPrunedStateHeight(ctx context.Context) (uint64, error) { return lastPrunedStateHeight, nil } - -func (p *Pruner) setLastPrunedStateHeight(ctx context.Context, height uint64) error { - bz := make([]byte, 8) - binary.LittleEndian.PutUint64(bz, height) - return p.store.SetMetadata(ctx, store.LastPrunedStateHeightKey, bz) -} diff --git a/block/internal/pruner/pruner_test.go b/block/internal/pruner/pruner_test.go index 8f0ad4773e..c87e0f8911 100644 --- a/block/internal/pruner/pruner_test.go +++ b/block/internal/pruner/pruner_test.go @@ -52,11 +52,16 @@ func TestPrunerPruneMetadata(t *testing.T) { } pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, execAdapter, cfg, 100*time.Millisecond, "") // Empty DA address + pruner.ctx = ctx require.NoError(t, pruner.pruneMetadata()) - _, err := stateStore.GetStateAtHeight(ctx, 1) + _, err := stateStore.GetStateAtHeight(ctx, 3) require.ErrorIs(t, err, ds.ErrNotFound) + // State 4 is kept so the oldest retained block (5) can be replayed. + _, err = stateStore.GetStateAtHeight(ctx, 4) + require.NoError(t, err) + _, err = stateStore.GetStateAtHeight(ctx, 5) require.NoError(t, err) @@ -98,31 +103,122 @@ func TestPrunerPruneBlocksWithoutDA(t *testing.T) { } pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, execAdapter, cfg, 100*time.Millisecond, "") // Empty DA address = DA disabled + pruner.ctx = ctx require.NoError(t, pruner.pruneBlocks()) - // Verify blocks were pruned (batch size is 40 blocks: 1s interval / 100ms block time * 4) - // So we expect to prune from height 1 up to min(0 + 40, 90) = 40 + // The per-tick budget (at least maxPruneChunk) covers the whole range, + // so everything up to height - KeepRecent = 90 is pruned in one tick. height, err := stateStore.Height(ctx) require.NoError(t, err) require.Equal(t, uint64(100), height) - // Verify old blocks were pruned (up to height 40) - for h := uint64(1); h <= 40; h++ { + for h := uint64(1); h <= 90; h++ { _, _, err := stateStore.GetBlockData(ctx, h) require.Error(t, err, "expected block data at height %d to be pruned", h) } - - // Verify blocks after batch were kept - for h := uint64(41); h <= 100; h++ { + for h := uint64(91); h <= 100; h++ { _, _, err := stateStore.GetBlockData(ctx, h) require.NoError(t, err, "expected block data at height %d to be kept", h) } - // Verify exec metadata was also pruned (strictly less than 40) - for h := uint64(1); h < 40; h++ { + // Verify exec metadata was also pruned (strictly less than 90) + for h := uint64(1); h < 90; h++ { _, exists := execAdapter.existing[h] require.False(t, exists, "expected exec metadata at height %d to be pruned", h) } + + // Verify state snapshots were pruned below 90; state 90 is kept to replay block 91. + for h := uint64(1); h < 90; h++ { + _, err := stateStore.GetStateAtHeight(ctx, h) + require.ErrorIs(t, err, ds.ErrNotFound, "expected state at height %d to be pruned", h) + } + for h := uint64(90); h <= 100; h++ { + _, err := stateStore.GetStateAtHeight(ctx, h) + require.NoError(t, err, "expected state at height %d to be kept", h) + } +} + +func TestPrunerPruneMetadataAfterBlockPruning(t *testing.T) { + t.Parallel() + + ctx := t.Context() + kv := dssync.MutexWrap(ds.NewMapDatastore()) + stateStore := store.New(kv) + + for height := uint64(1); height <= 20; height++ { + batch, err := stateStore.NewBatch(ctx) + require.NoError(t, err) + require.NoError(t, batch.SetHeight(height)) + require.NoError(t, batch.UpdateState(types.State{LastBlockHeight: height})) + require.NoError(t, batch.Commit()) + } + + // Simulate a node that previously pruned blocks without pruning states. + require.NoError(t, stateStore.SetMetadata(ctx, store.LastPrunedBlockHeightKey, []byte{10, 0, 0, 0, 0, 0, 0, 0})) + + cfg := config.PruningConfig{ + Mode: config.PruningModeMetadata, + Interval: config.DurationWrapper{Duration: 1 * time.Second}, + KeepRecent: 5, + } + + pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, nil, cfg, 100*time.Millisecond, "") + pruner.ctx = ctx + require.NoError(t, pruner.pruneMetadata()) + + for h := uint64(1); h <= 14; h++ { + _, err := stateStore.GetStateAtHeight(ctx, h) + require.ErrorIs(t, err, ds.ErrNotFound, "expected state at height %d to be pruned", h) + } + for h := uint64(15); h <= 20; h++ { + _, err := stateStore.GetStateAtHeight(ctx, h) + require.NoError(t, err, "expected state at height %d to be kept", h) + } +} + +func TestPrunerPruneStatesBudgetAndChunks(t *testing.T) { + t.Parallel() + + ctx := t.Context() + kv := dssync.MutexWrap(ds.NewMapDatastore()) + stateStore := store.New(kv) + + const total = 30_000 + batch, err := stateStore.NewBatch(ctx) + require.NoError(t, err) + for height := uint64(1); height <= total; height++ { + require.NoError(t, batch.UpdateState(types.State{LastBlockHeight: height})) + } + require.NoError(t, batch.SetHeight(total)) + require.NoError(t, batch.Commit()) + + // 1ms blocks with a 6s interval: 6,000 blocks per interval, budget 24,000, + // so the first tick spans three store chunks (10k, 10k, 4k). + cfg := config.PruningConfig{ + Mode: config.PruningModeMetadata, + Interval: config.DurationWrapper{Duration: 6 * time.Second}, + KeepRecent: 1, + } + pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, nil, cfg, time.Millisecond, "") + pruner.ctx = ctx + require.Equal(t, uint64(24_000), pruner.pruneBudget()) + + require.NoError(t, pruner.pruneMetadata()) + lastPruned, err := pruner.getLastPrunedStateHeight(ctx) + require.NoError(t, err) + require.Equal(t, uint64(24_000), lastPruned) + _, err = stateStore.GetStateAtHeight(ctx, 24_000) + require.ErrorIs(t, err, ds.ErrNotFound) + _, err = stateStore.GetStateAtHeight(ctx, 24_001) + require.NoError(t, err) + + // Second tick drains the rest, keeping the state for the oldest retained block. + require.NoError(t, pruner.pruneMetadata()) + lastPruned, err = pruner.getLastPrunedStateHeight(ctx) + require.NoError(t, err) + require.Equal(t, uint64(total-2), lastPruned) + _, err = stateStore.GetStateAtHeight(ctx, total-1) + require.NoError(t, err) } func TestPrunerPruneBlocksWithDAEnabled(t *testing.T) { @@ -154,6 +250,7 @@ func TestPrunerPruneBlocksWithDAEnabled(t *testing.T) { } pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, nil, cfg, 100*time.Millisecond, "localhost:1234") // DA enabled + pruner.ctx = ctx // Should return nil (skip pruning) since DA height is not available require.NoError(t, pruner.pruneBlocks()) diff --git a/pkg/store/store.go b/pkg/store/store.go index 4c045d1d89..0cf5b734f8 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -342,17 +342,9 @@ func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error { } // Track the last successfully pruned height so we can resume across restarts. - var lastPruned uint64 - meta, err := s.GetMetadata(ctx, LastPrunedBlockHeightKey) + lastPruned, err := s.getLastPrunedHeight(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) - } + return err } // Nothing new to prune. @@ -422,6 +414,63 @@ func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error { return nil } +// PruneStates removes per-height state snapshots up to and including the +// given height. Progress is recorded under LastPrunedStateHeightKey in the +// same batch, so the call is atomic and safe to repeat with the same or +// increasing heights. It does not modify block data or the current height. +func (s *DefaultStore) PruneStates(ctx context.Context, height uint64) error { + lastPruned, err := s.getLastPrunedHeight(ctx, LastPrunedStateHeightKey) + if err != nil { + return err + } + + if height <= lastPruned { + return nil + } + + batch, err := s.db.Batch(ctx) + if err != nil { + return fmt.Errorf("failed to create a new batch for state pruning: %w", err) + } + + for h := lastPruned + 1; h <= height; h++ { + if err := batch.Delete(ctx, ds.NewKey(getStateAtHeightKey(h))); err != nil && !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("failed to delete state at height %d during pruning: %w", h, err) + } + } + + if err := batch.Put(ctx, ds.NewKey(GetMetaKey(LastPrunedStateHeightKey)), encodeHeight(height)); err != nil { + return fmt.Errorf("failed to update last pruned state height: %w", err) + } + + if err := batch.Commit(ctx); err != nil { + return fmt.Errorf("failed to commit state pruning batch: %w", err) + } + + return nil +} + +// getLastPrunedHeight reads a last-pruned height marker, returning 0 if unset. +func (s *DefaultStore) getLastPrunedHeight(ctx context.Context, key string) (uint64, error) { + meta, err := s.GetMetadata(ctx, key) + if err != nil { + if errors.Is(err, ds.ErrNotFound) { + return 0, nil + } + return 0, fmt.Errorf("failed to get last pruned height %q: %w", key, err) + } + + if len(meta) != heightLength { + return 0, nil + } + + lastPruned, err := decodeHeight(meta) + if err != nil { + return 0, fmt.Errorf("failed to decode last pruned height %q: %w", key, err) + } + return lastPruned, nil +} + const heightLength = 8 func encodeHeight(height uint64) []byte { diff --git a/pkg/store/store_test.go b/pkg/store/store_test.go index 68903e1c7c..8603f16f93 100644 --- a/pkg/store/store_test.go +++ b/pkg/store/store_test.go @@ -557,6 +557,52 @@ func TestDeleteStateAtHeight(t *testing.T) { require.ErrorIs(err, ds.ErrNotFound) } +func TestPruneStates(t *testing.T) { + t.Parallel() + require := require.New(t) + ctx := t.Context() + + kv, err := NewTestInMemoryKVStore() + require.NoError(err) + store := New(kv) + + for h := uint64(1); h <= 10; h++ { + batch, err := store.NewBatch(ctx) + require.NoError(err) + require.NoError(batch.SetHeight(h)) + require.NoError(batch.UpdateState(types.State{LastBlockHeight: h})) + require.NoError(batch.Commit()) + } + + require.NoError(store.PruneStates(ctx, 4)) + // Lower or equal heights are no-ops. + require.NoError(store.PruneStates(ctx, 2)) + require.NoError(store.PruneStates(ctx, 4)) + // Resumes from the recorded marker. + require.NoError(store.PruneStates(ctx, 6)) + + for h := uint64(1); h <= 6; h++ { + _, err := store.GetStateAtHeight(ctx, h) + require.ErrorIs(err, ds.ErrNotFound, "state at height %d should be pruned", h) + } + for h := uint64(7); h <= 10; h++ { + _, err := store.GetStateAtHeight(ctx, h) + require.NoError(err, "state at height %d should be kept", h) + } + + meta, err := store.GetMetadata(ctx, LastPrunedStateHeightKey) + require.NoError(err) + require.Equal(uint64(6), binary.LittleEndian.Uint64(meta)) + + // Block height and current state are untouched. + height, err := store.Height(ctx) + require.NoError(err) + require.Equal(uint64(10), height) + state, err := store.GetState(ctx) + require.NoError(err) + require.Equal(uint64(10), state.LastBlockHeight) +} + func TestGetStateError(t *testing.T) { t.Parallel() require := require.New(t) diff --git a/pkg/store/tracing.go b/pkg/store/tracing.go index e94de6dad1..154918fd64 100644 --- a/pkg/store/tracing.go +++ b/pkg/store/tracing.go @@ -230,6 +230,21 @@ func (t *tracedStore) BatchMetadata(ctx context.Context, puts []MetadataKV, dele return nil } +func (t *tracedStore) PruneStates(ctx context.Context, height uint64) error { + ctx, span := t.tracer.Start(ctx, "Store.PruneStates", + trace.WithAttributes(attribute.Int64("height", int64(height))), + ) + defer span.End() + + if err := t.inner.PruneStates(ctx, height); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + + return nil +} + func (t *tracedStore) DeleteStateAtHeight(ctx context.Context, height uint64) error { ctx, span := t.tracer.Start(ctx, "Store.DeleteStateAtHeight", trace.WithAttributes(attribute.Int64("height", int64(height))), diff --git a/pkg/store/tracing_test.go b/pkg/store/tracing_test.go index 66211a2624..165db1cf23 100644 --- a/pkg/store/tracing_test.go +++ b/pkg/store/tracing_test.go @@ -134,6 +134,10 @@ func (m *tracingMockStore) PruneBlocks(ctx context.Context, height uint64) error return nil } +func (m *tracingMockStore) PruneStates(ctx context.Context, height uint64) error { + return nil +} + func (m *tracingMockStore) DeleteStateAtHeight(ctx context.Context, height uint64) error { if m.deleteStateAtHeightFn != nil { return m.deleteStateAtHeightFn(ctx, height) diff --git a/pkg/store/types.go b/pkg/store/types.go index d37b5debf9..660ae5d636 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -110,6 +110,9 @@ type Pruner interface { // 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 + // PruneStates removes per-height state snapshots up to and including the + // given height, tracking progress so repeated calls resume where they left off. + PruneStates(ctx context.Context, height uint64) error // DeleteStateAtHeight removes the state at the given height from the store. // It does not affect the current state or any states at other heights, allowing for targeted pruning of historical state snapshots. DeleteStateAtHeight(ctx context.Context, height uint64) error diff --git a/test/mocks/store.go b/test/mocks/store.go index 75024718a5..100c228acb 100644 --- a/test/mocks/store.go +++ b/test/mocks/store.go @@ -989,6 +989,63 @@ func (_c *MockStore_PruneBlocks_Call) RunAndReturn(run func(ctx context.Context, return _c } +// PruneStates provides a mock function for the type MockStore +func (_mock *MockStore) PruneStates(ctx context.Context, height uint64) error { + ret := _mock.Called(ctx, height) + + if len(ret) == 0 { + panic("no return value specified for PruneStates") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uint64) error); ok { + r0 = returnFunc(ctx, height) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockStore_PruneStates_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneStates' +type MockStore_PruneStates_Call struct { + *mock.Call +} + +// PruneStates is a helper method to define mock.On call +// - ctx context.Context +// - height uint64 +func (_e *MockStore_Expecter) PruneStates(ctx interface{}, height interface{}) *MockStore_PruneStates_Call { + return &MockStore_PruneStates_Call{Call: _e.mock.On("PruneStates", ctx, height)} +} + +func (_c *MockStore_PruneStates_Call) Run(run func(ctx context.Context, height uint64)) *MockStore_PruneStates_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uint64 + if args[1] != nil { + arg1 = args[1].(uint64) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockStore_PruneStates_Call) Return(err error) *MockStore_PruneStates_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockStore_PruneStates_Call) RunAndReturn(run func(ctx context.Context, height uint64) error) *MockStore_PruneStates_Call { + _c.Call.Return(run) + return _c +} + // Rollback provides a mock function for the type MockStore func (_mock *MockStore) Rollback(ctx context.Context, height uint64, aggregator bool) error { ret := _mock.Called(ctx, height, aggregator) From 14ed4262b7265c8cad76f0e858bd98a14466c6d8 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Mon, 12 Oct 2026 17:11:29 +0200 Subject: [PATCH 2/2] chore: add changelog entry for #3469 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 806700a37b..bad7904823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Prune per-height state snapshots in `all` pruning mode, keep the state needed to replay the oldest retained block, and stop pruning from falling behind when more than 10k blocks are produced per interval [#3469](https://github.com/evstack/ev-node/pull/3469). + ## v1.2.3 ### Changed