diff --git a/core/bintrie_transition_test.go b/core/bintrie_transition_test.go new file mode 100644 index 0000000000..4c3aff90f8 --- /dev/null +++ b/core/bintrie_transition_test.go @@ -0,0 +1,107 @@ +// 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 ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/params" +) + +// TestBinaryTransitionRegistryBootstrap exercises the registry deployment on +// the first UBT block: pre-fork blocks must leave the registry uninitialised, +// the first UBT block must mark started=true and the base root must be +// captured for every subsequent UBT block. +func TestBinaryTransitionRegistryBootstrap(t *testing.T) { + var ( + ubtTime uint64 = 30 + coinbase = common.HexToAddress("0x71562b71999873DB5b286dF957af199Ec94617F7") + gspec = &Genesis{ + Config: ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + Ethash: new(params.EthashConfig), + ShanghaiTime: u64(0), + CancunTime: u64(0), + PragueTime: u64(0), + UBTTime: &ubtTime, + TerminalTotalDifficulty: common.Big0, + EnableUBTAtGenesis: false, + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + UBT: params.DefaultPragueBlobConfig, + }, + }, + Alloc: GenesisAlloc{ + coinbase: { + Balance: big.NewInt(1000000000000000000), + }, + params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0}, + params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0}, + params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0}, + params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0}, + }, + } + ) + config := gspec.Config + engine := beacon.New(ethash.NewFaker()) + + registryAddr := params.BinaryTransitionRegistryAddress + slotStarted := common.Hash{} + slotBaseRoot := common.BytesToHash([]byte{5}) + + GenerateChainWithGenesis(gspec, engine, 6, func(i int, gen *BlockGen) { + gen.SetPoS() + + blockNum := gen.Number() + blockTime := gen.Timestamp() + isUBT := config.IsUBT(blockNum, blockTime) + + started := gen.GetState(registryAddr, slotStarted) + baseRoot := gen.GetState(registryAddr, slotBaseRoot) + t.Logf("block %d: num=%d time=%d isUBT=%v started=%x baseRoot=%x", + i, blockNum.Uint64(), blockTime, isUBT, started, baseRoot) + + if !isUBT { + if started != (common.Hash{}) { + t.Errorf("block %d: pre-transition block should not have registry initialized", i) + } + return + } + if started == (common.Hash{}) { + t.Errorf("block %d: UBT block should have registry slot 0 (started) set", i) + } + if baseRoot == (common.Hash{}) { + t.Errorf("block %d: UBT block should have registry slot 5 (base root) set", i) + } + }) +} diff --git a/core/blockchain.go b/core/blockchain.go index 296ef6bc16..882fb86927 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/history" + "github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" @@ -257,6 +258,13 @@ func (cfg BlockChainConfig) WithNoAsyncFlush(on bool) *BlockChainConfig { return &cfg } +// newBinaryTrieDB opens the sibling triedb that holds binary trie nodes +// during the MPT-to-binary transition. It uses the same scheme and limits as +// the main triedb but is forced into UBT mode. +func newBinaryTrieDB(db ethdb.Database, cfg *BlockChainConfig) *triedb.Database { + return triedb.NewDatabase(db, cfg.triedbConfig(true)) +} + // triedbConfig derives the configures for trie database. func (cfg *BlockChainConfig) triedbConfig(isUBT bool) *triedb.Config { config := &triedb.Config{ @@ -323,6 +331,7 @@ type BlockChain struct { lastWrite uint64 // Last block when the state was flushed flushInterval atomic.Int64 // Time interval (processing time) after which to flush a state triedb *triedb.Database // The database handler for maintaining trie nodes. + bintriedb *triedb.Database // Sibling triedb for binary trie nodes during the MPT-to-binary transition; nil if no UBT fork is configured or the chain is binary at genesis. codedb *state.CodeDB // The database handler for maintaining contract codes. txIndexer *txIndexer // Transaction indexer, might be nil if not enabled @@ -382,16 +391,26 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, if err != nil { return nil, err } - triedb := triedb.NewDatabase(db, cfg.triedbConfig(enableVerkle)) + mainTriedb := triedb.NewDatabase(db, cfg.triedbConfig(enableVerkle)) + + // Open a sibling binary trie database when the chain is configured for an + // MPT-to-binary transition (i.e. UBTTime is set and the chain did not start + // binary at genesis). The two databases run in parallel during the + // transition window: writes target the binary trie, reads fall through to + // the frozen MPT base resolved from the transition registry. + var bintriedb *triedb.Database // Write the supplied genesis to the database if it has not been initialized // yet. The corresponding chain config will be returned, either from the // provided genesis or from the locally stored configuration if the genesis // has already been initialized. - chainConfig, genesisHash, compatErr, err := SetupGenesisBlockWithOverride(db, triedb, genesis, cfg.Overrides, cfg.VmConfig.Tracer) + chainConfig, genesisHash, compatErr, err := SetupGenesisBlockWithOverride(db, mainTriedb, genesis, cfg.Overrides, cfg.VmConfig.Tracer) if err != nil { return nil, err } + if !enableVerkle && chainConfig.UBTTime != nil { + bintriedb = newBinaryTrieDB(db, cfg) + } log.Info("") log.Info(strings.Repeat("-", 153)) for _, line := range strings.Split(chainConfig.Description(), "\n") { @@ -404,7 +423,8 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, chainConfig: chainConfig, cfg: cfg, db: db, - triedb: triedb, + triedb: mainTriedb, + bintriedb: bintriedb, codedb: state.NewCodeDB(db), triegc: prque.New[int64, common.Hash](nil), chainmu: syncx.NewClosableMutex(), @@ -1349,6 +1369,11 @@ func (bc *BlockChain) Stop() { if err := bc.triedb.Journal(bc.CurrentBlock().Root); err != nil { log.Info("Failed to journal in-memory trie nodes", "err", err) } + if bc.bintriedb != nil { + if err := bc.bintriedb.Journal(bc.CurrentBlock().Root); err != nil { + log.Info("Failed to journal in-memory binary trie nodes", "err", err) + } + } } else { // Ensure the state of a recent block is also stored to disk before exiting. // We're writing three different states to catch different restart scenarios: @@ -1390,6 +1415,11 @@ func (bc *BlockChain) Stop() { if err := bc.triedb.Close(); err != nil { log.Error("Failed to close trie database", "err", err) } + if bc.bintriedb != nil { + if err := bc.bintriedb.Close(); err != nil { + log.Error("Failed to close binary trie database", "err", err) + } + } log.Info("Blockchain stopped") } @@ -2108,6 +2138,72 @@ type ExecuteConfig struct { EnableWitnessStats bool } +// stateDatabase returns the appropriate state.Database for executing a block +// with the given header against the supplied parent root. The routing is: +// +// - pre-UBT-fork blocks → MPTDatabase backed by bc.triedb. +// - first UBT block (parent is pre-UBT) → transition UBTDatabase, with the +// binary trie as primary store and the parent's MPT root as the frozen +// base. The MPT triedb is provided for read-through during the transition. +// - subsequent UBT blocks → if the registry still reports an active +// transition, a transition UBTDatabase is built; otherwise (transition +// ended, or chainConfig.UBTTransitionEndTime crossed) a plain UBTDatabase +// is returned with the transition wrap disabled. +func (bc *BlockChain) stateDatabase(parentRoot common.Hash, header *types.Header) (state.Database, error) { + if !bc.chainConfig.IsUBT(header.Number, header.Time) { + return state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps), nil + } + // Past the configured transition end: pure binary, no wrap, no MPT base. + if !bc.chainConfig.UBTTransitionActive(header.Number, header.Time) { + return state.NewUBTDatabase(bc.bintriedbOrMain(), bc.codedb).WithTransitionTreeWrap(false), nil + } + // First UBT block: parent is pre-UBT; seed the transition with parent root. + parent := bc.GetHeaderByHash(header.ParentHash) + if parent != nil && !bc.chainConfig.IsUBT(parent.Number, parent.Time) { + return state.NewTransitionUBTDatabase(bc.bintriedbOrMain(), bc.triedb, bc.codedb, parentRoot), nil + } + // Subsequent UBT block while the transition is active: probe the registry + // to see whether a base root is still recorded. + return bc.probeTransitionDatabase(parentRoot) +} + +// bintriedbOrMain returns bc.bintriedb when present, otherwise the main triedb. +// Binary-at-genesis chains have no sibling binary triedb because the main +// triedb is already in UBT mode. +func (bc *BlockChain) bintriedbOrMain() *triedb.Database { + if bc.bintriedb != nil { + return bc.bintriedb + } + return bc.triedb +} + +// probeTransitionDatabase reads the transition registry from the binary trie +// at the given root and chooses between a transition-mode UBTDatabase (if +// the registry still records a base root) and a plain UBTDatabase otherwise. +func (bc *BlockChain) probeTransitionDatabase(root common.Hash) (state.Database, error) { + bindb := bc.bintriedbOrMain() + plain := state.NewUBTDatabase(bindb, bc.codedb) + reader, err := plain.StateReader(root) + if err != nil { + return nil, err + } + ts, err := overlay.LoadTransitionState(storageReaderFunc(reader.Storage)) + if err != nil { + return nil, err + } + if ts == nil || ts.Transitioned() || ts.BaseRoot == (common.Hash{}) { + return plain, nil + } + return state.NewTransitionUBTDatabase(bindb, bc.triedb, bc.codedb, ts.BaseRoot), nil +} + +// storageReaderFunc adapts a state-reader Storage method to overlay.StorageReader. +type storageReaderFunc func(addr common.Address, slot common.Hash) (common.Hash, error) + +func (f storageReaderFunc) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + return f(addr, slot) +} + // ProcessBlock executes and validates the given block. If there was no error // it writes the block and associated state to database. func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) { @@ -2116,14 +2212,12 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, startTime = time.Now() statedb *state.StateDB interrupt atomic.Bool - sdb state.Database ) defer interrupt.Store(true) // terminate the prefetch at the end - if bc.chainConfig.IsUBT(block.Number(), block.Time()) { - sdb = state.NewUBTDatabase(bc.triedb, bc.codedb) - } else { - sdb = state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps) + sdb, err := bc.stateDatabase(parentRoot, block.Header()) + if err != nil { + return nil, err } // If prefetching is enabled, run that against the current state to pre-cache // transactions and probabilistically some of the account/storage trie nodes. diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 18afa9ce9d..c9c6671b39 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -421,27 +421,21 @@ func (bc *BlockChain) State() (*state.StateDB, error) { // StateAt returns a new mutable state based on a particular point in time. func (bc *BlockChain) StateAt(header *types.Header) (*state.StateDB, error) { - if bc.chainConfig.IsUBT(header.Number, header.Time) { - return state.New(header.Root, state.NewUBTDatabase(bc.triedb, bc.codedb)) + db, err := bc.stateDatabase(header.Root, header) + if err != nil { + return nil, err } - return state.New(header.Root, state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)) + return state.New(header.Root, db) } // StateAtForkBoundary returns a new mutable state based on the parent state // and the given header, handling the transition across the UBT fork. func (bc *BlockChain) StateAtForkBoundary(parent *types.Header, header *types.Header) (*state.StateDB, error) { - // The parent is already in the UBT fork. - if bc.chainConfig.IsUBT(parent.Number, parent.Time) { - return state.New(parent.Root, state.NewUBTDatabase(bc.triedb, bc.codedb)) + db, err := bc.stateDatabase(parent.Root, header) + if err != nil { + return nil, err } - // The current block is the first block in the UBT fork - // (i.e., the parent is the last MPT block). - if bc.chainConfig.IsUBT(header.Number, header.Time) { - // TODO(gballet): register chain context if needed - return state.New(parent.Root, state.NewUBTDatabase(bc.triedb, bc.codedb)) - } - // Both the parent and current block are in the MPT fork. - return state.New(parent.Root, state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)) + return state.New(parent.Root, db) } // HistoricState returns a historic state specified by the given header. diff --git a/core/chain_makers.go b/core/chain_makers.go index 46cd98de61..aefae433ca 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -172,6 +172,14 @@ func (b *BlockGen) GetBalance(addr common.Address) *uint256.Int { return b.statedb.GetBalance(addr) } +// GetState returns the storage slot value for the given address and key as +// observed by the in-progress block generator. Useful in tests that need to +// inspect system-contract slots written during block production (e.g. the +// binary transition registry). +func (b *BlockGen) GetState(addr common.Address, key common.Hash) common.Hash { + return b.statedb.GetState(addr, key) +} + // AddUncheckedTx forcefully adds a transaction to the block without any validation. // // AddUncheckedTx will cause consensus failures when used during real @@ -399,6 +407,13 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse ProcessParentBlockHash(b.header.ParentHash, evm) } + // On the first UBT block, deploy the binary transition registry and + // record the parent's MPT root as the frozen base root. + if config.IsUBT(b.header.Number, b.header.Time) && !config.IsUBT(parent.Number(), parent.Time()) { + InitializeBinaryTransitionRegistry(statedb) + WriteBinaryTransitionBaseRoot(statedb, parent.Root()) + } + // Execute any user modifications to the block if gen != nil { gen(i, b) diff --git a/core/genesis.go b/core/genesis.go index d77ea10d8c..8095b4442a 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -276,10 +276,11 @@ func (e *GenesisMismatchError) Error() string { // ChainOverrides contains the changes to chain config. type ChainOverrides struct { - OverrideOsaka *uint64 - OverrideBPO1 *uint64 - OverrideBPO2 *uint64 - OverrideUBT *uint64 + OverrideOsaka *uint64 + OverrideBPO1 *uint64 + OverrideBPO2 *uint64 + OverrideUBT *uint64 + OverrideUBTTransitionEnd *uint64 } // apply applies the chain overrides on the supplied chain config. @@ -299,6 +300,9 @@ func (o *ChainOverrides) apply(cfg *params.ChainConfig) error { if o.OverrideUBT != nil { cfg.UBTTime = o.OverrideUBT } + if o.OverrideUBTTransitionEnd != nil { + cfg.UBTTransitionEndTime = o.OverrideUBTTransitionEnd + } return cfg.CheckConfigForkOrder() } diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go index afd2bab017..58680cae30 100644 --- a/core/overlay/state_transition.go +++ b/core/overlay/state_transition.go @@ -17,29 +17,39 @@ 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" ) +// Storage slots used by the binary transition registry system contract at +// params.BinaryTransitionRegistryAddress. +var ( + transitionStartedKey = common.Hash{} + conversionProgressAddressKey = common.BytesToHash([]byte{1}) + conversionProgressSlotKey = common.BytesToHash([]byte{2}) + conversionProgressStorageProcessed = common.BytesToHash([]byte{3}) + transitionEndedKey = common.BytesToHash([]byte{4}) + baseRootKey = common.BytesToHash([]byte{5}) +) + +// StorageReader is a minimal interface for reading contract storage slots. +type StorageReader interface { + Storage(addr common.Address, slot common.Hash) (common.Hash, error) +} + // TransitionState is a structure that holds the progress markers of the // translation process. type TransitionState struct { - CurrentAccountAddress *common.Address // addresss of the last translated account + CurrentAccountAddress *common.Address // address of the last translated account CurrentSlotHash common.Hash // hash of the last translated storage slot - CurrentPreimageOffset int64 // next byte to read from the preimage file Started, Ended bool - // Mark whether the storage for an account has been processed. This is useful if the - // maximum number of leaves of the conversion is reached before the whole storage is - // processed. + // StorageProcessed marks whether the storage of the current account has + // been fully processed. Useful when the maximum number of leaves of the + // conversion is reached before the storage is exhausted. StorageProcessed bool - BaseRoot common.Hash // hash of the last read-only MPT base tree + BaseRoot common.Hash // frozen MPT base root captured at the fork block } // InTransition returns true if the translation process is in progress. @@ -55,12 +65,11 @@ func (ts *TransitionState) Transitioned() bool { // Copy returns a deep copy of the TransitionState object. func (ts *TransitionState) Copy() *TransitionState { ret := &TransitionState{ - Started: ts.Started, - Ended: ts.Ended, - CurrentSlotHash: ts.CurrentSlotHash, - CurrentPreimageOffset: ts.CurrentPreimageOffset, - StorageProcessed: ts.StorageProcessed, - BaseRoot: ts.BaseRoot, + Started: ts.Started, + Ended: ts.Ended, + CurrentSlotHash: ts.CurrentSlotHash, + StorageProcessed: ts.StorageProcessed, + BaseRoot: ts.BaseRoot, } if ts.CurrentAccountAddress != nil { addr := *ts.CurrentAccountAddress @@ -69,38 +78,54 @@ 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, isUBT 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 +// IsTransitionActive checks whether the binary transition registry has been +// initialised by reading slot 0 (started) from the system contract. +func IsTransitionActive(reader StorageReader) bool { + val, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionStartedKey) + if err != nil { + return false } - - // 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", isUBT) - - // Start with a fresh state - ts = &TransitionState{Ended: isUBT} - } - return ts + return val != (common.Hash{}) +} + +// LoadTransitionState reads the full transition state from the binary +// transition registry system contract storage. Returns nil when the +// registry has not been initialised (i.e. the chain has not yet reached the +// UBT fork block). +func LoadTransitionState(reader StorageReader) (*TransitionState, error) { + started, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionStartedKey) + if err != nil { + return nil, err + } + if started == (common.Hash{}) { + return nil, nil + } + + ended, _ := reader.Storage(params.BinaryTransitionRegistryAddress, transitionEndedKey) + baseRoot, _ := reader.Storage(params.BinaryTransitionRegistryAddress, baseRootKey) + + var currentAddr *common.Address + addrVal, _ := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressAddressKey) + if addrVal != (common.Hash{}) { + addr := common.BytesToAddress(addrVal.Bytes()) + currentAddr = &addr + } + + slotHash, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressSlotKey) + if err != nil { + return nil, err + } + storageProcessed, err := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressStorageProcessed) + if err != nil { + return nil, err + } + + return &TransitionState{ + Started: true, + Ended: ended != (common.Hash{}), + BaseRoot: baseRoot, + CurrentAccountAddress: currentAddr, + CurrentSlotHash: slotHash, + StorageProcessed: storageProcessed != (common.Hash{}), + }, nil } diff --git a/core/rawdb/accessors_overlay.go b/core/rawdb/accessors_overlay.go deleted file mode 100644 index 364cc889d1..0000000000 --- a/core/rawdb/accessors_overlay.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2025 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 rawdb - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethdb" -) - -func ReadVerkleTransitionState(db ethdb.KeyValueReader, hash common.Hash) ([]byte, error) { - return db.Get(transitionStateKey(hash)) -} - -func WriteVerkleTransitionState(db ethdb.KeyValueWriter, hash common.Hash, state []byte) error { - return db.Put(transitionStateKey(hash), state) -} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 39e1a64e5a..ccf2eace65 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -691,7 +691,7 @@ var knownMetadataKeys = [][]byte{ snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey, uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey, persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, snapSyncStatusFlagKey, - filterMapsRangeKey, headStateHistoryIndexKey, headTrienodeHistoryIndexKey, VerkleTransitionStatePrefix, + filterMapsRangeKey, headStateHistoryIndexKey, headTrienodeHistoryIndexKey, } // printChainMetadata prints out chain metadata to stderr. diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 54c76143b4..a5ebb961f7 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -165,9 +165,6 @@ var ( preimageCounter = metrics.NewRegisteredCounter("db/preimage/total", nil) preimageHitsCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil) preimageMissCounter = metrics.NewRegisteredCounter("db/preimage/miss", nil) - - // Verkle transition information - VerkleTransitionStatePrefix = []byte("verkle-transition-state-") ) // LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary @@ -460,8 +457,3 @@ func trienodeHistoryIndexBlockKey(addressHash common.Hash, path []byte, blockID return out } - -// transitionStateKey = transitionStatusKey + hash -func transitionStateKey(hash common.Hash) []byte { - return append(VerkleTransitionStatePrefix, hash.Bytes()...) -} diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go index 718d93df87..287d683a8f 100644 --- a/core/state/database_ubt.go +++ b/core/state/database_ubt.go @@ -17,18 +17,25 @@ package state import ( + "errors" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/bintrie" + "github.com/ethereum/go-ethereum/trie/transitiontrie" "github.com/ethereum/go-ethereum/triedb" ) // UBTDatabase is an implementation of Database interface for Unified Binary Trie. -// It provides the same functionality as MPTDatabase but uses unified binary -// trie for state hashing instead of Merkle Patricia Tries. type UBTDatabase struct { - triedb *triedb.Database - codedb *CodeDB + triedb *triedb.Database + mpttriedb *triedb.Database + codedb *CodeDB + baseRoot common.Hash + wrapInTransitionTrie bool } // Type returns Binary, indicating this database is backed by a Universal Binary Trie. @@ -40,11 +47,38 @@ func NewUBTDatabase(triedb *triedb.Database, codedb *CodeDB) *UBTDatabase { codedb = NewCodeDB(triedb.Disk()) } return &UBTDatabase{ - triedb: triedb, - codedb: codedb, + triedb: triedb, + codedb: codedb, + wrapInTransitionTrie: true, } } +// NewTransitionUBTDatabase creates a UBTDatabase for the active MPT-to-binary +// transition window. The binary trie is the primary store; reads fall through +// to the frozen MPT at baseRoot via the supplied mpttriedb, and writes go +// only to the binary trie. +func NewTransitionUBTDatabase(bintriedb, mpttriedb *triedb.Database, codedb *CodeDB, baseRoot common.Hash) *UBTDatabase { + if codedb == nil { + codedb = NewCodeDB(bintriedb.Disk()) + } + return &UBTDatabase{ + triedb: bintriedb, + mpttriedb: mpttriedb, + codedb: codedb, + baseRoot: baseRoot, + wrapInTransitionTrie: true, + } +} + +// WithTransitionTreeWrap toggles whether reads at this database are wrapped +// in a TransitionTrie. Setting it to false disables the wrap regardless of +// the registry state and is intended for callers that have already crossed +// the configured UBTTransitionEndTime. +func (db *UBTDatabase) WithTransitionTreeWrap(wrap bool) *UBTDatabase { + db.wrapInTransitionTrie = wrap + return db +} + // StateReader returns a state reader associated with the specified state root. func (db *UBTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) { var readers []StateReader @@ -61,7 +95,7 @@ func (db *UBTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) { } // Configure the trie reader, which is expected to be available as the // gatekeeper unless the state is corrupted. - tr, err := newUBTTrieReader(stateRoot, db.triedb) + tr, err := newUBTTrieReader(stateRoot, db.triedb, db.mpttriedb, db.wrapInTransitionTrie) if err != nil { return nil, err } @@ -96,13 +130,30 @@ func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Rea // OpenTrie opens the main account trie at a specific root hash. func (db *UBTDatabase) OpenTrie(root common.Hash) (Trie, error) { - return bintrie.NewBinaryTrie(root, db.triedb) + bt, err := bintrie.NewBinaryTrie(root, db.triedb) + if err != nil { + return nil, err + } + if db.mpttriedb == nil || db.baseRoot == (common.Hash{}) { + return transitiontrie.NewTransitionTrie(nil, bt, false), nil + } + base, err := trie.NewStateTrie(trie.StateTrieID(db.baseRoot), db.mpttriedb) + if err != nil { + return nil, err + } + return transitiontrie.NewTransitionTrie(base, bt, false), nil } -// OpenStorageTrie opens the storage trie of an account. In binary trie mode, -// all state objects share one unified trie, so the main trie is returned. +// OpenStorageTrie opens the storage trie of an account. In binary trie mode +// the unified trie carries all state, so the main trie is reused. func (db *UBTDatabase) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) { - return self, nil + if self != nil && self.IsUBT() { + return self, nil + } + if db.mpttriedb == nil { + return nil, errors.New("no MPT trie database available for storage trie outside the transition window") + } + return trie.NewStateTrie(trie.StorageTrieID(stateRoot, crypto.Keccak256Hash(address.Bytes()), root), db.mpttriedb) } // TrieDB retrieves any intermediate trie-node caching layer. @@ -128,10 +179,17 @@ func (db *UBTDatabase) Commit(update *StateUpdate) error { return err } } + // On the first transition block, the originRoot is the MPT base root, + // but the binary trie's parent state is empty. Substitute the empty + // binary hash so triedb.Update doesn't reject the mismatch. + originRoot := update.OriginRoot + if db.mpttriedb != nil && originRoot == db.baseRoot { + originRoot = types.EmptyBinaryHash + } // Encode the state mutations in the UBT format accounts, accountOrigin, storages, storageOrigin := update.EncodeUBTState() - return db.triedb.Update(update.Root, update.OriginRoot, update.BlockNumber, update.Nodes, &triedb.StateSet{ + return db.triedb.Update(update.Root, originRoot, update.BlockNumber, update.Nodes, &triedb.StateSet{ Accounts: accounts, AccountsOrigin: accountOrigin, Storages: storages, diff --git a/core/state/reader.go b/core/state/reader.go index 5df0acbb9b..140dbc3b60 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -252,46 +252,57 @@ type ubtTrieReader struct { lock sync.Mutex // Lock for protecting concurrent read } +// binTrieStorageReader adapts a *bintrie.BinaryTrie to overlay.StorageReader +// so the transition state can be loaded directly from the binary trie at a +// specific state root. +type binTrieStorageReader struct { + tr *bintrie.BinaryTrie +} + +func (r *binTrieStorageReader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { + val, err := r.tr.GetStorage(addr, slot.Bytes()) + if err != nil { + return common.Hash{}, err + } + var out common.Hash + out.SetBytes(val) + return out, nil +} + // newUBTTrieReader constructs a Unified-binary-trie reader of the specific state. // An error will be returned if the associated trie specified by root is not existent. -func newUBTTrieReader(root common.Hash, db *triedb.Database) (*ubtTrieReader, error) { - binTrie, binErr := bintrie.NewBinaryTrie(root, db) - if binErr != nil { - return nil, binErr +// +// When wrapInTransitionTrie is true, reads fall through to the frozen MPT +// base whenever the transition registry has captured a non-zero base root +// in slot 5 and an MPT triedb is available. When wrapInTransitionTrie is +// false, the MPT base is never consulted regardless of registry state. +// +// Both branches still build a TransitionTrie because *bintrie.BinaryTrie +// cannot satisfy the state.Trie interface directly (import cycle between +// trie and trie/bintrie). When wrap is false the base argument is nil and +// the wrapper degenerates to a passthrough. +func newUBTTrieReader(root common.Hash, bindb *triedb.Database, mptdb *triedb.Database, wrapInTransitionTrie bool) (*ubtTrieReader, error) { + binTrie, err := bintrie.NewBinaryTrie(root, bindb) + if err != nil { + return nil, err } - // Based on the transition status, determine if the overlay - // tree needs to be created, or if a single, target tree is - // to be picked. - var ( - tr Trie - ts = overlay.LoadTransitionState(db.Disk(), root, true) - ) - if ts.InTransition() { - mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db) + var base *trie.StateTrie + if wrapInTransitionTrie && mptdb != nil { + ts, err := overlay.LoadTransitionState(&binTrieStorageReader{tr: binTrie}) if err != nil { return nil, err } - tr = transitiontrie.NewTransitionTrie(mpt, binTrie, false) - } else { - // HACK: Use TransitionTrie with nil base as a wrapper to make BinaryTrie - // satisfy the Trie interface. This works around the import cycle between - // trie and trie/bintrie packages. - // - // TODO: In future PRs, refactor the package structure to avoid this hack: - // - Option 1: Move common interfaces (Trie, NodeIterator) to a separate - // package that both trie and trie/bintrie can import - // - Option 2: Create a factory function in the trie package that returns - // BinaryTrie as a Trie interface without direct import - // - Option 3: Move BinaryTrie to the main trie package - // - // The current approach works but adds unnecessary overhead and complexity - // by using TransitionTrie when there's no actual transition happening. - tr = transitiontrie.NewTransitionTrie(nil, binTrie, false) + if ts != nil && ts.BaseRoot != (common.Hash{}) { + base, err = trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), mptdb) + if err != nil { + return nil, err + } + } } return &ubtTrieReader{ root: root, - db: db, - tr: tr, + db: bindb, + tr: transitiontrie.NewTransitionTrie(base, binTrie, false), }, nil } diff --git a/core/state_processor.go b/core/state_processor.go index 54ebbd047b..c2c52402e7 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -96,6 +96,17 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated if config.IsPrague(block.Number(), block.Time()) || config.IsUBT(block.Number(), block.Time()) { ProcessParentBlockHash(block.ParentHash(), evm) } + // On the first block after the UBT activation, deploy the binary + // transition registry system contract and capture the frozen MPT base + // root in slot 5. The registry is what every subsequent block reads to + // reconstruct the transition state. + if config.IsUBT(block.Number(), block.Time()) { + parent := p.chain.GetHeaderByHash(block.ParentHash()) + if parent != nil && !config.IsUBT(parent.Number, parent.Time) { + InitializeBinaryTransitionRegistry(statedb) + WriteBinaryTransitionBaseRoot(statedb, parent.Root) + } + } // Iterate over and process the individual transactions for i, tx := range block.Transactions() { diff --git a/core/transition_registry.go b/core/transition_registry.go new file mode 100644 index 0000000000..5dff348f00 --- /dev/null +++ b/core/transition_registry.go @@ -0,0 +1,62 @@ +// 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" +) + +// transitionStatusByteCode is a minimal contract that returns a single 32-byte +// storage slot to the caller. CALLDATALOAD picks the slot index from the call +// input and SLOAD reads the value, which is then returned as a 32-byte word. +var transitionStatusByteCode = []byte{ + 0x60, 0x00, // PUSH1 0 + 0x35, // CALLDATALOAD (slot index) + 0x54, // SLOAD + 0x60, 0x00, // PUSH1 0 + 0x52, // MSTORE + 0x60, 0x20, // PUSH1 32 + 0x60, 0x00, // PUSH1 0 + 0xf3, // RETURN +} + +// transitionRegistryBaseRootSlot is slot 5 of the transition registry, where +// the frozen MPT base root is stored. The slot indices match those decoded by +// overlay.LoadTransitionState; the layout is intentionally kept private to +// the core package so external callers go through these helpers. +var transitionRegistryBaseRootSlot = common.BytesToHash([]byte{5}) + +// InitializeBinaryTransitionRegistry deploys the binary transition registry +// system contract and marks the transition as started by writing 1 into slot +// 0. It must be called exactly once, on the first block after the UBT +// activation. +func InitializeBinaryTransitionRegistry(statedb *state.StateDB) { + statedb.SetCode(params.BinaryTransitionRegistryAddress, transitionStatusByteCode, tracing.CodeChangeUnspecified) + statedb.SetNonce(params.BinaryTransitionRegistryAddress, 1, tracing.NonceChangeUnspecified) + statedb.SetState(params.BinaryTransitionRegistryAddress, common.Hash{}, common.Hash{1}) +} + +// WriteBinaryTransitionBaseRoot records the frozen MPT base root in slot 5 of +// the transition registry. This must be called on the first UBT block, right +// after InitializeBinaryTransitionRegistry, with the parent block's state +// root. +func WriteBinaryTransitionBaseRoot(statedb *state.StateDB, baseRoot common.Hash) { + statedb.SetState(params.BinaryTransitionRegistryAddress, transitionRegistryBaseRootSlot, baseRoot) +} diff --git a/miner/worker.go b/miner/worker.go index 42e3695025..892f3ced30 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -335,6 +335,13 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, if miner.chainConfig.IsPrague(header.Number, header.Time) { core.ProcessParentBlockHash(header.ParentHash, env.evm) } + // Deploy the binary transition registry on the first UBT block and seed + // it with the parent's MPT root. Subsequent blocks read the registry to + // reconstruct the transition state. + if miner.chainConfig.IsUBT(header.Number, header.Time) && !miner.chainConfig.IsUBT(parent.Number, parent.Time) { + core.InitializeBinaryTransitionRegistry(env.state) + core.WriteBinaryTransitionBaseRoot(env.state, parent.Root) + } return env, nil } diff --git a/params/config.go b/params/config.go index 17508cbf27..f02d239ad5 100644 --- a/params/config.go +++ b/params/config.go @@ -468,6 +468,11 @@ type ChainConfig struct { AmsterdamTime *uint64 `json:"amsterdamTime,omitempty"` // Amsterdam switch time (nil = no fork, 0 = already on amsterdam) UBTTime *uint64 `json:"ubtTime,omitempty"` // UBT switch time (nil = no fork, 0 = already on UBT) + // UBTTransitionEndTime is the timestamp at which the MPT-to-binary + // transition tree is no longer applied. Mirrors the threshold semantics + // of TerminalTotalDifficulty. + UBTTransitionEndTime *uint64 `json:"ubtTransitionEndTime,omitempty"` + // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` @@ -598,6 +603,9 @@ func (c *ChainConfig) String() string { if c.UBTTime != nil { result += fmt.Sprintf(", UBTTime: %v", *c.UBTTime) } + if c.UBTTransitionEndTime != nil { + result += fmt.Sprintf(", UBTTransitionEndTime: %v", *c.UBTTransitionEndTime) + } result += "}" return result } @@ -693,6 +701,9 @@ func (c *ChainConfig) Description() string { if c.UBTTime != nil { banner += fmt.Sprintf(" - UBT: @%-10v blob: (%s)\n", *c.UBTTime, c.BlobScheduleConfig.UBT) } + if c.UBTTransitionEndTime != nil { + banner += fmt.Sprintf(" - UBT transition tree ends: @%-10v\n", *c.UBTTransitionEndTime) + } banner += fmt.Sprintf("\nAll fork specifications can be found at https://ethereum.github.io/execution-specs/src/ethereum/forks/\n") return banner } @@ -871,16 +882,18 @@ func (c *ChainConfig) IsUBT(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.UBTTime, time) } -// IsUBTGenesis checks whether the verkle fork is activated at the genesis block. -// -// Verkle mode is considered enabled if the verkle fork time is configured, -// regardless of whether the local time has surpassed the fork activation time. -// This is a temporary workaround for verkle devnet testing, where verkle is -// activated at genesis, and the configured activation date has already passed. -// -// In production networks (mainnet and public testnets), verkle activation -// always occurs after the genesis block, making this function irrelevant in -// those cases. +// UBTTransitionActive reports whether state access at the given block number +// and time should still be wrapped in a TransitionTrie (binary overlay on top +// of the frozen MPT base). It is true when UBT is active and either no end +// time is configured or the block is still before the configured end time. +func (c *ChainConfig) UBTTransitionActive(num *big.Int, time uint64) bool { + if !c.IsUBT(num, time) { + return false + } + return c.UBTTransitionEndTime == nil || time < *c.UBTTransitionEndTime +} + +// IsUBTGenesis checks whether the UBT fork is activated at the genesis block. func (c *ChainConfig) IsUBTGenesis() bool { return c.EnableUBTAtGenesis } diff --git a/params/protocol_params.go b/params/protocol_params.go index 9da275c486..44a8cc89c3 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -222,6 +222,10 @@ var ( // EIP-7251 - Increase the MAX_EFFECTIVE_BALANCE ConsolidationQueueAddress = common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251") ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd") + + // BinaryTransitionRegistryAddress is the system contract that exposes the + // MPT-to-binary transition state via storage slots. + BinaryTransitionRegistryAddress = common.HexToAddress("0x1622162216221622162216221622162216221622") ) // System log events.