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") +}