Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pkg/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ type Adapter struct {
stackedEvents []StackedEvent
}

// PruneExecMeta implements execution.ExecMetaPruner for the ABCI adapter by
// delegating to the underlying ev-abci exec store. It prunes per-height ABCI
// execution metadata (block IDs and block responses) up to the given height.
// The method is safe to call multiple times with the same or increasing
// heights.
func (a *Adapter) PruneExecMeta(ctx context.Context, height uint64) error {
if a.Store == nil {
return nil
}

return a.Store.Prune(ctx, height)
}

// NewABCIExecutor creates a new Adapter instance that implements the go-execution.Executor interface.
// The Adapter wraps the provided ABCI application and delegates execution-related operations to it.
func NewABCIExecutor(
Expand Down
70 changes: 70 additions & 0 deletions pkg/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package store

import (
"context"
"errors"
"fmt"
"strconv"

Expand All @@ -24,6 +25,10 @@ const (
blockResponseKey = "br"
// blockIDKey is the key used for storing block IDs
blockIDKey = "bid"
// lastPrunedHeightKey tracks the highest height that has been pruned.
// This makes pruning idempotent and allows incremental pruning across
// multiple calls.
lastPrunedHeightKey = "lph"
)

// Store wraps a datastore with ABCI-specific functionality
Expand Down Expand Up @@ -147,3 +152,68 @@ func (s *Store) GetBlockResponse(ctx context.Context, height uint64) (*abci.Resp

return resp, nil
}

// Prune deletes per-height ABCI execution metadata (block IDs and block
// responses) for all heights up to and including the provided target
// height. The current ABCI state (stored under stateKey) is never pruned,
// as it is maintained separately by the application.
//
// Pruning is idempotent: the store tracks the highest pruned height and
// will skip work for already-pruned ranges.
func (s *Store) Prune(ctx context.Context, height uint64) error {
// Load the last pruned height, if any.
data, err := s.prefixedStore.Get(ctx, ds.NewKey(lastPrunedHeightKey))
if err != nil {
if !errors.Is(err, ds.ErrNotFound) {
return fmt.Errorf("failed to get last pruned height: %w", err)
}
}

var lastPruned uint64
if len(data) > 0 {
lastPruned, err = strconv.ParseUint(string(data), 10, 64)
if err != nil {
return fmt.Errorf("invalid last pruned height value %q: %w", string(data), err)
}
}

// Nothing to do if we've already pruned up to at least this height.
if height <= lastPruned {
return nil
}

// Use a batch to atomically delete all per-height ABCI metadata and
// update the last pruned height in a single transaction. This avoids
// leaving the store in a partially pruned state if an error occurs
// midway through the operation.
batch, err := s.prefixedStore.Batch(ctx)
if err != nil {
return fmt.Errorf("failed to create batch for pruning: %w", err)
}

// Delete per-height ABCI metadata (block IDs and block responses) for
// heights in (lastPruned, height]. Missing keys are ignored.
for h := lastPruned + 1; h <= height; h++ {
hStr := strconv.FormatUint(h, 10)
bidKey := ds.NewKey(blockIDKey).ChildString(hStr)
if err := batch.Delete(ctx, bidKey); err != nil && !errors.Is(err, ds.ErrNotFound) {
return fmt.Errorf("failed to add block ID deletion to batch at height %d: %w", h, err)
}

brKey := ds.NewKey(blockResponseKey).ChildString(hStr)
if err := batch.Delete(ctx, brKey); err != nil && !errors.Is(err, ds.ErrNotFound) {
return fmt.Errorf("failed to add block response deletion to batch at height %d: %w", h, err)
}
}

// Persist the updated last pruned height in the same batch.
if err := batch.Put(ctx, ds.NewKey(lastPrunedHeightKey), []byte(strconv.FormatUint(height, 10))); err != nil {
return fmt.Errorf("failed to add last pruned height update to batch: %w", err)
}

if err := batch.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit pruning batch: %w", err)
}

return nil
}
53 changes: 53 additions & 0 deletions pkg/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,56 @@ func TestStateIO(t *testing.T) {
require.NoError(t, gotErr)
assert.True(t, exists)
}

func TestPrune_RemovesPerHeightABCIKeysUpToTarget(t *testing.T) {
db := ds.NewMapDatastore()
absciStore := store.NewExecABCIStore(db)

ctx := t.Context()

// Seed per-height block ID and block response keys for heights 1..5.
for h := 1; h <= 5; h++ {
heightKey := ds.NewKey("/abci/bid").ChildString(string(rune(h + '0')))
require.NoError(t, db.Put(ctx, heightKey, []byte("bid")))

respKey := ds.NewKey("/abci/br").ChildString(string(rune(h + '0')))
require.NoError(t, db.Put(ctx, respKey, []byte("br")))
Comment on lines +34 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The key generation in this test uses string(rune(h + '0')) to convert the height h to a string. This is fragile as it only works for single-digit heights (0-9). The production code uses strconv.FormatUint, which is more robust. To make the test more reliable and align with the production code, you should use strconv.Itoa(h) for key generation. This applies to all key constructions in this test function.

Suggested change
heightKey := ds.NewKey("/abci/bid").ChildString(string(rune(h + '0')))
require.NoError(t, db.Put(ctx, heightKey, []byte("bid")))
respKey := ds.NewKey("/abci/br").ChildString(string(rune(h + '0')))
require.NoError(t, db.Put(ctx, respKey, []byte("br")))
heightKey := ds.NewKey("/abci/bid").ChildString(strconv.Itoa(h))
require.NoError(t, db.Put(ctx, heightKey, []byte("bid")))
respKey := ds.NewKey("/abci/br").ChildString(strconv.Itoa(h))
require.NoError(t, db.Put(ctx, respKey, []byte("br")))

}

// Seed state to ensure it is not affected by pruning.
require.NoError(t, db.Put(ctx, ds.NewKey("/abci/s"), []byte("state")))

// Prune up to height 3.
require.NoError(t, absciStore.Prune(ctx, 3))

// Heights 1..3 should be deleted.
for h := 1; h <= 3; h++ {
bidKey := ds.NewKey("/abci/bid").ChildString(string(rune(h + '0')))
exists, err := db.Has(ctx, bidKey)
require.NoError(t, err)
assert.False(t, exists)

respKey := ds.NewKey("/abci/br").ChildString(string(rune(h + '0')))
exists, err = db.Has(ctx, respKey)
require.NoError(t, err)
assert.False(t, exists)
}

// Heights 4..5 should remain.
for h := 4; h <= 5; h++ {
bidKey := ds.NewKey("/abci/bid").ChildString(string(rune(h + '0')))
exists, err := db.Has(ctx, bidKey)
require.NoError(t, err)
assert.True(t, exists)

respKey := ds.NewKey("/abci/br").ChildString(string(rune(h + '0')))
exists, err = db.Has(ctx, respKey)
require.NoError(t, err)
assert.True(t, exists)
}

// State should not be pruned.
exists, err := db.Has(ctx, ds.NewKey("/abci/s"))
require.NoError(t, err)
assert.True(t, exists)
}
Loading