|
| 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