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 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)