This commit is contained in:
Guillaume Ballet 2026-02-24 21:54:53 -08:00 committed by GitHub
commit e5cfa0798c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 256 additions and 70 deletions

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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: &currentAccount,
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)

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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())

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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")
)