mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-08 16:01:36 +00:00
core, eth: restore stateRoot field using atomic.Pointer
Live testing on bal-devnet-2 confirmed that computed roots DO diverge from header roots. Block 75315 computed root 0xe909c7.. vs header root 0x9acbbe.. — untracked contracts' storage roots in the local trie are from snap sync time and differ from the actual current roots, even when the storage root resolver successfully queries peers. This means subsequent blocks must chain off the computed root (via partialState.Root()), not the header root (via parent.Root()). Restore the stateRoot field using atomic.Pointer[common.Hash] instead of the previous sync.RWMutex for lock-free concurrent access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d50dee20ab
commit
962e2de6e1
5 changed files with 65 additions and 17 deletions
|
|
@ -3011,14 +3011,23 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) {
|
|||
|
||||
// Re-execute the reorged chain in case the head state is missing.
|
||||
if !bc.HasState(head.Root()) {
|
||||
// Partial state nodes can't re-execute blocks — they only apply BAL diffs.
|
||||
// The computed root may differ from the header root when untracked contracts
|
||||
// have unresolved storage roots. Check the partial state's tracked root too.
|
||||
if bc.partialState != nil {
|
||||
return common.Hash{}, fmt.Errorf("partial state: missing state for block %d root %x",
|
||||
head.NumberU64(), head.Root())
|
||||
partialRoot := bc.partialState.Root()
|
||||
if partialRoot == (common.Hash{}) || !bc.HasState(partialRoot) {
|
||||
return common.Hash{}, fmt.Errorf("partial state: missing state for block %d root %x", head.NumberU64(), head.Root())
|
||||
}
|
||||
log.Debug("SetCanonical: using partial state root (differs from header)",
|
||||
"block", head.NumberU64(), "headerRoot", head.Root(),
|
||||
"partialRoot", partialRoot)
|
||||
} else {
|
||||
if latestValidHash, err := bc.recoverAncestors(context.Background(), head, false); err != nil {
|
||||
return latestValidHash, err
|
||||
}
|
||||
log.Info("Recovered head state", "number", head.Number(), "hash", head.Hash())
|
||||
}
|
||||
if latestValidHash, err := bc.recoverAncestors(context.Background(), head, false); err != nil {
|
||||
return latestValidHash, err
|
||||
}
|
||||
log.Info("Recovered head state", "number", head.Number(), "hash", head.Hash())
|
||||
}
|
||||
// Run the reorg if necessary and set the given block as new head.
|
||||
start := time.Now()
|
||||
|
|
|
|||
|
|
@ -81,12 +81,18 @@ func (bc *BlockChain) ProcessBlockWithBAL(
|
|||
// balHash, block.Header().BlockAccessListHash)
|
||||
// }
|
||||
|
||||
// 3. Get parent state root from parent block header.
|
||||
parent := bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
|
||||
if parent == nil {
|
||||
return errors.New("parent block not found")
|
||||
// 3. Get parent state root. Use partialState's tracked root (the actual
|
||||
// computed root from the previous block) rather than the header root, which
|
||||
// may differ when untracked contracts have unresolved storage roots.
|
||||
parentRoot := bc.partialState.Root()
|
||||
if parentRoot == (common.Hash{}) {
|
||||
// First block after sync — use the parent block's header root
|
||||
parent := bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
|
||||
if parent == nil {
|
||||
return errors.New("parent block not found")
|
||||
}
|
||||
parentRoot = parent.Root()
|
||||
}
|
||||
parentRoot := parent.Root()
|
||||
|
||||
// 4. Apply BAL diffs and compute new state root.
|
||||
// Pass block.Root() as expectedRoot so the resolver can query peers for this
|
||||
|
|
@ -166,7 +172,10 @@ func (bc *BlockChain) HandlePartialReorg(
|
|||
}
|
||||
}
|
||||
|
||||
log.Debug("Starting partial state reorg from ancestor",
|
||||
// Step 1: Revert state to common ancestor
|
||||
bc.partialState.SetRoot(commonAncestor.Root())
|
||||
|
||||
log.Debug("Reverted partial state to ancestor",
|
||||
"ancestor", commonAncestor.Number(),
|
||||
"ancestorRoot", commonAncestor.Root().Hex(),
|
||||
"reorgDepth", reorgDepth)
|
||||
|
|
|
|||
|
|
@ -270,11 +270,16 @@ func TestHandlePartialReorg_EmptyNewBlocks(t *testing.T) {
|
|||
return &bal.BlockAccessList{}, nil
|
||||
}
|
||||
|
||||
// Empty reorg should succeed
|
||||
// Empty reorg should succeed (sets root to ancestor)
|
||||
err := bc.HandlePartialReorg(genesisBlock, newBlocks, getBAL)
|
||||
if err != nil {
|
||||
t.Fatalf("empty reorg should succeed: %v", err)
|
||||
}
|
||||
|
||||
// Verify state root is set to genesis root
|
||||
if bc.PartialState().Root() != genesisBlock.Root() {
|
||||
t.Errorf("expected root to be genesis root after empty reorg")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlePartialReorg_MissingBAL tests error when BAL is missing for a block.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package partial
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
|
|
@ -49,7 +50,8 @@ type PartialState struct {
|
|||
history *BALHistory
|
||||
resolver StorageRootResolver // optional, for resolving untracked storage roots
|
||||
|
||||
lastProcessedNum uint64 // last block successfully processed via BAL
|
||||
stateRoot atomic.Pointer[common.Hash] // computed root (may differ from header root)
|
||||
lastProcessedNum uint64 // last block successfully processed via BAL
|
||||
}
|
||||
|
||||
// SetResolver sets the storage root resolver used to fetch updated storage roots
|
||||
|
|
@ -73,6 +75,19 @@ func (s *PartialState) Filter() ContractFilter {
|
|||
return s.filter
|
||||
}
|
||||
|
||||
// SetRoot atomically sets the current computed state root.
|
||||
func (s *PartialState) SetRoot(root common.Hash) {
|
||||
s.stateRoot.Store(&root)
|
||||
}
|
||||
|
||||
// Root atomically returns the current computed state root.
|
||||
func (s *PartialState) Root() common.Hash {
|
||||
if p := s.stateRoot.Load(); p != nil {
|
||||
return *p
|
||||
}
|
||||
return common.Hash{}
|
||||
}
|
||||
|
||||
// History returns the BAL history manager.
|
||||
func (s *PartialState) History() *BALHistory {
|
||||
return s.history
|
||||
|
|
@ -292,8 +307,9 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
|
|||
stateSet := s.buildStateSet(accounts, accessList)
|
||||
|
||||
// Compute unresolved count for caller to decide root mismatch severity.
|
||||
// The computed root should match the header root since we maintain the full
|
||||
// account trie and resolve storage roots for untracked contracts.
|
||||
// The computed root may differ from the header root when untracked contracts
|
||||
// have unresolved storage roots. Subsequent blocks must chain off the
|
||||
// computed root (via partialState.Root()), not the header root.
|
||||
unresolvedCount := 0
|
||||
if len(untrackedAddrs) > 0 {
|
||||
unresolvedCount = len(untrackedAddrs)
|
||||
|
|
@ -316,6 +332,7 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
|
|||
return common.Hash{}, 0, fmt.Errorf("failed to update trie db: %w", err)
|
||||
}
|
||||
|
||||
s.stateRoot.Store(&root)
|
||||
return root, unresolvedCount, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -300,8 +300,16 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
|
|||
// If we try to SetCanonical, it will fail because HasState returns false and
|
||||
// partial state can't recoverAncestors. Instead, treat it like an unknown
|
||||
// block and trigger BeaconSync so the skeleton can start the sync cycle.
|
||||
//
|
||||
// After sync, the computed root may differ from the header root (unresolved
|
||||
// untracked storage roots), so we also check partialState's tracked root.
|
||||
partialRoot := common.Hash{}
|
||||
if api.eth.BlockChain().SupportsPartialState() {
|
||||
partialRoot = api.eth.BlockChain().PartialState().Root()
|
||||
}
|
||||
if api.eth.BlockChain().SupportsPartialState() &&
|
||||
!api.eth.BlockChain().HasState(block.Root()) {
|
||||
!api.eth.BlockChain().HasState(block.Root()) &&
|
||||
(partialRoot == common.Hash{} || !api.eth.BlockChain().HasState(partialRoot)) {
|
||||
log.Info("Forkchoice: block known but stateless (partial state sync in progress), triggering BeaconSync",
|
||||
"number", block.NumberU64(), "hash", update.HeadBlockHash, "root", block.Root())
|
||||
finalized := api.remoteBlocks.get(update.FinalizedBlockHash)
|
||||
|
|
|
|||
Loading…
Reference in a new issue