Skip to content

Alchemy - Code Fix - #11714

Merged
svlachakis merged 8 commits into
masterfrom
alchemy-code-fix
May 22, 2026
Merged

Alchemy - Code Fix#11714
svlachakis merged 8 commits into
masterfrom
alchemy-code-fix

Conversation

@svlachakis

Copy link
Copy Markdown
Contributor

Moved the persisted-code hint cache from StateProvider onto the ICodeDb itself, where the underlying storage actually lives.

Before: StateProvider._persistedCodeInsertFilter was a single long-lived in-memory cache that recorded "this code hash has been flushed to disk". It was set after every CommitCodeAsync, regardless of whether that commit went to durable production storage or a transient overlay storage.

After: the hint cache lives on the codeDb instance via ICodeDb.ContainsCode / MarkCodePersisted. Two implementations:

  • Production codeDb (isPersistent: true) — keeps the hint cache, gets the optimization for normal block sync where factory contracts redeploy popular bytecode.
  • Overlay codeDb (isPersistent: false, used by debug_traceCall / eth_call / state overrides) — hint cache is null. ContainsCode always returns false, MarkCodePersisted is a no-op. Overlay writes can no longer poison anything.

The bug:

  1. debug_traceCall runs through a scoped overlay — a temporary in-memory layer wrapping the production codeDb. Writes during the trace land in the overlay's temp buffer.
  2. On commit, CommitCodeAsync writes the just-deployed bytecode into the overlay and then calls _persistedCodeInsertFilter.Set(codeHash) — marking it "persisted."
  3. The trace ends. The overlay's temp buffer is discarded (ClearTempChanges). Production codeDb never received those bytes.
  4. _persistedCodeInsertFilter lived on StateProvider, outliving the overlay. So the next trace on the same pooled GethStyleTracer instance saw a "persisted" hint for code that does not exist in production.
  5. InsertCode consulted the hint, said "already there, skip", and didn't put the bytes into _codeBatch. The account's codeHash was still set to 0xb44f…ea55.
  6. ProcessDiffState then called GetCode(addr) → codeHash 0xb44f… → not in _codeBatch, not in overlay, not in production → throw.

The root issue was a lifetime mismatch: the hint cache's lifetime (long-lived on StateProvider) didn't match the lifetime of the storage it claimed to describe (short-lived overlay).

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

Added regression test which fails on master.

@svlachakis

Copy link
Copy Markdown
Contributor Author

@stdevMac needs to be cherry picked in 1.38.0

@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @svlachakis's task in 6m 30s —— View job


Code Review

  • Gather context and understand the request
  • Read coding standards and rules
  • Read all changed files
  • Analyze for correctness, edge cases, security, and performance
  • Post inline comments for specific issues
  • Post final review summary

Summary

The root-cause analysis is accurate and the fix is well-targeted. The bug was a lifetime mismatch: the persisted-code hint cache lived on the long-lived StateProvider but was invalidated by short-lived overlay scopes (used by debug_traceCall / eth_call). Moving the cache onto the ICodeDb instance ties its lifetime to the actual storage it describes.

Verdict: Approve with minor comments. No correctness regressions found. The three findings below are low-to-medium; none block merge.


Findings

Medium — IWorldStateScopeProvider.ICodeDb is a public interface (breaking change)

Adding ContainsCode and MarkCodePersisted to the ICodeDb nested interface is technically a breaking change for any external code that implements it, even though the PR is labelled "non-breaking bugfix". Within the Nethermind codebase there is only one concrete implementation (KeyValueWithBatchingBackedCodeDb), so there are no in-tree breakages. However, any plugin or library targeting Nethermind.Evm that implements ICodeDb will fail to compile after this change.

Adding default implementations with the overlay semantics (always-false / no-op) would eliminate the break entirely and also self-document that the safe default is the overlay behaviour:

bool ContainsCode(in ValueHash256 codeHash) => false;
void MarkCodePersisted(in ValueHash256 codeHash) { }

See inline comment on IWorldStateScopeProvider.cs:92.

Low — OverridableWorldStateManager silently relies on codeDbIsPersistent = false default

OverridableWorldStateManager (not modified by this PR) creates:

WorldState = new TrieStoreScopeProvider(overlayTrieStore, readOnlyDbProvider.CodeDb, logManager);

This is one of the affected overlay paths — it is correct precisely because the new default is false. But it is silent about its intent. Consider adding codeDbIsPersistent: false explicitly (as a follow-up commit) so future readers know this is a deliberate overlay path, not an oversight. Fix this →

