Skip to content

Commit c6dd42c

Browse files
authored
feat: replay mechanism to sync node with execution layer (#2771)
<!-- Please read and fill out this form before submitting your PR. Please make sure you have reviewed our contributors guide before submitting your first PR. NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/ --> ## Overview Closes: #2750 <!-- Please provide an explanation of the PR, including the appropriate context, background, goal, and rationale. If there is an issue with this information, please provide a tl;dr and link the issue. Ex: Closes #<issue number> -->
1 parent 9134ff1 commit c6dd42c

17 files changed

Lines changed: 753 additions & 8 deletions

File tree

apps/evm/single/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ replace github.com/celestiaorg/go-header => github.com/julienrbrt/go-header v0.0
66

77
replace (
88
github.com/evstack/ev-node => ../../../
9+
github.com/evstack/ev-node/core => ../../../core
910
github.com/evstack/ev-node/da => ../../../da
1011
github.com/evstack/ev-node/execution/evm => ../../../execution/evm
1112
github.com/evstack/ev-node/sequencers/single => ../../../sequencers/single

apps/evm/single/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,6 @@ github.com/ethereum/go-ethereum v1.16.5 h1:GZI995PZkzP7ySCxEFaOPzS8+bd8NldE//1qv
103103
github.com/ethereum/go-ethereum v1.16.5/go.mod h1:kId9vOtlYg3PZk9VwKbGlQmSACB5ESPTBGT+M9zjmok=
104104
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
105105
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
106-
github.com/evstack/ev-node/core v1.0.0-beta.3 h1:01K2Ygm3puX4m2OBxvg/HDxu+he54jeNv+KDmpgujFc=
107-
github.com/evstack/ev-node/core v1.0.0-beta.3/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
108106
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
109107
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
110108
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=

apps/grpc/single/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ require (
166166

167167
replace (
168168
github.com/evstack/ev-node => ../../../
169+
github.com/evstack/ev-node/core => ../../../core
169170
github.com/evstack/ev-node/da => ../../../da
170171
github.com/evstack/ev-node/execution/grpc => ../../../execution/grpc
171172
github.com/evstack/ev-node/sequencers/single => ../../../sequencers/single

apps/grpc/single/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
6262
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
6363
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
6464
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
65-
github.com/evstack/ev-node/core v1.0.0-beta.3 h1:01K2Ygm3puX4m2OBxvg/HDxu+he54jeNv+KDmpgujFc=
66-
github.com/evstack/ev-node/core v1.0.0-beta.3/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
6765
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
6866
github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs=
6967
github.com/filecoin-project/go-jsonrpc v0.8.0 h1:2yqlN3Vd8Gx5UtA3fib7tQu2aW1cSOJt253LEBWExo4=

apps/testapp/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ replace github.com/celestiaorg/go-header => github.com/julienrbrt/go-header v0.0
66

77
replace (
88
github.com/evstack/ev-node => ../../.
9+
github.com/evstack/ev-node/core => ../../core
910
github.com/evstack/ev-node/da => ../../da
1011
github.com/evstack/ev-node/sequencers/single => ../../sequencers/single
1112
)

apps/testapp/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
6060
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
6161
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
6262
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
63-
github.com/evstack/ev-node/core v1.0.0-beta.3 h1:01K2Ygm3puX4m2OBxvg/HDxu+he54jeNv+KDmpgujFc=
64-
github.com/evstack/ev-node/core v1.0.0-beta.3/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
6563
github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU=
6664
github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs=
6765
github.com/filecoin-project/go-jsonrpc v0.8.0 h1:2yqlN3Vd8Gx5UtA3fib7tQu2aW1cSOJt253LEBWExo4=

block/internal/common/replay.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package common
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/hex"
7+
"fmt"
8+
9+
"github.com/rs/zerolog"
10+
11+
coreexecutor "github.com/evstack/ev-node/core/execution"
12+
"github.com/evstack/ev-node/pkg/genesis"
13+
"github.com/evstack/ev-node/pkg/store"
14+
"github.com/evstack/ev-node/types"
15+
)
16+
17+
// Replayer handles synchronization of the execution layer with ev-node's state.
18+
// It replays blocks from the store to bring the execution layer up to date.
19+
type Replayer struct {
20+
store store.Store
21+
exec coreexecutor.Executor
22+
genesis genesis.Genesis
23+
logger zerolog.Logger
24+
}
25+
26+
// NewReplayer creates a new execution layer replayer.
27+
func NewReplayer(
28+
store store.Store,
29+
exec coreexecutor.Executor,
30+
genesis genesis.Genesis,
31+
logger zerolog.Logger,
32+
) *Replayer {
33+
return &Replayer{
34+
store: store,
35+
exec: exec,
36+
genesis: genesis,
37+
logger: logger.With().Str("component", "execution_replayer").Logger(),
38+
}
39+
}
40+
41+
// SyncToHeight checks if the execution layer is behind ev-node and syncs it to the target height.
42+
// This is useful for crash recovery scenarios where ev-node is ahead of the execution layer.
43+
//
44+
// Returns:
45+
// - error if sync fails or if execution layer is ahead of ev-node (unexpected state)
46+
func (s *Replayer) SyncToHeight(ctx context.Context, targetHeight uint64) error {
47+
// Check if the executor implements HeightProvider
48+
execHeightProvider, ok := s.exec.(coreexecutor.HeightProvider)
49+
if !ok {
50+
s.logger.Debug().Msg("executor does not implement HeightProvider, skipping sync")
51+
return nil
52+
}
53+
54+
// Skip sync check if we're at genesis
55+
if targetHeight < s.genesis.InitialHeight {
56+
s.logger.Debug().Msg("at genesis height, skipping execution layer sync check")
57+
return nil
58+
}
59+
60+
execHeight, err := execHeightProvider.GetLatestHeight(ctx)
61+
if err != nil {
62+
return fmt.Errorf("failed to get execution layer height: %w", err)
63+
}
64+
65+
s.logger.Info().
66+
Uint64("target_height", targetHeight).
67+
Uint64("exec_layer_height", execHeight).
68+
Msg("execution layer height check")
69+
70+
// If execution layer is ahead, this is unexpected, fail hard
71+
if execHeight > targetHeight {
72+
s.logger.Error().
73+
Uint64("target_height", targetHeight).
74+
Uint64("exec_layer_height", execHeight).
75+
Msg("execution layer is ahead of target height - this should not happen")
76+
return fmt.Errorf("execution layer height (%d) is ahead of target height (%d)", execHeight, targetHeight)
77+
}
78+
79+
// If execution layer is behind, sync the missing blocks
80+
if execHeight < targetHeight {
81+
s.logger.Info().
82+
Uint64("target_height", targetHeight).
83+
Uint64("exec_layer_height", execHeight).
84+
Uint64("blocks_to_sync", targetHeight-execHeight).
85+
Msg("execution layer is behind, syncing blocks")
86+
87+
// Sync blocks from execHeight+1 to targetHeight
88+
for height := execHeight + 1; height <= targetHeight; height++ {
89+
if err := s.replayBlock(ctx, height); err != nil {
90+
return fmt.Errorf("failed to replay block %d to execution layer: %w", height, err)
91+
}
92+
}
93+
94+
s.logger.Info().
95+
Uint64("synced_blocks", targetHeight-execHeight).
96+
Msg("successfully synced execution layer")
97+
} else {
98+
s.logger.Info().Msg("execution layer is in sync")
99+
}
100+
101+
return nil
102+
}
103+
104+
// replayBlock replays a specific block from the store to the execution layer.
105+
//
106+
// Validation assumptions:
107+
// - Blocks in the store have already been fully validated (signatures, timestamps, etc.)
108+
// - We only verify the AppHash matches to detect state divergence
109+
// - We skip re-validating signatures and consensus rules since this is a replay
110+
// - This is safe because we're re-executing transactions against a known-good state
111+
func (s *Replayer) replayBlock(ctx context.Context, height uint64) error {
112+
s.logger.Info().Uint64("height", height).Msg("replaying block to execution layer")
113+
114+
// Get the block from store
115+
header, data, err := s.store.GetBlockData(ctx, height)
116+
if err != nil {
117+
return fmt.Errorf("failed to get block data from store: %w", err)
118+
}
119+
120+
// Get the previous state
121+
var prevState types.State
122+
if height == s.genesis.InitialHeight {
123+
// For the first block, use genesis state
124+
prevState = types.State{
125+
ChainID: s.genesis.ChainID,
126+
InitialHeight: s.genesis.InitialHeight,
127+
LastBlockHeight: s.genesis.InitialHeight - 1,
128+
LastBlockTime: s.genesis.StartTime,
129+
AppHash: header.AppHash, // This will be updated by InitChain
130+
}
131+
} else {
132+
// Get previous state from store
133+
prevState, err = s.store.GetState(ctx)
134+
if err != nil {
135+
return fmt.Errorf("failed to get previous state: %w", err)
136+
}
137+
// We need the state at height-1, so load that block's app hash
138+
prevHeader, _, err := s.store.GetBlockData(ctx, height-1)
139+
if err != nil {
140+
return fmt.Errorf("failed to get previous block header: %w", err)
141+
}
142+
prevState.AppHash = prevHeader.AppHash
143+
prevState.LastBlockHeight = height - 1
144+
}
145+
146+
// Prepare transactions
147+
rawTxs := make([][]byte, len(data.Txs))
148+
for i, tx := range data.Txs {
149+
rawTxs[i] = []byte(tx)
150+
}
151+
152+
// Execute transactions on the execution layer
153+
s.logger.Debug().
154+
Uint64("height", height).
155+
Int("tx_count", len(rawTxs)).
156+
Msg("executing transactions on execution layer")
157+
158+
newAppHash, _, err := s.exec.ExecuteTxs(ctx, rawTxs, height, header.Time(), prevState.AppHash)
159+
if err != nil {
160+
return fmt.Errorf("failed to execute transactions: %w", err)
161+
}
162+
163+
// Verify the app hash matches
164+
if !bytes.Equal(newAppHash, header.AppHash) {
165+
err := fmt.Errorf("app hash mismatch: expected %s got %s",
166+
hex.EncodeToString(header.AppHash),
167+
hex.EncodeToString(newAppHash),
168+
)
169+
s.logger.Error().
170+
Str("expected", hex.EncodeToString(header.AppHash)).
171+
Str("got", hex.EncodeToString(newAppHash)).
172+
Uint64("height", height).
173+
Err(err).
174+
Msg("app hash mismatch during replay")
175+
return err
176+
}
177+
178+
s.logger.Info().
179+
Uint64("height", height).
180+
Msg("successfully replayed block to execution layer")
181+
182+
return nil
183+
}

0 commit comments

Comments
 (0)