Skip to content

perf: add fast MVCC snapshots for MemDb in FlatDb tests#10792

Merged
asdacap merged 13 commits intomasterfrom
treeterm/faster-memdb-snapshot
Mar 26, 2026
Merged

perf: add fast MVCC snapshots for MemDb in FlatDb tests#10792
asdacap merged 13 commits intomasterfrom
treeterm/faster-memdb-snapshot

Conversation

@asdacap
Copy link
Copy Markdown
Contributor

@asdacap asdacap commented Mar 11, 2026

Summary

Replaces slow O(n) full-copy snapshots with fast O(1) MVCC-based snapshots in FlatDb test infrastructure.

Changes

New Implementation

  • SnapshotableMemDb: MVCC-based in-memory database with O(1) snapshot creation

    • Uses SortedDictionary<byte[], VersionedEntry> with version tracking
    • Implements IKeyValueStoreWithSnapshot and ISortedKeyValueStore
    • Automatic version garbage collection when snapshots are disposed
    • Thread-safe with lock-based concurrency control
  • SnapshotableMemColumnsDb: Column database wrapper providing atomic snapshots across all columns

    • Uses SnapshotableMemDb per column
    • Snapshots capture consistent view across all columns

Test Infrastructure Migration

  • Updated PseudoNethermindModule to register SnapshotableMemColumnsDb for FlatDb tests
  • Fixed FlatTrieVerifierTests helper methods to use IDb interface

Performance Impact

Before: FlatDb test snapshots performed full database copy (O(n))
After: Snapshots are O(1) version captures

This significantly improves test performance when FlatDb creates frequent snapshots during test execution.

Testing

  • ✅ 21 SnapshotableMemDb unit tests (isolation, concurrency, sorted iteration, GC)
  • ✅ 11 SnapshotableMemColumnsDb unit tests (atomic snapshots, write batches)
  • ✅ 22 FlatTrieVerifierTests (all passing with new implementation)

Type of change

  • Performance improvement
  • Refactoring (test infrastructure improvement)
  • New tests

Documentation/Changelog

  • Readme/Documentation was updated
  • Changelog was updated

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces MVCC-style in-memory DB implementations to speed up FlatDb-related tests by making snapshot creation O(1), then wires the new DB into FlatDb test infrastructure.

Changes:

  • Add SnapshotableMemDb (MVCC snapshot-capable in-memory DB) and SnapshotableMemColumnsDb (multi-column wrapper).
  • Migrate FlatDb test DI and FlatTrieVerifierTests to use the new snapshot-capable column DB.
  • Add unit tests for the new DBs and update spelling dictionary for new terms.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/Nethermind/Nethermind.State.Flat.Test/FlatTrieVerifierTests.cs Switch FlatDb test setup from TestMemColumnsDb to SnapshotableMemColumnsDb and remove concrete DB casts.
