Skip to content

Commit 4f7ded1

Browse files
committed
fix(pruning): use local retention beyond DA inclusion
1 parent 21af7ab commit 4f7ded1

4 files changed

Lines changed: 47 additions & 47 deletions

File tree

‎block/components.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func NewSyncComponents(
186186
if p, ok := exec.(coreexecutor.ExecPruner); ok {
187187
execPruner = p
188188
}
189-
pruner := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration, config.DA.Address)
189+
pruner := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration)
190190

191191
var submitter *submitting.Submitter
192192
if daClient != nil {
@@ -278,7 +278,7 @@ func newAggregatorComponents(
278278
if p, ok := exec.(coreexecutor.ExecPruner); ok {
279279
execPruner = p
280280
}
281-
pruner := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration, config.DA.Address)
281+
pruner := pruner.New(logger, store, execPruner, config.Pruning, config.Node.BlockTime.Duration)
282282

283283
reaper, err := reaping.NewReaper(
284284
exec,

‎block/internal/pruner/pruner.go‎

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ type Pruner struct {
2222
execPruner coreexecutor.ExecPruner
2323
cfg config.PruningConfig
2424
blockTime time.Duration
25-
daEnabled bool
2625
logger zerolog.Logger
2726

2827
// Lifecycle
@@ -38,14 +37,12 @@ func New(
3837
execPruner coreexecutor.ExecPruner,
3938
cfg config.PruningConfig,
4039
blockTime time.Duration,
41-
daAddress string,
4240
) *Pruner {
4341
return &Pruner{
4442
store: store,
4543
execPruner: execPruner,
4644
cfg: cfg,
4745
blockTime: blockTime,
48-
daEnabled: daAddress != "", // DA is enabled if address is provided
4946
logger: logger.With().Str("component", "pruner").Logger(),
5047
}
5148
}
@@ -108,36 +105,20 @@ func (p *Pruner) pruneLoop() {
108105
}
109106
}
110107

111-
// pruneBlocks prunes blocks and their metadatas.
108+
// pruneBlocks prunes blocks and state snapshots outside local retention.
112109
func (p *Pruner) pruneBlocks() error {
113110
storeHeight, err := p.store.Height(p.ctx)
114111
if err != nil {
115112
return fmt.Errorf("failed to get store height for pruning: %w", err)
116113
}
117114

118-
upperBound := storeHeight
119-
120-
// If DA is enabled, only prune blocks that are DA included
121-
if p.daEnabled {
122-
var currentDAIncluded uint64
123-
currentDAIncludedBz, err := p.store.GetMetadata(p.ctx, store.DAIncludedHeightKey)
124-
if err == nil && len(currentDAIncludedBz) == 8 {
125-
currentDAIncluded = binary.LittleEndian.Uint64(currentDAIncludedBz)
126-
} else {
127-
p.logger.Debug().Msg("skipping pruning: DA is enabled but DA included height is not available yet")
128-
return nil
129-
}
130-
131-
// Never prune blocks that are not DA included
132-
upperBound = min(storeHeight, currentDAIncluded)
133-
}
134-
135-
if upperBound <= p.cfg.KeepRecent {
136-
// Not enough fully included blocks to prune
115+
// Retention follows the locally committed head. DA inclusion can remain
116+
// stalled when historical DA heights are no longer served, and must not
117+
// prevent operators from reclaiming local storage.
118+
if storeHeight <= p.cfg.KeepRecent {
137119
return nil
138120
}
139-
140-
targetHeight := upperBound - p.cfg.KeepRecent
121+
targetHeight := storeHeight - p.cfg.KeepRecent
141122

142123
lastBlock, err := p.getLastPrunedBlockHeight(p.ctx)
143124
if err != nil {
@@ -173,7 +154,7 @@ func (p *Pruner) pruneBlocks() error {
173154
start = batchEnd
174155
}
175156

176-
p.logger.Debug().Uint64("pruned_up_to_height", end).Bool("da_enabled", p.daEnabled).Msg("pruned blocks and state snapshots")
157+
p.logger.Debug().Uint64("pruned_up_to_height", end).Msg("pruned blocks and state snapshots")
177158
return nil
178159
}
179160

‎block/internal/pruner/pruner_test.go‎

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestPrunerPruneMetadata(t *testing.T) {
5353
KeepRecent: 1,
5454
}
5555

56-
pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, execAdapter, cfg, 100*time.Millisecond, "") // Empty DA address
56+
pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, execAdapter, cfg, 100*time.Millisecond)
5757
pruner.ctx = ctx
5858
require.NoError(t, pruner.pruneMetadata())
5959

@@ -93,14 +93,14 @@ func TestPrunerPruneBlocksWithoutDA(t *testing.T) {
9393
execAdapter.existing[h] = struct{}{}
9494
}
9595

96-
// Test with empty DA address (DA disabled) - should prune successfully
96+
// Prune by local retention.
9797
cfg := config.PruningConfig{
9898
Mode: config.PruningModeAll,
9999
Interval: config.DurationWrapper{Duration: 1 * time.Second},
100100
KeepRecent: 10,
101101
}
102102

103-
pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, execAdapter, cfg, 100*time.Millisecond, "") // Empty DA address = DA disabled
103+
pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, execAdapter, cfg, 100*time.Millisecond)
104104
pruner.ctx = ctx
105105
require.NoError(t, pruner.pruneBlocks())
106106

