core, core/state: dual-triedb routing for the MPT-to-UBT transition

This is the wiring that makes the binary transition registry actually
useful. The chain now keeps two trie databases in parallel during the
fork window:

  - bc.triedb stays in MPT mode and holds pre-fork state.
  - bc.bintriedb is a sibling triedb in UBT mode, allocated when
    UBTTime is set and the chain did not start binary at genesis.

Routing happens through the new bc.stateDatabase(parentRoot, header):

  - pre-UBT block: MPTDatabase backed by bc.triedb.
  - post-UBTTransitionEndTime block: plain UBTDatabase with the
    transition tree wrap explicitly disabled.
  - first UBT block (parent is pre-UBT): a transition UBTDatabase
    seeded with the parent root as the frozen MPT base.
  - subsequent UBT block while transitioning: probeTransitionDatabase
    inspects the registry and chooses between the transition variant
    (when slot 5 still records a non-zero base root) and the plain
    variant otherwise.

UBTDatabase grows the supporting fields:
  - mpttriedb + baseRoot for the transition window.
  - NewTransitionUBTDatabase constructor.
  - WithTransitionTreeWrap(bool) toggle for the override path.
  - OpenTrie / OpenStorageTrie now serve TransitionTrie / MPT storage
    tries while the transition is live.
  - Commit substitutes EmptyBinaryHash for the originRoot on the very
    first transition block, so the binary triedb does not reject the
    MPT-rooted parent.

StateAt / StateAtForkBoundary on BlockChain now defer to
stateDatabase, so the miner, ProcessBlock and historical state lookups
all share one routing path.

bc.bintriedb is journaled and closed alongside bc.triedb.

Adds core/chain_makers.go BlockGen.GetState so tests can inspect
storage slots written during block production, and a new
core/bintrie_transition_test.go that walks a chain across the fork
and asserts the registry's started flag and base root are populated
exactly when expected.
This commit is contained in:
Guillaume Ballet 2026-04-29 16:55:13 +02:00
parent 58518ea2b3
commit d8ed290a40
No known key found for this signature in database
5 changed files with 300 additions and 41 deletions

View file

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

View file

