From 40c29ad53a196c2ce24a5747fda02ce4c15d9ee3 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet Date: Wed, 29 Apr 2026 16:21:59 +0200 Subject: [PATCH] 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. --- core/overlay/state_transition.go | 128 ++++++++++++++++++------------- core/rawdb/accessors_overlay.go | 30 -------- core/rawdb/database.go | 2 +- core/rawdb/schema.go | 7 -- core/state/database_ubt.go | 6 +- core/state/reader.go | 73 ++++++++++-------- 6 files changed, 120 insertions(+), 126 deletions(-) delete mode 100644 core/rawdb/accessors_overlay.go diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go index afd2bab017..356c77b794 100644 --- a/core/overlay/state_transition.go +++ b/core/overlay/state_transition.go @@ -17,29 +17,42 @@ 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" ) -// TransitionState is a structure that holds the progress markers of the -// translation process. +// 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. +// 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 { - 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 +68,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 +81,48 @@ 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 + } + 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). +// +// The root parameter is unused; it is retained on the signature so callers +// can express the state version they intend to read. +func LoadTransitionState(reader StorageReader, root common.Hash) *TransitionState { + started, err := reader.Storage(params.BinaryTransitionRegistryAddress, transitionStartedKey) + if err != nil || started == (common.Hash{}) { + 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 } 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..d3f6944316 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 @@ -461,7 +458,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..66653f6112 100644 --- a/core/state/database_ubt.go +++ b/core/state/database_ubt.go @@ -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 - // gatekeeper unless the state is corrupted. - tr, err := newUBTTrieReader(stateRoot, db.triedb) + // gatekeeper unless the state is corrupted. The transition tree wrap is + // 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 { return nil, err } diff --git a/core/state/reader.go b/core/state/reader.go index 5df0acbb9b..7b8ff35ac1 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -252,46 +252,53 @@ 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) - if err != nil { - return nil, err + var base *trie.StateTrie + if wrapInTransitionTrie && mptdb != nil { + if ts := overlay.LoadTransitionState(&binTrieStorageReader{tr: binTrie}, root); ts != nil && ts.BaseRoot != (common.Hash{}) { + base, err = trie.NewStateTrie(trie.StateTrieID(ts.BaseRoot), mptdb) + 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{ root: root, - db: db, - tr: tr, + db: bindb, + tr: transitiontrie.NewTransitionTrie(base, binTrie, false), }, nil }