From f6f3931685337746a0f4bca7328ffc2f6d43357c Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Fri, 8 Aug 2025 06:46:17 +0300 Subject: [PATCH 01/14] Update blockchain.go --- core/blockchain.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/core/blockchain.go b/core/blockchain.go index 0b92a94b6c6..94e74a74604 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -64,6 +64,8 @@ var ( headFastBlockGauge = metrics.NewRegisteredGauge("chain/head/receipt", nil) headFinalizedBlockGauge = metrics.NewRegisteredGauge("chain/head/finalized", nil) headSafeBlockGauge = metrics.NewRegisteredGauge("chain/head/safe", nil) + stateMaxDepthGauge = metrics.NewRegisteredGauge("chain/state/maxdepth", nil) + stateAvgDepthGauge = metrics.NewRegisteredGauge("chain/state/avgdepth", nil) chainInfoGauge = metrics.NewRegisteredGaugeInfo("chain/info", nil) chainMgaspsMeter = metrics.NewRegisteredResettingTimer("chain/mgasps", nil) @@ -2132,6 +2134,38 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s mgasps := float64(res.GasUsed) * 1000 / float64(elapsed) chainMgaspsMeter.Update(time.Duration(mgasps)) + //get state db trie node iterator + it, error := statedb.GetTrie().NodeIterator(nil) + if error != nil { + //log that iterator was not working + log.Error("Failed to get trie node iterator", "error", error) + } else { + var ( + maxDepth int + sumDepth int + count int + ) + + for it.Next(true) { + // it.Path() is a hex-nibble slice; the “terminator” nibble (0x10) + // exists only on leaf-nodes, so strip it if present. + d := len(it.Path()) + if d > 0 && it.Path()[d-1] == 0x10 { + d-- + } + if d > maxDepth { + maxDepth = d + } + sumDepth += d + count++ + } + + stateMaxDepthGauge.Update(int64(maxDepth)) + if count > 0 { + stateAvgDepthGauge.Update(int64(sumDepth / count)) + } + } + return &blockProcessingResult{ usedGas: res.GasUsed, procTime: proctime, From e869586e134e481fad27e36f877ba78ec4cbf2f9 Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Mon, 11 Aug 2025 03:00:20 +0300 Subject: [PATCH 02/14] made changes for tracking depth during block processing --- core/blockchain.go | 32 ----------------------- core/state/statedb.go | 8 ++++++ core/state_processor.go | 3 +++ trie/trie.go | 4 +++ trie/trie_metrics.go | 56 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 32 deletions(-) create mode 100644 trie/trie_metrics.go diff --git a/core/blockchain.go b/core/blockchain.go index 94e74a74604..08d4c6f2686 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2134,38 +2134,6 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s mgasps := float64(res.GasUsed) * 1000 / float64(elapsed) chainMgaspsMeter.Update(time.Duration(mgasps)) - //get state db trie node iterator - it, error := statedb.GetTrie().NodeIterator(nil) - if error != nil { - //log that iterator was not working - log.Error("Failed to get trie node iterator", "error", error) - } else { - var ( - maxDepth int - sumDepth int - count int - ) - - for it.Next(true) { - // it.Path() is a hex-nibble slice; the “terminator” nibble (0x10) - // exists only on leaf-nodes, so strip it if present. - d := len(it.Path()) - if d > 0 && it.Path()[d-1] == 0x10 { - d-- - } - if d > maxDepth { - maxDepth = d - } - sumDepth += d - count++ - } - - stateMaxDepthGauge.Update(int64(maxDepth)) - if count > 0 { - stateAvgDepthGauge.Update(int64(sumDepth / count)) - } - } - return &blockProcessingResult{ usedGas: res.GasUsed, procTime: proctime, diff --git a/core/state/statedb.go b/core/state/statedb.go index efb09a08a02..cc2154cfb9a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1464,3 +1464,11 @@ func (s *StateDB) Witness() *stateless.Witness { func (s *StateDB) AccessEvents() *AccessEvents { return s.accessEvents } + +func (s *StateDB) BeginTrieDepthWindow() { + trie.StateDepthMetricsStartBlock() +} + +func (s *StateDB) EndTrieDepthWindow() { + trie.StateDepthMetricsEndBlock() +} diff --git a/core/state_processor.go b/core/state_processor.go index ee98326467f..ca0d27ee39a 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -64,6 +64,9 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg allLogs []*types.Log gp = new(GasPool).AddGas(block.GasLimit()) ) + // Start the trie depth metrics window + statedb.BeginTrieDepthWindow() + defer statedb.EndTrieDepthWindow() // Mutate the block and state according to any hard-fork specs if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { diff --git a/trie/trie.go b/trie/trie.go index 6c998b31595..cc4ae30ba60 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -376,6 +376,10 @@ func (t *Trie) update(key, value []byte) error { func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) { if len(key) == 0 { + if t.owner == (common.Hash{}) { + stateDepthAggregator.record(int64(len(prefix))) + } + if v, ok := n.(valueNode); ok { return !bytes.Equal(v, value.(valueNode)), value, nil } diff --git a/trie/trie_metrics.go b/trie/trie_metrics.go new file mode 100644 index 00000000000..a8b30f1c740 --- /dev/null +++ b/trie/trie_metrics.go @@ -0,0 +1,56 @@ +package trie + +import ( + "sync" + + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + avgAccessDepthInBlock = metrics.NewRegisteredMeter("trie/access/depth/avg", nil) + minAccessDepthInBlock = metrics.NewRegisteredMeter("trie/access/depth/min", nil) + stateDepthAggregator = &depthAggregator{} +) + +// depthAggregator aggregates trie access depth metrics for a block. +type depthAggregator struct { + mu sync.Mutex + sum, cnt int64 + min int64 +} + +// start initializes the aggregator for a new block. +func (d *depthAggregator) start() { + d.mu.Lock() + d.sum, d.cnt, d.min = 0, 0, -1 + d.mu.Unlock() +} + +// record records the access depth for a trie operation. +func (d *depthAggregator) record(depth int64) { + d.mu.Lock() + v := depth + d.sum += v + d.cnt++ + if d.min < 0 || v < d.min { + d.min = v + } + d.mu.Unlock() +} + +// end finalizes the metrics for the current block and updates the registered metrics. +func (d *depthAggregator) end() { + d.mu.Lock() + sum, cnt, min := d.sum, d.cnt, d.min + d.mu.Unlock() + if cnt > 0 { + avgAccessDepthInBlock.Mark(sum / cnt) + minAccessDepthInBlock.Mark(min) + } +} + +// StateDepthMetricsStartBlock initializes the depth aggregator for a new block. +func StateDepthMetricsStartBlock() { stateDepthAggregator.start() } + +// StateDepthMetricsEndBlock finalizes the depth metrics for the current block. +func StateDepthMetricsEndBlock() { stateDepthAggregator.end() } From 0f09a8c690190ffefda15aa1682727c0764f0cec Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Mon, 11 Aug 2025 03:04:39 +0300 Subject: [PATCH 03/14] small fix on metering function --- trie/trie_metrics.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/trie/trie_metrics.go b/trie/trie_metrics.go index a8b30f1c740..1f38b73243f 100644 --- a/trie/trie_metrics.go +++ b/trie/trie_metrics.go @@ -7,8 +7,8 @@ import ( ) var ( - avgAccessDepthInBlock = metrics.NewRegisteredMeter("trie/access/depth/avg", nil) - minAccessDepthInBlock = metrics.NewRegisteredMeter("trie/access/depth/min", nil) + avgAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/avg", nil) + minAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/min", nil) stateDepthAggregator = &depthAggregator{} ) @@ -44,8 +44,8 @@ func (d *depthAggregator) end() { sum, cnt, min := d.sum, d.cnt, d.min d.mu.Unlock() if cnt > 0 { - avgAccessDepthInBlock.Mark(sum / cnt) - minAccessDepthInBlock.Mark(min) + avgAccessDepthInBlock.Update(sum / cnt) + minAccessDepthInBlock.Update(min) } } From d4d1467d39c619b4deef721b08ccaf6f38c111f2 Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Mon, 11 Aug 2025 03:07:14 +0300 Subject: [PATCH 04/14] quick fix on previous update to make sure no code change in blockchain.go --- core/blockchain.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 08d4c6f2686..0b92a94b6c6 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -64,8 +64,6 @@ var ( headFastBlockGauge = metrics.NewRegisteredGauge("chain/head/receipt", nil) headFinalizedBlockGauge = metrics.NewRegisteredGauge("chain/head/finalized", nil) headSafeBlockGauge = metrics.NewRegisteredGauge("chain/head/safe", nil) - stateMaxDepthGauge = metrics.NewRegisteredGauge("chain/state/maxdepth", nil) - stateAvgDepthGauge = metrics.NewRegisteredGauge("chain/state/avgdepth", nil) chainInfoGauge = metrics.NewRegisteredGaugeInfo("chain/info", nil) chainMgaspsMeter = metrics.NewRegisteredResettingTimer("chain/mgasps", nil) From 765c380b6cf8145817a8d9065ce8e47a4b3223b4 Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:44:11 +0200 Subject: [PATCH 05/14] reset --- core/state/statedb.go | 8 ------ core/state_processor.go | 3 --- trie/trie.go | 15 +++++------ trie/trie_metrics.go | 56 ----------------------------------------- 4 files changed, 8 insertions(+), 74 deletions(-) delete mode 100644 trie/trie_metrics.go diff --git a/core/state/statedb.go b/core/state/statedb.go index cc2154cfb9a..efb09a08a02 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1464,11 +1464,3 @@ func (s *StateDB) Witness() *stateless.Witness { func (s *StateDB) AccessEvents() *AccessEvents { return s.accessEvents } - -func (s *StateDB) BeginTrieDepthWindow() { - trie.StateDepthMetricsStartBlock() -} - -func (s *StateDB) EndTrieDepthWindow() { - trie.StateDepthMetricsEndBlock() -} diff --git a/core/state_processor.go b/core/state_processor.go index ca0d27ee39a..ee98326467f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -64,9 +64,6 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg allLogs []*types.Log gp = new(GasPool).AddGas(block.GasLimit()) ) - // Start the trie depth metrics window - statedb.BeginTrieDepthWindow() - defer statedb.EndTrieDepthWindow() // Mutate the block and state according to any hard-fork specs if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { diff --git a/trie/trie.go b/trie/trie.go index cc4ae30ba60..c5b0377c12a 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -21,7 +21,6 @@ import ( "bytes" "errors" "fmt" - "slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -376,10 +375,6 @@ func (t *Trie) update(key, value []byte) error { func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) { if len(key) == 0 { - if t.owner == (common.Hash{}) { - stateDepthAggregator.record(int64(len(prefix))) - } - if v, ok := n.(valueNode); ok { return !bytes.Equal(v, value.(valueNode)), value, nil } @@ -522,7 +517,10 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // always creates a new slice) instead of append to // avoid modifying n.Key since it might be shared with // other nodes. - return true, &shortNode{slices.Concat(n.Key, child.Key), child.Val, t.newFlag()}, nil + if t.owner == (common.Hash{}) { + stateDepthAggregator.record(int64(len(prefix) + len(key))) + } + return true, &shortNode{concat(n.Key, child.Key...), child.Val, t.newFlag()}, nil default: return true, &shortNode{n.Key, child, t.newFlag()}, nil } @@ -579,7 +577,10 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // Replace the entire full node with the short node. // Mark the original short node as deleted since the // value is embedded into the parent now. - t.opTracer.onDelete(append(prefix, byte(pos))) + t.tracer.onDelete(append(prefix, byte(pos))) + if t.owner == (common.Hash{}) { + stateDepthAggregator.record(int64(len(prefix) + 1)) + } k := append([]byte{byte(pos)}, cnode.Key...) return true, &shortNode{k, cnode.Val, t.newFlag()}, nil diff --git a/trie/trie_metrics.go b/trie/trie_metrics.go deleted file mode 100644 index 1f38b73243f..00000000000 --- a/trie/trie_metrics.go +++ /dev/null @@ -1,56 +0,0 @@ -package trie - -import ( - "sync" - - "github.com/ethereum/go-ethereum/metrics" -) - -var ( - avgAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/avg", nil) - minAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/min", nil) - stateDepthAggregator = &depthAggregator{} -) - -// depthAggregator aggregates trie access depth metrics for a block. -type depthAggregator struct { - mu sync.Mutex - sum, cnt int64 - min int64 -} - -// start initializes the aggregator for a new block. -func (d *depthAggregator) start() { - d.mu.Lock() - d.sum, d.cnt, d.min = 0, 0, -1 - d.mu.Unlock() -} - -// record records the access depth for a trie operation. -func (d *depthAggregator) record(depth int64) { - d.mu.Lock() - v := depth - d.sum += v - d.cnt++ - if d.min < 0 || v < d.min { - d.min = v - } - d.mu.Unlock() -} - -// end finalizes the metrics for the current block and updates the registered metrics. -func (d *depthAggregator) end() { - d.mu.Lock() - sum, cnt, min := d.sum, d.cnt, d.min - d.mu.Unlock() - if cnt > 0 { - avgAccessDepthInBlock.Update(sum / cnt) - minAccessDepthInBlock.Update(min) - } -} - -// StateDepthMetricsStartBlock initializes the depth aggregator for a new block. -func StateDepthMetricsStartBlock() { stateDepthAggregator.start() } - -// StateDepthMetricsEndBlock finalizes the depth metrics for the current block. -func StateDepthMetricsEndBlock() { stateDepthAggregator.end() } From 80bc2f3ac11daddd700cd86a8a7f2694272a9be1 Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:39:09 +0200 Subject: [PATCH 06/14] add metrics for depth of state trie access --- core/blockchain.go | 22 ++++++++++++++++++++++ core/state/database.go | 4 ++++ core/state/statedb.go | 12 ++++++------ core/stateless/database.go | 1 + core/stateless/witness.go | 13 ++++++++++--- trie/secure_trie.go | 5 +++++ trie/tracer.go | 4 ++++ trie/trie.go | 21 ++++++++++++++++++--- trie/verkle.go | 5 +++++ 9 files changed, 75 insertions(+), 12 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 0b92a94b6c6..f40bcf6819c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -112,6 +112,9 @@ var ( errChainStopped = errors.New("blockchain is stopped") errInvalidOldChain = errors.New("invalid old chain") errInvalidNewChain = errors.New("invalid new chain") + + avgAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/avg", nil) + minAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/min", nil) ) var ( @@ -2083,6 +2086,7 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s return nil, fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash()) } } + xvtime := time.Since(xvstart) proctime := time.Since(startTime) // processing + validation + cross validation @@ -2118,6 +2122,24 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s if err != nil { return nil, err } + + // If witness was generated, update metrics regarding the access paths. + if witness != nil { + paths := witness.Paths + totaldepth, pathnum, mindepth := 0, 0, -1 + if len(paths) > 0 { + for path, _ := range paths { + if len(path) < mindepth || mindepth < 0 { + mindepth = len(path) + } + totaldepth += len(path) + pathnum++ + } + avgAccessDepthInBlock.Update(int64(totaldepth) / int64(pathnum)) + minAccessDepthInBlock.Update(int64(mindepth)) + } + } + // Update the metrics touched during block commit accountCommitTimer.Update(statedb.AccountCommits) // Account commits are complete, we can mark them storageCommitTimer.Update(statedb.StorageCommits) // Storage commits are complete, we can mark them diff --git a/core/state/database.go b/core/state/database.go index 55fb3a0d972..a66dbc73b79 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -132,6 +132,10 @@ type Trie interface { // The returned map could be nil if the witness is empty. Witness() map[string]struct{} + // WitnessPaths returns a set of paths for all trie nodes. For future reference, + // witness can be deprecated and used as a replacement to witness. + WitnessPaths() map[string]struct{} + // NodeIterator returns an iterator that returns nodes of the trie. Iteration // starts at the key after the given start key. And error will be returned // if fails to create node iterator. diff --git a/core/state/statedb.go b/core/state/statedb.go index efb09a08a02..bdfe27f2d1c 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -841,7 +841,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled and the state object has a trie, // gather the witnesses for its specific storage trie if s.witness != nil && obj.trie != nil { - s.witness.AddState(obj.trie.Witness()) + s.witness.AddState(obj.trie.Witness(), obj.trie.WitnessPaths()) } } return nil @@ -858,9 +858,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness()) + s.witness.AddState(trie.Witness(), trie.WitnessPaths()) } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness()) + s.witness.AddState(obj.trie.Witness(), obj.trie.WitnessPaths()) } } // Pull in only-read and non-destructed trie witnesses @@ -874,9 +874,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness()) + s.witness.AddState(trie.Witness(), trie.WitnessPaths()) } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness()) + s.witness.AddState(obj.trie.Witness(), obj.trie.WitnessPaths()) } } } @@ -942,7 +942,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled, gather the account trie witness if s.witness != nil { - s.witness.AddState(s.trie.Witness()) + s.witness.AddState(s.trie.Witness(), nil) } return hash } diff --git a/core/stateless/database.go b/core/stateless/database.go index f54c123ddaa..b2d3efb0b19 100644 --- a/core/stateless/database.go +++ b/core/stateless/database.go @@ -63,5 +63,6 @@ func (w *Witness) MakeHashDB() ethdb.Database { rawdb.WriteLegacyTrieNode(memdb, common.BytesToHash(hash), blob) } + return memdb } diff --git a/core/stateless/witness.go b/core/stateless/witness.go index aecfad1d52f..671adcd37bb 100644 --- a/core/stateless/witness.go +++ b/core/stateless/witness.go @@ -41,6 +41,7 @@ type Witness struct { Headers []*types.Header // Past headers in reverse order (0=parent, 1=parent's-parent, etc). First *must* be set. Codes map[string]struct{} // Set of bytecodes ran or accessed State map[string]struct{} // Set of MPT state trie nodes (account and storage together) + Paths map[string]struct{} // Set of MPT trie paths (i.e. all accessed nodes, not just the ones in state) chain HeaderReader // Chain reader to convert block hash ops to header proofs lock sync.Mutex // Lock to allow concurrent state insertions @@ -58,13 +59,15 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) { } headers = append(headers, parent) } - // Create the wtness with a reconstructed gutted out block + // Create the witness with a reconstructed gutted out block return &Witness{ context: context, Headers: headers, Codes: make(map[string]struct{}), State: make(map[string]struct{}), - chain: chain, + Paths: make(map[string]struct{}), + + chain: chain, }, nil } @@ -88,7 +91,7 @@ func (w *Witness) AddCode(code []byte) { } // AddState inserts a batch of MPT trie nodes into the witness. -func (w *Witness) AddState(nodes map[string]struct{}) { +func (w *Witness) AddState(nodes map[string]struct{}, paths map[string]struct{}) { if len(nodes) == 0 { return } @@ -96,6 +99,9 @@ func (w *Witness) AddState(nodes map[string]struct{}) { defer w.lock.Unlock() maps.Copy(w.State, nodes) + if paths != nil { + maps.Copy(w.Paths, paths) + } } // Copy deep-copies the witness object. Witness.Block isn't deep-copied as it @@ -105,6 +111,7 @@ func (w *Witness) Copy() *Witness { Headers: slices.Clone(w.Headers), Codes: maps.Clone(w.Codes), State: maps.Clone(w.State), + Paths: maps.Clone(w.Paths), chain: w.chain, } if w.context != nil { diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 408fe640511..09e333371c6 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -277,6 +277,11 @@ func (t *StateTrie) Witness() map[string]struct{} { return t.trie.Witness() } +// Witness returns a set containing all trie nodes that have been accessed. +func (t *StateTrie) WitnessPaths() map[string]struct{} { + return t.trie.WitnessPaths() +} + // Commit collects all dirty nodes in the trie and replaces them with the // corresponding node hash. All collected nodes (including dirty leaves if // collectLeaf is true) will be encapsulated into a nodeset for return. diff --git a/trie/tracer.go b/trie/tracer.go index 2e2d0928b51..186f0187d48 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -154,6 +154,10 @@ func (t *prevalueTracer) values() [][]byte { return slices.Collect(maps.Values(t.data)) } +func (t *prevalueTracer) keys() []string { + return slices.Collect(maps.Keys(t.data)) +} + // reset resets the cached content in the prevalueTracer. func (t *prevalueTracer) reset() { t.lock.Lock() diff --git a/trie/trie.go b/trie/trie.go index c5b0377c12a..82715fa291c 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -762,11 +762,26 @@ func (t *Trie) Witness() map[string]struct{} { if len(values) == 0 { return nil } - witness := make(map[string]struct{}, len(values)) + witnessStates := make(map[string]struct{}, len(values)) for _, val := range values { - witness[string(val)] = struct{}{} + witnessStates[string(val)] = struct{}{} } - return witness + + return witnessStates +} + +func (t *Trie) WitnessPaths() map[string]struct{} { + // Return the paths of all nodes that have been accessed. + // The paths are the keys of the prevalue tracer. + keys := t.prevalueTracer.keys() + if len(keys) == 0 { + return nil + } + witnessPaths := make(map[string]struct{}, len(keys)) + for _, key := range keys { + witnessPaths[string(key)] = struct{}{} + } + return witnessPaths } // Reset drops the referenced root node and cleans all internal state. diff --git a/trie/verkle.go b/trie/verkle.go index c8b9a6dd461..40bd495c391 100644 --- a/trie/verkle.go +++ b/trie/verkle.go @@ -455,3 +455,8 @@ func (t *VerkleTrie) nodeResolver(path []byte) ([]byte, error) { func (t *VerkleTrie) Witness() map[string]struct{} { panic("not implemented") } + +// Witness returns a set containing all trie nodes that have been accessed. +func (t *VerkleTrie) WitnessPaths() map[string]struct{} { + panic("not implemented") +} From 97519e0ff278797a882765eb8c9d4ee6b0f60dc5 Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:27:38 +0200 Subject: [PATCH 07/14] changes with witness tracking --- core/state/database.go | 6 +---- core/state/statedb.go | 12 +++++----- core/stateless/witness.go | 11 ++++----- trie/secure_trie.go | 2 +- trie/tracer.go | 4 ++++ trie/trie.go | 48 +++++++++++++++------------------------ trie/verkle.go | 7 +----- 7 files changed, 36 insertions(+), 54 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index a66dbc73b79..3a0ac422ee4 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -130,11 +130,7 @@ type Trie interface { // Witness returns a set containing all trie nodes that have been accessed. // The returned map could be nil if the witness is empty. - Witness() map[string]struct{} - - // WitnessPaths returns a set of paths for all trie nodes. For future reference, - // witness can be deprecated and used as a replacement to witness. - WitnessPaths() map[string]struct{} + Witness() map[string][]byte // NodeIterator returns an iterator that returns nodes of the trie. Iteration // starts at the key after the given start key. And error will be returned diff --git a/core/state/statedb.go b/core/state/statedb.go index bdfe27f2d1c..efb09a08a02 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -841,7 +841,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled and the state object has a trie, // gather the witnesses for its specific storage trie if s.witness != nil && obj.trie != nil { - s.witness.AddState(obj.trie.Witness(), obj.trie.WitnessPaths()) + s.witness.AddState(obj.trie.Witness()) } } return nil @@ -858,9 +858,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness(), trie.WitnessPaths()) + s.witness.AddState(trie.Witness()) } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness(), obj.trie.WitnessPaths()) + s.witness.AddState(obj.trie.Witness()) } } // Pull in only-read and non-destructed trie witnesses @@ -874,9 +874,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness(), trie.WitnessPaths()) + s.witness.AddState(trie.Witness()) } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness(), obj.trie.WitnessPaths()) + s.witness.AddState(obj.trie.Witness()) } } } @@ -942,7 +942,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled, gather the account trie witness if s.witness != nil { - s.witness.AddState(s.trie.Witness(), nil) + s.witness.AddState(s.trie.Witness()) } return hash } diff --git a/core/stateless/witness.go b/core/stateless/witness.go index 671adcd37bb..824513d29b6 100644 --- a/core/stateless/witness.go +++ b/core/stateless/witness.go @@ -91,16 +91,15 @@ func (w *Witness) AddCode(code []byte) { } // AddState inserts a batch of MPT trie nodes into the witness. -func (w *Witness) AddState(nodes map[string]struct{}, paths map[string]struct{}) { - if len(nodes) == 0 { +func (w *Witness) AddState(nodemap map[string][]byte) { + if len(nodemap) == 0 { return } w.lock.Lock() defer w.lock.Unlock() - - maps.Copy(w.State, nodes) - if paths != nil { - maps.Copy(w.Paths, paths) + for path, value := range nodemap { + w.State[string(value)] = struct{}{} + w.Paths[string(path)] = struct{}{} // Also track the path for the node } } diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 09e333371c6..85e77fe80ec 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -273,7 +273,7 @@ func (t *StateTrie) GetKey(shaKey []byte) []byte { } // Witness returns a set containing all trie nodes that have been accessed. -func (t *StateTrie) Witness() map[string]struct{} { +func (t *StateTrie) Witness() map[string][]byte { return t.trie.Witness() } diff --git a/trie/tracer.go b/trie/tracer.go index 186f0187d48..3220910a19a 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -158,6 +158,10 @@ func (t *prevalueTracer) keys() []string { return slices.Collect(maps.Keys(t.data)) } +func (t *prevalueTracer) getMap() map[string][]byte { + return maps.Clone(t.data) +} + // reset resets the cached content in the prevalueTracer. func (t *prevalueTracer) reset() { t.lock.Lock() diff --git a/trie/trie.go b/trie/trie.go index 82715fa291c..c7e2e856425 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -133,6 +133,12 @@ func (t *Trie) NodeIterator(start []byte) (NodeIterator, error) { return newNodeIterator(t, start), nil } +// Owner returns the owner of the trie, allowing us to distinguis between +// storage and account tries. +func (t *Trie) Owner() common.Hash { + return t.owner +} + // MustGet is a wrapper of Get and will omit any encountered error but just // print out an error message. func (t *Trie) MustGet(key []byte) []byte { @@ -517,9 +523,6 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // always creates a new slice) instead of append to // avoid modifying n.Key since it might be shared with // other nodes. - if t.owner == (common.Hash{}) { - stateDepthAggregator.record(int64(len(prefix) + len(key))) - } return true, &shortNode{concat(n.Key, child.Key...), child.Val, t.newFlag()}, nil default: return true, &shortNode{n.Key, child, t.newFlag()}, nil @@ -577,10 +580,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // Replace the entire full node with the short node. // Mark the original short node as deleted since the // value is embedded into the parent now. - t.tracer.onDelete(append(prefix, byte(pos))) - if t.owner == (common.Hash{}) { - stateDepthAggregator.record(int64(len(prefix) + 1)) - } + t.opTracer.onDelete(append(prefix, byte(pos))) k := append([]byte{byte(pos)}, cnode.Key...) return true, &shortNode{k, cnode.Val, t.newFlag()}, nil @@ -618,6 +618,13 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { } } +func concat(s1 []byte, s2 ...byte) []byte { + r := make([]byte, len(s1)+len(s2)) + copy(r, s1) + copy(r[len(s1):], s2) + return r +} + // copyNode deep-copies the supplied node along with its children recursively. func copyNode(n node) node { switch n := (n).(type) { @@ -757,31 +764,12 @@ func (t *Trie) hashRoot() []byte { } // Witness returns a set containing all trie nodes that have been accessed. -func (t *Trie) Witness() map[string]struct{} { - values := t.prevalueTracer.values() - if len(values) == 0 { - return nil - } - witnessStates := make(map[string]struct{}, len(values)) - for _, val := range values { - witnessStates[string(val)] = struct{}{} - } - - return witnessStates -} - -func (t *Trie) WitnessPaths() map[string]struct{} { - // Return the paths of all nodes that have been accessed. - // The paths are the keys of the prevalue tracer. - keys := t.prevalueTracer.keys() - if len(keys) == 0 { +func (t *Trie) Witness() map[string][]byte { + dataMap := t.prevalueTracer.getMap() + if len(dataMap) == 0 { return nil } - witnessPaths := make(map[string]struct{}, len(keys)) - for _, key := range keys { - witnessPaths[string(key)] = struct{}{} - } - return witnessPaths + return dataMap } // Reset drops the referenced root node and cleans all internal state. diff --git a/trie/verkle.go b/trie/verkle.go index 40bd495c391..e00ea21602c 100644 --- a/trie/verkle.go +++ b/trie/verkle.go @@ -452,11 +452,6 @@ func (t *VerkleTrie) nodeResolver(path []byte) ([]byte, error) { } // Witness returns a set containing all trie nodes that have been accessed. -func (t *VerkleTrie) Witness() map[string]struct{} { - panic("not implemented") -} - -// Witness returns a set containing all trie nodes that have been accessed. -func (t *VerkleTrie) WitnessPaths() map[string]struct{} { +func (t *VerkleTrie) Witness() map[string][]byte { panic("not implemented") } From f986915332b357de35c62d1225f5a420b842a723 Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:47:45 +0200 Subject: [PATCH 08/14] Changes to keep track of writes and account trie statistics. Also reduced memory for witness by only keeping statistics. --- core/blockchain.go | 33 ++++++++++------- core/state/database.go | 4 ++ core/state/statedb.go | 23 +++++++++--- core/stateless/witness.go | 78 ++++++++++++++++++++++++++++++--------- trie/secure_trie.go | 9 ++--- trie/verkle.go | 4 ++ 6 files changed, 110 insertions(+), 41 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index f40bcf6819c..b56562b1090 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -113,8 +113,12 @@ var ( errInvalidOldChain = errors.New("invalid old chain") errInvalidNewChain = errors.New("invalid new chain") - avgAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/avg", nil) - minAccessDepthInBlock = metrics.NewRegisteredGauge("trie/access/depth/min", nil) + accountAccessDepthAvg = metrics.NewRegisteredGauge("trie/access/account/avg", nil) + accountAccessDepthMin = metrics.NewRegisteredGauge("trie/access/account/min", nil) + accountAccessDepthMax = metrics.NewRegisteredGauge("trie/access/account/max", nil) + storageAccessDepthAvg = metrics.NewRegisteredGauge("trie/access/storage/avg", nil) + storageAccessDepthMin = metrics.NewRegisteredGauge("trie/access/storage/min", nil) + storageAccessDepthMax = metrics.NewRegisteredGauge("trie/access/storage/max", nil) ) var ( @@ -2125,18 +2129,21 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s // If witness was generated, update metrics regarding the access paths. if witness != nil { - paths := witness.Paths - totaldepth, pathnum, mindepth := 0, 0, -1 - if len(paths) > 0 { - for path, _ := range paths { - if len(path) < mindepth || mindepth < 0 { - mindepth = len(path) - } - totaldepth += len(path) - pathnum++ + a := witness.AccountDepthMetrics + if a.Count > 0 { + accountAccessDepthAvg.Update(int64(a.Sum) / int64(a.Count)) + if a.Min != -1 { + accountAccessDepthMin.Update(int64(a.Min)) + } + accountAccessDepthMax.Update(int64(a.Max)) + } + s := witness.StateDepthMetrics + if s.Count > 0 { + storageAccessDepthAvg.Update(int64(s.Sum) / int64(s.Count)) + if s.Min != -1 { + storageAccessDepthMin.Update(int64(s.Min)) } - avgAccessDepthInBlock.Update(int64(totaldepth) / int64(pathnum)) - minAccessDepthInBlock.Update(int64(mindepth)) + storageAccessDepthMax.Update(int64(s.Max)) } } diff --git a/core/state/database.go b/core/state/database.go index 3a0ac422ee4..4567a92ea1c 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -132,6 +132,10 @@ type Trie interface { // The returned map could be nil if the witness is empty. Witness() map[string][]byte + // Owner returns the owner of the trie, allowing us to distinguish between + // storage and account tries. + Owner() common.Hash + // NodeIterator returns an iterator that returns nodes of the trie. Iteration // starts at the key after the given start key. And error will be returned // if fails to create node iterator. diff --git a/core/state/statedb.go b/core/state/statedb.go index efb09a08a02..36f05887b24 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -841,7 +841,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled and the state object has a trie, // gather the witnesses for its specific storage trie if s.witness != nil && obj.trie != nil { - s.witness.AddState(obj.trie.Witness()) + s.witness.AddState(obj.trie.Witness(), obj.trie.Owner()) } } return nil @@ -858,9 +858,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness()) + s.witness.AddState(trie.Witness(), trie.Owner()) } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness()) + s.witness.AddState(obj.trie.Witness(), obj.trie.Owner()) } } // Pull in only-read and non-destructed trie witnesses @@ -874,9 +874,9 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness()) + s.witness.AddState(trie.Witness(), trie.Owner()) } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness()) + s.witness.AddState(obj.trie.Witness(), trie.Owner()) } } } @@ -942,7 +942,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled, gather the account trie witness if s.witness != nil { - s.witness.AddState(s.trie.Witness()) + s.witness.AddState(s.trie.Witness(), s.trie.Owner()) } return hash } @@ -1295,6 +1295,17 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag if err != nil { return nil, err } + + // This iterates through nodeset and tracks path depths for tracking + // state and account trie modifications. + if s.witness != nil && ret != nil && !ret.empty() { + for owner, set := range ret.nodes.Sets { + for path := range set.Origins { + s.witness.AddStateModify(len(path), owner) + } + } + } + // Commit dirty contract code if any exists if db := s.db.TrieDB().Disk(); db != nil && len(ret.codes) > 0 { batch := db.NewBatch() diff --git a/core/stateless/witness.go b/core/stateless/witness.go index 824513d29b6..3316767c8d5 100644 --- a/core/stateless/witness.go +++ b/core/stateless/witness.go @@ -33,6 +33,13 @@ type HeaderReader interface { GetHeader(hash common.Hash, number uint64) *types.Header } +type DepthTracker struct { + Sum int + Count int + Min int + Max int +} + // Witness encompasses the state required to apply a set of transactions and // derive a post state/receipt root. type Witness struct { @@ -41,10 +48,11 @@ type Witness struct { Headers []*types.Header // Past headers in reverse order (0=parent, 1=parent's-parent, etc). First *must* be set. Codes map[string]struct{} // Set of bytecodes ran or accessed State map[string]struct{} // Set of MPT state trie nodes (account and storage together) - Paths map[string]struct{} // Set of MPT trie paths (i.e. all accessed nodes, not just the ones in state) - chain HeaderReader // Chain reader to convert block hash ops to header proofs - lock sync.Mutex // Lock to allow concurrent state insertions + StateDepthMetrics DepthTracker // Metrics about the state trie paths, used for debugging + AccountDepthMetrics DepthTracker // Metrics about the account trie paths, used for debugging + chain HeaderReader // Chain reader to convert block hash ops to header proofs + lock sync.Mutex // Lock to allow concurrent state insertions } // NewWitness creates an empty witness ready for population. @@ -59,18 +67,35 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) { } headers = append(headers, parent) } + + var stateDepthTracker = DepthTracker{Sum: 0, Count: 0, Min: -1, Max: 0} + var accountDepthTracker = DepthTracker{Sum: 0, Count: 0, Min: -1, Max: 0} // Create the witness with a reconstructed gutted out block return &Witness{ - context: context, - Headers: headers, - Codes: make(map[string]struct{}), - State: make(map[string]struct{}), - Paths: make(map[string]struct{}), - - chain: chain, + context: context, + Headers: headers, + Codes: make(map[string]struct{}), + State: make(map[string]struct{}), + StateDepthMetrics: stateDepthTracker, + AccountDepthMetrics: accountDepthTracker, + chain: chain, }, nil } +func (d *DepthTracker) Add(n int) { + if n < 0 { + n = 0 + } + d.Sum += n + d.Count++ + if d.Min == -1 || n < d.Min { + d.Min = n + } + if n > d.Max { + d.Max = n + } +} + // AddBlockHash adds a "blockhash" to the witness with the designated offset from // chain head. Under the hood, this method actually pulls in enough headers from // the chain to cover the block being added. @@ -91,7 +116,7 @@ func (w *Witness) AddCode(code []byte) { } // AddState inserts a batch of MPT trie nodes into the witness. -func (w *Witness) AddState(nodemap map[string][]byte) { +func (w *Witness) AddState(nodemap map[string][]byte, owner common.Hash) { if len(nodemap) == 0 { return } @@ -99,7 +124,25 @@ func (w *Witness) AddState(nodemap map[string][]byte) { defer w.lock.Unlock() for path, value := range nodemap { w.State[string(value)] = struct{}{} - w.Paths[string(path)] = struct{}{} // Also track the path for the node + if owner != (common.Hash{}) { + w.AccountDepthMetrics.Add(len(path)) + } else { + w.StateDepthMetrics.Add(len(path)) + } + } +} + +// AddStateModify tracks the modification of a node in the witness. +func (w *Witness) AddStateModify(path int, owner common.Hash) { + if path < 0 { + return + } + w.lock.Lock() + defer w.lock.Unlock() + if owner != (common.Hash{}) { + w.AccountDepthMetrics.Add(path) + } else { + w.StateDepthMetrics.Add(path) } } @@ -107,11 +150,12 @@ func (w *Witness) AddState(nodemap map[string][]byte) { // is never mutated by Witness func (w *Witness) Copy() *Witness { cpy := &Witness{ - Headers: slices.Clone(w.Headers), - Codes: maps.Clone(w.Codes), - State: maps.Clone(w.State), - Paths: maps.Clone(w.Paths), - chain: w.chain, + Headers: slices.Clone(w.Headers), + Codes: maps.Clone(w.Codes), + State: maps.Clone(w.State), + StateDepthMetrics: w.StateDepthMetrics, + AccountDepthMetrics: w.AccountDepthMetrics, + chain: w.chain, } if w.context != nil { cpy.context = types.CopyHeader(w.context) diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 85e77fe80ec..4fb21d20ad1 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -96,6 +96,10 @@ func NewStateTrie(id *ID, db database.NodeDatabase) (*StateTrie, error) { return tr, nil } +func (t *StateTrie) Owner() common.Hash { + return t.trie.Owner() +} + // MustGet returns the value for key stored in the trie. // The value bytes must not be modified by the caller. // @@ -277,11 +281,6 @@ func (t *StateTrie) Witness() map[string][]byte { return t.trie.Witness() } -// Witness returns a set containing all trie nodes that have been accessed. -func (t *StateTrie) WitnessPaths() map[string]struct{} { - return t.trie.WitnessPaths() -} - // Commit collects all dirty nodes in the trie and replaces them with the // corresponding node hash. All collected nodes (including dirty leaves if // collectLeaf is true) will be encapsulated into a nodeset for return. diff --git a/trie/verkle.go b/trie/verkle.go index e00ea21602c..3ad638f0892 100644 --- a/trie/verkle.go +++ b/trie/verkle.go @@ -78,6 +78,10 @@ func (t *VerkleTrie) GetKey(key []byte) []byte { return key } +func (t *VerkleTrie) Owner() common.Hash { + panic("VerkleTrie does not have an owner") +} + // GetAccount implements state.Trie, retrieving the account with the specified // account address. If the specified account is not in the verkle tree, nil will // be returned. If the tree is corrupted, an error will be returned. From 23d24f3ce38c88f2c19636607c34c445b6d4c0ee Mon Sep 17 00:00:00 2001 From: shantichanal <158101918+shantichanal@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:56:42 +0200 Subject: [PATCH 09/14] small fix --- trie/transition.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/trie/transition.go b/trie/transition.go index 1670b8e793d..e8962e106b2 100644 --- a/trie/transition.go +++ b/trie/transition.go @@ -45,6 +45,10 @@ func NewTransitionTree(base *SecureTrie, overlay *VerkleTrie, st bool) *Transiti } } +func (t *TransitionTrie) Owner() common.Hash { + return t.overlay.Owner() +} + // Base returns the base trie. func (t *TransitionTrie) Base() *SecureTrie { return t.base From 3b354570a7f7e5aa0a266f785b2304e2519bc680 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 20 Aug 2025 15:41:48 +0800 Subject: [PATCH 10/14] core,trie: rework the witness stats --- core/blockchain.go | 39 ++++---------- core/state/database.go | 4 -- core/state/statedb.go | 49 ++++++++++------- core/stateless/stats.go | 108 ++++++++++++++++++++++++++++++++++++++ core/stateless/witness.go | 78 ++++++--------------------- core/vm/interpreter.go | 1 + miner/worker.go | 2 +- trie/secure_trie.go | 4 -- trie/transition.go | 6 +-- trie/trie.go | 12 +---- trie/verkle.go | 4 -- 11 files changed, 168 insertions(+), 139 deletions(-) create mode 100644 core/stateless/stats.go diff --git a/core/blockchain.go b/core/blockchain.go index b56562b1090..5205483af91 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -112,13 +112,6 @@ var ( errChainStopped = errors.New("blockchain is stopped") errInvalidOldChain = errors.New("invalid old chain") errInvalidNewChain = errors.New("invalid new chain") - - accountAccessDepthAvg = metrics.NewRegisteredGauge("trie/access/account/avg", nil) - accountAccessDepthMin = metrics.NewRegisteredGauge("trie/access/account/min", nil) - accountAccessDepthMax = metrics.NewRegisteredGauge("trie/access/account/max", nil) - storageAccessDepthAvg = metrics.NewRegisteredGauge("trie/access/storage/avg", nil) - storageAccessDepthMin = metrics.NewRegisteredGauge("trie/access/storage/min", nil) - storageAccessDepthMax = metrics.NewRegisteredGauge("trie/access/storage/max", nil) ) var ( @@ -2018,7 +2011,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s // If we are past Byzantium, enable prefetching to pull in trie node paths // while processing transactions. Before Byzantium the prefetcher is mostly // useless due to the intermediate root hashing after each transaction. - var witness *stateless.Witness + var ( + witness *stateless.Witness + witnessStats *stateless.WitnessStats + ) if bc.chainConfig.IsByzantium(block.Number()) { // Generate witnesses either if we're self-testing, or if it's the // only block being inserted. A bit crude, but witnesses are huge, @@ -2028,8 +2024,11 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s if err != nil { return nil, err } + if bc.cfg.VmConfig.EnableWitnessStats { + witnessStats = stateless.NewWitnessStats() + } } - statedb.StartPrefetcher("chain", witness) + statedb.StartPrefetcher("chain", witness, witnessStats) defer statedb.StopPrefetcher() } @@ -2126,25 +2125,9 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s if err != nil { return nil, err } - - // If witness was generated, update metrics regarding the access paths. - if witness != nil { - a := witness.AccountDepthMetrics - if a.Count > 0 { - accountAccessDepthAvg.Update(int64(a.Sum) / int64(a.Count)) - if a.Min != -1 { - accountAccessDepthMin.Update(int64(a.Min)) - } - accountAccessDepthMax.Update(int64(a.Max)) - } - s := witness.StateDepthMetrics - if s.Count > 0 { - storageAccessDepthAvg.Update(int64(s.Sum) / int64(s.Count)) - if s.Min != -1 { - storageAccessDepthMin.Update(int64(s.Min)) - } - storageAccessDepthMax.Update(int64(s.Max)) - } + // Report the collected witness statistics + if witnessStats != nil { + witnessStats.ReportMetrics() } // Update the metrics touched during block commit diff --git a/core/state/database.go b/core/state/database.go index 4567a92ea1c..3a0ac422ee4 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -132,10 +132,6 @@ type Trie interface { // The returned map could be nil if the witness is empty. Witness() map[string][]byte - // Owner returns the owner of the trie, allowing us to distinguish between - // storage and account tries. - Owner() common.Hash - // NodeIterator returns an iterator that returns nodes of the trie. Iteration // starts at the key after the given start key. And error will be returned // if fails to create node iterator. diff --git a/core/state/statedb.go b/core/state/statedb.go index 36f05887b24..688414e2980 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -136,7 +136,8 @@ type StateDB struct { journal *journal // State witness if cross validation is needed - witness *stateless.Witness + witness *stateless.Witness + witnessStats *stateless.WitnessStats // Measurements gathered during execution for debugging purposes AccountReads time.Duration @@ -191,12 +192,13 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. -func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) { +func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness, witnessStats *stateless.WitnessStats) { // Terminate any previously running prefetcher s.StopPrefetcher() // Enable witness collection if requested s.witness = witness + s.witnessStats = witnessStats // With the switch to the Proof-of-Stake consensus algorithm, block production // rewards are now handled at the consensus layer. Consequently, a block may @@ -841,7 +843,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled and the state object has a trie, // gather the witnesses for its specific storage trie if s.witness != nil && obj.trie != nil { - s.witness.AddState(obj.trie.Witness(), obj.trie.Owner()) + s.witness.AddState(obj.trie.Witness()) } } return nil @@ -858,9 +860,17 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness(), trie.Owner()) + witness := trie.Witness() + s.witness.AddState(witness) + if s.witnessStats != nil { + s.witnessStats.Add(witness, obj.addrHash) + } } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness(), obj.trie.Owner()) + witness := obj.trie.Witness() + s.witness.AddState(witness) + if s.witnessStats != nil { + s.witnessStats.Add(witness, obj.addrHash) + } } } // Pull in only-read and non-destructed trie witnesses @@ -874,9 +884,17 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { continue } if trie := obj.getPrefetchedTrie(); trie != nil { - s.witness.AddState(trie.Witness(), trie.Owner()) + witness := trie.Witness() + s.witness.AddState(witness) + if s.witnessStats != nil { + s.witnessStats.Add(witness, obj.addrHash) + } } else if obj.trie != nil { - s.witness.AddState(obj.trie.Witness(), trie.Owner()) + witness := trie.Witness() + s.witness.AddState(witness) + if s.witnessStats != nil { + s.witnessStats.Add(witness, obj.addrHash) + } } } } @@ -942,7 +960,11 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // If witness building is enabled, gather the account trie witness if s.witness != nil { - s.witness.AddState(s.trie.Witness(), s.trie.Owner()) + witness := s.trie.Witness() + s.witness.AddState(witness) + if s.witnessStats != nil { + s.witnessStats.Add(witness, common.Hash{}) + } } return hash } @@ -1295,17 +1317,6 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag if err != nil { return nil, err } - - // This iterates through nodeset and tracks path depths for tracking - // state and account trie modifications. - if s.witness != nil && ret != nil && !ret.empty() { - for owner, set := range ret.nodes.Sets { - for path := range set.Origins { - s.witness.AddStateModify(len(path), owner) - } - } - } - // Commit dirty contract code if any exists if db := s.db.TrieDB().Disk(); db != nil && len(ret.codes) > 0 { batch := db.NewBatch() diff --git a/core/stateless/stats.go b/core/stateless/stats.go new file mode 100644 index 00000000000..088e06c9bb3 --- /dev/null +++ b/core/stateless/stats.go @@ -0,0 +1,108 @@ +// 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 stateless + +import ( + "maps" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + accountTrieDepthAvg = metrics.NewRegisteredGauge("witness/trie/account/depth/avg", nil) + accountTrieDepthMin = metrics.NewRegisteredGauge("witness/trie/account/depth/min", nil) + accountTrieDepthMax = metrics.NewRegisteredGauge("witness/trie/account/depth/max", nil) + + storageTrieDepthAvg = metrics.NewRegisteredGauge("witness/trie/storage/depth/avg", nil) + storageTrieDepthMin = metrics.NewRegisteredGauge("witness/trie/storage/depth/min", nil) + storageTrieDepthMax = metrics.NewRegisteredGauge("witness/trie/storage/depth/max", nil) +) + +// depthStats tracks min/avg/max statistics for trie access depths. +type depthStats struct { + totalDepth int64 + samples int64 + minDepth int64 + maxDepth int64 +} + +// newDepthStats creates a new depthStats with default values. +func newDepthStats() *depthStats { + return &depthStats{minDepth: -1} +} + +// Add records a new depth sample. +func (d *depthStats) Add(n int64) { + if n < 0 { + return + } + d.totalDepth += n + d.samples++ + + if d.minDepth == -1 || n < d.minDepth { + d.minDepth = n + } + if n > d.maxDepth { + d.maxDepth = n + } +} + +// report uploads the collected statistics into the provided gauges. +func (d *depthStats) report(maxGauge, minGauge, avgGauge *metrics.Gauge) { + if d.samples == 0 { + return + } + maxGauge.Update(d.maxDepth) + minGauge.Update(d.minDepth) + avgGauge.Update(d.totalDepth / d.samples) +} + +// WitnessStats aggregates statistics for account and storage trie accesses. +type WitnessStats struct { + accountTrie *depthStats + storageTrie *depthStats +} + +// NewWitnessStats creates a new WitnessStats collector. +func NewWitnessStats() *WitnessStats { + return &WitnessStats{ + accountTrie: newDepthStats(), + storageTrie: newDepthStats(), + } +} + +// Add records trie access depths from the given node paths. +// If `owner` is the zero hash, accesses are attributed to the account trie; +// otherwise, they are attributed to the storage trie of that account. +func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) { + if owner == (common.Hash{}) { + for path := range maps.Keys(nodes) { + s.accountTrie.Add(int64(len(path))) + } + } else { + for path := range maps.Keys(nodes) { + s.storageTrie.Add(int64(len(path))) + } + } +} + +// ReportMetrics reports the collected statistics to the global metrics registry. +func (s *WitnessStats) ReportMetrics() { + s.accountTrie.report(accountTrieDepthMax, accountTrieDepthMin, accountTrieDepthAvg) + s.storageTrie.report(storageTrieDepthMax, storageTrieDepthMin, storageTrieDepthAvg) +} diff --git a/core/stateless/witness.go b/core/stateless/witness.go index 3316767c8d5..371a128f48a 100644 --- a/core/stateless/witness.go +++ b/core/stateless/witness.go @@ -33,13 +33,6 @@ type HeaderReader interface { GetHeader(hash common.Hash, number uint64) *types.Header } -type DepthTracker struct { - Sum int - Count int - Min int - Max int -} - // Witness encompasses the state required to apply a set of transactions and // derive a post state/receipt root. type Witness struct { @@ -49,10 +42,8 @@ type Witness struct { Codes map[string]struct{} // Set of bytecodes ran or accessed State map[string]struct{} // Set of MPT state trie nodes (account and storage together) - StateDepthMetrics DepthTracker // Metrics about the state trie paths, used for debugging - AccountDepthMetrics DepthTracker // Metrics about the account trie paths, used for debugging - chain HeaderReader // Chain reader to convert block hash ops to header proofs - lock sync.Mutex // Lock to allow concurrent state insertions + chain HeaderReader // Chain reader to convert block hash ops to header proofs + lock sync.Mutex // Lock to allow concurrent state insertions } // NewWitness creates an empty witness ready for population. @@ -67,35 +58,16 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) { } headers = append(headers, parent) } - - var stateDepthTracker = DepthTracker{Sum: 0, Count: 0, Min: -1, Max: 0} - var accountDepthTracker = DepthTracker{Sum: 0, Count: 0, Min: -1, Max: 0} // Create the witness with a reconstructed gutted out block return &Witness{ - context: context, - Headers: headers, - Codes: make(map[string]struct{}), - State: make(map[string]struct{}), - StateDepthMetrics: stateDepthTracker, - AccountDepthMetrics: accountDepthTracker, - chain: chain, + context: context, + Headers: headers, + Codes: make(map[string]struct{}), + State: make(map[string]struct{}), + chain: chain, }, nil } -func (d *DepthTracker) Add(n int) { - if n < 0 { - n = 0 - } - d.Sum += n - d.Count++ - if d.Min == -1 || n < d.Min { - d.Min = n - } - if n > d.Max { - d.Max = n - } -} - // AddBlockHash adds a "blockhash" to the witness with the designated offset from // chain head. Under the hood, this method actually pulls in enough headers from // the chain to cover the block being added. @@ -116,33 +88,15 @@ func (w *Witness) AddCode(code []byte) { } // AddState inserts a batch of MPT trie nodes into the witness. -func (w *Witness) AddState(nodemap map[string][]byte, owner common.Hash) { - if len(nodemap) == 0 { +func (w *Witness) AddState(nodes map[string][]byte) { + if len(nodes) == 0 { return } w.lock.Lock() defer w.lock.Unlock() - for path, value := range nodemap { - w.State[string(value)] = struct{}{} - if owner != (common.Hash{}) { - w.AccountDepthMetrics.Add(len(path)) - } else { - w.StateDepthMetrics.Add(len(path)) - } - } -} -// AddStateModify tracks the modification of a node in the witness. -func (w *Witness) AddStateModify(path int, owner common.Hash) { - if path < 0 { - return - } - w.lock.Lock() - defer w.lock.Unlock() - if owner != (common.Hash{}) { - w.AccountDepthMetrics.Add(path) - } else { - w.StateDepthMetrics.Add(path) + for _, value := range nodes { + w.State[string(value)] = struct{}{} } } @@ -150,12 +104,10 @@ func (w *Witness) AddStateModify(path int, owner common.Hash) { // is never mutated by Witness func (w *Witness) Copy() *Witness { cpy := &Witness{ - Headers: slices.Clone(w.Headers), - Codes: maps.Clone(w.Codes), - State: maps.Clone(w.State), - StateDepthMetrics: w.StateDepthMetrics, - AccountDepthMetrics: w.AccountDepthMetrics, - chain: w.chain, + Headers: slices.Clone(w.Headers), + Codes: maps.Clone(w.Codes), + State: maps.Clone(w.State), + chain: w.chain, } if w.context != nil { cpy.context = types.CopyHeader(w.context) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index a0637a6800e..52dbe83d86c 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -33,6 +33,7 @@ type Config struct { ExtraEips []int // Additional EIPS that are to be enabled StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose) + EnableWitnessStats bool // Whether trie access statistics collection is enabled } // ScopeContext contains the things that are per-call, such as stack and memory, diff --git a/miner/worker.go b/miner/worker.go index 5405fb24b93..0e2560f8445 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -271,7 +271,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase if err != nil { return nil, err } - state.StartPrefetcher("miner", bundle) + state.StartPrefetcher("miner", bundle, nil) } // Note the passed coinbase may be different with header.Coinbase. return &environment{ diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 4fb21d20ad1..7c7bd184bf8 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -96,10 +96,6 @@ func NewStateTrie(id *ID, db database.NodeDatabase) (*StateTrie, error) { return tr, nil } -func (t *StateTrie) Owner() common.Hash { - return t.trie.Owner() -} - // MustGet returns the value for key stored in the trie. // The value bytes must not be modified by the caller. // diff --git a/trie/transition.go b/trie/transition.go index e8962e106b2..0e82cb26271 100644 --- a/trie/transition.go +++ b/trie/transition.go @@ -45,10 +45,6 @@ func NewTransitionTree(base *SecureTrie, overlay *VerkleTrie, st bool) *Transiti } } -func (t *TransitionTrie) Owner() common.Hash { - return t.overlay.Owner() -} - // Base returns the base trie. func (t *TransitionTrie) Base() *SecureTrie { return t.base @@ -226,6 +222,6 @@ func (t *TransitionTrie) UpdateContractCode(addr common.Address, codeHash common } // Witness returns a set containing all trie nodes that have been accessed. -func (t *TransitionTrie) Witness() map[string]struct{} { +func (t *TransitionTrie) Witness() map[string][]byte { panic("not implemented") } diff --git a/trie/trie.go b/trie/trie.go index c7e2e856425..fe8b5837d49 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -133,12 +133,6 @@ func (t *Trie) NodeIterator(start []byte) (NodeIterator, error) { return newNodeIterator(t, start), nil } -// Owner returns the owner of the trie, allowing us to distinguis between -// storage and account tries. -func (t *Trie) Owner() common.Hash { - return t.owner -} - // MustGet is a wrapper of Get and will omit any encountered error but just // print out an error message. func (t *Trie) MustGet(key []byte) []byte { @@ -765,11 +759,7 @@ func (t *Trie) hashRoot() []byte { // Witness returns a set containing all trie nodes that have been accessed. func (t *Trie) Witness() map[string][]byte { - dataMap := t.prevalueTracer.getMap() - if len(dataMap) == 0 { - return nil - } - return dataMap + return t.prevalueTracer.values() } // Reset drops the referenced root node and cleans all internal state. diff --git a/trie/verkle.go b/trie/verkle.go index 3ad638f0892..e00ea21602c 100644 --- a/trie/verkle.go +++ b/trie/verkle.go @@ -78,10 +78,6 @@ func (t *VerkleTrie) GetKey(key []byte) []byte { return key } -func (t *VerkleTrie) Owner() common.Hash { - panic("VerkleTrie does not have an owner") -} - // GetAccount implements state.Trie, retrieving the account with the specified // account address. If the specified account is not in the verkle tree, nil will // be returned. If the tree is corrupted, an error will be returned. From ca8c82edce93a1ef862a6938379d91cee963035b Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 20 Aug 2025 15:48:36 +0800 Subject: [PATCH 11/14] core, trie: polish --- core/stateless/database.go | 1 - core/stateless/stats.go | 8 ++++---- trie/trie.go | 10 ++-------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/core/stateless/database.go b/core/stateless/database.go index b2d3efb0b19..f54c123ddaa 100644 --- a/core/stateless/database.go +++ b/core/stateless/database.go @@ -63,6 +63,5 @@ func (w *Witness) MakeHashDB() ethdb.Database { rawdb.WriteLegacyTrieNode(memdb, common.BytesToHash(hash), blob) } - return memdb } diff --git a/core/stateless/stats.go b/core/stateless/stats.go index 088e06c9bb3..46022ac74bc 100644 --- a/core/stateless/stats.go +++ b/core/stateless/stats.go @@ -46,8 +46,8 @@ func newDepthStats() *depthStats { return &depthStats{minDepth: -1} } -// Add records a new depth sample. -func (d *depthStats) Add(n int64) { +// add records a new depth sample. +func (d *depthStats) add(n int64) { if n < 0 { return } @@ -92,11 +92,11 @@ func NewWitnessStats() *WitnessStats { func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) { if owner == (common.Hash{}) { for path := range maps.Keys(nodes) { - s.accountTrie.Add(int64(len(path))) + s.accountTrie.add(int64(len(path))) } } else { for path := range maps.Keys(nodes) { - s.storageTrie.Add(int64(len(path))) + s.storageTrie.add(int64(len(path))) } } } diff --git a/trie/trie.go b/trie/trie.go index fe8b5837d49..874bd4f5103 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -21,6 +21,7 @@ import ( "bytes" "errors" "fmt" + "slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -517,7 +518,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // always creates a new slice) instead of append to // avoid modifying n.Key since it might be shared with // other nodes. - return true, &shortNode{concat(n.Key, child.Key...), child.Val, t.newFlag()}, nil + return true, &shortNode{slices.Concat(n.Key, child.Key...), child.Val, t.newFlag()}, nil default: return true, &shortNode{n.Key, child, t.newFlag()}, nil } @@ -612,13 +613,6 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { } } -func concat(s1 []byte, s2 ...byte) []byte { - r := make([]byte, len(s1)+len(s2)) - copy(r, s1) - copy(r[len(s1):], s2) - return r -} - // copyNode deep-copies the supplied node along with its children recursively. func copyNode(n node) node { switch n := (n).(type) { From d273d817cb64fdaf4774540170847cff0faccea6 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 20 Aug 2025 22:48:11 +0800 Subject: [PATCH 12/14] Apply suggestion from @gballet Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- trie/trie.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trie/trie.go b/trie/trie.go index 874bd4f5103..98cf751f477 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -518,7 +518,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // always creates a new slice) instead of append to // avoid modifying n.Key since it might be shared with // other nodes. - return true, &shortNode{slices.Concat(n.Key, child.Key...), child.Val, t.newFlag()}, nil + return true, &shortNode{slices.Concat(n.Key, child.Key), child.Val, t.newFlag()}, nil default: return true, &shortNode{n.Key, child, t.newFlag()}, nil } From 94a0320741b893ecb244209b6ab4b0ea1d448649 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 22 Aug 2025 14:28:56 +0800 Subject: [PATCH 13/14] trie: rebase --- trie/tracer.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/trie/tracer.go b/trie/tracer.go index 3220910a19a..b0542404a7c 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -18,7 +18,6 @@ package trie import ( "maps" - "slices" "sync" ) @@ -147,18 +146,10 @@ func (t *prevalueTracer) hasList(list [][]byte) []bool { } // values returns a list of values of the cached trie nodes. -func (t *prevalueTracer) values() [][]byte { +func (t *prevalueTracer) values() map[string][]byte { t.lock.RLock() defer t.lock.RUnlock() - return slices.Collect(maps.Values(t.data)) -} - -func (t *prevalueTracer) keys() []string { - return slices.Collect(maps.Keys(t.data)) -} - -func (t *prevalueTracer) getMap() map[string][]byte { return maps.Clone(t.data) } From b07c9bb3d0ee0ecbdef9bf08b4ebc14945b8cfb5 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 22 Aug 2025 14:31:33 +0800 Subject: [PATCH 14/14] core/state: fix nil trie --- core/state/statedb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 688414e2980..6474d3a2fae 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -890,7 +890,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { s.witnessStats.Add(witness, obj.addrHash) } } else if obj.trie != nil { - witness := trie.Witness() + witness := obj.trie.Witness() s.witness.AddState(witness) if s.witnessStats != nil { s.witnessStats.Add(witness, obj.addrHash)