core, core/state: instrument BAL slow-block metrics

Populates per-block state read/write counts in slow-block JSON for BAL
blocks (which #34892 left as TBD), and adds reader-level read timing.
Builds on top of bal-devnet-3 — most of the PR's earlier slow-block log
infrastructure was adapted into upstream by that commit, so this change
is now scoped to the metric population that the BAL alone can derive.

- BAL helpers: BlockAccessList.{UniqueAccountCount, UniqueStorageSlotCount,
  WrittenCounts}. WrittenCounts walks the BAL once and returns the
  block-aggregate write counts.

- Reader-level read timing: *reader times all synchronous Account/Storage/
  Code/CodeSize calls via atomic counters; exposed via ReadTimes()
  ReadDurations and the new state.ReadTimer interface. Replaces StateDB-
  level AccountReads/StorageReads/CodeReads tracking (the StateDB shouldn't
  time its dependencies — the reader is where the I/O happens).

- Reader-level code-load dedup: *reader.codeLoaded sync.Map records the
  first-seen byte length per address; CodeLoads() returns (count, bytes).
  Exposed via state.CodeLoadTracker. Replaces StateDB CodeLoaded/
  CodeLoadBytes tracking and the SnapshotCodeLoads aggregation pattern.

- BALStateTransition: caches BlockAccessList.WrittenCounts() once at
  construction; tracks accountDeleted/storageDeleted atomics for the
  parallel root-pass (the BAL alone can't distinguish a selfdestruct from
  a balance/nonce reset). Exposes Deletions() DeletionCounts. Drops the
  older accountUpdated/storageUpdated/codeUpdated/codeUpdateBytes counters
  (now derived from WrittenCounts).

- BAL block stats path (blockchain.go): populates StateCounts directly —
  AccountUpdated = WrittenCounts.Accounts - Deletions.Accounts (same for
  storage). AccountLoaded/StorageLoaded come from BAL. CodeLoaded/
  CodeLoadBytes come from the shared *reader (deduplicated across phase
  StateDBs naturally because they share one reader instance).

- Non-BAL block stats path: read durations come from the reader; counts
  from StateDB fields. StorageUpdated/StorageDeleted unified to int width.

- Hard type assertions: state.ReadTimer / state.CodeLoadTracker /
  state.ReaderStater consumers use direct casts (no silent zero
  fallback) — every Reader chain in production satisfies these
  interfaces.

- Meter alignment: account/storage Updated meters subtract Deletions to
  avoid double-reporting blocks under both Update and Delete dashboards.
This commit is contained in:
CPerezz 2026-05-07 00:12:51 +02:00
parent 2e771eaacf
commit 3f5e27e7b0
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
11 changed files with 277 additions and 98 deletions

View file

@ -656,6 +656,20 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block *
writeTime := time.Since(writeStart)
var stats ExecuteStats
wc := stateTransition.WrittenCounts()
d := stateTransition.Deletions()
codeLoaded, codeLoadBytes := prefetchReader.(state.CodeLoadTracker).CodeLoads()
stats.AccountLoaded = al.UniqueAccountCount()
stats.AccountUpdated = wc.Accounts - d.Accounts
stats.AccountDeleted = d.Accounts
stats.StorageLoaded = al.UniqueStorageSlotCount()
stats.StorageUpdated = wc.StorageSlots - d.Storage
stats.StorageDeleted = d.Storage
stats.CodeLoaded = codeLoaded
stats.CodeLoadBytes = codeLoadBytes
stats.CodeUpdated = wc.Codes
stats.CodeUpdateBytes = wc.CodeBytes
stats.ExecWall = res.ExecTime
stats.PostProcess = res.PostProcessTime
@ -666,12 +680,13 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block *
stats.DatabaseCommit = m.TrieDBCommits
stats.Prefetch = m.StatePrefetch
}
readerReads := prefetchReader.(state.ReadTimer).ReadTimes()
stats.AccountReads = readerReads.Account
stats.StorageReads = readerReads.Storage
stats.CodeReads = readerReads.Code
stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed
if r, ok := prefetchReader.(state.ReaderStater); ok {
stats.StateReadCacheStats = r.GetStats()
}
stats.StateReadCacheStats = prefetchReader.(state.ReaderStater).GetStats()
elapsed := time.Since(startTime) + 1 // prevent zero division
stats.TotalTime = elapsed
@ -2435,13 +2450,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
proctime = time.Since(startTime) // processing + validation + cross validation
stats = &ExecuteStats{}
)
// Update the metrics touched during block processing and validation
stats.AccountReads = statedb.AccountReads // Account reads are complete(in processing)
stats.StorageReads = statedb.StorageReads // Storage reads are complete(in processing)
stats.AccountUpdates = statedb.AccountUpdates // Account updates are complete(in validation)
stats.StorageUpdates = statedb.StorageUpdates // Storage updates are complete(in validation)
stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation)
stats.CodeReads = statedb.CodeReads
reads := statedb.Reader().(state.ReadTimer).ReadTimes()
codeLoaded, codeLoadBytes := statedb.Reader().(state.CodeLoadTracker).CodeLoads()
stats.AccountReads = reads.Account
stats.StorageReads = reads.Storage
stats.CodeReads = reads.Code
stats.AccountUpdates = statedb.AccountUpdates
stats.StorageUpdates = statedb.StorageUpdates
stats.AccountHashes = statedb.AccountHashes
stats.AccountLoaded = statedb.AccountLoaded
stats.AccountUpdated = statedb.AccountUpdated
@ -2449,15 +2465,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.StorageLoaded = statedb.StorageLoaded
stats.StorageUpdated = int(statedb.StorageUpdated.Load())
stats.StorageDeleted = int(statedb.StorageDeleted.Load())
stats.CodeLoaded = statedb.CodeLoaded
stats.CodeLoadBytes = statedb.CodeLoadBytes
stats.CodeLoaded = codeLoaded
stats.CodeLoadBytes = codeLoadBytes
stats.CodeUpdated = statedb.CodeUpdated
stats.CodeUpdateBytes = statedb.CodeUpdateBytes
stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation
stats.CrossValidation = xvtime // The time spent on stateless cross validation
stats.Execution = ptime - (reads.Account + reads.Storage + reads.Code)
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates)
stats.CrossValidation = xvtime
// Write the block to the chain and get the status.
var status WriteStatus