Low — _persistedHint cache size comment

The new _persistedHint = new AssociativeKeyCache<ValueHash256>(1_024) uses 1 024 entries but there is no comment explaining the sizing. The _blockCodeInsertFilter comment explains its 256-entry limit ("guarding hot duplicates within the current block"). The 1 024 entry cross-block hint would benefit from similar justification. See inline comment on TrieStoreScopeProvider.cs:278.


Correctness analysis

Scenario Before After
debug_traceCall deploys code → overlay reset → second call same code Throws Code 0x… missing ✓ Returns code
Production block processing: same factory bytecode deployed many blocks Skips DB write (cross-block hint) ✓ Same — _persistedHint on the durable codeDb ✓
Same code deployed twice in same block Skips second _codeBatch insert ✓ Same — _blockCodeInsertFilter
WorldStateManager.CreateResettableWorldState() (read-only) N/A codeDbIsPersistent: false default → no hint cache ✓
HealingWorldStateScopeProvider Was using old single filter codeDbIsPersistent: true passed explicitly ✓

Thread safety: AssociativeKeyCache uses lock-free seqlock reads and Volatile.Write for mutations — it is safe for concurrent access from the main thread (ContainsCode) and the Task.Run background thread (MarkCodePersisted).

_codeDb null guard in InsertCode: _codeDb?.ContainsCode(codeHash) ?? false is fail-open (false-negative), so code is redundantly re-inserted rather than silently skipped if _codeDb is null — correct direction.

Regression tests: Both KeyValueWithBatchingBackedCodeDb_ContainsCode_respects_isPersistent_flag (unit) and Same_code_can_be_redeployed_across_overlay_resets (integration) directly exercise the failure path. The integration test correctly relies on worldState.BeginScope dispose calling Reset() (which clears _blockCodeInsertFilter) between the two overlay passes.

Comment thread src/Nethermind/Nethermind.Evm/State/IWorldStateScopeProvider.cs
Comment thread src/Nethermind/Nethermind.State/TrieStoreScopeProvider.cs
Comment thread src/Nethermind/Nethermind.State.Test/StateProviderTests.cs
@svlachakis

Copy link
Copy Markdown
Contributor Author

@claude review again

@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @svlachakis's task in 3m 24s —— View job


Code Review

  • Gather context and understand the request
  • Read coding standards and rules
  • Read all changed files
  • Analyze for correctness, edge cases, security, and performance
  • Post inline comments for specific issues
  • Post final review summary

Summary

This is a re-review after the previous round of feedback. All three findings from the first review have been addressed:

Previous finding Status
Medium — ICodeDb interface breaking change for external implementors ✅ Resolved — default implementations added (=> false / { })
Low — OverridableWorldStateManager silently relying on codeDbIsPersistent = false default ✅ Resolved — now passes codeDbIsPersistent: false explicitly
Low — _persistedHint cache size comment missing ✅ Resolved — comment now explains the 1 024-entry rationale

Correctness analysis

Scenario Result
debug_traceCall deploys code → overlay reset → second call same code ✓ Fixed: overlay _persistedHint is null, ContainsCode always false
Production block sync: same factory bytecode across many blocks _persistedHint on durable codeDb still short-circuits redundant writes
Same code deployed twice in the same block _blockCodeInsertFilter (256-entry, intra-block) prevents duplicate inserts
OverridableWorldStateManager (debug/call overlays) codeDbIsPersistent: false explicit
HealingWorldStateScopeProvider (production) codeDbIsPersistent: true explicit
FlatScopeProvider read-only path isPersistent: !isReadOnly correct
NullCodeDb in benchmark stubs ✓ Explicitly overrides with correct no-op semantics (redundant but not wrong)

Thread safety: MarkCodePersisted runs inside the background Task.Run(PersistCodeBatch). The main thread calls codeFlushTask.GetAwaiter().GetResult() before the next block's InsertCode can run — no race on _persistedHint.

_codeDb null guard in InsertCode: _codeDb?.ContainsCode(codeHash) ?? false is fail-open (false-negative), so code is re-inserted rather than silently skipped when _codeDb is null — correct direction.


Remaining finding

Low — Session-specific test comment

StateProviderTests.cs:305–306 still says:

// One long-lived overridable scope reused across two BeginScope calls — mirrors
// OverridableEnv reusing _worldState across BuildAndOverride.

OverridableEnv and BuildAndOverride are not identifiers in the codebase. The comment describes PR context, not a stable invariant. Inline suggestion posted.