@@ -131,7 +131,7 @@ func TestPrunerPruneBlocksWithoutDA(t *testing.T) {
131131
}
132132
}
133133

134-
func TestPrunerPruneBlocksWithDAEnabled(t *testing.T) {
134+
func TestPrunerPruneBlocksWithoutDAInclusionMetadata(t *testing.T) {
135135
t.Parallel()
136136

137137
ctx := t.Context()
@@ -152,23 +152,35 @@ func TestPrunerPruneBlocksWithDAEnabled(t *testing.T) {
152152
require.NoError(t, batch.Commit())
153153
}
154154

155-
// Test with DA address provided (DA enabled) - should skip pruning when DA height is not available
155+
// Missing DA inclusion metadata must not block local retention.
156156
cfg := config.PruningConfig{
157157
Mode: config.PruningModeAll,
158158
Interval: config.DurationWrapper{Duration: 1 * time.Second},
159159
KeepRecent: 10,
160160
}
161161

162-
pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, nil, cfg, 100*time.Millisecond, "localhost:1234") // DA enabled
162+
pruner := New(zerolog.New(zerolog.NewTestWriter(t)), stateStore, nil, cfg, 100*time.Millisecond)
163163
pruner.ctx = ctx
164-
// Should return nil (skip pruning) since DA height is not available
164+
// Process all batches until the local retention target is reached.
165165
require.NoError(t, pruner.pruneBlocks())
166166

167-
// Verify no blocks were pruned (all blocks should still be retrievable)
167+
for range 2 {
168+
require.NoError(t, pruner.pruneBlocks())
169+
}
168170
for h := uint64(1); h <= 100; h++ {
169-
_, _, err := stateStore.GetBlockData(ctx, h)
170-
require.NoError(t, err, "expected block data at height %d to still exist (no pruning should have happened)", h)
171+
_, _, blockErr := stateStore.GetBlockData(ctx, h)
172+
_, stateErr := stateStore.GetStateAtHeight(ctx, h)
173+
if h <= 90 {
174+
require.ErrorIs(t, blockErr, ds.ErrNotFound)
175+
require.ErrorIs(t, stateErr, ds.ErrNotFound)
176+
} else {
177+
require.NoError(t, blockErr)
178+
require.NoError(t, stateErr)
179+
}
171180
}
181+
state, err := stateStore.GetState(ctx)
182+
require.NoError(t, err)
183+
require.Equal(t, uint64(100), state.LastBlockHeight)
172184
}
173185

