core/overlay: load transition state from system contract

Replace the gob-encoded `rawdb.{Read,Write}VerkleTransitionState`
plumbing with a direct read from the binary transition registry system
contract at `params.BinaryTransitionRegistryAddress`. The registry
exposes the transition state via fixed storage slots; a tiny
`StorageReader` interface (`Storage(addr, slot) (Hash, error)`)
captures what the loader needs.

`LoadTransitionState` now takes a `StorageReader` instead of an
`ethdb.KeyValueReader` and returns `nil` when the registry has not been
initialised (slot 0 unset). `IsTransitionActive` is exposed for callers
that only need the started flag.

`core/state/reader.go:newUBTTrieReader` is updated:
  - It now takes the binary triedb, an optional MPT triedb, and a
    `wrapInTransitionTrie` flag so callers can opt out of the wrap.
  - It uses a small `binTrieStorageReader` adapter to query the
    registry directly from the binary trie at the requested root,
    avoiding the MPT key-hashing in `flatReader`.
  - When wrap=true and the registry's BaseRoot is non-zero, the MPT
    base is opened against the supplied MPT triedb. With the current
    callers (mptdb=nil) the wrap degenerates to a passthrough,
    preserving existing master semantics until the dual-triedb routing
    lands in the next commit.

The dead `rawdb` accessors and `VerkleTransitionStatePrefix` schema
constant are removed.
This commit is contained in:
Guillaume Ballet 2026-04-29 16:21:59 +02:00
parent 4f6219d5e7
commit 40c29ad53a
No known key found for this signature in database
6 changed files with 120 additions and 126 deletions

View file

