Skip to content

Commit e965623

Browse files
shazam8253shantichanalrjl493456442gballet
authored
core, miner, trie: add metrics tracking state trie depth (#32388)
Co-authored-by: shantichanal <158101918+shantichanal@users.noreply.github.com> Co-authored-by: Gary Rong <garyrong0905@gmail.com> Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com>
1 parent ac17319 commit e965623

File tree

12 files changed

+166
-30
lines changed

12 files changed

+166
-30
lines changed

core/blockchain.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,7 +2011,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
20112011
// If we are past Byzantium, enable prefetching to pull in trie node paths
20122012
// while processing transactions. Before Byzantium the prefetcher is mostly
20132013
// useless due to the intermediate root hashing after each transaction.
2014-
var witness *stateless.Witness
2014+
var (
2015+
witness *stateless.Witness
2016+
witnessStats *stateless.WitnessStats
2017+
)
20152018
if bc.chainConfig.IsByzantium(block.Number()) {
20162019
// Generate witnesses either if we're self-testing, or if it's the
20172020
// only block being inserted. A bit crude, but witnesses are huge,
@@ -2021,8 +2024,11 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
20212024
if err != nil {
20222025
return nil, err
20232026
}
2027+
if bc.cfg.VmConfig.EnableWitnessStats {
2028+
witnessStats = stateless.NewWitnessStats()
2029+
}
20242030
}
2025-
statedb.StartPrefetcher("chain", witness)
2031+
statedb.StartPrefetcher("chain", witness, witnessStats)
20262032
defer statedb.StopPrefetcher()
20272033
}
20282034

@@ -2083,6 +2089,7 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
20832089
return nil, fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash())
20842090
}
20852091
}
2092+
20862093
xvtime := time.Since(xvstart)
20872094
proctime := time.Since(startTime) // processing + validation + cross validation
20882095

@@ -2118,6 +2125,11 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
21182125
if err != nil {
21192126
return nil, err
21202127
}
2128+
// Report the collected witness statistics
2129+
if witnessStats != nil {
2130+
witnessStats.ReportMetrics()
2131+
}
2132+
21212133
// Update the metrics touched during block commit
21222134
accountCommitTimer.Update(statedb.AccountCommits) // Account commits are complete, we can mark them
21232135
storageCommitTimer.Update(statedb.StorageCommits) // Storage commits are complete, we can mark them

core/state/database.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ type Trie interface {
130130

131131
// Witness returns a set containing all trie nodes that have been accessed.
132132
// The returned map could be nil if the witness is empty.
133-
Witness() map[string]struct{}
133+
Witness() map[string][]byte
134134

135135
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
136136
// starts at the key after the given start key. And error will be returned

core/state/statedb.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ type StateDB struct {
136136
journal *journal
137137

138138
// State witness if cross validation is needed
139-
witness *stateless.Witness
139+
witness *stateless.Witness
140+
witnessStats *stateless.WitnessStats
140141

141142
// Measurements gathered during execution for debugging purposes
142143
AccountReads time.Duration
@@ -191,12 +192,13 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro
191192
// StartPrefetcher initializes a new trie prefetcher to pull in nodes from the
192193
// state trie concurrently while the state is mutated so that when we reach the
193194
// commit phase, most of the needed data is already hot.
194-
func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) {
195+
func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness, witnessStats *stateless.WitnessStats) {
195196
// Terminate any previously running prefetcher
196197
s.StopPrefetcher()
197198

198199
// Enable witness collection if requested
199200
s.witness = witness
201+
s.witnessStats = witnessStats
200202

201203
// With the switch to the Proof-of-Stake consensus algorithm, block production
202204
// rewards are now handled at the consensus layer. Consequently, a block may
@@ -858,9 +860,17 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
858860
continue
859861
}
860862
if trie := obj.getPrefetchedTrie(); trie != nil {
861-
s.witness.AddState(trie.Witness())
863+
witness := trie.Witness()
864+
s.witness.AddState(witness)
865+
if s.witnessStats != nil {
866+
s.witnessStats.Add(witness, obj.addrHash)
867+
}
862868
} else if obj.trie != nil {
863-
s.witness.AddState(obj.trie.Witness())
869+
witness := obj.trie.Witness()
870+
s.witness.AddState(witness)
871+
if s.witnessStats != nil {
872+
s.witnessStats.Add(witness, obj.addrHash)
873+
}
864874
}
865875
}
866876
// Pull in only-read and non-destructed trie witnesses
@@ -874,9 +884,17 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
874884
continue
875885
}
876886
if trie := obj.getPrefetchedTrie(); trie != nil {
877-
s.witness.AddState(trie.Witness())
887+
witness := trie.Witness()
888+
s.witness.AddState(witness)
889+
if s.witnessStats != nil {
890+
s.witnessStats.Add(witness, obj.addrHash)
891+
}
878892
} else if obj.trie != nil {
879-
s.witness.AddState(obj.trie.Witness())
893+
witness := obj.trie.Witness()
894+
s.witness.AddState(witness)
895+
if s.witnessStats != nil {
896+
s.witnessStats.Add(witness, obj.addrHash)
897+
}
880898
}
881899
}
882900
}
@@ -942,7 +960,11 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
942960