View file

@ -38,17 +38,16 @@ type ExecuteStats struct {
StorageCommits time.Duration // Time spent on the storage trie commit
CodeReads time.Duration // Time spent on the contract code read
// TODO: code bytes loaded
AccountLoaded int // Number of accounts loaded
AccountUpdated int // Number of accounts updated
AccountDeleted int // Number of accounts deleted
StorageLoaded int // Number of storage slots loaded
StorageUpdated int // Number of storage slots updated
StorageDeleted int // Number of storage slots deleted
CodeLoaded int // Number of contract code loaded
CodeLoadBytes int // Number of bytes read from contract code
CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702)
CodeUpdateBytes int // Total bytes of code written
AccountLoaded int
AccountUpdated int
AccountDeleted int
StorageLoaded int
StorageUpdated int
StorageDeleted int
CodeLoaded int
CodeLoadBytes int
CodeUpdated int
CodeUpdateBytes int
Execution time.Duration // Time spent on the EVM execution
Validation time.Duration // Time spent on the block validation
@ -304,8 +303,8 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat
}
func (s *ExecuteStats) reportBALMetrics() {
accountCommitTimer.Update(s.AccountCommits) // Account commits are complete, we can mark them
storageCommitTimer.Update(s.StorageCommits) // Storage commits are complete, we can mark them
accountCommitTimer.Update(s.AccountCommits)
storageCommitTimer.Update(s.StorageCommits)
if m := s.balTransitionStats; m != nil {
stateTriePrefetchTimer.Update(m.StatePrefetch)

View file

@ -14,16 +14,13 @@ import (
"golang.org/x/sync/errgroup"
)
// ProcessResultWithMetrics wraps ProcessResult with some metrics that are
// emitted when executing blocks containing access lists.
// ProcessResultWithMetrics wraps ProcessResult with timing breakdown for BAL block processing.
type ProcessResultWithMetrics struct {
ProcessResult *ProcessResult
PreProcessTime time.Duration
StateTransitionMetrics *state.BALStateTransitionMetrics
// the time it took to execute all txs in the block
ExecTime time.Duration
PostProcessTime time.Duration
// TODO: have the prefetch metric in here as well?
ExecTime time.Duration
PostProcessTime time.Duration
}
// ParallelStateProcessor is used to execute and verify blocks containing

View file

@ -3,6 +3,7 @@ package state
import (
"maps"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
@ -15,30 +16,28 @@ import (
"golang.org/x/sync/errgroup"
)
// BALStateTransition is responsible for performing the state root update
// and commit for EIP 7928 access-list-containing blocks. An instance of
// this object is only used for a single block.
// BALStateTransition performs the state root update and commit for EIP-7928
// access-list-containing blocks. One instance per block.
type BALStateTransition struct {
accessList bal.AccessListReader
written bal.WrittenCounts
db Database
reader Reader
stateTrie Trie
parentRoot common.Hash
// the computed state root of the block
rootHash common.Hash
// the state modifications performed by the block
diffs bal.StateMutations
// a map of common.Address -> *types.StateAccount containing the block
// prestate of all accounts that will be modified
prestates sync.Map
diffs bal.StateMutations
prestates sync.Map
postStates map[common.Address]*types.StateAccount
// a map of common.Address -> Trie containing the account tries for all
// accounts with mutated storage
tries sync.Map //map[common.Address]Trie
deletions map[common.Address]struct{}
tries sync.Map
deletions map[common.Address]struct{}
// Deletion counters; not derivable from the BAL alone (selfdestruct vs
// balance/nonce reset is indistinguishable without prestate).
accountDeleted int
storageDeleted atomic.Int64
stateUpdate *stateUpdate
@ -52,6 +51,19 @@ func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics {
return &s.metrics
}
// DeletionCounts holds per-block deletion counters from the parallel root-pass.
type DeletionCounts struct {
Accounts int
Storage int
}
func (s *BALStateTransition) Deletions() DeletionCounts {
return DeletionCounts{
Accounts: s.accountDeleted,
Storage: int(s.storageDeleted.Load()),
}
}
type BALStateTransitionMetrics struct {
// trie hashing metrics
AccountUpdate time.Duration
@ -75,6 +87,7 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas
return &BALStateTransition{
accessList: bal.NewAccessListReader(*block.AccessList()),
written: block.AccessList().WrittenCounts(),
db: db,
reader: prefetchReader,
stateTrie: stateTrie,
@ -90,6 +103,11 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas
}, nil
}
// WrittenCounts returns the cached BAL write counts (computed once per block).
func (s *BALStateTransition) WrittenCounts() bal.WrittenCounts {
return s.written
}
func (s *BALStateTransition) Error() error {
return s.err
}
@ -334,15 +352,11 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b
return common.Hash{}, nil, err
}
/*
TODO: derive these from the BAL
^ I think even then, there is a semantic difference with how these metrics were calculated previously
I don't know if it makes sense to recompute those, or just derive new ones from the BAL
accountUpdatedMeter.Mark(int64(s.accountUpdated))
storageUpdatedMeter.Mark(s.storageUpdated.Load())
accountDeletedMeter.Mark(int64(s.accountDeleted))
storageDeletedMeter.Mark(s.storageDeleted.Load())
*/
storageDeleted := s.storageDeleted.Load()
accountUpdatedMeter.Mark(int64(s.written.Accounts - s.accountDeleted))
storageUpdatedMeter.Mark(int64(s.written.StorageSlots) - storageDeleted)
accountDeletedMeter.Mark(int64(s.accountDeleted))
storageDeletedMeter.Mark(storageDeleted)
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
@ -477,6 +491,7 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash {
return common.Hash{}
}
s.deletions[mutatedAddr] = struct{}{}
s.accountDeleted++
} else {
acct, code := s.updateAccount(mutatedAddr)

View file

@ -18,6 +18,10 @@ package state
import (
"errors"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/overlay"
"github.com/ethereum/go-ethereum/core/types"
@ -28,8 +32,6 @@ import (
"github.com/ethereum/go-ethereum/trie/transitiontrie"
"github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/database"
"sync"
"sync/atomic"
)
// ContractCodeReader defines the interface for accessing contract code.
@ -525,14 +527,18 @@ func (r *stateReaderWithStats) GetStateStats() StateReaderStats {
}
}
// reader aggregates a code reader and a state reader into a single object.
type reader struct {
ContractCodeReader
StateReader
PrefetcherMetricer
accountReadNS atomic.Int64
storageReadNS atomic.Int64
codeReadNS atomic.Int64
codeLoaded sync.Map // common.Address → int (first-seen len(code))
}
// newReader constructs a reader with the supplied code reader and state reader.
func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader {
return &reader{
ContractCodeReader: codeReader,
@ -548,6 +554,53 @@ func newReaderWithPrefetch(codeReader ContractCodeReader, stateReader StateReade
}
}
func (r *reader) Account(addr common.Address) (*types.StateAccount, error) {
defer func(start time.Time) { r.accountReadNS.Add(int64(time.Since(start))) }(time.Now())
return r.StateReader.Account(addr)
}
func (r *reader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
defer func(start time.Time) { r.storageReadNS.Add(int64(time.Since(start))) }(time.Now())
return r.StateReader.Storage(addr, slot)
}
func (r *reader) Code(addr common.Address, codeHash common.Hash) []byte {
defer func(start time.Time) { r.codeReadNS.Add(int64(time.Since(start))) }(time.Now())
code := r.ContractCodeReader.Code(addr, codeHash)
if len(code) > 0 {
r.codeLoaded.LoadOrStore(addr, len(code))
}
return code
}
func (r *reader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) {
defer func(start time.Time) { r.codeReadNS.Add(int64(time.Since(start))) }(time.Now())
size, err := r.ContractCodeReader.CodeSize(addr, codeHash)
if err == nil && size > 0 {
r.codeLoaded.LoadOrStore(addr, size)
}
return size, err
}
func (r *reader) ReadTimes() ReadDurations {
return ReadDurations{
Account: time.Duration(r.accountReadNS.Load()),
Storage: time.Duration(r.storageReadNS.Load()),
Code: time.Duration(r.codeReadNS.Load()),
}
}
// CodeLoads returns the count of unique contracts whose code was fetched and
// the sum of their first-seen byte lengths. Call after Reader use has quiesced.
func (r *reader) CodeLoads() (count, bytes int) {
r.codeLoaded.Range(func(_, v any) bool {
count++
bytes += v.(int)
return true
})
return
}
// GetCodeStats returns the statistics of code access.
func (r *reader) GetCodeStats() ContractCodeReaderStats {
if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok {
@ -571,3 +624,20 @@ func (r *reader) GetStats() ReaderStats {
StateStats: r.GetStateStats(),
}
}
// PrefetchReadTimes forwards to the wrapped prefetcher, or returns zero.
func (r *reader) PrefetchReadTimes() (account, storage time.Duration) {
if pr, ok := r.StateReader.(interface {
PrefetchReadTimes() (time.Duration, time.Duration)
}); ok {
return pr.PrefetchReadTimes()
}
return 0, 0
}
// WaitPrefetch blocks until the wrapped prefetcher drains; no-op otherwise.
func (r *reader) WaitPrefetch() {
if pr, ok := r.StateReader.(interface{ Wait() error }); ok {
_ = pr.Wait()
}
}

View file

@ -382,3 +382,21 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) {
}
list[slot] = struct{}{}
}
func (r *readerTracker) ReadTimes() ReadDurations {
return r.Reader.(ReadTimer).ReadTimes()
}
func (r *readerTracker) CodeLoads() (count, bytes int) {
return r.Reader.(CodeLoadTracker).CodeLoads()
}
// GetStateStats forwards stats from the wrapped reader; without this, BAL
// blocks would emit zero cache hit/miss counts.
func (r *prefetchStateReader) GetStateStats() StateReaderStats {
if stater, ok := r.StateReader.(StateReaderStater); ok {
return stater.GetStateStats()
}
return StateReaderStats{}
}

View file

@ -263,3 +263,31 @@ func TestTrackerSurvivesStateDBCache(t *testing.T) {
t.Fatal("slot must be tracked on cache hit (storage)")
}
}
// TestPrefetchStateReaderForwardsStats locks down that prefetchStateReader
// exposes the underlying stateReaderWithStats counters via GetStateStats.
func TestPrefetchStateReaderForwardsStats(t *testing.T) {
stub := newRefStateReader()
addr := testrand.Address()
cached := newStateReaderWithCache(stub)
withStats := newStateReaderWithStats(cached)
prefetch := newPrefetchStateReaderInternal(withStats, nil, 1)
if _, err := prefetch.Account(addr); err != nil {
t.Fatalf("Account: %v", err)
}
if _, err := prefetch.Account(addr); err != nil {
t.Fatalf("Account (second): %v", err)
}
stats := withStats.GetStateStats()
if stats.AccountCacheHit == 0 || stats.AccountCacheMiss == 0 {
t.Fatalf("inner stats not populated: %+v", stats)
}
gotStats := prefetch.GetStateStats()
if gotStats != stats {
t.Fatalf("forward mismatch: got %+v, want %+v", gotStats, stats)
}
}

View file

@ -16,6 +16,25 @@
package state
import "time"
// ReadDurations groups cumulative read durations by category.
type ReadDurations struct {
Account time.Duration
Storage time.Duration
Code time.Duration
}
// ReadTimer exposes a Reader's cumulative read durations.
type ReadTimer interface {
ReadTimes() ReadDurations
}
// CodeLoadTracker exposes a Reader's deduplicated code-load count and bytes.
type CodeLoadTracker interface {
CodeLoads() (count, bytes int)
}
// ContractCodeReaderStats aggregates statistics for the contract code reader.
type ContractCodeReaderStats struct {
CacheHit int64 // Number of cache hits

View file

@ -21,7 +21,6 @@ import (
"fmt"
"maps"
"slices"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
@ -226,13 +225,11 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
}
s.db.StorageLoaded++
start := time.Now()
value, err := s.db.reader.Storage(s.address, key)
if err != nil {
s.db.setError(err)
return common.Hash{}
}
s.db.StorageReads += time.Since(start)
// Schedule the resolved storage slots for prefetching if it's enabled.
if s.db.prefetcher != nil && s.data.Root != types.EmptyRootHash {
@ -644,12 +641,6 @@ func (s *stateObject) Code() []byte {
if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) {
return nil
}
defer func(start time.Time) {
s.db.CodeLoaded += 1
s.db.CodeReads += time.Since(start)
s.db.CodeLoadBytes += len(s.code)
}(time.Now())
code := s.db.reader.Code(s.address, common.BytesToHash(s.CodeHash()))
if len(code) == 0 {
s.db.setError(fmt.Errorf("code is not found %x", s.CodeHash()))
@ -668,11 +659,6 @@ func (s *stateObject) CodeSize() int {
if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) {
return 0
}
defer func(start time.Time) {
s.db.CodeLoaded += 1
s.db.CodeReads += time.Since(start)
}(time.Now())
size, err := s.db.reader.CodeSize(s.address, common.BytesToHash(s.CodeHash()))
if err != nil {
s.db.setError(err)

View file

@ -156,33 +156,24 @@ type StateDB struct {
// State witness if cross validation is needed
witness *stateless.Witness
// Measurements gathered during execution for debugging purposes
AccountReads time.Duration
// Per-block counters surfaced in ExecuteStats; read durations and code-loads
// are tracked on the reader (see ReadTimer, CodeLoadTracker).
AccountHashes time.Duration
AccountUpdates time.Duration
AccountCommits time.Duration
StorageReads time.Duration
StorageUpdates time.Duration
StorageCommits time.Duration
DatabaseCommits time.Duration
CodeReads time.Duration
AccountLoaded int // Number of accounts retrieved from the database during the state transition
AccountUpdated int // Number of accounts updated during the state transition
AccountDeleted int // Number of accounts deleted during the state transition
StorageLoaded int // Number of storage slots retrieved from the database during the state transition
StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition
StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition
// CodeLoadBytes is the total number of bytes read from contract code.
// This value may be smaller than the actual number of bytes read, since
// some APIs (e.g. CodeSize) may load the entire code from either the
// cache or the database when the size is not available in the cache.
CodeLoaded int // Number of contract code loaded during the state transition
CodeLoadBytes int // Total bytes of resolved code
CodeUpdated int // Number of contracts with code changes that persisted
CodeUpdateBytes int // Total bytes of persisted code written
AccountLoaded int
AccountUpdated int
AccountDeleted int
StorageLoaded int
StorageUpdated atomic.Int64
StorageDeleted atomic.Int64
CodeUpdated int
CodeUpdateBytes int
}
// New creates a new state from a given trie.
@ -635,13 +626,11 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
s.AccountLoaded++
start := time.Now()
acct, err := s.reader.Account(addr)
if err != nil {
s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err))
return nil
}
s.AccountReads += time.Since(start)
// Short circuit if the account is not found
if acct == nil {

View file

@ -44,6 +44,49 @@ import (
// BlockAccessList is the encoding format of AccessListBuilder.
type BlockAccessList []AccountAccess
// UniqueAccountCount returns the number of distinct account addresses in
// the block access list.
func (e BlockAccessList) UniqueAccountCount() int {
return len(e)
}
// UniqueStorageSlotCount returns the total number of distinct (address, slot)
// pairs accessed across all accounts. Reads and writes are disjoint per
// account by spec validation, so we can sum them directly.
func (e BlockAccessList) UniqueStorageSlotCount() int {
var n int
for i := range e {
n += len(e[i].StorageReads) + len(e[i].StorageChanges)
}
return n
}
// WrittenCounts groups per-block aggregate write counts derived from the BAL.
type WrittenCounts struct {
Accounts int
StorageSlots int
Codes int
CodeBytes int
}
// WrittenCounts walks the BAL once and returns the aggregate write counts.
func (e BlockAccessList) WrittenCounts() WrittenCounts {
var w WrittenCounts
for i := range e {
a := &e[i]
if len(a.StorageChanges) > 0 || len(a.BalanceChanges) > 0 ||
len(a.NonceChanges) > 0 || len(a.CodeChanges) > 0 {
w.Accounts++
}
w.StorageSlots += len(a.StorageChanges)
if n := len(a.CodeChanges); n > 0 {
w.Codes++
w.CodeBytes += len(a.CodeChanges[n-1].Code)
}
}
return w
}
func (e BlockAccessList) EncodeRLP(_w io.Writer) error {
w := rlp.NewEncoderBuffer(_w)
l := w.List()