Skip to content

Commit 68b4230

Browse files
committed
basic prunning
1 parent f14c6a7 commit 68b4230

File tree

7 files changed

+185
-1
lines changed

7 files changed

+185
-1
lines changed

block/internal/executing/executor.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,23 @@ func (e *Executor) produceBlock() error {
430430
// Update in-memory state after successful commit
431431
e.setLastState(newState)
432432

433+
// Run height-based pruning of stored block data if enabled. This is a
434+
// best-effort background maintenance step and should not cause block
435+
// production to fail, but it does run in the critical path and may add
436+
// some latency when large ranges are pruned.
437+
if e.config.Node.PruningEnabled && e.config.Node.PruningKeepRecent > 0 && e.config.Node.PruningInterval > 0 {
438+
if newHeight%e.config.Node.PruningInterval == 0 {
439+
// Compute the prune floor: all heights <= targetHeight are candidates
440+
// for pruning of header/data/signature/index entries.
441+
if newHeight > e.config.Node.PruningKeepRecent {
442+
targetHeight := newHeight - e.config.Node.PruningKeepRecent
443+
if err := e.store.PruneBlocks(e.ctx, targetHeight); err != nil {
444+
e.logger.Error().Err(err).Uint64("target_height", targetHeight).Msg("failed to prune old block data")
445+
}
446+
}
447+
}
448+
}
449+
433450
// broadcast header and data to P2P network
434451
g, ctx := errgroup.WithContext(e.ctx)
435452
g.Go(func() error { return e.headerBroadcaster.WriteToStoreAndBroadcast(ctx, header) })

pkg/config/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,13 @@ type NodeConfig struct {
210210
// Readiness / health configuration
211211
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."`
212212
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."`
213+
214+
// Pruning configuration
215+
// When enabled, the node will periodically prune old block data (headers, data,
216+
// signatures, and hash index) from the local store while keeping recent history.
217+
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)."`
218+
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."`
219+
PruningInterval uint64 `mapstructure:"pruning_interval" yaml:"pruning_interval" comment:"Run pruning every N blocks. Must be >= 1 when pruning is enabled."`
213220
}
214221

215222
// LogConfig contains all logging configuration parameters
@@ -341,6 +348,10 @@ func AddFlags(cmd *cobra.Command) {
341348
cmd.Flags().Uint64(FlagReadinessWindowSeconds, def.Node.ReadinessWindowSeconds, "time window in seconds for calculating readiness threshold based on block time (default: 15s)")
342349
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)")
343350
cmd.Flags().Duration(FlagScrapeInterval, def.Node.ScrapeInterval.Duration, "interval at which the reaper polls the execution layer for new transactions")
351+
// Pruning configuration flags
352+
cmd.Flags().Bool(FlagPrefixEvnode+"node.pruning_enabled", def.Node.PruningEnabled, "enable height-based pruning of stored block data (headers, data, signatures, index)")
353+
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)")
354+
cmd.Flags().Uint64(FlagPrefixEvnode+"node.pruning_interval", def.Node.PruningInterval, "run pruning every N blocks (must be >= 1 when pruning is enabled)")
344355

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

