Skip to content

Commit aedd573

Browse files
authored
fix(block): fix replay logic (#3053)
* claude replay fix * reduce comments * prep rc.3
1 parent 0425059 commit aedd573

File tree

3 files changed

+164
-86
lines changed

3 files changed

+164
-86
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
## v1.0.0-rc.3
13+
1214
### Added
1315

1416
- Add DA Hints for P2P transactions. This allows a catching up node to be on sync with both DA and P2P. ([#2891](https://github.com/evstack/ev-node/pull/2891))
@@ -18,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1820
- Improve `cache.NumPendingData` to not return empty data. Automatically bumps `LastSubmittedHeight` to reflect that. ([#3046](https://github.com/evstack/ev-node/pull/3046))
1921
- **BREAKING** Make pending events cache and tx cache fully ephemeral. Those will be re-fetched on restart. DA Inclusion cache persists until cleared up after DA inclusion has been processed. Persist accross restart using store metadata. ([#3047](https://github.com/evstack/ev-node/pull/3047))
2022
- Replace LRU cache by standard mem cache with manual eviction in `store_adapter`. When P2P blocks were fetched too fast, they would be evicted before being executed [#3051](https://github.com/evstack/ev-node/pull/3051)
23+
- Fix replay logic leading to app hashes by verifying against the wrong block [#3053](https://github.com/evstack/ev-node/pull/3053).
2124

2225
## v1.0.0-rc.2
2326

block/internal/common/replay.go

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -150,27 +150,21 @@ func (s *Replayer) replayBlock(ctx context.Context, height uint64) error {
150150
// Get the previous state
151151
var prevState types.State
152152
if height == s.genesis.InitialHeight {
153-
// For the first block, use genesis state
153+
// For the first block, use genesis state.
154154
prevState = types.State{
155155
ChainID: s.genesis.ChainID,
156156
InitialHeight: s.genesis.InitialHeight,
157157
LastBlockHeight: s.genesis.InitialHeight - 1,
158158
LastBlockTime: s.genesis.StartTime,
159-
AppHash: header.AppHash, // This will be updated by InitChain
159+
AppHash: header.AppHash, // Genesis app hash (input to first block execution)
160160
}
161161
} else {
162-
// Get previous state from store
162+
// GetStateAtHeight(height-1) returns the state AFTER block height-1 was executed,
163+
// which contains the correct AppHash to use as input for executing block at 'height'.
163164
prevState, err = s.store.GetStateAtHeight(ctx, height-1)
164165
if err != nil {
165166
return fmt.Errorf("failed to get previous state: %w", err)
166167
}
167-
// We need the state at height-1, so load that block's app hash
168-
prevHeader, _, err := s.store.GetBlockData(ctx, height-1)
169-
if err != nil {
170-
return fmt.Errorf("failed to get previous block header: %w", err)
171-
}
172-
prevState.AppHash = prevHeader.AppHash
173-
prevState.LastBlockHeight = height - 1
174168
}
175169

176170
// Prepare transactions
@@ -190,27 +184,39 @@ func (s *Replayer) replayBlock(ctx context.Context, height uint64) error {
190184
return fmt.Errorf("failed to execute transactions: %w", err)
191185
}
192186

193-
// DEBUG: Log comparison of expected vs actual app hash
194-
s.logger.Debug().
195-
Uint64("height", height).
196-
Str("expected_app_hash", hex.EncodeToString(header.AppHash)).
197-
Str("actual_app_hash", hex.EncodeToString(newAppHash)).
198-
Bool("hashes_match", bytes.Equal(newAppHash, header.AppHash)).
199-
Msg("replayBlock: ExecuteTxs completed")
200-
201-
// Verify the app hash matches
202-
if !bytes.Equal(newAppHash, header.AppHash) {
203-
err := fmt.Errorf("app hash mismatch: expected %s got %s",
204-
hex.EncodeToString(header.AppHash),
205-
hex.EncodeToString(newAppHash),
206-
)
207-
s.logger.Error().
208-
Str("expected", hex.EncodeToString(header.AppHash)).
209-
Str("got", hex.EncodeToString(newAppHash)).
187+
// The result of ExecuteTxs (newAppHash) should match the stored state at this height.
188+
// Note: header.AppHash is the PREVIOUS state's app hash (input), not the expected output.
189+
// The expected output is stored in state[height].AppHash or equivalently in header[height+1].AppHash.
190+
191+
// For verification, we need to get the expected app hash from the stored state at this height.
192+
// If this state doesn't exist (which would be unusual since we fetched the block), we skip verification.
193+
expectedState, err := s.store.GetStateAtHeight(ctx, height)
194+
if err == nil {
195+
// State exists, verify the app hash matches
196+
if !bytes.Equal(newAppHash, expectedState.AppHash) {
197+
err := fmt.Errorf("app hash mismatch at height %d: expected %s got %s",
198+
height,
199+
hex.EncodeToString(expectedState.AppHash),
200+
hex.EncodeToString(newAppHash),
201+
)
202+
s.logger.Error().
203+
Str("expected", hex.EncodeToString(expectedState.AppHash)).
204+
Str("got", hex.EncodeToString(newAppHash)).
205+
Uint64("height", height).
206+
Err(err).
207+
Msg("app hash mismatch during replay")
208+
return err
209+
}
210+
s.logger.Debug().
211+
Uint64("height", height).
212+
Str("app_hash", hex.EncodeToString(newAppHash)).
213+
Msg("replayBlock: app hash verified against stored state")
214+
} else {
215+
// State doesn't exist yet, we trust the execution result since we're replaying validated blocks.
216+
s.logger.Debug().
210217
Uint64("height", height).
211-
Err(err).
212-
Msg("app hash mismatch during replay")
213-
return err
218+
Str("app_hash", hex.EncodeToString(newAppHash)).
219+
Msg("replayBlock: ExecuteTxs completed (no stored state to verify against)")
214220
}
215221

216222
// Calculate new state

0 commit comments

Comments
 (0)