943961
// If witness building is enabled, gather the account trie witness
944962
if s.witness != nil {
945-
s.witness.AddState(s.trie.Witness())
963+
witness := s.trie.Witness()
964+
s.witness.AddState(witness)
965+
if s.witnessStats != nil {
966+
s.witnessStats.Add(witness, common.Hash{})
967+
}
946968
}
947969
return hash
948970
}

core/stateless/stats.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package stateless
18+
19+
import (
20+
"maps"
21+
22+
"github.com/ethereum/go-ethereum/common"
23+
"github.com/ethereum/go-ethereum/metrics"
24+
)
25+
26+
var (
27+
accountTrieDepthAvg = metrics.NewRegisteredGauge("witness/trie/account/depth/avg", nil)
28+
accountTrieDepthMin = metrics.NewRegisteredGauge("witness/trie/account/depth/min", nil)
29+
accountTrieDepthMax = metrics.NewRegisteredGauge("witness/trie/account/depth/max", nil)
30+
31+
storageTrieDepthAvg = metrics.NewRegisteredGauge("witness/trie/storage/depth/avg", nil)
32+
storageTrieDepthMin = metrics.NewRegisteredGauge("witness/trie/storage/depth/min", nil)
33+
storageTrieDepthMax = metrics.NewRegisteredGauge("witness/trie/storage/depth/max", nil)
34+
)
35+
36+
// depthStats tracks min/avg/max statistics for trie access depths.
37+
type depthStats struct {
38+
totalDepth int64
39+
samples int64
40+
minDepth int64
41+
maxDepth int64
42+
}
43+
44+
// newDepthStats creates a new depthStats with default values.
45+
func newDepthStats() *depthStats {
46+
return &depthStats{minDepth: -1}
47+
}
48+
49+
// add records a new depth sample.
50+
func (d *depthStats) add(n int64) {
51+
if n < 0 {
52+
return
53+
}
54+
d.totalDepth += n
55+
d.samples++
56+
57+
if d.minDepth == -1 || n < d.minDepth {
58+
d.minDepth = n
59+
}
60+
if n > d.maxDepth {
61+
d.maxDepth = n
62+
}
63+
}
64+
65+
// report uploads the collected statistics into the provided gauges.
66+
func (d *depthStats) report(maxGauge, minGauge, avgGauge *metrics.Gauge) {
67+
if d.samples == 0 {
68+
return
69+
}
70+
maxGauge.Update(d.maxDepth)
71+
minGauge.Update(d.minDepth)
72+
avgGauge.Update(d.totalDepth / d.samples)
73+
}
74+
75+
// WitnessStats aggregates statistics for account and storage trie accesses.
76+
type WitnessStats struct {
77+
accountTrie *depthStats
78+
storageTrie *depthStats
79+
}
80+
81+
// NewWitnessStats creates a new WitnessStats collector.
82+
func NewWitnessStats() *WitnessStats {
83+
return &WitnessStats{
84+
accountTrie: newDepthStats(),
85+
storageTrie: newDepthStats(),
86+
}
87+
}
88+
89+
// Add records trie access depths from the given node paths.
90+
// If `owner` is the zero hash, accesses are attributed to the account trie;
91+
// otherwise, they are attributed to the storage trie of that account.
92+
func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) {
93+
if owner == (common.Hash{}) {
94+
for path := range maps.Keys(nodes) {
95+
s.accountTrie.add(int64(len(path)))
96+
}
97+
} else {
98+
for path := range maps.Keys(nodes) {
99+
s.storageTrie.add(int64(len(path)))
100+
}
101+
}
102+
}
103+
104+
// ReportMetrics reports the collected statistics to the global metrics registry.
105+
func (s *WitnessStats) ReportMetrics() {
106+
s.accountTrie.report(accountTrieDepthMax, accountTrieDepthMin, accountTrieDepthAvg)
107+
s.storageTrie.report(storageTrieDepthMax, storageTrieDepthMin, storageTrieDepthAvg)
108+
}