174186
// recordingStore verifies the batch boundaries without allocating thousands of blocks.
@@ -200,7 +212,7 @@ func TestPrunerCatchupUsesBoundedBatches(t *testing.T) {
200212
p := New(zerolog.Nop(), st, nil, config.PruningConfig{
201213
Mode: config.PruningModeAll, KeepRecent: 10,
202214
Interval: config.DurationWrapper{Duration: 15 * time.Minute},
203-
}, 100*time.Millisecond, "")
215+
}, 100*time.Millisecond)
204216
p.ctx = ctx
205217
require.NoError(t, p.pruneBlocks())
206218
require.Equal(t, []uint64{10000, 20000, 30000, 36000}, st.blockBatches)
@@ -232,7 +244,7 @@ func TestPrunerRecoversSnapshotsAfterBlockOnlyPruning(t *testing.T) {
232244
p := New(zerolog.Nop(), st, nil, config.PruningConfig{
233245
Mode: mode, KeepRecent: 1,
234246
Interval: config.DurationWrapper{Duration: time.Second},
235-
}, 100*time.Millisecond, "")
247+
}, 100*time.Millisecond)
236248
p.ctx = ctx
237249
if mode == config.PruningModeAll {
238250
require.NoError(t, p.pruneBlocks())
@@ -272,22 +284,22 @@ func TestPrunerRetriesExecutionFailure(t *testing.T) {
272284
exec := &failingExecPruner{err: errors.New("execution pruning failed")}
273285
cfg := config.PruningConfig{Mode: config.PruningModeAll, KeepRecent: 1,
274286
Interval: config.DurationWrapper{Duration: time.Second}}
275-
p := New(zerolog.Nop(), st, exec, cfg, 100*time.Millisecond, "")
287+
p := New(zerolog.Nop(), st, exec, cfg, 100*time.Millisecond)
276288
p.ctx = ctx
277289
require.ErrorIs(t, p.pruneBlocks(), exec.err)
278290
_, err = st.GetMetadata(ctx, store.LastPrunedStateHeightKey)
279291
require.ErrorIs(t, err, ds.ErrNotFound)
280292

281293
// A restarted pruner must retry execution despite the advanced block cursor.
282294
exec.err = nil
283-
p = New(zerolog.Nop(), st, exec, cfg, 100*time.Millisecond, "")
295+
p = New(zerolog.Nop(), st, exec, cfg, 100*time.Millisecond)
284296
p.ctx = ctx
285297
require.NoError(t, p.pruneBlocks())
286298
require.Equal(t, []uint64{4, 4}, exec.calls)
287299
require.Equal(t, []uint64{4}, st.blockBatches)
288300
}
289301

290-
func TestPrunerRespectsDAInclusionBoundary(t *testing.T) {
302+
func TestPrunerPrunesBeyondStalledDAInclusion(t *testing.T) {
291303
ctx := t.Context()
292304
st := &recordingStore{Store: store.New(dssync.MutexWrap(ds.NewMapDatastore()))}
293305
batch, err := st.NewBatch(ctx)
@@ -300,9 +312,16 @@ func TestPrunerRespectsDAInclusionBoundary(t *testing.T) {
300312
p := New(zerolog.Nop(), st, nil, config.PruningConfig{
301313
Mode: config.PruningModeAll, KeepRecent: 10,
302314
Interval: config.DurationWrapper{Duration: time.Second},
303-
}, 100*time.Millisecond, "da")
315+
}, 100*time.Millisecond)
304316
p.ctx = ctx
305317
require.NoError(t, p.pruneBlocks())
306-
require.Equal(t, []uint64{10}, st.blockBatches)
307-
require.Equal(t, uint64(10), st.states[len(st.states)-1])
318+
require.Equal(t, []uint64{40}, st.blockBatches)
319+
require.NoError(t, p.pruneBlocks())
320+
require.NoError(t, p.pruneBlocks())
321+
require.Equal(t, []uint64{40, 80, 90}, st.blockBatches)
322+
require.Len(t, st.states, 90)
323+
// Inclusion progress stays at 20; pruning does not pretend DA recovered.
324+
included, err := st.GetMetadata(ctx, store.DAIncludedHeightKey)
325+
require.NoError(t, err)
326+
require.Equal(t, uint64(20), binary.LittleEndian.Uint64(included))
308327
}

‎pkg/config/config.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ const (
382382

383383
// PruningConfig contains all pruning configuration parameters
384384
type PruningConfig struct {
385-
Mode string `mapstructure:"pruning_mode" yaml:"pruning_mode" comment:"Pruning mode for stored block data and block metadata. Options: 'all' (prune all but recent blocks and their metatadas), 'metadata' (prune all but recent blocks metadatas), 'disabled' (keep all blocks and blocks metadata). Default: 'disabled'."`
385+
Mode string `mapstructure:"pruning_mode" yaml:"pruning_mode" comment:"Pruning mode for stored block data and block metadata. Options: 'all' (prune blocks and state snapshots beyond local retention, regardless of DA inclusion), 'metadata' (prune all but recent blocks metadatas), 'disabled' (keep all blocks and blocks metadata). Default: 'disabled'."`
386386
KeepRecent uint64 `mapstructure:"pruning_keep_recent" yaml:"pruning_keep_recent" comment:"Number of most recent blocks/blocks metadata to retain when pruning is enabled. Must be > 0."`
387387
Interval DurationWrapper `mapstructure:"pruning_interval" yaml:"pruning_interval" comment:"Run pruning every N minutes. Examples: \"5m\", \"10m\", \"24h\"."`
388388
}

0 commit comments

Comments
 (0)