core, eth: PR review fixes and remove stateRoot field from PartialState

Apply review fixes: BAL iterator start (Fix 2), fatal root mismatch when
all storage resolved (Fix 3), WriteBlockWithoutState error handling (Fix 4),
contract filter construction order (Fix 5), canonical hash backfill (Fix 6),
underflow guard in gap processing (Fix 8), O(n²) prepend fix (Fix 9),
ReadBALHistory corruption detection (Fix 11), incomplete resolution error
(Fix 13), RLP encode panic (Fix 14), gap processing log level (Fix 16),
TriggerPartialResync message (Fix 18), and comment accuracy fixes.

Remove the stateRoot field and sync.RWMutex from PartialState entirely.
Since partial state maintains the full account trie, the computed root
always matches the header root (assuming storage root resolution succeeds).
ProcessBlockWithBAL now derives parent root from parent.Root() directly,
matching how full nodes derive state root from currentBlock headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CPerezz 2026-02-18 10:54:39 +01:00
parent e59c92959c
commit d50dee20ab
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
11 changed files with 130 additions and 153 deletions

View file

@ -1389,16 +1389,21 @@ func (bc *BlockChain) AdvancePartialHead(hash common.Hash) error {
// Write canonical hashes for all blocks between the old head and the new head. // Write canonical hashes for all blocks between the old head and the new head.
// During snap sync, InsertReceiptChain skips blocks that already have bodies // During snap sync, InsertReceiptChain skips blocks that already have bodies
// (HasBlock returns true), so canonical hashes aren't written for post-pivot // (HasBlock returns true), so canonical hashes aren't written for post-pivot
// blocks. We backfill them here by walking from the new head back to the // blocks. We backfill them here by walking backward from the new block via
// current canonical head. // ParentHash() — this avoids relying on GetHeaderByNumber which itself
// depends on canonical hash mappings that don't exist yet.
batch := bc.db.NewBatch() batch := bc.db.NewBatch()
currentHead := bc.CurrentBlock() currentHead := bc.CurrentBlock()
for num := block.NumberU64(); num > currentHead.Number.Uint64(); num-- { current := block.Header()
h := bc.GetHeaderByNumber(num) for current.Number.Uint64() > currentHead.Number.Uint64() {
if h == nil { rawdb.WriteCanonicalHash(batch, current.Hash(), current.Number.Uint64())
parent := bc.GetHeader(current.ParentHash, current.Number.Uint64()-1)
if parent == nil {
log.Warn("Missing parent during canonical hash backfill",
"number", current.Number.Uint64()-1, "target", block.NumberU64())
break break
} }
rawdb.WriteCanonicalHash(batch, h.Hash(), num) current = parent
} }
rawdb.WriteHeadBlockHash(batch, block.Hash()) rawdb.WriteHeadBlockHash(batch, block.Hash())
rawdb.WriteHeadHeaderHash(batch, block.Hash()) rawdb.WriteHeadHeaderHash(batch, block.Hash())
@ -1836,8 +1841,8 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
} }
// WriteBlockWithoutState writes only the block and its metadata to the database, // WriteBlockWithoutState writes only the block and its metadata to the database,
// but does not write any state. This is used to construct competing side forks // but does not write any state. Used by the Engine API to persist blocks before
// up to the point where they exceed the canonical total difficulty. // state is available (e.g., during partial state sync or when the parent is unknown).
func (bc *BlockChain) WriteBlockWithoutState(block *types.Block) (err error) { func (bc *BlockChain) WriteBlockWithoutState(block *types.Block) (err error) {
if bc.insertStopped() { if bc.insertStopped() {
return errInsertionInterrupted return errInsertionInterrupted
@ -3006,23 +3011,14 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) {
// Re-execute the reorged chain in case the head state is missing. // Re-execute the reorged chain in case the head state is missing.
if !bc.HasState(head.Root()) { 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 { if bc.partialState != nil {
partialRoot := bc.partialState.Root() return common.Hash{}, fmt.Errorf("partial state: missing state for block %d root %x",
if partialRoot == (common.Hash{}) || !bc.HasState(partialRoot) { head.NumberU64(), head.Root())
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. // Run the reorg if necessary and set the given block as new head.
start := time.Now() start := time.Now()

View file

@ -37,8 +37,8 @@ var ErrDeepReorg = errors.New("reorg depth exceeds BAL retention")
// # Trust Model - Why We Don't Re-Verify Consensus Attestations // # Trust Model - Why We Don't Re-Verify Consensus Attestations
// //
// Post-Merge (PoS) Architecture Trust Boundary: // Post-Merge (PoS) Architecture Trust Boundary:
// - Consensus Layer (CL): Responsible for block proposal, attestations (2/3+ sync committee // - Consensus Layer (CL): Responsible for block proposal, validator attestations,
// threshold), finality proofs, proposer signatures, and all consensus rules // finality (Casper FFG), proposer signatures, and all consensus rules
// - Execution Layer (EL): Responsible for transaction execution, state computation, receipts // - Execution Layer (EL): Responsible for transaction execution, state computation, receipts
// //
// Blocks received via Engine API (engine_newPayloadV5) have ALREADY been attested by the CL // Blocks received via Engine API (engine_newPayloadV5) have ALREADY been attested by the CL
@ -81,31 +81,32 @@ func (bc *BlockChain) ProcessBlockWithBAL(
// balHash, block.Header().BlockAccessListHash) // balHash, block.Header().BlockAccessListHash)
// } // }
// 3. Get parent state root. Use partialState's tracked root (the actual // 3. Get parent state root from parent block header.
// computed root from the previous block) rather than the header root, which parent := bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
// may differ when untracked contracts have unresolved storage roots. if parent == nil {
parentRoot := bc.partialState.Root() return errors.New("parent block not found")
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. // 4. Apply BAL diffs and compute new state root.
// Pass block.Root() as expectedRoot so the resolver can query peers for this // Pass block.Root() as expectedRoot so the resolver can query peers for this
// state's untracked contracts. // state's untracked contracts.
newRoot, err := bc.partialState.ApplyBALAndComputeRoot(parentRoot, block.Root(), accessList) newRoot, unresolved, err := bc.partialState.ApplyBALAndComputeRoot(parentRoot, block.Root(), accessList)
if err != nil { if err != nil {
return fmt.Errorf("failed to apply BAL: %w", err) return fmt.Errorf("failed to apply BAL: %w", err)
} }
// 5. Verify computed root matches header (warning, not fatal — may use fallback) // 5. Verify computed root matches header.
// If all storage roots were resolved, a mismatch indicates a real bug.
// If some were unresolved, a mismatch is expected (stale storage roots).
if newRoot != block.Root() { if newRoot != block.Root() {
log.Warn("Partial state root sanity check", if unresolved == 0 {
"computed", newRoot, "header", block.Root(), "block", block.NumberU64()) return fmt.Errorf("state root mismatch (all storage resolved): computed %x, header %x, block %d",
newRoot, block.Root(), block.NumberU64())
}
log.Warn("Partial state root mismatch (unresolved storage roots)",
"computed", newRoot, "header", block.Root(), "block", block.NumberU64(),
"unresolved", unresolved)
} }
// 6. Track last processed block for gap detection and HasState checks. // 6. Track last processed block for gap detection and HasState checks.
@ -165,11 +166,7 @@ func (bc *BlockChain) HandlePartialReorg(
} }
} }
// Step 1: Revert state to common ancestor log.Debug("Starting partial state reorg from ancestor",
// Simply set state root to ancestor's root (we have all account trie data)
bc.partialState.SetRoot(commonAncestor.Root())
log.Debug("Reverted partial state to ancestor",
"ancestor", commonAncestor.Number(), "ancestor", commonAncestor.Number(),
"ancestorRoot", commonAncestor.Root().Hex(), "ancestorRoot", commonAncestor.Root().Hex(),
"reorgDepth", reorgDepth) "reorgDepth", reorgDepth)
@ -233,5 +230,5 @@ func (bc *BlockChain) TriggerPartialResync(ancestor *types.Header) error {
// 2. Use snap sync to fetch state at ancestor.Root // 2. Use snap sync to fetch state at ancestor.Root
// 3. Apply ContractFilter to only store tracked contract storage // 3. Apply ContractFilter to only store tracked contract storage
// 4. Resume normal operation once state is available // 4. Resume normal operation once state is available
return errors.New("partial state resync not yet implemented - manual intervention required") return errors.New("partial state resync not yet implemented: restart node to re-sync from scratch, or increase --partial-state.bal-retention to handle deeper reorgs")
} }

View file

@ -19,6 +19,7 @@ package core
import ( import (
"bytes" "bytes"
"math/big" "math/big"
"strings"
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -189,11 +190,14 @@ func TestProcessBlockWithBAL_StateRootMismatch(t *testing.T) {
} }
accessList := constructionToBlockAccessListCore(t, &cbal) accessList := constructionToBlockAccessListCore(t, &cbal)
// Root mismatch is now a warning, not an error — the expectedRoot fallback // When all storage roots are resolved (no untracked contracts), a root
// is used as the PathDB layer label when peer resolution isn't available. // mismatch is a fatal error — it indicates a real inconsistency.
err := bc.ProcessBlockWithBAL(block, accessList) err := bc.ProcessBlockWithBAL(block, accessList)
if err != nil { if err == nil {
t.Fatalf("unexpected error (root mismatch should be a warning): %v", err) t.Fatal("expected error for state root mismatch with no unresolved storage, got nil")
}
if !strings.Contains(err.Error(), "state root mismatch") {
t.Fatalf("expected state root mismatch error, got: %v", err)
} }
} }
@ -266,16 +270,11 @@ func TestHandlePartialReorg_EmptyNewBlocks(t *testing.T) {
return &bal.BlockAccessList{}, nil return &bal.BlockAccessList{}, nil
} }
// Empty reorg should succeed (just sets root to ancestor) // Empty reorg should succeed
err := bc.HandlePartialReorg(genesisBlock, newBlocks, getBAL) err := bc.HandlePartialReorg(genesisBlock, newBlocks, getBAL)
if err != nil { if err != nil {
t.Fatalf("empty reorg should succeed: %v", err) 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. // TestHandlePartialReorg_MissingBAL tests error when BAL is missing for a block.

View file

@ -18,6 +18,7 @@ package rawdb
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
@ -35,18 +36,21 @@ func balHistoryKey(blockNum uint64) []byte {
} }
// ReadBALHistory retrieves the Block Access List for a specific block number. // ReadBALHistory retrieves the Block Access List for a specific block number.
// Returns nil if the BAL is not found or cannot be decoded. // Returns (nil, nil) if the BAL is not found.
func ReadBALHistory(db ethdb.KeyValueReader, blockNum uint64) *bal.BlockAccessList { // Returns (nil, error) if the BAL exists but is corrupted.
func ReadBALHistory(db ethdb.KeyValueReader, blockNum uint64) (*bal.BlockAccessList, error) {
data, err := db.Get(balHistoryKey(blockNum)) data, err := db.Get(balHistoryKey(blockNum))
if err != nil || len(data) == 0 { if err != nil {
return nil return nil, nil // Not found (leveldb returns error for missing keys)
}
if len(data) == 0 {
return nil, nil
} }
var accessList bal.BlockAccessList var accessList bal.BlockAccessList
if err := rlp.DecodeBytes(data, &accessList); err != nil { if err := rlp.DecodeBytes(data, &accessList); err != nil {
log.Warn("Failed to decode BAL history", "block", blockNum, "err", err) return nil, fmt.Errorf("corrupted BAL at block %d: %w", blockNum, err)
return nil
} }
return &accessList return &accessList, nil
} }
// WriteBALHistory stores a Block Access List for a specific block number. // WriteBALHistory stores a Block Access List for a specific block number.
@ -70,34 +74,20 @@ func DeleteBALHistory(db ethdb.KeyValueWriter, blockNum uint64) {
// PruneBALHistory removes all BALs before the specified block number. // PruneBALHistory removes all BALs before the specified block number.
// This uses range iteration for safe, interruptible pruning. // This uses range iteration for safe, interruptible pruning.
func PruneBALHistory(db ethdb.Database, beforeBlock uint64) error { func PruneBALHistory(db ethdb.Database, beforeBlock uint64) error {
// Create iterator for BAL history range
start := balHistoryKey(0)
end := balHistoryKey(beforeBlock)
// Use batch deletion for efficiency
batch := db.NewBatch() batch := db.NewBatch()
it := db.NewIterator(balHistoryPrefix, start) it := db.NewIterator(balHistoryPrefix, nil) // nil = start from beginning of prefix
defer it.Release() defer it.Release()
deleted := 0 deleted := 0
for it.Next() { for it.Next() {
key := it.Key() key := it.Key()
// Stop if we've passed the end key // Extract block number and stop if we've passed the target
if len(key) >= len(balHistoryPrefix)+8 { if len(key) >= len(balHistoryPrefix)+8 {
blockNum := binary.BigEndian.Uint64(key[len(balHistoryPrefix):]) blockNum := binary.BigEndian.Uint64(key[len(balHistoryPrefix):])
if blockNum >= beforeBlock { if blockNum >= beforeBlock {
break break
} }
} }
// Check if key is within our prefix
if len(key) < len(balHistoryPrefix) {
continue
}
for i := range balHistoryPrefix {
if key[i] != balHistoryPrefix[i] {
goto done
}
}
batch.Delete(key) batch.Delete(key)
deleted++ deleted++
@ -109,7 +99,6 @@ func PruneBALHistory(db ethdb.Database, beforeBlock uint64) error {
batch.Reset() batch.Reset()
} }
} }
done:
// Write remaining items // Write remaining items
if batch.ValueSize() > 0 { if batch.ValueSize() > 0 {
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
@ -119,7 +108,6 @@ done:
if deleted > 0 { if deleted > 0 {
log.Debug("Pruned BAL history", "deleted", deleted, "beforeBlock", beforeBlock) log.Debug("Pruned BAL history", "deleted", deleted, "beforeBlock", beforeBlock)
} }
_ = end // silence unused variable warning (used for documentation)
return it.Error() return it.Error()
} }

View file

@ -20,6 +20,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
) )
// BALHistory manages storage and retrieval of Block Access Lists for reorg handling. // BALHistory manages storage and retrieval of Block Access Lists for reorg handling.
@ -45,7 +46,11 @@ func (h *BALHistory) Store(blockNum uint64, accessList *bal.BlockAccessList) {
// Get retrieves the BAL for a specific block number. // Get retrieves the BAL for a specific block number.
// Returns nil, false if not found. // Returns nil, false if not found.
func (h *BALHistory) Get(blockNum uint64) (*bal.BlockAccessList, bool) { func (h *BALHistory) Get(blockNum uint64) (*bal.BlockAccessList, bool) {
accessList := rawdb.ReadBALHistory(h.db, blockNum) accessList, err := rawdb.ReadBALHistory(h.db, blockNum)
if err != nil {
log.Error("Corrupted BAL history entry", "block", blockNum, "err", err)
return nil, false
}
return accessList, accessList != nil return accessList, accessList != nil
} }

View file

@ -49,11 +49,7 @@ type PartialState struct {
history *BALHistory history *BALHistory
resolver StorageRootResolver // optional, for resolving untracked storage roots resolver StorageRootResolver // optional, for resolving untracked storage roots
// Current state root (the actual computed root, may differ from header root) lastProcessedNum uint64 // last block successfully processed via BAL
stateRoot common.Hash
// Last block successfully processed via BAL
lastProcessedNum uint64
} }
// SetResolver sets the storage root resolver used to fetch updated storage roots // SetResolver sets the storage root resolver used to fetch updated storage roots
@ -77,16 +73,6 @@ func (s *PartialState) Filter() ContractFilter {
return s.filter return s.filter
} }
// SetRoot sets the current state root.
func (s *PartialState) SetRoot(root common.Hash) {
s.stateRoot = root
}
// Root returns the current state root.
func (s *PartialState) Root() common.Hash {
return s.stateRoot
}
// History returns the BAL history manager. // History returns the BAL history manager.
func (s *PartialState) History() *BALHistory { func (s *PartialState) History() *BALHistory {
return s.history return s.history
@ -125,11 +111,11 @@ type accountState struct {
// Phase 1.5: Resolve storage roots for untracked contracts with storage changes // Phase 1.5: Resolve storage roots for untracked contracts with storage changes
// Phase 2: Update account Root fields with committed storage roots // Phase 2: Update account Root fields with committed storage roots
// Phase 3: Commit account trie to get final state root // Phase 3: Commit account trie to get final state root
func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRoot common.Hash, accessList *bal.BlockAccessList) (common.Hash, error) { func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRoot common.Hash, accessList *bal.BlockAccessList) (common.Hash, int, error) {
// Open state trie at parent root // Open state trie at parent root
tr, err := trie.NewStateTrie(trie.StateTrieID(parentRoot), s.trieDB) tr, err := trie.NewStateTrie(trie.StateTrieID(parentRoot), s.trieDB)
if err != nil { if err != nil {
return common.Hash{}, fmt.Errorf("failed to open state trie: %w", err) return common.Hash{}, 0, fmt.Errorf("failed to open state trie: %w", err)
} }
// Collect all account states with origin tracking // Collect all account states with origin tracking
@ -145,7 +131,7 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
// Get current account state with origin tracking // Get current account state with origin tracking
data, err := tr.GetAccount(addr) data, err := tr.GetAccount(addr)
if err != nil { if err != nil {
return common.Hash{}, fmt.Errorf("failed to get account %s: %w", addr.Hex(), err) return common.Hash{}, 0, fmt.Errorf("failed to get account %s: %w", addr.Hex(), err)
} }
existed := data != nil existed := data != nil
@ -214,7 +200,7 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
newStorageRoot, storageNodes, err := s.applyStorageChanges( newStorageRoot, storageNodes, err := s.applyStorageChanges(
addr, parentRoot, account.Root, &access) addr, parentRoot, account.Root, &access)
if err != nil { if err != nil {
return common.Hash{}, fmt.Errorf("failed to apply storage for %s: %w", return common.Hash{}, 0, fmt.Errorf("failed to apply storage for %s: %w",
addr.Hex(), err) addr.Hex(), err)
} }
state.storageRoot = newStorageRoot state.storageRoot = newStorageRoot
@ -223,7 +209,7 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
// Merge storage nodes // Merge storage nodes
if storageNodes != nil { if storageNodes != nil {
if err := allNodes.Merge(storageNodes); err != nil { if err := allNodes.Merge(storageNodes); err != nil {
return common.Hash{}, err return common.Hash{}, 0, err
} }
} }
} }
@ -278,7 +264,7 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
// Only delete if it existed before (don't delete never-existed accounts) // Only delete if it existed before (don't delete never-existed accounts)
if state.existed { if state.existed {
if err := tr.DeleteAccount(state.addr); err != nil { if err := tr.DeleteAccount(state.addr); err != nil {
return common.Hash{}, fmt.Errorf("failed to delete account %s: %w", return common.Hash{}, 0, fmt.Errorf("failed to delete account %s: %w",
state.addr.Hex(), err) state.addr.Hex(), err)
} }
} }
@ -287,7 +273,7 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
} }
if err := tr.UpdateAccount(state.addr, state.account, 0); err != nil { if err := tr.UpdateAccount(state.addr, state.account, 0); err != nil {
return common.Hash{}, fmt.Errorf("failed to update account %s: %w", return common.Hash{}, 0, fmt.Errorf("failed to update account %s: %w",
state.addr.Hex(), err) state.addr.Hex(), err)
} }
} }
@ -298,19 +284,19 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
// Merge account nodes // Merge account nodes
if accountNodes != nil { if accountNodes != nil {
if err := allNodes.Merge(accountNodes); err != nil { if err := allNodes.Merge(accountNodes); err != nil {
return common.Hash{}, err return common.Hash{}, 0, err
} }
} }
// Build StateSet for PathDB compatibility // Build StateSet for PathDB compatibility
stateSet := s.buildStateSet(accounts, accessList) stateSet := s.buildStateSet(accounts, accessList)
// Always use the actual computed root for the PathDB layer. Even if untracked // Compute unresolved count for caller to decide root mismatch severity.
// contracts have stale storage roots (making the computed root differ from the // The computed root should match the header root since we maintain the full
// header), subsequent blocks must chain off the real trie structure. // account trie and resolve storage roots for untracked contracts.
// ProcessBlockWithBAL uses partialState.Root() (not header root) as parentRoot. unresolvedCount := 0
if len(untrackedAddrs) > 0 { if len(untrackedAddrs) > 0 {
unresolvedCount := len(untrackedAddrs) unresolvedCount = len(untrackedAddrs)
if resolved != nil { if resolved != nil {
for _, addr := range untrackedAddrs { for _, addr := range untrackedAddrs {
if _, ok := resolved[addr]; ok { if _, ok := resolved[addr]; ok {
@ -327,11 +313,10 @@ func (s *PartialState) ApplyBALAndComputeRoot(parentRoot common.Hash, expectedRo
// Write all trie nodes and state to database // Write all trie nodes and state to database
if err := s.trieDB.Update(root, parentRoot, 0, allNodes, stateSet); err != nil { if err := s.trieDB.Update(root, parentRoot, 0, allNodes, stateSet); err != nil {
return common.Hash{}, fmt.Errorf("failed to update trie db: %w", err) return common.Hash{}, 0, fmt.Errorf("failed to update trie db: %w", err)
} }
s.stateRoot = root return root, unresolvedCount, nil
return root, nil
} }
// buildStateSet constructs StateSet for trieDB.Update() (required for PathDB). // buildStateSet constructs StateSet for trieDB.Update() (required for PathDB).
@ -386,7 +371,10 @@ func (s *PartialState) addStorageToStateSet(stateSet *triedb.StateSet, addr comm
storageMap[slotHash] = nil // nil = deletion storageMap[slotHash] = nil // nil = deletion
} else { } else {
// Prefix-zero-trimmed RLP encoding // Prefix-zero-trimmed RLP encoding
blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:])) blob, err := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:]))
if err != nil {
panic(fmt.Sprintf("failed to RLP-encode storage value: %v", err))
}
storageMap[slotHash] = blob storageMap[slotHash] = blob
} }
} }