core/stateless/witness.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) {
5858
}
5959
headers = append(headers, parent)
6060
}
61-
// Create the wtness with a reconstructed gutted out block
61+
// Create the witness with a reconstructed gutted out block
6262
return &Witness{
6363
context: context,
6464
Headers: headers,
@@ -88,14 +88,16 @@ func (w *Witness) AddCode(code []byte) {
8888
}
8989

9090
// AddState inserts a batch of MPT trie nodes into the witness.
91-
func (w *Witness) AddState(nodes map[string]struct{}) {
91+
func (w *Witness) AddState(nodes map[string][]byte) {
9292
if len(nodes) == 0 {
9393
return
9494
}
9595
w.lock.Lock()
9696
defer w.lock.Unlock()
9797

98-
maps.Copy(w.State, nodes)
98+
for _, value := range nodes {
99+
w.State[string(value)] = struct{}{}
100+
}
99101
}
100102

101103
// Copy deep-copies the witness object. Witness.Block isn't deep-copied as it

core/vm/interpreter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Config struct {
3333
ExtraEips []int // Additional EIPS that are to be enabled
3434

3535
StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose)
36+
EnableWitnessStats bool // Whether trie access statistics collection is enabled
3637
}
3738

3839
// ScopeContext contains the things that are per-call, such as stack and memory,

miner/worker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
271271
if err != nil {
272272
return nil, err
273273
}
274-
state.StartPrefetcher("miner", bundle)
274+
state.StartPrefetcher("miner", bundle, nil)
275275
}
276276
// Note the passed coinbase may be different with header.Coinbase.
277277
return &environment{

trie/secure_trie.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ func (t *StateTrie) GetKey(shaKey []byte) []byte {
273273
}
274274

275275
// Witness returns a set containing all trie nodes that have been accessed.
276-
func (t *StateTrie) Witness() map[string]struct{} {
276+
func (t *StateTrie) Witness() map[string][]byte {
277277
return t.trie.Witness()
278278
}
279279

trie/tracer.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package trie
1818

1919
import (
2020
"maps"
21-
"slices"
2221
"sync"
2322
)
2423

@@ -147,11 +146,11 @@ func (t *prevalueTracer) hasList(list [][]byte) []bool {
147146
}
148147

149148
// values returns a list of values of the cached trie nodes.
150-
func (t *prevalueTracer) values() [][]byte {
149+
func (t *prevalueTracer) values() map[string][]byte {
151150
t.lock.RLock()
152151
defer t.lock.RUnlock()
153152

154-
return slices.Collect(maps.Values(t.data))
153+
return maps.Clone(t.data)
155154
}
156155

157156
// reset resets the cached content in the prevalueTracer.

trie/transition.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,6 @@ func (t *TransitionTrie) UpdateContractCode(addr common.Address, codeHash common
222222
}
223223

224224
// Witness returns a set containing all trie nodes that have been accessed.
225-
func (t *TransitionTrie) Witness() map[string]struct{} {
225+
func (t *TransitionTrie) Witness() map[string][]byte {
226226
panic("not implemented")
227227
}

0 commit comments

Comments
 (0)