From edbce93d07b1d0469e3d91de8ada1b6f4afcdac8 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:38:42 +0100 Subject: [PATCH] solve the issue by adding an atomic value in CachingDB --- core/blockchain.go | 8 ++ core/blockchain_reader.go | 8 ++ core/state/database.go | 41 +++++- core/state/fork_boundary_test.go | 226 +++++++++++++++++++++++++++++++ core/state/reader.go | 27 +--- miner/worker.go | 5 + 6 files changed, 293 insertions(+), 22 deletions(-) create mode 100644 core/state/fork_boundary_test.go diff --git a/core/blockchain.go b/core/blockchain.go index db3f71c44d..a3776442d2 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -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 + 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 { statedb, err = state.New(parentRoot, bc.statedb) if err != nil { diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index ee15c152c4..de0ef360db 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -416,6 +416,14 @@ func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) { 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. // Live states are not available and won't be served, please use `State` // or `StateAt` instead. diff --git a/core/state/database.go b/core/state/database.go index d79324b84c..b7085467b0 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -18,6 +18,7 @@ package state import ( "fmt" + "sync/atomic" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" @@ -158,6 +159,8 @@ type CachingDB struct { // Transition-specific fields 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. @@ -178,6 +181,23 @@ func NewDatabaseForTesting() *CachingDB { 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 ( transitionStartedKey = common.Hash{} // slot 0: non-zero if transition started 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 // gatekeeper unless the state is corrupted. 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. func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { + if db.isForkBoundary(root) { + return db.openForkBoundaryTrie(root) + } reader, err := db.triedb.StateReader(root) if err != nil { 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) } if !ts.InTransition() { - // Transition complete, use BinaryTrie only 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 { 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 } +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. func (db *CachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) { if self != nil && self.IsVerkle() { diff --git a/core/state/fork_boundary_test.go b/core/state/fork_boundary_test.go new file mode 100644 index 0000000000..3fba1d8030 --- /dev/null +++ b/core/state/fork_boundary_test.go @@ -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") + } +} diff --git a/core/state/reader.go b/core/state/reader.go index fd999b11ac..2f0f7516ea 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -318,18 +318,18 @@ func newTrieReader(root common.Hash, db *triedb.Database, ts *overlay.Transition tr Trie err error ) - if !db.IsVerkle() { + if !db.IsVerkle() && !ts.InTransition() && !ts.Transitioned() { tr, err = trie.NewStateTrie(trie.StateTrieID(root), db) } else { - // When IsVerkle() is true, create a BinaryTrie wrapped in TransitionTrie - binTrie, binErr := bintrie.NewBinaryTrie(root, db) + binRoot := root + if ts.InTransition() && ts.BaseRoot == root { + binRoot = types.EmptyBinaryHash + } + binTrie, binErr := bintrie.NewBinaryTrie(binRoot, db) if binErr != nil { 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() { mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db) if err != nil { @@ -337,19 +337,6 @@ func newTrieReader(root common.Hash, db *triedb.Database, ts *overlay.Transition } tr = transitiontrie.NewTransitionTrie(mpt, binTrie, false) } 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) } } @@ -404,7 +391,7 @@ func (r *trieReader) Storage(addr common.Address, key common.Hash) (common.Hash, found bool value common.Hash ) - if r.db.IsVerkle() { + if r.mainTrie.IsVerkle() { tr = r.mainTrie } else { tr, found = r.subTries[addr] diff --git a/miner/worker.go b/miner/worker.go index cb2e24d0cd..b74f44edca 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -252,6 +252,11 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir header.ExcessBlobGas = &excessBlobGas 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. // Note genParams.coinbase can be different with header.Coinbase // since clique algorithm can modify the coinbase field in header.