state/bal: fix multiple bugs in BAL state transition

Fix several bugs in the BAL (Block Access List) state transition that
caused state root mismatches between the block building path and the
BAL validation path:

1. setError inverted condition: `if s.err != nil` should be `if s.err == nil`.
   This caused all errors to be silently swallowed, matching StateDB.setError.

2. IntermediateRoot ignores deleteEmptyObjects: The building path calls
   StateDB.IntermediateRoot(true) which deletes empty accounts (EIP-158),
   but the BAL path discarded this parameter. Now properly deletes empty
   accounts when deleteEmptyObjects is true.

3. parallel_state_processor hardcoded false for deleteEmptyObjects: Should
   pass config.IsEIP158(block.Number()) like block_validator does.

4. Non-deterministic ModifiedAccounts ordering: Map iteration produced
   random address ordering. Now sorted deterministically.

5. loadOriginStorages deadlock: On Storage() error, goroutine returned
   without sending to channel, causing pendingStorageCount to never reach 0.
   Now always sends to channel regardless of error.

6. Data race on metrics.StateHash: Removed racy per-goroutine writes to
   s.metrics.StateHash that were overwritten by the final state trie hash.
This commit is contained in:
qu0b 2026-02-01 14:45:03 +00:00
parent 323fe9e09c
commit d163f06283
3 changed files with 26 additions and 12 deletions

View file

@ -230,7 +230,7 @@ type stateRootCalculationResult struct {
func (p *ParallelStateProcessor) calcAndVerifyRoot(preState *state.StateDB, block *types.Block, stateTransition *state.BALStateTransition, resCh chan stateRootCalculationResult) { func (p *ParallelStateProcessor) calcAndVerifyRoot(preState *state.StateDB, block *types.Block, stateTransition *state.BALStateTransition, resCh chan stateRootCalculationResult) {
// calculate and apply the block state modifications // calculate and apply the block state modifications
//root, prestateLoadTime, rootCalcTime := preState.BlockAccessList().StateRoot(preState) //root, prestateLoadTime, rootCalcTime := preState.BlockAccessList().StateRoot(preState)
root := stateTransition.IntermediateRoot(false) root := stateTransition.IntermediateRoot(p.chainConfig().IsEIP158(block.Number()))
res := stateRootCalculationResult{ res := stateRootCalculationResult{
// TODO: I think we can remove the root from this struct // TODO: I think we can remove the root from this struct

View file

@ -3,13 +3,15 @@ package state
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"sync"
) )
// TODO: probably unnecessary to cache the resolved state object here as it will already be in the db cache? // TODO: probably unnecessary to cache the resolved state object here as it will already be in the db cache?
@ -133,13 +135,17 @@ func NewBALReader(block *types.Block, reader Reader) *BALReader {
return r return r
} }
// ModifiedAccounts returns a list of all accounts with mutations in the access list // ModifiedAccounts returns a deterministically-ordered list of all accounts
// with mutations in the access list.
func (r *BALReader) ModifiedAccounts() (res []common.Address) { func (r *BALReader) ModifiedAccounts() (res []common.Address) {
for addr, access := range r.accesses { for addr, access := range r.accesses {
if len(access.NonceChanges) != 0 || len(access.CodeChanges) != 0 || len(access.StorageChanges) != 0 || len(access.BalanceChanges) != 0 { if len(access.NonceChanges) != 0 || len(access.CodeChanges) != 0 || len(access.StorageChanges) != 0 || len(access.BalanceChanges) != 0 {
res = append(res, addr) res = append(res, addr)
} }
} }
slices.SortFunc(res, func(a, b common.Address) int {
return a.Cmp(b)
})
return res return res
} }

View file

@ -107,7 +107,7 @@ func (s *BALStateTransition) Error() error {
} }
func (s *BALStateTransition) setError(err error) { func (s *BALStateTransition) setError(err error) {
if s.err != nil { if s.err == nil {
s.err = err s.err = err
} }
} }
@ -390,7 +390,6 @@ func (s *BALStateTransition) loadOriginStorages() {
val, err := s.reader.Storage(addr, common.Hash(storageKey)) val, err := s.reader.Storage(addr, common.Hash(storageKey))
if err != nil { if err != nil {
s.setError(err) s.setError(err)
return
} }
originStoragesCh <- &originStorage{ originStoragesCh <- &originStorage{
addr, addr,
@ -418,7 +417,7 @@ func (s *BALStateTransition) loadOriginStorages() {
// IntermediateRoot applies block state mutations and computes the updated state // IntermediateRoot applies block state mutations and computes the updated state
// trie root. // trie root.
func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { func (s *BALStateTransition) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
if s.rootHash != (common.Hash{}) { if s.rootHash != (common.Hash{}) {
return s.rootHash return s.rootHash
} }
@ -502,9 +501,7 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash {
} }
} }
hashStart := time.Now()
tr.Hash() tr.Hash()
s.metrics.StateHash = time.Since(hashStart)
} }
}() }()
} }
@ -548,11 +545,22 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash {
return common.Hash{} return common.Hash{}
} }
} }
if err := s.stateTrie.UpdateAccount(mutatedAddr, acct, len(code)); err != nil { // Delete empty accounts when deleteEmptyObjects is set, matching
s.setError(err) // the behavior of StateDB.Finalise which deletes accounts where
return common.Hash{} // nonce == 0, balance == 0, and codeHash == emptyCodeHash.
if deleteEmptyObjects && acct.Nonce == 0 && acct.Balance.IsZero() && common.BytesToHash(acct.CodeHash) == types.EmptyCodeHash {
if err := s.stateTrie.DeleteAccount(mutatedAddr); err != nil {
s.setError(err)
return common.Hash{}
}
s.deletions[mutatedAddr] = struct{}{}
} else {
if err := s.stateTrie.UpdateAccount(mutatedAddr, acct, len(code)); err != nil {
s.setError(err)
return common.Hash{}
}
s.postStates[mutatedAddr] = acct
} }
s.postStates[mutatedAddr] = acct
} }
} }