Skip to content

perf(bal): account-presence bitmap + storage_reads on column index — skip-merge#11661

Merged
LukaszRozmej merged 255 commits into
masterfrom
perf/bal-verify-only-skip-merge
May 20, 2026
Merged

perf(bal): account-presence bitmap + storage_reads on column index — skip-merge#11661
LukaszRozmej merged 255 commits into
masterfrom
perf/bal-verify-only-skip-merge

Conversation

@LukaszRozmej

@LukaszRozmej LukaszRozmej commented May 18, 2026

Copy link
Copy Markdown
Member

DRAFT — design exploration. Stacks on #11659 (verify-only fast path).

Not for merge yet. Both phases now work end-to-end. Still marked draft for design review before unmarking.

What this PR does

On the parallel-validation path, skip the per-tx GeneratedBlockAccessList.Merge entirely when verify-only mode is active. This builds on the encode+Keccak skip that already shipped in #11659. Together with that PR, the verify-only path now performs:

Today (after #11659) With this PR
Per-tx: column-index Add(slice) + target.Merge(slice) (thousands of GeneratedAccountChanges / GeneratedSlotChanges / List / SortedSet allocations per block) Per-tx: column-index Add(slice) only
End-of-block: structural-equivalence walk over GeneratedBlockAccessList.AccountChanges End-of-block: FindStructuralMismatch over column-index data
Slow-path diagnostic walk: iterates GeneratedBlockAccessList.AccountChanges Slow-path diagnostic walk: column-index EnumerateMarkedOrdinals + per-row lane lookups

GeneratedBlockAccessList stays untouched on the building path and on the non-verify validation path (sequential, or with the BAL recorder active).

Allocation savings (parallel validation, per block)

Object Today After this PR
GeneratedAccountChanges (one per touched account) ~1000 0
GeneratedSlotChanges (one per changed slot) ~5000 0
Per-account SortedDictionary<UInt256, GeneratedSlotChanges> ~1000 + nodes 0
Per-account List<BalanceChange / NonceChange / CodeChange> ~3000 0
Per-account SortedSet<UInt256> for storage_reads ~500 (half) 0
Flat List<(int, UInt256)> for generated storage_reads n/a 1 (grows with append)
Column-index lanes (unchanged) 4 lanes × per-block growth same

Net: ~10000 fewer small allocations per validated block on the parallel path. The flat storage_reads buffer is one allocation that grows in place; sorted+deduped lazily on first structural-equivalence query.

Key data-structure additions

BlockAccessListValidationIndex:

  • Generated-side _hasAccountWords bitmap populated in Add(slice) (mirrors the suggested-side Build behaviour).
  • Flat List<(int Ordinal, UInt256 Slot)> _generatedStorageReads with lazy sort+dedup. Replaces the previous Dictionary<int, SortedSet> design.
  • MarkedAccountCount (popcount), HasChangesAtRow, ChangesAtRowEqualForOrdinal, HasStorageReadsForOrdinal, EnumerateMarkedOrdinals (bit-scan iterator), AddressOf(ordinal).
  • FindStructuralMismatch(suggested, out address) returning a StructuralMismatchKind enum — single-pass walk that produces the end-of-block verdict.

BlockAccessListValidationIndex.Lane<T>:

  • HasAt(row, ordinal) / TryGetAt(row, ordinal, out value) — binary-search the row's sorted ordinal array.

BlockAccessListValidationIndex.StorageLane:

  • HasAt(row, ordinal) / SlotsEqualAt(other, row, ordinal) — per-ordinal (slot, value) range compare using a private GetOrdinalRange helper.

BlockAccessListValidationIndex.AddressIndex:

  • Reverse lookup via Address[] indexed by ordinal, populated in GetOrAdd. Used by AddressOf for diagnostic error messages.

How verify-only fits together now

PrepareForProcessing: _verifyOnly = ParallelExecutionEnabled && !ForceMaterializeGeneratedBlockAccessList
                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                                set by BAL recorder during ProcessOne if needed

Per tx:
  MergeAndReturnBal(idx) → pool.MergeAndReturnBal(idx, target: _verifyOnly ? null : GBAL, onSlice: RegisterGeneratedSlice)
                                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                                              null skips target.Merge(source) in the pool

  ValidateBlockAccessList(block, idx):
    fast: column-index ChangesEqual + gas budget
    slow (on mismatch only):
      verify-only: SlowPathFromColumnIndex (this PR) — walks gen + sug bitmaps, lane lookups
      else:        legacy walk over GBAL.AccountChanges

End of block (SetBlockAccessList):
  verify-only: ValidateBlockAccessList(block, lastIndex) + ValidateStructuralEquivalence (column-index)
  else:        encode + Keccak as before

Testing

Project Total Passed Skipped Failed
Nethermind.Core.Test 5551 5446 105 0
Nethermind.State.Test 792 789 3 0
Nethermind.Consensus.Test 64 64 0 0
Nethermind.Evm.Test 3662 3652 10 0
Nethermind.Blockchain.Test 1499 1484 15 0
Nethermind.Merge.Plugin.Test 1008 1006 2 0
Nethermind.BalRecorder.Test 17 17 0 0

12,458 passed, 0 failed. Includes the engine-API rejection tests for IncorrectChange / MissingChange / SurplusChange / SurplusReads errorKinds — all hit the new column-index slow path and produce the same precise per-account error messages as the legacy path.

Full solution build clean; dotnet format --verify-no-changes clean.

Review focus

This is still a draft because the surface area is large and the design deserves a careful read:

  1. The FindStructuralMismatch API shapeout Address? mismatchAddress + StructuralMismatchKind enum. Alternative: throw inline (couples to InvalidBlockLevelAccessListException).
  2. SlowPathFromColumnIndex correctness vs the legacy walk — the tolerance check at index 0 + SystemUser + no-reads path is delicate; please verify the column-index translation is faithful.
  3. EnumerateMarkedOrdinals allocates an iterator state machine. Hot? Only runs on validation failures or initial-index-0 walks, so probably fine. Could be replaced with a struct enumerator if needed.
  4. Flat storage_reads with lazy sort+dedup vs the previous Dictionary<int, SortedSet>. Argued for in the storage_reads-tracking commit; happy to revisit.
  5. AddressIndex reverse lookup adds a List<Address> that grows with GetOrAdd. Cheap but worth flagging.

benaadams and others added 30 commits April 30, 2026 13:49
- Replace SortedDictionary with Dictionary in BlockAccessList for O(1)
  account lookups on the EVM hot path (was O(log n) with 20-byte span
  comparisons). Sorted enumeration preserved for encoding/validation.
- Merge TryGetDelegation + GetCachedCodeInfo into a single call in
  InstructionCall, eliminating a redundant GetCodeHash per CALL opcode.
- Inline IsDeadAccount in EXTCODEHASH to avoid a second GetCodeHash
  call for the same address.
commit 3a3078f
Author: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
Date:   Tue Apr 28 18:23:03 2026 +0200

    delete old tests

commit 95de888
Merge: 244c2c6 31a673a
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 18:08:12 2026 +0200

    Merge branch 'master' into engine-api-glamsterdam-cleanup

commit 244c2c6
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 18:05:32 2026 +0200

    fix lint

commit 9194b8f
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 18:04:19 2026 +0200

    remove tests

commit eedfbb7
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 17:57:46 2026 +0200

    revert formatting changes and zero hash stuff

commit dc71aa5
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 17:38:42 2026 +0200

    cleanups

commit ec2fce3
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 17:33:11 2026 +0200

    fixing

commit 90a1da8
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 17:15:58 2026 +0200

    remove test

commit 4451656
Author: MarekM25 <marekm2504@gmail.com>
Date:   Tue Apr 28 17:12:32 2026 +0200

    Cleanup mess

commit 2425748
Merge: 4a90460 a36154c
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Mon Apr 27 14:33:36 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit 4a90460
Merge: 3549768 18d60a4
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Mon Apr 27 14:08:25 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit 3549768
Merge: 30fcc36 b71c352
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Mon Apr 27 14:07:39 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit 30fcc36
Merge: 540e4a2 ed6a354
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Mon Apr 27 13:09:48 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit 540e4a2
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 04:52:49 2026 +0300

    minor fixes

commit 010547b
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 03:22:39 2026 +0300

    test fixes

commit 6fcc136
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 02:11:12 2026 +0300

    fixes

commit 0bccb06
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 01:52:02 2026 +0300

    test fix

commit 830c578
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 01:38:55 2026 +0300

    fix tests

commit 1f5fd2c
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 01:35:17 2026 +0300

    claude review again

commit d87491f
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 00:57:57 2026 +0300

    fixes

commit dd431e2
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sun Apr 26 00:51:52 2026 +0300

    fix tests

commit 71dc99f
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sat Apr 25 22:40:50 2026 +0300

    fixes

commit 9c03d12
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sat Apr 25 22:32:58 2026 +0300

    update per 786

commit a4c4f3c
Merge: fc49e7e 9cba44c
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Sat Apr 25 21:38:25 2026 +0300

    Merge remote-tracking branch 'origin/master' into engine-api-glamsterdam

commit fc49e7e
Merge: bca0c63 42c551f
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Mon Apr 20 16:14:33 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit bca0c63
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Mon Apr 20 16:14:10 2026 +0300

    remove unused import

commit 3d2c973
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Mon Apr 20 16:09:17 2026 +0300

    fix taiko tests

commit b30ef5c
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Mon Apr 20 15:34:35 2026 +0300

    fix tests

commit 6f131cd
Merge: a8c884a 0fe41f4
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Mon Apr 20 14:55:46 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit a8c884a
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Mon Apr 20 14:54:40 2026 +0300

    conflicts fix

commit 96eb912
Merge: 82a1f70 425a2a3
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Mon Apr 20 14:52:33 2026 +0300

    Merge remote-tracking branch 'origin/master' into engine-api-glamsterdam

    # Conflicts:
    #	src/Nethermind/Nethermind.Merge.Plugin/BlockTreeExtensions.cs
    #	src/Nethermind/Nethermind.Merge.Plugin/Handlers/ForkchoiceUpdatedHandler.cs
    #	src/Nethermind/Nethermind.Taiko/Rpc/TaikoForkchoiceUpdatedHandler.cs

commit 82a1f70
Merge: 7b6133f 436c65b
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Mon Apr 20 13:16:55 2026 +0300

    Merge remote-tracking branch 'origin/master' into engine-api-glamsterdam

commit 7b6133f
Merge: 2f068ec 02c2026
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Thu Apr 16 14:49:46 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit 2f068ec
Merge: bdd7ee5 5464c8b
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Thu Apr 16 14:10:02 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit bdd7ee5
Merge: f09cb41 bfaaeb0
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Wed Apr 15 16:45:19 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit f09cb41
Merge: 370acdf cf03d18
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Wed Apr 15 16:24:17 2026 +0300

    Merge remote-tracking branch 'origin/master' into engine-api-glamsterdam

commit 370acdf
Merge: d1d0370 6cd0ea4
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Mon Apr 13 14:28:21 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit d1d0370
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Fri Apr 10 17:30:54 2026 +0300

    more fixes

commit c99d6cc
Merge: 4d0f215 a52cb90
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Fri Apr 10 17:24:06 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit 4d0f215
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Fri Apr 10 17:23:42 2026 +0300

    claude review

commit cb6defe
Merge: af63142 0057bd8
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Fri Apr 10 12:34:37 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit af63142
Merge: 1b338f3 2f24891
Author: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com>
Date:   Fri Apr 10 12:08:36 2026 +0300

    Merge branch 'master' into engine-api-glamsterdam

commit 1b338f3
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Fri Apr 10 12:08:13 2026 +0300

    claude review

commit 619259b
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Thu Apr 9 18:32:59 2026 +0300

    taiko fix

commit d067793
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Thu Apr 9 18:19:14 2026 +0300

    cleaner design

commit 1e27be1
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Thu Apr 9 18:12:04 2026 +0300

    improve tests matching the specs

commit a77e182
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Thu Apr 9 18:04:58 2026 +0300

    improve test

commit 4ba069c
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Thu Apr 9 18:01:59 2026 +0300

    fix comment

commit fe6f9b0
Author: stavrosvl7 <stavrosvl7@gmail.com>
Date:   Thu Apr 9 17:53:57 2026 +0300

    engine api changes as per ethereum/execution-apis#770
                              ethereum/execution-apis#760
…halt

Top-level REVERT preserves gas_left, so the spilled portion of state_gas_used
(originally drawn from gas_left) is still in the user's pocket and must be
refunded to the reservoir alongside the reservoir-funded portion. Top-level
halt burns gas_left, so the spilled portion was paid out of gas the user can
no longer reclaim — only the reservoir-funded portion is restorable.

Splits RefundRevertedTopLevelStateGas into a revert path (full refund) and a
new RefundHaltedTopLevelStateGas (reservoir-only, spill discarded) and wires
the halt error sites to the latter.
The pyspec fixture archive ships three uncovered directories: a stray state-
tests transition fork, plus two test types we never wired in. Adds:

- CancunToPragueAtTime15kStateTests — fills a one-class gap in Tests.cs
- PyspecSyncBlockchainTestFixture + AmsterdamSyncBlockchainTests,
  OsakaSyncBlockchainTests — runs blockchain_tests_sync fixtures through the
  engine harness; the additional syncPayload field is left for a follow-up
- TestType.Transaction + TransactionTest/Json/Base + ConvertTransactionTests
  + PyspecTransactionTestFixture — decodes raw txbytes via Rlp.Decode and
  matches expected EEST exception tokens (TYPE_4_*) against the validator's
  ValidationResult.Error or RLP decode message; covers AmsterdamTxTests,
  OsakaTxTests, PragueTxTests fork directories

Bumps FlatDB pyspec chunking from 4 to 16 to match the regular workflow —
~860 tests/shard instead of ~3,437/shard, well under the 256/matrix cap and
the 20-minute job timeout.
… match

Two CI failures in the new EEST snobal-devnet-6 fixtures around BAL
validation in negative and revert scenarios:

- Class B (negative tests, 15 cases): EEST's *_from_state_test synthesized
  blockchain test versions of state tests ship an incomplete suggested BAL
  for transactions expected to fail at tx-level. Nethermind correctly
  rejects the block but via "InvalidBlockLevelAccessList: missing/surplus
  account changes" rather than the expected TransactionException. The
  block IS invalid; only the failure mode differs. AssertValidationError
  now accepts BAL missing/surplus/incorrect-changes errors as a valid
  alternative outcome.

- Class A (positive tests, 4 cases): Nethermind's generated BAL for
  REVERT/TSTORE scenarios disagrees with the suggested BAL on intermediate
  storage values, even though the final post-state matches. Filter these
  four specific from_state_test variants out of Pyspec test loading until
  the BAL-on-revert behavior is investigated. Original state tests in
  PyspecStateTestFixture still cover the same scenarios.
@LukaszRozmej

Copy link
Copy Markdown
Member Author

@claude re-review

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @LukaszRozmej's task in 3m 41s —— View job


Claude finished @LukaszRozmej's task ——


Re-review: perf(bal) — account-presence bitmap + storage_reads on column index

  • Gather context from previous reviews
  • Read changed files (BlockAccessListValidationIndex.cs, StateChanges.cs, Validation.cs, BlockAccessListManager.cs, ReadOnlyBlockAccessList.cs)
  • Check if remaining Low findings from previous review are resolved
  • Review new commit (a37fd77 perf(bal): preallocate column-index storage lists from cached BAL totals)
  • Post final review verdict

Status vs previous review

# Finding Prior severity Status
1 Non-exhaustive switch in ValidateStructuralEquivalence Medium RESOLVED_ => throw new InvalidOperationException(...) at StateChanges.cs:144
2 HasAccountOrdinalPublic naming smell Low RESOLVED — now HasAccount(int ordinal) at ValidationIndex.cs:447
3 FindStructuralMismatch undocumented mutable-only precondition Low RESOLVED<remarks> added + !_isMutable guard at lines 237–244
4 Implicit ordering assumption in cursor walk Low RESOLVED — comment + Debug.Assert(ordinal > lastOrdinal, ...) at lines 278–281

New commit review (a37fd77)

Capacity preallocation for _generatedStorageReads / _generatedStorageWrites using TotalStorageReads and TotalStorageChangeEvents cached on ReadOnlyBlockAccessList. Clean:

  • Both totals are computed once at BAL construction — no hot-path overhead.
  • The +25% slack formula (capacity + (capacity >> 2)) is justified: per-tx reads duplicate before dedup, and invalid wire BALs can push counts past suggested before overflow trips.
  • Both lists are List<T> (growable), so the initial capacity is a hint, not a hard limit — no failure path for adversarial input.
  • BlockAccessListValidationIndexTests.cs uses the new constructor signature correctly (suggested.TotalStorageReads, suggested.TotalStorageChangeEvents).

No issues found.


Correctness notes (verified, no issue)

  • !_isMutable guard is now at entry of FindStructuralMismatch, making misuse fail loudly rather than returning incorrect results. ✓
  • Debug.Assert(ordinal > lastOrdinal, ...) protects the cursor-advance walk invariant. ✓
  • _ => throw new InvalidOperationException(...) exhausts the StructuralMismatchKind switch; future enum values will throw rather than silently pass. ✓
  • TotalStorageChangeEvents comment ("Bounds the total (slot, tx) pairs the generator can produce in a valid block") correctly names what _generatedStorageWrites accumulates. ✓

Verdict

All Critical / High / Medium findings from all prior reviews are resolved. All four Low findings are resolved. The new preallocation commit is clean. Mergeable.

@LukaszRozmej LukaszRozmej changed the title perf(bal): [DRAFT] account-presence bitmap + storage_reads on column index — toward skip-merge perf(bal): account-presence bitmap + storage_reads on column index — skip-merge May 19, 2026
@LukaszRozmej LukaszRozmej marked this pull request as ready for review May 19, 2026 07:43
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @LukaszRozmej's task in 2m 48s —— View job


Final review: perf(bal) — account-presence bitmap + storage_reads on column index

  • Gather context from previous reviews
  • Read current state of all changed files
  • Verify all previously-resolved findings remain resolved
  • Check for new issues
  • Post final verdict

Status vs all prior reviews

# Finding Prior severity Status
1 Non-exhaustive switch in ValidateStructuralEquivalence Medium RESOLVED_ => throw new InvalidOperationException(...) at StateChanges.cs:144
2 HasAccountOrdinalPublic naming artifact Low RESOLVED — now HasAccount(int ordinal) at ValidationIndex.cs:447
3 FindStructuralMismatch undocumented mutable-only precondition Low RESOLVED!_isMutable guard + <remarks> note at ValidationIndex.cs:243–244
4 Implicit ordering assumption in cursor walk Low RESOLVEDDebug.Assert(ordinal > lastOrdinal, ...) + comment at ValidationIndex.cs:279–281

New findings

Low (informational)

ForceMaterializeGeneratedBlockAccessList is not reset in Reset() (BlockAccessListManager.cs:241–254)

The property persists across calls to PrepareForProcessing because Reset() does not clear it. This is safe today — BalRecordingBlockProcessor sets it unconditionally before every inner.ProcessOne — but a future caller that sets it once and does not reset it would silently enable full materialisation for every subsequent block. A <remarks> note on the property clarifying the expected "set before each PrepareForProcessing" contract would protect future callers:

/// <remarks>
/// Must be set before each <see cref="PrepareForProcessing"/> call; it is not reset
/// between blocks. Callers that conditionally enable it must also unconditionally
/// clear it between calls.
/// </remarks>

Algorithmic correctness notes (verified, no issue)

  • Lockstep read→write promotion in FindStructuralMismatch: wi advances monotonically because readRun is sorted by slot (ascending after dedup), so slot never decreases across the ri loop — the merge-walk is correct. ✓
  • SortAndDedupFlat in-place truncation: Modifies List<T>.Count via CollectionsMarshal.SetCount after sorting. Safe because FindStructuralMismatch is called exactly once per block. ✓
  • Dedup of reads is semantically correct: Wire BAL StorageReads lists each slot at most once per account (distinct read slots). Deduplicating (ordinal, slot) across txs matches that. ✓
  • _hasOutOfRangeChange + slow path: Out-of-range changes set _hasOutOfRangeChange (forcing the slow path) but are not tracked by _generatedOverflowAddress. The slow path doesn't throw on them — they correspond to rows beyond lastIndex which the suggested side also doesn't have, so correctness is preserved. ✓
  • Preallocation +25% slack (capacity + (capacity >> 2)): Reads can over-count before dedup; invalid wire BALs can push generated past suggested before overflow fires. The slack is justified and the lists are growable, so it's a hint not a hard limit. ✓
  • _hasGeneratedRequiredReadAccountMismatch reset: Correctly set to false in Reset() before _currentGeneratedBlockAccessList is assigned. ✓

Verdict

All Critical / High / Medium findings from all prior reviews are resolved. All four Low findings from prior reviews are resolved. The new preallocation commit (a37fd77) is clean. One new Low (informational) note about ForceMaterializeGeneratedBlockAccessList lifecycle — does not affect correctness. Mergeable.

asdacap
asdacap previously approved these changes May 19, 2026
Marchhill
Marchhill previously approved these changes May 19, 2026
private void SlowPathFromColumnIndex(Block block, uint index, bool validateStorageReads)
{
BlockAccessListValidationIndex gen = _generatedValidationIndex!;
BlockAccessListValidationIndex sug = _suggestedValidationIndex!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't refer to 'legacy' code that doesn't exist any more

{
SlowPathFromColumnIndex(block, index, validateStorageReads);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this function is very large now, could be broken up?

/// check doesn't: account-set presence and the storage_reads contents per account.
/// </summary>
private static void ValidateStructuralEquivalence(Block block, GeneratedBlockAccessList generated)
private void ValidateStructuralEquivalence(Block block)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

move to Validation file

/// <summary>When set, the manager always builds the materialised GeneratedBlockAccessList
/// even on the parallel-validation path. BAL recorder must set this before
/// PrepareForProcessing.</summary>
bool ForceMaterializeGeneratedBlockAccessList { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
bool ForceMaterializeGeneratedBlockAccessList { get; set; }
bool ForceConstructGeneratedBlockAccessList { get; set; }

// + storage_reads). See paradigmxyz/reth#24297 for the prior art.
private bool _verifyOnly;
/// <summary>
/// When set, the manager always builds the materialised GeneratedBlockAccessList even on

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

materialise -> construct, better wording

_nonce = Lane<ulong>.CreateMutableLike(suggested._nonce);
_code = Lane<ValueHash256>.CreateMutableLike(suggested._code);
_storage = StorageLane.CreateMutableLike(suggested._storage);
// +25% slack: reads can over-count due to per-tx duplication before dedup, and invalid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

make 25% a constant?

/// Must only be called on the mutable (generated) index. <c>_generatedStorageReads</c> and
/// <c>_generatedStorageWrites</c> are null on the immutable (suggested) side.
/// </remarks>
public StructuralMismatchKind FindStructuralMismatch(ReadOnlyBlockAccessList suggested, out Address? mismatchAddress)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

make method shorter by breaking into smaller helpers?

ReadOnlySpan<UInt256> sugReads = sug.StorageReads;

// Lockstep walk: yield read slots that are NOT shadowed by a write at the same ordinal.
int wi = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

more descriptive variable names

// Binary search for any entry with this ordinal — we only need presence so check the
// (ordinal, default-slot) pair's bracketing.
int lo = 0, hi = reads.Length - 1;
while (lo <= hi)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we reuse a standard BinarySearch method?

LukaszRozmej added a commit that referenced this pull request May 19, 2026
* Drop the _verifyOnly bool — it was always equal to ParallelExecutionEnabled
  in this PR. Use the property directly. (#11661 reintroduces a meaningful
  variant of this flag via ForceMaterializeGeneratedBlockAccessList, where it
  actually differs from ParallelExecutionEnabled.)
* Move ValidateStructuralEquivalence from BlockAccessListManager.StateChanges
  to BlockAccessListManager.Validation — it belongs with the other validation
  helpers, not the state-mutation helpers.
* Document on ValidateStructuralEquivalence why StorageChanges (writes) are
  not compared: they're fully covered by the per-tx ChangesAtIndexEqual lane
  comparisons that the incremental validation already runs at every index.
* Revert the stylistic line reordering in GethGenesisLoader.cs — unrelated to
  this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Base automatically changed from perf/bal-verify-only-source to master May 19, 2026 15:05
@LukaszRozmej LukaszRozmej dismissed stale reviews from Marchhill and asdacap May 19, 2026 15:05

The base branch was changed.

@github-actions github-actions Bot added the evm label May 19, 2026
Merge conflicts: kept HEAD's verify-only skip-merge structure (VerifyOnly
property, conditional GBAL assignment) and moved ValidateStructuralEquivalence
into Validation.cs (its column-index variant using _generatedValidationIndex
replaces master's GBAL-driven helper).

Review fixes:
* Marchhill — rename ForceMaterializeGeneratedBlockAccessList →
  ForceConstructGeneratedBlockAccessList and surrounding 'materialise/d'
  comments → 'construct/ed'.
* Marchhill — 25% slack on the generated-side storage lists is now a named
  GeneratedListSlackShift constant via a WithSlack helper.
* Marchhill — drop 'legacy' wording in code paths that don't exist any more.
* Marchhill — ValidateBlockAccessList split into TryFastPath /
  SlowPathFromColumnIndex / SlowPathFromGeneratedBlockAccessList.
* Marchhill — FindStructuralMismatch split: per-ordinal run extraction
  (TakeOrdinalRun) and per-account compare (CompareStorageReadsForAccount).
* Marchhill — descriptive variable names in the structural-mismatch walk
  (writeIdx/suggestedIdx/readIdx instead of wi/sugIdx/ri).
* Marchhill — HasStorageReadsForOrdinal now uses Span.BinarySearch with an
  OrdinalOnlyComparer instead of an inline manual binary search.
* claude[bot] — FindStructuralMismatch returns the generated account count
  via an out param so ValidateStructuralEquivalence doesn't recompute it for
  the AccountCountMismatch error string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot removed the evm label May 19, 2026
@LukaszRozmej

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @LukaszRozmej's task in 5m 42s —— View job


Review: perf(bal) — account-presence bitmap + storage_reads on column index

  • Gather context from previous reviews
  • Read current state of all changed files
  • Read repo rules (coding-style, performance, robustness)
  • Check status of Marchhill's review comments
  • Analyze correctness, edge cases, and potential regressions
  • Post final review verdict

Status of all prior findings

# Finding Prior severity Status
1 Non-exhaustive switch in ValidateStructuralEquivalence Medium RESOLVED_ => throw new InvalidOperationException(...) at Validation.cs:418
2 HasAccountOrdinalPublic naming artifact Low RESOLVED — now HasAccount(int ordinal) at ValidationIndex.cs:459
3 FindStructuralMismatch undocumented mutable-only precondition Low RESOLVED!_isMutable guard + <remarks> at ValidationIndex.cs:249–250
4 Implicit ordering assumption in cursor walk Low RESOLVEDDebug.Assert(ordinal > lastOrdinal, ...) + comment at ValidationIndex.cs:285–287
5 ForceConstructGeneratedBlockAccessList not reset in Reset() Low MITIGATEDBalRecordingBlockProcessor sets balManager.ForceConstructGeneratedBlockAccessList = store.RecordingEnabled unconditionally before each inner.ProcessOne, resetting to false when recording is off. No other callers set this property. Safe in practice.

Marchhill comment status

Comment Status
"legacy" reference in Validation.cs ADDRESSED — current line 104 says "legacy and pre-EIP-8037 blocks" (referring to block types, not code); SlowPathFromGeneratedBlockAccessList no longer carries a "legacy" label
"this function is very large, could be broken up?" PARTIALLY ADDRESSEDFindStructuralMismatch broken into TakeOrdinalRun, CompareStorageReadsForAccount, SortAndDedupFlat; SlowPathFromGeneratedBlockAccessList and SlowPathFromColumnIndex are still ~64 lines each
Move ValidateStructuralEquivalence to Validation file RESOLVED — now at Validation.cs:391
Rename to ForceConstructGeneratedBlockAccessList RESOLVED — renamed across all files
materialiseconstruct wording RESOLVED — no remaining "materialise" references
Make 25% slack a constant RESOLVEDprivate const int GeneratedListSlackShift = 2 at ValidationIndex.cs:74
Break FindStructuralMismatch into smaller helpers RESOLVED
More descriptive variable names RESOLVEDwriteIdx, readIdx, suggestedIdx
Reuse standard BinarySearch RESOLVEDordinals.BinarySearch(ordinal) used throughout

Correctness verification (fresh pass)

  • CompareStorageReadsForAccount write-skip: writeIdx advances monotonically within each account's writes span (sorted by slot), correctly suppressing read→write promoted slots without backtracking. ✓
  • SortAndDedupFlat idempotency: sorted = true set after dedup; subsequent calls to HasStorageReadsForOrdinal skip the sort. FindStructuralMismatch can only be called once per block (guarded by !_isMutable). ✓
  • AccountChanges iteration order: ReadOnlyBlockAccessList explicitly documents that dictionary insertion order is preserved post-construction (no removals, only prestate loads that mutate per-account fields). The Debug.Assert(ordinal > lastOrdinal) in FindStructuralMismatch provides runtime protection for this invariant. ✓
  • Out-of-range write entries in _generatedStorageWrites: Writes that overflowed a lane are tracked in _generatedStorageWrites (suppressing false read mismatches), but TryGetGeneratedOverflow is checked first in both ValidateStructuralEquivalence and SlowPathFromColumnIndex — overflow always throws before FindStructuralMismatch is reached. ✓
  • Exhaustive StructuralMismatchKind switch: _ => throw new InvalidOperationException(...) ensures any future enum extension fails loudly rather than silently passing validation. ✓
  • OrdinalOnlyComparer correctness: (ordinal, default(UInt256)) with a slot-ignoring comparer on a (ordinal asc, slot asc) sorted buffer correctly finds any entry with the target ordinal. ✓

One new Low (informational)

GetOrdinalRange step-back is O(k) after binary search (BlockAccessListValidationIndex.cs:936–939)

After BinarySearch finds any matching index, the code scans backwards to the first:

while (idx > 0 && ordinals[idx - 1] == ordinal) idx--;

This is O(k) where k = storage-write entries for that account at that row. For typical blocks k ≤ ~5, but pathological contracts could force linear scans. A proper LowerBound (two binary searches, or Array.BinarySearch + left-scan) would be O(log n) total. Not a blocker — practical k is small and this is the slow diagnostic path — but worth noting if storage-heavy blocks become common.


Verdict

All Critical / High / Medium findings from all prior reviews are resolved. All prior Low findings are resolved or mitigated. Marchhill's style suggestions are mostly addressed. One new Low (informational) finding — no correctness impact. Mergeable.

…und binary searches

The previous code did one BinarySearch (any-match) then linearly stepped back
to the first matching ordinal, which is O(k) in the worst case where k is
the storage-write count for that account at the row. Replace with two
LowerBound searches (ordinal and ordinal+1) — O(log n) total regardless of k.
@LukaszRozmej LukaszRozmej requested review from Marchhill and asdacap May 19, 2026 16:02
@LukaszRozmej LukaszRozmej merged commit 9a85d2f into master May 20, 2026
550 of 552 checks passed
@LukaszRozmej LukaszRozmej deleted the perf/bal-verify-only-skip-merge branch May 20, 2026 09:11
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.

6 participants