src/Nethermind/Nethermind.Db/SnapshotableMemDb.cs New MVCC-based in-memory DB with snapshot + sorted view support and version pruning.
src/Nethermind/Nethermind.Db/SnapshotableMemColumnsDb.cs New in-memory columns DB that creates per-column snapshots.
src/Nethermind/Nethermind.Db.Test/SnapshotableMemDbTests.cs New unit tests for MVCC DB behavior (including a currently ignored pruning regression).
src/Nethermind/Nethermind.Db.Test/SnapshotableMemColumnsDbTests.cs New unit tests for columns DB snapshot behavior and write batches.
src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindModule.cs Register SnapshotableMemColumnsDb for FlatDb tests (with pruning disabled).
cspell.json Add “MVCC” and “Snapshotable” to spelling allowlist.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +395 to +430
[Test]
[Ignore("Known bug in PruneVersionsOlderThan - will be fixed in separate PR. Use neverPrune option for now.")]
public void Snapshot_survives_pruning_when_newer_snapshot_disposed()
{
// Use default (pruning enabled) to verify the fix
SnapshotableMemDb memDb = new();

// Write key A at version 1
memDb.Set(TestItem.KeccakA, new byte[] { 1 });

// Write key B at version 2
memDb.Set(TestItem.KeccakB, new byte[] { 2 });

// Create snapshot1 at version 2 (sees both keys)
IKeyValueStoreSnapshot snapshot1 = memDb.CreateSnapshot();

// Update key A at version 3
memDb.Set(TestItem.KeccakA, new byte[] { 3 });

// Create snapshot2 at version 3
IKeyValueStoreSnapshot snapshot2 = memDb.CreateSnapshot();

// Dispose snapshot2 - triggers PruneVersionsOlderThan(2)
snapshot2.Dispose();

// snapshot1 should still see the original value for key A
byte[]? valueA = snapshot1.Get(TestItem.KeccakA);
valueA.Should().NotBeNull("snapshot1 at version 2 should still see key A written at version 1");
valueA.Should().BeEquivalentTo(new byte[] { 1 });

// Key B should still work
byte[]? valueB = snapshot1.Get(TestItem.KeccakB);
valueB.Should().BeEquivalentTo(new byte[] { 2 });

snapshot1.Dispose();
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This test is newly introduced but marked [Ignore] due to a known pruning bug. Shipping a new ignored test usually means the PR knowingly introduces incomplete/incorrect behavior; it also leaves the broken pruning path untested. Prefer fixing the pruning logic in SnapshotableMemDb and enabling this test, or remove the pruning feature until it’s correct.

Copilot uses AI. Check for mistakes.
_logManager = LimboLogs.Instance;

_columnsDb = new TestMemColumnsDb<FlatDbColumns>();
_columnsDb = new SnapshotableMemColumnsDb<FlatDbColumns>();
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

FlatTrieVerifierTests constructs SnapshotableMemColumnsDb with default settings (pruning enabled), but SnapshotableMemDb currently has a known pruning bug (see ignored test in SnapshotableMemDbTests). If these persistence paths rely on snapshots, this risks intermittent wrong reads depending on snapshot disposal order. Either fix pruning in SnapshotableMemDb, or construct the test DB with neverPrune: true until pruning is correct.

Suggested change
_columnsDb = new SnapshotableMemColumnsDb<FlatDbColumns>();
_columnsDb = new SnapshotableMemColumnsDb<FlatDbColumns>(neverPrune: true);

Copilot uses AI. Check for mistakes.
// Flatdb (if used) need a more complete memcolumndb implementation with snapshots and sorted view.
.AddSingleton<IColumnsDb<FlatDbColumns>>((_) => new TestMemColumnsDb<FlatDbColumns>())
// FlatDb uses SnapshotableMemColumnsDb for fast O(1) MVCC snapshots instead of slow O(n) full copies
.AddSingleton<IColumnsDb<FlatDbColumns>>((_) => new SnapshotableMemColumnsDb<FlatDbColumns>(neverPrune: true))
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Registering SnapshotableMemColumnsDb with neverPrune: true disables version GC entirely. Given FlatDb creates many snapshots, this can cause unbounded memory growth during longer test runs. Once pruning is fixed, prefer enabling pruning (or at least adding a bounded GC strategy) so tests don’t risk OOMs.

Suggested change
.AddSingleton<IColumnsDb<FlatDbColumns>>((_) => new SnapshotableMemColumnsDb<FlatDbColumns>(neverPrune: true))
.AddSingleton<IColumnsDb<FlatDbColumns>>((_) => new SnapshotableMemColumnsDb<FlatDbColumns>(neverPrune: false))

Copilot uses AI. Check for mistakes.
Comment on lines +308 to +330
// Find the latest entry for this key with version <= requested version
// Since entries are sorted by (key, version), we can iterate backwards from the upper bound

// Get all entries that could match: same key with version <= requested version
// We need to find the entry with the highest version <= requested version
foreach (var kvp in _db.Reverse())
{
(byte[] entryKey, int entryVersion) = kvp.Key;

// If we've gone past this key entirely, stop
if (entryKey.AsSpan().SequenceCompareTo(key) < 0)
{
break;
}

// If this entry is for our key and version is <= requested
if (entryKey.AsSpan().SequenceCompareTo(key) == 0 && entryVersion <= version)
{
return kvp.Value; // This could be null (tombstone)
}
}

return null;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

GetValueAtVersion enumerates _db.Reverse() which is LINQ Enumerable.Reverse() over a SortedDictionary. That buffers the entire dictionary on every read and then scans linearly, making reads O(n) with per-call allocations (and will dominate performance in FlatDb tests). Consider a data structure that supports floor queries without full scans (e.g., per-key version list + binary search, or a composite key approach with a lookup to the last <= version using custom range search).

Suggested change
// Find the latest entry for this key with version <= requested version
// Since entries are sorted by (key, version), we can iterate backwards from the upper bound
// Get all entries that could match: same key with version <= requested version
// We need to find the entry with the highest version <= requested version
foreach (var kvp in _db.Reverse())
{
(byte[] entryKey, int entryVersion) = kvp.Key;
// If we've gone past this key entirely, stop
if (entryKey.AsSpan().SequenceCompareTo(key) < 0)
{
break;
}
// If this entry is for our key and version is <= requested
if (entryKey.AsSpan().SequenceCompareTo(key) == 0 && entryVersion <= version)
{
return kvp.Value; // This could be null (tombstone)
}
}
return null;
// Find the latest entry for this key with version <= requested version.
// Entries are sorted by (key, version) in ascending order.
byte[]? result = null;
int bestVersion = int.MinValue;
foreach (var kvp in _db)
{
(byte[] entryKey, int entryVersion) = kvp.Key;
int cmp = entryKey.AsSpan().SequenceCompareTo(key);
// Skip entries for keys that are less than the requested key.
if (cmp < 0)
{
continue;
}
// Once we pass the requested key, no further entries can match.
if (cmp > 0)
{
break;
}
// Same key: track the highest version <= requested version.
if (entryVersion <= version && entryVersion > bestVersion)
{
bestVersion = entryVersion;
result = kvp.Value; // This could be null (tombstone)
}
}
return result;

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +74
public IColumnDbSnapshot<TKey> CreateSnapshot()
{
Dictionary<TKey, IKeyValueStoreSnapshot> snapshots = new();
foreach (KeyValuePair<TKey, SnapshotableMemDb> kvp in _columnDbs)
{
snapshots[kvp.Key] = kvp.Value.CreateSnapshot();
}
return new ColumnSnapshot(snapshots);
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

SnapshotableMemColumnsDb claims/aims to provide consistent snapshots across columns, but CreateSnapshot takes independent per-column snapshots without any shared version/lock. With concurrent writes across columns, the returned IColumnDbSnapshot can observe different points-in-time per column (unlike RocksDb ColumnsDb which uses a single underlying snapshot). If atomicity is required, consider sharing a global version/lock across all column stores, or redesign so writes go through a wrapper that can coordinate snapshot boundaries.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +52
public IDb GetColumnDb(TKey key)
{
if (!_columnDbs.TryGetValue(key, out SnapshotableMemDb? db))
{
db = new SnapshotableMemDb($"Column_{key}", _neverPrune);
_columnDbs[key] = db;
}
return db;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

_columnDbs is a plain Dictionary accessed without synchronization in GetColumnDb/ColumnKeys/CreateSnapshot/Dispose/Flush. Since this type is used in tests marked parallelizable and the PR description mentions thread-safety, this can race if columns are accessed from multiple threads. Consider using a lock (or ConcurrentDictionary) around dictionary mutations/iteration and ensure CreateSnapshot enumerates a stable snapshot of the columns.

Copilot uses AI. Check for mistakes.

// Get all entries that could match: same key with version <= requested version
// We need to find the entry with the highest version <= requested version
foreach (var kvp in _db.Reverse())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oit! Reverse is slow.

{
lock (_versionLock)
{
foreach (byte[] key in GetAllUniqueKeys())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

WTF

lock (_db._versionLock)
{
_keysInRange = new List<byte[]>();
foreach (byte[] key in _db.GetAllUniqueKeys())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

WTF, you are suppose to iterate the sorted dictionary!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.


lock (_versionLock)
{
_currentVersion++;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

With neverPrune=false, versions are only garbage-collected from _db when a snapshot is disposed (via OnSnapshotDisposed). If the store is used without ever creating snapshots, every Set/Remove keeps accumulating historical versions indefinitely, causing unbounded memory growth.

Consider pruning on write when _activeSnapshotVersions.Count == 0 (e.g., remove previous versions for the same key immediately), so the non-snapshot case behaves like a normal MemDb and doesn't leak memory.

Suggested change
_currentVersion++;
_currentVersion++;
if (!_neverPrune && _activeSnapshotVersions.Count == 0)
{
// No active snapshots: we can safely drop all previous versions for this key
List<(byte[] Key, int Version)> versionsToRemove = new();
foreach ((byte[] existingKey, int existingVersion) in _db.Keys)
{
if (existingVersion < _currentVersion && existingKey.AsSpan().SequenceEqual(keyArray))
{
versionsToRemove.Add((existingKey, existingVersion));
}
}
foreach ((byte[] removeKey, int removeVersion) in versionsToRemove)
{
_db.Remove((removeKey, removeVersion));
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +291 to +309
private byte[]? GetValueAtVersion(byte[] key, int version)
{
byte[]? result = null;

foreach (var kvp in _db)
{
(byte[] entryKey, int entryVersion) = kvp.Key;
int cmp = entryKey.AsSpan().SequenceCompareTo(key);

if (cmp > 0)
break;

if (cmp == 0 && entryVersion <= version)
{
result = kvp.Value;
}
}

return result;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

GetValueAtVersion linearly scans _db from the beginning for every Get/KeyExists, making reads O(total entries up to the key) rather than O(log n). With MVCC this can become very expensive as versions accumulate.

Consider changing the data structure to index by key first (e.g., SortedDictionary<byte[], VersionChain> where VersionChain supports binary search for the latest version <= requested) so point lookups are logarithmic and don't require full scans.

Copilot uses AI. Check for mistakes.
Comment on lines +586 to +631
public bool MoveNext()
{
lock (_db._versionLock)
{
byte[]? candidateKey = null;
byte[]? candidateValue = null;

foreach (var kvp in _db._db)
{
(byte[] entryKey, int entryVersion) = kvp.Key;

if (entryKey.AsSpan().SequenceCompareTo(_firstKey) < 0) continue;
if (entryKey.AsSpan().SequenceCompareTo(_lastKey) >= 0) break;

// Skip entries at or before current position
if (_currentKey is not null && entryKey.AsSpan().SequenceCompareTo(_currentKey) <= 0)
continue;

if (candidateKey is not null && candidateKey.AsSpan().SequenceCompareTo(entryKey) != 0)
{
if (candidateValue is not null)
{
_currentKey = candidateKey;
_currentValue = candidateValue;
return true;
}
candidateKey = null;
candidateValue = null;
}

if (entryVersion <= _version)
{
candidateKey = entryKey;
candidateValue = kvp.Value;
}
}

if (candidateKey is not null && candidateValue is not null)
{
_currentKey = candidateKey;
_currentValue = candidateValue;
return true;
}

return false;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

MemDbSortedView.MoveNext() (and StartBefore) re-scan the entire _db._db from the beginning on every call. This makes iterating a range O(n²) for n keys and can negate the intended performance improvements when FlatDb performs long co-iterations.

Consider implementing ISortedView with a persistent enumerator / iterator state (or pre-materialize the range once) so each MoveNext() is amortized O(1) within the chosen range.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +106 to +130
lock (_versionLock)
{
foreach (byte[] key in GetAllUniqueKeys())
{
byte[]? value = GetValueAtVersion(key, _currentVersion);
if (value is not null)
{
yield return new KeyValuePair<byte[], byte[]?>(key, value);
}
}
}
}

public IEnumerable<byte[]> GetAllKeys(bool ordered = false)
{
lock (_versionLock)
{
foreach (byte[] key in GetAllUniqueKeys())
{
if (GetValueAtVersion(key, _currentVersion) is not null)
{
yield return key;
}
}
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

GetAll() holds _versionLock while yielding results. Because this is an iterator block, the lock remains held for the entire enumeration, which can block writers/readers and can deadlock if the caller iterates and calls back into the DB. Prefer materializing the results (or keys) under the lock and returning an already-built collection/array so the lock is not held across consumer code (same pattern applies to GetAllKeys/GetAllValues/GetAllAtVersion).

Suggested change
lock (_versionLock)
{
foreach (byte[] key in GetAllUniqueKeys())
{
byte[]? value = GetValueAtVersion(key, _currentVersion);
if (value is not null)
{
yield return new KeyValuePair<byte[], byte[]?>(key, value);
}
}
}
}
public IEnumerable<byte[]> GetAllKeys(bool ordered = false)
{
lock (_versionLock)
{
foreach (byte[] key in GetAllUniqueKeys())
{
if (GetValueAtVersion(key, _currentVersion) is not null)
{
yield return key;
}
}
}
List<KeyValuePair<byte[], byte[]?>> result;
lock (_versionLock)
{
result = new List<KeyValuePair<byte[], byte[]?>>();
foreach (byte[] key in GetAllUniqueKeys())
{
byte[]? value = GetValueAtVersion(key, _currentVersion);
if (value is not null)
{
result.Add(new KeyValuePair<byte[], byte[]?>(key, value));
}
}
}
foreach (KeyValuePair<byte[], byte[]?> item in result)
{
yield return item;
}
}
public IEnumerable<byte[]> GetAllKeys(bool ordered = false)
{
List<byte[]> result;
lock (_versionLock)
{
result = new List<byte[]>();
foreach (byte[] key in GetAllUniqueKeys())
{
if (GetValueAtVersion(key, _currentVersion) is not null)
{
result.Add(key);
}
}
}
foreach (byte[] key in result)
{
yield return key;
}

Copilot uses AI. Check for mistakes.
Comment on lines +515 to +517
_currentKey = null;
_currentValue = null;
return false;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

MemDbSortedView.StartBefore() returns false when the computed range is empty (e.g., when key is before _firstKey). In existing ISortedView implementations in this repo (e.g., TestMemDb.FakeSortedView), StartBefore typically returns false only when the view itself is empty, and callers may interpret false as “no data”. Consider handling key < _firstKey by positioning to “before start” (so the next MoveNext yields the first element) and returning true when the view has any elements.

Suggested change
_currentKey = null;
_currentValue = null;
return false;
// key is before the first key in this view: position "before start"
_currentKey = null;
_currentValue = null;
// Determine if there is any visible element in the view at this version.
var fullLower = (_firstKey, 0, (byte[]?)null);
var fullUpper = (_lastKey, 0, (byte[]?)null);
var fullView = _db._db.GetViewBetween(fullLower, fullUpper);
byte[]? bestKeyInRange = null;
byte[]? bestValueInRange = null;
byte[]? candidateKeyInRange = null;
byte[]? candidateValueInRange = null;
foreach (var entry in fullView)
{
if (candidateKeyInRange is not null && candidateKeyInRange.AsSpan().SequenceCompareTo(entry.Key) != 0)
{
if (candidateValueInRange is not null)
{
bestKeyInRange = candidateKeyInRange;
bestValueInRange = candidateValueInRange;
}
candidateKeyInRange = null;
candidateValueInRange = null;
}
if (entry.Version <= _version)
{
candidateKeyInRange = entry.Key;
candidateValueInRange = entry.Value;
}
}
if (candidateValueInRange is not null)
{
bestKeyInRange = candidateKeyInRange;
bestValueInRange = candidateValueInRange;
}
// Return true if there is at least one visible element in the view.
return bestKeyInRange is not null;

Copilot uses AI. Check for mistakes.
@asdacap asdacap force-pushed the treeterm/faster-memdb-snapshot branch from 2431f92 to ab7e16c Compare March 25, 2026 23:10
/// </summary>
public class SnapshotableMemColumnsDb<TKey> : IColumnsDb<TKey> where TKey : struct, Enum
{
private readonly IDictionary<TKey, SnapshotableMemDb> _columnDbs = new Dictionary<TKey, SnapshotableMemDb>();
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.

Does it need locking? Also why IDictionary when is privately assigned Dictionary

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No it doesn't need locking, columns are initialized in the constructor and only read after. Changed IDictionary to Dictionary. Fixed in d39d3c3.

-- Claude (on behalf of asdacap)

private readonly EntryComparer _entryComparer = new();
private int _currentVersion = 0;
private readonly HashSet<int> _activeSnapshotVersions = new();
private readonly object _versionLock = new();
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.

Suggested change
private readonly object _versionLock = new();
private readonly Lock _versionLock = new();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, changed to Lock. Fixed in d39d3c3.

-- Claude (on behalf of asdacap)

asdacap and others added 10 commits March 26, 2026 08:19
Implements SnapshotableMemDb and SnapshotableMemColumnsDb with O(1)
snapshot creation using Multi-Version Concurrency Control (MVCC),
replacing the O(n) full-copy approach when snapshots are needed.

Key features:
- O(1) snapshot creation by capturing version numbers
- Multiple concurrent snapshots with full isolation
- Automatic version garbage collection on snapshot disposal
- ISortedKeyValueStore support for sorted iteration
- Thread-safe with proper locking

This enables efficient snapshot-based testing, particularly for
FlatDb tests where snapshots are created frequently. The new classes
are drop-in replacements for MemDb/MemColumnsDb when snapshot
support is required.

All 21 unit tests passing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace TestMemColumnsDb with SnapshotableMemColumnsDb in FlatDb test infrastructure to enable fast O(1) MVCC snapshots instead of slow O(n) full database copies.

Changes:
- PseudoNethermindModule: Register SnapshotableMemColumnsDb for FlatDbColumns
- FlatTrieVerifierTests: Update field type and helper method casts to use IDb interface

All tests passing (22/22 FlatTrieVerifierTests).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Convert all constructors to primary constructor syntax (C# 12)
- Add dedicated MemDbWriteBatch that locks only once during commit
- Replace InMemoryWriteBatch with optimized batch that collects operations and commits atomically

Performance improvement:
- Before: Each Set() in batch acquired lock individually
- After: Single lock acquisition for entire batch commit

All tests passing (54/54).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace List with ArrayPoolList to use pooled arrays instead of allocating new arrays for each write batch.

Changes:
- Replace List<...> with ArrayPoolList<...> (initial capacity: 16)
- Dispose ArrayPoolList to return arrays to pool
- Restructure Dispose() to ensure proper cleanup

Performance impact: Reduces GC allocations for write batch operations.

All tests passing (54/54).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…in tests

Add neverPrune constructor parameter to SnapshotableMemDb and SnapshotableMemColumnsDb
to disable version pruning. Enable this option in PseudoNethermindModule for tests to
work around a bug in PruneVersionsOlderThan that removes versions still needed by active
snapshots.

The pruning bug will be fixed in a separate PR. For tests, memory is not a concern and
disabling pruning is the simplest workaround.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The previous constructor chain set _neverPrune AFTER calling GetColumnDb,
which meant all SnapshotableMemDb instances were created with neverPrune=false
even when neverPrune=true was passed to the constructor.

Fixed by creating a private constructor that sets _neverPrune before the
GetColumnDb loop, and routing all public constructors through it.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed three critical race conditions:

1. MemDbSortedView.MoveNext() and CurrentValue accessed _db without lock
   - Changed GetValueAtVersion() calls to GetAtVersion() which acquires lock
   - Prevents concurrent modification exceptions when iterating while writing

2. GetViewBetween() read _currentVersion without lock
   - Now captures version inside lock for consistency

These race conditions could cause exceptions or incorrect results when
accessing the database from multiple threads concurrently.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace SortedDictionary.Reverse() with forward iteration in
GetValueAtVersion and KeepOnlyLatestVersions to avoid O(n) intermediate
allocations. Add single-pass FindFirstKeyAtVersion/FindLastKeyAtVersion
helpers. Rewrite MemDbSortedView to iterate _db directly instead of
buffering all keys. Use ArrayPoolList in pruning methods.

Fix PruneVersionsOlderThan which incorrectly removed all entries below
minVersion — now keeps the latest pre-minVersion entry per key so active
snapshots can still resolve those keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…h GetViewBetween

Restructure the internal data store from SortedDictionary<(byte[], int), byte[]?> to
SortedSet<(byte[], int, byte[]?)> to leverage GetViewBetween for O(log n) point lookups
and efficient range iteration, fixing O(n) linear scan in GetValueAtVersion and O(n²)
re-scanning in MemDbSortedView.MoveNext().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
asdacap and others added 3 commits March 26, 2026 08:19
FindFirstKeyAtVersion now delegates to O(log n) GetValueAtVersion per
unique key. FindLastKeyAtVersion iterates in reverse to return early
instead of scanning the entire set forward.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix double-counting reads in bulk key accessor by using GetValueAtVersion
  directly under a single lock instead of calling Get() which re-increments
- Materialize GetAll/GetAllKeys/GetAllValues/GetAllAtVersion results under
  lock to prevent yield-while-holding-lock deadlock risk
- Prune old versions on write when no snapshots are active to prevent
  unbounded memory growth in the non-snapshot case
- Fix StartBefore to return true when key < firstKey so MoveNext correctly
  yields the first element

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tionary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@asdacap asdacap force-pushed the treeterm/faster-memdb-snapshot branch from d39d3c3 to 7af1c37 Compare March 26, 2026 00:19
@asdacap asdacap merged commit e14c4f7 into master Mar 26, 2026
417 checks passed
@asdacap asdacap deleted the treeterm/faster-memdb-snapshot branch March 26, 2026 00:39
benaadams added a commit that referenced this pull request Mar 26, 2026
commit f56e0ef
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 19:39:34 2026 +0000

    fix: use TryGetValue for account cache hit/miss metrics

    GetOrAdd + ThreadLocalStateTreeReads counter check misclassified misses
    as hits because StateProvider.GetState() increments the counter before
    calling _tree.Get() (ScopeWrapper.Get). The factory GetFromBaseTree
    doesn't increment any counter, so the delta is always zero.

    Switch account Get to TryGetValue + Set (on miss) so the TryGetValue
    result directly drives hit/miss classification. Storage Get retains
    GetOrAdd since LoadFromTreeStorage increments the counter itself.

    Remove unused _getFromBaseTree delegate.

commit 5fe1d11
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 18:51:33 2026 +0000

    refactor: unify SeqlockCache read path — always GetOrAdd on both prewarmer and main processor

    Remove the two-mode branching in ScopeWrapper.Get and StorageTreeWrapper.Get.
    Both prewarmer and main processor now use GetOrAdd (write on miss). On cache
    hit, HintGet is still called to populate the scope's _loadedAccounts.

    This means the main processor also populates the cache on miss, giving the
    next block more carry-forward hits. The populatePreBlockCache flag is retained
    only for StartWriteBatch (controls carry-forward buffering).

    Remove populatePreBlockCache from StorageTreeWrapper constructor since the
    read path no longer branches on it.

    Disable prewarming in ShouldAnnounceReorgOnDispose pruning test — the test
    exercises trie pruning infrastructure and was accidentally dependent on cache
    misses driving trie node loads. With prewarming (or the unified read path),
    cache hits reduce memory pressure, delaying persistence thresholds. The test
    was already fragile with prewarming enabled.

commit 79ac860
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 17:42:05 2026 +0000

    fix: subscribe to OnAccountUpdated for correct storage roots in carry-forward

    The base write batch fires OnAccountUpdated during Dispose() with the
    final storage root. CachePopulatingWriteBatch.Set() captured accounts
    before storage roots were updated, producing stale StorageRoot values
    in the carry-forward buffer. Now subscribe to OnAccountUpdated on the
    base batch to overwrite stale entries with corrected accounts.

    Fixes E2ESyncTests and AuRa test failures where trie vs flat storage
    roots diverged due to stale carry-forward data.

commit 8c2aa77
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 17:31:18 2026 +0000

    refactor: use List<T> for state carry-forward buffer, keep ConcurrentQueue for storage

    State writes (StateProvider.FlushToTree) are single-threaded, so List<T>
    avoids unnecessary interlocked overhead. Storage writes remain in
    ConcurrentQueue because PersistentStorageProvider.FlushToTree
    parallelizes across contracts via ParallelUnbalancedWork.

commit 9cf509f
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 17:14:42 2026 +0000

    fix: buffer carry-forward writes to avoid same-scope cache poisoning

    The CachePopulatingWriteBatch was writing directly into the SeqlockCache
    during FlushToTree. Since the same ScopeWrapper reads from that cache,
    post-commit values were immediately visible to subsequent reads within
    the same block processing scope (e.g. coinbase creation after tx
    execution), causing wrong state roots in EF state/VM tests.

    Fix: buffer carry-forward writes in ConcurrentQueues on PreBlockCaches
    during FlushToTree, then flush them into the SeqlockCaches from
    BranchProcessor after WaitAndClear (prewarm task done) and Reset
    (no more reads from current scope). This eliminates the same-scope
    feedback loop while preserving cross-block carry-forward.

commit 244445a
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 16:21:52 2026 +0000

    Tidy

commit 313c5cc
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 16:08:37 2026 +0000

    feat: cross-block SeqlockCache carry-forward with reorg invalidation

    Add cache-populating write-batch wrappers in PrewarmerScopeProvider that
    mirror account and storage writes into PreBlockCaches during FlushToTree.
    This allows the SeqlockCaches to carry forward block N's post-state as
    block N+1's pre-state, reducing trie reads on sequential processing.

    PreBlockCaches tracks committed block number/hash metadata and a legacy
    storage-clear flag for pre-EIP-6780 SELFDESTRUCT handling.
    BranchProcessor validates cache continuity before the first prewarm and
    invalidates on parent mismatch, errors, or legacy storage clears.

commit 2275710
Author: Tomass <155266802+zeroprooff@users.noreply.github.com>
Date:   Thu Mar 26 07:49:30 2026 +0000

    Fix incorrect NodeType in TrieNodeTests (#10917)

commit 707affd
Author: Damian Orzechowski <114909782+damian-orzechowski@users.noreply.github.com>
Date:   Thu Mar 26 08:48:19 2026 +0100

    Extend `CallResult` constructor visibility (#10949)

    Extend constructor visibility

commit a791dc6
Author: Damian Orzechowski <114909782+damian-orzechowski@users.noreply.github.com>
Date:   Thu Mar 26 08:48:00 2026 +0100

    New constructor (#10950)

commit e14c4f7
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Thu Mar 26 08:39:16 2026 +0800

    perf: add fast MVCC snapshots for MemDb in FlatDb tests (#10792)

    * feat: add MVCC snapshot support to MemDb for fast test snapshots

    Implements SnapshotableMemDb and SnapshotableMemColumnsDb with O(1)
    snapshot creation using Multi-Version Concurrency Control (MVCC),
    replacing the O(n) full-copy approach when snapshots are needed.

    Key features:
    - O(1) snapshot creation by capturing version numbers
    - Multiple concurrent snapshots with full isolation
    - Automatic version garbage collection on snapshot disposal
    - ISortedKeyValueStore support for sorted iteration
    - Thread-safe with proper locking

    This enables efficient snapshot-based testing, particularly for
    FlatDb tests where snapshots are created frequently. The new classes
    are drop-in replacements for MemDb/MemColumnsDb when snapshot
    support is required.

    All 21 unit tests passing.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * chore: migrate FlatDb tests to use SnapshotableMemColumnsDb

    Replace TestMemColumnsDb with SnapshotableMemColumnsDb in FlatDb test infrastructure to enable fast O(1) MVCC snapshots instead of slow O(n) full database copies.

    Changes:
    - PseudoNethermindModule: Register SnapshotableMemColumnsDb for FlatDbColumns
    - FlatTrieVerifierTests: Update field type and helper method casts to use IDb interface

    All tests passing (22/22 FlatTrieVerifierTests).

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * refactor: use primary constructors and optimize write batches

    - Convert all constructors to primary constructor syntax (C# 12)
    - Add dedicated MemDbWriteBatch that locks only once during commit
    - Replace InMemoryWriteBatch with optimized batch that collects operations and commits atomically

    Performance improvement:
    - Before: Each Set() in batch acquired lock individually
    - After: Single lock acquisition for entire batch commit

    All tests passing (54/54).

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * perf: use ArrayPoolList in MemDbWriteBatch to reduce GC pressure

    Replace List with ArrayPoolList to use pooled arrays instead of allocating new arrays for each write batch.

    Changes:
    - Replace List<...> with ArrayPoolList<...> (initial capacity: 16)
    - Dispose ArrayPoolList to return arrays to pool
    - Restructure Dispose() to ensure proper cleanup

    Performance impact: Reduces GC allocations for write batch operations.

    All tests passing (54/54).

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * refactor: Change SnapshottedMemDb from Dictionary<byte[], Version> to Dictionary<(byte[], int), byte[]>

    * fix: Add neverPrune option to SnapshotableMemDb to avoid pruning bug in tests

    Add neverPrune constructor parameter to SnapshotableMemDb and SnapshotableMemColumnsDb
    to disable version pruning. Enable this option in PseudoNethermindModule for tests to
    work around a bug in PruneVersionsOlderThan that removes versions still needed by active
    snapshots.

    The pruning bug will be fixed in a separate PR. For tests, memory is not a concern and
    disabling pruning is the simplest workaround.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * fix: Initialize neverPrune before creating column databases

    The previous constructor chain set _neverPrune AFTER calling GetColumnDb,
    which meant all SnapshotableMemDb instances were created with neverPrune=false
    even when neverPrune=true was passed to the constructor.

    Fixed by creating a private constructor that sets _neverPrune before the
    GetColumnDb loop, and routing all public constructors through it.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * fix: Critical thread-safety bugs in SnapshotableMemDb

    Fixed three critical race conditions:

    1. MemDbSortedView.MoveNext() and CurrentValue accessed _db without lock
       - Changed GetValueAtVersion() calls to GetAtVersion() which acquires lock
       - Prevents concurrent modification exceptions when iterating while writing

    2. GetViewBetween() read _currentVersion without lock
       - Now captures version inside lock for consistency

    These race conditions could cause exceptions or incorrect results when
    accessing the database from multiple threads concurrently.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * fix: Remove Reverse() allocations and fix PruneVersionsOlderThan bug

    Replace SortedDictionary.Reverse() with forward iteration in
    GetValueAtVersion and KeepOnlyLatestVersions to avoid O(n) intermediate
    allocations. Add single-pass FindFirstKeyAtVersion/FindLastKeyAtVersion
    helpers. Rewrite MemDbSortedView to iterate _db directly instead of
    buffering all keys. Use ArrayPoolList in pruning methods.

    Fix PruneVersionsOlderThan which incorrectly removed all entries below
    minVersion — now keeps the latest pre-minVersion entry per key so active
    snapshots can still resolve those keys.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * perf: Switch SnapshotableMemDb from SortedDictionary to SortedSet with GetViewBetween

    Restructure the internal data store from SortedDictionary<(byte[], int), byte[]?> to
    SortedSet<(byte[], int, byte[]?)> to leverage GetViewBetween for O(log n) point lookups
    and efficient range iteration, fixing O(n) linear scan in GetValueAtVersion and O(n²)
    re-scanning in MemDbSortedView.MoveNext().

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * perf: Simplify FindFirst/LastKeyAtVersion using GetValueAtVersion

    FindFirstKeyAtVersion now delegates to O(log n) GetValueAtVersion per
    unique key. FindLastKeyAtVersion iterates in reverse to return early
    instead of scanning the entire set forward.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * fix: Address PR review comments on SnapshotableMemDb

    - Fix double-counting reads in bulk key accessor by using GetValueAtVersion
      directly under a single lock instead of calling Get() which re-increments
    - Materialize GetAll/GetAllKeys/GetAllValues/GetAllAtVersion results under
      lock to prevent yield-while-holding-lock deadlock risk
    - Prune old versions on write when no snapshots are active to prevent
      unbounded memory growth in the non-snapshot case
    - Fix StartBefore to return true when key < firstKey so MoveNext correctly
      yields the first element

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * fix: Address PR review - use Lock type and Dictionary instead of IDictionary

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

    ---------

    Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

commit fbf3aff
Author: Ben {chmark} Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 00:17:29 2026 +0000

    test: add engine blockchain pyspec fixtures and fix BAL gas budget tracking (#10939)

    * feat: add engine blockchain test fixtures and fix BAL gas budget tracking

    - Add PyspecEngineBlockchainTestFixture base class for engine blockchain
      tests (blockchain_tests_engine directory)
    - Add engine blockchain test classes for all forks (Frontier through Osaka)
    - Add Amsterdam engine blockchain test fixtures for all Amsterdam EIPs
    - Fix BAL validation gas budget: use BlockGasUsed instead of SpentGas
      (SpentGas includes execution gas, BlockGasUsed is the block-level
      gas accounting that BAL validation should track)
    - Add regression test verifying BAL validation budget uses block gas

    * fix: version-aware ExecutionPayload creation and engine test improvements

    - Create version-appropriate ExecutionPayload type based on newPayloadVersion
      (V3 for version >= 3, V4 for version >= 5, base for older)
    - Only set V3/V4 fields on matching payload types (blob gas, BAL, slots)
    - Add missing NUnit using in Tests.cs
    - Use ExecutionPayload base type in BlockchainTestBase for broader compat
    - Add Convert_engine_payloads_uses_declared_payload_version test
    - Use Should().Equal instead of BeEquivalentTo for ordered list assertion

    * fix: restructure CreateExecutionPayload to explicit switch on derived type

    - Switch on most-derived first (V4 then V3) instead of two separate
      if-checks that both fire for V4 - makes the mutual exclusivity explicit
    - Move ParentBeaconBlockRoot to base properties (it lives on ExecutionPayload)
    - Set ExecutionRequests = [] for V3 payloads too (engine_newPayloadV4
      passes executionRequests as a parameter)

    * fix: only set ExecutionRequests=[] on V4 payloads, not V3 - caused 'Execution requests must be set' failures

    * refactor: use two if-checks instead of switch to avoid duplicating V3 blob gas fields

    * refactor: add HexToNumber/HexToNullableNumber helpers, document version mapping

    Address PR feedback:
    - Extract HexToNumber<T> and HexToNullableNumber<T> helpers to replace
      repeated (ulong)Bytes.FromHexString(...).ToUnsignedBigInteger() casts
    - Add comment documenting the version-to-type mapping source of truth
      (engine API method signatures in EngineRpcModule.*.cs)

    * fix: handle both JSON object and RLP hex string formats for withdrawals in engine test fixtures

    * fix: default ExecutionRequests to empty array for V4 engine tests on ExecutionPayloadV3

    * fix: parse executionRequests from params[3] for engine_newPayloadV4/V5

    params[3] is executionRequests for V4+ (Prague/Amsterdam), not
    validationError. The code was deserializing actual consolidation and
    withdrawal requests as validation errors, causing blocks to be
    processed with empty requests and producing wrong state roots.

    For V3: params = [payload, blobHashes, parentBeaconRoot, ?validationError]
    For V4+: params = [payload, blobHashes, parentBeaconRoot, executionRequests, ?validationError]

    * fixes

    * fix

    * feedback

    * fix: remove pre-merge engine test fixtures to fix CI timeout

    The engine blockchain test classes for Frontier through Paris were
    causing 20-minute CI timeouts. Pre-merge forks don't use the Engine
    API, and the regular BlockchainTests already cover them. Keep only
    Cancun+ where newPayload versions differ (blobs, execution requests,
    BAL).

    * fix: remove brittle hardcoded engine test name

    The Engine_from_state_test_on_top_of_genesis_should_not_sync test used
    .Single() with a hardcoded fixture name from v5.0.0. The name changed
    in v5.4.0, causing "Sequence contains no matching element". The same
    test case runs via the generic TestCaseSource fixture.

    * fix: add timeout to ProcessHelper.RunAndReadOutput

    WaitForExit with a 5s timeout before ReadToEnd prevents the static
    constructor of RuntimeInformation from blocking indefinitely when
    wmic hangs under heavy parallel test load. Returns null on timeout
    so PhysicalCoreCount falls back to Environment.ProcessorCount.

    * fix: restrict engine and Amsterdam tests to Linux only

    Engine blockchain tests and Amsterdam tests are heavy (full DI + Engine
    API per test, separate fixture download). They exceed the 20-minute CI
    timeout on Windows/macOS/ARM runners. Restrict to Linux where they
    complete within budget.

    * refactor: route engine tests through JsonRpcService instead of reflection

    Replace custom deserialization + Activator.CreateInstance + MethodInfo
    reflection with IJsonRpcService.SendRequestAsync. The test fixture
    params are passed as raw JSON through the real RPC pipeline, which
    handles method resolution and parameter binding. This removes the
    need for ParamsExecutionPayload intermediate types and per-version
    parameter marshalling in the test path.

    * fix: BAL memory leak and shared reference bug

    Two bugs in block-level access list handling:

    1. TracingEnabled was set to true once and never reset. Every subsequent
       block (including non-BAL blocks) accumulated AccountChanges,
       StorageChanges, and Change stack entries without bound.

    2. GeneratedBlockAccessList was a singleton reference assigned to Block
       objects via SetBlockAccessList, then Clear()ed on the next block.
       Previous blocks lost their BAL data.

    Fix: reset TracingEnabled per block based on the spec, and allocate a
    fresh BlockAccessList per block instead of reusing/clearing.

    * fix: skip heavy engine/Amsterdam tests in checked and no-intrinsics CI

    Add TEST_SKIP_HEAVY env var to the checked/no-intrinsics workflow.
    CiRunnerGuard checks this to unconditionally skip engine and Amsterdam
    blockchain tests in variant builds that don't benefit from running
    them (the main workflow already covers correctness).

    * Single spec look up

    * fix: cancel uncancelled Task.Delays that prevent GC of disposed containers

    RefCountingPersistenceReader: Task.Delay(60_000) in a loop with no
    cancellation token kept DB snapshots alive for 60s after scope disposal.
    With 32 concurrent tests, ~120 zombie snapshots accumulated at steady
    state. Fix: cancel via CTS on CleanUp.

    GCKeeper: Task.Delay(postBlockDelayMs) without cancellation held the
    GCKeeper closure alive after container disposal. Fix: add IDisposable,
    cancel via CTS on Dispose.

    * fix: guard GCKeeper.Dispose against double-dispose of CTS

    CancellationTokenSource.Cancel() throws ObjectDisposedException if
    the CTS was already disposed by a prior Dispose call (Autofac can
    call Dispose multiple times). Guard with try/catch.

    * refactor: address PR review feedback

    - Use reflection to get newPayload param count from IEngineRpcModule
      instead of hardcoding version-dependent logic
    - Use EngineApiVersions.Latest for default version fallbacks
    - Add Latest constants to EngineApiVersions.Fcu and NewPayload
    - Move GeneratedBlockAccessList reset into LoadSuggestedBlockAccessList
      so callers don't need to remember to create a new instance

    * fix: handle RPC-level errors in engine test negative cases

    When the Engine API rejects a payload at the RPC layer (e.g. wrong
    payload version like "ExecutionPayloadV2 expected"), it returns a
    JsonRpcErrorResponse instead of a PayloadStatusV1 with INVALID status.
    For negative tests (validationError is set), this is valid — the
    engine correctly rejected the block. Continue to the next payload
    instead of failing the assertion.

    * fix: dispose PeriodicTimer instances to prevent resource leaks

    DiscoveryApp, DiscoveryPersistenceManager, MultiSyncModeSelector,
    RetryCache: add using to PeriodicTimer so it is disposed when the
    loop exits. SessionMonitor: dispose _pingTimer on stop.

    * fix: cancel MultiSyncModeSelector timer on Dispose

    MultiSyncModeSelector starts a PeriodicTimer loop (1ms interval in
    tests) in its constructor. Dispose() only disposed the CTS without
    cancelling it first, so the timer loop ran forever, holding the
    entire container graph alive through the async state machine closure.
    This caused 10k+ zombie containers accumulating over the test run.

    * fix: skip heavy engine/Amsterdam tests in Flat DB CI jobs

    * perf: use NullTxPool and disable prewarmer in blockchain tests

    Blockchain pyspec tests process pre-built blocks from fixtures — they
    don't need a real TxPool (32 GB allocation per full run) or prewarmer.
    Use NullTxPool.Instance and set PreWarmStateConcurrency=0.

    * perf: disable prewarmer and revert ProcessHelper to original

    Set PreWarmStateOnBlockProcessing=false before module registration
    (Intercept runs too late). Revert ProcessHelper.RunAndReadOutput to
    original ReadToEnd-then-WaitForExit pattern — the reversed order
    caused deadlock on Linux when /proc/cpuinfo output exceeded pipe
    buffer size.

    * refactor: address PR review feedback on Task.Delay and Dispose patterns

    - Add TaskExtensions.DelaySafe helper for cancellable delays that
      return instead of throwing OperationCanceledException
    - Use DelaySafe in GCKeeper, TrieStore, RefCountingPersistenceReader,
      DataFeed
    - Guard GCKeeper.Dispose and MultiSyncModeSelector.Dispose with
      Interlocked.Exchange to prevent double-dispose
    - Add comment on SortedDictionary in BlockAccessList (pre-existing,
      count check on line 40 handles mismatched entry sets)

    * chore: remove codex-dotnet.ps1 and AGENTS.md changes

    * refactor: remove Convert engine payload tests

    These tests only verified JsonToEthereumTest.Convert which is no
    longer used in the engine test path (replaced by IJsonRpcService).
    Remove tests, helper method, and unused imports.

    * refactor: simplify after code review

    - Fix missing .Pass.Should().BeTrue() assertion in Amsterdam test
      fixtures (tests were silently passing without verifying results)
    - Remove dead Convert(TestEngineNewPayloadsJson[]) and all its
      helpers (CreateExecutionPayload, HexToNumber, ParseWithdrawal) —
      engine tests now go through IJsonRpcService directly
    - Cache reflection param count lookup in static dictionary
    - Remove empty SetUp() and unreachable _logManager ??= assignment
    - Remove comments that restate what code does

    * refactor: DelaySafe returns bool to indicate cancellation

    Callers can use `if (!await DelaySafe(...)) return;` instead of
    the separate `IsCancellationRequested` check after the delay.

    * fix: remove Setup() calls from runner projects after method removal

    * refactor: use CancelDisposeAndClear for CTS cleanup

    Replace manual Cancel+Dispose patterns with the existing
    CancellationTokenExtensions.CancelDisposeAndClear helper which
    handles thread-safety via Interlocked internally.

    * refactor: address automated code review feedback

    - Add descriptive NotSupportedException for missing engine methods
      instead of null-forgiving NullReferenceException
    - Improve genesisUsesTargetFork comment to explain why EIP-7928
      requires target fork rules at genesis (BlockAccessListHash)

commit a075b36
Author: Lukasz Rozmej <lukasz.rozmej@gmail.com>
Date:   Wed Mar 25 11:50:21 2026 +0100

    chore: add fix-nethtest agent skill for EF test debugging (#10903)

    * chore: add fix-nethtest agent skill for debugging EF test failures

    Adds a /fix-nethtest slash command that automates the diagnostic
    workflow for failing Ethereum Foundation tests run with
    Nethermind.Test.Runner (nethtest). The skill auto-detects test type
    (state vs blockchain), runs with tracing, classifies the failure, and
    guides root cause analysis through the EVM/spec/test harness code.

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

    * Update .agents/skills/fix-nethtest/SKILL.md

    Co-authored-by: Gaurav Dhiman <newmanifold000@gmail.com>

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    Co-authored-by: Gaurav Dhiman <newmanifold000@gmail.com>

commit ec00ffc
Author: emmmm <155267286+eeemmmmmm@users.noreply.github.com>
Date:   Wed Mar 25 07:45:26 2026 -0300

    fix: remove duplicate NewHeadBlock unsubscription in PoSSwitcher (#10938)

commit a429d12
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Wed Mar 25 17:14:34 2026 +0800

    Migrate TxGossipPolicy to DI using OrderedComponents (#10941)

    * refactor: migrate TxGossipPolicy to DI using OrderedComponents

    Move ITxGossipPolicy registration from manual init-step wiring
    (api.TxGossipPolicy.Policies.Add) to DI modules using AddLast<ITxGossipPolicy>
    for ordered registration and CompositeTxGossipPolicy as the composite wrapper.

    Remove IApiWithBlockchain.TxGossipPolicy so any remaining usage fails
    compilation. HiveModule overrides the composite with an empty instance
    to preserve hive test behavior.

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

    * refactor: address review feedback for TxGossipPolicy DI migration

    - Add AddLast<T, TImpl> and AddFirst<T, TImpl> overloads to
      OrderedComponentsContainerBuilderExtensions for auto-resolution
      (avoids ctx.Resolve anti-pattern)
    - Make AddDecorator<T, TDecorator> OrderedComponents-aware: when
      ordered components exist for T, registers the decorator as T
      taking T[] from the ordered collection
    - Add OrderedComponents<T>.Clear() and ClearOrderedComponents<T>()
      DSL for plugins that need to disable all ordered policies (Hive)
    - Move CompositeTxGossipPolicy to constructor injection in
      InitializeBlockchain and all subclasses (AuRa, Xdc, Optimism, Taiko)
    - Remove redundant composite registration from PseudoNetworkModule
      (NetworkModule already handles it)

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

    * docs: add anti-pattern section to DI patterns rules

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

    * fix: use AddComposite instead of AddDecorator for OrderedComponents

    AddComposite is the correct pattern for wrapping multiple ordered
    policies into a single ITxGossipPolicy. Make AddComposite
    OrderedComponents-aware: when ordered components exist for T,
    register TComposite as T via RegisterType (not RegisterComposite)
    so it receives T[] from the ordered collection. Guard against
    double registration.

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

    * refactor: resolve ITxGossipPolicy instead of CompositeTxGossipPolicy

    Consumers should depend on the interface, not the concrete composite.

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

    * refactor: use separate AddCompositeOrderedComponents DSL

    - Simplify AddLast<T, TImpl> and AddFirst<T, TImpl> to call the
      existing factory overload with a closure (avoids duplicating logic)
    - Add AddCompositeOrderedComponents<T, TComposite> as a separate DSL
      in OrderedComponentsContainerBuilderExtensions. This is separate from
      AddComposite because it uses RegisterType (not RegisterComposite) to
      receive T[] from OrderedComponents, and relaxes the safety check to
      allow this single T registration.
    - Revert AddComposite in ContainerBuilderExtensions to its original form
    - Remove SingleInstance from composite registration
    - Add unit tests verifying composite is resolved as T even when
      AddComposite is not the last registration

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

    * fix: test should call AddCompositeOrderedComponents before AddLast

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

    * fix: use Lazy<ITxGossipPolicy> to avoid eager DI resolution in init steps

    Resolving ITxGossipPolicy eagerly in InitializeBlockchain's constructor
    triggers the full sync dependency chain (SyncedTxGossipPolicy →
    MultiSyncModeSelector → ... → IBackgroundTaskScheduler) before the
    step has run. Use Lazy<ITxGossipPolicy> to defer resolution until
    CreateTxPool, when all dependencies are available.

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

    * docs: add comment explaining why ITxGossipPolicy is Lazy

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

    * refactor: add InvalidTxReceived disconnect reason

    Replace DisconnectReason.Other with a dedicated InvalidTxReceived
    reason in TxFloodController for peers sending invalid transactions.
    Maps to EthDisconnectReason.Other to preserve existing behavior.

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

    * fix: make CompositeTxGossipPolicy resolve policies lazily

    Move laziness into the composite itself: take Lazy<ITxGossipPolicy[]>
    so the ordered components (and their dependency chains) are only
    resolved when a gossip check is first made at runtime, not during
    DI construction. This avoids the eager chain
    SyncedTxGossipPolicy → ISyncModeSelector → ... → IBackgroundTaskScheduler
    which isn't available during init step construction.

    Revert Lazy<ITxGossipPolicy> from InitializeBlockchain and all
    subclasses — no longer needed with lazy composite.

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

    * Update src/Nethermind/Nethermind.Core.Test/Container/OrderedComponentsTests.cs

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update .agents/rules/di-patterns.md

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

commit 84e328f
Author: Damian Orzechowski <114909782+damian-orzechowski@users.noreply.github.com>
Date:   Tue Mar 24 14:19:36 2026 +0100

    Reinstate removed `IGasPolicy` methods and apply for eip-8037 (#10897)

    * Reinstate removed `IGasPolicy` methods and apply for eip-8037

    * Simplify ConsumeStorageWrite

    * Fix ConsumeStorageWrite

    * Refactor ConsumeStorageWrite

    * Fix whitespace

    * bit more readable

    * Revert "bit more readable"

    This reverts commit d08decc.

    * Remove duplicated code

    ---------

    Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>

commit 44f65f8
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Tue Mar 24 17:45:38 2026 +0800

    Connection reset metric (#10935)

commit 5a6c779
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Tue Mar 24 09:46:36 2026 +0800

    fix(flat): periodically clear ReadOnlySnapshotBundle cache (#10922)

    * fix(flat): periodically clear ReadOnlySnapshotBundle cache to prevent stale readers

    The snapshot bundle cache was only cleared on compaction/persistence events.
    If persistence stalled, old entries held RefCountingPersistenceReader leases
    indefinitely, preventing database compaction. Add a 15-second periodic timer
    to force-clear stale cache entries.

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

    * test: add unit test for periodic bundle cache clearing

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

    * refactor: iterate ConcurrentDictionary directly instead of copying keys

    Address PR review feedback: use TryRemove while iterating the
    ConcurrentDictionary directly, avoiding the temporary key list copy.

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

    * refactor: remove IsEmpty check as it acquires all bucket locks

    Address review feedback: ConcurrentDictionary.IsEmpty acquires all
    bucket locks, making it more expensive than just iterating directly.

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

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    Co-authored-by: Ben {chmark} Adams <thundercat@illyriad.co.uk>

commit d9e819e
Author: Ben {chmark} Adams <thundercat@illyriad.co.uk>
Date:   Mon Mar 23 22:42:46 2026 +0000

    test: update pyspec fixtures to v5.4.0/v5.5.1 and adapt to forked release layout (#10931)

    * Adapt pyspec fixtures to forked release layout

    * Fix Amsterdam SSTORE gas ordering

    * fix: add missing usings for TestItem and InsertCode extension in Eip8037 test

    * Restore prestate for invalid access-list state tests

    * fix: add missing usings and fix Ether extension in pre-Berlin access list test

    * hmm

    * fix: detect AccessList tx type by field presence, not list emptiness

    JsonToEthereumTest.Convert set TxType.AccessList only when the built
    access list was non-empty. Pyspec fixtures with empty accessLists: [[]]
    were misclassified as legacy txs, so pre-Berlin rejection didn't fire
    and the post-state root diverged.

    Check whether the accessLists/accessList JSON field was present rather
    than whether the parsed list has entries. Rebuild regression test
    programmatically using the expected hash from pyspec fixture
    test_eip2930_tx_validity[fork_Istanbul-invalid-state_test].

    * test: add Convert regression test for empty accessLists field detection

commit 057441c
Author: Gaurav Dhiman <newmanifold000@gmail.com>
Date:   Tue Mar 24 04:02:29 2026 +0530

    Fix buffer leak tests to use PooledBufferLeakDetector (#10887)

commit ae8e0ee
Author: Tomass <155266802+zeroprooff@users.noreply.github.com>
Date:   Mon Mar 23 22:32:03 2026 +0000

    Remove duplicate assertion in SnapshotCompactorTests (#10923)

    Co-authored-by: Ben {chmark} Adams <thundercat@illyriad.co.uk>

commit d4214dd
Author: Marc <Marchhill@users.noreply.github.com>
Date:   Mon Mar 23 17:59:29 2026 +0000

    Gnosis Osaka (#10906)

    osaka gnosis config

    Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>

commit 4228cb3
Author: Alexey Osipov <me@flcl.me>
Date:   Mon Mar 23 20:50:00 2026 +0300

    Dispose on exception (#10921)

    * Dispose more

    * Dispose in rare case

    * Catch more; cleanup

    ---------

    Co-authored-by: Ben {chmark} Adams <thundercat@illyriad.co.uk>

commit 871e9c7
Author: Marc <Marchhill@users.noreply.github.com>
Date:   Mon Mar 23 16:20:47 2026 +0000

    Fix AuRaMergeEngineModuleTests (#10872)

# Conflicts:
#	src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/AmsterdamTestFixture.cs
#	src/Nethermind/Ethereum.Blockchain.Pyspec.Test/PyspecTestFixture.cs
#	src/Nethermind/Ethereum.Transaction.Test/TransactionJsonTest.cs
#	src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs
#	src/Nethermind/Nethermind.State.Flat/Persistence/RefCountingPersistenceReader.cs
benaadams added a commit that referenced this pull request Mar 27, 2026
commit 5cdc84a
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Fri Mar 27 04:17:44 2026 +0000

    Don't throw in shutdown

commit 00922cb
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Fri Mar 27 04:10:29 2026 +0000

    fix

commit dbe10db
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Fri Mar 27 03:48:23 2026 +0000

    fixes

commit 797a4ab
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Fri Mar 27 03:29:39 2026 +0000

    fix: add canonical chain validation to startup repair and promote non-canonical persisted boundary

    - Startup reconciliation now also checks that the restored block is
      canonical; a non-canonical block matching the exact persisted state
      triggers the repair path
    - Add PromoteStartupBoundaryToMainChain to swap the persisted-state
      block into the main chain position when its state matches but its
      chain level markers are stale
    - Replace FindBlockByStateRoot (scanned all blocks at level) with
      FindCanonicalBlockByStateRoot (checks canonical block only) to
      avoid restoring the wrong fork
    - Remove FindRecoverableCanonicalBoundary backward walk — repair now
      fails loudly if no canonical match exists at the persisted height
    - Extract FlatPersistedStateInfoProvider from FlatDbManager into its
      own class, registered via DI instead of Bind<>
    - Harden StartupBlockTreeFixer to only suggest blocks descending from
      the repaired head using eligible parent hash tracking per level
    - Update tests for canonical validation, promotion, and per-level
      fixer suggestion filtering

commit c9f800f
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Fri Mar 27 02:35:31 2026 +0000

    fix: use named argument for genesisBlockNumber in XdcBlockTree base call

    The new optional persistedStateInfoProvider parameter on BlockTree's
    constructor sits before genesisBlockNumber, causing a type mismatch
    when genesisBlockNumber was passed positionally.

commit 24e0f91
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Fri Mar 27 02:32:56 2026 +0000

    simplify: reduce duplication and fix minor issues in FlatDB stall fix

    - Extract CreateTreeWithForkAtBlock2 helper to eliminate 7x repeated
      block tree setup in BlockTreeTests (net -74 lines)
    - Extract SetupSaturatedCompactorQueue helper for backpressure tests
    - Replace StubPersistedStateInfoProvider with NSubstitute factory
    - Expose metadata address constants as internal for test use instead
      of inline byte array reconstruction
    - Move BestPersistedState setter batch guard to avoid empty write
      batch allocation when value is null
    - Reuse captured persistedStateInfoValue in startup log instead of
      re-querying the provider
    - Replace flaky Task.Delay(250) with polling loop in AddressWarmer test

commit 7333545
Author: Ben Adams <thundercat@illyriad.co.uk>
Date:   Fri Mar 27 02:07:46 2026 +0000

    fix: prevent FlatDB live stall and make restart recovery exact

    - Replace unbounded producer-side wait in FlatDbManager.AddSnapshot with
      inline drain fallback when compactor queue is saturated
    - Add processing watchdog, startup diagnostics, and backpressure metrics
    - Harden AddressWarmer.Wait, WaitAndClear, and tx execution cancellation
      with periodic warnings instead of silent unbounded blocking
    - Persist exact boundary hash metadata alongside block number for startup
    - Add startup reconciliation that validates restored head against FlatDB
      persisted StateId and repairs stale metadata before setting Head
    - Enrich the max-branch-size exception with persisted-boundary diagnostics
    - Add IPersistedStateInfoProvider seam for cross-project startup visibility
    - Add regression tests for backpressure, exact restore, fixer, force-persist,
      and end-to-end restart recovery

commit 2275710
Author: Tomass <155266802+zeroprooff@users.noreply.github.com>
Date:   Thu Mar 26 07:49:30 2026 +0000

    Fix incorrect NodeType in TrieNodeTests (#10917)

commit 707affd
Author: Damian Orzechowski <114909782+damian-orzechowski@users.noreply.github.com>
Date:   Thu Mar 26 08:48:19 2026 +0100

    Extend `CallResult` constructor visibility (#10949)

    Extend constructor visibility

commit a791dc6
Author: Damian Orzechowski <114909782+damian-orzechowski@users.noreply.github.com>
Date:   Thu Mar 26 08:48:00 2026 +0100

    New constructor (#10950)

commit e14c4f7
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Thu Mar 26 08:39:16 2026 +0800

    perf: add fast MVCC snapshots for MemDb in FlatDb tests (#10792)

    * feat: add MVCC snapshot support to MemDb for fast test snapshots

    Implements SnapshotableMemDb and SnapshotableMemColumnsDb with O(1)
    snapshot creation using Multi-Version Concurrency Control (MVCC),
    replacing the O(n) full-copy approach when snapshots are needed.

    Key features:
    - O(1) snapshot creation by capturing version numbers
    - Multiple concurrent snapshots with full isolation
    - Automatic version garbage collection on snapshot disposal
    - ISortedKeyValueStore support for sorted iteration
    - Thread-safe with proper locking

    This enables efficient snapshot-based testing, particularly for
    FlatDb tests where snapshots are created frequently. The new classes
    are drop-in replacements for MemDb/MemColumnsDb when snapshot
    support is required.

    All 21 unit tests passing.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * chore: migrate FlatDb tests to use SnapshotableMemColumnsDb

    Replace TestMemColumnsDb with SnapshotableMemColumnsDb in FlatDb test infrastructure to enable fast O(1) MVCC snapshots instead of slow O(n) full database copies.

    Changes:
    - PseudoNethermindModule: Register SnapshotableMemColumnsDb for FlatDbColumns
    - FlatTrieVerifierTests: Update field type and helper method casts to use IDb interface

    All tests passing (22/22 FlatTrieVerifierTests).

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * refactor: use primary constructors and optimize write batches

    - Convert all constructors to primary constructor syntax (C# 12)
    - Add dedicated MemDbWriteBatch that locks only once during commit
    - Replace InMemoryWriteBatch with optimized batch that collects operations and commits atomically

    Performance improvement:
    - Before: Each Set() in batch acquired lock individually
    - After: Single lock acquisition for entire batch commit

    All tests passing (54/54).

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * perf: use ArrayPoolList in MemDbWriteBatch to reduce GC pressure

    Replace List with ArrayPoolList to use pooled arrays instead of allocating new arrays for each write batch.

    Changes:
    - Replace List<...> with ArrayPoolList<...> (initial capacity: 16)
    - Dispose ArrayPoolList to return arrays to pool
    - Restructure Dispose() to ensure proper cleanup

    Performance impact: Reduces GC allocations for write batch operations.

    All tests passing (54/54).

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * refactor: Change SnapshottedMemDb from Dictionary<byte[], Version> to Dictionary<(byte[], int), byte[]>

    * fix: Add neverPrune option to SnapshotableMemDb to avoid pruning bug in tests

    Add neverPrune constructor parameter to SnapshotableMemDb and SnapshotableMemColumnsDb
    to disable version pruning. Enable this option in PseudoNethermindModule for tests to
    work around a bug in PruneVersionsOlderThan that removes versions still needed by active
    snapshots.

    The pruning bug will be fixed in a separate PR. For tests, memory is not a concern and
    disabling pruning is the simplest workaround.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * fix: Initialize neverPrune before creating column databases

    The previous constructor chain set _neverPrune AFTER calling GetColumnDb,
    which meant all SnapshotableMemDb instances were created with neverPrune=false
    even when neverPrune=true was passed to the constructor.

    Fixed by creating a private constructor that sets _neverPrune before the
    GetColumnDb loop, and routing all public constructors through it.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * fix: Critical thread-safety bugs in SnapshotableMemDb

    Fixed three critical race conditions:

    1. MemDbSortedView.MoveNext() and CurrentValue accessed _db without lock
       - Changed GetValueAtVersion() calls to GetAtVersion() which acquires lock
       - Prevents concurrent modification exceptions when iterating while writing

    2. GetViewBetween() read _currentVersion without lock
       - Now captures version inside lock for consistency

    These race conditions could cause exceptions or incorrect results when
    accessing the database from multiple threads concurrently.

    Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

    * fix: Remove Reverse() allocations and fix PruneVersionsOlderThan bug

    Replace SortedDictionary.Reverse() with forward iteration in
    GetValueAtVersion and KeepOnlyLatestVersions to avoid O(n) intermediate
    allocations. Add single-pass FindFirstKeyAtVersion/FindLastKeyAtVersion
    helpers. Rewrite MemDbSortedView to iterate _db directly instead of
    buffering all keys. Use ArrayPoolList in pruning methods.

    Fix PruneVersionsOlderThan which incorrectly removed all entries below
    minVersion — now keeps the latest pre-minVersion entry per key so active
    snapshots can still resolve those keys.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * perf: Switch SnapshotableMemDb from SortedDictionary to SortedSet with GetViewBetween

    Restructure the internal data store from SortedDictionary<(byte[], int), byte[]?> to
    SortedSet<(byte[], int, byte[]?)> to leverage GetViewBetween for O(log n) point lookups
    and efficient range iteration, fixing O(n) linear scan in GetValueAtVersion and O(n²)
    re-scanning in MemDbSortedView.MoveNext().

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * perf: Simplify FindFirst/LastKeyAtVersion using GetValueAtVersion

    FindFirstKeyAtVersion now delegates to O(log n) GetValueAtVersion per
    unique key. FindLastKeyAtVersion iterates in reverse to return early
    instead of scanning the entire set forward.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * fix: Address PR review comments on SnapshotableMemDb

    - Fix double-counting reads in bulk key accessor by using GetValueAtVersion
      directly under a single lock instead of calling Get() which re-increments
    - Materialize GetAll/GetAllKeys/GetAllValues/GetAllAtVersion results under
      lock to prevent yield-while-holding-lock deadlock risk
    - Prune old versions on write when no snapshots are active to prevent
      unbounded memory growth in the non-snapshot case
    - Fix StartBefore to return true when key < firstKey so MoveNext correctly
      yields the first element

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * fix: Address PR review - use Lock type and Dictionary instead of IDictionary

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

    ---------

    Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>

commit fbf3aff
Author: Ben {chmark} Adams <thundercat@illyriad.co.uk>
Date:   Thu Mar 26 00:17:29 2026 +0000

    test: add engine blockchain pyspec fixtures and fix BAL gas budget tracking (#10939)

    * feat: add engine blockchain test fixtures and fix BAL gas budget tracking

    - Add PyspecEngineBlockchainTestFixture base class for engine blockchain
      tests (blockchain_tests_engine directory)
    - Add engine blockchain test classes for all forks (Frontier through Osaka)
    - Add Amsterdam engine blockchain test fixtures for all Amsterdam EIPs
    - Fix BAL validation gas budget: use BlockGasUsed instead of SpentGas
      (SpentGas includes execution gas, BlockGasUsed is the block-level
      gas accounting that BAL validation should track)
    - Add regression test verifying BAL validation budget uses block gas

    * fix: version-aware ExecutionPayload creation and engine test improvements

    - Create version-appropriate ExecutionPayload type based on newPayloadVersion
      (V3 for version >= 3, V4 for version >= 5, base for older)
    - Only set V3/V4 fields on matching payload types (blob gas, BAL, slots)
    - Add missing NUnit using in Tests.cs
    - Use ExecutionPayload base type in BlockchainTestBase for broader compat
    - Add Convert_engine_payloads_uses_declared_payload_version test
    - Use Should().Equal instead of BeEquivalentTo for ordered list assertion

    * fix: restructure CreateExecutionPayload to explicit switch on derived type

    - Switch on most-derived first (V4 then V3) instead of two separate
      if-checks that both fire for V4 - makes the mutual exclusivity explicit
    - Move ParentBeaconBlockRoot to base properties (it lives on ExecutionPayload)
    - Set ExecutionRequests = [] for V3 payloads too (engine_newPayloadV4
      passes executionRequests as a parameter)

    * fix: only set ExecutionRequests=[] on V4 payloads, not V3 - caused 'Execution requests must be set' failures

    * refactor: use two if-checks instead of switch to avoid duplicating V3 blob gas fields

    * refactor: add HexToNumber/HexToNullableNumber helpers, document version mapping

    Address PR feedback:
    - Extract HexToNumber<T> and HexToNullableNumber<T> helpers to replace
      repeated (ulong)Bytes.FromHexString(...).ToUnsignedBigInteger() casts
    - Add comment documenting the version-to-type mapping source of truth
      (engine API method signatures in EngineRpcModule.*.cs)

    * fix: handle both JSON object and RLP hex string formats for withdrawals in engine test fixtures

    * fix: default ExecutionRequests to empty array for V4 engine tests on ExecutionPayloadV3

    * fix: parse executionRequests from params[3] for engine_newPayloadV4/V5

    params[3] is executionRequests for V4+ (Prague/Amsterdam), not
    validationError. The code was deserializing actual consolidation and
    withdrawal requests as validation errors, causing blocks to be
    processed with empty requests and producing wrong state roots.

    For V3: params = [payload, blobHashes, parentBeaconRoot, ?validationError]
    For V4+: params = [payload, blobHashes, parentBeaconRoot, executionRequests, ?validationError]

    * fixes

    * fix

    * feedback

    * fix: remove pre-merge engine test fixtures to fix CI timeout

    The engine blockchain test classes for Frontier through Paris were
    causing 20-minute CI timeouts. Pre-merge forks don't use the Engine
    API, and the regular BlockchainTests already cover them. Keep only
    Cancun+ where newPayload versions differ (blobs, execution requests,
    BAL).

    * fix: remove brittle hardcoded engine test name

    The Engine_from_state_test_on_top_of_genesis_should_not_sync test used
    .Single() with a hardcoded fixture name from v5.0.0. The name changed
    in v5.4.0, causing "Sequence contains no matching element". The same
    test case runs via the generic TestCaseSource fixture.

    * fix: add timeout to ProcessHelper.RunAndReadOutput

    WaitForExit with a 5s timeout before ReadToEnd prevents the static
    constructor of RuntimeInformation from blocking indefinitely when
    wmic hangs under heavy parallel test load. Returns null on timeout
    so PhysicalCoreCount falls back to Environment.ProcessorCount.

    * fix: restrict engine and Amsterdam tests to Linux only

    Engine blockchain tests and Amsterdam tests are heavy (full DI + Engine
    API per test, separate fixture download). They exceed the 20-minute CI
    timeout on Windows/macOS/ARM runners. Restrict to Linux where they
    complete within budget.

    * refactor: route engine tests through JsonRpcService instead of reflection

    Replace custom deserialization + Activator.CreateInstance + MethodInfo
    reflection with IJsonRpcService.SendRequestAsync. The test fixture
    params are passed as raw JSON through the real RPC pipeline, which
    handles method resolution and parameter binding. This removes the
    need for ParamsExecutionPayload intermediate types and per-version
    parameter marshalling in the test path.

    * fix: BAL memory leak and shared reference bug

    Two bugs in block-level access list handling:

    1. TracingEnabled was set to true once and never reset. Every subsequent
       block (including non-BAL blocks) accumulated AccountChanges,
       StorageChanges, and Change stack entries without bound.

    2. GeneratedBlockAccessList was a singleton reference assigned to Block
       objects via SetBlockAccessList, then Clear()ed on the next block.
       Previous blocks lost their BAL data.

    Fix: reset TracingEnabled per block based on the spec, and allocate a
    fresh BlockAccessList per block instead of reusing/clearing.

    * fix: skip heavy engine/Amsterdam tests in checked and no-intrinsics CI

    Add TEST_SKIP_HEAVY env var to the checked/no-intrinsics workflow.
    CiRunnerGuard checks this to unconditionally skip engine and Amsterdam
    blockchain tests in variant builds that don't benefit from running
    them (the main workflow already covers correctness).

    * Single spec look up

    * fix: cancel uncancelled Task.Delays that prevent GC of disposed containers

    RefCountingPersistenceReader: Task.Delay(60_000) in a loop with no
    cancellation token kept DB snapshots alive for 60s after scope disposal.
    With 32 concurrent tests, ~120 zombie snapshots accumulated at steady
    state. Fix: cancel via CTS on CleanUp.

    GCKeeper: Task.Delay(postBlockDelayMs) without cancellation held the
    GCKeeper closure alive after container disposal. Fix: add IDisposable,
    cancel via CTS on Dispose.

    * fix: guard GCKeeper.Dispose against double-dispose of CTS

    CancellationTokenSource.Cancel() throws ObjectDisposedException if
    the CTS was already disposed by a prior Dispose call (Autofac can
    call Dispose multiple times). Guard with try/catch.

    * refactor: address PR review feedback

    - Use reflection to get newPayload param count from IEngineRpcModule
      instead of hardcoding version-dependent logic
    - Use EngineApiVersions.Latest for default version fallbacks
    - Add Latest constants to EngineApiVersions.Fcu and NewPayload
    - Move GeneratedBlockAccessList reset into LoadSuggestedBlockAccessList
      so callers don't need to remember to create a new instance

    * fix: handle RPC-level errors in engine test negative cases

    When the Engine API rejects a payload at the RPC layer (e.g. wrong
    payload version like "ExecutionPayloadV2 expected"), it returns a
    JsonRpcErrorResponse instead of a PayloadStatusV1 with INVALID status.
    For negative tests (validationError is set), this is valid — the
    engine correctly rejected the block. Continue to the next payload
    instead of failing the assertion.

    * fix: dispose PeriodicTimer instances to prevent resource leaks

    DiscoveryApp, DiscoveryPersistenceManager, MultiSyncModeSelector,
    RetryCache: add using to PeriodicTimer so it is disposed when the
    loop exits. SessionMonitor: dispose _pingTimer on stop.

    * fix: cancel MultiSyncModeSelector timer on Dispose

    MultiSyncModeSelector starts a PeriodicTimer loop (1ms interval in
    tests) in its constructor. Dispose() only disposed the CTS without
    cancelling it first, so the timer loop ran forever, holding the
    entire container graph alive through the async state machine closure.
    This caused 10k+ zombie containers accumulating over the test run.

    * fix: skip heavy engine/Amsterdam tests in Flat DB CI jobs

    * perf: use NullTxPool and disable prewarmer in blockchain tests

    Blockchain pyspec tests process pre-built blocks from fixtures — they
    don't need a real TxPool (32 GB allocation per full run) or prewarmer.
    Use NullTxPool.Instance and set PreWarmStateConcurrency=0.

    * perf: disable prewarmer and revert ProcessHelper to original

    Set PreWarmStateOnBlockProcessing=false before module registration
    (Intercept runs too late). Revert ProcessHelper.RunAndReadOutput to
    original ReadToEnd-then-WaitForExit pattern — the reversed order
    caused deadlock on Linux when /proc/cpuinfo output exceeded pipe
    buffer size.

    * refactor: address PR review feedback on Task.Delay and Dispose patterns

    - Add TaskExtensions.DelaySafe helper for cancellable delays that
      return instead of throwing OperationCanceledException
    - Use DelaySafe in GCKeeper, TrieStore, RefCountingPersistenceReader,
      DataFeed
    - Guard GCKeeper.Dispose and MultiSyncModeSelector.Dispose with
      Interlocked.Exchange to prevent double-dispose
    - Add comment on SortedDictionary in BlockAccessList (pre-existing,
      count check on line 40 handles mismatched entry sets)

    * chore: remove codex-dotnet.ps1 and AGENTS.md changes

    * refactor: remove Convert engine payload tests

    These tests only verified JsonToEthereumTest.Convert which is no
    longer used in the engine test path (replaced by IJsonRpcService).
    Remove tests, helper method, and unused imports.

    * refactor: simplify after code review

    - Fix missing .Pass.Should().BeTrue() assertion in Amsterdam test
      fixtures (tests were silently passing without verifying results)
    - Remove dead Convert(TestEngineNewPayloadsJson[]) and all its
      helpers (CreateExecutionPayload, HexToNumber, ParseWithdrawal) —
      engine tests now go through IJsonRpcService directly
    - Cache reflection param count lookup in static dictionary
    - Remove empty SetUp() and unreachable _logManager ??= assignment
    - Remove comments that restate what code does

    * refactor: DelaySafe returns bool to indicate cancellation

    Callers can use `if (!await DelaySafe(...)) return;` instead of
    the separate `IsCancellationRequested` check after the delay.

    * fix: remove Setup() calls from runner projects after method removal

    * refactor: use CancelDisposeAndClear for CTS cleanup

    Replace manual Cancel+Dispose patterns with the existing
    CancellationTokenExtensions.CancelDisposeAndClear helper which
    handles thread-safety via Interlocked internally.

    * refactor: address automated code review feedback

    - Add descriptive NotSupportedException for missing engine methods
      instead of null-forgiving NullReferenceException
    - Improve genesisUsesTargetFork comment to explain why EIP-7928
      requires target fork rules at genesis (BlockAccessListHash)

commit a075b36
Author: Lukasz Rozmej <lukasz.rozmej@gmail.com>
Date:   Wed Mar 25 11:50:21 2026 +0100

    chore: add fix-nethtest agent skill for EF test debugging (#10903)

    * chore: add fix-nethtest agent skill for debugging EF test failures

    Adds a /fix-nethtest slash command that automates the diagnostic
    workflow for failing Ethereum Foundation tests run with
    Nethermind.Test.Runner (nethtest). The skill auto-detects test type
    (state vs blockchain), runs with tracing, classifies the failure, and
    guides root cause analysis through the EVM/spec/test harness code.

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

    * Update .agents/skills/fix-nethtest/SKILL.md

    Co-authored-by: Gaurav Dhiman <newmanifold000@gmail.com>

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    Co-authored-by: Gaurav Dhiman <newmanifold000@gmail.com>

commit ec00ffc
Author: emmmm <155267286+eeemmmmmm@users.noreply.github.com>
Date:   Wed Mar 25 07:45:26 2026 -0300

    fix: remove duplicate NewHeadBlock unsubscription in PoSSwitcher (#10938)

commit a429d12
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Wed Mar 25 17:14:34 2026 +0800

    Migrate TxGossipPolicy to DI using OrderedComponents (#10941)

    * refactor: migrate TxGossipPolicy to DI using OrderedComponents

    Move ITxGossipPolicy registration from manual init-step wiring
    (api.TxGossipPolicy.Policies.Add) to DI modules using AddLast<ITxGossipPolicy>
    for ordered registration and CompositeTxGossipPolicy as the composite wrapper.

    Remove IApiWithBlockchain.TxGossipPolicy so any remaining usage fails
    compilation. HiveModule overrides the composite with an empty instance
    to preserve hive test behavior.

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

    * refactor: address review feedback for TxGossipPolicy DI migration

    - Add AddLast<T, TImpl> and AddFirst<T, TImpl> overloads to
      OrderedComponentsContainerBuilderExtensions for auto-resolution
      (avoids ctx.Resolve anti-pattern)
    - Make AddDecorator<T, TDecorator> OrderedComponents-aware: when
      ordered components exist for T, registers the decorator as T
      taking T[] from the ordered collection
    - Add OrderedComponents<T>.Clear() and ClearOrderedComponents<T>()
      DSL for plugins that need to disable all ordered policies (Hive)
    - Move CompositeTxGossipPolicy to constructor injection in
      InitializeBlockchain and all subclasses (AuRa, Xdc, Optimism, Taiko)
    - Remove redundant composite registration from PseudoNetworkModule
      (NetworkModule already handles it)

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

    * docs: add anti-pattern section to DI patterns rules

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

    * fix: use AddComposite instead of AddDecorator for OrderedComponents

    AddComposite is the correct pattern for wrapping multiple ordered
    policies into a single ITxGossipPolicy. Make AddComposite
    OrderedComponents-aware: when ordered components exist for T,
    register TComposite as T via RegisterType (not RegisterComposite)
    so it receives T[] from the ordered collection. Guard against
    double registration.

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

    * refactor: resolve ITxGossipPolicy instead of CompositeTxGossipPolicy

    Consumers should depend on the interface, not the concrete composite.

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

    * refactor: use separate AddCompositeOrderedComponents DSL

    - Simplify AddLast<T, TImpl> and AddFirst<T, TImpl> to call the
      existing factory overload with a closure (avoids duplicating logic)
    - Add AddCompositeOrderedComponents<T, TComposite> as a separate DSL
      in OrderedComponentsContainerBuilderExtensions. This is separate from
      AddComposite because it uses RegisterType (not RegisterComposite) to
      receive T[] from OrderedComponents, and relaxes the safety check to
      allow this single T registration.
    - Revert AddComposite in ContainerBuilderExtensions to its original form
    - Remove SingleInstance from composite registration
    - Add unit tests verifying composite is resolved as T even when
      AddComposite is not the last registration

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

    * fix: test should call AddCompositeOrderedComponents before AddLast

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

    * fix: use Lazy<ITxGossipPolicy> to avoid eager DI resolution in init steps

    Resolving ITxGossipPolicy eagerly in InitializeBlockchain's constructor
    triggers the full sync dependency chain (SyncedTxGossipPolicy →
    MultiSyncModeSelector → ... → IBackgroundTaskScheduler) before the
    step has run. Use Lazy<ITxGossipPolicy> to defer resolution until
    CreateTxPool, when all dependencies are available.

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

    * docs: add comment explaining why ITxGossipPolicy is Lazy

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

    * refactor: add InvalidTxReceived disconnect reason

    Replace DisconnectReason.Other with a dedicated InvalidTxReceived
    reason in TxFloodController for peers sending invalid transactions.
    Maps to EthDisconnectReason.Other to preserve existing behavior.

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

    * fix: make CompositeTxGossipPolicy resolve policies lazily

    Move laziness into the composite itself: take Lazy<ITxGossipPolicy[]>
    so the ordered components (and their dependency chains) are only
    resolved when a gossip check is first made at runtime, not during
    DI construction. This avoids the eager chain
    SyncedTxGossipPolicy → ISyncModeSelector → ... → IBackgroundTaskScheduler
    which isn't available during init step construction.

    Revert Lazy<ITxGossipPolicy> from InitializeBlockchain and all
    subclasses — no longer needed with lazy composite.

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

    * Update src/Nethermind/Nethermind.Core.Test/Container/OrderedComponentsTests.cs

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    * Update .agents/rules/di-patterns.md

    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

commit 84e328f
Author: Damian Orzechowski <114909782+damian-orzechowski@users.noreply.github.com>
Date:   Tue Mar 24 14:19:36 2026 +0100

    Reinstate removed `IGasPolicy` methods and apply for eip-8037 (#10897)

    * Reinstate removed `IGasPolicy` methods and apply for eip-8037

    * Simplify ConsumeStorageWrite

    * Fix ConsumeStorageWrite

    * Refactor ConsumeStorageWrite

    * Fix whitespace

    * bit more readable

    * Revert "bit more readable"

    This reverts commit d08decc.

    * Remove duplicated code

    ---------

    Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>

commit 44f65f8
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Tue Mar 24 17:45:38 2026 +0800

    Connection reset metric (#10935)

commit 5a6c779
Author: Amirul Ashraf <asdacap@gmail.com>
Date:   Tue Mar 24 09:46:36 2026 +0800

    fix(flat): periodically clear ReadOnlySnapshotBundle cache (#10922)

    * fix(flat): periodically clear ReadOnlySnapshotBundle cache to prevent stale readers

    The snapshot bundle cache was only cleared on compaction/persistence events.
    If persistence stalled, old entries held RefCountingPersistenceReader leases
    indefinitely, preventing database compaction. Add a 15-second periodic timer
    to force-clear stale cache entries.

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

    * test: add unit test for periodic bundle cache clearing

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

    * refactor: iterate ConcurrentDictionary directly instead of copying keys

    Address PR review feedback: use TryRemove while iterating the
    ConcurrentDictionary directly, avoiding the temporary key list copy.

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

    * refactor: remove IsEmpty check as it acquires all bucket locks

    Address review feedback: ConcurrentDictionary.IsEmpty acquires all
    bucket locks, making it more expensive than just iterating directly.

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

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    Co-authored-by: Ben {chmark} Adams <thundercat@illyriad.co.uk>

commit d9e819e
Author: Ben {chmark} Adams <thundercat@illyriad.co.uk>
Date:   Mon Mar 23 22:42:46 2026 +0000

    test: update pyspec fixtures to v5.4.0/v5.5.1 and adapt to forked release layout (#10931)

    * Adapt pyspec fixtures to forked release layout

    * Fix Amsterdam SSTORE gas ordering

    * fix: add missing usings for TestItem and InsertCode extension in Eip8037 test

    * Restore prestate for invalid access-list state tests

    * fix: add missing usings and fix Ether extension in pre-Berlin access list test

    * hmm

    * fix: detect AccessList tx type by field presence, not list emptiness

    JsonToEthereumTest.Convert set TxType.AccessList only when the built
    access list was non-empty. Pyspec fixtures with empty accessLists: [[]]
    were misclassified as legacy txs, so pre-Berlin rejection didn't fire
    and the post-state root diverged.

    Check whether the accessLists/accessList JSON field was present rather
    than whether the parsed list has entries. Rebuild regression test
    programmatically using the expected hash from pyspec fixture
    test_eip2930_tx_validity[fork_Istanbul-invalid-state_test].

    * test: add Convert regression test for empty accessLists field detection

commit 057441c
Author: Gaurav Dhiman <newmanifold000@gmail.com>
Date:   Tue Mar 24 04:02:29 2026 +0530

    Fix buffer leak tests to use PooledBufferLeakDetector (#10887)

commit ae8e0ee
Author: Tomass <155266802+zeroprooff@users.noreply.github.com>
Date:   Mon Mar 23 22:32:03 2026 +0000

    Remove duplicate assertion in SnapshotCompactorTests (#10923)

    Co-authored-by: Ben {chmark} Adams <thundercat@illyriad.co.uk>

commit d4214dd
Author: Marc <Marchhill@users.noreply.github.com>
Date:   Mon Mar 23 17:59:29 2026 +0000

    Gnosis Osaka (#10906)

    osaka gnosis config

    Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>

commit 4228cb3
Author: Alexey Osipov <me@flcl.me>
Date:   Mon Mar 23 20:50:00 2026 +0300

    Dispose on exception (#10921)

    * Dispose more

    * Dispose in rare case

    * Catch more; cleanup

    ---------

    Co-authored-by: Ben {chmark} Adams <thundercat@illyriad.co.uk>

commit 871e9c7
Author: Marc <Marchhill@users.noreply.github.com>
Date:   Mon Mar 23 16:20:47 2026 +0000

    Fix AuRaMergeEngineModuleTests (#10872)
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