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/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/overlay/state_transition.go b/core/overlay/state_transition.go index a52d9139c9..2fb7e9c3fd 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" ) // 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, 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 @@ -68,39 +72,3 @@ func (ts *TransitionState) Copy() *TransitionState { } return ret } - -// 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 - } - - // 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} - } - return ts -} diff --git a/core/state/database.go b/core/state/database.go index 4a5547d075..0fc6a6e3bd 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,76 @@ func NewDatabaseForTesting() *CachingDB { return NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil) } +var ( + 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 + baseRootKey = common.Hash{5} // slot 5: MPT base root at transition start +) + +// 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 { + return nil + } + ended := endedBytes != (common.Hash{}) + + currentAccountBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressAddressKey) + if err != nil { + return nil + } + currentAccount := common.BytesToAddress(currentAccountBytes[12:]) + + currentSlotHash, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressSlotKey) + if err != nil { + return nil + } + + storageProcessedBytes, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressStorageProcessed) + if err != nil { + return nil + } + 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, + } +} + // StateReader returns a state reader associated with the specified state root. func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) { var readers []StateReader @@ -190,6 +261,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 @@ -197,12 +269,16 @@ 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)) + 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 // 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 } @@ -238,27 +314,51 @@ 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") + // 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, err } - if ts.Transitioned() { - // Use BinaryTrie instead of VerkleTrie when IsVerkle is set - // (IsVerkle actually means Binary Trie mode in this codebase) - return bintrie.NewBinaryTrie(root, db.triedb) + flatReader := newFlatReader(reader) + + 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 } } - 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. 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/reader.go b/core/state/reader.go index 35b732173b..74ff665804 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -313,24 +313,28 @@ 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 ) - 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. - ts := overlay.LoadTransitionState(db.Disk(), root, true) if ts.InTransition() { mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db) if err != nil { diff --git a/core/state/state_object.go b/core/state/state_object.go index f7109bddee..d4981974a1 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -154,6 +154,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 3a2d9c9ac2..e8cb86f0f3 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -351,6 +351,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) 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 @@ -825,7 +836,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 @@ -839,7 +850,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() @@ -856,7 +867,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 @@ -913,7 +924,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 { @@ -1139,7 +1150,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 { diff --git a/core/state_processor.go b/core/state_processor.go index 6eea74bdd8..7b75339e29 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -89,6 +89,22 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated 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) } @@ -119,6 +135,15 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if err != nil { return nil, 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 new file mode 100644 index 0000000000..20f4a282ad --- /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}) // slot 0: started +} diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 1261320b58..66cb1e3458 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -251,6 +251,9 @@ 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()) { + 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 9e2140bd04..d7ee1461cb 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -186,6 +186,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 @@ -282,6 +291,17 @@ 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) } 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") )