Verdict: Approve. The bug fix is correct and well-targeted. The interface change is non-breaking. All call sites have the right isPersistent / codeDbIsPersistent value. Regression tests directly exercise the failure path. The single remaining item (test comment) is cosmetic and does not block merge.

Comment thread src/Nethermind/Nethermind.State.Test/StateProviderTests.cs Outdated
@svlachakis
svlachakis marked this pull request as ready for review May 21, 2026 15:04
@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @svlachakis's task in 4m 56s —— View job


Code Review

  • Gather context and understand the request
  • Read coding standards and rules
  • Read all changed files
  • Analyze for correctness, edge cases, security, and performance
  • Post final review summary

Summary

This is a complete re-review. All findings from both prior review rounds have been addressed. The implementation is correct, well-tested, and safe.


Previously flagged findings — all resolved

Finding Status
Medium — ICodeDb interface breaking change ✅ Default implementations added (=> false / { })
Low — OverridableWorldStateManager implicit false codeDbIsPersistent: false now explicit
Low — _persistedHint cache size undocumented ✅ Comment explains 1 024-entry rationale
Low — Session-specific test comment ("OverridableEnv", "BuildAndOverride") ✅ Removed; no such comment exists in the current file

Unmodified TrieStoreScopeProvider instantiation sites — all verified correct

The search turned up four sites not touched by this PR. Each is correctly using the default codeDbIsPersistent: false:

Site Code db Why false is correct
WorldStateManager.CreateResettableWorldState() _readaOnlyCodeCbReadOnlyDb(…, true) overlay Temp-overlay writes are discarded on reset; true would re-introduce the same lifetime bug for block producers
FlatOverridableWorldScope new ReadOnlyDb(codeDb, true) overlay Same overlay semantics
StatelessBlockProcessingEnv witness.CreateCodeDb() — witness-backed, not RocksDB No persistent store to cache against
WitnessGeneratingBlockProcessingEnvFactory readOnlyDbProvider.CodeDbReadOnlyDb overlay Temp overlay; writes discarded

Correctness analysis

Scenario Result
debug_traceCall deploys code → overlay reset → second call same code ✓ Fixed: _persistedHint is null on overlay codeDb; ContainsCode always false
Production block sync: same factory bytecode across many blocks _persistedHint (1 024-entry) on durable codeDb still short-circuits redundant writes
Same code deployed twice in the same block _blockCodeInsertFilter (256-entry, intra-block, cleared on Reset) prevents duplicate inserts
OverridableWorldStateManager (debug/call overlays) codeDbIsPersistent: false explicit
HealingWorldStateScopeProvider (production) codeDbIsPersistent: true hardcoded in constructor
Non-healing production path (PruningTrieStateFactory) codeDbIsPersistent: true explicit
FlatScopeProvider isPersistent: !isReadOnly — write paths durable, read-only paths overlay

Thread safety: MarkCodePersisted runs in Task.Run(PersistCodeBatch). The main thread blocks via codeFlushTask.GetAwaiter().GetResult() before the next block's InsertCode can run — no race on _persistedHint. AssociativeKeyCache seqlock reads and Volatile.Write mutations are safe for this access pattern.

_codeDb null guard in InsertCode: _codeDb?.ContainsCode(codeHash) ?? false is fail-open (false-negative direction) — code is re-inserted rather than silently skipped when _codeDb is null. Correct.

MarkCodePersisted ordering: Called after the using (ICodeSetter batch = codeDb.BeginCodeWrite()) block completes — code is durably written before the hint is set. Correct.

Regression tests: KeyValueWithBatchingBackedCodeDb_ContainsCode_respects_isPersistent_flag (parameterized unit) and Same_code_can_be_redeployed_across_overlay_resets (integration, both trie and flat backends) directly exercise the failure path. The integration test correctly disposes the scope (triggering Reset() which clears _blockCodeInsertFilter) and calls ResetOverrides() between the two overlay passes.


Verdict: Approve. No outstanding findings. The fix is minimal, correct, and well-tested.

@github-actions github-actions Bot added the rpc label May 21, 2026
@svlachakis

Copy link
Copy Markdown
Contributor Author

@claude review my regression tests we should have one unit, one in state module and one full end to end on rpc layer

@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @svlachakis's task in 4m 24s —— View job


Regression Test Review

  • Gather context and understand the request
  • Read coding standards and test rules
  • Read all changed test files
  • Identify unit test, state module test, and RPC end-to-end test
  • Analyze coverage, correctness, and gaps
  • Post final review summary