View file

@ -157,7 +157,7 @@ func TestApplyBALAndComputeRoot_EmptyBAL(t *testing.T) {
emptyBAL := bal.BlockAccessList{} emptyBAL := bal.BlockAccessList{}
accessList := &emptyBAL accessList := &emptyBAL
newRoot, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply empty BAL: %v", err) t.Fatalf("failed to apply empty BAL: %v", err)
} }
@ -189,7 +189,7 @@ func TestApplyBALAndComputeRoot_BalanceChange(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -232,7 +232,7 @@ func TestApplyBALAndComputeRoot_NonceChange(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -273,7 +273,7 @@ func TestApplyBALAndComputeRoot_StorageChange(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -333,7 +333,7 @@ func TestApplyBALAndComputeRoot_UntrackedContractStorageIgnored(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -365,7 +365,7 @@ func TestApplyBALAndComputeRoot_NewAccount(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -412,7 +412,7 @@ func TestApplyBALAndComputeRoot_CodeChange(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -459,7 +459,7 @@ func TestApplyBALAndComputeRoot_MultipleTransactions(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -527,7 +527,7 @@ func TestApplyBALAndComputeRoot_StorageDeletion(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -567,7 +567,7 @@ func TestApplyBALAndComputeRoot_MultipleStorageWritesSameSlot(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -613,7 +613,7 @@ func TestApplyBALAndComputeRoot_AccountDeletion_EIP161(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -642,7 +642,7 @@ func TestApplyBALAndComputeRoot_NeverExistedEmptyAccount(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(emptyRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -700,7 +700,7 @@ func TestApplyBALAndComputeRoot_CodeChangeUntracked(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -762,7 +762,7 @@ func TestApplyBALAndComputeRoot_MixedChanges(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -809,7 +809,7 @@ func TestApplyBALAndComputeRoot_ErrorInvalidParentRoot(t *testing.T) {
cbal.BalanceChange(0, addr, uint256.NewInt(1000)) cbal.BalanceChange(0, addr, uint256.NewInt(1000))
accessList := cbal.Build(t) accessList := cbal.Build(t)
_, err := ps.ApplyBALAndComputeRoot(invalidRoot, common.Hash{}, accessList) _, _, err := ps.ApplyBALAndComputeRoot(invalidRoot, common.Hash{}, accessList)
if err == nil { if err == nil {
t.Fatal("expected error for invalid parent root, got nil") t.Fatal("expected error for invalid parent root, got nil")
} }
@ -928,7 +928,7 @@ func TestBuildStateSet_AccountModification(t *testing.T) {
cbal.BalanceChange(0, addr, uint256.NewInt(2000)) cbal.BalanceChange(0, addr, uint256.NewInt(2000))
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -973,7 +973,7 @@ func TestBuildStateSet_StorageRLPEncoding(t *testing.T) {
cbal.StorageWrite(0, addr, slot, value) cbal.StorageWrite(0, addr, slot, value)
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -1019,7 +1019,7 @@ func TestBuildStateSet_OriginTracking(t *testing.T) {
cbal.NonceChange(addr, 0, 11) cbal.NonceChange(addr, 0, 11)
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }
@ -1092,7 +1092,7 @@ func TestApplyBALAndComputeRoot_MultipleAccountTypes(t *testing.T) {
accessList := cbal.Build(t) accessList := cbal.Build(t)
newRoot, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList) newRoot, _, err := ps.ApplyBALAndComputeRoot(parentRoot, common.Hash{}, accessList)
if err != nil { if err != nil {
t.Fatalf("failed to apply BAL: %v", err) t.Fatalf("failed to apply BAL: %v", err)
} }

View file

@ -284,8 +284,13 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
options.Overrides = &overrides options.Overrides = &overrides
options.BALExecutionMode = config.BALExecutionMode options.BALExecutionMode = config.BALExecutionMode
// Wire partial state configuration into the blockchain // Wire partial state configuration into the blockchain.
// Load contracts from file FIRST, before wiring into blockchain, so both
// blockchain and downloader see the same contract list.
if config.PartialState.Enabled { if config.PartialState.Enabled {
if err := config.PartialState.LoadPartialStateContracts(); err != nil {
return nil, fmt.Errorf("failed to load partial state contracts: %w", err)
}
options.PartialStateEnabled = true options.PartialStateEnabled = true
options.PartialStateContracts = config.PartialState.Contracts options.PartialStateContracts = config.PartialState.Contracts
options.PartialStateBALRetention = config.PartialState.BALRetention options.PartialStateBALRetention = config.PartialState.BALRetention
@ -348,12 +353,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
// Permit the downloader to use the trie cache allowance during fast sync // Permit the downloader to use the trie cache allowance during fast sync
cacheLimit := options.TrieCleanLimit + options.TrieDirtyLimit + options.SnapshotLimit cacheLimit := options.TrieCleanLimit + options.TrieDirtyLimit + options.SnapshotLimit
// Create partial state filter if enabled // Create partial state filter if enabled (contracts already loaded above)
var partialFilter partial.ContractFilter var partialFilter partial.ContractFilter
if config.PartialState.Enabled { if config.PartialState.Enabled {
if err := config.PartialState.LoadPartialStateContracts(); err != nil {
return nil, fmt.Errorf("failed to load partial state contracts: %w", err)
}
partialFilter = partial.NewConfiguredFilter(config.PartialState.Contracts) partialFilter = partial.NewConfiguredFilter(config.PartialState.Contracts)
log.Info("Partial state mode enabled", log.Info("Partial state mode enabled",
"contracts", len(config.PartialState.Contracts), "contracts", len(config.PartialState.Contracts),

View file

@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
"slices"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -299,16 +300,8 @@ func (api *ConsensusAPI) forkchoiceUpdated(ctx context.Context, update engine.Fo
// If we try to SetCanonical, it will fail because HasState returns false and // If we try to SetCanonical, it will fail because HasState returns false and
// partial state can't recoverAncestors. Instead, treat it like an unknown // partial state can't recoverAncestors. Instead, treat it like an unknown
// block and trigger BeaconSync so the skeleton can start the sync cycle. // 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() && 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", log.Info("Forkchoice: block known but stateless (partial state sync in progress), triggering BeaconSync",
"number", block.NumberU64(), "hash", update.HeadBlockHash, "root", block.Root()) "number", block.NumberU64(), "hash", update.HeadBlockHash, "root", block.Root())
finalized := api.remoteBlocks.get(update.FinalizedBlockHash) finalized := api.remoteBlocks.get(update.FinalizedBlockHash)
@ -885,6 +878,7 @@ func (api *ConsensusAPI) newPayload(ctx context.Context, params engine.Executabl
if api.eth.BlockChain().SupportsPartialState() { if api.eth.BlockChain().SupportsPartialState() {
if err := api.eth.BlockChain().WriteBlockWithoutState(block); err != nil { if err := api.eth.BlockChain().WriteBlockWithoutState(block); err != nil {
log.Warn("NewPayload: failed to persist block for partial state catch-up", "number", block.NumberU64(), "err", err) log.Warn("NewPayload: failed to persist block for partial state catch-up", "number", block.NumberU64(), "err", err)
return engine.PayloadStatusV1{Status: engine.SYNCING}, nil
} }
if params.BlockAccessList != nil { if params.BlockAccessList != nil {
rawdb.WriteAccessList(api.eth.ChainDb(), block.Hash(), block.NumberU64(), params.BlockAccessList) rawdb.WriteAccessList(api.eth.ChainDb(), block.Hash(), block.NumberU64(), params.BlockAccessList)
@ -909,6 +903,7 @@ func (api *ConsensusAPI) newPayload(ctx context.Context, params engine.Executabl
if api.eth.BlockChain().SupportsPartialState() { if api.eth.BlockChain().SupportsPartialState() {
if err := api.eth.BlockChain().WriteBlockWithoutState(block); err != nil { if err := api.eth.BlockChain().WriteBlockWithoutState(block); err != nil {
log.Warn("NewPayload: failed to persist block for partial state catch-up", "number", block.NumberU64(), "err", err) log.Warn("NewPayload: failed to persist block for partial state catch-up", "number", block.NumberU64(), "err", err)
return engine.PayloadStatusV1{Status: engine.SYNCING}, nil
} }
if params.BlockAccessList != nil { if params.BlockAccessList != nil {
rawdb.WriteAccessList(api.eth.ChainDb(), block.Hash(), block.NumberU64(), params.BlockAccessList) rawdb.WriteAccessList(api.eth.ChainDb(), block.Hash(), block.NumberU64(), params.BlockAccessList)
@ -922,12 +917,13 @@ func (api *ConsensusAPI) newPayload(ctx context.Context, params engine.Executabl
if api.eth.BlockChain().SupportsPartialState() && params.BlockAccessList != nil { if api.eth.BlockChain().SupportsPartialState() && params.BlockAccessList != nil {
log.Info("NewPayload: entering BAL processing path", log.Info("NewPayload: entering BAL processing path",
"number", block.NumberU64(), "hash", block.Hash(), "number", block.NumberU64(), "hash", block.Hash(),
"parent", parent.NumberU64(), "hasBAL", params.BlockAccessList != nil) "parent", parent.NumberU64())
// Before processing this block, catch up any unprocessed ancestor // Before processing this block, catch up any unprocessed ancestor
// blocks that accumulated during the second state sync phase. Their // blocks that accumulated during the second state sync phase. Their
// bodies and BALs were persisted to the database when delayed. // bodies and BALs were persisted to the database when delayed.
if err := api.processPartialStateGap(block); err != nil { if err := api.processPartialStateGap(block); err != nil {
log.Warn("Failed to process partial state gap", "block", block.NumberU64(), "error", err) log.Error("Failed to process partial state gap, delaying block",
"block", block.NumberU64(), "error", err)
return api.delayPayloadImport(block), nil return api.delayPayloadImport(block), nil
} }
log.Trace("Processing block with BAL (partial state mode)", "hash", block.Hash(), "number", block.Number()) log.Trace("Processing block with BAL (partial state mode)", "hash", block.Hash(), "number", block.Number())
@ -942,8 +938,7 @@ func (api *ConsensusAPI) newPayload(ctx context.Context, params engine.Executabl
} }
processingTime := time.Since(start) processingTime := time.Since(start)
// Write block to DB so ForkchoiceUpdated can find it via GetBlockByHash. // Write block (header + body) to DB so ForkchoiceUpdated can find it via GetBlockByHash.
// This writes header + body + BAL without requiring receipts or full state.
if err := api.eth.BlockChain().WriteBlockWithoutState(block); err != nil { if err := api.eth.BlockChain().WriteBlockWithoutState(block); err != nil {
return api.invalid(err, parent.Header()), nil return api.invalid(err, parent.Header()), nil
} }
@ -1054,6 +1049,9 @@ func (api *ConsensusAPI) processPartialStateGap(target *types.Block) error {
var gap []*types.Block var gap []*types.Block
current := target current := target
for { for {
if current.NumberU64() == 0 {
break
}
parentHash := current.ParentHash() parentHash := current.ParentHash()
parentNum := current.NumberU64() - 1 parentNum := current.NumberU64() - 1
@ -1067,9 +1065,10 @@ func (api *ConsensusAPI) processPartialStateGap(target *types.Block) error {
if bc.HasState(parent.Root()) || parent.NumberU64() <= bc.PartialState().LastProcessedBlock() { if bc.HasState(parent.Root()) || parent.NumberU64() <= bc.PartialState().LastProcessedBlock() {
break // Found an ancestor with state — this is our starting point break // Found an ancestor with state — this is our starting point
} }
gap = append([]*types.Block{parent}, gap...) gap = append(gap, parent)
current = parent current = parent
} }
slices.Reverse(gap)
if len(gap) == 0 { if len(gap) == 0 {
return nil // No gap to fill return nil // No gap to fill
} }

View file

@ -98,6 +98,9 @@ func (h *handler) ResolveStorageRoots(
log.Warn("Failed to resolve storage root", "addr", addr, "attempts", storageRootMaxRetries) log.Warn("Failed to resolve storage root", "addr", addr, "attempts", storageRootMaxRetries)
} }
} }
if len(resolved) < len(addrs) {
return resolved, fmt.Errorf("resolved %d/%d storage roots", len(resolved), len(addrs))
}
return resolved, nil return resolved, nil
} }

View file

@ -172,7 +172,7 @@ func (e *invalidBlockTimestampError) ErrorCode() int { return errCodeBlockTimest
type blockGasLimitReachedError struct{ message string } type blockGasLimitReachedError struct{ message string }
// Partial state error codes per EIP-7928 / partial statefulness spec // Partial state error codes for untracked contract queries
const ( const (
errCodeStorageNotTracked = -32001 errCodeStorageNotTracked = -32001
errCodeCodeNotTracked = -32002 errCodeCodeNotTracked = -32002