From 397497c0f642a48998f6feafde9fdf17b01a9f95 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:01:47 +0100 Subject: [PATCH 01/10] core, params: transition tree initialization --- core/overlay/state_transition.go | 64 ++++++++++++++++---------------- core/state/database.go | 19 +++++++--- core/state/reader.go | 2 +- core/state/statedb.go | 11 ++++++ params/protocol_params.go | 3 ++ 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go index a52d9139c9..44cbdab11d 100644 --- a/core/overlay/state_transition.go +++ b/core/overlay/state_transition.go @@ -17,17 +17,21 @@ package overlay import ( - "bytes" - "encoding/gob" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie/bintrie" ) // TransitionState is a structure that holds the progress markers of the // translation process. +// TODO gballet: +// * see if I can get rid of the pointer now that this piece +// has been rewritten. +// * the conversion pointers should no longer be necessary, +// remove them when it's been confirmed. +// * we can't keep the preimage offset in the file type TransitionState struct { CurrentAccountAddress *common.Address // addresss of the last translated account CurrentSlotHash common.Hash // hash of the last translated storage slot @@ -69,38 +73,32 @@ func (ts *TransitionState) Copy() *TransitionState { return ret } +var ( + conversionProgressAddressKey = common.Hash{1} + conversionProgressSlotKey = common.Hash{2} + conversionProgressStorageProcessed = common.Hash{3} +) + // LoadTransitionState retrieves the Verkle transition state associated with // the given state root hash from the database. -func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash, isVerkle bool) *TransitionState { - var ts *TransitionState - - data, _ := rawdb.ReadVerkleTransitionState(db, root) - - // if a state could be read from the db, attempt to decode it - if len(data) > 0 { - var ( - newts TransitionState - buf = bytes.NewBuffer(data[:]) - dec = gob.NewDecoder(buf) - ) - // Decode transition state - err := dec.Decode(&newts) - if err != nil { - log.Error("failed to decode transition state", "err", err) - return nil - } - ts = &newts +func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash) *TransitionState { + stem := bintrie.GetBinaryTreeKeyStorageSlot(params.BinaryTransitionRegistryAddress, conversionProgressAddressKey[:]) + leaf := rawdb.ReadStorageTrieNode(db, common.Hash{}, stem[:bintrie.StemSize]) + if len(leaf) == 0 { + return &TransitionState{} } - // Fallback that should only happen before the transition - if ts == nil { - // Initialize the first transition state, with the "ended" - // field set to true if the database was created - // as a verkle database. - log.Debug("no transition state found, starting fresh", "verkle", isVerkle) - - // Start with a fresh state - ts = &TransitionState{Ended: isVerkle} + node, err := bintrie.DeserializeNode(leaf, 0) + if err != nil { + panic("could not deserialize conversion pointers contract storage") + } + leafNode := node.(*bintrie.StemNode) + currentAccount := common.BytesToAddress(leafNode.Values[65][12:]) + currentSlotHash := common.BytesToHash(leafNode.Values[66]) + storageProcessed := len(leafNode.Values[67]) > 0 && leafNode.Values[67][0] != 0 + return &TransitionState{ + CurrentAccountAddress: ¤tAccount, + CurrentSlotHash: currentSlotHash, + StorageProcessed: storageProcessed, } - return ts } diff --git a/core/state/database.go b/core/state/database.go index 4a5547d075..99a8349537 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -239,15 +239,24 @@ 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.triedb.IsVerkle() { - ts := overlay.LoadTransitionState(db.TrieDB().Disk(), root, db.triedb.IsVerkle()) - if ts.InTransition() { - panic("state tree transition isn't supported yet") + // if the transition has started, it will be present in the overlay tree, + // so we open it regardless. + bt, err := bintrie.NewBinaryTrie(root, db.triedb) + if err != nil { + return nil, fmt.Errorf("could not open the overlay tree: %w", err) } - if ts.Transitioned() { + ts := overlay.LoadTransitionState(db.TrieDB().Disk(), root) + if !ts.InTransition() { // Use BinaryTrie instead of VerkleTrie when IsVerkle is set // (IsVerkle actually means Binary Trie mode in this codebase) - return bintrie.NewBinaryTrie(root, db.triedb) + return bt, nil } + + base, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) + if err != nil { + return nil, fmt.Errorf("could not create base trie in OpenTrie: %w", err) + } + return transitiontrie.NewTransitionTrie(base, bt, false), nil } tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) if err != nil { diff --git a/core/state/reader.go b/core/state/reader.go index 35b732173b..6191cf7944 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -330,7 +330,7 @@ func newTrieReader(root common.Hash, db *triedb.Database) (*trieReader, error) { // Based on the transition status, determine if the overlay // tree needs to be created, or if a single, target tree is // to be picked. - ts := overlay.LoadTransitionState(db.Disk(), root, true) + ts := overlay.LoadTransitionState(db.Disk(), root) if ts.InTransition() { mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db) if err != nil { diff --git a/core/state/statedb.go b/core/state/statedb.go index 610e7173cf..294f5f719d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -349,6 +349,17 @@ func (s *StateDB) GetStorageRoot(addr common.Address) common.Hash { return common.Hash{} } +// TransitionComplete checks if the EIP-7612 transition is complete. +func (s *StateDB) InTransition() bool { + completeKey := common.Hash{} // slot 0 for completion flag + completeValue := s.GetState(params.BinaryTransitionRegistryAddress, completeKey) + return completeValue != (common.Hash{}) +} + +func (s *StateDB) ProcessBinaryTreeTransition(maxSlot uint64) { + +} + // TxIndex returns the current transaction index set by SetTxContext. func (s *StateDB) TxIndex() int { return s.txIndex diff --git a/params/protocol_params.go b/params/protocol_params.go index bb506af015..60c0cfe51e 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -219,4 +219,7 @@ var ( // EIP-7251 - Increase the MAX_EFFECTIVE_BALANCE ConsolidationQueueAddress = common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251") ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd") + + // EIP-7612 - Tree transition registry contract address + BinaryTransitionRegistryAddress = common.HexToAddress("0x1622162216221622162216221622162216221622162216221622162216221622") ) From 8e1239bc6afb164ac6a0169b0a862a44c25411ac Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:29:17 +0100 Subject: [PATCH 02/10] slight cleanup --- core/overlay/state_transition.go | 6 +++++- core/state/database.go | 5 +++-- core/state/statedb.go | 4 ---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go index 44cbdab11d..b4eeec66b8 100644 --- a/core/overlay/state_transition.go +++ b/core/overlay/state_transition.go @@ -31,7 +31,11 @@ import ( // has been rewritten. // * the conversion pointers should no longer be necessary, // remove them when it's been confirmed. -// * we can't keep the preimage offset in the file +// * we can't keep the preimage offset in the file, since +// some clients might decide to record their preimages and +// skip the use of the file altogether. Therefore, they can't +// know what the offset it, unless they keep track of how many +// bytes have been read since the start, which is a possibility. type TransitionState struct { CurrentAccountAddress *common.Address // addresss of the last translated account CurrentSlotHash common.Hash // hash of the last translated storage slot diff --git a/core/state/database.go b/core/state/database.go index 99a8349537..fbc1c2dcf2 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -239,8 +239,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.triedb.IsVerkle() { - // if the transition has started, it will be present in the overlay tree, - // so we open it regardless. + // if the transition has started, the pointers will be present in the + // overlay tree, and the tree itself will be present, so the tree is + // opened and the pointers read, regardless of what comes afterwards. bt, err := bintrie.NewBinaryTrie(root, db.triedb) if err != nil { return nil, fmt.Errorf("could not open the overlay tree: %w", err) diff --git a/core/state/statedb.go b/core/state/statedb.go index 294f5f719d..61d80df56e 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -356,10 +356,6 @@ func (s *StateDB) InTransition() bool { return completeValue != (common.Hash{}) } -func (s *StateDB) ProcessBinaryTreeTransition(maxSlot uint64) { - -} - // TxIndex returns the current transaction index set by SetTxContext. func (s *StateDB) TxIndex() int { return s.txIndex From cd2389968adb4cf595b8a3910f2d4dc7ffae65a5 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:08:13 +0100 Subject: [PATCH 03/10] review feedback --- core/overlay/state_transition.go | 38 ++++++++++++++++++-------------- core/state/database.go | 10 +++++++-- core/state/reader.go | 3 +-- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go index b4eeec66b8..099d68cca5 100644 --- a/core/overlay/state_transition.go +++ b/core/overlay/state_transition.go @@ -17,11 +17,12 @@ package overlay import ( + "fmt" + "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie/bintrie" + "github.com/ethereum/go-ethereum/triedb/database" ) // TransitionState is a structure that holds the progress markers of the @@ -85,21 +86,26 @@ var ( // LoadTransitionState retrieves the Verkle transition state associated with // the given state root hash from the database. -func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash) *TransitionState { - stem := bintrie.GetBinaryTreeKeyStorageSlot(params.BinaryTransitionRegistryAddress, conversionProgressAddressKey[:]) - leaf := rawdb.ReadStorageTrieNode(db, common.Hash{}, stem[:bintrie.StemSize]) - if len(leaf) == 0 { - return &TransitionState{} - } - - node, err := bintrie.DeserializeNode(leaf, 0) +func LoadTransitionState(reader database.StateReader, root common.Hash) *TransitionState { + addrHash := crypto.Keccak256Hash(params.BinaryTransitionRegistryAddress[:]) + currentAccountBytes, err := reader.Storage(addrHash, conversionProgressAddressKey) if err != nil { - panic("could not deserialize conversion pointers contract storage") + panic(fmt.Errorf("error reading conversion account pointer: %w", err)) } - leafNode := node.(*bintrie.StemNode) - currentAccount := common.BytesToAddress(leafNode.Values[65][12:]) - currentSlotHash := common.BytesToHash(leafNode.Values[66]) - storageProcessed := len(leafNode.Values[67]) > 0 && leafNode.Values[67][0] != 0 + currentAccount := common.BytesToAddress(currentAccountBytes[12:]) + + currentSlotBytes, err := reader.Storage(addrHash, conversionProgressSlotKey) + if err != nil { + panic(fmt.Errorf("error reading conversion slot pointer: %w", err)) + } + currentSlotHash := common.BytesToHash(currentSlotBytes) + + storageProcessedBytes, err := reader.Storage(addrHash, conversionProgressStorageProcessed) + if err != nil { + panic(fmt.Errorf("error reading conversion storage processing completion status: %w", err)) + } + storageProcessed := storageProcessedBytes[0] == 1 + return &TransitionState{ CurrentAccountAddress: ¤tAccount, CurrentSlotHash: currentSlotHash, diff --git a/core/state/database.go b/core/state/database.go index fbc1c2dcf2..c94e62ba54 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -190,6 +190,7 @@ func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) { readers = append(readers, newFlatReader(snap)) } } + var ts *overlay.TransitionState // Configure the state reader using the path database in path mode. // This reader offers improved performance but is optional and only // partially useful if the snapshot data in path database is not @@ -198,11 +199,12 @@ func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) { reader, err := db.triedb.StateReader(stateRoot) if err == nil { readers = append(readers, newFlatReader(reader)) + ts = overlay.LoadTransitionState(reader, 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) + tr, err := newTrieReader(stateRoot, db.triedb, ts) if err != nil { return nil, err } @@ -246,7 +248,11 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { if err != nil { return nil, fmt.Errorf("could not open the overlay tree: %w", err) } - ts := overlay.LoadTransitionState(db.TrieDB().Disk(), root) + reader, err := db.StateReader(root) + if err != nil { + return nil, fmt.Errorf("could not get reader for checking overlay status: %w", err) + } + ts := overlay.LoadTransitionState(reader, root) if !ts.InTransition() { // Use BinaryTrie instead of VerkleTrie when IsVerkle is set // (IsVerkle actually means Binary Trie mode in this codebase) diff --git a/core/state/reader.go b/core/state/reader.go index 6191cf7944..fd999b11ac 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -313,7 +313,7 @@ type trieReader struct { // newTrieReader constructs a trie reader of the specific state. An error will be // returned if the associated trie specified by root is not existent. -func newTrieReader(root common.Hash, db *triedb.Database) (*trieReader, error) { +func newTrieReader(root common.Hash, db *triedb.Database, ts *overlay.TransitionState) (*trieReader, error) { var ( tr Trie err error @@ -330,7 +330,6 @@ func newTrieReader(root common.Hash, db *triedb.Database) (*trieReader, error) { // Based on the transition status, determine if the overlay // tree needs to be created, or if a single, target tree is // to be picked. - ts := overlay.LoadTransitionState(db.Disk(), root) if ts.InTransition() { mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db) if err != nil { From 09b9c27c83cabab2a07681f8f189a822c6f2308c Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:54:38 +0100 Subject: [PATCH 04/10] use the flat reader in LoadTransitionState --- core/overlay/state_transition.go | 40 -------------------------------- core/state/database.go | 38 ++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go index 099d68cca5..2fb7e9c3fd 100644 --- a/core/overlay/state_transition.go +++ b/core/overlay/state_transition.go @@ -17,12 +17,7 @@ package overlay import ( - "fmt" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/triedb/database" ) // TransitionState is a structure that holds the progress markers of the @@ -77,38 +72,3 @@ func (ts *TransitionState) Copy() *TransitionState { } return ret } - -var ( - conversionProgressAddressKey = common.Hash{1} - conversionProgressSlotKey = common.Hash{2} - conversionProgressStorageProcessed = common.Hash{3} -) - -// LoadTransitionState retrieves the Verkle transition state associated with -// the given state root hash from the database. -func LoadTransitionState(reader database.StateReader, root common.Hash) *TransitionState { - addrHash := crypto.Keccak256Hash(params.BinaryTransitionRegistryAddress[:]) - currentAccountBytes, err := reader.Storage(addrHash, conversionProgressAddressKey) - if err != nil { - panic(fmt.Errorf("error reading conversion account pointer: %w", err)) - } - currentAccount := common.BytesToAddress(currentAccountBytes[12:]) - - currentSlotBytes, err := reader.Storage(addrHash, conversionProgressSlotKey) - if err != nil { - panic(fmt.Errorf("error reading conversion slot pointer: %w", err)) - } - currentSlotHash := common.BytesToHash(currentSlotBytes) - - storageProcessedBytes, err := reader.Storage(addrHash, conversionProgressStorageProcessed) - if err != nil { - panic(fmt.Errorf("error reading conversion storage processing completion status: %w", err)) - } - storageProcessed := storageProcessedBytes[0] == 1 - - return &TransitionState{ - CurrentAccountAddress: ¤tAccount, - CurrentSlotHash: currentSlotHash, - StorageProcessed: storageProcessed, - } -} diff --git a/core/state/database.go b/core/state/database.go index c94e62ba54..763a73aa19 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/bintrie" "github.com/ethereum/go-ethereum/trie/transitiontrie" @@ -177,6 +178,39 @@ func NewDatabaseForTesting() *CachingDB { return NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil) } +var ( + conversionProgressAddressKey = common.Hash{1} + conversionProgressSlotKey = common.Hash{2} + conversionProgressStorageProcessed = common.Hash{3} +) + +// LoadTransitionState retrieves the Verkle transition state associated with +// the given state root hash from the database. +func LoadTransitionState(reader StateReader, root common.Hash) *overlay.TransitionState { + currentAccountBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressAddressKey) + if err != nil { + panic(fmt.Errorf("error reading conversion account pointer: %w", err)) + } + currentAccount := common.BytesToAddress(currentAccountBytes[12:]) + + currentSlotHash, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressSlotKey) + if err != nil { + panic(fmt.Errorf("error reading conversion slot pointer: %w", err)) + } + + storageProcessedBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressStorageProcessed) + if err != nil { + panic(fmt.Errorf("error reading conversion storage processing completion status: %w", err)) + } + storageProcessed := storageProcessedBytes[0] == 1 + + return &overlay.TransitionState{ + CurrentAccountAddress: ¤tAccount, + CurrentSlotHash: currentSlotHash, + StorageProcessed: storageProcessed, + } +} + // StateReader returns a state reader associated with the specified state root. func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) { var readers []StateReader @@ -199,7 +233,7 @@ func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) { reader, err := db.triedb.StateReader(stateRoot) if err == nil { readers = append(readers, newFlatReader(reader)) - ts = overlay.LoadTransitionState(reader, stateRoot) + ts = LoadTransitionState(readers[len(readers)-1], stateRoot) } } // Configure the trie reader, which is expected to be available as the @@ -252,7 +286,7 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { if err != nil { return nil, fmt.Errorf("could not get reader for checking overlay status: %w", err) } - ts := overlay.LoadTransitionState(reader, root) + ts := LoadTransitionState(reader, root) if !ts.InTransition() { // Use BinaryTrie instead of VerkleTrie when IsVerkle is set // (IsVerkle actually means Binary Trie mode in this codebase) From 9cef9dc75e168a11a8a6da58851045a277839372 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:28:34 -0500 Subject: [PATCH 05/10] rework activation --- core/state/database.go | 67 +++++++++++++++++++++++++++++++---------- core/state_processor.go | 5 +++ eth/state_accessor.go | 7 +++++ miner/worker.go | 6 ++++ 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index 763a73aa19..fc134e8b5e 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -179,14 +179,43 @@ func NewDatabaseForTesting() *CachingDB { } var ( - conversionProgressAddressKey = common.Hash{1} - conversionProgressSlotKey = common.Hash{2} - conversionProgressStorageProcessed = common.Hash{3} + transitionStartedKey = common.Hash{} // slot 0: non-zero if transition started + conversionProgressAddressKey = common.Hash{1} // slot 1: current account pointer + conversionProgressSlotKey = common.Hash{2} // slot 2: current slot pointer + conversionProgressStorageProcessed = common.Hash{3} // slot 3: storage processed flag + transitionEndedKey = common.Hash{4} // slot 4: non-zero if transition ended ) +// isTransitionActive checks if the binary tree transition has been activated +func isTransitionActive(reader StateReader) bool { + val, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionStartedKey) + if err != nil { + return false + } + return val != (common.Hash{}) +} + // LoadTransitionState retrieves the Verkle transition state associated with // the given state root hash from the database. func LoadTransitionState(reader StateReader, root common.Hash) *overlay.TransitionState { + startedBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionStartedKey) + if err != nil { + return nil + } + started := startedBytes != (common.Hash{}) + + // If not started, return nil to indicate no active transition + if !started { + return nil + } + + endedBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionEndedKey) + if err != nil { + // Registry exists but can't read ended flag - treat as still in transition + endedBytes = common.Hash{} + } + ended := endedBytes != (common.Hash{}) + currentAccountBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressAddressKey) if err != nil { panic(fmt.Errorf("error reading conversion account pointer: %w", err)) @@ -205,6 +234,8 @@ func LoadTransitionState(reader StateReader, root common.Hash) *overlay.Transiti storageProcessed := storageProcessedBytes[0] == 1 return &overlay.TransitionState{ + Started: started, + Ended: ended, CurrentAccountAddress: ¤tAccount, CurrentSlotHash: currentSlotHash, StorageProcessed: storageProcessed, @@ -232,8 +263,11 @@ func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) { if db.TrieDB().Scheme() == rawdb.PathScheme { reader, err := db.triedb.StateReader(stateRoot) if err == nil { - readers = append(readers, newFlatReader(reader)) - ts = LoadTransitionState(readers[len(readers)-1], stateRoot) + flatReader := newFlatReader(reader) + readers = append(readers, flatReader) + if isTransitionActive(flatReader) || db.triedb.IsVerkle() { + ts = LoadTransitionState(flatReader, stateRoot) + } } } // Configure the trie reader, which is expected to be available as the @@ -274,22 +308,23 @@ 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.triedb.IsVerkle() { - // if the transition has started, the pointers will be present in the - // overlay tree, and the tree itself will be present, so the tree is - // opened and the pointers read, regardless of what comes afterwards. + reader, err := db.StateReader(root) + if err != nil { + tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) + if err != nil { + return nil, err + } + return tr, nil + } + + if isTransitionActive(reader) || db.triedb.IsVerkle() { bt, err := bintrie.NewBinaryTrie(root, db.triedb) if err != nil { return nil, fmt.Errorf("could not open the overlay tree: %w", err) } - reader, err := db.StateReader(root) - if err != nil { - return nil, fmt.Errorf("could not get reader for checking overlay status: %w", err) - } ts := LoadTransitionState(reader, root) if !ts.InTransition() { - // Use BinaryTrie instead of VerkleTrie when IsVerkle is set - // (IsVerkle actually means Binary Trie mode in this codebase) + // Transition complete, use BinaryTrie only return bt, nil } @@ -308,7 +343,7 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { // 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 db.triedb.IsVerkle() { + if self != nil && self.IsVerkle() { return self, nil } tr, err := trie.NewStateTrie(trie.StorageTrieID(stateRoot, crypto.Keccak256Hash(address.Bytes()), root), db.triedb) diff --git a/core/state_processor.go b/core/state_processor.go index b4b22e4318..5d76560825 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -93,6 +93,11 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg if config.IsPrague(block.Number(), block.Time()) || config.IsVerkle(block.Number(), block.Time()) { ProcessParentBlockHash(block.ParentHash(), evm) } + if config.IsVerkle(header.Number, header.Time) { + statedb.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) + statedb.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) + statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) + } // Iterate over and process the individual transactions for i, tx := range block.Transactions() { diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 79c91043a3..2ff27581fe 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -26,10 +26,12 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" ) @@ -251,6 +253,11 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, if eth.blockchain.Config().IsPrague(block.Number(), block.Time()) { core.ProcessParentBlockHash(block.ParentHash(), evm) } + if eth.blockchain.Config().IsVerkle(block.Number(), block.Time()) { + statedb.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) + statedb.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) + statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) + } if txIndex == 0 && len(block.Transactions()) == 0 { return nil, context, statedb, release, nil } diff --git a/miner/worker.go b/miner/worker.go index 45d7073ed7..4a7057c505 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -266,6 +267,11 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir if miner.chainConfig.IsPrague(header.Number, header.Time) { core.ProcessParentBlockHash(header.ParentHash, env.evm) } + if miner.chainConfig.IsVerkle(header.Number, header.Time) { + env.state.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) + env.state.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) + env.state.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) + } return env, nil } From a0ad1eecc0c6f329e3050a931d125fbddfc7dd65 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:12:47 +0100 Subject: [PATCH 06/10] fix test by accepting a non-deployed contract --- core/chain_makers.go | 3 +++ core/state/database.go | 16 ++++++++-------- core/state_processor.go | 4 +--- core/transition_registry.go | 35 +++++++++++++++++++++++++++++++++++ eth/state_accessor.go | 6 +----- miner/worker.go | 5 +---- 6 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 core/transition_registry.go diff --git a/core/chain_makers.go b/core/chain_makers.go index 7ce86b14e9..76b634e57e 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -396,6 +396,9 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{}) ProcessParentBlockHash(b.header.ParentHash, evm) } + if config.IsVerkle(b.header.Number, b.header.Time) { + InitializeBinaryTransitionRegistry(statedb) + } // Execute any user modifications to the block if gen != nil { diff --git a/core/state/database.go b/core/state/database.go index fc134e8b5e..01ece45b8b 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -211,25 +211,24 @@ func LoadTransitionState(reader StateReader, root common.Hash) *overlay.Transiti endedBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionEndedKey) if err != nil { - // Registry exists but can't read ended flag - treat as still in transition - endedBytes = common.Hash{} + return nil } ended := endedBytes != (common.Hash{}) currentAccountBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressAddressKey) if err != nil { - panic(fmt.Errorf("error reading conversion account pointer: %w", err)) + return nil } currentAccount := common.BytesToAddress(currentAccountBytes[12:]) currentSlotHash, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressSlotKey) if err != nil { - panic(fmt.Errorf("error reading conversion slot pointer: %w", err)) + return nil } storageProcessedBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressStorageProcessed) if err != nil { - panic(fmt.Errorf("error reading conversion storage processing completion status: %w", err)) + return nil } storageProcessed := storageProcessedBytes[0] == 1 @@ -308,7 +307,8 @@ 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) { - reader, err := db.StateReader(root) + reader, err := db.triedb.StateReader(root) + flatReader := newFlatReader(reader) if err != nil { tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) if err != nil { @@ -317,12 +317,12 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { return tr, nil } - if isTransitionActive(reader) || db.triedb.IsVerkle() { + if isTransitionActive(flatReader) || db.triedb.IsVerkle() { bt, err := bintrie.NewBinaryTrie(root, db.triedb) if err != nil { return nil, fmt.Errorf("could not open the overlay tree: %w", err) } - ts := LoadTransitionState(reader, root) + ts := LoadTransitionState(flatReader, root) if !ts.InTransition() { // Transition complete, use BinaryTrie only return bt, nil diff --git a/core/state_processor.go b/core/state_processor.go index 5d76560825..558181ca5b 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -94,9 +94,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg ProcessParentBlockHash(block.ParentHash(), evm) } if config.IsVerkle(header.Number, header.Time) { - statedb.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) - statedb.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) - statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) + InitializeBinaryTransitionRegistry(statedb) } // Iterate over and process the individual transactions diff --git a/core/transition_registry.go b/core/transition_registry.go new file mode 100644 index 0000000000..83cb62e30a --- /dev/null +++ b/core/transition_registry.go @@ -0,0 +1,35 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/params" +) + +// InitializeBinaryTransitionRegistry seeds the transition registry account used +// during the MPT->BinaryTrie transition. +func InitializeBinaryTransitionRegistry(statedb *state.StateDB) { + if statedb == nil { + return + } + statedb.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) + statedb.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) + statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) +} diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 2ff27581fe..385b38ed97 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -26,12 +26,10 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" ) @@ -254,9 +252,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, core.ProcessParentBlockHash(block.ParentHash(), evm) } if eth.blockchain.Config().IsVerkle(block.Number(), block.Time()) { - statedb.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) - statedb.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) - statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) + core.InitializeBinaryTransitionRegistry(statedb) } if txIndex == 0 && len(block.Transactions()) == 0 { return nil, context, statedb, release, nil diff --git a/miner/worker.go b/miner/worker.go index 4a7057c505..cb2e24d0cd 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -29,7 +29,6 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/stateless" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -268,9 +267,7 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir core.ProcessParentBlockHash(header.ParentHash, env.evm) } if miner.chainConfig.IsVerkle(header.Number, header.Time) { - env.state.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) - env.state.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) - env.state.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) + core.InitializeBinaryTransitionRegistry(env.state) } return env, nil } From c61d2d307d2fc1c038a12bf6557bc29f17de18f7 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:54:10 +0100 Subject: [PATCH 07/10] move stuff around --- core/state/database.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index 01ece45b8b..d79324b84c 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -308,7 +308,6 @@ 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) { reader, err := db.triedb.StateReader(root) - flatReader := newFlatReader(reader) if err != nil { tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) if err != nil { @@ -316,13 +315,14 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { } return tr, nil } + flatReader := newFlatReader(reader) + ts := LoadTransitionState(flatReader, root) if isTransitionActive(flatReader) || db.triedb.IsVerkle() { bt, err := bintrie.NewBinaryTrie(root, db.triedb) if err != nil { return nil, fmt.Errorf("could not open the overlay tree: %w", err) } - ts := LoadTransitionState(flatReader, root) if !ts.InTransition() { // Transition complete, use BinaryTrie only return bt, nil From 9f89625a0d376be6ba98346618af15613abcfb6b Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:06:25 +0100 Subject: [PATCH 08/10] fix some incorrect shuffling --- core/state/database.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index d79324b84c..fadaae1b5b 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -309,11 +309,7 @@ func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (ReaderWithSta func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { reader, err := db.triedb.StateReader(root) if err != nil { - tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) - if err != nil { - return nil, err - } - return tr, nil + return nil, err } flatReader := newFlatReader(reader) @@ -328,17 +324,14 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) { 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) } return transitiontrie.NewTransitionTrie(base, bt, false), nil } - tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb) - if err != nil { - return nil, err - } - return tr, nil + + return trie.NewStateTrie(trie.StateTrieID(root), db.triedb) } // OpenStorageTrie opens the storage trie of an account. From 8cc4463bbdde286c0f8bae31d9d13fafb91e6227 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Sat, 7 Feb 2026 09:02:43 +0100 Subject: [PATCH 09/10] two-steps bootstrapping --- core/blockchain_reader.go | 13 +++++++- core/state/database.go | 60 +++++++++++++++++++++++++------------ core/state_processor.go | 28 +++++++++++++++-- core/transition_registry.go | 2 +- miner/worker.go | 23 ++++++++++++-- 5 files changed, 99 insertions(+), 27 deletions(-) diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index ee15c152c4..9938df0595 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -32,6 +32,8 @@ import ( "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/trie/bintrie" "github.com/ethereum/go-ethereum/triedb" ) @@ -370,8 +372,17 @@ func (bc *BlockChain) TxIndexDone() bool { } // HasState checks if state trie is fully present in the database or not. +// It avoids using OpenTrie which has transition-aware logic that may try +// to open a binary tree before it exists. Instead, it directly attempts +// to decode the root node as an MPT first, and if that fails, as a binary +// trie. func (bc *BlockChain) HasState(hash common.Hash) bool { - _, err := bc.statedb.OpenTrie(hash) + // Try to open as a Merkle Patricia Trie first. + if _, err := trie.NewStateTrie(trie.StateTrieID(hash), bc.triedb); err == nil { + return true + } + // Fall back to trying as a binary trie. + _, err := bintrie.NewBinaryTrie(hash, bc.triedb) return err == nil } diff --git a/core/state/database.go b/core/state/database.go index fadaae1b5b..0fc6a6e3bd 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -184,6 +184,7 @@ var ( conversionProgressSlotKey = common.Hash{2} // slot 2: current slot pointer conversionProgressStorageProcessed = common.Hash{3} // slot 3: storage processed flag transitionEndedKey = common.Hash{4} // slot 4: non-zero if transition ended + baseRootKey = common.Hash{5} // slot 5: MPT base root at transition start ) // isTransitionActive checks if the binary tree transition has been activated @@ -232,12 +233,18 @@ func LoadTransitionState(reader StateReader, root common.Hash) *overlay.Transiti } storageProcessed := storageProcessedBytes[0] == 1 + baseRoot, err := reader.Storage(params.BinaryTransitionRegistryAddress, baseRootKey) + if err != nil { + return nil + } + return &overlay.TransitionState{ Started: started, Ended: ended, CurrentAccountAddress: ¤tAccount, CurrentSlotHash: currentSlotHash, StorageProcessed: storageProcessed, + BaseRoot: baseRoot, } } @@ -307,30 +314,45 @@ 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) { - reader, err := db.triedb.StateReader(root) - if err != nil { - return nil, err - } - flatReader := newFlatReader(reader) - - ts := LoadTransitionState(flatReader, root) - if isTransitionActive(flatReader) || db.triedb.IsVerkle() { - bt, err := bintrie.NewBinaryTrie(root, db.triedb) + // Only attempt transition-aware trie opening in path scheme, since + // hashdb does not implement StateReader. + if db.TrieDB().Scheme() == rawdb.PathScheme { + reader, err := db.triedb.StateReader(root) if err != nil { - return nil, fmt.Errorf("could not open the overlay tree: %w", err) - } - if !ts.InTransition() { - // Transition complete, use BinaryTrie only - return bt, nil + return nil, err } + flatReader := newFlatReader(reader) - 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) + ts := LoadTransitionState(flatReader, root) + if isTransitionActive(flatReader) || db.triedb.IsVerkle() { + fmt.Printf("Opening transition-aware trie for root %s with transition state: %+v\n", root, ts) + + // special case of the tree bootsrap: the root will be that of the MPT, so in that + // case, open an empty binary tree. + var bt *bintrie.BinaryTrie + if ts.BaseRoot == (common.Hash{}) { + bt, err = bintrie.NewBinaryTrie(common.Hash{}, db.triedb) + if err != nil { + return nil, fmt.Errorf("could not bootstrap the overlay tree: %w", err) + } + } else { + bt, err = bintrie.NewBinaryTrie(root, db.triedb) + if err != nil { + 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(ts.BaseRoot), db.triedb) + if err != nil { + return nil, fmt.Errorf("could not create base trie in OpenTrie: %w", err) + } + return transitiontrie.NewTransitionTrie(base, bt, false), nil } - return transitiontrie.NewTransitionTrie(base, bt, false), nil } - return trie.NewStateTrie(trie.StateTrieID(root), db.triedb) } diff --git a/core/state_processor.go b/core/state_processor.go index 558181ca5b..eeffc5bb16 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -87,15 +87,28 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg context = NewEVMBlockContext(header, p.chain, nil) evm := vm.NewEVM(context, tracingStateDB, config, cfg) + if config.IsVerkle(header.Number, header.Time) { + // Bootstrap part deux: initialize the base root in the registry, + // as this is the first UBT block (which is the _second_ block of + // the transition, after the bootstrapping block that initializes + // the registry). + parentHeader := p.chain.GetHeaderByHash(block.ParentHash()) + // Confusingly, the first IsVerkle block isn't "verkle" + if config.IsVerkle(parentHeader.Number, parentHeader.Time) { + // Store the parent's state root as the MPT base root for the + // binary trie transition. Only written once (first verkle block), + // before InitializeBinaryTransitionRegistry sets slot 0. + if statedb.GetState(params.BinaryTransitionRegistryAddress, common.Hash{5}) == (common.Hash{}) { + statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{5}, parentHeader.Root) + } + } + } if beaconRoot := block.BeaconRoot(); beaconRoot != nil { ProcessBeaconBlockRoot(*beaconRoot, evm) } if config.IsPrague(block.Number(), block.Time()) || config.IsVerkle(block.Number(), block.Time()) { ProcessParentBlockHash(block.ParentHash(), evm) } - if config.IsVerkle(header.Number, header.Time) { - InitializeBinaryTransitionRegistry(statedb) - } // Iterate over and process the individual transactions for i, tx := range block.Transactions() { @@ -129,6 +142,15 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return nil, fmt.Errorf("failed to process consolidation queue: %w", err) } } + if config.IsVerkle(header.Number, header.Time) { + // Bootstrap part one: initialize the registry to mark the transition as started, + // which has to be done at the end of the _previous_ block, so that the information + // can bee made available inside the tree. + parentHeader := p.chain.GetHeaderByHash(block.ParentHash()) + if !config.IsVerkle(parentHeader.Number, parentHeader.Time) { + InitializeBinaryTransitionRegistry(statedb) + } + } // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) diff --git a/core/transition_registry.go b/core/transition_registry.go index 83cb62e30a..20f4a282ad 100644 --- a/core/transition_registry.go +++ b/core/transition_registry.go @@ -31,5 +31,5 @@ func InitializeBinaryTransitionRegistry(statedb *state.StateDB) { } statedb.SetCode(params.BinaryTransitionRegistryAddress, []byte{1, 2, 3}, tracing.CodeChangeUnspecified) statedb.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) - statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) + statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) // slot 0: started } diff --git a/miner/worker.go b/miner/worker.go index cb2e24d0cd..a3325c10cb 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -167,6 +167,15 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay return &newPayloadResult{err: err} } } + if miner.chainConfig.IsVerkle(work.header.Number, work.header.Time) { + // Bootstrap part one: initialize the registry to mark the transition as started, + // which has to be done at the end of the _previous_ block, so that the information + // can bee made available inside the tree. + parentHeader := miner.chain.GetHeaderByHash(work.header.ParentHash) // XXX parent could be added to the environment in prepareWork to avoid this lookup + if !miner.chainConfig.IsVerkle(parentHeader.Number, parentHeader.Time) { + core.InitializeBinaryTransitionRegistry(work.state) + } + } if requests != nil { reqHash := types.CalcRequestsHash(requests) work.header.RequestsHash = &reqHash @@ -260,15 +269,23 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir log.Error("Failed to create sealing context", "err", err) return nil, err } + if miner.chainConfig.IsVerkle(header.Number, header.Time) { + // Bootstrap part deux: initialize the base root in the registry, + // as this is the first UBT block (which is the _second_ block of + // the transition, after the bootstrapping block that initializes + // the registry). + if miner.chainConfig.IsVerkle(parent.Number, parent.Time) { + if env.state.GetState(params.BinaryTransitionRegistryAddress, common.Hash{5}) == (common.Hash{}) { + env.state.SetState(params.BinaryTransitionRegistryAddress, common.Hash{5}, parent.Root) + } + } + } if header.ParentBeaconRoot != nil { core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, env.evm) } if miner.chainConfig.IsPrague(header.Number, header.Time) { core.ProcessParentBlockHash(header.ParentHash, env.evm) } - if miner.chainConfig.IsVerkle(header.Number, header.Time) { - core.InitializeBinaryTransitionRegistry(env.state) - } return env, nil } From 14839c9113a421b2ddf6b39205257ad1f5dee3cf Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:39:44 +0100 Subject: [PATCH 10/10] fix prefetcher issue + reader bootstrap and get rid of triedb.IsVerkle() checks (#564) - Fix an issue in which the prefetcher was creating a new MPT tree in verkle mode, based on the fact that triedb.IsVerkle() was false. - Also fix the tree bootstrapping in the reader - generally, no longer use db.triedb.IsVerkle() as it's only true if the db is used when starting in verkle mode. --- core/state/reader.go | 13 +++++++++---- core/state/state_object.go | 3 +++ core/state/statedb.go | 14 +++++++++----- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/core/state/reader.go b/core/state/reader.go index fd999b11ac..74ff665804 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -318,17 +318,22 @@ func newTrieReader(root common.Hash, db *triedb.Database, ts *overlay.Transition tr Trie err error ) - if !db.IsVerkle() { + if !db.IsVerkle() && (ts == nil || !ts.InTransition()) { 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) + var binTrie *bintrie.BinaryTrie + var binErr error + if ts.BaseRoot == (common.Hash{}) { + binTrie, binErr = bintrie.NewBinaryTrie(common.Hash{}, db) + } else { + binTrie, binErr = bintrie.NewBinaryTrie(root, 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 + // 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) diff --git a/core/state/state_object.go b/core/state/state_object.go index 2873c3cb8a..418014b0d0 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -147,6 +147,9 @@ func (s *stateObject) getTrie() (Trie, error) { func (s *stateObject) getPrefetchedTrie() Trie { // If there's nothing to meaningfully return, let the user figure it out by // pulling the trie from disk. + if s.db.trie != nil && s.db.trie.IsVerkle() { + return nil + } if (s.data.Root == types.EmptyRootHash && !s.db.db.TrieDB().IsVerkle()) || s.db.prefetcher == nil { return nil } diff --git a/core/state/statedb.go b/core/state/statedb.go index 61d80df56e..8dfe979b77 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -356,6 +356,10 @@ func (s *StateDB) InTransition() bool { return completeValue != (common.Hash{}) } +func (s *StateDB) isVerkle() bool { + return s.db.TrieDB().IsVerkle() || (s.trie != nil && s.trie.IsVerkle()) +} + // TxIndex returns the current transaction index set by SetTxContext. func (s *StateDB) TxIndex() int { return s.txIndex @@ -830,7 +834,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { start = time.Now() workers errgroup.Group ) - if s.db.TrieDB().IsVerkle() { + if s.isVerkle() { // Whilst MPT storage tries are independent, Verkle has one single trie // for all the accounts and all the storage slots merged together. The // former can thus be simply parallelized, but updating the latter will @@ -844,7 +848,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { } obj := s.stateObjects[addr] // closure for the task runner below workers.Go(func() error { - if s.db.TrieDB().IsVerkle() { + if s.isVerkle() { obj.updateTrie() } else { obj.updateRoot() @@ -861,7 +865,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled, gather all the read-only accesses. // Skip witness collection in Verkle mode, they will be gathered // together at the end. - if s.witness != nil && !s.db.TrieDB().IsVerkle() { + if s.witness != nil && !s.isVerkle() { // Pull in anything that has been accessed before destruction for _, obj := range s.stateObjectsDestruct { // Skip any objects that haven't touched their storage @@ -918,7 +922,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // only a single trie is used for state hashing. Replacing a non-nil verkle tree // here could result in losing uncommitted changes from storage. start = time.Now() - if s.prefetcher != nil { + if s.prefetcher != nil && !s.isVerkle() { if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil { log.Error("Failed to retrieve account pre-fetcher trie") } else { @@ -1137,7 +1141,7 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco deletes[addrHash] = op // Short circuit if the origin storage was empty. - if prev.Root == types.EmptyRootHash || s.db.TrieDB().IsVerkle() { + if prev.Root == types.EmptyRootHash || s.isVerkle() { continue } if noStorageWiping {