Skip to content
3 changes: 2 additions & 1 deletion cmd/geth/chaincmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`,
utils.MetricsInfluxDBBucketFlag,
utils.MetricsInfluxDBOrganizationFlag,
utils.StateSizeTrackingFlag,
utils.StateBlocksPerPeriodFlag,
utils.TxLookupLimitFlag,
utils.VMTraceFlag,
utils.VMTraceJsonConfigFlag,
Expand Down Expand Up @@ -721,7 +722,7 @@ func downloadEra(ctx *cli.Context) error {
flags.CheckExclusive(ctx, eraBlockFlag, eraEpochFlag, eraAllFlag)

// Resolve the network.
var network = "mainnet"
network := "mainnet"
if utils.IsNetworkPreset(ctx) {
switch {
case ctx.IsSet(utils.MainnetFlag.Name):
Expand Down
118 changes: 118 additions & 0 deletions cmd/geth/dbcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/signal"
Expand Down Expand Up @@ -82,6 +83,7 @@ Remove blockchain and state databases`,
dbMetadataCmd,
dbCheckStateContentCmd,
dbInspectHistoryCmd,
dbTrieVersionCmd,
},
}
dbInspectCmd = &cli.Command{
Expand Down Expand Up @@ -206,6 +208,13 @@ WARNING: This is a low-level operation which may cause database corruption!`,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "This command queries the history of the account or storage slot within the specified block range",
}
dbTrieVersionCmd = &cli.Command{
Action: dbTrieVersion,
Name: "trie-version",
Usage: "Check storage format version of path-based trie nodes",
Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
Description: "This command iterates through all path-based trie nodes and reports their storage format version (old RLP format vs new versioned format with period).",
}
)

func removeDB(ctx *cli.Context) error {
Expand Down Expand Up @@ -906,3 +915,112 @@ func inspectHistory(ctx *cli.Context) error {
}
return inspectStorage(triedb, start, end, address, slot, ctx.Bool("raw"))
}

// dbTrieVersion iterates through all path-based trie nodes and reports storage
// format statistics including period distribution.
func dbTrieVersion(ctx *cli.Context) error {
stack, _ := makeConfigNode(ctx)
defer stack.Close()

db := utils.MakeChainDatabase(ctx, stack, true)
defer db.Close()

const periodSize = 8

var (
periodCounts = make(map[uint64]int64) // Count per period
totalNodes int64
startTime = time.Now()
lastLog = time.Now()
)

// decodePeriod extracts the period from a trie node value.
// Nodes without period suffix are treated as period 0.
decodePeriod := func(data []byte) uint64 {
if len(data) == 0 {
return 0
}
// Use rlp.Split to find where the RLP value ends
_, _, rest, err := rlp.Split(data)
if err != nil {
return 0
}
// If there are exactly 8 trailing bytes, that's the period
if len(rest) == periodSize {
return binary.BigEndian.Uint64(rest)
}
return 0
}

// Iterate account trie nodes
log.Info("Checking account trie nodes...")
accountIter := db.NewIterator(rawdb.TrieNodeAccountPrefix, nil)
for accountIter.Next() {
key := accountIter.Key()
if !rawdb.IsAccountTrieNode(key) {
continue
}
period := decodePeriod(accountIter.Value())
periodCounts[period]++
totalNodes++

if time.Since(lastLog) > 8*time.Second {
log.Info("Checking account trie nodes", "total", totalNodes, "elapsed", common.PrettyDuration(time.Since(startTime)))
lastLog = time.Now()
}
}
accountIter.Release()
if err := accountIter.Error(); err != nil {
return fmt.Errorf("account trie iteration failed: %w", err)
}

accountNodes := totalNodes
log.Info("Finished checking account trie nodes", "count", accountNodes)

// Iterate storage trie nodes
log.Info("Checking storage trie nodes...")
storageIter := db.NewIterator(rawdb.TrieNodeStoragePrefix, nil)
for storageIter.Next() {
key := storageIter.Key()
if !rawdb.IsStorageTrieNode(key) {
continue
}
period := decodePeriod(storageIter.Value())
periodCounts[period]++
totalNodes++

if time.Since(lastLog) > 8*time.Second {
log.Info("Checking storage trie nodes", "total", totalNodes-accountNodes, "elapsed", common.PrettyDuration(time.Since(startTime)))
lastLog = time.Now()
}
}
storageIter.Release()
if err := storageIter.Error(); err != nil {
return fmt.Errorf("storage trie iteration failed: %w", err)
}

storageNodes := totalNodes - accountNodes

// Print results
fmt.Println("\nTrie Node Storage Format (Path-based)")
fmt.Println("======================================")
fmt.Printf("Account trie nodes: %d\n", accountNodes)
fmt.Printf("Storage trie nodes: %d\n", storageNodes)
fmt.Println("--------------------------------------")
fmt.Printf("Total nodes: %d\n", totalNodes)
fmt.Printf("Time elapsed: %s\n", common.PrettyDuration(time.Since(startTime)))

// Print period distribution
fmt.Println("\nPeriod Distribution")
fmt.Println("-------------------")
periods := make([]uint64, 0, len(periodCounts))
for p := range periodCounts {
periods = append(periods, p)
}
slices.Sort(periods)
for _, p := range periods {
fmt.Printf("Period %d: %d nodes\n", p, periodCounts[p])
}

return nil
}
1 change: 1 addition & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ var (
utils.BeaconCheckpointFlag,
utils.BeaconCheckpointFileFlag,
utils.LogSlowBlockFlag,
utils.StateBlocksPerPeriodFlag,
}, utils.NetworkFlags, utils.DatabaseFlags)

rpcFlags = []cli.Flag{
Expand Down
12 changes: 12 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
"github.com/ethereum/go-ethereum/core/vm"
Expand Down Expand Up @@ -289,6 +290,12 @@ var (
Value: ethconfig.Defaults.EnableStateSizeTracking,
Category: flags.StateCategory,
}
StateBlocksPerPeriodFlag = &cli.Uint64Flag{
Name: "state.blocks-per-period",
Usage: "Number of blocks per period for archive expiry (default = 1,314,000, ~6 months)",
Value: 1_314_000,
Category: flags.StateCategory,
}
StateHistoryFlag = &cli.Uint64Flag{
Name: "history.state",
Usage: "Number of recent blocks to retain state history for, only relevant in state.scheme=path (default = 90,000 blocks, 0 = entire chain)",
Expand Down Expand Up @@ -1702,6 +1709,11 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
if ctx.IsSet(StateSchemeFlag.Name) {
cfg.StateScheme = ctx.String(StateSchemeFlag.Name)
}
if ctx.IsSet(StateBlocksPerPeriodFlag.Name) {
// TODO(weiihann): this is just a hacky way of setting the param without too many code changes for now.
// Ideally we want to propagate the param through the config.
state.NumBlocksPerPeriod = ctx.Uint64(StateBlocksPerPeriodFlag.Name)
}
// Parse transaction history flag, if user is still using legacy config
// file with 'TxLookupLimit' configured, copy the value to 'TransactionHistory'.
if cfg.TransactionHistory == ethconfig.Defaults.TransactionHistory && cfg.TxLookupLimit != ethconfig.Defaults.TxLookupLimit {
Expand Down
92 changes: 88 additions & 4 deletions core/rawdb/accessors_trie.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
package rawdb

import (
"encoding/binary"
"fmt"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
)

// HashScheme is the legacy hash-based state scheme with which trie nodes are
Expand All @@ -44,10 +46,65 @@ const HashScheme = "hash"
// on extra state diffs to survive deep reorg.
const PathScheme = "path"

// periodSize is the size of the period counter appended to trie nodes.
const periodSize = 8

// encodeTrieNode encodes a trie node blob with optional period metadata.
// Format: [RLP blob][8-byte period BE] if period > 0, otherwise just [RLP blob].
// The period can be detected on read by checking if data length exceeds RLP length.
func encodeTrieNode(blob []byte, period uint64) []byte {
if period == 0 {
return blob
}
result := make([]byte, len(blob)+periodSize)
copy(result, blob)
binary.BigEndian.PutUint64(result[len(blob):], period)
return result
}

// decodeTrieNode decodes a trie node from storage, returning the blob and period.
// It detects period by comparing stored length with RLP-decoded length.
// For nodes without period suffix, period defaults to 0 (backward compatible).
func decodeTrieNode(data []byte) ([]byte, uint64) {
if len(data) == 0 {
return nil, 0
}
// Use rlp.Split to find where the RLP value ends
_, _, rest, err := rlp.Split(data)
if err != nil {
return data, 0 // malformed, return as-is
}
blobLen := len(data) - len(rest)
blob := data[:blobLen]

// If there are exactly 8 trailing bytes, that's the period
if len(rest) == periodSize {
return blob, binary.BigEndian.Uint64(rest)
}

return blob, 0
}

// DecodeTrieNodeToBlob extracts the raw node blob from stored data,
// stripping any period metadata. Useful for computing node hashes.
func DecodeTrieNodeToBlob(data []byte) []byte {
blob, _ := decodeTrieNode(data)
return blob
}

// ReadAccountTrieNode retrieves the account trie node with the specified node path.
// It strips any period metadata and returns only the raw node blob.
func ReadAccountTrieNode(db ethdb.KeyValueReader, path []byte) []byte {
data, _ := db.Get(accountTrieNodeKey(path))
return data
blob, _ := decodeTrieNode(data)
return blob
}

// ReadAccountTrieNodeWithPeriod retrieves the account trie node along with its
// period counter. For old format nodes without period metadata, period is 0.
func ReadAccountTrieNodeWithPeriod(db ethdb.KeyValueReader, path []byte) ([]byte, uint64) {
data, _ := db.Get(accountTrieNodeKey(path))
return decodeTrieNode(data)
}

// HasAccountTrieNode checks the presence of the account trie node with the
Expand All @@ -61,8 +118,17 @@ func HasAccountTrieNode(db ethdb.KeyValueReader, path []byte) bool {
}

// WriteAccountTrieNode writes the provided account trie node into database.
// This writes with period=0 for backward compatibility with callers that
// don't need period tracking.
func WriteAccountTrieNode(db ethdb.KeyValueWriter, path []byte, node []byte) {
if err := db.Put(accountTrieNodeKey(path), node); err != nil {
WriteAccountTrieNodeWithPeriod(db, path, node, 0)
}

// WriteAccountTrieNodeWithPeriod writes the provided account trie node into
// database along with its period counter.
func WriteAccountTrieNodeWithPeriod(db ethdb.KeyValueWriter, path []byte, node []byte, period uint64) {
encoded := encodeTrieNode(node, period)
if err := db.Put(accountTrieNodeKey(path), encoded); err != nil {
log.Crit("Failed to store account trie node", "err", err)
}
}
Expand All @@ -75,9 +141,18 @@ func DeleteAccountTrieNode(db ethdb.KeyValueWriter, path []byte) {
}

// ReadStorageTrieNode retrieves the storage trie node with the specified node path.
// It strips any period metadata and returns only the raw node blob.
func ReadStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) []byte {
data, _ := db.Get(storageTrieNodeKey(accountHash, path))
return data
blob, _ := decodeTrieNode(data)
return blob
}

// ReadStorageTrieNodeWithPeriod retrieves the storage trie node along with its
// period counter. For old format nodes without period metadata, period is 0.
func ReadStorageTrieNodeWithPeriod(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) ([]byte, uint64) {
data, _ := db.Get(storageTrieNodeKey(accountHash, path))
return decodeTrieNode(data)
}

// HasStorageTrieNode checks the presence of the storage trie node with the
Expand All @@ -91,8 +166,17 @@ func HasStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path [
}

// WriteStorageTrieNode writes the provided storage trie node into database.
// This writes with period=0 for backward compatibility with callers that
// don't need period tracking.
func WriteStorageTrieNode(db ethdb.KeyValueWriter, accountHash common.Hash, path []byte, node []byte) {
if err := db.Put(storageTrieNodeKey(accountHash, path), node); err != nil {
WriteStorageTrieNodeWithPeriod(db, accountHash, path, node, 0)
}

// WriteStorageTrieNodeWithPeriod writes the provided storage trie node into
// database along with its period counter.
func WriteStorageTrieNodeWithPeriod(db ethdb.KeyValueWriter, accountHash common.Hash, path []byte, node []byte, period uint64) {
encoded := encodeTrieNode(node, period)
if err := db.Put(storageTrieNodeKey(accountHash, path), encoded); err != nil {
log.Crit("Failed to store storage trie node", "err", err)
}
}
Expand Down
5 changes: 3 additions & 2 deletions core/state/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ type Trie interface {
// collectLeaf is true) will be encapsulated into a nodeset for return.
// The returned nodeset can be nil if the trie is clean(nothing to commit).
// Once the trie is committed, it's not usable anymore. A new trie must
// be created with new root and updated trie database for following usage
Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet)
// be created with new root and updated trie database for following usage.
// The period parameter specifies the period counter stored alongside nodes.
Commit(collectLeaf bool, period uint64) (common.Hash, *trienode.NodeSet)

// Witness returns a set containing all trie nodes that have been accessed.
// The returned map could be nil if the witness is empty.
Expand Down
2 changes: 1 addition & 1 deletion core/state/snapshot/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefi
for i, key := range result.keys {
tr.Update(key, result.vals[i])
}
_, nodes := tr.Commit(false)
_, nodes := tr.Commit(false, 0)
hashSet := nodes.HashSet()
resolver = func(owner common.Hash, path []byte, hash common.Hash) []byte {
return hashSet[hash]
Expand Down
4 changes: 2 additions & 2 deletions core/state/snapshot/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,15 @@ func (t *testHelper) makeStorageTrie(accKey string, keys []string, vals []string
if !commit {
return stTrie.Hash()
}
root, nodes := stTrie.Commit(false)
root, nodes := stTrie.Commit(false, 0)
if nodes != nil {
t.nodes.Merge(nodes)
}
return root
}

func (t *testHelper) Commit() common.Hash {
root, nodes := t.accTrie.Commit(true)
root, nodes := t.accTrie.Commit(true, 0)
if nodes != nil {
t.nodes.Merge(nodes)
}
Expand Down
4 changes: 2 additions & 2 deletions core/state/state_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
//
// Note, commit may run concurrently across all the state objects. Do not assume
// thread-safe access to the statedb.
func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
func (s *stateObject) commit(period uint64) (*accountUpdate, *trienode.NodeSet, error) {
// commit the account metadata changes
op := &accountUpdate{
address: s.address,
Expand Down Expand Up @@ -454,7 +454,7 @@ func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
s.origin = s.data.Copy()
return op, nil, nil
}
root, nodes := s.trie.Commit(false)
root, nodes := s.trie.Commit(false, period)
s.data.Root = root
s.origin = s.data.Copy()
return op, nodes, nil
Expand Down
Loading