From c90d21b3cdd2eed5502a00525206e0aa55c9a833 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Fri, 13 Feb 2026 15:05:19 +0100 Subject: [PATCH 1/5] Store pending block separately --- apps/evm/go.mod | 8 +- apps/evm/go.sum | 4 - apps/testapp/go.mod | 2 +- apps/testapp/go.sum | 2 - block/internal/executing/executor.go | 21 ++--- .../executing/executor_restart_test.go | 20 ++--- block/internal/executing/pending.go | 89 +++++++++++++++++++ pkg/store/header_store_adapter_test.go | 2 +- pkg/store/keys.go | 2 +- pkg/store/store.go | 13 +-- 10 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 block/internal/executing/pending.go diff --git a/apps/evm/go.mod b/apps/evm/go.mod index ed4b6c5126..4052c3afb2 100644 --- a/apps/evm/go.mod +++ b/apps/evm/go.mod @@ -2,10 +2,10 @@ module github.com/evstack/ev-node/apps/evm go 1.25.6 -//replace ( -// github.com/evstack/ev-node => ../../ -// github.com/evstack/ev-node/execution/evm => ../../execution/evm -//) +replace ( + github.com/evstack/ev-node => ../../ + github.com/evstack/ev-node/execution/evm => ../../execution/evm +) require ( github.com/ethereum/go-ethereum v1.16.8 diff --git a/apps/evm/go.sum b/apps/evm/go.sum index 00e5995e9a..49e723062b 100644 --- a/apps/evm/go.sum +++ b/apps/evm/go.sum @@ -411,12 +411,8 @@ github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9i github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= -github.com/evstack/ev-node v1.0.0-rc.4 h1:Ju7pSETFdadBZxmAj0//4z7hHkXbSRDy9iTzhF60Dew= -github.com/evstack/ev-node v1.0.0-rc.4/go.mod h1:xGCH5NCdGiYk6v3GVPm4NhzAtcKQgnaVnORg8b4tbOk= github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE= github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY= -github.com/evstack/ev-node/execution/evm v1.0.0-rc.3 h1:3o8H1TNywnst56lo2RlS2SXulDfp9yZJtkYYh7ZJrdM= -github.com/evstack/ev-node/execution/evm v1.0.0-rc.3/go.mod h1:VUEEklKoclg45GL7dzLoDwu3UQ4ptT3rF8bw5zUmnRk= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= diff --git a/apps/testapp/go.mod b/apps/testapp/go.mod index befa3aa536..d4e7306a95 100644 --- a/apps/testapp/go.mod +++ b/apps/testapp/go.mod @@ -2,7 +2,7 @@ module github.com/evstack/ev-node/apps/testapp go 1.25.6 -//replace github.com/evstack/ev-node => ../../ +replace github.com/evstack/ev-node => ../../ require ( github.com/evstack/ev-node v1.0.0-rc.4 diff --git a/apps/testapp/go.sum b/apps/testapp/go.sum index f07cb58dc1..13b8f9adce 100644 --- a/apps/testapp/go.sum +++ b/apps/testapp/go.sum @@ -367,8 +367,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= -github.com/evstack/ev-node v1.0.0-rc.4 h1:Ju7pSETFdadBZxmAj0//4z7hHkXbSRDy9iTzhF60Dew= -github.com/evstack/ev-node v1.0.0-rc.4/go.mod h1:xGCH5NCdGiYk6v3GVPm4NhzAtcKQgnaVnORg8b4tbOk= github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE= github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index 94417b9b79..786816d0ab 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -429,12 +429,12 @@ func (e *Executor) ProduceBlock(ctx context.Context) error { // Check if there's an already stored block at the newHeight // If there is use that instead of creating a new block - pendingHeader, pendingData, err := e.store.GetBlockData(ctx, newHeight) - if err == nil { + pendingHeader, pendingData, err := e.getPendingBlock(ctx) + if err == nil && pendingHeader != nil && pendingHeader.Height() == newHeight { e.logger.Info().Uint64("height", newHeight).Msg("using pending block") header = pendingHeader data = pendingData - } else if !errors.Is(err, datastore.ErrNotFound) { + } else if err != nil && !errors.Is(err, datastore.ErrNotFound) { return fmt.Errorf("failed to get block data: %w", err) } else { // get batch from sequencer @@ -452,18 +452,9 @@ func (e *Executor) ProduceBlock(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to create block: %w", err) } - - // saved early for crash recovery, will be overwritten later with the final signature - batch, err := e.store.NewBatch(ctx) - if err != nil { - return fmt.Errorf("failed to create batch for early save: %w", err) - } - if err = batch.SaveBlockData(header, data, &types.Signature{}); err != nil { + if err := e.savePendingBlock(ctx, header, data); err != nil { return fmt.Errorf("failed to save block data: %w", err) } - if err = batch.Commit(); err != nil { - return fmt.Errorf("failed to commit early save batch: %w", err) - } } if e.raftNode != nil && !e.raftNode.HasQuorum() { @@ -535,6 +526,10 @@ func (e *Executor) ProduceBlock(ctx context.Context) error { } e.logger.Debug().Uint64("height", newHeight).Msg("proposed block to raft") } + if err := e.deletePendingBlock(e.ctx, batch); err != nil { + e.logger.Warn().Err(err).Uint64("height", newHeight).Msg("failed to delete pending block metadata") + } + if err := batch.Commit(); err != nil { return fmt.Errorf("failed to commit batch: %w", err) } diff --git a/block/internal/executing/executor_restart_test.go b/block/internal/executing/executor_restart_test.go index 571bc75214..e5c3b6af40 100644 --- a/block/internal/executing/executor_restart_test.go +++ b/block/internal/executing/executor_restart_test.go @@ -79,7 +79,7 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { require.NoError(t, exec1.initializeState()) // Set up context for first executor - exec1.ctx, exec1.cancel = context.WithCancel(context.Background()) + exec1.ctx, exec1.cancel = context.WithCancel(t.Context()) // First executor produces a block normally mockSeq1.EXPECT().GetNextBatch(mock.Anything, mock.AnythingOfType("sequencer.GetNextBatchRequest")). @@ -101,12 +101,12 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { require.NoError(t, err) // Verify first block was produced - h1, err := memStore.Height(context.Background()) + h1, err := memStore.Height(t.Context()) require.NoError(t, err) assert.Equal(t, uint64(1), h1) // Store the produced block data for later verification - originalHeader, originalData, err := memStore.GetBlockData(context.Background(), 1) + originalHeader, originalData, err := memStore.GetBlockData(t.Context(), 1) require.NoError(t, err) assert.Equal(t, 2, len(originalData.Txs), "first block should have 2 transactions") @@ -158,11 +158,7 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { pendingHeader.DataHash = pendingData.DACommitment() // Save pending block data (this is what would happen during a crash) - batch, err := memStore.NewBatch(context.Background()) - require.NoError(t, err) - err = batch.SaveBlockData(pendingHeader, pendingData, &types.Signature{}) - require.NoError(t, err) - err = batch.Commit() + err = exec1.savePendingBlock(t.Context(), pendingHeader, pendingData) require.NoError(t, err) // Stop first executor (simulating crash/restart) @@ -199,7 +195,7 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { require.NoError(t, exec2.initializeState()) // Set up context for second executor - exec2.ctx, exec2.cancel = context.WithCancel(context.Background()) + exec2.ctx, exec2.cancel = context.WithCancel(t.Context()) defer exec2.cancel() // Verify that the state is at height 1 (pending block at height 2 wasn't committed) @@ -221,12 +217,12 @@ func TestExecutor_RestartUsesPendingHeader(t *testing.T) { require.NoError(t, err) // Verify height advanced to 2 - h2, err := memStore.Height(context.Background()) + h2, err := memStore.Height(t.Context()) require.NoError(t, err) assert.Equal(t, uint64(2), h2, "height should advance to 2 using pending block") // Verify the block at height 2 matches the pending block data - finalHeader, finalData, err := memStore.GetBlockData(context.Background(), 2) + finalHeader, finalData, err := memStore.GetBlockData(t.Context(), 2) require.NoError(t, err) assert.Equal(t, 3, len(finalData.Txs), "should use pending block with 3 transactions") assert.Equal(t, []byte("pending_tx1"), []byte(finalData.Txs[0])) @@ -388,7 +384,7 @@ func TestExecutor_RestartNoPendingHeader(t *testing.T) { require.NoError(t, err) // Verify normal operation - h, err := memStore.Height(context.Background()) + h, err := memStore.Height(t.Context()) require.NoError(t, err) assert.Equal(t, uint64(numBlocks+1), h) diff --git a/block/internal/executing/pending.go b/block/internal/executing/pending.go new file mode 100644 index 0000000000..687b5de26c --- /dev/null +++ b/block/internal/executing/pending.go @@ -0,0 +1,89 @@ +package executing + +import ( + "context" + "errors" + "fmt" + + "github.com/evstack/ev-node/pkg/store" + "github.com/evstack/ev-node/types" + ds "github.com/ipfs/go-datastore" +) + +const ( + headerKey = "pending_header" + dataKey = "pending_data" +) + +// getPendingBlock retrieves the pending block from metadata if it exists +func (e *Executor) getPendingBlock(ctx context.Context) (*types.SignedHeader, *types.Data, error) { + headerBytes, err := e.store.GetMetadata(ctx, headerKey) + if err != nil { + if errors.Is(err, ds.ErrNotFound) { + return nil, nil, nil + } + return nil, nil, err + } + + dataBytes, err := e.store.GetMetadata(ctx, dataKey) + if err != nil { + if errors.Is(err, ds.ErrNotFound) { + return nil, nil, nil + } + return nil, nil, err + } + + header := new(types.SignedHeader) + if err := header.UnmarshalBinary(headerBytes); err != nil { + return nil, nil, fmt.Errorf("unmarshal pending header: %w", err) + } + + data := new(types.Data) + if err := data.UnmarshalBinary(dataBytes); err != nil { + return nil, nil, fmt.Errorf("unmarshal pending data: %w", err) + } + return header, data, nil +} + +// savePendingBlock saves a block to metadata as pending +func (e *Executor) savePendingBlock(ctx context.Context, header *types.SignedHeader, data *types.Data) error { + headerBytes, err := header.MarshalBinary() + if err != nil { + return fmt.Errorf("marshal header: %w", err) + } + + dataBytes, err := data.MarshalBinary() + if err != nil { + return fmt.Errorf("marshal data: %w", err) + } + + batch, err := e.store.NewBatch(ctx) + if err != nil { + return fmt.Errorf("create batch for early save: %w", err) + } + + if err := batch.Put(ds.NewKey(store.GetMetaKey(headerKey)), headerBytes); err != nil { + return fmt.Errorf("save pending header: %w", err) + } + + if err := batch.Put(ds.NewKey(store.GetMetaKey(dataKey)), dataBytes); err != nil { + return fmt.Errorf("save pending data: %w", err) + } + + if err := batch.Commit(); err != nil { + return fmt.Errorf("commit pending block: %w", err) + } + return nil +} + +// deletePendingBlock removes pending block metadata +func (e *Executor) deletePendingBlock(ctx context.Context, batch store.Batch) error { + if err := batch.Delete(ds.NewKey(store.GetMetaKey(headerKey))); err != nil { + return fmt.Errorf("delete pending header: %w", err) + } + + if err := batch.Delete(ds.NewKey(store.GetMetaKey(dataKey))); err != nil { + return fmt.Errorf("delete pending data: %w", err) + } + return nil +} diff --git a/pkg/store/header_store_adapter_test.go b/pkg/store/header_store_adapter_test.go index c80374aa71..33f804c676 100644 --- a/pkg/store/header_store_adapter_test.go +++ b/pkg/store/header_store_adapter_test.go @@ -599,7 +599,7 @@ func TestHeaderStoreAdapter_HeadPrefersPending(t *testing.T) { func TestHeaderStoreAdapter_GetFromPendingByHash(t *testing.T) { t.Parallel() - ctx := context.Background() + ctx := t.Context() ds, err := NewTestInMemoryKVStore() require.NoError(t, err) diff --git a/pkg/store/keys.go b/pkg/store/keys.go index dd989c0e82..520567d3e1 100644 --- a/pkg/store/keys.go +++ b/pkg/store/keys.go @@ -50,7 +50,7 @@ func getStateAtHeightKey(height uint64) string { return GenerateKey([]string{statePrefix, strconv.FormatUint(height, 10)}) } -func getMetaKey(key string) string { +func GetMetaKey(key string) string { return GenerateKey([]string{metaPrefix, key}) } diff --git a/pkg/store/store.go b/pkg/store/store.go index eafa47ae75..79c3f1deaf 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -113,6 +113,7 @@ func (s *DefaultStore) GetHeader(ctx context.Context, height uint64) (*types.Sig if err = header.UnmarshalBinary(headerBlob); err != nil { return nil, fmt.Errorf("unmarshal block header: %w", err) } + return header, nil } @@ -176,7 +177,7 @@ func (s *DefaultStore) GetStateAtHeight(ctx context.Context, height uint64) (typ // // Metadata is separated from other data by using prefix in KV. func (s *DefaultStore) SetMetadata(ctx context.Context, key string, value []byte) error { - err := s.db.Put(ctx, ds.NewKey(getMetaKey(key)), value) + err := s.db.Put(ctx, ds.NewKey(GetMetaKey(key)), value) if err != nil { return fmt.Errorf("failed to set metadata for key '%s': %w", key, err) } @@ -185,7 +186,7 @@ func (s *DefaultStore) SetMetadata(ctx context.Context, key string, value []byte // GetMetadata returns values stored for given key with SetMetadata. func (s *DefaultStore) GetMetadata(ctx context.Context, key string) ([]byte, error) { - data, err := s.db.Get(ctx, ds.NewKey(getMetaKey(key))) + data, err := s.db.Get(ctx, ds.NewKey(GetMetaKey(key))) if err != nil { return nil, fmt.Errorf("failed to get metadata for key '%s': %w", key, err) } @@ -196,7 +197,7 @@ func (s *DefaultStore) GetMetadata(ctx context.Context, key string) ([]byte, err // This is more efficient than iterating through known keys when the set of keys is unknown. func (s *DefaultStore) GetMetadataByPrefix(ctx context.Context, prefix string) ([]MetadataEntry, error) { // The full key in the datastore includes the meta prefix - fullPrefix := getMetaKey(prefix) + fullPrefix := GetMetaKey(prefix) results, err := s.db.Query(ctx, dsq.Query{Prefix: fullPrefix}) if err != nil { @@ -213,7 +214,7 @@ func (s *DefaultStore) GetMetadataByPrefix(ctx context.Context, prefix string) ( // Extract the original key by removing the meta prefix // The key from datastore is like "/m/cache/header-da-included/hash" // We want to return "cache/header-da-included/hash" - metaKeyPrefix := getMetaKey("") + metaKeyPrefix := GetMetaKey("") key := strings.TrimPrefix(result.Key, metaKeyPrefix) key = strings.TrimPrefix(key, "/") // Remove leading slash for consistency @@ -228,7 +229,7 @@ func (s *DefaultStore) GetMetadataByPrefix(ctx context.Context, prefix string) ( // DeleteMetadata removes a metadata key from the store. func (s *DefaultStore) DeleteMetadata(ctx context.Context, key string) error { - err := s.db.Delete(ctx, ds.NewKey(getMetaKey(key))) + err := s.db.Delete(ctx, ds.NewKey(GetMetaKey(key))) if err != nil { return fmt.Errorf("failed to delete metadata for key '%s': %w", key, err) } @@ -279,7 +280,7 @@ func (s *DefaultStore) Rollback(ctx context.Context, height uint64, aggregator b } else { // in case of syncing issues, rollback the included height is OK. bz := make([]byte, 8) binary.LittleEndian.PutUint64(bz, height) - if err := batch.Put(ctx, ds.NewKey(getMetaKey(DAIncludedHeightKey)), bz); err != nil { + if err := batch.Put(ctx, ds.NewKey(GetMetaKey(DAIncludedHeightKey)), bz); err != nil { return fmt.Errorf("failed to update DA included height: %w", err) } } From 522cdfd8ac749439b9d8bf7412963e89e3ee874d Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Fri, 13 Feb 2026 15:42:35 +0100 Subject: [PATCH 2/5] Support migration --- block/internal/executing/executor.go | 6 +++ block/internal/executing/pending.go | 59 ++++++++++++++++++++++++++++ pkg/store/keys.go | 16 ++++++-- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index 786816d0ab..ae37683300 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -270,6 +270,12 @@ func (e *Executor) initializeState() error { e.logger.Info().Uint64("height", state.LastBlockHeight). Str("chain_id", state.ChainID).Msg("initialized state") + // Migrate any old-style pending block (stored at height N+1 via SaveBlockData + // with empty signature) to the new metadata-key format. + if err := e.migrateLegacyPendingBlock(e.ctx); err != nil { + return fmt.Errorf("failed to migrate legacy pending block: %w", err) + } + // Determine sync target: use Raft height if node is behind Raft consensus syncTargetHeight := state.LastBlockHeight if e.raftNode != nil { diff --git a/block/internal/executing/pending.go b/block/internal/executing/pending.go index 687b5de26c..7d005e9c46 100644 --- a/block/internal/executing/pending.go +++ b/block/internal/executing/pending.go @@ -2,11 +2,13 @@ package executing import ( "context" + "crypto/sha256" "errors" "fmt" "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/types" + "github.com/ipfs/go-datastore" ds "github.com/ipfs/go-datastore" ) @@ -87,3 +89,60 @@ func (e *Executor) deletePendingBlock(ctx context.Context, batch store.Batch) er } return nil } + +// migrateLegacyPendingBlock detects old-style pending blocks that were stored +// at height N+1 via SaveBlockData with an empty signature (pre-upgrade format) +// and migrates them to the new metadata-key format (m/pending_header, m/pending_data). +// +// This prevents double-signing when a node is upgraded: without migration the +// new code would not find the pending block and would create+sign a new one at +// the same height. +func (e *Executor) migrateLegacyPendingBlock(ctx context.Context) error { + candidateHeight := e.getLastState().LastBlockHeight + 1 + pendingHeader, pendingData, err := e.store.GetBlockData(ctx, candidateHeight) + if err != nil { + if !errors.Is(err, datastore.ErrNotFound) { + return fmt.Errorf("get block data: %w", err) + } + return nil + } + if len(pendingHeader.Signature) != 0 { + return errors.New("pending block with signatures found") + } + // Migrate: write header+data to the new metadata keys. + if err := e.savePendingBlock(ctx, pendingHeader, pendingData); err != nil { + return fmt.Errorf("save migrated pending block: %w", err) + } + + // Clean up old-style keys. + batch, err := e.store.NewBatch(ctx) + if err != nil { + return fmt.Errorf("create cleanup batch: %w", err) + } + + headerBytes, err := pendingHeader.MarshalBinary() + if err != nil { + return fmt.Errorf("marshal header for hash: %w", err) + } + headerHash := sha256.Sum256(headerBytes) + + for _, key := range []string{ + store.GetHeaderKey(candidateHeight), + store.GetDataKey(candidateHeight), + store.GetSignatureKey(candidateHeight), + store.GetIndexKey(headerHash[:]), + } { + if err := batch.Delete(ds.NewKey(key)); err != nil { + return fmt.Errorf("delete legacy key %s: %w", key, err) + } + } + + if err := batch.Commit(); err != nil { + return fmt.Errorf("commit cleanup batch: %w", err) + } + + e.logger.Info(). + Uint64("height", candidateHeight). + Msg("migrated legacy pending block to metadata format") + return nil +} diff --git a/pkg/store/keys.go b/pkg/store/keys.go index 520567d3e1..610b857823 100644 --- a/pkg/store/keys.go +++ b/pkg/store/keys.go @@ -34,18 +34,24 @@ const ( heightPrefix = "t" ) -func getHeaderKey(height uint64) string { +func GetHeaderKey(height uint64) string { return GenerateKey([]string{headerPrefix, strconv.FormatUint(height, 10)}) } -func getDataKey(height uint64) string { +func getHeaderKey(height uint64) string { return GetHeaderKey(height) } + +func GetDataKey(height uint64) string { return GenerateKey([]string{dataPrefix, strconv.FormatUint(height, 10)}) } -func getSignatureKey(height uint64) string { +func getDataKey(height uint64) string { return GetDataKey(height) } + +func GetSignatureKey(height uint64) string { return GenerateKey([]string{signaturePrefix, strconv.FormatUint(height, 10)}) } +func getSignatureKey(height uint64) string { return GetSignatureKey(height) } + func getStateAtHeightKey(height uint64) string { return GenerateKey([]string{statePrefix, strconv.FormatUint(height, 10)}) } @@ -54,10 +60,12 @@ func GetMetaKey(key string) string { return GenerateKey([]string{metaPrefix, key}) } -func getIndexKey(hash types.Hash) string { +func GetIndexKey(hash types.Hash) string { return GenerateKey([]string{indexPrefix, hash.String()}) } +func getIndexKey(hash types.Hash) string { return GetIndexKey(hash) } + func getHeightKey() string { return GenerateKey([]string{heightPrefix}) } From ca22251d1d6e8ad640625324e11d75bbe1d434ef Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Mon, 16 Feb 2026 10:27:25 +0100 Subject: [PATCH 3/5] Review feedback (cherry picked from commit 354bc76e32eb7a150d1437aa9674b697cd95af09) --- block/internal/executing/executor.go | 3 ++- block/internal/executing/pending.go | 6 +++--- pkg/store/keys.go | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/block/internal/executing/executor.go b/block/internal/executing/executor.go index ae37683300..ac68f2cd85 100644 --- a/block/internal/executing/executor.go +++ b/block/internal/executing/executor.go @@ -272,6 +272,7 @@ func (e *Executor) initializeState() error { // Migrate any old-style pending block (stored at height N+1 via SaveBlockData // with empty signature) to the new metadata-key format. + // Todo remove in the future: https://github.com/evstack/ev-node/issues/2795 if err := e.migrateLegacyPendingBlock(e.ctx); err != nil { return fmt.Errorf("failed to migrate legacy pending block: %w", err) } @@ -532,7 +533,7 @@ func (e *Executor) ProduceBlock(ctx context.Context) error { } e.logger.Debug().Uint64("height", newHeight).Msg("proposed block to raft") } - if err := e.deletePendingBlock(e.ctx, batch); err != nil { + if err := e.deletePendingBlock(batch); err != nil { e.logger.Warn().Err(err).Uint64("height", newHeight).Msg("failed to delete pending block metadata") } diff --git a/block/internal/executing/pending.go b/block/internal/executing/pending.go index 7d005e9c46..44bdf3aa6f 100644 --- a/block/internal/executing/pending.go +++ b/block/internal/executing/pending.go @@ -30,7 +30,7 @@ func (e *Executor) getPendingBlock(ctx context.Context) (*types.SignedHeader, *t dataBytes, err := e.store.GetMetadata(ctx, dataKey) if err != nil { if errors.Is(err, ds.ErrNotFound) { - return nil, nil, nil + return nil, nil, fmt.Errorf("pending header exists but data is missing: corrupt state") } return nil, nil, err } @@ -79,7 +79,7 @@ func (e *Executor) savePendingBlock(ctx context.Context, header *types.SignedHea } // deletePendingBlock removes pending block metadata -func (e *Executor) deletePendingBlock(ctx context.Context, batch store.Batch) error { +func (e *Executor) deletePendingBlock(batch store.Batch) error { if err := batch.Delete(ds.NewKey(store.GetMetaKey(headerKey))); err != nil { return fmt.Errorf("delete pending header: %w", err) } @@ -132,7 +132,7 @@ func (e *Executor) migrateLegacyPendingBlock(ctx context.Context) error { store.GetSignatureKey(candidateHeight), store.GetIndexKey(headerHash[:]), } { - if err := batch.Delete(ds.NewKey(key)); err != nil { + if err := batch.Delete(ds.NewKey(key)); err != nil && !errors.Is(err, ds.ErrNotFound) { return fmt.Errorf("delete legacy key %s: %w", key, err) } } diff --git a/pkg/store/keys.go b/pkg/store/keys.go index e498286877..f2aa45d8ab 100644 --- a/pkg/store/keys.go +++ b/pkg/store/keys.go @@ -39,18 +39,21 @@ const ( heightPrefix = "t" ) +// GetHeaderKey returns the store key for a block header at the given height. func GetHeaderKey(height uint64) string { return GenerateKey([]string{headerPrefix, strconv.FormatUint(height, 10)}) } func getHeaderKey(height uint64) string { return GetHeaderKey(height) } +// GetDataKey returns the store key for block data at the given height. func GetDataKey(height uint64) string { return GenerateKey([]string{dataPrefix, strconv.FormatUint(height, 10)}) } func getDataKey(height uint64) string { return GetDataKey(height) } +// GetSignatureKey returns the store key for a block signature at the given height. func GetSignatureKey(height uint64) string { return GenerateKey([]string{signaturePrefix, strconv.FormatUint(height, 10)}) } @@ -61,10 +64,12 @@ func getStateAtHeightKey(height uint64) string { return GenerateKey([]string{statePrefix, strconv.FormatUint(height, 10)}) } +// GetMetaKey returns the store key for a metadata entry. func GetMetaKey(key string) string { return GenerateKey([]string{metaPrefix, key}) } +// GetIndexKey returns the store key for indexing a block by its hash. func GetIndexKey(hash types.Hash) string { return GenerateKey([]string{indexPrefix, hash.String()}) } From 838a1f802c6b81f0be5729af32fa75eb77f26622 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Mon, 16 Feb 2026 10:47:18 +0100 Subject: [PATCH 4/5] Review feedback --- pkg/store/store.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/store/store.go b/pkg/store/store.go index cf1b56a5e0..b042149047 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -403,12 +403,12 @@ func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error { } // Delete per-height DA metadata associated with this height, if any. - if err := batch.Delete(ctx, ds.NewKey(getMetaKey(GetHeightToDAHeightHeaderKey(h)))); err != nil { + if err := batch.Delete(ctx, ds.NewKey(GetMetaKey(GetHeightToDAHeightHeaderKey(h)))); err != nil { if !errors.Is(err, ds.ErrNotFound) { return fmt.Errorf("failed to delete header DA height metadata at height %d during pruning: %w", h, err) } } - if err := batch.Delete(ctx, ds.NewKey(getMetaKey(GetHeightToDAHeightDataKey(h)))); err != nil { + if err := batch.Delete(ctx, ds.NewKey(GetMetaKey(GetHeightToDAHeightDataKey(h)))); err != nil { if !errors.Is(err, ds.ErrNotFound) { return fmt.Errorf("failed to delete data DA height metadata at height %d during pruning: %w", h, err) } @@ -423,7 +423,7 @@ func (s *DefaultStore) PruneBlocks(ctx context.Context, height uint64) error { } // Persist the updated last pruned height. - if err := batch.Put(ctx, ds.NewKey(getMetaKey(LastPrunedBlockHeightKey)), encodeHeight(height)); err != nil { + if err := batch.Put(ctx, ds.NewKey(GetMetaKey(LastPrunedBlockHeightKey)), encodeHeight(height)); err != nil { return fmt.Errorf("failed to update last pruned height: %w", err) } From eb7d790206ecff4080a9e618ef56bb42fd562d71 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Mon, 16 Feb 2026 10:51:27 +0100 Subject: [PATCH 5/5] Linter --- block/internal/executing/pending.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/block/internal/executing/pending.go b/block/internal/executing/pending.go index 44bdf3aa6f..dc758e558c 100644 --- a/block/internal/executing/pending.go +++ b/block/internal/executing/pending.go @@ -8,7 +8,6 @@ import ( "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/types" - "github.com/ipfs/go-datastore" ds "github.com/ipfs/go-datastore" ) @@ -101,7 +100,7 @@ func (e *Executor) migrateLegacyPendingBlock(ctx context.Context) error { candidateHeight := e.getLastState().LastBlockHeight + 1 pendingHeader, pendingData, err := e.store.GetBlockData(ctx, candidateHeight) if err != nil { - if !errors.Is(err, datastore.ErrNotFound) { + if !errors.Is(err, ds.ErrNotFound) { return fmt.Errorf("get block data: %w", err) } return nil