You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
EVM: Set FinalizedHash correctly in forkChoiceUpdatedV3 call in ExecuteTxs #2402
In #2394, in order to have sequencer restarts work well, we modified the first call to forkChoiceUpdatedV3 in the ExecuteTxs EVM implementation to assign the previous block hash to HeadBlockHash, SafeBlockHash, and FinalizedBlockHash.
We should revisit this and take one or more of the following approaches:
Set the FinalizedBlockHash based on the DA Included Height from the Rollkit Store. For the Head and Safe block hashes, use the Rollkit height which should be the same as the reth block height but it might be good to actually sanity check that on startup and set them in engineClient correctly on start.
Persist the current versions of head, safe, and finalizedBlockHashes somewhere so on restarts they can be loaded back easily.
The concrete consequences of setting finalizedHash incorrectly are unknown currently but it'll likely be at least an issue in indexers and block explorers showing inconsistent finalized block hashes for a small period of time.
I've analyzed the codebase and identified the issue with FinalizedHash setting in the EVM's forkChoiceUpdatedV3 call. Here's my comprehensive solution proposal:
Todo List:
Understand the current codebase structure and find EVM-related code
Locate the problematic forkChoiceUpdatedV3 implementation
Research DA layer integration and block height tracking
Analyze block hash persistence options
Propose a concrete solution with implementation details
Problem Analysis
The issue is in /execution/evm/execution.go:162-166 where the first forkChoiceUpdatedV3 call incorrectly sets all three block hashes to the previous block hash:
args:= engine.ForkchoiceStateV1{
HeadBlockHash: prevBlockHash,
SafeBlockHash: prevBlockHash,
FinalizedBlockHash: prevBlockHash, // ❌ This is incorrect
}
This was a temporary fix for sequencer restarts but causes indexers and block explorers to show inconsistent finalized block hashes.
Root Cause
Incorrect Finalized Hash: Setting FinalizedBlockHash to prevBlockHash doesn't reflect the actual finalized state
Missing DA Integration: The finalized hash should correspond to the DA-included height, not the previous block
No State Persistence: Block hash states aren't persisted across restarts
Proposed Solution
I recommend Approach #1 from the issue description with enhancements:
1. Use DA Included Height for FinalizedBlockHash
The codebase already has a robust DA inclusion tracking system:
DAIncludedHeight is tracked in the store (pkg/store/keys.go:16)
incrementDAIncludedHeight() in block/da_includer.go:54 calls SetFinal() when blocks are DA-included
This provides the correct finalized block height
2. Enhanced Implementation Plan
Phase 1: Fix FinalizedBlockHash Logic
// In ExecuteTxs, replace lines 162-166 with:func (c*EngineClient) getForkChoiceState(ctx context.Context, prevBlockHash common.Hash) (engine.ForkchoiceStateV1, error) {
c.mu.Lock()
deferc.mu.Unlock()
// Use the stored finalized hash, fallback to genesis if not setfinalizedHash:=c.currentFinalizedBlockHashiffinalizedHash== (common.Hash{}) {
finalizedHash=c.genesisHash
}
return engine.ForkchoiceStateV1{
HeadBlockHash: prevBlockHash,
SafeBlockHash: prevBlockHash,
FinalizedBlockHash: finalizedHash,
}, nil
}
Phase 2: Integrate with DA Height System
// Add method to sync EVM state with DA-included heightfunc (c*EngineClient) SyncWithDAHeight(ctx context.Context, daIncludedHeightuint64) error {
ifdaIncludedHeight==0 {
returnnil// Nothing to finalize yet
}
// Get the block hash at the DA-included heightfinalizedHash, _, _, _, err:=c.getBlockInfo(ctx, daIncludedHeight)
iferr!=nil {
returnfmt.Errorf("failed to get block info for DA height %d: %w", daIncludedHeight, err)
}
c.mu.Lock()
c.currentFinalizedBlockHash=finalizedHashc.mu.Unlock()
returnc.setFinal(ctx, finalizedHash, true)
}
Your key insight: In P2P-only nodes (DA-only sync disabled), there's no DA inclusion mechanism, so SetFinal() would never be called, leaving the finalized hash perpetually stuck at the genesis block.
Analysis of Current Architecture
I've traced through the codebase and found:
DA-Synced Nodes: The Submitter component (block/internal/submitting/submitter.go:266) calls exec.SetFinal() when blocks are DA-included
P2P-Only Nodes: The Syncer component (block/internal/syncing/syncer.go) handles P2P block synchronization but never calls SetFinal()
EVM State: The EngineClient tracks finalized hash in memory (execution/evm/execution.go:126) but has no persistence or depth-based finalization logic
Proposed Solution: Hybrid Finalization Strategy
I propose a two-pronged approach that works for both DA-synced and P2P-only nodes:
1. For DA-Synced Nodes: Use DA Inclusion (Current Behavior)
Continue using the existing SetFinal() mechanism in the Submitter
DA inclusion provides canonical finality
2. For P2P-Only Nodes: Implement Depth-Based Finalization
Introduce a configurable finalization depth (e.g., 64 blocks, similar to Ethereum's weak subjectivity)
Automatically call SetFinal() when a block is N blocks deep
This matches the behavior expected by indexers and provides consistency
3. Persist Block Hash State Across Restarts
Save head/safe/finalized hashes to store metadata
Load on restart to maintain consistency
Implementation Plan
Phase 1: Add Depth-Based Finalization to Syncer
Location: block/internal/syncing/syncer.go
Add a finalization mechanism that tracks block depth:
// In syncer.go processLoop or after applying blocksfunc (s*Syncer) maybeFinalizeByDepth(currentHeightuint64) error {
// Only use depth-based finalization if we're not using DA// (DA-based finalization takes precedence)ifs.config.DA.Address!="" {
returnnil// DA-synced nodes use DA inclusion for finality
}
// Use a safe finalization depth (e.g., 64 blocks)finalizationDepth:=uint64(64)
ifcurrentHeight<=finalizationDepth {
returnnil// Not enough blocks yet
}
finalizedHeight:=currentHeight-finalizationDepth// Check if we've already finalized this heightlastFinalizedHeight, err:=s.getLastFinalizedHeight()
iferr==nil&&lastFinalizedHeight>=finalizedHeight {
returnnil// Already finalized
}
// Finalize the block at depthiferr:=s.exec.SetFinal(s.ctx, finalizedHeight); err!=nil {
returnfmt.Errorf("failed to set final at depth: %w", err)
}
// Persist the finalized heightiferr:=s.setLastFinalizedHeight(finalizedHeight); err!=nil {
s.logger.Warn().Err(err).Msg("failed to persist finalized height")
}
returnnil
}
Phase 2: Persist EVM Block Hash State
Location: execution/evm/execution.go
Update the EngineClient to persist state:
// Add to setFinal method (around line 414)func (c*EngineClient) setFinal(ctx context.Context, blockHash common.Hash, isFinalbool) error {
c.mu.Lock()
ifisFinal {
c.currentFinalizedBlockHash=blockHash
} else {
c.currentHeadBlockHash=blockHashc.currentSafeBlockHash=blockHash
}
args:= engine.ForkchoiceStateV1{
HeadBlockHash: c.currentHeadBlockHash,
SafeBlockHash: c.currentSafeBlockHash,
FinalizedBlockHash: c.currentFinalizedBlockHash,
}
c.mu.Unlock()
// ... existing forkchoice call ...// NEW: Persist state for restart resilienceiferr:=c.persistBlockHashState(ctx); err!=nil {
c.logger.Warn().Err(err).Msg("failed to persist block hash state")
// Don't fail the operation if persistence fails
}
returnnil
}
// Add persistence helper (needs access to Store)func (c*EngineClient) persistBlockHashState(ctx context.Context) error {
state:=map[string]string{
"head": c.currentHeadBlockHash.Hex(),
"safe": c.currentSafeBlockHash.Hex(),
"finalized": c.currentFinalizedBlockHash.Hex(),
}
data, _:=json.Marshal(state)
// Store in a well-known metadata key// Note: EngineClient would need Store reference passed in constructorreturnc.store.SetMetadata(ctx, "evm_fork_choice_state", data)
}
Activity
tac0turtle commentedon Aug 8, 2025
@claude can you propose a solution to this issue
claude commentedon Aug 8, 2025
Claude finished @tac0turtle's task —— View job
EVM FinalizedHash Issue Analysis & Solution Proposal
I've analyzed the codebase and identified the issue with FinalizedHash setting in the EVM's
forkChoiceUpdatedV3call. Here's my comprehensive solution proposal:Todo List:
Problem Analysis
The issue is in
/execution/evm/execution.go:162-166where the firstforkChoiceUpdatedV3call incorrectly sets all three block hashes to the previous block hash:This was a temporary fix for sequencer restarts but causes indexers and block explorers to show inconsistent finalized block hashes.
Root Cause
FinalizedBlockHashtoprevBlockHashdoesn't reflect the actual finalized stateProposed Solution
I recommend Approach #1 from the issue description with enhancements:
1. Use DA Included Height for FinalizedBlockHash
The codebase already has a robust DA inclusion tracking system:
DAIncludedHeightis tracked in the store (pkg/store/keys.go:16)incrementDAIncludedHeight()inblock/da_includer.go:54callsSetFinal()when blocks are DA-included2. Enhanced Implementation Plan
Phase 1: Fix FinalizedBlockHash Logic
Phase 2: Integrate with DA Height System
Phase 3: Persist Block Hash State
Implementation Benefits
Integration Points
The solution integrates with existing systems:
block/da_includer.go:58already callsSetFinal()when blocks are DA-includedpkg/store/store.goTesting Strategy
Would you like me to implement this solution? The changes would be focused in the EVM execution package with minimal impact on other systems.
tac0turtle commentedon Dec 8, 2025
@claude if we never sync from da an only use p2p then the finalized blockhash would never be updated
claude commentedon Dec 8, 2025
Claude finished @tac0turtle's task —— View job
EVM FinalizedHash Issue - Revised Analysis
You're absolutely right - my previous solution has a critical flaw for P2P-only nodes. Let me propose a better approach.
Todo List:
Problem Summary
The current issue in
/execution/evm/execution.go:300-304sets all three block hashes (Head, Safe, Finalized) toprevBlockHash:Your key insight: In P2P-only nodes (DA-only sync disabled), there's no DA inclusion mechanism, so
SetFinal()would never be called, leaving the finalized hash perpetually stuck at the genesis block.Analysis of Current Architecture
I've traced through the codebase and found:
Submittercomponent (block/internal/submitting/submitter.go:266) callsexec.SetFinal()when blocks are DA-includedSyncercomponent (block/internal/syncing/syncer.go) handles P2P block synchronization but never callsSetFinal()EngineClienttracks finalized hash in memory (execution/evm/execution.go:126) but has no persistence or depth-based finalization logicProposed Solution: Hybrid Finalization Strategy
I propose a two-pronged approach that works for both DA-synced and P2P-only nodes:
1. For DA-Synced Nodes: Use DA Inclusion (Current Behavior)
SetFinal()mechanism in the Submitter2. For P2P-Only Nodes: Implement Depth-Based Finalization
SetFinal()when a block is N blocks deep3. Persist Block Hash State Across Restarts
Implementation Plan
Phase 1: Add Depth-Based Finalization to Syncer
Location:
block/internal/syncing/syncer.goAdd a finalization mechanism that tracks block depth:
Phase 2: Persist EVM Block Hash State
Location:
execution/evm/execution.goUpdate the
EngineClientto persist state:Phase 3: Fix Startup Logic
Location:
execution/evm/execution.goconstructorLoad persisted state on startup:
Alternative: Simpler Approach
If the above is too complex, here's a minimal fix that addresses the immediate issue:
Option A: Use a conservative finalization lag
In
execution/evm/execution.go:300-304, change to:This ensures:
Recommendation
I recommend implementing the full hybrid solution (Phases 1-3) because:
The simpler "Option A" works but sacrifices the accuracy of DA-based finality.
Would you like me to implement the full solution or the simpler approach?