Coverage map

All three layers are present:

Layer Test File
Unit KeyValueWithBatchingBackedCodeDb_ContainsCode_respects_isPersistent_flag Nethermind.State.Test/StateProviderTests.cs:274
State module Same_code_can_be_redeployed_across_overlay_resets Nethermind.State.Test/StateProviderTests.cs:287
RPC end-to-end Debug_traceCall_CREATE_replayed_back_to_back_does_not_throw_code_missing Nethermind.JsonRpc.Test/Modules/DebugRpcModuleTests.cs:248

Findings

Medium — Unit test runs 4× instead of 2×

StateProviderTests is [TestFixture(false)][TestFixture(true)]. The unit test (KeyValueWithBatchingBackedCodeDb_ContainsCode_respects_isPersistent_flag) does not use useFlat at all, so NUnit runs it twice redundantly — once per fixture variant. Combined with its own [TestCase] parameterization it executes 4 times. Consider moving it to a dedicated [TestFixture] CodeDbTests class. See inline comment.

Low — RPC test has no content assertion

Debug_traceCall_CREATE_replayed_back_to_back_does_not_throw_code_missing only asserts BeOfType<JsonRpcSuccessResponse> — it doesn't verify the deployed bytecode appears in the prestate diff. A regression where the trace silently returns wrong data (no exception, wrong code) would slip past. See inline comment.


What each test exercises

Unit (KeyValueWithBatchingBackedCodeDb_ContainsCode_respects_isPersistent_flag)
Directly instantiates KeyValueWithBatchingBackedCodeDb with isPersistent: true/false, calls MarkCodePersisted, and asserts ContainsCode matches. No state layer involved. Directly locks in the invariant at the lowest level. ✓

State module (Same_code_can_be_redeployed_across_overlay_resets)
Creates an IOverridableWorldScope, deploys code (triggering CommitCodeAsync which flushes the code batch and calls MarkCodePersisted), resets the overlay, then redeploys the same hash in a fresh scope. Runs against both flat and trie backends via the [TestFixture] parameterization. Correctly reproduces the lifetime-mismatch scenario. ✓

RPC end-to-end (Debug_traceCall_CREATE_replayed_back_to_back_does_not_throw_code_missing)
Sends the same CREATE transaction to debug_traceCall three times against the same pooled IDebugRpcModule. The comment explains why 3 iterations are used — the bug first surfaces on call 2, the third makes it deterministic. The init code is carefully crafted so the deployed runtime is non-empty (codeHash != keccak("")), which is the path that actually hits the codeDb on GetCode. ✓


Comment thread src/Nethermind/Nethermind.State.Test/StateProviderTests.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc.Test/Modules/DebugRpcModuleTests.cs Outdated
@svlachakis

Copy link
Copy Markdown
Contributor Author

@claude review again

@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @svlachakis's task in 4m 38s —— View job


Code Review

  • Gather context and understand the request
  • Read coding standards and rules
  • Read all changed files
  • Analyze for correctness, edge cases, security, and performance
  • Post final review summary

Summary

This is a full re-review of the current branch state. All findings from the three prior review rounds have been resolved. The implementation is correct, well-tested, and safe.


Previously flagged findings — all resolved

Finding Status
Medium — ICodeDb interface breaking change for external implementors ✅ Default implementations added (=> false / { })
Low — OverridableWorldStateManager implicit codeDbIsPersistent: false ✅ Now explicit
Low — _persistedHint cache size undocumented ✅ Comment explains 1 024-entry rationale
Low — Session-specific test comment ("OverridableEnv", "BuildAndOverride") ✅ Removed
Medium — Unit test KeyValueWithBatchingBackedCodeDb_… running 4× ✅ Extracted to CodeDbTests class (no fixture parameterization)
Low — RPC test has no content assertion result.Should().Contain("\"code\":\"0x00\"") added

Correctness analysis

Scenario Result
debug_traceCall deploys code → overlay reset → second call same code ✓ Fixed: overlay _persistedHint is null; ContainsCode always false
Production block sync: same factory bytecode across many blocks _persistedHint (1 024-entry) on durable codeDb still short-circuits redundant writes
Same code deployed twice in the same block _blockCodeInsertFilter (256-entry, cleared on Reset) prevents duplicate inserts
OverridableWorldStateManager codeDbIsPersistent: false explicit
HealingWorldStateScopeProvider codeDbIsPersistent: true hardcoded
PruningTrieStateFactory production path codeDbIsPersistent: true explicit
FlatScopeProvider isPersistent: !isReadOnly — write paths durable, read-only paths transient
WorldStateManager.CreateResettableWorldState() codeDbIsPersistent: false (default) — _readaOnlyCodeCb is a ReadOnlyDb overlay; writes are transient
FlatOverridableWorldScope codeDbIsPersistent: false (default) — _codeDbOverlay is new ReadOnlyDb(codeDb, true); writes are transient