pkg/config/defaults.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ func DefaultConfig() Config {
6969
ReadinessWindowSeconds: defaultReadinessWindowSeconds,
7070
ReadinessMaxBlocksBehind: calculateReadinessMaxBlocksBehind(defaultBlockTime.Duration, defaultReadinessWindowSeconds),
7171
ScrapeInterval: DurationWrapper{1 * time.Second},
72+
PruningEnabled: false,
73+
PruningKeepRecent: 0,
74+
PruningInterval: 0,
7275
},
7376
DA: DAConfig{
7477
Address: "http://localhost:7980",

pkg/store/keys.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ const (
2525
// LastSubmittedHeaderHeightKey is the key used for persisting the last submitted header height in store.
2626
LastSubmittedHeaderHeightKey = "last-submitted-header-height"
2727

28+
// LastPrunedBlockHeightKey is the metadata key used for persisting the last
29+
// pruned block height in the store. All block data (header, data,
30+
// signature, and hash index) for heights <= this value are considered
31+
// pruned and may be missing from the store.
32+
LastPrunedBlockHeightKey = "last-pruned-block-height"
33+
2834
headerPrefix = "h"
2935
dataPrefix = "d"
3036
signaturePrefix = "c"

pkg/store/store.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,85 @@ func (s *DefaultStore) Rollback(ctx context.Context, height uint64, aggregator b
274274
return nil
275275
}
276276

277+
// PruneBlocks removes block data (header, data, signature, and hash index)
278+
// up to and including the given height from the store. It does not modify
279+
// the current chain height or any state snapshots.
280+
//
281+
// This method is intended for long-term storage reduction and is safe to
282+
// call repeatedly with the same or increasing heights.
283+
func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error {
284+
batch, err := s.db.Batch(ctx)
285+
if err != nil {
286+
return fmt.Errorf("failed to create a new batch for pruning: %w", err)
287+
}
288+
289+
// Track the last successfully pruned height so we can resume across restarts.
290+
var lastPruned uint64
291+
meta, err := s.GetMetadata(ctx, LastPrunedBlockHeightKey)
292+
if err != nil {
293+
if !errors.Is(err, ds.ErrNotFound) {
294+
return fmt.Errorf("failed to get last pruned height: %w", err)
295+
}
296+
} else if len(meta) == heightLength {
297+
lastPruned, err = decodeHeight(meta)
298+
if err != nil {
299+
return fmt.Errorf("failed to decode last pruned height: %w", err)
300+
}
301+
}
302+
303+
// Nothing new to prune.
304+
if height <= lastPruned {
305+
return nil
306+
}
307+
308+
// Delete block data for heights in (lastPruned, height].
309+
for h := lastPruned + 1; h <= height; h++ {
310+
// Get header blob to compute the hash index key. If header is already
311+
// missing (e.g. due to previous partial pruning), just skip this height.
312+
headerBlob, err := s.db.Get(ctx, ds.NewKey(getHeaderKey(h)))
313+
if err != nil {
314+
if errors.Is(err, ds.ErrNotFound) {
315+
continue
316+
}
317+
return fmt.Errorf("failed to get header at height %d during pruning: %w", h, err)
318+
}
319+
320+
if err := batch.Delete(ctx, ds.NewKey(getHeaderKey(h))); err != nil {
321+
return fmt.Errorf("failed to delete header at height %d during pruning: %w", h, err)
322+
}
323+
324+
if err := batch.Delete(ctx, ds.NewKey(getDataKey(h))); err != nil {
325+
if !errors.Is(err, ds.ErrNotFound) {
326+
return fmt.Errorf("failed to delete data at height %d during pruning: %w", h, err)
327+
}
328+
}
329+
330+
if err := batch.Delete(ctx, ds.NewKey(getSignatureKey(h))); err != nil {
331+
if !errors.Is(err, ds.ErrNotFound) {
332+
return fmt.Errorf("failed to delete signature at height %d during pruning: %w", h, err)
333+
}
334+
}
335+
336+
headerHash := sha256.Sum256(headerBlob)
337+
if err := batch.Delete(ctx, ds.NewKey(getIndexKey(headerHash[:]))); err != nil {
338+
if !errors.Is(err, ds.ErrNotFound) {
339+
return fmt.Errorf("failed to delete index for height %d during pruning: %w", h, err)
340+
}
341+
}
342+
}
343+
344+
// Persist the updated last pruned height.
345+
if err := batch.Put(ctx, ds.NewKey(getMetaKey(LastPrunedBlockHeightKey)), encodeHeight(height)); err != nil {
346+
return fmt.Errorf("failed to update last pruned height: %w", err)
347+
}
348+
349+
if err := batch.Commit(ctx); err != nil {
350+
return fmt.Errorf("failed to commit pruning batch: %w", err)
351+
}
352+
353+
return nil
354+
}
355+
277356
const heightLength = 8
278357

279358
func encodeHeight(height uint64) []byte {

pkg/store/store_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,57 @@ func TestRollback(t *testing.T) {
734734
require.Equal(rollbackToHeight, state.LastBlockHeight)
735735
}
736736

737+
func TestPruneBlocks_RemovesOldBlockDataOnly(t *testing.T) {
738+
t.Parallel()
739+
740+
ctx := context.Background()
741+
ds, err := NewTestInMemoryKVStore()
742+
require.NoError(t, err)
743+
744+
s := New(ds).(*DefaultStore)
745+
746+
// create and store a few blocks with headers, data, signatures and state
747+
batch, err := s.NewBatch(ctx)
748+
require.NoError(t, err)
749+
750+
var lastState types.State
751+
for h := uint64(1); h <= 5; h++ {
752+
header := &types.SignedHeader{Header: types.Header{BaseHeader: types.BaseHeader{Height: h}}}
753+
data := &types.Data{}
754+
sig := types.Signature([]byte{byte(h)})
755+
756+
require.NoError(t, batch.SaveBlockData(header, data, &sig))
757+
758+
// fake state snapshot per height
759+
lastState = types.State{LastBlockHeight: h}
760+
require.NoError(t, batch.UpdateState(lastState))
761+
}
762+
require.NoError(t, batch.SetHeight(5))
763+
require.NoError(t, batch.Commit())
764+
765+
// prune everything up to height 3
766+
require.NoError(t, s.PruneBlocks(ctx, 3))
767+
768+
// old block data should be gone
769+
for h := uint64(1); h <= 3; h++ {
770+
_, _, err := s.GetBlockData(ctx, h)
771+
assert.Error(t, err, "expected block data at height %d to be pruned", h)
772+
}
773+
774+
// recent block data should remain
775+
for h := uint64(4); h <= 5; h++ {
776+
_, _, err := s.GetBlockData(ctx, h)
777+
assert.NoError(t, err, "expected block data at height %d to be kept", h)
778+
}
779+
780+
// state snapshots are not pruned by PruneBlocks
781+
for h := uint64(1); h <= 5; h++ {
782+
st, err := s.GetStateAtHeight(ctx, h)
783+
assert.NoError(t, err, "expected state at height %d to remain", h)
784+
assert.Equal(t, h, st.LastBlockHeight)
785+
}
786+
}
787+
737788
// TestRollbackToSameHeight verifies that rollback to same height is a no-op
738789
func TestRollbackToSameHeight(t *testing.T) {
739790
t.Parallel()

pkg/store/types.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ type Batch interface {
3030
}
3131

3232
// Store is minimal interface for storing and retrieving blocks, commits and state.
33+
//
34+
// It is composed from three concerns:
35+
// - Reader: read access to blocks, state, and metadata
36+
// - Rollback: consensus rollback logic (used for chain reorgs / recovery)
37+
// - Pruner: long-term height-based pruning of historical block data
3338
type Store interface {
34-
Rollback
3539
Reader
40+
Rollback
41+
Pruner
3642

3743
// SetMetadata saves arbitrary value in the store.
3844
//
@@ -75,3 +81,14 @@ type Rollback interface {
7581
// Aggregator is used to determine if the rollback is performed on the aggregator node.
7682
Rollback(ctx context.Context, height uint64, aggregator bool) error
7783
}
84+
85+
// Pruner provides long-term, height-based pruning of historical block data.
86+
//
87+
// Implementations SHOULD be idempotent and safe to call multiple times for
88+
// the same or increasing target heights.
89+
type Pruner interface {
90+
// PruneBlocks removes block data (header, data, signature, and hash index)
91+
// up to and including the given height from the store, without modifying
92+
// state snapshots or the current chain height.
93+
PruneBlocks(ctx context.Context, height uint64) error
94+
}

0 commit comments

Comments
 (0)