diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go
index cc9f44e460a..cdddb014285 100644
--- a/consensus/beacon/consensus.go
+++ b/consensus/beacon/consensus.go
@@ -354,9 +354,9 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.
// FinalizeAndAssemble implements consensus.Engine, setting the final state and
// assembling the block.
-func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) {
+func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, statedb *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) {
if !beacon.IsPoSHeader(header) {
- return beacon.ethone.FinalizeAndAssemble(chain, header, state, body, receipts)
+ return beacon.ethone.FinalizeAndAssemble(chain, header, statedb, body, receipts)
}
shanghai := chain.Config().IsShanghai(header.Number, header.Time)
if shanghai {
@@ -370,10 +370,10 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
}
}
// Finalize and assemble the block.
- beacon.Finalize(chain, header, state, body)
+ beacon.Finalize(chain, header, statedb, body)
// Assign the final state root to header.
- header.Root = state.IntermediateRoot(true)
+ header.Root = statedb.IntermediateRoot(true)
// Assemble the final block.
block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))
@@ -381,19 +381,19 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
// Create the block witness and attach to block.
// This step needs to happen as late as possible to catch all access events.
if chain.Config().IsVerkle(header.Number, header.Time) {
- keys := state.AccessEvents().Keys()
+ keys := statedb.AccessEvents().Keys()
// Open the pre-tree to prove the pre-state against
parent := chain.GetHeaderByNumber(header.Number.Uint64() - 1)
if parent == nil {
return nil, fmt.Errorf("nil parent header for block %d", header.Number)
}
- preTrie, err := state.Database().OpenTrie(parent.Root)
+ preTrie, err := statedb.Database().OpenTrie(parent.Root)
if err != nil {
return nil, fmt.Errorf("error opening pre-state tree root: %w", err)
}
vktPreTrie, okpre := preTrie.(*trie.VerkleTrie)
- vktPostTrie, okpost := state.GetTrie().(*trie.VerkleTrie)
+ vktPostTrie, okpost := statedb.GetTrie().(*trie.VerkleTrie)
// The witness is only attached iff both parent and current block are
// using verkle tree.
diff --git a/core/blockchain.go b/core/blockchain.go
index d56996dadbe..c08b05e7f47 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -283,12 +283,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis
if cacheConfig == nil {
cacheConfig = defaultCacheConfig
}
- // Open trie database with provided config
- enableVerkle, err := EnableVerkleAtGenesis(db, genesis)
- if err != nil {
- return nil, err
- }
- triedb := triedb.NewDatabase(db, cacheConfig.triedbConfig(enableVerkle))
+ triedb := triedb.NewDatabase(db, cacheConfig.triedbConfig(genesis.IsVerkle()))
// Write the supplied genesis to the database if it has not been initialized
// yet. The corresponding chain config will be returned, either from the
diff --git a/core/chain_makers.go b/core/chain_makers.go
index 7a258dc05f8..e7106f82e9a 100644
--- a/core/chain_makers.go
+++ b/core/chain_makers.go
@@ -536,8 +536,10 @@ func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine
return block, b.receipts
}
+ sdb := state.NewDatabase(trdb, nil)
+
for i := 0; i < n; i++ {
- statedb, err := state.New(parent.Root(), state.NewDatabase(trdb, nil))
+ statedb, err := state.New(parent.Root(), sdb)
if err != nil {
panic(err)
}
diff --git a/core/genesis.go b/core/genesis.go
index 95782a827a4..cdd810dd082 100644
--- a/core/genesis.go
+++ b/core/genesis.go
@@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
+ "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/tracing"
@@ -443,7 +444,7 @@ func (g *Genesis) chainConfigOrDefault(ghash common.Hash, stored *params.ChainCo
// IsVerkle indicates whether the state is already stored in a verkle
// tree at genesis time.
func (g *Genesis) IsVerkle() bool {
- return g.Config.IsVerkleGenesis()
+ return g != nil && g.Config != nil && g.Config.VerkleTime != nil && *g.Config.VerkleTime == g.Timestamp
}
// ToBlock returns the genesis block according to genesis specification.
@@ -556,6 +557,12 @@ func (g *Genesis) Commit(db ethdb.Database, triedb *triedb.Database) (*types.Blo
rawdb.WriteHeadFastBlockHash(batch, block.Hash())
rawdb.WriteHeadHeaderHash(batch, block.Hash())
rawdb.WriteChainConfig(batch, block.Hash(), config)
+
+ // In case of verkle-at-genesis, we need to ensure that the conversion
+ // markers are indicating that the conversion has completed.
+ if g.IsVerkle() {
+ overlay.StoreTransitionState(batch, block.Root(), &overlay.TransitionState{Ended: true})
+ }
return block, batch.Write()
}
@@ -569,29 +576,6 @@ func (g *Genesis) MustCommit(db ethdb.Database, triedb *triedb.Database) *types.
return block
}
-// EnableVerkleAtGenesis indicates whether the verkle fork should be activated
-// at genesis. This is a temporary solution only for verkle devnet testing, where
-// verkle fork 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.
-func EnableVerkleAtGenesis(db ethdb.Database, genesis *Genesis) (bool, error) {
- if genesis != nil {
- if genesis.Config == nil {
- return false, errGenesisNoConfig
- }
- return genesis.Config.EnableVerkleAtGenesis, nil
- }
- if ghash := rawdb.ReadCanonicalHash(db, 0); ghash != (common.Hash{}) {
- chainCfg := rawdb.ReadChainConfig(db, ghash)
- if chainCfg != nil {
- return chainCfg.EnableVerkleAtGenesis, nil
- }
- }
- return false, nil
-}
-
// DefaultGenesisBlock returns the Ethereum main net genesis block.
func DefaultGenesisBlock() *Genesis {
return &Genesis{
diff --git a/core/genesis_test.go b/core/genesis_test.go
index 726bda86bb0..1204279bbae 100644
--- a/core/genesis_test.go
+++ b/core/genesis_test.go
@@ -286,7 +286,6 @@ func TestVerkleGenesisCommit(t *testing.T) {
OsakaTime: &verkleTime,
VerkleTime: &verkleTime,
TerminalTotalDifficulty: big.NewInt(0),
- EnableVerkleAtGenesis: true,
Ethash: nil,
Clique: nil,
BlobScheduleConfig: ¶ms.BlobScheduleConfig{
diff --git a/core/overlay/state_transition.go b/core/overlay/state_transition.go
new file mode 100644
index 00000000000..ad33324fa1b
--- /dev/null
+++ b/core/overlay/state_transition.go
@@ -0,0 +1,123 @@
+// 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 overlay
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// TransitionState represents the progress of the Verkle transition process,
+// tracking which parts of the legacy MPT-based state have already been
+// migrated to the Verkle-based state.
+type TransitionState struct {
+ // CurrentAccountHash is the hash of the next account to be migrated
+ // from the MPT state to the Verkle state.
+ CurrentAccountHash common.Hash
+
+ // CurrentSlotHash is the hash of the next storage slot (within the
+ // current account) to be migrated from the MPT state to the Verkle state.
+ // This field is irrelevant if StorageProcessed is true, indicating that
+ // all storage slots for the current account have already been migrated.
+ CurrentSlotHash common.Hash
+
+ // StorageProcessed indicates whether all storage slots for the current
+ // account have been fully migrated. This is useful in cases where the
+ // transition was interrupted before all state entries were processed
+ // (e.g., due to a configured migration step limit).
+ StorageProcessed bool
+
+ // CurrentPreimageOffset is the byte offset in the preimage file from
+ // which the migration should resume.
+ CurrentPreimageOffset uint64
+
+ // Started is true if the transition process has begun. Note that the
+ // transition is considered started only after the MPT state referenced
+ // by BaseRoot has been finalized.
+ Started bool
+
+ // Ended is true if the transition process has completed, meaning the entire
+ // MPT-based state has been fully migrated. When true, the complete Ethereum
+ // state is available in the Verkle state, and constructing a mixed state
+ // view is no longer necessary.
+ Ended bool
+
+ // BaseRoot is the MPT root hash of the read-only base state, the original
+ // state prior to Verkle activation.
+ BaseRoot common.Hash
+}
+
+// InTransition returns true if the translation process is in progress.
+func (ts *TransitionState) InTransition() bool {
+ return ts != nil && ts.Started && !ts.Ended
+}
+
+// Transitioned returns true if the translation process has been completed.
+func (ts *TransitionState) Transitioned() bool {
+ return ts != nil && ts.Ended
+}
+
+// LogAttrs returns a list of attributes for logging purpose.
+func (ts *TransitionState) LogAttrs(root common.Hash) []any {
+ var attrs []any
+ if !ts.Started {
+ attrs = append(attrs, "started", false)
+ return attrs
+ }
+ if ts.Ended {
+ attrs = append(attrs, "ended", true)
+ return attrs
+ }
+ attrs = append(attrs, []interface{}{"root", root, "base", ts.BaseRoot}...)
+
+ if ts.StorageProcessed {
+ attrs = append(attrs, []interface{}{"at", ts.CurrentAccountHash}...)
+ } else {
+ attrs = append(attrs, []interface{}{
+ "in", ts.CurrentAccountHash,
+ "at", ts.CurrentSlotHash,
+ }...)
+ }
+ return attrs
+}
+
+// LoadTransitionState retrieves the Verkle transition state associated with
+// the given state root hash from the database.
+func LoadTransitionState(db ethdb.KeyValueReader, root common.Hash) (*TransitionState, error) {
+ data := rawdb.ReadVerkleTransitionState(db, root)
+ if len(data) == 0 {
+ return nil, nil
+ }
+ var ts TransitionState
+ if err := rlp.DecodeBytes(data, &ts); err != nil {
+ return nil, err
+ }
+ return &ts, nil
+}
+
+// StoreTransitionState serializes and writes the provided Verkle transition state
+// to the database with the given state root hash.
+func StoreTransitionState(db ethdb.KeyValueWriter, root common.Hash, ts *TransitionState) error {
+ data, err := rlp.EncodeToBytes(ts)
+ if err != nil {
+ return err
+ }
+ rawdb.WriteVerkleTransitionState(db, root, data)
+ return nil
+}
diff --git a/core/rawdb/accessors_metadata.go b/core/rawdb/accessors_metadata.go
index 859566f722f..6996031be2c 100644
--- a/core/rawdb/accessors_metadata.go
+++ b/core/rawdb/accessors_metadata.go
@@ -174,16 +174,3 @@ func UpdateUncleanShutdownMarker(db ethdb.KeyValueStore) {
log.Warn("Failed to write unclean-shutdown marker", "err", err)
}
}
-
-// ReadTransitionStatus retrieves the eth2 transition status from the database
-func ReadTransitionStatus(db ethdb.KeyValueReader) []byte {
- data, _ := db.Get(transitionStatusKey)
- return data
-}
-
-// WriteTransitionStatus stores the eth2 transition status to the database
-func WriteTransitionStatus(db ethdb.KeyValueWriter, data []byte) {
- if err := db.Put(transitionStatusKey, data); err != nil {
- log.Crit("Failed to store the eth2 transition status", "err", err)
- }
-}
diff --git a/core/rawdb/accessors_overlay.go b/core/rawdb/accessors_overlay.go
new file mode 100644
index 00000000000..18853bc781a
--- /dev/null
+++ b/core/rawdb/accessors_overlay.go
@@ -0,0 +1,36 @@
+// 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"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// ReadVerkleTransitionState reads the verkle transition data from the database.
+func ReadVerkleTransitionState(db ethdb.KeyValueReader, hash common.Hash) []byte {
+ data, _ := db.Get(transitionStateKey(hash))
+ return data
+}
+
+// WriteVerkleTransitionState writes the verkle transition data into the database.
+func WriteVerkleTransitionState(db ethdb.KeyValueWriter, hash common.Hash, state []byte) {
+ if err := db.Put(transitionStateKey(hash), state); err != nil {
+ log.Crit("Failed to write the verkle transition state", "hash", hash, "err", err)
+ }
+}
diff --git a/core/rawdb/database.go b/core/rawdb/database.go
index 2a50e3f9eef..7e0ee228461 100644
--- a/core/rawdb/database.go
+++ b/core/rawdb/database.go
@@ -566,7 +566,7 @@ var knownMetadataKeys = [][]byte{
snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey,
uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey,
persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, snapSyncStatusFlagKey,
- filterMapsRangeKey,
+ filterMapsRangeKey, verkleTransitionStatePrefix,
}
// printChainMetadata prints out chain metadata to stderr.
diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go
index fa125cecc05..4cc79b266dd 100644
--- a/core/rawdb/schema.go
+++ b/core/rawdb/schema.go
@@ -91,7 +91,7 @@ var (
uncleanShutdownKey = []byte("unclean-shutdown") // config prefix for the db
// transitionStatusKey tracks the eth2 transition status.
- transitionStatusKey = []byte("eth2-transition")
+ transitionStatusKey = []byte("eth2-transition") // deprecated!
// snapSyncStatusFlagKey flags that status of snap sync.
snapSyncStatusFlagKey = []byte("SnapSyncStatus")
@@ -147,6 +147,9 @@ 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
@@ -362,3 +365,8 @@ func filterMapBlockLVKey(number uint64) []byte {
binary.BigEndian.PutUint64(key[l:], number)
return key
}
+
+// transitionStateKey = verkleTransitionStatePrefix + hash
+func transitionStateKey(hash common.Hash) []byte {
+ return append(verkleTransitionStatePrefix, hash.Bytes()...)
+}
diff --git a/core/state/database.go b/core/state/database.go
index faf4954650b..ea2b3f77d65 100644
--- a/core/state/database.go
+++ b/core/state/database.go
@@ -21,11 +21,13 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
+ "github.com/ethereum/go-ethereum/core/overlay"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/trie/utils"
@@ -151,6 +153,9 @@ type CachingDB struct {
codeCache *lru.SizeConstrainedCache[common.Hash, []byte]
codeSizeCache *lru.Cache[common.Hash, int]
pointCache *utils.PointCache
+
+ // Transition-specific fields
+ transitions *lru.Cache[common.Hash, *overlay.TransitionState]
}
// NewDatabase creates a state database with the provided data sources.
@@ -162,6 +167,7 @@ func NewDatabase(triedb *triedb.Database, snap *snapshot.Tree) *CachingDB {
codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize),
codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize),
pointCache: utils.NewPointCache(pointCacheSize),
+ transitions: lru.NewCache[common.Hash, *overlay.TransitionState](1000),
}
}
@@ -277,3 +283,38 @@ func mustCopyTrie(t Trie) Trie {
panic(fmt.Errorf("unknown trie type %T", t))
}
}
+
+// SaveTransitionState stores the given transition state associated with the
+// specified root. It writes the state to persistent storage and also updates
+// the in-memory cache.
+func (db *CachingDB) SaveTransitionState(root common.Hash, ts *overlay.TransitionState) error {
+ if err := overlay.StoreTransitionState(db.disk, root, ts); err != nil {
+ return err
+ }
+ db.transitions.Add(root, ts)
+ log.Debug("Saved transition state", ts.LogAttrs(root)...)
+ return nil
+}
+
+// LoadTransitionState loads the transition state associated with the specified
+// root. The transition state may be unavailable in the following cases:
+//
+// - Verkle has not been activated yet (the legacy MPT state should be used)
+// - The first block immediately since Verkle activation
+// - The block after the Verkle transition has completed
+func (db *CachingDB) LoadTransitionState(root common.Hash) (*overlay.TransitionState, error) {
+ ts, ok := db.transitions.Get(root)
+ if ok {
+ return ts, nil
+ }
+ ts, err := overlay.LoadTransitionState(db.disk, root)
+ if err != nil {
+ return nil, err
+ }
+ if ts == nil {
+ log.Debug("Transition state is not found", "root", root)
+ return nil, nil
+ }
+ log.Debug("Loaded transition state", ts.LogAttrs(root)...)
+ return ts, nil
+}
diff --git a/core/verkle_witness_test.go b/core/verkle_witness_test.go
index de2280ced1c..353c4310150 100644
--- a/core/verkle_witness_test.go
+++ b/core/verkle_witness_test.go
@@ -20,6 +20,7 @@ import (
"bytes"
"encoding/binary"
"encoding/hex"
+ "fmt"
"math/big"
"slices"
"testing"
@@ -58,7 +59,6 @@ var (
ShanghaiTime: u64(0),
VerkleTime: u64(0),
TerminalTotalDifficulty: common.Big0,
- EnableVerkleAtGenesis: true,
BlobScheduleConfig: ¶ms.BlobScheduleConfig{
Verkle: params.DefaultPragueBlobConfig,
},
@@ -82,7 +82,6 @@ var (
ShanghaiTime: u64(0),
VerkleTime: u64(0),
TerminalTotalDifficulty: common.Big0,
- EnableVerkleAtGenesis: true,
BlobScheduleConfig: ¶ms.BlobScheduleConfig{
Verkle: params.DefaultPragueBlobConfig,
},
@@ -202,6 +201,9 @@ func TestProcessVerkle(t *testing.T) {
t.Log("verified verkle proof, inserting blocks into the chain")
+ for i, b := range chain {
+ fmt.Printf("%d %x\n", i, b.Root())
+ }
endnum, err := blockchain.InsertChain(chain)
if err != nil {
t.Fatalf("block %d imported with error: %v", endnum, err)
diff --git a/params/config.go b/params/config.go
index 8f9e02583bf..b7e839ff8af 100644
--- a/params/config.go
+++ b/params/config.go
@@ -415,19 +415,6 @@ type ChainConfig struct {
DepositContractAddress common.Address `json:"depositContractAddress,omitempty"`
- // EnableVerkleAtGenesis is a flag that specifies whether the network uses
- // the Verkle tree starting from the genesis block. If set to true, the
- // genesis state will be committed using the Verkle tree, eliminating the
- // need for any Verkle transition later.
- //
- // This is a temporary flag only 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 flag irrelevant in
- // those cases.
- EnableVerkleAtGenesis bool `json:"enableVerkleAtGenesis,omitempty"`
-
// Various consensus engines
Ethash *EthashConfig `json:"ethash,omitempty"`
Clique *CliqueConfig `json:"clique,omitempty"`
@@ -651,20 +638,6 @@ func (c *ChainConfig) IsVerkle(num *big.Int, time uint64) bool {
return c.IsLondon(num) && isTimestampForked(c.VerkleTime, time)
}
-// IsVerkleGenesis 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.
-func (c *ChainConfig) IsVerkleGenesis() bool {
- return c.EnableVerkleAtGenesis
-}
-
// IsEIP4762 returns whether eip 4762 has been activated at given block.
func (c *ChainConfig) IsEIP4762(num *big.Int, time uint64) bool {
return c.IsVerkle(num, time)