solve the issue by adding an atomic value in CachingDB

This commit is contained in:
Guillaume Ballet 2026-02-04 18:38:42 +01:00
parent c61d2d307d
commit edbce93d07
No known key found for this signature in database
6 changed files with 293 additions and 22 deletions

View file

@ -2081,6 +2081,14 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s
) )
defer interrupt.Store(true) // terminate the prefetch at the end defer interrupt.Store(true) // terminate the prefetch at the end
if bc.chainConfig.IsVerkle(block.Number(), block.Time()) {
parent := bc.GetHeader(block.ParentHash(), block.NumberU64()-1)
if parent != nil && !bc.chainConfig.IsVerkle(parent.Number, parent.Time) {
bc.SetForkBoundary(parentRoot)
defer bc.ClearForkBoundary()
}
}
if bc.cfg.NoPrefetch { if bc.cfg.NoPrefetch {
statedb, err = state.New(parentRoot, bc.statedb) statedb, err = state.New(parentRoot, bc.statedb)
if err != nil { if err != nil {

View file

@ -416,6 +416,14 @@ func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
return state.New(root, bc.statedb) return state.New(root, bc.statedb)
} }
func (bc *BlockChain) SetForkBoundary(root common.Hash) {
bc.statedb.SetForkBoundary(root)
}
func (bc *BlockChain) ClearForkBoundary() {
bc.statedb.ClearForkBoundary()
}
// HistoricState returns a historic state specified by the given root. // HistoricState returns a historic state specified by the given root.
// Live states are not available and won't be served, please use `State` // Live states are not available and won't be served, please use `State`
// or `StateAt` instead. // or `StateAt` instead.

View file

@ -18,6 +18,7 @@ package state
import ( import (
"fmt" "fmt"
"sync/atomic"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/common/lru"
@ -158,6 +159,8 @@ type CachingDB struct {
// Transition-specific fields // Transition-specific fields
TransitionStatePerRoot *lru.Cache[common.Hash, *overlay.TransitionState] TransitionStatePerRoot *lru.Cache[common.Hash, *overlay.TransitionState]
forkBoundaryRoot atomic.Value // stores common.Hash; non-zero means fork boundary active for this root
} }
// NewDatabase creates a state database with the provided data sources. // NewDatabase creates a state database with the provided data sources.
@ -178,6 +181,23 @@ func NewDatabaseForTesting() *CachingDB {
return NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil) return NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil)
} }
func (db *CachingDB) SetForkBoundary(root common.Hash) {
db.forkBoundaryRoot.Store(root)
}
func (db *CachingDB) ClearForkBoundary() {
db.forkBoundaryRoot.Store(common.Hash{})
}
func (db *CachingDB) isForkBoundary(root common.Hash) bool {
v := db.forkBoundaryRoot.Load()
if v == nil {
return false
}
stored, ok := v.(common.Hash)
return ok && stored != (common.Hash{}) && stored == root
}
var ( var (
transitionStartedKey = common.Hash{} // slot 0: non-zero if transition started transitionStartedKey = common.Hash{} // slot 0: non-zero if transition started
conversionProgressAddressKey = common.Hash{1} // slot 1: current account pointer conversionProgressAddressKey = common.Hash{1} // slot 1: current account pointer
@ -269,6 +289,9 @@ func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
} }
} }
} }
if ts == nil && db.isForkBoundary(stateRoot) {
ts = &overlay.TransitionState{Started: true, BaseRoot: stateRoot}
}
// Configure the trie reader, which is expected to be available as the // Configure the trie reader, which is expected to be available as the
// gatekeeper unless the state is corrupted. // gatekeeper unless the state is corrupted.
tr, err := newTrieReader(stateRoot, db.triedb, ts) tr, err := newTrieReader(stateRoot, db.triedb, ts)
@ -307,6 +330,9 @@ func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (ReaderWithSta
// OpenTrie opens the main account trie at a specific root hash. // OpenTrie opens the main account trie at a specific root hash.
func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) {
if db.isForkBoundary(root) {
return db.openForkBoundaryTrie(root)
}
reader, err := db.triedb.StateReader(root) reader, err := db.triedb.StateReader(root)
if err != nil { if err != nil {
tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb)
@ -324,11 +350,10 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) {
return nil, fmt.Errorf("could not open the overlay tree: %w", err) return nil, fmt.Errorf("could not open the overlay tree: %w", err)
} }
if !ts.InTransition() { if !ts.InTransition() {
// Transition complete, use BinaryTrie only
return bt, nil return bt, nil
} }
base, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) base, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db.triedb)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not create base trie in OpenTrie: %w", err) return nil, fmt.Errorf("could not create base trie in OpenTrie: %w", err)
} }
@ -341,6 +366,18 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) {
return tr, nil return tr, nil
} }
func (db *CachingDB) openForkBoundaryTrie(root common.Hash) (Trie, error) {
bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db.triedb)
if err != nil {
return nil, fmt.Errorf("could not open empty binary trie at fork boundary: %w", err)
}
base, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb)
if err != nil {
return nil, fmt.Errorf("could not open base MPT at fork boundary: %w", err)
}
return transitiontrie.NewTransitionTrie(base, bt, false), nil
}
// OpenStorageTrie opens the storage trie of an account. // OpenStorageTrie opens the storage trie of an account.
func (db *CachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) { func (db *CachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) {
if self != nil && self.IsVerkle() { if self != nil && self.IsVerkle() {

View file

@ -0,0 +1,226 @@
package state
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie/transitiontrie"
"github.com/ethereum/go-ethereum/triedb"
"github.com/holiman/uint256"
)
func TestForkBoundaryOpenTrie(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
statedb, err := New(types.EmptyRootHash, sdb)
if err != nil {
t.Fatalf("failed to create statedb: %v", err)
}
addr := common.HexToAddress("0x1234")
statedb.SetBalance(addr, uint256.NewInt(1000), tracing.BalanceIncreaseGenesisBalance)
statedb.SetNonce(addr, 1, tracing.NonceChangeGenesis)
root, err := statedb.Commit(0, false, false)
if err != nil {
t.Fatalf("failed to commit: %v", err)
}
if err := tdb.Commit(root, false); err != nil {
t.Fatalf("failed to commit triedb: %v", err)
}
sdb.SetForkBoundary(root)
defer sdb.ClearForkBoundary()
tr, err := sdb.OpenTrie(root)
if err != nil {
t.Fatalf("failed to open trie: %v", err)
}
if _, ok := tr.(*transitiontrie.TransitionTrie); !ok {
t.Fatalf("expected TransitionTrie at fork boundary, got %T", tr)
}
acct, err := tr.GetAccount(addr)
if err != nil {
t.Fatalf("failed to get account: %v", err)
}
if acct == nil {
t.Fatal("expected non-nil account from MPT base")
}
if acct.Balance.Cmp(uint256.NewInt(1000)) != 0 {
t.Fatalf("unexpected balance: got %v, want 1000", acct.Balance)
}
}
func TestForkBoundaryStateReader(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
statedb, err := New(types.EmptyRootHash, sdb)
if err != nil {
t.Fatalf("failed to create statedb: %v", err)
}
addr := common.HexToAddress("0x5678")
statedb.SetBalance(addr, uint256.NewInt(2000), tracing.BalanceIncreaseGenesisBalance)
root, err := statedb.Commit(0, false, false)
if err != nil {
t.Fatalf("failed to commit: %v", err)
}
if err := tdb.Commit(root, false); err != nil {
t.Fatalf("failed to commit triedb: %v", err)
}
sdb.SetForkBoundary(root)
defer sdb.ClearForkBoundary()
reader, err := sdb.StateReader(root)
if err != nil {
t.Fatalf("failed to create state reader: %v", err)
}
acct, err := reader.Account(addr)
if err != nil {
t.Fatalf("failed to get account: %v", err)
}
if acct == nil {
t.Fatal("expected non-nil account")
}
if acct.Balance.Cmp(uint256.NewInt(2000)) != 0 {
t.Fatalf("unexpected balance: got %v, want 2000", acct.Balance)
}
}
func TestForkBoundaryWrite(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
statedb, err := New(types.EmptyRootHash, sdb)
if err != nil {
t.Fatalf("failed to create statedb: %v", err)
}
addr := common.HexToAddress("0xabcd")
statedb.SetBalance(addr, uint256.NewInt(500), tracing.BalanceIncreaseGenesisBalance)
root, err := statedb.Commit(0, false, false)
if err != nil {
t.Fatalf("failed to commit: %v", err)
}
if err := tdb.Commit(root, false); err != nil {
t.Fatalf("failed to commit triedb: %v", err)
}
sdb.SetForkBoundary(root)
defer sdb.ClearForkBoundary()
tr, err := sdb.OpenTrie(root)
if err != nil {
t.Fatalf("failed to open trie: %v", err)
}
newAddr := common.HexToAddress("0xef01")
err = tr.UpdateAccount(newAddr, &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}, 0)
if err != nil {
t.Fatalf("failed to update account: %v", err)
}
acct, err := tr.GetAccount(newAddr)
if err != nil {
t.Fatalf("failed to get new account: %v", err)
}
if acct == nil {
t.Fatal("expected non-nil new account")
}
if acct.Balance.Cmp(uint256.NewInt(100)) != 0 {
t.Fatalf("unexpected balance for new account: got %v, want 100", acct.Balance)
}
origAcct, err := tr.GetAccount(addr)
if err != nil {
t.Fatalf("failed to get original account: %v", err)
}
if origAcct == nil {
t.Fatal("expected non-nil original account from MPT base")
}
}
func TestForkBoundaryNotActive(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
statedb, err := New(types.EmptyRootHash, sdb)
if err != nil {
t.Fatalf("failed to create statedb: %v", err)
}
addr := common.HexToAddress("0x1111")
statedb.SetBalance(addr, uint256.NewInt(300), tracing.BalanceIncreaseGenesisBalance)
root, err := statedb.Commit(0, false, false)
if err != nil {
t.Fatalf("failed to commit: %v", err)
}
if err := tdb.Commit(root, false); err != nil {
t.Fatalf("failed to commit triedb: %v", err)
}
tr, err := sdb.OpenTrie(root)
if err != nil {
t.Fatalf("failed to open trie: %v", err)
}
if _, ok := tr.(*transitiontrie.TransitionTrie); ok {
t.Fatal("should not get TransitionTrie without fork boundary set")
}
}
func TestForkBoundaryWrongRoot(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
statedb, err := New(types.EmptyRootHash, sdb)
if err != nil {
t.Fatalf("failed to create statedb: %v", err)
}
addr := common.HexToAddress("0x2222")
statedb.SetBalance(addr, uint256.NewInt(400), tracing.BalanceIncreaseGenesisBalance)
root, err := statedb.Commit(0, false, false)
if err != nil {
t.Fatalf("failed to commit: %v", err)
}
if err := tdb.Commit(root, false); err != nil {
t.Fatalf("failed to commit triedb: %v", err)
}
otherRoot := common.HexToHash("0xdeadbeef")
sdb.SetForkBoundary(otherRoot)
defer sdb.ClearForkBoundary()
tr, err := sdb.OpenTrie(root)
if err != nil {
t.Fatalf("failed to open trie: %v", err)
}
if _, ok := tr.(*transitiontrie.TransitionTrie); ok {
t.Fatal("should not get TransitionTrie for different root")
}
}

View file

@ -318,18 +318,18 @@ func newTrieReader(root common.Hash, db *triedb.Database, ts *overlay.Transition
tr Trie tr Trie
err error err error
) )
if !db.IsVerkle() { if !db.IsVerkle() && !ts.InTransition() && !ts.Transitioned() {
tr, err = trie.NewStateTrie(trie.StateTrieID(root), db) tr, err = trie.NewStateTrie(trie.StateTrieID(root), db)
} else { } else {
// When IsVerkle() is true, create a BinaryTrie wrapped in TransitionTrie binRoot := root
binTrie, binErr := bintrie.NewBinaryTrie(root, db) if ts.InTransition() && ts.BaseRoot == root {
binRoot = types.EmptyBinaryHash
}
binTrie, binErr := bintrie.NewBinaryTrie(binRoot, db)
if binErr != nil { if binErr != nil {
return nil, binErr return nil, binErr
} }
// Based on the transition status, determine if the overlay
// tree needs to be created, or if a single, target tree is
// to be picked.
if ts.InTransition() { if ts.InTransition() {
mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db) mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db)
if err != nil { if err != nil {
@ -337,19 +337,6 @@ func newTrieReader(root common.Hash, db *triedb.Database, ts *overlay.Transition
} }
tr = transitiontrie.NewTransitionTrie(mpt, binTrie, false) tr = transitiontrie.NewTransitionTrie(mpt, binTrie, false)
} else { } else {
// HACK: Use TransitionTrie with nil base as a wrapper to make BinaryTrie
// satisfy the Trie interface. This works around the import cycle between
// trie and trie/bintrie packages.
//
// TODO: In future PRs, refactor the package structure to avoid this hack:
// - Option 1: Move common interfaces (Trie, NodeIterator) to a separate
// package that both trie and trie/bintrie can import
// - Option 2: Create a factory function in the trie package that returns
// BinaryTrie as a Trie interface without direct import
// - Option 3: Move BinaryTrie to the main trie package
//
// The current approach works but adds unnecessary overhead and complexity
// by using TransitionTrie when there's no actual transition happening.
tr = transitiontrie.NewTransitionTrie(nil, binTrie, false) tr = transitiontrie.NewTransitionTrie(nil, binTrie, false)
} }
} }
@ -404,7 +391,7 @@ func (r *trieReader) Storage(addr common.Address, key common.Hash) (common.Hash,
found bool found bool
value common.Hash value common.Hash
) )
if r.db.IsVerkle() { if r.mainTrie.IsVerkle() {
tr = r.mainTrie tr = r.mainTrie
} else { } else {
tr, found = r.subTries[addr] tr, found = r.subTries[addr]

View file

@ -252,6 +252,11 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir
header.ExcessBlobGas = &excessBlobGas header.ExcessBlobGas = &excessBlobGas
header.ParentBeaconRoot = genParams.beaconRoot header.ParentBeaconRoot = genParams.beaconRoot
} }
if miner.chainConfig.IsVerkle(header.Number, header.Time) &&
!miner.chainConfig.IsVerkle(parent.Number, parent.Time) {
miner.chain.SetForkBoundary(parent.Root)
defer miner.chain.ClearForkBoundary()
}
// Could potentially happen if starting to mine in an odd state. // Could potentially happen if starting to mine in an odd state.
// Note genParams.coinbase can be different with header.Coinbase // Note genParams.coinbase can be different with header.Coinbase
// since clique algorithm can modify the coinbase field in header. // since clique algorithm can modify the coinbase field in header.