@ -17,29 +17,42 @@
package overlay package overlay
import ( import (
"bytes"
"encoding/gob"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
) )
// TransitionState is a structure that holds the progress markers of the // Storage slots used by the binary transition registry system contract at
// translation process. // 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.
// It is satisfied by *state.flatReader, allowing the transition state to be
// loaded without a full state.StateDB.
type StorageReader interface {
Storage(addr common.Address, slot common.Hash) (common.Hash, error)
}
// TransitionState holds the progress markers of the MPT-to-binary
// translation process. It is reconstructed on demand from the storage of the
// binary transition registry system contract.
type TransitionState struct { 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 CurrentSlotHash common.Hash // hash of the last translated storage slot
CurrentPreimageOffset int64 // next byte to read from the preimage file
Started, Ended bool Started, Ended bool
// Mark whether the storage for an account has been processed. This is useful if the // StorageProcessed marks whether the storage of the current account has
// maximum number of leaves of the conversion is reached before the whole storage is // been fully processed. Useful when the maximum number of leaves of the
// processed. // conversion is reached before the storage is exhausted.
StorageProcessed bool 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. // InTransition returns true if the translation process is in progress.
@ -55,12 +68,11 @@ func (ts *TransitionState) Transitioned() bool {
// Copy returns a deep copy of the TransitionState object. // Copy returns a deep copy of the TransitionState object.
func (ts *TransitionState) Copy() *TransitionState { func (ts *TransitionState) Copy() *TransitionState {
ret := &TransitionState{ ret := &TransitionState{
Started: ts.Started, Started: ts.Started,
Ended: ts.Ended, Ended: ts.Ended,
CurrentSlotHash: ts.CurrentSlotHash, CurrentSlotHash: ts.CurrentSlotHash,
CurrentPreimageOffset: ts.CurrentPreimageOffset, StorageProcessed: ts.StorageProcessed,
StorageProcessed: ts.StorageProcessed, BaseRoot: ts.BaseRoot,
BaseRoot: ts.BaseRoot,
} }
if ts.CurrentAccountAddress != nil { if ts.CurrentAccountAddress != nil {
addr := *ts.CurrentAccountAddress addr := *ts.CurrentAccountAddress
@ -69,38 +81,48 @@ func (ts *TransitionState) Copy() *TransitionState {
return ret return ret
} }
// LoadTransitionState retrieves the Verkle transition state associated with // IsTransitionActive checks whether the binary transition registry has been
// the given state root hash from the database. // initialised by reading slot 0 (started) from the system contract.
func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash, isUBT bool) *TransitionState { func IsTransitionActive(reader StorageReader) bool {
var ts *TransitionState val, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionStartedKey)
if err != nil {
data, _ := rawdb.ReadVerkleTransitionState(db, root) return false
}
// if a state could be read from the db, attempt to decode it return val != (common.Hash{})
if len(data) > 0 { }
var (
newts TransitionState // LoadTransitionState reads the full transition state from the binary
buf = bytes.NewBuffer(data[:]) // transition registry system contract storage. Returns nil when the
dec = gob.NewDecoder(buf) // registry has not been initialised (i.e. the chain has not yet reached the
) // UBT fork block).
// Decode transition state //
err := dec.Decode(&newts) // The root parameter is unused; it is retained on the signature so callers
if err != nil { // can express the state version they intend to read.
log.Error("failed to decode transition state", "err", err) func LoadTransitionState(reader StorageReader, root common.Hash) *TransitionState {
return nil started, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionStartedKey)
} if err != nil || started == (common.Hash{}) {
ts = &newts return 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, _ := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressSlotKey)
storageProcessed, _ := reader.Storage(params.BinaryTransitionRegistryAddress, conversionProgressStorageProcessed)
return &TransitionState{
Started: true,
Ended: ended != (common.Hash{}),
BaseRoot: baseRoot,
CurrentAccountAddress: currentAddr,
CurrentSlotHash: slotHash,
StorageProcessed: storageProcessed != (common.Hash{}),
} }
// 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
} }

View file

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

View file

@ -691,7 +691,7 @@ var knownMetadataKeys = [][]byte{
snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey, snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey,
uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey, uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey,
persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, snapSyncStatusFlagKey, persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, snapSyncStatusFlagKey,
filterMapsRangeKey, headStateHistoryIndexKey, headTrienodeHistoryIndexKey, VerkleTransitionStatePrefix, filterMapsRangeKey, headStateHistoryIndexKey, headTrienodeHistoryIndexKey,
} }
// printChainMetadata prints out chain metadata to stderr. // printChainMetadata prints out chain metadata to stderr.

View file

@ -165,9 +165,6 @@ var (
preimageCounter = metrics.NewRegisteredCounter("db/preimage/total", nil) preimageCounter = metrics.NewRegisteredCounter("db/preimage/total", nil)
preimageHitsCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil) preimageHitsCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil)
preimageMissCounter = metrics.NewRegisteredCounter("db/preimage/miss", 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 // LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary
@ -461,7 +458,3 @@ func trienodeHistoryIndexBlockKey(addressHash common.Hash, path []byte, blockID
return out return out
} }
// transitionStateKey = transitionStatusKey + hash
func transitionStateKey(hash common.Hash) []byte {
return append(VerkleTransitionStatePrefix, hash.Bytes()...)
}

View file

@ -60,8 +60,10 @@ 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. // gatekeeper unless the state is corrupted. The transition tree wrap is
tr, err := newUBTTrieReader(stateRoot, db.triedb) // kept on by default so the registry's BaseRoot (when populated) is
// 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
} }

View file

@ -252,46 +252,53 @@ type ubtTrieReader struct {
lock sync.Mutex // Lock for protecting concurrent read 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. // 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. // 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) // When wrapInTransitionTrie is true, reads fall through to the frozen MPT
if binErr != nil { // base whenever the transition registry has captured a non-zero base root
return nil, binErr // 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 var base *trie.StateTrie
// tree needs to be created, or if a single, target tree is if wrapInTransitionTrie && mptdb != nil {
// to be picked. if ts := overlay.LoadTransitionState(&binTrieStorageReader{tr: binTrie}, root); ts != nil && ts.BaseRoot != (common.Hash{}) {
var ( base, err = trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), mptdb)
tr Trie if err != nil {
ts = overlay.LoadTransitionState(db.Disk(), root, true) return nil, err
) }
if ts.InTransition() {
mpt, err := trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), db)
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)
} }
return &ubtTrieReader{ return &ubtTrieReader{
root: root, root: root,
db: db, db: bindb,
tr: tr, tr: transitiontrie.NewTransitionTrie(base, binTrie, false),
}, nil }, nil
} }