diff --git a/execution/stagedsync/exec3_parallel.go b/execution/stagedsync/exec3_parallel.go
index 6763b6a913c..d1ee79119ff 100644
--- a/execution/stagedsync/exec3_parallel.go
+++ b/execution/stagedsync/exec3_parallel.go
@@ -2895,7 +2895,17 @@ func (be *blockExecutor) nextResult(ctx context.Context, pe *parallelExecutor, r
}
// Mirror txtask.go's genesis rules-clobber so empty allocs (AuRa ZeroAddress) survive.
emptyRemoval := be.blockNum != 0 && pe.cfg.chainConfig.IsEIP161Enabled(be.blockNum)
- normWrites, normErr := rawWrites.Normalize(be.versionMap, txVersion.TxIndex, resultIncarnation, stateReader, domainStorageKeys, emptyRemoval, pe.cfg.chainConfig.Aura != nil, txTask.Rules().IsAmsterdam)
+ // Experiment: serve Normalize's fallback reads from what the worker
+ // already recorded, and assert the domain agrees on every one.
+ normReader := state.NewNormalizeReader(txVersion.TxIndex, be.blockIO.ReadSet(txVersion.TxIndex), be.versionMap, stateReader)
+ normWrites, normErr := rawWrites.Normalize(be.versionMap, txVersion.TxIndex, resultIncarnation, normReader, domainStorageKeys, emptyRemoval, pe.cfg.chainConfig.Aura != nil, txTask.Rules().IsAmsterdam)
+ if state.AssertNormalizeReadsEnabled() && normErr == nil && stateReader != nil {
+ domainWrites, domainErr := rawWrites.Normalize(be.versionMap, txVersion.TxIndex, resultIncarnation, stateReader, domainStorageKeys, emptyRemoval, pe.cfg.chainConfig.Aura != nil, txTask.Rules().IsAmsterdam)
+ if domainErr != nil {
+ return nil, fmt.Errorf("[parallel] normalize cross-check: %w", domainErr)
+ }
+ state.AssertNormalizeMatches(domainWrites, normWrites)
+ }
if domainKeysErr != nil {
return nil, fmt.Errorf("[parallel] iterate storage prefix for block write normalization: %w", domainKeysErr)
}
@@ -3170,7 +3180,8 @@ func (be *blockExecutor) nextResult(ctx context.Context, pe *parallelExecutor, r
}
emptyRemoval := be.blockNum != 0 && pe.cfg.chainConfig.IsEIP161Enabled(be.blockNum)
var normErr error
- finalizeWrites, normErr = writes.Normalize(be.versionMap, finalVersion.TxIndex, finalVersion.Incarnation, reader, domainStorageKeys, emptyRemoval, pe.cfg.chainConfig.Aura != nil, pe.cfg.chainConfig.IsAmsterdam(tt.Header.Time))
+ finalizeNormReader := state.NewNormalizeReader(finalVersion.TxIndex, be.blockIO.ReadSet(finalVersion.TxIndex), be.versionMap, reader)
+ finalizeWrites, normErr = writes.Normalize(be.versionMap, finalVersion.TxIndex, finalVersion.Incarnation, finalizeNormReader, domainStorageKeys, emptyRemoval, pe.cfg.chainConfig.Aura != nil, pe.cfg.chainConfig.IsAmsterdam(tt.Header.Time))
if domainKeysErr != nil {
return nil, fmt.Errorf("[parallel] finalize iterate storage prefix for block write normalization: %w", domainKeysErr)
}
diff --git a/execution/state/checked_reader.go b/execution/state/checked_reader.go
new file mode 100644
index 00000000000..7e2ccbbf343
--- /dev/null
+++ b/execution/state/checked_reader.go
@@ -0,0 +1,209 @@
+// Copyright 2024 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon 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.
+//
+// Erigon 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 Erigon. If not, see .
+
+package state
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/holiman/uint256"
+
+ "github.com/erigontech/erigon/common/dbg"
+ "github.com/erigontech/erigon/execution/types/accounts"
+)
+
+// assertNormalizeReads runs Normalize a second time against the domain reader
+// and asserts the two outputs match. Diagnostic only: it doubles Normalize.
+var assertNormalizeReads = dbg.EnvBool("NORMALIZE_ASSERT_READS", true)
+
+// AssertNormalizeReadsEnabled reports whether the output-level check is on.
+func AssertNormalizeReadsEnabled() bool { return assertNormalizeReads }
+
+// NewNormalizeReader returns the reader Normalize should use: the tx's own
+// recorded reads first, then the versionMap, then the domain.
+func NewNormalizeReader(txIndex int, reads ReadSet, versionMap *VersionMap, domain StateReader) StateReader {
+ if domain == nil { // nil means "no reader"; Normalize guards on it
+ return nil
+ }
+ return NewVersionedStateReader(txIndex, reads, versionMap, domain)
+}
+
+// CheckedStateReader answers from want and asserts that got agrees, panicking
+// on any disagreement. It exists to settle one question empirically: can
+// Normalize take the values a worker already recorded in its read set instead
+// of re-reading the domain on the apply loop? Every divergence is a case where
+// it cannot, and the panic names it.
+//
+// Diagnostic only, and not side-effect free: see NewNormalizeReader.
+type CheckedStateReader struct {
+ want StateReader // read set -> versionMap -> domain
+ got StateReader // domain
+}
+
+func NewCheckedStateReader(want, got StateReader) *CheckedStateReader {
+ return &CheckedStateReader{want: want, got: got}
+}
+
+func mismatch(op string, addr accounts.Address, detail string) {
+ panic(fmt.Sprintf("checked reader: %s disagrees for %x: %s", op, addr.Value(), detail))
+}
+
+func (r *CheckedStateReader) ReadAccountData(address accounts.Address) (*accounts.Account, error) {
+ want, err := r.want.ReadAccountData(address)
+ if err != nil {
+ mismatch("ReadAccountData", address, fmt.Sprintf("read-set path errored: %v", err))
+ }
+ got, gotErr := r.got.ReadAccountData(address)
+ if gotErr != nil {
+ mismatch("ReadAccountData", address, fmt.Sprintf("domain errored (%v) while read set answered", gotErr))
+ }
+ switch {
+ case want == nil && got == nil:
+ case want == nil || got == nil:
+ mismatch("ReadAccountData", address, fmt.Sprintf("presence differs: readset=%v domain=%v", want != nil, got != nil))
+ case want.Nonce != got.Nonce || !want.Balance.Eq(&got.Balance) ||
+ want.Incarnation != got.Incarnation || want.CodeHash.Value() != got.CodeHash.Value():
+ mismatch("ReadAccountData", address, fmt.Sprintf(
+ "readset={n:%d bal:%s inc:%d ch:%x} domain={n:%d bal:%s inc:%d ch:%x}",
+ want.Nonce, want.Balance.String(), want.Incarnation, want.CodeHash.Value(),
+ got.Nonce, got.Balance.String(), got.Incarnation, got.CodeHash.Value()))
+ }
+ return want, nil
+}
+
+func (r *CheckedStateReader) ReadAccountStorage(address accounts.Address, key accounts.StorageKey) (uint256.Int, bool, error) {
+ want, wantOK, err := r.want.ReadAccountStorage(address, key)
+ if err != nil {
+ mismatch("ReadAccountStorage", address, fmt.Sprintf("read-set path errored: %v", err))
+ }
+ got, gotOK, gotErr := r.got.ReadAccountStorage(address, key)
+ if gotErr != nil {
+ mismatch("ReadAccountStorage", address, fmt.Sprintf("domain errored (%v) while read set answered", gotErr))
+ }
+ // Absent and present-with-zero are the same slot to the no-op filter: it
+ // drops a zero write either way and keeps a non-zero one either way, so
+ // only the effective value has to agree. The read set reports found=true
+ // for a slot the worker read as zero; the domain reports it absent.
+ wantEff, gotEff := want, got
+ if !wantOK {
+ wantEff = uint256.Int{}
+ }
+ if !gotOK {
+ gotEff = uint256.Int{}
+ }
+ if !wantEff.Eq(&gotEff) {
+ mismatch("ReadAccountStorage", address, fmt.Sprintf(
+ "slot %x: readset={%s,found:%v} domain={%s,found:%v}",
+ key.Value(), want.String(), wantOK, got.String(), gotOK))
+ }
+ return want, wantOK, nil
+}
+
+func (r *CheckedStateReader) ReadAccountCode(address accounts.Address) ([]byte, error) {
+ want, err := r.want.ReadAccountCode(address)
+ if err != nil {
+ mismatch("ReadAccountCode", address, fmt.Sprintf("read-set path errored: %v", err))
+ }
+ got, gotErr := r.got.ReadAccountCode(address)
+ if gotErr != nil {
+ mismatch("ReadAccountCode", address, fmt.Sprintf("domain errored (%v) while read set answered", gotErr))
+ }
+ if !bytes.Equal(want, got) {
+ mismatch("ReadAccountCode", address, fmt.Sprintf("len readset=%d domain=%d", len(want), len(got)))
+ }
+ return want, nil
+}
+
+func (r *CheckedStateReader) ReadAccountCodeSize(address accounts.Address) (int, error) {
+ want, err := r.want.ReadAccountCodeSize(address)
+ if err != nil {
+ return 0, err
+ }
+ got, gotErr := r.got.ReadAccountCodeSize(address)
+ if gotErr != nil {
+ mismatch("ReadAccountCodeSize", address, fmt.Sprintf("domain errored (%v) while read set answered", gotErr))
+ }
+ if want != got {
+ mismatch("ReadAccountCodeSize", address, fmt.Sprintf("readset=%d domain=%d", want, got))
+ }
+ return want, nil
+}
+
+func (r *CheckedStateReader) HasStorage(address accounts.Address) (bool, error) {
+ want, err := r.want.HasStorage(address)
+ if err != nil {
+ return false, err
+ }
+ got, gotErr := r.got.HasStorage(address)
+ if gotErr != nil {
+ mismatch("HasStorage", address, fmt.Sprintf("domain errored (%v) while read set answered", gotErr))
+ }
+ if want != got {
+ mismatch("HasStorage", address, fmt.Sprintf("readset=%v domain=%v", want, got))
+ }
+ return want, nil
+}
+
+func (r *CheckedStateReader) ReadAccountIncarnation(address accounts.Address) (uint64, error) {
+ want, err := r.want.ReadAccountIncarnation(address)
+ if err != nil {
+ return 0, err
+ }
+ got, gotErr := r.got.ReadAccountIncarnation(address)
+ if gotErr != nil {
+ mismatch("ReadAccountIncarnation", address, fmt.Sprintf("domain errored (%v) while read set answered", gotErr))
+ }
+ if want != got {
+ mismatch("ReadAccountIncarnation", address, fmt.Sprintf("readset=%d domain=%d", want, got))
+ }
+ return want, nil
+}
+
+func (r *CheckedStateReader) ReadAccountDataForDebug(address accounts.Address) (*accounts.Account, error) {
+ return r.want.ReadAccountDataForDebug(address)
+}
+
+func (r *CheckedStateReader) SetTrace(trace bool, tracePrefix string) {
+ r.want.SetTrace(trace, tracePrefix)
+}
+
+func (r *CheckedStateReader) Trace() bool { return r.want.Trace() }
+
+func (r *CheckedStateReader) TracePrefix() string { return r.want.TracePrefix() }
+
+// AssertNormalizeMatches panics when two Normalize outputs differ. Compared at
+// the output rather than at each read: the apply-side reader fills the block
+// state cache on a miss, so reading it twice to cross-check is a mutation, not
+// an observation, and fails blocks that otherwise pass.
+func AssertNormalizeMatches(domain, readSet *WriteSet) {
+ if domain.Count() != readSet.Count() {
+ panic(fmt.Sprintf("normalize output differs: domain=%d readset=%d writes",
+ domain.Count(), readSet.Count()))
+ }
+ for h := range domain.AllHeaders() {
+ if !readSet.Has(h) {
+ panic(fmt.Sprintf("normalize output differs: read set lacks %x path=%v key=%x",
+ h.Address.Value(), h.Path, h.Key.Value()))
+ }
+ }
+ for h := range readSet.AllHeaders() {
+ if !domain.Has(h) {
+ panic(fmt.Sprintf("normalize output differs: domain lacks %x path=%v key=%x",
+ h.Address.Value(), h.Path, h.Key.Value()))
+ }
+ }
+}
diff --git a/execution/state/normalize_probe.go b/execution/state/normalize_probe.go
new file mode 100644
index 00000000000..0fa3c05d2f2
--- /dev/null
+++ b/execution/state/normalize_probe.go
@@ -0,0 +1,261 @@
+// Copyright 2024 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon 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.
+//
+// Erigon 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 Erigon. If not, see .
+
+package state
+
+import (
+ "fmt"
+ "runtime"
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+
+ "github.com/erigontech/erigon/common/dbg"
+ "github.com/erigontech/erigon/execution/types/accounts"
+)
+
+// Temporary instrumentation for the Normalize read-set experiment. It answers
+// one question: when the fill loop falls through to an account read, what did
+// the tx's own read set hold for that address, and what did the read return?
+var normalizeProbe = dbg.EnvBool("NORMALIZE_PROBE", false)
+
+// skipAbsentDomainRead turns the "recorded absent -> skip the domain" short
+// circuit off, so the A/B can be measured without editing code.
+var skipAbsentDomainRead = dbg.EnvBool("NORMALIZE_SKIP_ABSENT", true)
+
+type normProbeClass int
+
+const (
+ normProbeNotVersioned normProbeClass = iota // plain domain reader (blockgen / builder)
+ normProbeAddrWithVal // AddressPath entry carrying an account
+ normProbeAddrNilVal // AddressPath entry recorded header-only
+ normProbeNoAddrTouched // no AddressPath entry, other paths read
+ normProbeNoAddrCold // no read recorded for this address at all
+ normProbeClassCount
+)
+
+var normProbeClassName = [normProbeClassCount]string{
+ "notVersioned", "addrWithVal", "addrNilVal", "noAddrTouched", "noAddrCold",
+}
+
+var normProbe struct {
+ seen [normProbeClassCount]atomic.Uint64
+ gotAcc [normProbeClassCount]atomic.Uint64 // the read returned an account
+ gotNil [normProbeClassCount]atomic.Uint64 // the read returned nil
+ fromMap [normProbeClassCount]atomic.Uint64 // versionMap AddressPath answered
+ domainReads atomic.Uint64 // versioned reader reached the domain
+
+ stgFallbacks atomic.Uint64 // no-op filter reached ReadAccountStorage
+ stgSlotInReads atomic.Uint64 // the tx recorded a read of this exact slot
+ stgAddrInReads atomic.Uint64 // other slots of this address recorded, not this one
+ stgAddrCold atomic.Uint64 // no storage read recorded for this address
+ cacheWritten atomic.Uint64 // CachedReaderV3 hit an account written this block
+ cacheCommitted atomic.Uint64 // ... the decoded committed view
+ cacheMiss atomic.Uint64 // ... nothing cached, went to the domain
+
+ stgDomainReads atomic.Uint64 // versioned reader reached the domain for a slot
+ stgDomainFound atomic.Uint64 // ... and the slot existed
+ stgWriteIsZero atomic.Uint64 // the write being filtered is zero
+ stgFilteredOut atomic.Uint64 // the fallback concluded "no-op", write dropped
+ stgFilteredKeep atomic.Uint64 // the fallback kept the write
+}
+
+var normProbeCallers struct {
+ sync.Mutex
+ m map[string]uint64
+ addrs map[string]map[accounts.Address]uint64
+}
+
+// normProbeCaller attributes one domain read to its call site.
+func normProbeCaller(probeAddr accounts.Address) {
+ var pcs [8]uintptr
+ n := runtime.Callers(3, pcs[:])
+ frames := runtime.CallersFrames(pcs[:n])
+ var site string
+ for {
+ f, more := frames.Next()
+ if !strings.Contains(f.Function, "erigon/execution/state.") {
+ site = fmt.Sprintf("%s:%d", f.Function, f.Line)
+ break
+ }
+ if !more {
+ break
+ }
+ }
+ normProbeCallers.Lock()
+ if normProbeCallers.m == nil {
+ normProbeCallers.m = map[string]uint64{}
+ normProbeCallers.addrs = map[string]map[accounts.Address]uint64{}
+ }
+ normProbeCallers.m[site]++
+ if normProbeCallers.addrs[site] == nil {
+ normProbeCallers.addrs[site] = map[accounts.Address]uint64{}
+ }
+ normProbeCallers.addrs[site][probeAddr]++
+ normProbeCallers.Unlock()
+}
+
+func normProbeFallback(reader StateReader, addr accounts.Address) normProbeClass {
+ class := normProbeNotVersioned
+ if vr, ok := reader.(*versionedStateReader); ok {
+ switch {
+ case hasAddrRead(vr, addr, true):
+ class = normProbeAddrWithVal
+ case hasAddrRead(vr, addr, false):
+ class = normProbeAddrNilVal
+ case vr.reads.touched(addr):
+ class = normProbeNoAddrTouched
+ default:
+ class = normProbeNoAddrCold
+ }
+ if vr.versionMap != nil {
+ if _, res, ok := vr.versionMap.ReadAddress(addr, vr.txIndex); ok && res.Status() == MVReadResultDone {
+ normProbe.fromMap[class].Add(1)
+ }
+ }
+ }
+ normProbe.seen[class].Add(1)
+ return class
+}
+
+func hasAddrRead(vr *versionedStateReader, addr accounts.Address, withVal bool) bool {
+ tr, ok := vr.reads.GetAddress(addr)
+ if !ok {
+ return false
+ }
+ return withVal == (tr.Val != nil && !tr.Val.IsNil())
+}
+
+func normProbeResult(class normProbeClass, acc *accounts.Account) {
+ if acc != nil {
+ normProbe.gotAcc[class].Add(1)
+ return
+ }
+ normProbe.gotNil[class].Add(1)
+}
+
+func normProbeStorageFallback(reader StateReader, addr accounts.Address, key accounts.StorageKey, writeIsZero bool) {
+ normProbe.stgFallbacks.Add(1)
+ if writeIsZero {
+ normProbe.stgWriteIsZero.Add(1)
+ }
+ vr, ok := reader.(*versionedStateReader)
+ if !ok {
+ return
+ }
+ switch {
+ case hasSlotRead(vr, addr, key):
+ normProbe.stgSlotInReads.Add(1)
+ case len(vr.reads.storage[addr]) > 0:
+ normProbe.stgAddrInReads.Add(1)
+ default:
+ normProbe.stgAddrCold.Add(1)
+ }
+}
+
+func hasSlotRead(vr *versionedStateReader, addr accounts.Address, key accounts.StorageKey) bool {
+ _, ok := vr.reads.GetStorage(addr, key)
+ return ok
+}
+
+func normProbeStorageResult(dropped bool) {
+ if dropped {
+ normProbe.stgFilteredOut.Add(1)
+ return
+ }
+ normProbe.stgFilteredKeep.Add(1)
+}
+
+func (s *ReadSet) touched(addr accounts.Address) bool {
+ if _, ok := s.balance[addr]; ok {
+ return true
+ }
+ if _, ok := s.nonce[addr]; ok {
+ return true
+ }
+ if _, ok := s.incarnation[addr]; ok {
+ return true
+ }
+ if _, ok := s.codeHash[addr]; ok {
+ return true
+ }
+ if _, ok := s.code[addr]; ok {
+ return true
+ }
+ if _, ok := s.codeSize[addr]; ok {
+ return true
+ }
+ if _, ok := s.selfDestruct[addr]; ok {
+ return true
+ }
+ if _, ok := s.createContract[addr]; ok {
+ return true
+ }
+ _, ok := s.storage[addr]
+ return ok
+}
+
+// NormalizeProbeDump prints the counters. Called from a last-sorting test file.
+func NormalizeProbeDump(label string) {
+ var applyLoop uint64
+ for c := normProbeAddrWithVal; c < normProbeClassCount; c++ {
+ applyLoop += normProbe.seen[c].Load()
+ }
+ if applyLoop == 0 {
+ fmt.Printf("NORMALIZE_PROBE %s: no apply-loop fill-loop fallbacks\n", label)
+ return
+ }
+ fmt.Printf("NORMALIZE_PROBE %s: applyLoop=%d notVersioned=%d versionedReaderDomainReads=%d\n",
+ label, applyLoop, normProbe.seen[normProbeNotVersioned].Load(), normProbe.domainReads.Load())
+ for c := normProbeAddrWithVal; c < normProbeClassCount; c++ {
+ seen := normProbe.seen[c].Load()
+ fmt.Printf("NORMALIZE_PROBE %s: %-14s %6d (%5.1f%%) gotAccount=%d gotNil=%d viaVersionMap=%d\n",
+ label, normProbeClassName[c], seen, 100*float64(seen)/float64(applyLoop),
+ normProbe.gotAcc[c].Load(), normProbe.gotNil[c].Load(), normProbe.fromMap[c].Load())
+ }
+ normProbeCallers.Lock()
+ type kv struct {
+ k string
+ v uint64
+ }
+ var sites []kv
+ for k, v := range normProbeCallers.m {
+ sites = append(sites, kv{k, v})
+ }
+ normProbeCallers.Unlock()
+ sort.Slice(sites, func(i, j int) bool { return sites[i].v > sites[j].v })
+ for i, s := range sites {
+ if i == 8 {
+ break
+ }
+ fmt.Printf("NORMALIZE_PROBE %s: domainRead site %6d reads / %d distinct addrs %s\n",
+ label, s.v, len(normProbeCallers.addrs[s.k]), s.k)
+ }
+ fmt.Printf("NORMALIZE_PROBE %s: cachedReader written=%d committed=%d miss=%d\n",
+ label, normProbe.cacheWritten.Load(), normProbe.cacheCommitted.Load(), normProbe.cacheMiss.Load())
+ stg := normProbe.stgFallbacks.Load()
+ if stg == 0 {
+ return
+ }
+ spct := func(v uint64) string { return fmt.Sprintf("%d (%.1f%%)", v, 100*float64(v)/float64(stg)) }
+ fmt.Printf("NORMALIZE_PROBE %s: storageFallbacks=%d slotInReads=%s addrInReads=%s addrCold=%s writeIsZero=%s\n",
+ label, stg, spct(normProbe.stgSlotInReads.Load()), spct(normProbe.stgAddrInReads.Load()),
+ spct(normProbe.stgAddrCold.Load()), spct(normProbe.stgWriteIsZero.Load()))
+ fmt.Printf("NORMALIZE_PROBE %s: storageDomainReads=%d found=%d dropped=%d kept=%d\n",
+ label, normProbe.stgDomainReads.Load(), normProbe.stgDomainFound.Load(),
+ normProbe.stgFilteredOut.Load(), normProbe.stgFilteredKeep.Load())
+}
diff --git a/execution/state/rw_v3.go b/execution/state/rw_v3.go
index 53c35355a7e..90a05232b94 100644
--- a/execution/state/rw_v3.go
+++ b/execution/state/rw_v3.go
@@ -1156,15 +1156,21 @@ func (c *BlockStateCache) DeleteAccount(addr accounts.Address, txNum uint64) {
c.mu.Unlock()
}
+// writtenAccount returns the blob written this block, without the committed
+// fallback — callers that hold a decoded committed account skip the re-encode.
+func (c *BlockStateCache) writtenAccount(addr accounts.Address) ([]byte, bool) {
+ c.mu.RLock()
+ enc, ok := c.currentAccounts[addr]
+ c.mu.RUnlock()
+ return enc, ok
+}
+
// GetCurrentAccount returns the latest account blob (including intra-block writes).
// Falls back to committed state if no write exists. Returns (nil, false) if not cached.
func (c *BlockStateCache) GetCurrentAccount(addr accounts.Address) ([]byte, bool) {
- c.mu.RLock()
- if enc, ok := c.currentAccounts[addr]; ok {
- c.mu.RUnlock()
+ if enc, ok := c.writtenAccount(addr); ok {
return enc, true
}
- c.mu.RUnlock()
// The committed fallback runs after releasing mu, so the two reads are not one
// point-in-time snapshot. That is safe because committedAccounts is a
// write-once immutable pre-block view (a sync.Map for lock-free reads): a
@@ -1310,7 +1316,10 @@ func (r *CachedReaderV3) ReadAccountData(address accounts.Address) (*accounts.Ac
if r.blockCache != nil {
if r.readCurrent {
// Read from write buffer — sees accumulated per-TX writes.
- if enc, ok := r.blockCache.GetCurrentAccount(address); ok {
+ if enc, ok := r.blockCache.writtenAccount(address); ok {
+ if normalizeProbe {
+ normProbe.cacheWritten.Add(1)
+ }
if enc == nil {
return nil, nil
}
@@ -1320,6 +1329,18 @@ func (r *CachedReaderV3) ReadAccountData(address accounts.Address) (*accounts.Ac
}
return &acc, nil
}
+ // Unwritten this block, so the committed view is the current one;
+ // take it decoded rather than through a re-encode.
+ if acc, ok := r.blockCache.GetCommittedAccount(address); ok {
+ if normalizeProbe {
+ normProbe.cacheCommitted.Add(1)
+ }
+ if acc == nil {
+ return nil, nil
+ }
+ result := *acc
+ return &result, nil
+ }
} else {
// Read from committed cache — stable pre-block view.
if acc, ok := r.blockCache.GetCommittedAccount(address); ok {
@@ -1331,6 +1352,9 @@ func (r *CachedReaderV3) ReadAccountData(address accounts.Address) (*accounts.Ac
}
}
}
+ if normalizeProbe {
+ normProbe.cacheMiss.Add(1)
+ }
acc, err := r.ReaderV3.ReadAccountData(address)
if err != nil {
return nil, err
diff --git a/execution/state/versionedio.go b/execution/state/versionedio.go
index 2080c2d03ef..5c5812ed1a0 100644
--- a/execution/state/versionedio.go
+++ b/execution/state/versionedio.go
@@ -1589,7 +1589,8 @@ func (vr *versionedStateReader) TracePrefix() string {
}
func (vr *versionedStateReader) ReadAccountData(address accounts.Address) (*accounts.Account, error) {
- if r, ok := vr.reads.GetAddress(address); ok && r.Val != nil && !r.Val.IsNil() {
+ r, recorded := vr.reads.GetAddress(address)
+ if recorded && r.Val != nil && !r.Val.IsNil() {
account := r.Val.Account()
updated := vr.applyVersionedUpdates(address, *account)
return &updated, nil
@@ -1614,7 +1615,15 @@ func (vr *versionedStateReader) ReadAccountData(address accounts.Address) (*acco
}
}
- if vr.stateReader != nil {
+ // A recorded AddressPath read with no account is the tx's own conclusion that
+ // the address holds nothing: the header goes in first and is overwritten with
+ // the value as soon as a load finds one, so a header-only entry means the load
+ // came back empty. No point asking the domain again.
+ if vr.stateReader != nil && !(recorded && skipAbsentDomainRead) {
+ if normalizeProbe {
+ normProbe.domainReads.Add(1)
+ normProbeCaller(address)
+ }
account, err := vr.stateReader.ReadAccountData(address)
if err != nil {
@@ -1774,7 +1783,14 @@ func (vr versionedStateReader) ReadAccountStorage(address accounts.Address, key
}
if vr.stateReader != nil {
- return vr.stateReader.ReadAccountStorage(address, key)
+ if normalizeProbe {
+ normProbe.stgDomainReads.Add(1)
+ }
+ v, found, err := vr.stateReader.ReadAccountStorage(address, key)
+ if normalizeProbe && found {
+ normProbe.stgDomainFound.Add(1)
+ }
+ return v, found, err
}
return uint256.Int{}, false, nil
@@ -3077,3 +3093,40 @@ func (s *WriteSet) createdEmpty(addr accounts.Address) bool {
_, hasCodeSize := s.codeSize[addr]
return !hasCode && !hasIncarnation && !destroyed && !createdContract && !hasCodeSize && len(s.storage[addr]) == 0
}
+
+// accountFieldResolver lets Normalize fill one account field from a source
+// richer than the StateReader interface exposes. Optional: Normalize type
+// asserts for it and falls back to a whole-account domain read without it.
+type accountFieldResolver interface {
+ ResolveAccountField(out *WriteSet, addr accounts.Address, path AccountPath, ver Version) bool
+}
+
+// ResolveAccountField serves one field from what this tx already read, so the
+// fill loop doesn't fetch a whole account from the domain to recover it. Only
+// the requested path is answered: the read set records reads per path, and a
+// synthesised account with the unread fields zeroed would be wrong.
+func (vr *versionedStateReader) ResolveAccountField(out *WriteSet, addr accounts.Address, path AccountPath, ver Version) bool {
+ switch path {
+ case BalancePath:
+ if r, ok := vr.reads.GetBalance(addr); ok {
+ out.SetBalance(addr, &VersionedWrite[uint256.Int]{WriteHeader: WriteHeader{Address: addr, Path: BalancePath, Version: ver}, Val: r.Val})
+ return true
+ }
+ case NoncePath:
+ if r, ok := vr.reads.GetNonce(addr); ok {
+ out.SetNonce(addr, &VersionedWrite[uint64]{WriteHeader: WriteHeader{Address: addr, Path: NoncePath, Version: ver}, Val: r.Val})
+ return true
+ }
+ case IncarnationPath:
+ if r, ok := vr.reads.GetIncarnation(addr); ok {
+ out.SetIncarnation(addr, &VersionedWrite[uint64]{WriteHeader: WriteHeader{Address: addr, Path: IncarnationPath, Version: ver}, Val: r.Val})
+ return true
+ }
+ case CodeHashPath:
+ if r, ok := vr.reads.GetCodeHash(addr); ok {
+ out.SetCodeHash(addr, &VersionedWrite[accounts.CodeHash]{WriteHeader: WriteHeader{Address: addr, Path: CodeHashPath, Version: ver}, Val: r.Val})
+ return true
+ }
+ }
+ return false
+}
diff --git a/execution/state/writeset_normalize.go b/execution/state/writeset_normalize.go
index c42c137a429..3731670b5bf 100644
--- a/execution/state/writeset_normalize.go
+++ b/execution/state/writeset_normalize.go
@@ -305,16 +305,28 @@ func (writes *WriteSet) Normalize(vm *VersionMap, txIndex int, incarnation int,
continue
}
} else {
+ if normalizeProbe {
+ normProbeStorageFallback(stateReader, h.Address, h.Key, writeVal.IsZero())
+ }
preVal, found, err := stateReader.ReadAccountStorage(h.Address, h.Key)
if err != nil {
return nil, err
}
if !found && writeVal.IsZero() {
+ if normalizeProbe {
+ normProbeStorageResult(true)
+ }
continue
}
if found && writeVal.Eq(&preVal) {
+ if normalizeProbe {
+ normProbeStorageResult(true)
+ }
continue
}
+ if normalizeProbe {
+ normProbeStorageResult(false)
+ }
}
}
filtered.SetStorage(h.Address, h.Key, h)
@@ -385,13 +397,24 @@ func (writes *WriteSet) Normalize(vm *VersionMap, txIndex int, incarnation int,
if SetAccountFieldFromMap(filtered, vm, addr, path, ver, txIndex+1) {
continue
}
+ // Then what this tx already read, before paying for a domain read.
+ if r, ok := stateReader.(accountFieldResolver); ok && r.ResolveAccountField(filtered, addr, path, ver) {
+ continue
+ }
// Fall back to stateReader for pre-block account
if stateReader != nil {
if !fallbackLoaded {
+ var probeClass normProbeClass
+ if normalizeProbe {
+ probeClass = normProbeFallback(stateReader, addr)
+ }
acc, err := stateReader.ReadAccountData(addr)
if err != nil {
return nil, err
}
+ if normalizeProbe {
+ normProbeResult(probeClass, acc)
+ }
fallbackAcc = acc
fallbackLoaded = true
}
diff --git a/execution/state/zz_cache_read_bench_test.go b/execution/state/zz_cache_read_bench_test.go
new file mode 100644
index 00000000000..cccf588908d
--- /dev/null
+++ b/execution/state/zz_cache_read_bench_test.go
@@ -0,0 +1,67 @@
+package state
+
+import (
+ "testing"
+
+ "github.com/holiman/uint256"
+
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/crypto"
+ "github.com/erigontech/erigon/execution/types/accounts"
+)
+
+func benchAccount() *accounts.Account {
+ acc := accounts.NewAccount()
+ acc.Nonce = 42
+ acc.Balance = *uint256.NewInt(1e18)
+ acc.Incarnation = 1
+ acc.CodeHash = accounts.InternCodeHash(crypto.Keccak256Hash([]byte{0x60, 0x00}))
+ return &acc
+}
+
+// BenchmarkCachedReaderAccountRead prices one apply-loop account read that hits
+// the block state cache, on each of the cache's two paths.
+func BenchmarkCachedReaderAccountRead(b *testing.B) {
+ addr := accounts.InternAddress(common.HexToAddress("0xc0ffee"))
+ acc := benchAccount()
+
+ b.Run("committed", func(b *testing.B) {
+ cache := NewBlockStateCache()
+ cache.PutCommittedAccount(addr, acc)
+ r := NewCurrentCachedReaderV3(nil, cache)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ got, err := r.ReadAccountData(addr)
+ if err != nil || got == nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("current", func(b *testing.B) {
+ cache := NewBlockStateCache()
+ cache.WriteAccount(addr, accounts.SerialiseV3(acc), 1)
+ r := NewCurrentCachedReaderV3(nil, cache)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ got, err := r.ReadAccountData(addr)
+ if err != nil || got == nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ b.Run("codec_only", func(b *testing.B) {
+ enc := accounts.SerialiseV3(acc)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ var out accounts.Account
+ if err := accounts.DeserialiseV3(&out, enc); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
diff --git a/execution/tests/zzz_normalize_probe_test.go b/execution/tests/zzz_normalize_probe_test.go
new file mode 100644
index 00000000000..cf88b7a3f16
--- /dev/null
+++ b/execution/tests/zzz_normalize_probe_test.go
@@ -0,0 +1,11 @@
+package executiontests
+
+import (
+ "testing"
+
+ "github.com/erigontech/erigon/execution/state"
+)
+
+func TestZZZNormalizeProbeDump(t *testing.T) {
+ state.NormalizeProbeDump("execution/tests")
+}
diff --git a/rpc/jsonrpc/zzz_normalize_probe_test.go b/rpc/jsonrpc/zzz_normalize_probe_test.go
new file mode 100644
index 00000000000..0ab10e4600b
--- /dev/null
+++ b/rpc/jsonrpc/zzz_normalize_probe_test.go
@@ -0,0 +1,11 @@
+package jsonrpc
+
+import (
+ "testing"
+
+ "github.com/erigontech/erigon/execution/state"
+)
+
+func TestZZZNormalizeProbeDump(t *testing.T) {
+ state.NormalizeProbeDump("rpc/jsonrpc")
+}