CommitCodeAsync ordering in test: worldState.Commit(spec) invokes the extension method with commitRoots: true, which calls CommitCodeAsync. The background Task.Run(PersistCodeBatch) completes before scope disposal (blocked by GetAwaiter().GetResult()). MarkCodePersisted is called inside PersistCodeBatch after the batch write completes — code is on disk before the hint is set. ✓

Reset() clears _blockCodeInsertFilter: On scope disposal, Reset(resetBlockChanges: true) clears both _blockCodeInsertFilter and _codeBatch. So in the second scope, the only filter consulted is _codeDb.ContainsCode(codeHash), which returns false (overlay, _persistedHint is null). InsertCode adds to _codeBatch. GetCode finds it there. ✓

Thread safety: MarkCodePersisted runs in Task.Run(PersistCodeBatch); the main thread blocks via GetAwaiter().GetResult() before the next block's InsertCode can run — no race on _persistedHint. AssociativeKeyCache seqlock reads and Volatile.Write mutations are safe for concurrent access. ✓

_codeDb null guard: _codeDb?.ContainsCode(codeHash) ?? false is fail-open (false-negative) — code is re-inserted rather than silently skipped when _codeDb is null. ✓


Remaining notes (pre-existing, unmodified files — not blocking)

Two call sites in unmodified files still use the implicit isPersistent: false default rather than the explicit named parameter:

  • FlatOverridableWorldScope.cs:47: new TrieStoreScopeProvider.KeyValueWithBatchingBackedCodeDb(_codeDbOverlay)_codeDbOverlay is new ReadOnlyDb(codeDb, true), so false is correct, just implicit.
  • WorldStateManager.cs:64 CreateResettableWorldState(): new TrieStoreScopeProvider(_readOnlyTrieStore, _readaOnlyCodeCb, _logManager)_readaOnlyCodeCb is a ReadOnlyDb overlay, so false is correct, just implicit.

These are harmless and pre-date this PR; worth a follow-up pass to make intent explicit, but do not block merge.


Verdict: Approve. No outstanding findings. The fix is minimal, correct, and well-tested across all three layers (unit, state module, RPC end-to-end).

Comment on lines +417 to +419
public bool ContainsCode(in ValueHash256 codeHash) => false;

public void MarkCodePersisted(in ValueHash256 codeHash) { }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed as there is default implementation in interface?

Comment on lines +269 to +271
public bool ContainsCode(in ValueHash256 codeHash) => false;

public void MarkCodePersisted(in ValueHash256 codeHash) { }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

// code is used by multiple deployments. Either from factory contracts (e.g. LPs)
// or people copy and pasting popular contracts
if (!_blockCodeInsertFilter.Get(codeHash) && !_persistedCodeInsertFilter.Get(codeHash))
if (!_blockCodeInsertFilter.Get(codeHash) && !(_codeDb?.ContainsCode(codeHash) ?? false))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO more readable:

Suggested change
if (!_blockCodeInsertFilter.Get(codeHash) && !(_codeDb?.ContainsCode(codeHash) ?? false))
if (!_blockCodeInsertFilter.Get(codeHash) && !(_codeDb?.ContainsCode(codeHash) == true))

@LukaszRozmej

Copy link
Copy Markdown
Member

Does this supersede #11712? Should we close that one?

@svlachakis
svlachakis merged commit febe5fa into master May 22, 2026
687 of 690 checks passed
@svlachakis
svlachakis deleted the alchemy-code-fix branch May 22, 2026 07:23
@svlachakis

svlachakis commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

@LukaszRozmej I've left #11712 in case we wanted to release 1.37.3 because it's based of from release and for the customer image diff, looks like not so I'm closing it