@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core/history" "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/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/state/snapshot"
@ -257,6 +258,13 @@ func (cfg BlockChainConfig) WithNoAsyncFlush(on bool) *BlockChainConfig {
return &cfg 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. // triedbConfig derives the configures for trie database.
func (cfg *BlockChainConfig) triedbConfig(isUBT bool) *triedb.Config { func (cfg *BlockChainConfig) triedbConfig(isUBT bool) *triedb.Config {
config := &triedb.Config{ config := &triedb.Config{
@ -323,6 +331,7 @@ type BlockChain struct {
lastWrite uint64 // Last block when the state was flushed lastWrite uint64 // Last block when the state was flushed
flushInterval atomic.Int64 // Time interval (processing time) after which to flush a state flushInterval atomic.Int64 // Time interval (processing time) after which to flush a state
triedb *triedb.Database // The database handler for maintaining trie nodes. 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. codedb *state.CodeDB // The database handler for maintaining contract codes.
txIndexer *txIndexer // Transaction indexer, might be nil if not enabled 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 { if err != nil {
return nil, err 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 // Write the supplied genesis to the database if it has not been initialized
// yet. The corresponding chain config will be returned, either from the // yet. The corresponding chain config will be returned, either from the
// provided genesis or from the locally stored configuration if the genesis // provided genesis or from the locally stored configuration if the genesis
// has already been initialized. // 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 { if err != nil {
return nil, err return nil, err
} }
if !enableVerkle && chainConfig.UBTTime != nil {
bintriedb = newBinaryTrieDB(db, cfg)
}
log.Info("") log.Info("")
log.Info(strings.Repeat("-", 153)) log.Info(strings.Repeat("-", 153))
for _, line := range strings.Split(chainConfig.Description(), "\n") { for _, line := range strings.Split(chainConfig.Description(), "\n") {
@ -404,7 +423,8 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
chainConfig: chainConfig, chainConfig: chainConfig,
cfg: cfg, cfg: cfg,
db: db, db: db,
triedb: triedb, triedb: mainTriedb,
bintriedb: bintriedb,
codedb: state.NewCodeDB(db), codedb: state.NewCodeDB(db),
triegc: prque.New[int64, common.Hash](nil), triegc: prque.New[int64, common.Hash](nil),
chainmu: syncx.NewClosableMutex(), chainmu: syncx.NewClosableMutex(),
@ -1349,6 +1369,11 @@ func (bc *BlockChain) Stop() {
if err := bc.triedb.Journal(bc.CurrentBlock().Root); err != nil { if err := bc.triedb.Journal(bc.CurrentBlock().Root); err != nil {
log.Info("Failed to journal in-memory trie nodes", "err", err) 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 { } else {
// Ensure the state of a recent block is also stored to disk before exiting. // 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: // 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 { if err := bc.triedb.Close(); err != nil {
log.Error("Failed to close trie database", "err", err) 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") log.Info("Blockchain stopped")
} }
@ -2108,6 +2138,69 @@ type ExecuteConfig struct {
EnableWitnessStats bool 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 {
if !bc.chainConfig.IsUBT(header.Number, header.Time) {
return state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
}
// 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)
}
// 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)
}
// 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 {
bindb := bc.bintriedbOrMain()
plain := state.NewUBTDatabase(bindb, bc.codedb)
reader, err := plain.StateReader(root)
if err != nil {
return plain
}
ts := overlay.LoadTransitionState(storageReaderFunc(reader.Storage), root)
if ts == nil || ts.Transitioned() || ts.BaseRoot == (common.Hash{}) {
return plain
}
return state.NewTransitionUBTDatabase(bindb, bc.triedb, bc.codedb, ts.BaseRoot)
}
// 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 // ProcessBlock executes and validates the given block. If there was no error
// it writes the block and associated state to database. // 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) { func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) {
@ -2116,15 +2209,10 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
startTime = time.Now() startTime = time.Now()
statedb *state.StateDB statedb *state.StateDB
interrupt atomic.Bool interrupt atomic.Bool
sdb state.Database
) )
defer interrupt.Store(true) // terminate the prefetch at the end defer interrupt.Store(true) // terminate the prefetch at the end
if bc.chainConfig.IsUBT(block.Number(), block.Time()) { sdb := bc.stateDatabase(parentRoot, block.Header())
sdb = state.NewUBTDatabase(bc.triedb, bc.codedb)
} else {
sdb = state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
}
// If prefetching is enabled, run that against the current state to pre-cache // If prefetching is enabled, run that against the current state to pre-cache
// transactions and probabilistically some of the account/storage trie nodes. // transactions and probabilistically some of the account/storage trie nodes.
// //

View file

@ -421,27 +421,13 @@ func (bc *BlockChain) State() (*state.StateDB, error) {
// StateAt returns a new mutable state based on a particular point in time. // StateAt returns a new mutable state based on a particular point in time.
func (bc *BlockChain) StateAt(header *types.Header) (*state.StateDB, error) { func (bc *BlockChain) StateAt(header *types.Header) (*state.StateDB, error) {
if bc.chainConfig.IsUBT(header.Number, header.Time) { return state.New(header.Root, bc.stateDatabase(header.Root, header))
return state.New(header.Root, state.NewUBTDatabase(bc.triedb, bc.codedb))
}
return state.New(header.Root, state.NewMPTDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps))
} }
// StateAtForkBoundary returns a new mutable state based on the parent state // StateAtForkBoundary returns a new mutable state based on the parent state
// and the given header, handling the transition across the UBT fork. // and the given header, handling the transition across the UBT fork.
func (bc *BlockChain) StateAtForkBoundary(parent *types.Header, header *types.Header) (*state.StateDB, error) { func (bc *BlockChain) StateAtForkBoundary(parent *types.Header, header *types.Header) (*state.StateDB, error) {
// The parent is already in the UBT fork. return state.New(parent.Root, bc.stateDatabase(parent.Root, header))
if bc.chainConfig.IsUBT(parent.Number, parent.Time) {
return state.New(parent.Root, state.NewUBTDatabase(bc.triedb, bc.codedb))
}
// 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))
} }
// HistoricState returns a historic state specified by the given header. // HistoricState returns a historic state specified by the given header.

View file

@ -172,6 +172,14 @@ func (b *BlockGen) GetBalance(addr common.Address) *uint256.Int {
return b.statedb.GetBalance(addr) 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 forcefully adds a transaction to the block without any validation.
// //
// AddUncheckedTx will cause consensus failures when used during real // AddUncheckedTx will cause consensus failures when used during real

View file

@ -17,34 +17,78 @@
package state package state
import ( import (
"errors"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "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/bintrie"
"github.com/ethereum/go-ethereum/trie/transitiontrie"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
) )
// UBTDatabase is an implementation of Database interface for Unified Binary Trie. // 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. // In its plain form it uses a single binary trie database. During the
// MPT-to-binary transition, an optional MPT trie database (mpttriedb) and a
// frozen base root provide read-only access to pre-transition state. The
// wrapInTransitionTrie flag controls whether reads at this database are
// served via a TransitionTrie that overlays the binary trie on the MPT
// base; it is normally driven by chainConfig.UBTTransitionActive.
type UBTDatabase struct { type UBTDatabase struct {
triedb *triedb.Database triedb *triedb.Database
codedb *CodeDB mpttriedb *triedb.Database
codedb *CodeDB
baseRoot common.Hash
wrapInTransitionTrie bool
} }
// Type returns Binary, indicating this database is backed by a Universal Binary Trie. // Type returns Binary, indicating this database is backed by a Universal Binary Trie.
func (db *UBTDatabase) Type() DatabaseType { return TypeUBT } func (db *UBTDatabase) Type() DatabaseType { return TypeUBT }
// NewUBTDatabase creates a state database with the Unified binary trie manner. // NewUBTDatabase creates a state database with the Unified binary trie manner.
// State access is wrapped in a TransitionTrie by default (which degenerates
// to a passthrough when there is no MPT base) so callers that don't care
// about the override get sensible defaults.
func NewUBTDatabase(triedb *triedb.Database, codedb *CodeDB) *UBTDatabase { func NewUBTDatabase(triedb *triedb.Database, codedb *CodeDB) *UBTDatabase {
if codedb == nil { if codedb == nil {
codedb = NewCodeDB(triedb.Disk()) codedb = NewCodeDB(triedb.Disk())
} }
return &UBTDatabase{ return &UBTDatabase{
triedb: triedb, triedb: triedb,
codedb: codedb, 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. // StateReader returns a state reader associated with the specified state root.
func (db *UBTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) { func (db *UBTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) {
var readers []StateReader var readers []StateReader
@ -60,10 +104,8 @@ func (db *UBTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) {
} }
} }
// Configure the trie reader, which is expected to be available as the // Configure the trie reader, which is expected to be available as the
// gatekeeper unless the state is corrupted. The transition tree wrap is // gatekeeper unless the state is corrupted.
// kept on by default so the registry's BaseRoot (when populated) is tr, err := newUBTTrieReader(stateRoot, db.triedb, db.mpttriedb, db.wrapInTransitionTrie)
// honoured; the MPT triedb is plumbed through in a later commit.
tr, err := newUBTTrieReader(stateRoot, db.triedb, nil, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -96,15 +138,36 @@ func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Rea
return ra, rb, nil return ra, rb, nil
} }
// OpenTrie opens the main account trie at a specific root hash. // OpenTrie opens the main account trie at a specific root hash. During an
// active transition, the binary trie is wrapped in a TransitionTrie so writes
// land on the binary trie while reads fall through to the frozen MPT base.
func (db *UBTDatabase) OpenTrie(root common.Hash) (Trie, error) { 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, // 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. // the unified trie carries all state, so the main trie is reused. During the
// transition, an MPT storage trie is opened for accounts that have not yet
// been migrated.
func (db *UBTDatabase) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) { 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. // TrieDB retrieves any intermediate trie-node caching layer.
@ -130,10 +193,17 @@ func (db *UBTDatabase) Commit(update *StateUpdate) error {
return err 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 // Encode the state mutations in the UBT format
accounts, accountOrigin, storages, storageOrigin := update.EncodeUBTState() 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, Accounts: accounts,
AccountsOrigin: accountOrigin, AccountsOrigin: accountOrigin,
Storages: storages, Storages: storages,