stdevMac pushed a commit that referenced this pull request May 24, 2026
stdevMac pushed a commit that referenced this pull request May 26, 2026
stdevMac added a commit that referenced this pull request Jun 1, 2026
* RLP tx decoding fixes (#11496)

* Handle null txs

* PR feedback

* Revert `BlobTxStorage.TryDecodeFullTx` changes

Silent `false` may be worse that NRE on DB corruption

---------

Co-authored-by: Alexey Osipov <me@flcl.me>

* eth/71 (#10844)

* Improve eth/70 checks (#11456)

* Improve eth/70 verification

* Comments

* Gas remake

* Fix full sync

* fix: prevent negative RequestSize crash when beacon pivot destination advances mid-sync (#11478)

* fix: prevent negative RequestSize crash when beacon pivot destination advances mid-sync

`HeadersSyncFeed.ShouldBuildANewBatch` checked
`_lowestRequestedHeaderNumber == HeadersDestinationNumber`. For beacon
headers, `HeadersDestinationNumber` is `BeaconPivot.PivotDestinationNumber`,
which tracks `Head.Number - Reorganization.MaxDepth + 1` and so advances
upward as the chain head progresses. When it stepped above
`_lowestRequestedHeaderNumber` mid-sync, the `==` check missed it,
`BuildNewBatch` produced a negative `RequestSize`, and
`HeaderStore.FindReversedHeaders` crashed with
`ArgumentOutOfRangeException` on `new Dictionary<>(negativeCount)`.

Widen the guard to `<=` and add a regression test that reproduces the
scenario via mocked `IBeaconPivot`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: shorten inline comments per review

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add new default for gnosis and gnosis archive config (#11269)

feat: add Db.SkipCheckingSstFileSizesOnDbOpen=true default for gnosis and gnosis archive

* Alchemy - Code Fix (#11714)

* feat: add SkipMetricsTracking property to DbSettings (#11515)

* feat: add SkipMetricsTracking property to DbSettings

- Introduced SkipMetricsTracking property in DbSettings to control metrics tracking for specific databases.
- Updated FullPruningInnerDbFactory to set SkipMetricsTracking to true for inner databases to prevent stale references after pruning.
- Added unit tests to verify the behavior of metrics tracking based on the new property.
- Enhanced DbMonitoringModule to respect the SkipMetricsTracking setting when adding databases to the tracker.

* fix: address PR feedback for db metrics tracking

- Add XML doc to DbSettings.SkipMetricsTracking property
- Clarify WorldStateModule comment for both FullPruningInnerDbFactory
  and MemDbFactory branches
- DbMonitoringModule: clear stale dictionary entries on GatherMetric
  failure and log only once per failure streak (with recovery info log)
- DbTrackerTests: add [TearDown] to reset shared static metrics keys,
  collapse double enumeration in TestSkipMetricsTracking, and add
  FullPruningDbTrackedWrapper_SurvivesPruningCycle integration test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: dedupe DbTrackerTests container setup and metric-map iteration

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: Initialize _failingDbs with an empty HashSet

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>

* Fix DbTracker repeatedly logging ObjectDisposedException after disposal (#11720)

* Fix DbTracker repeatedly logging ObjectDisposedException after disposal

When the Autofac LifetimeScope (or the shared cache SafeHandle) is disposed
while MonitoringService's timer is still scheduled, `_sharedBlockCache.Value`
in `UpdateDbMetrics` throws `ObjectDisposedException` via Autofac's
LazyRegistrationSource. The generic catch logs it at Error and the callback
stays registered, so the same exception re-fires on every metric interval —
producing dozens of identical errors per minute on affected nodes.

Catch `ObjectDisposedException` explicitly and short-circuit subsequent ticks
via a `_stopped` flag. Adds a regression test that disposes the container
and asserts the callback neither throws nor logs on repeated invocations.

Fixes #11719

* Address review: debug-log first stop, drop redundant CreateDb in test

- Log at Debug level in the new `ObjectDisposedException` branch so there is
  a (no-cost on production) signal that DbTracker has stopped updating
  metrics, rather than only inferring it from the absence of further Error
  logs.
- Remove the duplicate `CreateDb` call in the regression test — the helper
  `ConfigureMetricUpdater` already registers the test DB.
- Disable `TestLogger.IsDebug` in the regression test so the new Debug
  message does not trip the `LogList.Should().BeEmpty()` assertion; the
  test still asserts no Error-level spam, which was the bug.

* Address review: make DbTracker IDisposable, drop redundant comment

- Implement IDisposable on DbTracker so Autofac proactively sets _stopped
  during scope teardown, short-circuiting subsequent monitoring ticks
  before they touch disposed resources. The catch (ObjectDisposedException)
  remains as a backstop for the race where a tick is already executing
  when Dispose runs.
- Mark _stopped as volatile since it is now written from the disposing
  thread and read from the monitoring timer thread.
- Drop the inline comment in the catch block; the Debug log message
  already conveys the same information.

* Fix Eth69/Eth70 receipt tests for null-means-unknown contract

After dropping the FindHeader pre-check, the response loop relies solely
on GetReceipts returning null to detect an unknown block. The two
"unknown block hash" tests still mocked the old contract (FindHeader
returns null + GetReceipts returns []), so the loop saw [] as a
legitimate zero-tx block and kept going instead of breaking.

Update the mocks to return null for unknown hashes, matching the
ISyncServer.GetReceipts contract (null = unknown, [] = exists w/ 0 txs).

* Make EraE tests visible and green (#11727)

* fix(eth/70): reject null receipt payloads (#11615)

* fix(eth/70): reject null receipt payloads

* fix(eth/70): validate receipt payloads while decoding

* refactor(eth): move null receipt validation into base serializer

Apply the validation in V63 ReceiptsMessageSerializer so eth/63, eth/66,
eth/69 and eth/70 all reject null receipt payloads at decode time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit fd2fd25)

* fix(eth/70): stop response early when block has txs but no receipts

FulfillReceiptsRequest used to emit `txReceipts.Add([])` for any block where
SyncServer.GetReceipts(hash) returned empty, regardless of whether the block
actually had zero transactions. The eth/70 receiver validates segment-complete
responses against its own transaction count and throws SubprotocolException
("Receipt count mismatch with block transactions count") + disconnects the
peer when an [] arrives for a block that locally has transactions.

That made a node that is briefly without receipts (e.g. still syncing
receipts, or its receipt store is pruned for the requested block) appear
malicious to the requester. We observed this disconnect-storm pattern
materially starving receipt-sync on small networks.

Distinguish the two cases at the sender by looking up the block:
- block is null or body is missing  → we can't safely claim anything; break
  the response and let the requester ask another peer
- block.Transactions.Length > 0     → same: we don't actually have the
  receipts even though we have the body; break
- block.Transactions.Length == 0    → block is legitimately empty; emit []
  as before

Update Should_return_empty_receipts_block_when_local_block_has_no_receipts
→ ..._has_no_transactions to reflect the new precondition, and stub
SyncServer.Find on two pre-existing empty-receipts-in-the-middle tests so
they still represent the legitimate empty case. Add new regression test
Should_stop_response_when_local_block_has_transactions_but_no_receipts
covering the bug.

Closes #11752.

(cherry picked from commit f0f6ea2)

* refactor(eth/70): disambiguate "unknown" vs "legit empty" in ISyncServer.GetReceipts

Following @LukaszRozmej's review suggestion on #11752: rather than have the
protocol handler do a second SyncServer.Find lookup to figure out whether an
empty receipts array means "block has zero transactions" or "I don't have the
receipts yet", push the disambiguation down to where the data lives.

ISyncServer.GetReceipts now returns TxReceipt[]?:
  null      → receipts are not known locally (block missing, body missing, or
              receipts not stored). Callers MUST NOT emit [] on the wire.
  empty []  → block is known and legitimately has zero transactions.
  non-empty → receipts for an executed block.

SyncServer.GetReceipts implements the three cases directly:
  - blockHash is null OR block not found OR block body missing → null
  - block.Transactions.Length == 0                              → []
  - block has txs, receipts not stored                          → null
  - block has txs and receipts                                  → receipts

Eth70ProtocolHandler.FulfillReceiptsRequest is now a single null-check instead
of the previous Find-then-classify dance. SyncPeerProtocolHandlerBase.Fulfill
(eth/63-69 path) gets the same fix for free — same bug, same one-line guard.

Tests: replace the Find-based stubs with GetReceipts-returning-null stubs and
update OldStyleFullSynchronizerTests.Can_retrieve_empty_receipts to assert the
new contract (genesis → BeEmpty; unknown blocks → BeNull).

Closes #11752.

(cherry picked from commit 1d880be)

* Apply suggestions from code review

Co-authored-by: Lukasz Rozmej <lukasz.rozmej@gmail.com>
(cherry picked from commit c01295b)

* Apply suggestion from @LukaszRozmej

(cherry picked from commit 27258b9)

* Drop redundant FindHeader pre-check in receipt response loop

GetReceipts now returns null for unknown blocks (block missing, body
missing, or receipts not stored), so the up-front FindHeader call before
GetReceipts is redundant — the `if (receipts is null) break;` below it
already handles the unknown case.

Per @flcl42 review on #11754.

(cherry picked from commit 742cb1a)

* Fix shutdown race in SnapProvider PLINQ (closes #11806) (#11807)

* Unwrap AggregateException(ObjectDisposedException) from snap PLINQ on shutdown

When the node is stopped during snap sync, SnapProvider.AddAccountRange's
parallel code-existence check (codeHashes.AsParallel().Where(_codeDb.KeyExists))
races RocksDB disposal in the DI container teardown. The resulting
ObjectDisposedException is wrapped by PLINQ in an AggregateException, which
falls past the snap dispatcher's existing `catch (ObjectDisposedException) →
Info("Ignoring sync response as the DB has already closed.")` guard and lands
on `catch (Exception) → Error("Error when handling response", e)`.

The node recovers correctly on restart — this is purely a noisy shutdown log
line — but the post-merge fuzz tests' StabilityVerification watchdog scans
for non-allowlisted exception lines and fails the test on it, blocking the
1.38 release smoke run.

Unwrap the AggregateException at the point of throw so the dispatcher's
existing benign guard handles it uniformly. No new log path; reuses the
already-tested "Ignoring sync response..." Info message.

Race introduced 2024-03-28 by PR #6873 "Perf/dont redownload downloaded code"
(commit 7059b45), latent until the fuzz watchdog started catching it.

* Tidy unwrap: single Flatten, preserve stack via ExceptionDispatchInfo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Guard against empty InnerExceptions in unwrap filter

Enumerable.All() returns true vacuously on an empty sequence, which
would let the filter pass and then InnerExceptions[0] throw
ArgumentOutOfRangeException instead of re-throwing the original.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Default Discovery to V4 (#11614)

* Default Discovery to V4

* Update tests

* Activate BAL only when needed (#11795)

* Activate BAL only when needed

* Guard ChangeState against same-state transitions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Align IsFinished with ShouldFinish; short-circuit cheap checks first

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update Directory.Build.props for 1.38.0

---------

Co-authored-by: Alex <alexb5dh@gmail.com>
Co-authored-by: Alexey Osipov <me@flcl.me>
Co-authored-by: Amirul Ashraf <asdacap@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Co-authored-by: Carlos Bermudez Porto <43155355+cbermudez97@users.noreply.github.com>
Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>
Co-authored-by: DeFi Junkie <deffie.jnkiee@gmail.com>
Co-authored-by: Ben {chmark} Adams <thundercat@illyriad.co.uk>
LukaszRozmej added a commit to LukaszRozmej/membership-1 that referenced this pull request Jul 8, 2026
Name / Identifier
Stavros Vlachakis

Team / Project
Nethermind

Start date of relevant projects
July 2025 (part-time)
February 2026 (full-time)

Proposed weight
Full (1.0)

Summary of work / eligibility

Stavros joined Nethermind in March 2025. Since July 2025, he has contributed part-time to the Nethermind Ethereum Execution Client (Core team) alongside other responsibilities. Since February 2026, he has worked full-time on Nethermind Client, with 100+ merged PRs in total. He owns the JSON-RPC for Nethermind Client. He is also expected to contribute extensively to Frame Transactions on Hegota.

Representative work:

- EIP-4444 history expiry (EraE): implemented the EraE archive format end-to-end — era export/import and remote download with SHA-256 verification. (#10812 (NethermindEth/nethermind#10812))
- JSON-RPC (owner): broad spec-compliance and Geth-parity work plus new endpoints — e.g. Geth-compatible error codes (#11335 (NethermindEth/nethermind#11335)) and eth_signTransaction / raw-transaction methods (#11517 (NethermindEth/nethermind#11517), #11521 (NethermindEth/nethermind#11521)).
- Streaming for large RPC responses: streaming approach for trace_* and debug_* results, avoiding buffering huge responses in memory. (#11755 (NethermindEth/nethermind#11755), #11693 (NethermindEth/nethermind#11693)).
- EVM & execution performance: eth_call interpreter/dispatch optimizations (#11965 (NethermindEth/nethermind#11965)), EVM memory pooling and SLOAD / flat-state read caching (#11991 (NethermindEth/nethermind#11991), #12043 (NethermindEth/nethermind#12043)).
- State & consensus reliability: correct persisted-code tracking in the code DB (#11714 (NethermindEth/nethermind#11714)), forkchoice canonical-chain corruption healing after beacon sync (#10876 (NethermindEth/nethermind#10876)).

All merged PRs: https://github.com/NethermindEth/nethermind/pulls?q=is%3Apr+author%3Asvlachakis+is%3Aclosed (edited)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants