Skip to content

EIP-8369: enforce Profile 2 omissions with claimed-index replay and per-IL VERIFY budget - #13590

Draft
Marchhill wants to merge 429 commits into
masterfrom
feature/eip-8369-profile-2-focil-eligibility
Draft

Marchhill wants to merge 429 commits into
masterfrom
feature/eip-8369-profile-2-focil-eligibility

Conversation

@Marchhill

@Marchhill Marchhill commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Changes

InclusionListValidator excused every frame transaction (if (il[i].SupportsFrames) continue;), so a builder could omit any of them. This implements EIP-7805's Profile 2 omission claims and per-IL VERIFY budget, and the Bogota engine API that carries them:

Activation

Profile 2 is active when EIP-7805, EIP-8141, EIP-8250 and EIP-8272 are all enabled (Eip8369Profile2.IsActive). Before that, omitted frame transactions are excused and claims have no effect. Once it is active, an omission outside both profiles is excused, blob transactions included. Nothing changes before EIP-7805.

Profile 2

  • Candidates (Eip8369Profile2.Classify): no blobs; a declared prefix of an optional expiry_verify, then an optional recent_root_verify, then self_verify, deploy | self_verify, only_verify | pay or deploy | only_verify | pay; no ATOMIC_BATCH_FLAG in the prefix; no VERIFY frame after it; verify_cost ≤ MAX_VERIFY_GAS_PER_TX, counting the signature cost and the prefix's execution limits.
  • VERIFY budget: filled separately for each of the 16 inclusion lists, each deduplicated and taken in (hash, envelope) order from MAX_VERIFY_GAS_PER_IL. Entries failing the noncryptographic candidate checks are skipped without a debit; one costlier than what remains is skipped without a debit; otherwise the signature cost is debited, and the prefix cost too once the signatures verify. An entry is admitted if any list admits it, and its signatures are verified once however many lists carry it. The builder runs the same fill.
  • Evaluation index: min(claim, n) for an omitted candidate whose hash is claimed exactly once, otherwise n. Each candidate is replayed once, at that index alone.
  • Replay (Profile2EligibilityReplayer): parent state plus the EIP-7928 changes with block_access_index ≤ k, under MAX_VERIFY_GAS_PER_TX, the AA-VOPS access rules and the replay code bounds. The deploy frame's factory must be an undelegated contract.
  • Gas fit at k uses the EIP-8037 execution and state gas used before k, recorded from the execution receipts.

Engine API (execution-apis#911)

  • engine_newPayloadV6 and engine_newPayloadWithWitnessV6 take inclusionLists (exactly 16 lists, each within 8 KiB before deduplication, no empty entry) and omissionClaims ({transactionHash, evaluationIndex}, at most 4096, the index a canonical QUANTITY whatever JsonRpc.StrictHexFormat is). Malformed inputs answer -32602, after the -38005 fork check.
  • Inclusion is checked apart from execution. Once execution is VALID, newPayloadV6 schedules the check and answers at once with inclusionCheckId (keccak256("engine/inclusion-check/v1" || RLP([blockHash, inclusionLists, claims]))) and inclusionListSatisfied, which is null until the check completes. The pipeline no longer judges the list, so execution validity is never held for it; the flat-list check it ran (Block.InclusionListTransactions, InclusionListSatisfactionChecker, ProcessingResult.InclusionListUnsatisfied) is removed, and BranchProcessor records the per-transaction gas the check needs.
  • engine_getInclusionListVerdictV1(inclusionCheckId) returns the verdict, null while pending, or -38007 Unknown inclusion check. Identical checks share one run, a completed verdict never changes, and polling starts nothing. At most 256 checks are kept; past that a check for a block at or below the finalized one is evicted first, then a pending one, and a completed verdict only when nothing else is left. At most four run at once. A check that cannot be decided (state or gas counters unavailable) is evicted rather than completed, and a resubmission runs it again.
  • engine_forkchoiceUpdatedV5 takes PayloadAttributesV5.inclusionLists (-38003 when malformed) and returns PayloadStatusV1; it no longer reports inclusion. The retained-list machinery behind that, and the queued-copy tracking the handler kept for per-call lists, are removed.
  • engine_getPayloadV7 returns omissionClaims. The builder claims the payload length at the first failed attempt to append an admitted candidate, only when the candidate is invalid there.
  • engine_getInclusionListV1 also lists ready Profile 2 candidate frame transactions once Profile 2 is active, keeping their total VERIFY cost within MAX_VERIFY_GAS_PER_IL and leaving out candidates already expired at the parent.
  • SSZ-REST carries the same fields; the verdict query is JSON-RPC only.

Constants

MAX_VERIFY_GAS_PER_IL = 2**20, MAX_VERIFY_GAS_PER_TX = 2**18 (also Blocks.FocilProfile2MaxVerifyGas, where 0 lifts it), AA_VOPS_SLOT_COUNT = 4, MAX_OMISSION_CLAIMS = 2**12, MAX_REPLAY_CODE_BODIES = 16, MAX_REPLAY_CODE_BYTES = 2**17.

Types of changes

What types of changes does your code introduce?

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

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

  • Unit tests: Eip8369Profile2Tests, InclusionListValidatorTests, Profile2EligibilityReplayerTests, FrameTxValidationPrefixSimulationTests, TransactionsExecutorTests, InclusionListTxSourceTests, PayloadAttributes*Tests, InclusionCheckIdTests, InclusionChecksTests, EngineModuleTests (V7 and Eip8369), SszCodecTests and SszMiddlewareTests.
  • All 96 engine fixtures filled from execution-specs#3777 (tests/bogota/eip8369_focil_frames and tests/bogota/eip7805_focil) pass with nethtest -e --forkAlias Bogota=Eip8369Prototype; the runner polls engine_getInclusionListVerdictV1 for each VALID payload. Eip8369Prototype is a test-only fork: Bogota with EIP-8141, EIP-8250 and EIP-8272.
  • The focilTest CI job still runs tests-focil-devnet@v0.3.0, whose engine_newPayloadV6 carries a flat list, and fails until that release is refilled against execution-apis#911.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Marchhill and others added 30 commits August 27, 2026 20:18
…JSON (#12923)

* fix(rpc): keep frame receipt fields when a receipt is read back from JSON

ReceiptForRpc surfaces Payer and FrameReceipts but ToReceipt() dropped
both, so a frame transaction receipt deserialized through
TxReceiptConverter (debug_insertReceipts) came back without them.

* fix(rpc): derive frame receipt logs and status from the frames

ToReceipt took Logs and StatusCode from the top-level JSON fields while
FrameReceipts came from the payload, so a debug_insertReceipts payload
carrying frames but contradicting top-level fields broke the union
TxReceipt.FrameReceipts documents, leaving eth_getLogs and the bloom
disagreeing with the frame receipts on the same stored receipt. Derive
both from the frames as the wire decoder does, but only when the payload
actually carries frames — an empty set aggregates to success and would
otherwise stamp that onto a receipt the caller marked failed.

Annotate FrameReceiptForRpc.Logs nullable to match what the deserializer
can produce, and cover the JSON layer with a serializer round trip.

* fix(rpc): carry the frame receipt fields through TxReceiptConverter

Write emitted neither payer nor frameReceipts, so a receipt serialized by the
registered converter and read back through Read lost both, leaving the DTO fix
to help only debug_insertReceipts. Emitted under the frame type so every other
receipt keeps its existing shape.

Logs is also nullable now: Write emits "logs": null for an empty log set, which
the deserializer honours, so ToReceipt threw on the converter's own output.

* fix(rpc): derive a frame receipt's bloom from its frames too

Deriving Logs from the frames while keeping the caller's LogsBloom only moved
the contradiction from one half of the pair to the other. The EIP-8141 wire
receipt carries no bloom at all, so DecodeFrameTxReceipt leaves TxReceipt to
derive it from Logs; clearing it here does the same.

* fix(rpc): reject malformed receipt payloads instead of failing internally

ToReceipt projected the caller-controlled arrays with LINQ, so a
debug_insertReceipts payload carrying "frameReceipts": [null] or
"logs": [null] dereferenced null and JsonRpcService answered -32603
Internal error. Bind both with indexed loops that reject null entries
with JsonException, which maps to invalid params, and enforce EIP-8141's
MAX_FRAMES bound on the way in.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
…eipt population (#12925)

* fix(opt): stop the Optimism receipts tracer from bypassing shared receipt population

The Optimism tracer overrode BuildReceipt outright, so it never ran the
shared population: a frame transaction got no payer, no per-frame
receipts and no aggregated status, and the tracer's per-transaction
frame state was never cleared, leaving a later failing transaction free
to inherit the previous frame transaction's logs.

Chain-specific receipt types now come from a CreateReceipt factory the
base fills in, so the two cannot drift apart again.

* fix(opt): make CreateReceipt the only receipt extension point

BuildReceipt stayed overridable after the Optimism tracer stopped
overriding it, so nothing but convention kept a future override from
bypassing the shared population again. No in-tree tracer overrides it,
so seal it: the extension surface is now exactly CreateReceipt, and its
remark states that anything BuildReceipt assigns is overwritten.

Cover the deposit-field population that moved into CreateReceipt. It
runs on every deposit today but no test reached it through the tracer -
the existing ones hand-build receipts with DepositNonce already set -
so nothing pinned the nonce read happening after cumulative gas
tracking and before the world state moves on.

* test(opt): pin the receipt fields the tracer refactor recovered

EffectiveGasPrice, ExecutionGasUsed and the CreatesTopLevelContract-keyed
ContractAddress were asserted nowhere, so an Optimism-side shortcut that
dropped them again would leave the suite green. Dispose each tracer.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
Validation-prefix simulation is listed as missing but has landed, and the
list omits the paymaster rules, which are absent from this branch entirely.

Audited each item against EIP-8141 and the branch: the failed-APPROVE rule
is enforced (a prefix that sets no payer is rejected) but unbounded, the
per-payer exposure bound reserves the frame-gas sum rather than max_cost,
nothing revalidates a pending prefix on a new head, and no canonical
paymaster instance can be recognised because the EIP pins no runtime code.

Reorg re-admission is dropped from the list: the cap is on the pending set,
so re-admitting one transaction per sponsor is what the spec asks for.

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
- ConcatFrameLogs in the frame transaction processor: only the
  declaration survived, the live call site uses TxFrameReceipt.ConcatLogs
- CountingAdapter.ExecutedPerAttempt: written, never read
- TxFrameDecoder.EncodeArray's rlpBehaviors parameter: the sole caller
  passes the default and Encode ignores it

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
…efix simulator (#12903)

* perf(txpool): stop re-verifying frame signatures inside the prefix simulator

Every opaque EIP-8141 frame transaction had its frame_signatures verified twice
on the pool path: once in FrameTxSignatureFilter, then again inside
FrameTxPrefixSimulator's lock, which serialises all admission simulations behind
one resettable world state.

ExecutionOptions.FrameSignaturesPreValidated lets the caller assert the check has
already run. Only the validation-prefix path reads it, and the pool's simulation
filter is its only setter — every other entry point into the processor verifies
unconditionally, so a stray flag there is inert.

FrameTxSignatureDuplicationMeasurement sizes what this removes from the lock:
~25 us per SECP256K1 entry and ~40 us per P256 entry, against 1.4 us for a
trivial prefix, 22 us for a 5k-gas one and 207 us for a 50k-gas one. At the
MAX_VERIFY_GAS bound on entries it is 2.7 ms.

* refactor(txpool): narrow the pre-validated flag's docs and drop the measurement harness

State that the flag is read only for signature verification rather than being
inert elsewhere -- options are compared by exact equality in places -- and that
the filter and the simulator read the head separately. Remove the explicit
measurement harness now its numbers are recorded, and keep the flag guard's
scan out of build output.

* refactor(txpool): carry frame-signature verification on the filtering state

signaturesPreValidated: true was backed only by FrameTxSignatureFilter sitting
earlier in _postHashFilters, and nothing pinned that order: swapping the two
kept every test green while the prefix resolved a payer from an unverified
signature and the exposure gate reserved that stranger's budget.

The signature filter now records the fact on TxFilteringState and the simulation
filter reads it, so a reorder degrades to re-verifying rather than trusting.

* refactor(txpool): put the simulator's cancellation token last

CA1068: the pre-validated flag was sandwiching the token, which also forced a
filler Arg.Any<CancellationToken>() through every mock setup.

* fix(txpool): keep the pre-validated signal writable only inside the pool

IIncomingTxFilter is public and implemented outside Nethermind.TxPool, so any
such filter could assert verification it never did. Also repoints the new test
at the fork that still enables frame transactions after the Bogota split.

---------

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

ReceiptForRpc.Logs became nullable in #12923, which turned the unguarded
foreach in BuildUserDepositTransactions into CS8602 and broke the build
for the whole solution, not just the docs build.

The null is reachable rather than theoretical: these receipts are
deserialized from an L1 node's eth_getBlockReceipts response, and
TxReceiptConverter.Write emits "logs": null for a receipt with no logs.
The sibling SystemConfigDeriver on the same derivation path already
guards this way.

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
Brings the branch up to 3bc542e. Thirteen files needed both sides.

VirtualMachine.cs was the only textual conflict: master's RestoreRipemdTouch
and the branch's RestoreStateGasJournal both land after the same
_worldState.Restore, and both are kept at every rollback site.

Eth72ProtocolHandler.cs takes master's cell-mask restore and requeue-reason
rework together with the branch's chain-head spec hoist.

TracedAccessWorldState.cs drops the IsStorageEmpty override with EIP-7610
while keeping the idle-guard asymmetry: state methods delegate when no slice
is installed, Clear/SetIndex/IncrementIndex still throw on a missed setup.

run-nethtest.yml keeps master's v8.1.2 pin and FOCIL exclusion alongside the
branch's frameTest lane, its Bogota=Eip8141Prototype alias and the separate
bogota scope branch.
…12776)

* docs: EIP-8141 public-mempool rules design note

* EIP-8141: per-payer mempool exposure accounting for frame transactions

Bounds each resolved frame-tx payer's summed pending maximum cost to the
payer's balance at admission, per the merged public-mempool rules
(ethereum/EIPs#12007). Reuses the DelegationCache event-counter precedent: a
PayerExposureCache maintained from the pool insert/remove events, read by a new
FrameTxPayerExposureFilter placed after the payer-resolution filter.

Enforcement is scoped to natively-resolved payers (Transaction.PayerAddress);
unresolved and non-frame txs pass through. Canonical/non-canonical paymaster
reservation, replacement payer-switch atomicity, eviction ordering,
dependency-indexed revalidation, and the MAX_VERIFY_GAS bound remain documented
follow-ups (see MEMPOOL-RULES-DESIGN.md).

* EIP-8141: close exposure-bound gaps from mempool-rules review

- Reject blob fields on frame txs at validation (NonBlobFieldsTxValidator
  in the frame composite) so a blob-carrying frame tx can no longer bypass
  the gas-only per-payer exposure bound while frame-blob support is off.
- Make the reservation atomic: PayerExposureCache.TryReserve compare-and-sets
  the check and reserve in the filter, closing the check-then-act race where
  concurrent same-payer submissions could all pass a stale total. Release moves
  to the Removed event plus the non-insert (ReplacementNotAllowed) path.
- Saturate the reservation counter and drop per-call closures in the cache.
- Add a TxPool-level accounting test (two frame txs sharing a sponsor payer,
  second rejected, released on removal) and parameterize the filter tests.
- Correct the max-cost wording (gas-only for frame txs) in the filter comment
  and MEMPOOL-RULES-DESIGN.md.

* EIP-8141: reject only blob-carrying frame txs, fix exposure leak

Replace the presence-based NonBlobFieldsTxValidator in the frame-tx
composite with a value check in FrameTxValidation: the decoder always
populates max_fee_per_blob_gas and blob_versioned_hashes, so the presence
check rejected every frame tx. Now only frame txs with a non-empty blob
hash list are rejected; the no-hashes zero-fee shape is admitted.

Release the per-payer exposure reservation from a finally in AddCore so a
throw before insertion cannot leak it, and document the deliberate
under-count for a persistent-broadcast-retained frame tx (no release hook
on the broadcaster). TryReserve now returns the observed reservation so
the trace reports the total the decision was made on.

Remove the design note from the repo tree.

* EIP-8141: relocate end-to-end payer-exposure test to simulation layer

The per-payer exposure gate only fires for a resolved third-party payer.
Native admission now resolves only self-payers, which the sender balance
filter already bounds, so the shared-sponsor end-to-end case moves to the
layer where simulation resolves the payer. The filter-level unit tests
still prove the gate at this layer.

* EIP-8141: share the validation-prefix grammar between pricing and payer resolution

FrameTxPayerResolver restated the prefix-shape predicates that Core's
FrameTxValidation already owns and the verify-gas admission filter prices
against, and the two had drifted: Core recognised a leading deploy frame in
the prefix, the resolver did not, so [deploy, self_verify] got recognised-
prefix pricing yet never resolved natively. Expose the shape predicates from
FrameTxValidation and consume them in the resolver, skipping an optional
leading deploy frame like the expiry frame so pricing and the payer verdict
classify a prefix the same way.

* EIP-8141: collapse redundant only_verify branch in payer resolution

Both the only_verify branch and the fall-through returned RequiresSimulation,
so the branch was inert. Collapse to a single return, preserving behaviour.

* EIP-8141: in-pool validation-prefix simulation for opaque frame transactions

Admit the opaque frame-tx prefixes the native resolver defers
(RequiresSimulation: deployed-code sender, code-carrying paymaster,
unrecognized shape) by simulating their validation prefix in a bounded,
read-only EVM, resolving the payer and enforcing the trace/opcode rules.

The standard, natively-resolvable prefixes keep the EVM-free fast path:
FrameTxSimulationFilter returns immediately for a payer already resolved
by FrameTxPayerFilter, so the simulator is never consulted for them.

- IFrameTxPrefixSimulator abstraction in Nethermind.TxPool (no reference
  to Consensus's processing env, which would cycle); implemented by
  FrameTxPrefixSimulator at the composition root wrapping
  IReadOnlyTxProcessingEnvFactory, injected optionally into the pool.
- ExecutionOptions.FrameValidationPrefixOnly + a validation-prefix
  simulation path in the frame processor: halts once the payer is set,
  bounds cumulative work by MAX_VERIFY_GAS, always restores state.
- FrameTxValidationTracer enforces the banned-opcode list, the
  GAS-before-call and TIMESTAMP-in-expiry-verifier caveats, and
  SLOAD-restricted-to-sender; violations reject the transaction.

When no simulator is wired, RequiresSimulation stays deferred as in
Phase 1. Deferred (design note §4): dependency-set-keyed caching,
head-change re-simulation indexing, wall-clock cancellation, and a
multi-simulation admission budget.

* EIP-8141: enforce CALL*/EXTCODE* target rules in validation-prefix tracer

Address review findings on the Phase 2 in-pool simulation:

- High: FrameTxValidationTracer now rejects CALL*/EXTCODE* whose target is
  neither an existing contract nor a precompile, or that uses an EIP-7702
  delegation (spec §Validation Trace Rules, L816/L853), exempting tx.sender.
  Previously these prefixes were silently admitted despite the doc claim.
- TIMESTAMP exemption now checks the EXPIRY_VERIFIER code hash as well as the
  address (L788), via a new Eip8141Constants.ExpiryVerifierCodeHash.
- GAS-before-call is validated by peeking the next opcode instead of a
  cross-frame mutable flag, so a trailing GAS is no longer missed or
  mis-attributed across frame boundaries.
- The post-frame MAX_VERIFY_GAS bound is now reachable: a prefix frame capped
  to the remaining budget that then exhausts it is rejected as over-budget,
  distinct from a within-budget revert.
- IFrameTxPrefixSimulator.Simulate takes a CancellationToken (honored at
  entry; OperationCanceledException propagates).

Adds tests for the CALL* codeless-target rejection, the existing-contract
allow case, and the over-budget rejection reason.

* EIP-8141: reconcile opaque-payer simulation with updated payer resolution

The payer resolver now takes the sender account and defers all only_verify|pay
prefixes to simulation, so the simulation filter passes the sender account when
re-resolving, and its tests drive the resolver through the same account state.
Also stop attributing an explicit within-budget REVERT to the MAX_VERIFY_GAS
cap: only a capped frame that ran out of gas is reported as over-budget.

* EIP-8141: add end-to-end payer-exposure test at the simulation layer

Relocated from the mempool-rules PR, where native admission no longer resolves a
third-party payer. Here the opaque only_verify|pay sponsor prefix is resolved by
validation-prefix simulation, so the exposure gate bounds the sponsor's summed
pending cost to its balance end-to-end and releases the reservation on removal.

* reconcile phase-2 simulation with configurable verify-gas bound

Re-add the consensus MAX_VERIFY_GAS constant the prefix simulator needs;
isolate the exposure test from the configurable ingress bound.

* EIP-8141: price per-payer exposure with the shared max_cost helper

* EIP-8141: keep blob-carrying frame transactions out of the pool

* EIP-8141: correct the frame-tx blob validator comment

* EIP-8141: name the deploy-frame decline and meter prefix simulation

Reject a validation prefix containing a deploy frame with its own reason rather than
falling through to "never set a payer", correct the docs that attributed the decline to
the tracer's opcode bans, and add a rejection counter for the simulating filter.

* EIP-8141: assert the gas-budget helper in the exposure fixtures

Also reuse the shared blob-gas helper in TryCalculateMaxCost instead of re-deriving the
blob count.

* chore: drop unused usings from the frame-transaction measurement fixtures

* EIP-8141: make the shared expiry-frame predicate self-guarding and meter the exposure gate

Restore the value and data-length checks the resolver's private copy had, so reading the
deadline out of a matching frame needs no separate precondition; add the sibling rejection
counter; correct the exposure comment, which described a bound the branch no longer has.

* EIP-8141: keep the exposure reserve and release symmetric on a zero cost

A zero-cost reservation inserted an entry the matching release never reclaimed. Also read
the payer's balance from the account the pipeline already cached when the payer is the
sender, and pin the overflow and concurrency properties the reservation exists to provide.

* EIP-8141: cover the payer reservation lifecycle through the pool

A same-nonce replacement is the one shape that reaches the exposure gate at this layer, so it
pins both the reservation taken through the filter chain and its release on the pool event.
Also trim the gate comment's inventory of what is still missing.

* EIP-8141: name the exposure rejection like its siblings and make the expiry read total

Rename the admission result and its message to the FrameTx prefix the other frame-tx outcomes
and metrics use, mark the under-reserved max cost as a deviation rather than a gap, restore the
qualified payer-exposure entry to the gate inventory, and route TryGetExpiryDeadline through the
self-guarding predicate so it can no longer throw on a malformed frame.

* EIP-8141: stop a payerless frame transaction from disconnecting the peer

AcceptTxResult compares by id, so returning Invalid for a structurally payerless prefix reached
the flood controller's immediate-disconnect arm. Give it its own result, as the expired and
verify-gas verdicts already have, and cover the refused-replacement release path.

* chore: trim the frame-tx exposure comments to the non-obvious why

* chore: trim the prefix-simulation comments to the non-obvious why

* EIP-8141: make a leaked payer reservation observable

Gauge the number of payers holding a reservation, tracked on the two dictionary transitions, so
an idle pool reading non-zero is the leak itself. Also merge the duplicated remarks on the expiry
predicate, derive the pricing assertion from its frames, and vary the refused replacement by a
field the reservation is not computed from.

* EIP-8141: publish the reservation gauge atomically and keep its pair symmetric

Interlocked on the metric itself, so a racing transition cannot leave a stale count standing,
and release now compare-and-sets like reserve so a double release touches nothing. Narrow the
pay-frame predicate back to private, and vary the refused replacement by its target.

* EIP-8141: cover the self-paying exposure path and make its counter atomic

The fixture only ever built third-party payers, so the branch every real admission takes was
untested. Also match the rejection counter to the gauge beside it, select the replacement's frame
by shape rather than index, and extend the payerless summary to the rule it now states.

* EIP-8141: pin the blob term in the payer exposure bound

Nothing asserted that a blob-carrying frame tx reserves its blob leg, so the bound was gas-only
by accident of every fixture leaving the hash list null.

* chore: reattach the exposure fixture doc and guard its frame lookup

* EIP-8141: pin the blob exposure term by magnitude and the deploy-prefix edges

The blob case asserted only that the reserved amount grew, so a bound that
dropped the GasPerBlob or blob-count factor stayed green; it now pins the
inclusive boundary and the reserved total for one and two blobs.

Adds the two unpinned edges of the widened validation-prefix grammar: a second
deploy frame is not skipped (RequiresSimulation, matching the pricing grammar),
and an expiry frame followed by a deploy frame still records the expiry
dependency.

* chore: sort the payer exposure cache usings

* EIP-8141: widen the blob exposure arithmetic and regroup the fixture

The blob term was computed in int, which wraps once the blob count times the
blob fee exceeds 16383; it is now long, pinned by a six-blob case that fails
on the int version. Tests and private helpers are no longer interleaved, so a
new test cannot orphan a helper's doc comment again.

* chore: trim the frame-transaction comments to the non-obvious why

Also states the exposure deviation's floor honestly: the reserved amount is
zero, not a fraction, when every frame gas limit is zero.

* EIP-8141: trim frame-tx mempool comments

* chore: keep the comment trim to code this change owns

Reverts the trim on FrameTxValidation and TxValidator, whose bodies this change
leaves untouched: the param tags and memoization contract on TryCalculateGasBudget
and the sidecar-validator note were documentation, not commentary. Restores the
EIP8141 deferred-work marker on the expiry pass, which is the only record of the
broadcaster-held expired frame txs this sweep does not reach.

Also drops a using left unnecessary by the blob fixture helper.

* EIP-8141: restore comments on code this change does not touch

* chore: correct two stale comments on the payerless and reserve paths

A payerless frame tx is no longer rejected as Invalid, and TryReserve can also
report a zero reservation when it refuses the very first one.

* EIP-8141: drain the payer reservations when the pool is disposed

DisposeAsync unsubscribed the Removed handler without releasing what was still
reserved, so those payers stayed counted by a process-wide gauge no pool could
decrement — making a non-zero reading the norm rather than the leak signal the
gauge exists to give.

* EIP-8141: keep the exposure gauge pairing in one place

Both removal paths now go through RemoveTracked, so the decrement cannot drift
from the entry count as sites are added. Clear no longer claims to close the
teardown window: a submission already in flight can still reserve after it.

* EIP-8141: make the teardown drain total again

Routing Clear through the value-comparing removal let a reservation updated
during enumeration survive with its gauge increment, permanently — the floor
the drain exists to remove. Clear removes unconditionally; TryAdd is the only
increment, so retiring an entry still retires exactly one.

* EIP-8141: price a replacement without the reservation it displaces

The exposure gate runs before AddCore resolves a same-sender/same-nonce
replacement, so the incumbent was still reserved and the bump was charged for
both. Any self-paying frame tx costing more than half its payer's balance could
therefore never be fee-bumped, while non-frame txs of the same account stayed
replaceable. The bound is on the pending set the pool would hold, and EIP-8141
decrements on replacement, so the displaced tx is excluded from it — held, not
settled, since its own release still runs when Removed fires.

* Restore the exposure gate's independent test coverage and skip its bucket walk when unneeded

Adds a pool-level case pinning the payer bound over nonces above the replaced one,
which BalanceTooLowFilter does not sum; verified it fails with the filter removed.
The replacement discount is now computed only when the payer holds a reservation,
since TryReserve ignores it otherwise.

* Size the exposure test's balance off the reservation rather than a fixed constant

The frame gas the builder emits is not TxGasLimit, so the hardcoded balance left the
bound slack and the case stopped discriminating once max_cost was repriced.

* Validate recent-root references before the prefix simulation runs

RECENTROOTREFLOAD is legal in a VERIFY frame and reads the declared references on the
strength of the pre-state check the main path performs, which the simulation path
skipped. Anchored to the earliest slot the transaction could execute in, so a reference
to the head slot is not rejected for being one slot early.

Also tightens the deploy-frame decline to RecognizedPrefixLength's actual precondition,
drops the calldata-stat guard GasLimitTxFilter already makes redundant, and pins
AcceptTxResult id uniqueness, which the compiler cannot see.

* Do not disconnect the relaying peer over an unpriceable max cost

AcceptTxResult.Invalid is the one result TxFloodController maps to a disconnect, and
an unpriceable max_cost is unincludable rather than malformed - MalformedTxFilter has
already passed the transaction. Matches the sibling balance filters, which return
Int256Overflow for the same arithmetic condition.

* EIP-8141: pin that an unpriceable max cost does not disconnect the peer

Revert-checked: restoring AcceptTxResult.Invalid on the unpriceable path fails the
assertion, so the result the flood controller reads is now pinned.

* EIP-8141: raise MAX_VERIFY_GAS to 300k and trim review comments

Consensus MaxVerifyGas and the mempool's FrameTxMaxVerifyGas default move together so the
two numbers of the same name cannot disagree; both admit strictly more than before.

Narrow the simulator's catch to exclude OutOfMemoryException, matching the Rlp.cs
convention, and say why a broad catch is still needed at that boundary.

Attribute the SETDELEGATE ban and the Rule N numbers to their EIP-8141 sections, drop the
blob-pricing gap note superseded downstack, and extract the deploy-prefix predicate.

Delete summaries that restate the signature.

* Drop the ExpiryVerifierCodeHash summary

* Split the prefix simulation into preparation, costing and gas-capping helpers

* Assert the max-cost probes in the payer exposure tests

Matches the sibling asserts: a silent false would leave the probe at zero
and fail the submits instead of the real cause.

* Keep the mempool verify-gas default at 100k

Only the consensus MaxVerifyGas was asked to move to 300k. Reverting the pool default
narrows the change to that and removes it as a variable in the checked-build
Nethermind.TxPool.Test regression this branch picked up.

* Restore the mempool verify-gas default to 300k

Reverts 14d51516bc. Both the consensus constant and the pool default were asked to
move to 300k; leaving the pool at 100k made the mempool stricter than consensus, so a
prefix legal at 300k was rejected at ingress. No test reads the default — every case
sets FrameTxMaxVerifyGas explicitly.

* Do not blame frame transactions for node-side simulation faults

The validation-prefix simulator rejected on any exception. AcceptTxResult
.FrameSimulationFailed is not Invalid, so it causes no immediate disconnect,
but TxFloodController.Report counts every non-accepted result: past 10/s it
downgrades the peer and past 100/s it disconnects it for tx flooding. A
systemic node-side fault such as a missing trie node after pruning therefore
rejected every opaque frame transaction in turn and worked its way up to
disconnecting honest peers, one individually reasonable rejection at a time.

Split the simulation outcome three ways and return Undecided for faults that
indict the node rather than the transaction, keyed off the existing
IInternalNethermindException marker plus disposal and IO failures. The pool
then admits the transaction with an unresolved payer, exactly as it already
does when no simulator is wired, instead of returning a result the sending
peer is charged for. Having no chain head and a disposed simulator are the
same kind of node-side condition and move with them.

* Drop the unreachable SETDELEGATE ban from the validation tracer

0xF6 has no Instruction member, so the EVM's opcode table maps it to the
default bad-instruction handler and the validation prefix halts on it on
every fork we ship. The tracer's raw-byte check sat outside the opcode switch
only because there was no enum member to use as a case, and it could never
fire. If the byte is ever assigned it gains an Instruction member and belongs
in the switch with the other banned opcodes.

Cover the halt with a test instead, so the property the ban was standing in
for is the thing that is actually asserted.

* Relax the validation prefix banned opcode list

ORIGIN, BLOBHASH, TLOAD and TSTORE are no longer banned during the validation
prefix. Each is determined by the frame or the transaction payload rather than
the block environment, so none makes the prefix depend on state that could
differ between simulation and inclusion: ORIGIN resolves to the frame's caller,
transient storage is discarded between frames, and BLOBHASH indexes the
transaction's own versioned hashes. TSTORE additionally stays unreachable in a
VERIFY frame through the ordinary static-call restriction, so dropping its entry
here removes a duplicate rather than a protection.

Also drop the spec line-number citations from the surrounding comments; they
point into a document that moves under them.

* Correct the MAX_VERIFY_GAS scope note and drop rule numbers from comments

MAX_VERIFY_GAS is mempool-scoped throughout EIP-8141: it bounds the validation
work a node performs admitting a transaction, and nothing enforces it during
block execution. Both uses sit on the simulation-only path, so calling it a
consensus cap described a rule that does not exist.

Comments that cited structural rules by number now state the rule instead, so
they survive the spec being renumbered and read without it to hand.

* Ban SLOTNUM in the validation prefix and sharpen the simulation reporting

SLOTNUM is on the EIP's banned list and was missing from the tracer. It is
reachable: EIP-7843 registers it from Amsterdam, which the frame prototype spec
derives from. Unlike NUMBER or TIMESTAMP the slot cannot coincide between the
simulated head and an including block, so a prefix gating approval on it can be
admitted and then rejected at execution while still holding a payer reservation.

The simulation filter now names the accepted outcome instead of reaching it by
fall-through, so a future outcome cannot silently record a payer it does not
carry. Undecided verdicts get their own counter: they mean payer exposure has
stopped being accounted for, which was previously visible only in debug logs.
Repeated node-side faults log once at warn rather than once per submission.

The pool config description no longer calls its limit the consensus
MAX_VERIFY_GAS; it bounds the declared-gas check, and the constant caps a
simulated prefix per frame regardless of it.

* Make the fail-safe simulation outcome the default and unlatch the fault warning

Undecided becomes the zero value, so a default-constructed
FrameTxSimulationResult defers instead of reading as an accept with a
null payer and admitting the transaction with no exposure reservation
and no counter.

The node-fault warn latch now clears once the EVM runs again, so a
transient fault no longer downgrades every later fault to debug for the
rest of the process lifetime.

The two simulation counters become fields bumped with Interlocked: the
filter runs under the pool head read lock alongside concurrent
submitters, and the undecided counter is the signal that payer exposure
has stopped being accounted for.

* Merge master into eip8141-frame-txs-devnet7 (#12893)

* fix(rpc-bench): run benchmarked nodes the way production runs them (#12625)

* diag(rpc-bench): run Nethermind with production-default runtime (no TC=0 pin)

* reword comment for master

* review: document expb divergence, add NODE_ENV_VARS escape hatch

- README: the 'Alignment with expb' section no longer claims the removed
  env pins; documents the deliberate code-gen divergence and that JIT
  warm-up now lands inside the measured window; dotTrace reports are not
  comparable across this change
- start-node.sh: reword comment (no warm-up phase exists yet), add
  NODE_ENV_VARS passthrough for deliberate one-off code-gen experiments

* trim comments to one-liners; rationale stays in the PR

* drop the Merge GC flags: inert here and misleading

GCKeeper only runs on Engine API calls; this harness parks the node at a
snapshot head and never sends newPayload, so the three flags changed
nothing while implying the node ran a non-production GC configuration.

* keep the image entrypoint for Nethermind

The override skipped entrypoint.sh, which applies host tuning and enables a
shipped PGO profile. Its comment claimed parity with expb, but expb only
overrides the entrypoint for dotTrace, so normal expb runs (and production)
do run entrypoint.sh - this harness was the outlier.

* Rename EIP-8037 regular gas dimension to execution gas (#12600)

* Auto-update fast sync settings (#12665)

Co-authored-by: rubo <rubo@users.noreply.github.com>

* refactor(net): namespace snap by version (#12606)

* refactor(net): namespace snap messages by version

Prepare the snap subprotocol for a second version by moving the concrete
snap/1 messages, message codes and protocol handler into a versioned
namespace, so a snap/2 handler can be added alongside without touching
snap/1 code.

  Snap/Messages/*            -> Snap/V1/Messages/*
  Snap/SnapMessageCode       -> Snap/V1/Snap1MessageCode
  Snap/SnapProtocolHandler   -> Snap/V1/Snap1ProtocolHandler
  P2P/P2PMessageKey.cs       -> P2P/VersionedProtocol.cs  (file renamed to
                                match the type it declares)

SnapMessageBase and SnapSerializerBase deliberately stay in
Snap.Messages: they are shared by all protocol versions, not specific to
snap/1.

Introduce SnapVersions constants and ISnapSyncPeer.SnapProtocolVersion so
version checks can replace the magic numbers currently spelled as 1.

PeerInfoExtensions.CanGetSnapData is renamed to CanGetTrieNodes to say
what it actually tests. The rename is nominal - the peer probe is
unchanged, so behaviour is identical.

No functional change.

* refactor(net): remove Snap2 version constant from SnapVersions

* address review comments

* rename

* feat(sync): serve block access lists from the snap server (#12607)

* Refactor SnapServer and SnapStateServer integration

- Renamed SnapServer to SnapStateServer for clarity and consistency.
- Updated WorldStateManager to use SnapStateServer instead of SnapServer.
- Modified StateSyncFeedTestsBase to accommodate changes in SnapServer instantiation.
- Adjusted SnapProviderTests to reflect the new SnapStateServer type.
- Introduced SnapServerTests to validate SnapServer functionality.
- Added SnapStateServerTests to ensure robust testing of state management.
- Implemented new methods in SnapServer for handling bytecode and block access lists.
- Enhanced test coverage for account range retrieval and storage management.

* refactor: change SnapServer field type to interface ISnapServer

* test: enhance SnapServerTests with additional block access list scenarios

* chore: Update Dockerfiles (#12663)

Update Dockerfiles

Co-authored-by: rubo <rubo@users.noreply.github.com>

* test: make prewarmer env-return assertion pool-hit independent (#12616)

PreWarmCaches_ReturnsAddressWarmEnvWhenScopeBuildThrows asserts
Returned == Created, but with maxPoolSize 1 an env returned to the pool
can be rented again without a Create, so a pool hit legitimately
increments Returned twice against a single Create. Whether a hit occurs
depends on worker interleaving: flaky on CI, fails deterministically on
high-core machines (0/30 locally).

ThrowingBuildPolicy.Return now refuses retention, so every rental is a
fresh Create and the invariant holds under any interleaving (30/30
locally). Stack-trace instrumentation confirmed every rental is returned
exactly once - the prewarmer itself is correct.

* Update OP Superchain chains (#12664)

Co-authored-by: emlautarom1 <emlautarom1@users.noreply.github.com>
Co-authored-by: Lukasz Rozmej <lukasz.rozmej@gmail.com>

* fix(receipts): restore the post-merge flag before regeneration (#12641)

* fix(receipts): restore the post-merge flag before regeneration

Stored headers do not carry IsPostMerge and regeneration bypasses the
recovery step that restores it, so post-merge blocks re-executed with
PREVRANDAO evaluating to the zeroed difficulty - any transaction reading
it produced receipts failing the root check (~7% of post-merge blocks
refused on mainnet archives deriving receipts from state).

* test(receipts): dispose buffer, pin logged value

* fix(receipts): classify post-merge via the switcher

A difficulty heuristic misreads chains that repurpose the field -
Taiko stores per-block ZK gas in Difficulty while AlwaysPoS - so ask
IPoSSwitcher instead, mirroring MergeProcessingRecoveryStep. On
mainnet the switcher's TD-null branch is the same difficulty check,
so behavior there is unchanged.

* test(receipts): pin the real switcher's TD-null derivation

The production failure arrived as a mainnet-shaped header with
TotalDifficulty unset; cover PoSSwitcher's TD-null branch end to end,
not only the honour-the-switcher contract.

* test(receipts): cover the switcher registration path

A hand-injected switcher cannot catch a composition regression that
leaves the container-resolved regenerator on the NoPoS default, so
resolve it from a graph whose IPoSSwitcher registration is overridden
the way a merge-enabled node overrides it.

* fix(tests): mark RecoverReceiptsBlockchain.Create as hiding

* Expose the node's ENR in admin_nodeInfo (#12631)

feat(rpc): expose the node's ENR in admin_nodeInfo

Nethermind is the only discv5-capable execution client whose
admin_nodeInfo omits the node record. Tooling that bootstraps
discv5-only networks reads the ENR from this endpoint and has to
special-case Nethermind to the enode instead, which is useless once
discv4 is disabled.

NodeRecordProvider already maintains a signed, sequence-numbered self
record, so surface it as an 'enr' field. Move INodeRecordProvider to
Nethermind.Network so Nethermind.JsonRpc can reference it without a new
dependency on Nethermind.Network.Discovery; the implementation stays in
Discovery. The provider is only registered when discovery is enabled, so
it is resolved optionally and the field is omitted otherwise.

* Validate ABI decode allocation bounds (#12588)

* ci: disable stateless glamsterdam-devnet-7 scheduled run (#12680)

* Fix EIP-7708 tracing with logs (#12577)

* Test for EIP-7708 top frame log

* [WIP] attach log to parent frame

* Attach log to correct frame

* Reuse common code in tests

* More tests

* Naming

* fix(flat): guard the trie-warmer against a TransientResource recycle race (storage reads as 0x00) (#12429)

* fix(flatdb): warm the trie from persistence only

The trie warmer read the recyclable `_snapshots` and `_transientResource`
while the warm job held only a `ReadOnlySnapshotBundle` lease, which does not
cover them. A concurrent scope reset could recycle those under the running
warmer, so a warm read could return a torn or foreign node.

The warmer only needs to warm from persistence, so restrict its reads to the
trie node cache and the `ReadOnlySnapshotBundle` - exactly what the lease
covers. In-memory nodes are already hot and do not need warming.

Claude-Session: https://claude.ai/code/session_01S3BG321zDG9BNjAgWhZhCX

* fix(flat): warm the transient resource via a per-job lease

The trie warmer now warms nodes into the per-job-pinned TransientResource
(not just the trie node cache and ReadOnlySnapshotBundle), covered by a single
transient lease held for the whole warm traversal. It still never reads the
recyclable _snapshots. A warmer read on a torn-down bundle bails to a
persistence-only read instead of spinning for a lease that will never land.

Claude-Session: https://claude.ai/code/session_01S3BG321zDG9BNjAgWhZhCX

* refactor(flat): drop the warmer transient ThreadStatic capture

Measured TS-on vs always-per-node-lease on x10 (3 runs, heavy-block warmer
load): newPayload 178 vs 178 ms, getProof p99 20.1 vs 20.2 ms, RSS identical.
The ambient capture bought nothing, so keep only the per-node lease + ABA
re-check (the actual recycle-race fix) and drop the ThreadStatic + the
EnterWarmerTransientScope pins in the two warm-job callers.

* fix(flat): register the transient return owner at pool checkout

- ResourcePool.GetCachedResource now calls OnRented, so every checkout
  carries a registered return owner; a final ReleaseLease without one
  throws instead of silently dropping the resource (which leaked the
  BloomFilter allocation on the public AddSnapshot path)
- re-check _isDisposed after the warmer's lease acquire: Dispose releases
  the owner lease but leaves _transientResource pointing at the recycled
  instance, so the identity re-check alone could latch a resource already
  re-rented by another bundle
- document why ReleaseLease is distinct from Dispose: the pool contract
  reserves Dispose for destroying an over-capacity resource
- run Nethermind.State.Flat.Test in the nethermind-tests.yml matrix; it
  was compiled but not run by any CI job
- FlatDbManagerTests duplicate-snapshot test asserts the resource lands
  back in the checkout pool; new ResourcePoolTests cover the final-release
  return and the unregistered-release throw; refresh stale warmer test
  comments

* fix(flat): pin the transient resource for prewarm dedupe reads

ShouldQueuePrewarm read _transientResource without pinning it. The dedupe
bloom lives on that recyclable resource and the call runs on prewarmer and
BAL threads, so the owner could retire the resource mid-read: the pool
Resets it and, on overflow, Disposes the BloomFilter, whose backing store is
native memory. Route both overloads through the same lease + ABA re-check
the warmer node reads already use, and decline the prewarm on a torn-down
bundle. Rename the helper accordingly, since it is no longer warmer-only.

The FlatWorldStateScopeProvider and FlatOverridableWorldScope test doubles
returned the committed resource to the pool directly instead of releasing
its lease, mirroring neither AddSnapshot implementation. That recycles the
resource while a warmer lease is outstanding and returns it a second time
when that lease is released; the scope provider double also returned the
wrong instance and then returned it again on teardown. Both now release the
lease, which is the single return-to-pool path.

Test changes:
- the persistence-only test now commits the written nodes into the bundle's
  recyclable _snapshots before reading, so the warmer's Unknown result is a
  genuine miss. Previously the node was still in the transient (SetStateNode
  writes both) and was itself Unknown, so the assertion held either way.
- the churn test gives every epoch its own persisted node instance, so a
  read served from another epoch's recycled transient is caught by identity
  rather than by value, drives both recycle paths (CollectAndApplySnapshot
  swap and Dispose), exercises ShouldQueuePrewarm alongside the node reads,
  and joins the readers with a bounded wait instead of blocking forever.
- new test: a warmer read and a prewarm check on a disposed bundle fall back
  to the leased persistence reader within a bounded wait, covering the
  Dispose bail-out deterministically.

* fix(jsonrpc): synchronise SubscriptionManager per-client subscription bag (#12672)

* fix(jsonrpc): synchronise SubscriptionManager per-client bag

The per-client subscription bag is a HashSet mutated and enumerated from
multiple threads: concurrent subscribe requests (socket worker tasks),
unsubscribe, and the Closed handler that fires on connection teardown.
Concurrent HashSet access could corrupt it, dropping a subscription so
its event handlers stayed attached and leaked. Lock on the bag for every
add, remove, and snapshot-before-dispose.

Fixes #12668

* refactor(jsonrpc): dispose client subscriptions under bag lock without snapshot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(jsonrpc): race unsubscribe path too; drop bag field comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test: eth_createAccessList affordability with omitted fee fields (#12629)

* test(rpc): eth_createAccessList affordability with omitted fee fields (execution-apis #854)

execution-apis PR #854 clarifies that eth_createAccessList must not fail
solely because an unfunded sender cannot afford client-selected default fees
when all gas-fee fields are omitted. Nethermind already conforms (verified in
hive rpc-compat); this adds a regression test mirroring the conformance
fixture: unfunded sender, codeless recipient, zero value, no gas/fee fields
-> {"accessList":[],"gasUsed":"0x5208"}.

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

* test(rpc): run affordability test on London chain, assert no in-body error, reuse helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

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

* Fix stale transaction pool snapshots (#12685)

* Encode engine_newPayloadWithWitness witness as an RLP data string (#12635)

* fix

Signed-off-by: jsign <jsign.uy@gmail.com>

* Tighten witness RLP JSON encoding

---------

Signed-off-by: jsign <jsign.uy@gmail.com>
Co-authored-by: jsign <jsign.uy@gmail.com>

* perf(state): skip trie warmup for read-only BAL accounts in flat layout (#12681)

* perf(state): skip trie warmup for read-only BAL accounts in flat layout

With a suggested BAL the block's write set is known upfront, and trie
nodes are only needed at commit for written accounts. Gate address
trie-warm hints (HintBal, HintGet, HintWarmAccount) on the BAL write
set so read-only accounts no longer trigger state-trie path walks.

On BAL blocks dominated by cold account reads this removes up to ~8-10
wasted trie-node DB reads per unique read-only account from the
measured processing window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Apply suggestions from code review

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

* refactor(state): extract QueueStateTrieWarmup and address review findings

- Extract the NeedsStateTrieWarmup + PushAddressJob + increment pattern
  into QueueStateTrieWarmup, used by all three address warmup call sites
- Hoist CancelHintBal above the empty-BAL early return so a stale write
  set never survives into the next block
- Drop the inaccurate bloom false-positive-rate comment
- Tests: parameterize warm-per-write-kind over balance/nonce/code/storage,
  add empty-BAL reset regression test, split the HintWarmAccount test,
  wrap scopes in using, use order-insensitive assertions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update src/Nethermind/Nethermind.State.Flat/ScopeProvider/FlatWorldStateScope.cs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* refactor(test): reuse TestContext for recording-warmer scope construction

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(state): bind BAL warmup gate to HintBal lifecycle; address review findings

- Keep _warmupWriteSet across StartWriteBatch: on the parallel BAL path the
  BAL apply commits mid-block, concurrently with tx workers, so clearing the
  gate in CancelHintBal collapsed it at a nondeterministic point. The gate is
  now replaced only by the next HintBal.
- Drop the token from Task.Run in both HintBal implementations: a task
  cancelled before being dequeued never ran the finally that returns the
  pooled accountChanges array. The body already observes the token.
- Align TrieStoreScopeProvider.HintBal with the flat scope: a new hint
  supersedes the previous one even when it carries no work itself.
- Remove the stale prestate-load mutation mention from ReadOnlyBlockAccessList
  docs, state the immutability invariant, and seal the type.
- Tests: pin the gate surviving StartWriteBatch and a second BAL replacing
  the previous write set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* test: make can-never-fail tests assert what their names claim (#12690)

* test(xdc): drop redundant trailing Assert.Pass in SpecialTransactionsTests

* test(core): bound McsLock re-acquire test instead of passing unconditionally

SingleThreadAcquireRelease asserted nothing and is subsumed by
ReacquireAfterReleaseSucceeds, which now runs on a worker with a timeout
so a broken release fails in seconds instead of hanging the test host.

* test(flat): assert real postconditions instead of Assert.Pass

Add_ConcurrentWithMightContain_ShouldWork now verifies no write is lost
under concurrent readers (a bloom filter never false-negatives).
DisposeAsync test renamed: FlatDbManager does not dispose the repository
(the container owns it); it now asserts bounded drain completion and
idempotent double-dispose. ConstructorAcceptsPersistedRepository removed -
Not.Null on a fresh object, subsumed by every other test in the file.

* test(merge): assert pending-validation cleanup instead of catch-only assertions

The memory-leak test asserted only inside catch blocks and ended with
Assert.Pass, so it passed whenever nothing threw; it now drives the
handler directly and asserts the pending-validation count stays zero
across repeated timed-out payloads. The TrySet double-completion test
is removed: it was a weaker duplicate of the concurrent-calls test
above it (same scenario, but swallowing OperationCanceledException and
keying Assert.Fail on exception message text).

* test(merge): await header-sync test helpers

The helpers were async void and invoked without awaiting - including one
un-awaited call inside the other helper - so their assertions raced the
test body and failures could surface as host crashes or not at all.

* test(network): restore DisconnectsAnalyzer assertions with deterministic flush capture

All four tests had their assertions commented out as CI-flaky, leaving
them assertion-free. The flakiness came from racing the 10ms flush
timer: reports are now recorded while the default 10s interval is in
effect and only then is the interval shortened, so a flush cannot fire
mid-arrangement. Assertions scan captured flush reports, including that
counters aggregate, reset after each flush, and cleared categories do
not resurface (the analyzer double-buffers, so a lost clear shows up as
a stale count in every other flush, not as a doubled count).

* test: address review findings on strengthened tests

Will_clear_after_report no longer issues a second report at all - a
report can race the flush's enumerate-then-clear window (Timer.Stop does
not drain a queued Elapsed callback, so no test-side quiesce is airtight).
A lost clear is observable without it: the analyzer double-buffers, so a
stale count resurfaces in later flushes, and the test asserts the
category appears in exactly one flush across several more (mutation-
verified). Bloom capacity raised to 100k - at 10k the saturated filter's
~34% false-positive rate masked a single lost write - and misses are
collected into one assertion. The repeated-timeouts payload test is
dropped rather than parameterized: the pending dictionary is keyed by
block hash and the test resubmits one block, so the count can never
exceed one and iterations add no coverage. The FlatDbManager dispose
test states its intent with Assert.DoesNotThrowAsync and a corrected
comment (WaitAsync bounds the wait, not the drain). Wait timeouts are
named constants and polling uses Thread.Sleep(1).

* test: simplify comments per ASD-STE100 and drop dead times parameter

Comments now use short, active, single-topic sentences. The times
parameter of ShouldEventuallyReport had no remaining non-default call
site after the second-report removal.

* test: use SpinWait.SpinUntil instead of a custom poll helper

Keep one condition re-check after a timeout: a flush can land in
SpinUntil's final sleep tick.

* Stop parallel transaction execution once BAL validation rejects the block (#12697)

* fix(consensus): stop parallel tx execution once BAL validation rejects

The parallel BAL block validator ran the incremental validator alongside the
transaction workers, but a validator failure faulted neither the worker loop nor
its cancellation token. `ParallelUnbalancedWork` only stops fetching new indices
on caller cancellation or a transaction-worker fault, so a block rejected at an
early transaction index still executed every remaining transaction before the
foreground observed the failure at `GetResult()`. For a block whose invalidity is
decided by a cheap prefix, that turns a sub-second rejection into slot-scale CPU
work.

`IncrementalValidationWorkItem` now owns a cancellation source that is signalled
together with the stored exception, and the worker loop runs under that token, so
workers stop pulling transactions as soon as validation becomes terminal. The
resulting `OperationCanceledException` is translated back into the original
validation failure, keeping the returned error identical.

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

* refactor(consensus): signal BAL validation failure with a flag, not cancellation

Routing the stop signal through ParallelUnbalancedWork's cancellation token meant
`For` ended by throwing an OperationCanceledException that existed only to be
caught and translated back into the original validation failure. The token also
needed a per-block CancellationTokenSource, whose recycling in Schedule was only
safe because the previous block's validator had already been joined.

Replace all of it with a volatile read of the exception the work item already
stores: workers check `HasFailed` before doing any work, the loop drains its
remaining indices without executing anything, and `GetResult()` reports the
rejection on the normal return path. Same behaviour, no exception used as control
flow, no extra state to keep in sync, and no ordering constraint between Schedule
and the work that follows it.

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

* refactor(test): trim comments and simplify the tail-cancellation test

Comments cut back to the non-obvious reasoning only. The regression test now
counts executions instead of collecting indices, so a single Is.InRange assertion
covers both requirements — the decisive prefix ran, the tail did not — and the
prefix array, the index bag and one assertion all go away. Transaction count is a
plain constant rather than derived from the canonical lead, which the test never
depended on: with uniform gas limits the tail sort is stable, so the schedule is
natural order regardless. CreateParallelValidationTransactions takes an optional
gas limit so the block no longer needs patching after construction.

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

* fix(consensus): address review — exempt iteration 0, loosen test bound

Iteration 0 (WaitForBalWarmup + ApplyStateChanges) is now exempt from the
HasFailed guard, so pre-execution keeps its previous semantics instead of being
droppable when validation fails before any worker starts. Skipping it was traced
as benign, but the fix only needs to stop transaction execution, so leaving the
pre-execution step alone keeps the behavioural change narrower at no cost.

The regression test's upper bound leaned on SpinWait outlasting exception
unwinding, because the fake validator releases its gate before the work item
stores the exception. Bound is now txCount / 8, which asserts the tail stopped
without depending on cancellation-propagation timing — a revert still executes
all 2048 and fails.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* test: pin eth/62-66 serializer wire encodings with hand-derived goldens (#12696)

* test(network): remove duplicate eth serializer tests

ZeroNewBlockMessageSerializerTests.Roundtrip2 and V63
NodeDataMessageSerializerTests.Zero_roundtrip were verbatim copies of
their Roundtrip siblings. Can_deserialize_own_eth_64 shared its body
and one payload with Can_deserialize_eth_64; its unique payload moves
there as another TestCase.

* test(network): pin eth/62-66 wire encodings with hand-derived goldens

Every serializer test file in Eth V62-V66 now asserts an exact wire
encoding somewhere. The goldens are derived from the RLP rules and
verified with an independent encoder (pyrlp + pycryptodome keccak);
values shared across files live in EthSerializerGoldens. ToString
smoke tests now assert the log line names the message type, and the
32-byte-selector test asserts the exact decoded hash.

* test(network): address review feedback on serializer goldens

- BlockBodies: pin the null-body framing (c1c0) and the empty-vs-absent
  withdrawals distinction (e9e8...c0c0)
- Status To_string: assert against the independent Protocol.Eth constant
- V63 NodeData: rename Roundtrip_with_nulls to Roundtrip_with_empty_entry
  (the data holds an empty array, not null)

* feat: EIP-8037 two-dimensional gas tracing (stateGasTracer + callTracer) (#12628)

* feat(tracing): add EIP-8037 stateGasTracer (execution-apis #852)

Implements the `stateGasTracer` named tracer specified in execution-apis
PR #852, returning the per-transaction two-dimensional gas summary
`{gasUsed, regularGasUsed, stateGasUsed, gasRefund}` (EIP-8037/EIP-7778).

The values are already computed for block-level gas accounting and carried
on `GasConsumed`; the tracer only reads and formats them. Adds the missing
applied EIP-3529 refund (capped) to `GasConsumed.GasRefund`, populated in
the success and top-level-halt refund paths. The native tracer factory is
threaded with `IReleaseSpec` so the fork is determined explicitly.

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

* feat(tracing): add EIP-8037 two-dimensional gas to callTracer (execution-apis #852)

Adds `regularGasUsed`, `stateGasUsed` and `gasRefund` to the callTracer
top-level frame for Amsterdam+ blocks, per execution-apis PR #852. The
fields are gated on `IReleaseSpec.IsEip8037Enabled` (MUST NOT appear before
the fork) and set only on the top frame (omitted on sub-frames). Values are
read from the transaction's `GasConsumed` result, matching the stateGasTracer.

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

* refactor(tracing): address #12628 review feedback

- Restore native-tracer factory API back-compat: keep the public 4-arg
  GethLikeNativeTracerFactoryDelegate/RegisterTracer/CreateTracer so external
  plugin registrations stay source- and binary-compatible; built-ins receive
  the (nullable) IReleaseSpec via an internal spec-aware factory.
- stateGasTracer: disable IsTracingOpLevelStorage/IsTracingStack so the
  terminal-only tracer stops invoking per-opcode storage/stack callbacks.
- Consolidate the callTracer top-frame regularGasUsed/stateGasUsed/gasRefund
  into a single TwoDimensionalGas? value, removing the coupled nullables and
  the unsafe null-forgiving dereferences in the converter.
- Document that regularGasUsed is floor-clamped (block-accounting value) so
  the two-dimensional invariant is exempted under the calldata floor.
- Add an end-to-end stateGasTracer test executing a real Amsterdam tx through
  the TransactionProcessor (fresh SSTORE + in-tx reset) that exercises field
  selection and the GasConsumed.GasRefund plumbing; assert
  regularGasUsed + stateGasUsed == gasUsed + gasRefund.
- Test cleanups: multiple-assert scope and a shared helper for the two
  callTracer Amsterdam cases.

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

* refactor(tracing): address #12628 re-review nits

- Document the spec-less CreateTracer overload's pre-fork fallback and cover it
  with a factory test (back-compat public contract was untested).
- stateGasTracer hex test now forces the ambient NumberConversion.Raw so it
  actually exercises StateGasTraceConverter's hex-quantity override.
- E2E test: use a `using` tracer and correct the refund assertion message
  (the slot is reset to its original zero value within the tx, not pre-nonzero).
- Convert the remaining `//` member comments to XML doc.

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

* style: remove unused using in NativeStateGasTracerE2ETests

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

* refactor(tracing): drop excess comments; non-nullable spec in native tracer factory

Address flcl42 review on #12628:
- Remove the newly added explanatory comments that restated the code (kept only a
  few essential EIP-referenced DTO docs and the hex-conversion note).
- Make the release spec non-nullable through GethLikeNativeTracerFactory and the
  native tracers: drop the unused spec-less 4-arg CreateTracer overload (the only
  null source) — RegisterTracer, the actual plugin API, is unchanged. Also drops
  the brittle regularGasUsed occurrence-count assertion flcl42 flagged.

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

* style: collapse double blank line before DeepNesting test

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

---------

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

* fix(simulate): skip EIP-3607 on the EIP-7928 BAL path in eth_simulateV1 (#12691)

* fix(simulate): skip EIP-3607 on the EIP-7928 BAL path in eth_simulateV1

eth_simulateV1 relaxes EIP-3607 so a state-overridden contract can be the tx
`from`. That relaxation covered only the main execution (via
SimulateTransactionProcessorAdapter). Under glamsterdam (EIP-7928),
BlockProcessor runs transactions through the BlockAccessListManager's own tx
processors, which bypass the adapter and re-enforce EIP-3607 — so a contract
sender is rejected with `-38024 sender has deployed code` instead of reaching
the normal balance/fee checks (hive rpc-compat divergence on glamsterdam-devnet-8:
ethSimulate-simple-send-from-contract*, ethSimulate-override-address-twice).

Relax EIP-3607 on the block execution context in
SimulateBlockValidationTransactionsExecutor instead:
ParallelBlockValidationTransactionsExecutor sets that context on both the main tx
processor and the BAL manager, so both paths skip the check — while BlockProcessor
still receives the unwrapped spec, preserving chain-specific release-spec
interfaces (Taiko / XDC / Optimism).

Verified end-to-end on a glamsterdam-at-genesis chain (the three hive fixtures
flip -38024 -> -38014/-38012, matching besu/erigon/reth/geth) and by an
integration test through the real EIP-7928 BAL path that fails -38024 without the
fix and passes with it.

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

* fix(simulate): preserve PrevRandao in context rebuild; harden 3607 BAL test

Address re-review on #12691:
- SimulateBlockValidationTransactionsExecutor is now the single context-rebuild
  funnel for the simulate scope, so preserve the incoming PrevRandao (via
  BlockExecutionContext.WithPrevRandao*) instead of re-deriving the default —
  a BlockProcessor subclass (e.g. XdcBlockProcessor) may have supplied a
  non-default value.
- Make eth_simulateV1_contract_sender_skips_eip3607_on_bal_path validation-enabled
  so the -38014 expectation is fork-independent and stable across the #12692 fix
  (with validation:false the -38014 relied on the BAL path ignoring NoValidation).
  Pin the EIP-7928 premise with an explicit BlockLevelAccessListsEnabled assert.

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

* fix(simulate): forward BlobBaseFee too in the context rebuild

Re-review follow-up on #12691: WithPrevRandao recomputes BlobBaseFee from the
header, so the no-override branch was newly lossy for a processor that forced a
non-derivable value (XdcBlockProcessor sets BlobBaseFee = 0 on a header cloned
with ExcessBlobGas = 0; recomputation yields MinBlobGasPrice = 1). Collapse to a
single WithPrevRandaoAndBlobBaseFee that forwards the incoming BlobBaseFee (or the
block override when present), leaving Spec as the only field the rebuild changes.

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

* fix(simulate): relax EIP-3607 via execution flag, not spec wrapping

The eth_simulateV1 contract-sender fix relaxed EIP-3607 by wrapping the block
execution context spec with WithoutEip3607(), which returns a NoEip3607Spec
decorator implementing only IReleaseSpec. Because the tx processors read their
spec from that context (TransactionProcessor.GetSpec => BlockExecutionContext.Spec),
the decorator reached chain-specific processors that hard-cast it — e.g.
TaikoTransactionProcessor.PayFees does (ITaikoReleaseSpec)spec and
XdcTransactionProcessor casts on every tx — so eth_simulateV1 on Taiko/XDC threw
InvalidCastException/InvalidOperationException instead of returning a result
(Eip3607Transition defaults to 0, so the wrap is always active there).

Replace the spec wrapping with a dedicated ExecutionOptions.SkipSenderCodeCheck
policy flag gated in ValidateSender. The main simulate adapter ORs it into its
Process call; the EIP-7928 BAL path receives it via a small
BlockAccessListTxExecutionOptions injected into BlockAccessListManager and threaded
to its ExecuteTransactionProcessorAdapter. The release spec now keeps its concrete
runtime type on every path, so chain-specific interfaces survive.

Regression test: Nethermind.Taiko.Test exercises a code-bearing sender through
TaikoTransactionProcessor.PayFees — it passes with the flag and throws
InvalidCastException under the old spec-wrapping. eth_simulateV1 BAL-path and full
simulate suite remain green.

Follow-up to #12691; addresses the residual type-erasure raised in its review.

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

* chore(simulate): trim explanatory comments to essentials

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

* refactor(simulate): carry EIP-3607 relaxation on BlockExecutionContext

Replace the ExecutionOptions.SkipSenderCodeCheck flag (and the machinery to thread
it into the EIP-7928 BAL path) with a SkipSenderCodeCheck field on
BlockExecutionContext. Both the main tx processor and the BAL manager's own
processors already share the context via SetBlockExecutionContext, so the field
reaches every path for free — no threading through BlockAccessListManager /
TxProcessorPool / ExecuteTransactionProcessorAdapter, and no injected policy type.

The context is the same shared channel #12691 originally relaxed on; this just uses
a flag instead of a spec decorator, keeping the spec's concrete runtime type (so
ITaikoReleaseSpec/IXdcReleaseSpec casts survive). Net simpler diff and consistent
with the existing IsGenesis flag on the same struct.

ValidateSender reads VirtualMachine.BlockExecutionContext.SkipSenderCodeCheck; the
simulate executor sets it in its context rebuild. Taiko regression test updated to
set the flag on the context (still throws InvalidCastException under spec-wrapping).

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

* refactor(simulate): carry EIP-3607 relaxation on TransactionProcessor

Per review preference (@LukaszRozmej), move the SkipSenderCodeCheck flag from
BlockExecutionContext onto TransactionProcessor. ValidateSender reads the
processor's SkipSenderCodeCheck property. The simulate scope sets it type-
preservingly on both creation paths: Intercept<ITransactionProcessor> for the
main processor, and a factory decorator for the EIP-7928 BAL processors — so each
chain keeps its concrete processor type (no spec wrapping, no processor-type swap).

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

* style: trim comments to the essential why

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

* refactor(simulate): remove dead WithoutEip3607; address review polish

Follow-up to the SkipSenderCodeCheck switch (review by @claude on #12691):
- Remove the now-dead EIP-3607 spec wrapper — this PR dropped its last production
  caller. Deletes WithoutEip3607, NoEip3607Spec, GetNoEip3607Spec (std + zkevm) and
  the _noEip3607Specs cache; keeps WithoutEip158 (still live). Fixes two comments
  that cited the removed decorator.
- Enforce the relaxation invariant: a shared Apply() throws if the resolved
  ITransactionProcessor isn't a TransactionProcessorBase, instead of silently
  no-op'ing back to -38024. Used by both the Intercept hook and the factory decorator.
- XML-doc SkipSenderCodeCheck with its set-before-use / unsynchronised-read invariant.
- Make SkipSenderCodeCheckTransactionProcessorFactory internal.
- Revert SimulateBlockValidationTransactionsExecutor to master (the fix no longer
  touches it; its PrevRandao tweak was unrelated).
- Assert fee payment in the Taiko regression test (matches its name).

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

---------

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

* fix(dns): verify EIP-1459 subtree hashes (#12707)

* fix(dns): verify EIP-1459 subtree hashes

EIP-1459 serves every subtree entry from the subdomain
base32(keccak256(entry)[..16]). The root signature covers only the
enrtree-root entry, so this hash chain is the only binding between the
signed root and the branch and ENR records a client consumes. The
crawler never checked it, so any resolver or poisoned cache could
substitute arbitrary node records and steer the crawl to
attacker-chosen labels.

Verification follows go-ethereum: unpadded standard base32, prefix
comparison against keccak256 of the record, abbreviated labels accepted
down to 12 decoded bytes (minHashLength). The tree root is exempt, as
EIP-1459 serves it from the bare domain with no hash label. A mismatch
logs at Warn and skips the record.

No base32 decoder existed in src/Nethermind, so EnrTreeHash carries a
small one, tested against labels produced by the reference
implementation.

Validated against live trees: all.mainnet.ethdisco.net crawls 3000
records with zero rejections, so the check does not misfire on
anything the reference publisher emits.

* refactor(dns): simplify and harden EnrTreeHash

- Reuse Keccak.Size instead of a local MaxHashLength constant.
- Decode base32 arithmetically instead of scanning the alphabet string.
- Guard the decoded-length check against int overflow on hostile input.
- Hash UTF-8 bytes from a stackalloc/pooled buffer instead of allocating.

* refactor(dns): log hash-mismatch rejections at Debug, clarify base32 mapping

---------

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

* Selectable dotTrace profiling mode + dotnet-trace EventPipe sidecar for benchmark workflows (#12708)

* benchmarks: selectable dotTrace profiling mode (sampling | tracing | timeline)

The dottrace input on run-rpc-benchmarks and run-expb-reproducible-benchmarks
becomes a choice: false | sampling | tracing | timeline ('true' stays accepted
as a legacy alias for sampling on API dispatches). The mode maps to the
dotTrace CLI's --profiling-type: rpc-bench sets it on the wrapped entrypoint in
start-node.sh; the EXPB workflow passes --dottrace-mode to expb (only when
non-default, so pinned expb versions keep working for sampling runs; requires
execution-payloads-benchmarks feature/dottrace-profiling-modes for the new
modes).

Timeline snapshots cannot be converted to XML by Reporter.exe, so the
generate-dottrace-reports and summary jobs skip that mode - the raw .dtp
artifact is still collected and uploaded for the dotTrace UI. Line-by-line is
deliberately not offered: it needs PDBs the client docker images do not carry.
EXPB additionally rejects trace_blocks with timeline, since per-block
snapshots ride the MeasureProfiler API, which needs a performance session.

* expb workflow: collect a dotnet-trace EventPipe sidecar with every dotTrace run

Whenever dottrace is enabled the run also passes --dotnet-trace to expb: a
host-side EventPipe session records gc/contention/threading/exception events
(no CPU sampler - dotTrace owns the stacks) and the .nettrace ships in the same
dottrace-* artifact. Requires expb feature/dottrace-profiling-modes.

* rpcbench/expb: document the profiling modes and fix two review nits

Docs still described the pre-mode behaviour: the rpc-bench README documented a
dottrace command line with no --profiling-type and stated capture deliberately
runs in default sampling mode, and its input table listed dottrace as a boolean.
AGENTS.md's expb section had the same gap. Both now cover the four choices, when
to reach for each, that timeline yields no XML, and the dotnet-trace sidecar -
that section is what agents read to interpret run artifacts.

Timeline snapshots now save as .dtt rather than .dtp. The report job is gated off
for timeline either way, but the extension is what stops Reporter.exe's .dtp glob
from picking up a snapshot it cannot convert if that gate is ever relaxed.

The expb dottrace flag no longer special-cases sampling: the run already requires
an expb that understands --dotnet-trace, so there is no older-expb compatibility
left to preserve, and the flag string was spell…
The machine re-applies the EIP-161 RIPEMD-160 touch over its own rollback
(#13016), but the frame executor then restores to a snapshot taken before the
machine ran, rewinding past it. With 0x03 present and empty, a frame that makes
a zero-value call to it and reverts left the account undeleted at commit and the
state root diverged.

Three rollbacks needed the restore, being the ones that leave the transaction
valid and committed: the frame revert in ExecuteFrame, the POST_TX assertion
failure, and the atomic-batch unroll. The last two run long after the touching
frame's substate is gone, so the flag is accumulated for the transaction. The
rollbacks that return MalformedTransaction, the prefix simulator and the
CallAndRestore path all discard their state, so none of them needs it.
)

* chore(frames): cut EIP-8141 comments down and drop stale markers

Comment-only sweep over the frame-transaction delta: multi-line docs cut to
1-2 lines, session narration and merged EIP PR references removed, and gap
markers the spec has since settled deleted.

* chore(frames): retire four EIP-8141 markers settled since they were written

Verified against the current EIP-8141 text and the frames branches, not inferred:

- SupportsBlobs: the type-6 blob network wrapper, proof validation, pool
  routing, and production path all landed. The predicate is deliberately
  type-3-only; blob presence is Transaction.CarriesBlobs.
- APPROVE return data: the spec now states the memory region becomes the
  frame's return data following RETURN semantics, which is what we do.
- NotSupportedTxFilter: dependency-set revalidation is implemented on
  eip8141-dependency-revalidation. The failed-APPROVE replay bound and the
  payer reservation for restored blob-pool records are still open.
- Expiry sweep: the unswept broadcaster half is fixed separately; only the
  linear scan remains.

* chore(frames): record the predeploy nonce intent and the two-dimensional wire shapes

The expiry verifier installs with no nonce, which reads as an oversight beside
its two siblings that declare 1; note that leaving it is a stop-gap expected to
change once it deploys like the other system contracts.

Correct the frame-tx payload doc to the grouped fees list the decoder reads, and
spell out that a frame receipt's gas_used is the execution/state pair.

* chore(frames): restore the review facts the cull took with it

Four comments the sweep trimmed too far:

- FrameTxPayerExposureFilter: restore the EIP8141-DEVIATION marker and its
  zero-gas-limit clause. GasLimit is the frame-gas sum, so with every frame
  limit at zero the reservation is ~0 and the exposure bound does not bind.
- TxPool: name where _expiringFrameTxCount is maintained, so a reader knows
  why the read side only needs Volatile.Read.
- FrameTxValidation: restore the floor-headroom semantics and why the memo
  needs a type test, plus the intrinsicGas param doc.
- FrameTxValidationTests: the header promised an EIP8141- note the file no
  longer carries; describe the convention it actually follows.

* chore(frames): restore the summaries the sweep dropped and unsay "uncapped"

FrameTxDecoder's DecodeKeys was left documented by a <remarks> alone and its
sibling EncodeKeys by nothing; GasEstimator.CannotEstimateGasExceeded, the one
public member in a run of private error strings, lost its only doc; and
RecentRootStore.IsReferenceValid's summary was reworded to "As above", which
names nothing in IntelliSense and nothing in the file either, the overload above
it carrying no doc at all.

The filter-ordering comment also called the frame signature list uncapped, which
FrameTxDecoder.SignaturesDecodeCap bounds at 1024, and dropped the elliptic-curve
cost that is the actual reason the filter runs last. Both halves corrected at all
three sites carrying the claim.

* chore(frames): keep the why on APPROVE's payment ordering guard

The cull removed a sentence covering all three payment guards. Two of them —
a second payer and an underfunded payer — do restate the code, but the middle
guard encodes an EIP-8141 ordering rule that is not legible from the boolean:
payment may not be approved before execution unless the same APPROVE grants
both. Restores that clause only, on the guard it describes.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
…EstimateFrameTx (#12996)

* fix(frames): report missing frames distinctly from a gas overflow in EstimateFrameTx

A type-6 transaction reaches EstimateFrameTx on its type alone, so one with no
frames returned the gas-overflow message for an overflow that never happened.
Report the missing frames instead, with a regression test.

* test(frames): route the DoS harness output through FRAME_RETRY_OUT

The verify-cost harness appended to a literal /tmp path, which threw on Windows
and diverged from the sibling harnesses. Use FRAME_RETRY_OUT with a
Path.GetTempPath() fallback, matching UnpaidBurnPerAttempt alongside it.

* fix(frames): reject a frame transaction carrying no frames instead of faulting on it

A type-6 transaction reaches the frame processor and the frame estimator on its type
alone, so an entry point that skips validation (eth_call, eth_estimateGas) can hand
either one a transaction whose frame list is absent or empty.

The processor dereferenced that list unguarded, so eth_estimateGas for a type-6
transaction without frames faulted with a NullReferenceException and answered the caller
with an internal error and a stack trace. It now refuses the transaction as malformed
under the canonical FrameTxValidation.MissingFrames message, which is what the caller
sees, since eth_estimateGas probes before it estimates and the probe's error wins.

The estimator's own guard now matches the canonical frame-count check rather than a
subset of it: an empty or oversized list prices into a budget no valid transaction can
carry, and only an absent list took the gas-overflow path it was previously blamed on.

* fix(frames): enforce the frame count upper bound where the processor reports it

The processor guard reported the 1-to-64 message while only rejecting an
absent or empty frame list, so an oversized type-6 transaction was admitted
on eth_call and executed frame by frame while eth_estimateGas refused it.

---------

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

* docs: EIP-8141 public-mempool rules design note

* EIP-8141: per-payer mempool exposure accounting for frame transactions

Bounds each resolved frame-tx payer's summed pending maximum cost to the
payer's balance at admission, per the merged public-mempool rules
(ethereum/EIPs#12007). Reuses the DelegationCache event-counter precedent: a
PayerExposureCache maintained from the pool insert/remove events, read by a new
FrameTxPayerExposureFilter placed after the payer-resolution filter.

Enforcement is scoped to natively-resolved payers (Transaction.PayerAddress);
unresolved and non-frame txs pass through. Canonical/non-canonical paymaster
reservation, replacement payer-switch atomicity, eviction ordering,
dependency-indexed revalidation, and the MAX_VERIFY_GAS bound remain documented
follow-ups (see MEMPOOL-RULES-DESIGN.md).

* EIP-8141: close exposure-bound gaps from mempool-rules review

- Reject blob fields on frame txs at validation (NonBlobFieldsTxValidator
  in the frame composite) so a blob-carrying frame tx can no longer bypass
  the gas-only per-payer exposure bound while frame-blob support is off.
- Make the reservation atomic: PayerExposureCache.TryReserve compare-and-sets
  the check and reserve in the filter, closing the check-then-act race where
  concurrent same-payer submissions could all pass a stale total. Release moves
  to the Removed event plus the non-insert (ReplacementNotAllowed) path.
- Saturate the reservation counter and drop per-call closures in the cache.
- Add a TxPool-level accounting test (two frame txs sharing a sponsor payer,
  second rejected, released on removal) and parameterize the filter tests.
- Correct the max-cost wording (gas-only for frame txs) in the filter comment
  and MEMPOOL-RULES-DESIGN.md.

* EIP-8141: reject only blob-carrying frame txs, fix exposure leak

Replace the presence-based NonBlobFieldsTxValidator in the frame-tx
composite with a value check in FrameTxValidation: the decoder always
populates max_fee_per_blob_gas and blob_versioned_hashes, so the presence
check rejected every frame tx. Now only frame txs with a non-empty blob
hash list are rejected; the no-hashes zero-fee shape is admitted.

Release the per-payer exposure reservation from a finally in AddCore so a
throw before insertion cannot leak it, and document the deliberate
under-count for a persistent-broadcast-retained frame tx (no release hook
on the broadcaster). TryReserve now returns the observed reservation so
the trace reports the total the decision was made on.

Remove the design note from the repo tree.

* EIP-8141: relocate end-to-end payer-exposure test to simulation layer

The per-payer exposure gate only fires for a resolved third-party payer.
Native admission now resolves only self-payers, which the sender balance
filter already bounds, so the shared-sponsor end-to-end case moves to the
layer where simulation resolves the payer. The filter-level unit tests
still prove the gate at this layer.

* EIP-8141: share the validation-prefix grammar between pricing and payer resolution

FrameTxPayerResolver restated the prefix-shape predicates that Core's
FrameTxValidation already owns and the verify-gas admission filter prices
against, and the two had drifted: Core recognised a leading deploy frame in
the prefix, the resolver did not, so [deploy, self_verify] got recognised-
prefix pricing yet never resolved natively. Expose the shape predicates from
FrameTxValidation and consume them in the resolver, skipping an optional
leading deploy frame like the expiry frame so pricing and the payer verdict
classify a prefix the same way.

* EIP-8141: collapse redundant only_verify branch in payer resolution

Both the only_verify branch and the fall-through returned RequiresSimulation,
so the branch was inert. Collapse to a single return, preserving behaviour.

* EIP-8141: in-pool validation-prefix simulation for opaque frame transactions

Admit the opaque frame-tx prefixes the native resolver defers
(RequiresSimulation: deployed-code sender, code-carrying paymaster,
unrecognized shape) by simulating their validation prefix in a bounded,
read-only EVM, resolving the payer and enforcing the trace/opcode rules.

The standard, natively-resolvable prefixes keep the EVM-free fast path:
FrameTxSimulationFilter returns immediately for a payer already resolved
by FrameTxPayerFilter, so the simulator is never consulted for them.

- IFrameTxPrefixSimulator abstraction in Nethermind.TxPool (no reference
  to Consensus's processing env, which would cycle); implemented by
  FrameTxPrefixSimulator at the composition root wrapping
  IReadOnlyTxProcessingEnvFactory, injected optionally into the pool.
- ExecutionOptions.FrameValidationPrefixOnly + a validation-prefix
  simulation path in the frame processor: halts once the payer is set,
  bounds cumulative work by MAX_VERIFY_GAS, always restores state.
- FrameTxValidationTracer enforces the banned-opcode list, the
  GAS-before-call and TIMESTAMP-in-expiry-verifier caveats, and
  SLOAD-restricted-to-sender; violations reject the transaction.

When no simulator is wired, RequiresSimulation stays deferred as in
Phase 1. Deferred (design note §4): dependency-set-keyed caching,
head-change re-simulation indexing, wall-clock cancellation, and a
multi-simulation admission budget.

* EIP-8141: enforce CALL*/EXTCODE* target rules in validation-prefix tracer

Address review findings on the Phase 2 in-pool simulation:

- High: FrameTxValidationTracer now rejects CALL*/EXTCODE* whose target is
  neither an existing contract nor a precompile, or that uses an EIP-7702
  delegation (spec §Validation Trace Rules, L816/L853), exempting tx.sender.
  Previously these prefixes were silently admitted despite the doc claim.
- TIMESTAMP exemption now checks the EXPIRY_VERIFIER code hash as well as the
  address (L788), via a new Eip8141Constants.ExpiryVerifierCodeHash.
- GAS-before-call is validated by peeking the next opcode instead of a
  cross-frame mutable flag, so a trailing GAS is no longer missed or
  mis-attributed across frame boundaries.
- The post-frame MAX_VERIFY_GAS bound is now reachable: a prefix frame capped
  to the remaining budget that then exhausts it is rejected as over-budget,
  distinct from a within-budget revert.
- IFrameTxPrefixSimulator.Simulate takes a CancellationToken (honored at
  entry; OperationCanceledException propagates).

Adds tests for the CALL* codeless-target rejection, the existing-contract
allow case, and the over-budget rejection reason.

* EIP-8141: reconcile opaque-payer simulation with updated payer resolution

The payer resolver now takes the sender account and defers all only_verify|pay
prefixes to simulation, so the simulation filter passes the sender account when
re-resolving, and its tests drive the resolver through the same account state.
Also stop attributing an explicit within-budget REVERT to the MAX_VERIFY_GAS
cap: only a capped frame that ran out of gas is reported as over-budget.

* EIP-8141: add end-to-end payer-exposure test at the simulation layer

Relocated from the mempool-rules PR, where native admission no longer resolves a
third-party payer. Here the opaque only_verify|pay sponsor prefix is resolved by
validation-prefix simulation, so the exposure gate bounds the sponsor's summed
pending cost to its balance end-to-end and releases the reservation on removal.

* reconcile phase-2 simulation with configurable verify-gas bound

Re-add the consensus MAX_VERIFY_GAS constant the prefix simulator needs;
isolate the exposure test from the configurable ingress bound.

* EIP-8141: cap pending frame txs per non-canonical paymaster

The public-mempool rules bound how many pending transactions a single
non-canonical paymaster may sponsor (MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER
= 1), so one sponsor's balance or code change cannot invalidate an unbounded
set of pending transactions. Nothing enforced that: a flood of frame txs all
paying through one contract sponsor was admitted and gossiped.

FrameTxPaymasterFilter rejects a frame tx whose recognized validation prefix
ends in a pay frame targeting a code-carrying account that already sponsors the
maximum pending transactions. The count is keyed on the pay-frame target, which
FrameTxValidation.GetPrefixPaymaster derives from the frame layout alone, so the
pool's insert and remove hooks stay symmetric even when the paymaster's code
changes while a transaction is pending. A default-code sponsor is not a
paymaster and stays governed by the per-payer exposure rule.

No canonical paymaster runtime is pinned in production yet, so every code-
carrying pay target is treated as non-canonical; exempting canonical instances
additionally requires their balance reservation, which remains outstanding.

* EIP-8141: let a sponsored frame tx be replaced, and cap it before signature work

The pending cap counted the transaction a fee bump would displace, so with
MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER = 1 a sponsored frame tx could
never be re-priced or cancelled by replacement: the tx it replaces still held
the paymaster's only slot when the filter ran, and the pool only swaps them
later, inside TryInsert. The spec decrements on replacement, so the pending set
does not grow; discount a same-sender, same-nonce pending tx naming the same
paymaster before comparing against the cap.

Requiring the same paymaster matters, since replacing a tx sponsored elsewhere
frees that sponsor's slot while still taking one here.

Also move the filter ahead of FrameTxSignatureFilter. It needs only the frame
layout and one account read, so leaving it behind the uncapped elliptic-curve
work defeated the point of a cheap flood gate; the trade-off is that a flood tx
is no longer also reported as malformed.

Cover the TxPool wiring end to end, which the hand-driven counter in the filter
tests did not: two senders sharing one code-carrying pay target, admitted then
capped then admitted again after removal.

* EIP-8141: correct the frame-tx blob validator comment

* EIP-8141: name the deploy-frame decline and meter prefix simulation

Reject a validation prefix containing a deploy frame with its own reason rather than
falling through to "never set a payer", correct the docs that attributed the decline to
the tracer's opcode bans, and add a rejection counter for the simulating filter.

* chore: drop unused usings from the frame-transaction measurement fixtures

* EIP-8141: make the shared expiry-frame predicate self-guarding and meter the exposure gate

Restore the value and data-length checks the resolver's private copy had, so reading the
deadline out of a matching frame needs no separate precondition; add the sibling rejection
counter; correct the exposure comment, which described a bound the branch no longer has.

* EIP-8141: log the undiscounted paymaster count and record the recount gap

* EIP-8141: keep the exposure reserve and release symmetric on a zero cost

A zero-cost reservation inserted an entry the matching release never reclaimed. Also read
the payer's balance from the account the pipeline already cached when the payer is the
sender, and pin the overflow and concurrency properties the reservation exists to provide.

* EIP-8141: cover the payer reservation lifecycle through the pool

A same-nonce replacement is the one shape that reaches the exposure gate at this layer, so it
pins both the reservation taken through the filter chain and its release on the pool event.
Also trim the gate comment's inventory of what is still missing.

* EIP-8141: state that the paymaster carve-out is the empty code hash, not self-payment

Pin a code-carrying sender naming itself in the pay frame as capped, drive the blob-path
replacement test through the real blob pool type, and record the reorg re-admission and
broadcaster-retained count gaps.

* EIP-8141: name the exposure rejection like its siblings and make the expiry read total

Rename the admission result and its message to the FrameTx prefix the other frame-tx outcomes
and metrics use, mark the under-reserved max cost as a deviation rather than a gap, restore the
qualified payer-exposure entry to the gate inventory, and route TryGetExpiryDeadline through the
self-guarding predicate so it can no longer throw on a malformed frame.

* EIP-8141: stop a payerless frame transaction from disconnecting the peer

AcceptTxResult compares by id, so returning Invalid for a structurally payerless prefix reached
the flood controller's immediate-disconnect arm. Give it its own result, as the expired and
verify-gas verdicts already have, and cover the refused-replacement release path.

* chore: trim the frame-tx exposure comments to the non-obvious why

* Merge branch 'eip8141-mempool-phase2' into eip8141-paymaster-pending-cap

Renumber the paymaster-cap result: AcceptTxResult compares by id, so it collided with the
simulation-failed result the merge brought down.

* chore: trim the prefix-simulation comments to the non-obvious why

* EIP-8141: make a leaked payer reservation observable

Gauge the number of payers holding a reservation, tracked on the two dictionary transitions, so
an idle pool reading non-zero is the leak itself. Also merge the duplicated remarks on the expiry
predicate, derive the pricing assertion from its frames, and vary the refused replacement by a
field the reservation is not computed from.

* EIP-8141: publish the reservation gauge atomically and keep its pair symmetric

Interlocked on the metric itself, so a racing transition cannot leave a stale count standing,
and release now compare-and-sets like reserve so a double release touches nothing. Narrow the
pay-frame predicate back to private, and vary the refused replacement by its target.

* EIP-8141: cover the self-paying exposure path and make its counter atomic

The fixture only ever built third-party payers, so the branch every real admission takes was
untested. Also match the rejection counter to the gauge beside it, select the replacement's frame
by shape rather than index, and extend the payerless summary to the rule it now states.

* EIP-8141: pin the blob term in the payer exposure bound

Nothing asserted that a blob-carrying frame tx reserves its blob leg, so the bound was gas-only
by accident of every fixture leaving the hash list null.

* chore: reattach the exposure fixture doc and guard its frame lookup

* EIP-8141: pin the blob exposure term by magnitude and the deploy-prefix edges

The blob case asserted only that the reserved amount grew, so a bound that
dropped the GasPerBlob or blob-count factor stayed green; it now pins the
inclusive boundary and the reserved total for one and two blobs.

Adds the two unpinned edges of the widened validation-prefix grammar: a second
deploy frame is not skipped (RequiresSimulation, matching the pricing grammar),
and an expiry frame followed by a deploy frame still records the expiry
dependency.

* chore: sort the payer exposure cache usings

* EIP-8141: widen the blob exposure arithmetic and regroup the fixture

The blob term was computed in int, which wraps once the blob count times the
blob fee exceeds 16383; it is now long, pinned by a six-blob case that fails
on the int version. Tests and private helpers are no longer interleaved, so a
new test cannot orphan a helper's doc comment again.

* chore: trim the frame-transaction comments to the non-obvious why

Also states the exposure deviation's floor honestly: the reserved amount is
zero, not a fraction, when every frame gas limit is zero.

* EIP-8141: trim frame-tx mempool comments

* EIP-8141: trim paymaster cap comments

* chore: keep the comment trim to code this change owns

Reverts the trim on FrameTxValidation and TxValidator, whose bodies this change
leaves untouched: the param tags and memoization contract on TryCalculateGasBudget
and the sidecar-validator note were documentation, not commentary. Restores the
EIP8141 deferred-work marker on the expiry pass, which is the only record of the
broadcaster-held expired frame txs this sweep does not reach.

Also drops a using left unnecessary by the blob fixture helper.

* EIP-8141: restore comments on code this change does not touch

* EIP-8141: keep the deferred-work items in the gap marker

* chore: correct two stale comments on the payerless and reserve paths

A payerless frame tx is no longer rejected as Invalid, and TryReserve can also
report a zero reservation when it refuses the very first one.

* EIP-8141: drain the payer reservations when the pool is disposed

DisposeAsync unsubscribed the Removed handler without releasing what was still
reserved, so those payers stayed counted by a process-wide gauge no pool could
decrement — making a non-zero reading the norm rather than the leak signal the
gauge exists to give.

* EIP-8141: keep the exposure gauge pairing in one place

Both removal paths now go through RemoveTracked, so the decrement cannot drift
from the entry count as sites are added. Clear no longer claims to close the
teardown window: a submission already in flight can still reserve after it.

* EIP-8141: make the teardown drain total again

Routing Clear through the value-comparing removal let a reservation updated
during enumeration survive with its gauge increment, permanently — the floor
the drain exists to remove. Clear removes unconditionally; TryAdd is the only
increment, so retiring an entry still retires exactly one.

* EIP-8141: price a replacement without the reservation it displaces

The exposure gate runs before AddCore resolves a same-sender/same-nonce
replacement, so the incumbent was still reserved and the bump was charged for
both. Any self-paying frame tx costing more than half its payer's balance could
therefore never be fee-bumped, while non-frame txs of the same account stayed
replaceable. The bound is on the pending set the pool would hold, and EIP-8141
decrements on replacement, so the displaced tx is excluded from it — held, not
settled, since its own release still runs when Removed fires.

* Restore the exposure gate's independent test coverage and skip its bucket walk when unneeded

Adds a pool-level case pinning the payer bound over nonces above the replaced one,
which BalanceTooLowFilter does not sum; verified it fails with the filter removed.
The replacement discount is now computed only when the payer holds a reservation,
since TryReserve ignores it otherwise.

* Size the exposure test's balance off the reservation rather than a fixed constant

The frame gas the builder emits is not TxGasLimit, so the hardcoded balance left the
bound slack and the case stopped discriminating once max_cost was repriced.

* Validate recent-root references before the prefix simulation runs

RECENTROOTREFLOAD is legal in a VERIFY frame and reads the declared references on the
strength of the pre-state check the main path performs, which the simulation path
skipped. Anchored to the earliest slot the transaction could execute in, so a reference
to the head slot is not rejected for being one slot early.

Also tightens the deploy-frame decline to RecognizedPrefixLength's actual precondition,
drops the calldata-stat guard GasLimitTxFilter already makes redundant, and pins
AcceptTxResult id uniqueness, which the compiler cannot see.

* Give the paymaster cap a free result id and the pool's competing key

NonCanonicalPaymasterLimitReached shared id 25 with FrameSimulationFailed, and
AcceptTxResult compares by id alone, so the two were equal; 26 is taken by
FrameSimulationDeferred on the simulation-guards branch, so this takes 27.

The replacement discount now matches on the pool's competing key, since an EIP-8250
transaction sharing a nonce in another key domain joins the pending set rather than
displacing it and must still count against the sponsor.

Restores the payer-exposure gap item, which is still open on this branch.

* Merge master into eip8141-frame-txs-devnet7 (#12893)

* fix(rpc-bench): run benchmarked nodes the way production runs them (#12625)

* diag(rpc-bench): run Nethermind with production-default runtime (no TC=0 pin)

* reword comment for master

* review: document expb divergence, add NODE_ENV_VARS escape hatch

- README: the 'Alignment with expb' section no longer claims the removed
  env pins; documents the deliberate code-gen divergence and that JIT
  warm-up now lands inside the measured window; dotTrace reports are not
  comparable across this change
- start-node.sh: reword comment (no warm-up phase exists yet), add
  NODE_ENV_VARS passthrough for deliberate one-off code-gen experiments

* trim comments to one-liners; rationale stays in the PR

* drop the Merge GC flags: inert here and misleading

GCKeeper only runs on Engine API calls; this harness parks the node at a
snapshot head and never sends newPayload, so the three flags changed
nothing while implying the node ran a non-production GC configuration.

* keep the image entrypoint for Nethermind

The override skipped entrypoint.sh, which applies host tuning and enables a
shipped PGO profile. Its comment claimed parity with expb, but expb only
overrides the entrypoint for dotTrace, so normal expb runs (and production)
do run entrypoint.sh - this harness was the outlier.

* Rename EIP-8037 regular gas dimension to execution gas (#12600)

* Auto-update fast sync settings (#12665)

Co-authored-by: rubo <rubo@users.noreply.github.com>

* refactor(net): namespace snap by version (#12606)

* refactor(net): namespace snap messages by version

Prepare the snap subprotocol for a second version by moving the concrete
snap/1 messages, message codes and protocol handler into a versioned
namespace, so a snap/2 handler can be added alongside without touching
snap/1 code.

  Snap/Messages/*            -> Snap/V1/Messages/*
  Snap/SnapMessageCode       -> Snap/V1/Snap1MessageCode
  Snap/SnapProtocolHandler   -> Snap/V1/Snap1ProtocolHandler
  P2P/P2PMessageKey.cs       -> P2P/VersionedProtocol.cs  (file renamed to
                                match the type it declares)

SnapMessageBase and SnapSerializerBase deliberately stay in
Snap.Messages: they are shared by all protocol versions, not specific to
snap/1.

Introduce SnapVersions constants and ISnapSyncPeer.SnapProtocolVersion so
version checks can replace the magic numbers currently spelled as 1.

PeerInfoExtensions.CanGetSnapData is renamed to CanGetTrieNodes to say
what it actually tests. The rename is nominal - the peer probe is
unchanged, so behaviour is identical.

No functional change.

* refactor(net): remove Snap2 version constant from SnapVersions

* address review comments

* rename

* feat(sync): serve block access lists from the snap server (#12607)

* Refactor SnapServer and SnapStateServer integration

- Renamed SnapServer to SnapStateServer for clarity and consistency.
- Updated WorldStateManager to use SnapStateServer instead of SnapServer.
- Modified StateSyncFeedTestsBase to accommodate changes in SnapServer instantiation.
- Adjusted SnapProviderTests to reflect the new SnapStateServer type.
- Introduced SnapServerTests to validate SnapServer functionality.
- Added SnapStateServerTests to ensure robust testing of state management.
- Implemented new methods in SnapServer for handling bytecode and block access lists.
- Enhanced test coverage for account range retrieval and storage management.

* refactor: change SnapServer field type to interface ISnapServer

* test: enhance SnapServerTests with additional block access list scenarios

* chore: Update Dockerfiles (#12663)

Update Dockerfiles

Co-authored-by: rubo <rubo@users.noreply.github.com>

* test: make prewarmer env-return assertion pool-hit independent (#12616)

PreWarmCaches_ReturnsAddressWarmEnvWhenScopeBuildThrows asserts
Returned == Created, but with maxPoolSize 1 an env returned to the pool
can be rented again without a Create, so a pool hit legitimately
increments Returned twice against a single Create. Whether a hit occurs
depends on worker interleaving: flaky on CI, fails deterministically on
high-core machines (0/30 locally).

ThrowingBuildPolicy.Return now refuses retention, so every rental is a
fresh Create and the invariant holds under any interleaving (30/30
locally). Stack-trace instrumentation confirmed every rental is returned
exactly once - the prewarmer itself is correct.

* Update OP Superchain chains (#12664)

Co-authored-by: emlautarom1 <emlautarom1@users.noreply.github.com>
Co-authored-by: Lukasz Rozmej <lukasz.rozmej@gmail.com>

* fix(receipts): restore the post-merge flag before regeneration (#12641)

* fix(receipts): restore the post-merge flag before regeneration

Stored headers do not carry IsPostMerge and regeneration bypasses the
recovery step that restores it, so post-merge blocks re-executed with
PREVRANDAO evaluating to the zeroed difficulty - any transaction reading
it produced receipts failing the root check (~7% of post-merge blocks
refused on mainnet archives deriving receipts from state).

* test(receipts): dispose buffer, pin logged value

* fix(receipts): classify post-merge via the switcher

A difficulty heuristic misreads chains that repurpose the field -
Taiko stores per-block ZK gas in Difficulty while AlwaysPoS - so ask
IPoSSwitcher instead, mirroring MergeProcessingRecoveryStep. On
mainnet the switcher's TD-null branch is the same difficulty check,
so behavior there is unchanged.

* test(receipts): pin the real switcher's TD-null derivation

The production failure arrived as a mainnet-shaped header with
TotalDifficulty unset; cover PoSSwitcher's TD-null branch end to end,
not only the honour-the-switcher contract.

* test(receipts): cover the switcher registration path

A hand-injected switcher cannot catch a composition regression that
leaves the container-resolved regenerator on the NoPoS default, so
resolve it from a graph whose IPoSSwitcher registration is overridden
the way a merge-enabled node overrides it.

* fix(tests): mark RecoverReceiptsBlockchain.Create as hiding

* Expose the node's ENR in admin_nodeInfo (#12631)

feat(rpc): expose the node's ENR in admin_nodeInfo

Nethermind is the only discv5-capable execution client whose
admin_nodeInfo omits the node record. Tooling that bootstraps
discv5-only networks reads the ENR from this endpoint and has to
special-case Nethermind to the enode instead, which is useless once
discv4 is disabled.

NodeRecordProvider already maintains a signed, sequence-numbered self
record, so surface it as an 'enr' field. Move INodeRecordProvider to
Nethermind.Network so Nethermind.JsonRpc can reference it without a new
dependency on Nethermind.Network.Discovery; the implementation stays in
Discovery. The provider is only registered when discovery is enabled, so
it is resolved optionally and the field is omitted otherwise.

* Validate ABI decode allocation bounds (#12588)

* ci: disable stateless glamsterdam-devnet-7 scheduled run (#12680)

* Fix EIP-7708 tracing with logs (#12577)

* Test for EIP-7708 top frame log

* [WIP] attach log to parent frame

* Attach log to correct frame

* Reuse common code in tests

* More tests

* Naming

* fix(flat): guard the trie-warmer against a TransientResource recycle race (storage reads as 0x00) (#12429)

* fix(flatdb): warm the trie from persistence only

The trie warmer read the recyclable `_snapshots` and `_transientResource`
while the warm job held only a `ReadOnlySnapshotBundle` lease, which does not
cover them. A concurrent scope reset could recycle those under the running
warmer, so a warm read could return a torn or foreign node.

The warmer only needs to warm from persistence, so restrict its reads to the
trie node cache and the `ReadOnlySnapshotBundle` - exactly what the lease
covers. In-memory nodes are already hot and do not need warming.

Claude-Session: https://claude.ai/code/session_01S3BG321zDG9BNjAgWhZhCX

* fix(flat): warm the transient resource via a per-job lease

The trie warmer now warms nodes into the per-job-pinned TransientResource
(not just the trie node cache and ReadOnlySnapshotBundle), covered by a single
transient lease held for the whole warm traversal. It still never reads the
recyclable _snapshots. A warmer read on a torn-down bundle bails to a
persistence-only read instead of spinning for a lease that will never land.

Claude-Session: https://claude.ai/code/session_01S3BG321zDG9BNjAgWhZhCX

* refactor(flat): drop the warmer transient ThreadStatic capture

Measured TS-on vs always-per-node-lease on x10 (3 runs, heavy-block warmer
load): newPayload 178 vs 178 ms, getProof p99 20.1 vs 20.2 ms, RSS identical.
The ambient capture bought nothing, so keep only the per-node lease + ABA
re-check (the actual recycle-race fix) and drop the ThreadStatic + the
EnterWarmerTransientScope pins in the two warm-job callers.

* fix(flat): register the transient return owner at pool checkout

- ResourcePool.GetCachedResource now calls OnRented, so every checkout
  carries a registered return owner; a final ReleaseLease without one
  throws instead of silently dropping the resource (which leaked the
  BloomFilter allocation on the public AddSnapshot path)
- re-check _isDisposed after the warmer's lease acquire: Dispose releases
  the owner lease but leaves _transientResource pointing at the recycled
  instance, so the identity re-check alone could latch a resource already
  re-rented by another bundle
- document why ReleaseLease is distinct from Dispose: the pool contract
  reserves Dispose for destroying an over-capacity resource
- run Nethermind.State.Flat.Test in the nethermind-tests.yml matrix; it
  was compiled but not run by any CI job
- FlatDbManagerTests duplicate-snapshot test asserts the resource lands
  back in the checkout pool; new ResourcePoolTests cover the final-release
  return and the unregistered-release throw; refresh stale warmer test
  comments

* fix(flat): pin the transient resource for prewarm dedupe reads

ShouldQueuePrewarm read _transientResource without pinning it. The dedupe
bloom lives on that recyclable resource and the call runs on prewarmer and
BAL threads, so the owner could retire the resource mid-read: the pool
Resets it and, on overflow, Disposes the BloomFilter, whose backing store is
native memory. Route both overloads through the same lease + ABA re-check
the warmer node reads already use, and decline the prewarm on a torn-down
bundle. Rename the helper accordingly, since it is no longer warmer-only.

The FlatWorldStateScopeProvider and FlatOverridableWorldScope test doubles
returned the committed resource to the pool directly instead of releasing
its lease, mirroring neither AddSnapshot implementation. That recycles the
resource while a warmer lease is outstanding and returns it a second time
when that lease is released; the scope provider double also returned the
wrong instance and then returned it again on teardown. Both now release the
lease, which is the single return-to-pool path.

Test changes:
- the persistence-only test now commits the written nodes into the bundle's
  recyclable _snapshots before reading, so the warmer's Unknown result is a
  genuine miss. Previously the node was still in the transient (SetStateNode
  writes both) and was itself Unknown, so the assertion held either way.
- the churn test gives every epoch its own persisted node instance, so a
  read served from another epoch's recycled transient is caught by identity
  rather than by value, drives both recycle paths (CollectAndApplySnapshot
  swap and Dispose), exercises ShouldQueuePrewarm alongside the node reads,
  and joins the readers with a bounded wait instead of blocking forever.
- new test: a warmer read and a prewarm check on a disposed bundle fall back
  to the leased persistence reader within a bounded wait, covering the
  Dispose bail-out deterministically.

* fix(jsonrpc): synchronise SubscriptionManager per-client subscription bag (#12672)

* fix(jsonrpc): synchronise SubscriptionManager per-client bag

The per-client subscription bag is a HashSet mutated and enumerated from
multiple threads: concurrent subscribe requests (socket worker tasks),
unsubscribe, and the Closed handler that fires on connection teardown.
Concurrent HashSet access could corrupt it, dropping a subscription so
its event handlers stayed attached and leaked. Lock on the bag for every
add, remove, and snapshot-before-dispose.

Fixes #12668

* refactor(jsonrpc): dispose client subscriptions under bag lock without snapshot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(jsonrpc): race unsubscribe path too; drop bag field comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test: eth_createAccessList affordability with omitted fee fields (#12629)

* test(rpc): eth_createAccessList affordability with omitted fee fields (execution-apis #854)

execution-apis PR #854 clarifies that eth_createAccessList must not fail
solely because an unfunded sender cannot afford client-selected default fees
when all gas-fee fields are omitted. Nethermind already conforms (verified in
hive rpc-compat); this adds a regression test mirroring the conformance
fixture: unfunded sender, codeless recipient, zero value, no gas/fee fields
-> {"accessList":[],"gasUsed":"0x5208"}.

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

* test(rpc): run affordability test on London chain, assert no in-body error, reuse helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

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

* Fix stale transaction pool snapshots (#12685)

* Encode engine_newPayloadWithWitness witness as an RLP data string (#12635)

* fix

Signed-off-by: jsign <jsign.uy@gmail.com>

* Tighten witness RLP JSON encoding

---------

Signed-off-by: jsign <jsign.uy@gmail.com>
Co-authored-by: jsign <jsign.uy@gmail.com>

* perf(state): skip trie warmup for read-only BAL accounts in flat layout (#12681)

* perf(state): skip trie warmup for read-only BAL accounts in flat layout

With a suggested BAL the block's write set is known upfront, and trie
nodes are only needed at commit for written accounts. Gate address
trie-warm hints (HintBal, HintGet, HintWarmAccount) on the BAL write
set so read-only accounts no longer trigger state-trie path walks.

On BAL blocks dominated by cold account reads this removes up to ~8-10
wasted trie-node DB reads per unique read-only account from the
measured processing window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Apply suggestions from code review

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

* refactor(state): extract QueueStateTrieWarmup and address review findings

- Extract the NeedsStateTrieWarmup + PushAddressJob + increment pattern
  into QueueStateTrieWarmup, used by all three address warmup call sites
- Hoist CancelHintBal above the empty-BAL early return so a stale write
  set never survives into the next block
- Drop the inaccurate bloom false-positive-rate comment
- Tests: parameterize warm-per-write-kind over balance/nonce/code/storage,
  add empty-BAL reset regression test, split the HintWarmAccount test,
  wrap scopes in using, use order-insensitive assertions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update src/Nethermind/Nethermind.State.Flat/ScopeProvider/FlatWorldStateScope.cs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* refactor(test): reuse TestContext for recording-warmer scope construction

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(state): bind BAL warmup gate to HintBal lifecycle; address review findings

- Keep _warmupWriteSet across StartWriteBatch: on the parallel BAL path the
  BAL apply commits mid-block, concurrently with tx workers, so clearing the
  gate in CancelHintBal collapsed it at a nondeterministic point. The gate is
  now replaced only by the next HintBal.
- Drop the token from Task.Run in both HintBal implementations: a task
  cancelled before being dequeued never ran the finally that returns the
  pooled accountChanges array. The body already observes the token.
- Align TrieStoreScopeProvider.HintBal with the flat scope: a new hint
  supersedes the previous one even when it carries no work itself.
- Remove the stale prestate-load mutation mention from ReadOnlyBlockAccessList
  docs, state the immutability invariant, and seal the type.
- Tests: pin the gate surviving StartWriteBatch and a second BAL replacing
  the previous write set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* test: make can-never-fail tests assert what their names claim (#12690)

* test(xdc): drop redundant trailing Assert.Pass in SpecialTransactionsTests

* test(core): bound McsLock re-acquire test instead of passing unconditionally

SingleThreadAcquireRelease asserted nothing and is subsumed by
ReacquireAfterReleaseSucceeds, which now runs on a worker with a timeout
so a broken release fails in seconds instead of hanging the test host.

* test(flat): assert real postconditions instead of Assert.Pass

Add_ConcurrentWithMightContain_ShouldWork now verifies no write is lost
under concurrent readers (a bloom filter never false-negatives).
DisposeAsync test renamed: FlatDbManager does not dispose the repository
(the container owns it); it now asserts bounded drain completion and
idempotent double-dispose. ConstructorAcceptsPersistedRepository removed -
Not.Null on a fresh object, subsumed by every other test in the file.

* test(merge): assert pending-validation cleanup instead of catch-only assertions

The memory-leak test asserted only inside catch blocks and ended with
Assert.Pass, so it passed whenever nothing threw; it now drives the
handler directly and asserts the pending-validation count stays zero
across repeated timed-out payloads. The TrySet double-completion test
is removed: it was a weaker duplicate of the concurrent-calls test
above it (same scenario, but swallowing OperationCanceledException and
keying Assert.Fail on exception message text).

* test(merge): await header-sync test helpers

The helpers were async void and invoked without awaiting - including one
un-awaited call inside the other helper - so their assertions raced the
test body and failures could surface as host crashes or not at all.

* test(network): restore DisconnectsAnalyzer assertions with deterministic flush capture

All four tests had their assertions commented out as CI-flaky, leaving
them assertion-free. The flakiness came from racing the 10ms flush
timer: reports are now recorded while the default 10s interval is in
effect and only then is the interval shortened, so a flush cannot fire
mid-arrangement. Assertions scan captured flush reports, including that
counters aggregate, reset after each flush, and cleared categories do
not resurface (the analyzer double-buffers, so a lost clear shows up as
a stale count in every other flush, not as a doubled count).

* test: address review findings on strengthened tests

Will_clear_after_report no longer issues a second report at all - a
report can race the flush's enumerate-then-clear window (Timer.Stop does
not drain a queued Elapsed callback, so no test-side quiesce is airtight).
A lost clear is observable without it: the analyzer double-buffers, so a
stale count resurfaces in later flushes, and the test asserts the
category appears in exactly one flush across several more (mutation-
verified). Bloom capacity raised to 100k - at 10k the saturated filter's
~34% false-positive rate masked a single lost write - and misses are
collected into one assertion. The repeated-timeouts payload test is
dropped rather than parameterized: the pending dictionary is keyed by
block hash and the test resubmits one block, so the count can never
exceed one and iterations add no coverage. The FlatDbManager dispose
test states its intent with Assert.DoesNotThrowAsync and a corrected
comment (WaitAsync bounds the wait, not the drain). Wait timeouts are
named constants and polling uses Thread.Sleep(1).

* test: simplify comments per ASD-STE100 and drop dead times parameter

Comments now use short, active, single-topic sentences. The times
parameter of ShouldEventuallyReport had no remaining non-default call
site after the second-report removal.

* test: use SpinWait.SpinUntil instead of a custom poll helper

Keep one condition re-check after a timeout: a flush can land in
SpinUntil's final sleep tick.

* Stop parallel transaction execution once BAL validation rejects the block (#12697)

* fix(consensus): stop parallel tx execution once BAL validation rejects

The parallel BAL block validator ran the incremental validator alongside the
transaction workers, but a validator failure faulted neither the worker loop nor
its cancellation token. `ParallelUnbalancedWork` only stops fetching new indices
on caller cancellation or a transaction-worker fault, so a block rejected at an
early transaction index still executed every remaining transaction before the
foreground observed the failure at `GetResult()`. For a block whose invalidity is
decided by a cheap prefix, that turns a sub-second rejection into slot-scale CPU
work.

`IncrementalValidationWorkItem` now owns a cancellation source that is signalled
together with the stored exception, and the worker loop runs under that token, so
workers stop pulling transactions as soon as validation becomes terminal. The
resulting `OperationCanceledException` is translated back into the original
validation failure, keeping the returned error identical.

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

* refactor(consensus): signal BAL validation failure with a flag, not cancellation

Routing the stop signal through ParallelUnbalancedWork's cancellation token meant
`For` ended by throwing an OperationCanceledException that existed only to be
caught and translated back into the original validation failure. The token also
needed a per-block CancellationTokenSource, whose recycling in Schedule was only
safe because the previous block's validator had already been joined.

Replace all of it with a volatile read of the exception the work item already
stores: workers check `HasFailed` before doing any work, the loop drains its
remaining indices without executing anything, and `GetResult()` reports the
rejection on the normal return path. Same behaviour, no exception used as control
flow, no extra state to keep in sync, and no ordering constraint between Schedule
and the work that follows it.

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

* refactor(test): trim comments and simplify the tail-cancellation test

Comments cut back to the non-obvious reasoning only. The regression test now
counts executions instead of collecting indices, so a single Is.InRange assertion
covers both requirements — the decisive prefix ran, the tail did not — and the
prefix array, the index bag and one assertion all go away. Transaction count is a
plain constant rather than derived from the canonical lead, which the test never
depended on: with uniform gas limits the tail sort is stable, so the schedule is
natural order regardless. CreateParallelValidationTransactions takes an optional
gas limit so the block no longer needs patching after construction.

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

* fix(consensus): address review — exempt iteration 0, loosen test bound

Iteration 0 (WaitForBalWarmup + ApplyStateChanges) is now exempt from the
HasFailed guard, so pre-execution keeps its previous semantics instead of being
droppable when validation fails before any worker starts. Skipping it was traced
as benign, but the fix only needs to stop transaction execution, so leaving the
pre-execution step alone keeps the behavioural change narrower at no cost.

The regression test's upper bound leaned on SpinWait outlasting exception
unwinding, because the fake validator releases its gate before the work item
stores the exception. Bound is now txCount / 8, which asserts the tail stopped
without depending on cancellation-propagation timing — a revert still executes
all 2048 and fails.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* test: pin eth/62-66 serializer wire encodings with hand-derived goldens (#12696)

* test(network): remove duplicate eth serializer tests

ZeroNewBlockMessageSerializerTests.Roundtrip2 and V63
NodeDataMessageSerializerTests.Zero_roundtrip were verbatim copies of
their Roundtrip siblings. Can_deserialize_own_eth_64 shared its body
and one payload with Can_deserialize_eth_64; its unique payload moves
there as another TestCase.

* test(network): pin eth/62-66 wire encodings with hand-derived goldens

Every serializer test file in Eth V62-V66 now asserts an exact wire
encoding somewhere. The goldens are derived from the RLP rules and
verified with an independent encoder (pyrlp + pycryptodome keccak);
values shared across files live in EthSerializerGoldens. ToString
smoke tests now assert the log line names the message type, and the
32-byte-selector test asserts the exact decoded hash.

* test(network): address review feedback on serializer goldens

- BlockBodies: pin the null-body framing (c1c0) and the empty-vs-absent
  withdrawals distinction (e9e8...c0c0)
- Status To_string: assert against the independent Protocol.Eth constant
- V63 NodeData: rename Roundtrip_with_nulls to Roundtrip_with_empty_entry
  (the data holds an empty array, not null)

* feat: EIP-8037 two-dimensional gas tracing (stateGasTracer + callTracer) (#12628)

* feat(tracing): add EIP-8037 stateGasTracer (execution-apis #852)

Implements the `stateGasTracer` named tracer specified in execution-apis
PR #852, returning the per-transaction two-dimensional gas summary
`{gasUsed, regularGasUsed, stateGasUsed, gasRefund}` (EIP-8037/EIP-7778).

The values are already computed for block-level gas accounting and carried
on `GasConsumed`; the tracer only reads and formats them. Adds the missing
applied EIP-3529 refund (capped) to `GasConsumed.GasRefund`, populated in
the success and top-level-halt refund paths. The native tracer factory is
threaded with `IReleaseSpec` so the fork is determined explicitly.

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

* feat(tracing): add EIP-8037 two-dimensional gas to callTracer (execution-apis #852)

Adds `regularGasUsed`, `stateGasUsed` and `gasRefund` to the callTracer
top-level frame for Amsterdam+ blocks, per execution-apis PR #852. The
fields are gated on `IReleaseSpec.IsEip8037Enabled` (MUST NOT appear before
the fork) and set only on the top frame (omitted on sub-frames). Values are
read from the transaction's `GasConsumed` result, matching the stateGasTracer.

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

* refactor(tracing): address #12628 review feedback

- Restore native-tracer factory API back-compat: keep the public 4-arg
  GethLikeNativeTracerFactoryDelegate/RegisterTracer/CreateTracer so external
  plugin registrations stay source- and binary-compatible; built-ins receive
  the (nullable) IReleaseSpec via an internal spec-aware factory.
- stateGasTracer: disable IsTracingOpLevelStorage/IsTracingStack so the
  terminal-only tracer stops invoking per-opcode storage/stack callbacks.
- Consolidate the callTracer top-frame regularGasUsed/stateGasUsed/gasRefund
  into a single TwoDimensionalGas? value, removing the coupled nullables and
  the unsafe null-forgiving dereferences in the converter.
- Document that regularGasUsed is floor-clamped (block-accounting value) so
  the two-dimensional invariant is exempted under the calldata floor.
- Add an end-to-end stateGasTracer test executing a real Amsterdam tx through
  the TransactionProcessor (fresh SSTORE + in-tx reset) that exercises field
  selection and the GasConsumed.GasRefund plumbing; assert
  regularGasUsed + stateGasUsed == gasUsed + gasRefund.
- Test cleanups: multiple-assert scope and a shared helper for the two
  callTracer Amsterdam cases.

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

* refactor(tracing): address #12628 re-review nits

- Document the spec-less CreateTracer overload's pre-fork fallback and cover it
  with a factory test (back-compat public contract was untested).
- stateGasTracer hex test now forces the ambient NumberConversion.Raw so it
  actually exercises StateGasTraceConverter's hex-quantity override.
- E2E test: use a `using` tracer and correct the refund assertion message
  (the slot is reset to its original zero value within the tx, not pre-nonzero).
- Convert the remaining `//` member comments to XML doc.

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

* style: remove unused using in NativeStateGasTracerE2ETests

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

* refactor(tracing): drop excess comments; non-nullable spec in native tracer factory

Address flcl42 review on #12628:
- Remove the newly added explanatory comments that restated the code (kept only a
  few essential EIP-referenced DTO docs and the hex-conversion note).
- Make the release spec non-nullable through GethLikeNativeTracerFactory and the
  native tracers: drop the unused spec-less 4-arg CreateTracer overload (the only
  null source) — RegisterTracer, the actual plugin API, is unchanged. Also drops
  the brittle regularGasUsed occurrence-count assertion flcl42 flagged.

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

* style: collapse double blank line before DeepNesting test

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

---------

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

* fix(simulate): skip EIP-3607 on the EIP-7928 BAL path in eth_simulateV1 (#12691)

* fix(simulate): skip EIP-3607 on the EIP-7928 BAL path in eth_simulateV1

eth_simulateV1 relaxes EIP-3607 so a state-overridden contract can be the tx
`from`. That relaxation covered only the main execution (via
SimulateTransactionProcessorAdapter). Under glamsterdam (EIP-7928),
BlockProcessor runs transactions through the BlockAccessListManager's own tx
processors, which bypass the adapter and re-enforce EIP-3607 — so a contract
sender is rejected with `-38024 sender has deployed code` instead of reaching
the normal balance/fee checks (hive rpc-compat divergence on glamsterdam-devnet-8:
ethSimulate-simple-send-from-contract*, ethSimulate-override-address-twice).

Relax EIP-3607 on the block execution context in
SimulateBlockValidationTransactionsExecutor instead:
ParallelBlockValidationTransactionsExecutor sets that context on both the main tx
processor and the BAL manager, so both paths skip the check — while BlockProcessor
still receives the unwrapped spec, preserving chain-specific release-spec
interfaces (Taiko / XDC / Optimism).

Verified end-to-end on a glamsterdam-at-genesis chain (the three hive fixtures
flip -38024 -> -38014/-38012, matching besu/erigon/reth/geth) and by an
integration test through the real EIP-7928 BAL path that fails -38024 without the
fix and passes with it.

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

* fix(simulate): preserve PrevRandao in context rebuild; harden 3607 BAL test

Address re-review on #12691:
- SimulateBlockValidationTransactionsExecutor is now the single context-rebuild
  funnel for the simulate scope, so preserve the incoming PrevRandao (via
  BlockExecutionContext.WithPrevRandao*) instead of re-deriving the default —
  a BlockProcessor subclass (e.g. XdcBlockProcessor) may have supplied a
  non-default value.
- Make eth_simulateV1_contract_sender_skips_eip3607_on_bal_path validation-enabled
  so the -38014 expectation is fork-independent and stable across the #12692 fix
  (with validation:false the -38014 relied on the BAL path ignoring NoValidation).
  Pin the EIP-7928 premise with an explicit BlockLevelAccessListsEnabled assert.

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

* fix(simulate): forward BlobBaseFee too in the context rebuild

Re-review follow-up on #12691: WithPrevRandao recomputes BlobBaseFee from the
header, so the no-override branch was newly lossy for a processor that forced a
non-derivable value (XdcBlockProcessor sets BlobBaseFee = 0 on a header cloned
with ExcessBlobGas = 0; recomputation yields MinBlobGasPrice = 1). Collapse to a
single WithPrevRandaoAndBlobBaseFee that forwards the incoming BlobBaseFee (or the
block override when present), leaving Spec as the only field the rebuild changes.

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

* fix(simulate): relax EIP-3607 via execution flag, not spec wrapping

The eth_simulateV1 contract-sender fix relaxed EIP-3607 by wrapping the block
execution context spec with WithoutEip3607(), which returns a NoEip3607Spec
decorator implementing only IReleaseSpec. Because the tx processors read their
spec from that context (TransactionProcessor.GetSpec => BlockExecutionContext.Spec),
the decorator reached chain-specific processors that hard-cast it — e.g.
TaikoTransactionProcessor.PayFees does (ITaikoReleaseSpec)spec and
XdcTransactionProcessor casts on every tx — so eth_simulateV1 on Taiko/XDC threw
InvalidCastException/InvalidOperationException instead of returning a result
(Eip3607Transition defaults to 0, so the wrap is always active there).

Replace the spec wrapping with a dedicated ExecutionOptions.SkipSenderCodeCheck
policy flag gated in ValidateSender. The main simulate adapter ORs it into its
Process call; the EIP-7928 BAL path receives it via a small
BlockAccessListTxExecutionOptions injected into BlockAccessListManager and threaded
to its ExecuteTransactionProcessorAdapter. The release spec now keeps its concrete
runtime type on every path, so chain-specific interfaces survive.

Regression test: Nethermind.Taiko.Test exercises a code-bearing sender through
TaikoTransactionProcessor.PayFees — it passes with the flag and throws
InvalidCastException under the old spec-wrapping. eth_simulateV1 BAL-path and full
simulate suite remain green.

Follow-up to #12691; addresses the residual type-erasure raised in its review.

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

* chore(simulate): trim explanatory comments to essentials

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

* refactor(simulate): carry EIP-3607 relaxation on BlockExecutionContext

Replace the ExecutionOptions.SkipSenderCodeCheck flag (and the machinery to thread
it into the EIP-7928 BAL path) with a SkipSenderCodeCheck field on
BlockExecutionContext. Both the main tx processor and the BAL manager's own
processors already share the context via SetBlockExecutionContext, so the field
reaches every path for free — no threading through BlockAccessListManager /
TxProcessorPool / ExecuteTransactionProcessorAdapter, and no injected policy type.

The context is the same shared channel #12691 originally relaxed on; this just uses
a flag instead of a spec decorator, keeping the spec's concrete runtime type (so
ITaikoReleaseSpec/IXdcReleaseSpec casts survive). Net simpler diff and consistent
with the existing IsGenesis flag on the same struct.

ValidateSender reads VirtualMachine.BlockExecutionContext.SkipSenderCodeCheck; the
simulate executor sets it in its context rebuild. Taiko regression test updated to
set the flag on the context (still throws InvalidCastException under spec-wrapping).

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

* refactor(simulate): carry EIP-3607 relaxation on TransactionProcessor

Per review preference (@LukaszRozmej), move the SkipSenderCodeCheck flag from
BlockExecutionContext onto TransactionProcessor. ValidateSender reads the
processor's SkipSenderCodeCheck property. The simulate scope sets it type-
preservingly on both creation paths: Intercept<ITransactionProcessor> for the
main processor, and a factory decorator for the EIP-7928 BAL processors — so each
chain keeps its concrete processor type (no spec wrapping, no processor-type swap).

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

* style: trim comments to the essential why

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

* refactor(simulate): remove dead WithoutEip3607; address review polish

Follow-up to the SkipSenderCodeCheck switch (review by @claude on #12691):
- Remove the now-dead EIP-3607 spec wrapper — this PR dropped its last production
  caller. Deletes WithoutEip3607, NoEip3607Spec, GetNoEip3607Spec (std + zkevm) and
  the _noEip3607Specs cache; keeps WithoutEip158 (still live). Fixes two comments
  that cited the removed decorator.
- Enforce the relaxation invariant: a shared Apply() throws if the resolved
  ITransactionProcessor isn't a TransactionProcessorBase, instead of silently
  no-op'ing back to -38024. Used by both the Intercept hook and the factory decorator.
- XML-doc SkipSenderCodeCheck with its set-before-use / unsynchronised-read invariant.
- Make SkipSenderCodeCheckTransactionProcessorFactory internal.
- Revert SimulateBlockValidationTransactionsExecutor to master (the fix no longer
  touches it; its PrevRandao tweak was unrelated).
- Assert fee payment in the Taiko regression test (matches its name).

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

---------

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

* fix(dns): verify EIP-1459 subtree hashes (#12707)

* fix(dns): verify EIP-1459 subtree hashes

EIP-1459 serves every subtree entry from the subdomain
base32(keccak256(entry)[..16]). The root signature covers only the
enrtree-root entry, so this hash chain is the only binding between the
signed root and the branch and ENR records a client consumes. The
crawler never checked it, so any resolver or poisoned cache could
substitute arbitrary node records and steer the crawl to
attacker-chosen labels.

Verification follows go-ethereum: unpadded standard base32, prefix
comparison against keccak256 of the record, abbreviated labels accepted
down to 12 decoded bytes (minHashLength). The tree root is exempt, as
EIP-1459 serves it from the bare domain with no hash label. A mismatch
logs at Warn and skips the record.

No base32 decoder existed in src/Nethermind, so EnrTreeHash carries a
small one, tested against labels produced by the reference
implementation.

Validated against live trees: all.mainnet.ethdisco.net crawls 3000
records with zero rejections, so the check does not misfire on
anything the reference publisher emits.

* refactor(dns): simplify and harden EnrTreeHash

- Reuse Keccak.Size instead of a local MaxHashLength constant.
- Decode base32 arithmetically instead of scanning the alphabet string.
- Guard the decoded-length check against int overflow on hostile input.
- Hash UTF-8 bytes from a stackalloc/pooled buffer instead of allocating.

* refactor(dns): log hash-mismatch rejections at Debug, clarify base32 mapping

---------

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

* Selectable dotTrace profiling mode + dotnet-trace EventPipe sidecar for benchmark workflows (#12708)

* benchmarks: selectable dotTrace profiling mode (sampling | tracing | timeline)

The dottrace input on run-rpc-benchmarks and run-expb-reproducible-benchmarks
becomes a choice: false | sampling | tracing | timeline ('true' stays accepted
as a legacy alias for sampling on API dispatches). The mode maps to the
dotTrace CLI's --profiling-type: rpc-bench sets it on the wrapped entrypoint in
start-node.sh; the EXPB workflow passes --dottrace-mode to expb (only when
non-default, so pinned expb versions keep working for sampling runs; requires
execution-payloads-benchmarks feature/dottrace-profiling-modes for the new
modes).

Timeline snapshots cannot be converted to XML by Reporter.exe, so the
generate-dottrace-reports and summary jobs skip that mode - the raw .dtp
artifact is still collected and uploaded for the dotTrace UI. Line-by-line is
deliberately not offered: it needs PDBs the client docker images do not carry.
EXPB additionally rejects trace_blocks with timeline, since per-block
snapshots ride the MeasureProfiler API, which needs a performance session.

* expb workflow: collect a dotnet-trace EventPipe sidecar with every dotTrace run

Whenever dottrace is enabled the run also passes --dotnet-trace to expb: a
host-side EventPipe session records gc/contention/threading/exception events
(no CPU sampler - dotTrace owns the stacks) and the .nettrace ships in the same
dottrace-* artifact. Requires expb feature/dottrace-profiling-modes.

* rpcbench/expb: document the profiling modes and fix two review nits

Docs still described the pre-mode behaviour: the rpc-bench README documented a
dottrace command line with no --profiling-type and stated capture deliberately
runs in default sampling mode, and its input table listed dottrace as a boolean.
AGENTS.md's expb section had the same gap. Both now cover the four choices, when
to reach for each, that timeline yields no XML, and the dotnet-trace sidecar -
that section is what agents read to interpret run artifacts.

Timeline snapshots now save as .dtt rather than .dtp. The report job is gated off
for timeline either way, but the extension is what stops Reporter.exe's .dtp glob
from picking up a snapshot it cannot convert if that gate is ever relaxed.

The expb dottrace flag no longer special-cases sampling: the run already requires
an expb that understands --dotnet-trace, so there is no older-expb compatibility
left to preserve, and the flag string was spelled out twice in each of two
copy-pasted job bodies. The trace_blocks default guard becomes an explicit if -
as the last statement of its if body the AND-list left the block with status 1,
surviving only through errexit's AND-OR exemption.

* docs: scope the EventPipe sidecar to EXPB

The rpc-bench README claimed every profiled run drops a .nettrace into the
dottrace-rpcbench artifact, but --dotnet-trace was only wired into the EXPB
workflow - nothing in run-rpc-benchmarks.yml or scripts/rpc-bench collects one, so
the paragraph sent readers hunting for a file that is never produced, and promised
it precisely for timeline runs, which have no XML either. Say what a timeline
rpc-bench run actually yields: the .dtt snapshot alone.

* fix(jsonrpc): serialize receipt root as full-width DATA (#12706)

* fix(jsonrpc): serialize receipt root as full-width DATA

* test(jsonrpc): parameterize the receipt-root width cases

* test(jsonrpc): pin the whole-byte leading-zero root case

* test: add hand-derived golden tests for eth/71 and snap serializers (#12699)

* test(network): pin eth/71 and snap serializer wire encodings

Every serializer test file in Eth V71 and Snap/V1 now asserts an exact
wire encoding. The goldens are hand-derived from the RLP rules and
verified with an independent encoder (pyrlp + pycryptodome keccak).
Random request ids are pinned only in the goldened tests; ByteCodes
gains a sibling roundtrip that keeps the constructor-assigned id, and
GetAccountRange pins long.MaxValue to keep the eight-byte id path.

* test(network): share repeated snap golden fragments

The request-id, keccak-of-empty-string, and range-hash fragments moved
into SnapSerializerGoldens. Each golden fragment and its test input
come from one hex constant, so the expectation cannot drift from the
input.

* test(network): address review feedback on snap golden tests

- ByteCodes: one parameterized Roundtrip; long.MaxValue pins the
  eight-byte request id deterministically instead of a random id
- GetAccountRange: independent field asserts in EnterMultipleScope
- SnapSerializerGoldens: expose RequestId1111 next to its RLP item;
  the remarks state which fragments share hex with inputs and that
  the keccak("") fragment is an independent literal on purpose

* `debug_trace*`: Fix phantom logs on frame revert (#12621)

* Test for EIP-7708 top frame log

* [WIP] attach log to parent frame

* Attach log to correct frame

* Reuse common code in tests

* More tests

* Phantom log test

* Remove logs on a reverted frame

* Naming

* Fix leaking `ArrayPooList`

* Code cleanup

* Additional test

* Simplify tests

* Build fix

* Snap sync: reject storage range responses with unmatched slot lists (#12729)

* fix(snap): reject storage range responses with unmatched slot lists

A `StorageRanges` reply carries one slot list per requested account, but
nothing verified that the responder returned no more lists than were asked
for. `AddStorageRange` looped over the response count and indexed the request
account list with it, so a longer-than-requested reply read past the end of
the request.

Reject such a response up front and retry the range, which also settles the
active storage request accounting so that the range download phase can still
reach completion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(snap): pin the slot list count boundary

Cover equal and fewer slot lists than requested accounts so that widening the
guard to `>=` cannot pa…
…tate

A frame that ran to completion and then failed TryApplyApproval had its substate
replaced wholesale. ShouldRestoreRipemdTouch is init-only, so the replacement
carried false: the rollback that follows re-applied nothing, and the transaction
accumulator read the replacement rather than what the frame recorded.

Reaching it in a transaction that commits is narrow. Both refusal points need
approvesPayment, which the APPROVE opcode allows only while the payer is unset,
so the failing frame is always in the validation prefix and the transaction is
normally rejected for want of a payer. The nonce-ceiling refusal is permanent and
can only end that way; the new-account state-gas refusal is not, so a later frame
can still approve payment and the transaction commits with the touch lost.
…12788)

* test(eip8141): map the frame-format exception for transaction tests

The transaction-test exception table carried no TYPE_6_* entry, so a fixture
naming TransactionException.TYPE_6_INVALID_FRAME_FORMAT could never match: the
runner reported the expected label against the client message even when the
transaction had been rejected for exactly the right reason.

Point the entry at the shared FrameExceptionFragments sets rather than repeating
them. TYPE_6_INVALID_FRAME_FORMAT is the only label this path can produce, since
it stops at TxValidator.IsWellFormed, which runs neither the signature validator
nor the transaction processor.

Decode is used in place of the neighbouring s_rlpDecodeFragments, whose broader
"Invalid signature" fragment also matches the frame signature failures and the
authorization-list signature error, which would let a format fixture pass on a
rejection the fixtures file under a different label.

Test-only: nothing outside Ethereum.Test.Base changes.

* test(eip8141): cover the trailing-signature decoder rejection

The decoder rejects a frame transaction whose trailing element is present but is
not the recent-root-reference sequence, and no fragment matched that wording. It
is reachable on the transaction-test path, where a decode exception reaches the
label comparison verbatim, so the fixture label went unmatched.

The decoder has no constant to reference for it, so the literal is listed, as
Decode already does for the generic decoder wordings.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
Comparing the present-and-empty arm against the absent one catches a transaction
that stops committing, but not a rollback that stops happening: if the touching
frame starts succeeding, nothing is at risk, the account is deleted anyway and the
arms still match while testing nothing. That is live for the approval arm, which
rests on a state-gas limit of 0 being below the new-account cost.

Each arm now asserts that the transaction executed and that the frame it depends
on was actually rolled back, so either drift fails loudly instead of silently.
…13011)

* fix(txpool): make merged master code frame-transaction aware

Three follow-ups to the master merge in #13001, all of the same shape:
code that came across from master and is not aware of frame transactions.

- The blob pool's ready filter compared an EIP-8250 keyed sequence to the
  account nonce, so a keyed blob-carrying frame transaction never looked
  ready and never reached a block. Reuse the keyed-aware IsNonceReady the
  non-blob path already uses.
- A persisted light blob record this build cannot read threw out of the
  pool constructor and so out of node startup. The blob pool is a cache:
  skip the record and log instead.
- eth/72 inherited the unhoisted head-spec lookup that eth/68 hoists, so
  every type-6 announcement in the loop repeated it.

* fix(txpool): aggregate the unreadable-record warning and hoist the delivery gate

Review follow-ups to the three #13001 fixes.

- A record layout change leaves every persisted light blob record unreadable
  at once, so one Warn per record floods startup in exactly the scenario the
  skip exists for. Count the skips and log once, quoting the first failure so
  a lone corrupt record stays diagnosable. The record is left in place: the
  summary is one line per start, and it is the only signal that the pool is
  carrying records this build cannot read.
- eth/72's delivery path repeated the head-spec lookup for every type-6 in the
  packet, the same way the announcement path did before the hoist. Thread the
  resolved gate through ValidateAnnouncedPooledTransaction, which leaves the
  per-call CanRequestPooledTransaction overload unused; drop it.
- LightTxDecoderTests claimed GetAllValues walks the column in key order. It
  does not — GetAll passes ordered: false and MemDb returns
  ConcurrentDictionary.Values. Nothing in the test needs an order.

* fix(txpool): name the exception type in the skipped-record summary

Review follow-ups.

Exception.Message carries no type name, so the summary named only the message.
The type is what separates a whole-column layout change from a single corrupt
record, which are the two cases this catch covers, so quote both. The
regression test derives the expected type from the same decoder call rather
than hard-coding it, and asserts it reaches the warning.

Also drops the ordering comment left stranded on the two key fields: it
described the enumeration in a test two methods above and said nothing about
the fields it sat on. The test does not depend on the order, so there is
nothing for it to record.

* fix(txpool): narrow the skipped-record catch to the reachable roots

The bare Exception catch is replaced by a filter over the types the decode
path can actually produce. RlpException covers the RLP grammar errors,
including RlpLimitException and DecodeKeccakRlpException, which derive from
it. ArgumentException covers BlobCellMask.FromBytes rejecting a mask that is
not 16 bytes, and RlpReader.Read slicing past the buffer, since Read does
Data.Slice(Position, length) with no bounds check. IndexOutOfRangeException
covers a bogus long-form length walking the reader past the end.

Narrowing to RlpException alone would have let a truncated record abort node
startup, which is what this skip exists to prevent.

Records_failing_in_different_ways_are_all_skipped pins the width with four
corruption shapes spanning the three roots, so removing any one of them from
the filter now fails a test rather than going unnoticed.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
Brings the branch up to master at 3bc542e. Merged with a real merge commit, not
squashed, so master enters this branch's ancestry and later merges do not replay
these conflicts.

Thirteen files needed both sides; VirtualMachine.cs was the only textual conflict,
where master's RestoreRipemdTouch and the branch's RestoreStateGasJournal both land
after the same _worldState.Restore and both are kept.

Also carries two consensus fixes for the EIP-161 RIPEMD touch, which only surface
once master's #13016 and the frame executor coexist: the touch is re-applied after
each frame rollback that leaves the transaction valid, and it survives the substate
replacement an approval failure performs.
…ory (#13026)

* fix(evm): resolve a frame delegation's precompile through the repository

ExecuteFrame asked the spec whether an EIP-7702 designation points at a
precompile, then took the code from the repository. Under a precompile
state override the two disagree: a moved precompile is absent from the
spec's list but still dispatched by the repository, so the frame decided
"not a precompile" and then executed one through the designation.

Ask the repository, which is what dispatches. GetPrecompile records no
account access, so the EIP-7928 block access list is untouched, and on
the non-overridden path it agrees with the spec by construction.

The entry access charges stay on the spec: there the precompile check
stands in for EIP-2929's pre-warmed set, as ConsumeAccountAccessGas has
it, so a frame pays what an equivalent CALL pays.

* test(evm): pin the vacated precompile address and collapse the processor setup

A state override moves a precompile away from its spec address as well as to a new
one, so a designation to the vacated address now dispatches as code. Pin that, and
share the processor and block construction the three cases had each copied.

* test(evm): cover a plain code override on a precompile address

The move and the code override apply independently, so an override carrying code alone
also makes the address dispatch as code. Parameterise rather than copy the scenario.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
* fix(rpc): carry frame receipts through debug_insertReceipts

ReceiptForRpc.ToReceipt() dropped Payer and FrameReceipts, so every
EIP-8141 frame receipt submitted to debug_insertReceipts reached storage
as the frames-less shape — which encodes as an empty frames list but no
longer decodes, since a frame transaction always executes at least one
frame. In practice the receipts-root check in DebugBridge.InsertReceipts
rejected it, so nothing malformed was stored, but a well-formed frame
receipt could not be inserted either and failed with a misleading
"Receipts root mismatch".

Propagate the payer and per-frame results in ToReceipt(), and reject a
frame-typed receipt carrying no frames at the RPC boundary, where the
input is untrusted and there is an error channel. Validating in the
encoder instead would fire inside block processing and peer serving, and
would need duplicating across the three receipt encoders.

* fix(rpc): reject a null receipt entry in debug_insertReceipts

A null array element dereferenced into an internal error, where the method
already treats a shape EIP-8141 cannot produce as a caller error.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
* test(eip8141): pin frame tx blob presence and drop the stale spec-gap marker

The marker claimed the spec leaves sidecar handling undefined and that blob support is
off. Neither holds: EIP-8141 specifies the EIP-7594 wrapper for blob-carrying frame txs,
and the branch implements it end to end. SupportsBlobs staying type-3-only is deliberate,
since a frame tx may or may not carry blobs — state that instead.

Cover the invariant that makes it safe: a blob-carrying frame tx is still recognised as
carrying blobs, which is what every blob path in the node keys on.

* test(eip8141): build the blob hash list without LINQ

Drop the System.Linq dependency the new case pulled into the file, and soften
the comment: one validator path still keys on SupportsBlobs, so a narrowed
CarriesBlobs would not slip past literally every blob path.

* test(eip8141): cover the absent blob hash list alongside the empty one

A null BlobVersionedHashes reaches a different branch of both CarriesBlobs
and GetBlobCount than an empty array, and only the latter was pinned.

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
* test(eip8141): cover APPROVE return data and drop the stale spec-gap marker

EIP-8141 now states that APPROVE's offset and length designate a memory region that
becomes the frame's return data, following RETURN semantics — which is what the
instruction already does, so the marker asking the spec to settle it is obsolete.

The behaviour had no coverage. A DELEGATECALL preserves ADDRESS, so it is the only
path where an APPROVE leaves a caller able to observe the region; assert it there.

* test(eip8141): stop claiming DELEGATECALL is the only observable APPROVE path

CALLCODE preserves ADDRESS too, so it reaches the same resolved-target check and its
caller observes the return data alike. DELEGATECALL stays the vehicle for the test;
only the comment's exclusivity claim was wrong.

Thread the approval length through the helper so the operand and the assertion that
checks it no longer state 32 independently.

* test(eip8141): budget the APPROVE return-data frame for two-dimensional gas

EIP-8141 devnet7 splits the frame budget, and the six-argument TxFrame overload leaves
limits.state at zero. The frame's two SSTOREs then exhaust state gas, discarding the
approval with them, so the transaction never executes.

* test(eip8141): assert the frame statuses rather than the transaction status

Transaction-level status is derived from the POST_TX outcome alone, so a
failed DEFAULT or SENDER frame still reports success. Asserting the frame
statuses is what the sibling scenarios do and names the failing frame.

---------

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

Copy link
Copy Markdown
Contributor Author

Builder claims only replay-invalid candidates (0a540ac); replay code bounds MAX_REPLAY_CODE_BODIES/MAX_REPLAY_CODE_BYTES enforced (cfe5bf1). All 39 FOCIL/frame fixtures pass.

@claude review

@claude

claude Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 1m 43s —— View job


PR review

  • Gather context: the commits since the last review (e655f869, 0a540acd, cfe5bf17)
  • Review builder claims (only replay-invalid candidates)
  • Review replay code bounds (MAX_REPLAY_CODE_BODIES / MAX_REPLAY_CODE_BYTES)
  • Post findings

Verdict: not mergeable yet. There is one High finding. The new code-body counting in cfe5bf17 lets an includer drive gigabytes of Keccak on the attester's replay path. Builder claims (0a540acd) and the per-tx gas gate (e655f869) are fine.

High

LoadCode re-hashes the whole code body every time a loop jumps back to pc 0. Inline comment: FrameTxValidationTracer.cs:166.

  • The branch pc == 0 && _codeBodies is not null runs on every instruction at offset 0, not just when a frame starts. LoadCode computes ValueKeccak.Compute(code) before the dedup Add.
  • Take a sender whose code is a JUMPDEST PUSH0 JUMP loop (11 gas per iteration), padded to Bogota's 64 KiB MaxCodeSize, which is under MAX_REPLAY_CODE_BYTES. Under the 2**18 cap that is about 23.8k iterations, or about 1.5 GB of Keccak per replay.
  • Up to about 4 such candidates per position fit in the budget, across 16 positions. All of it runs under the replayer's lock, synchronously in BranchProcessor and NewPayloadHandler.
  • The includer pays nothing: the prefix runs out of gas, so the candidate is ineligible and never goes on chain.
  • Fix: skip CodeInfo instances already counted (reference comparer) before hashing. Use the stamped CodeInfo.CodeHash, or the codeHash the EXTCODE* path already has, instead of recomputing it. Add a loop regression test.

Builder claims only where the candidate is invalid (0a540acd): checked and fine

  • SetInvalid covers exactly the reasons the attester re-checks at k:
    • the execution and state reservation fit, which the attester evaluates against gasUsedBefore[k] in each dimension;
    • the nonce and the keyed nonce set, both checked in the replay;
    • an execution rejection, which comes from the transaction processor.
  • Policy skips are left without a claim, so the attester judges them at n. These are: too large for the CL, already in the block, init code too large, an EIP-3607 contract sender, a sender-balance skip (non-frame only), the gas reservation not being computable, and any AddingTransaction handler. A claim binds the builder, so not claiming a policy skip is the safe direction.
  • Set resets IsInvalidHere, so a handler that overrides the action later cannot leave a stale flag.
  • invalidHere is read only together with claimOnSkip, which still requires the first attempt and a claimable hash.
  • TransactionsExecutorTests covers both sides.

Per-tx gas recorded only once Profile 2 is active (e655f869): fine

The last review's optional Low is addressed. Before activation GasUsedBeforeTransactions is null, and nothing reads it there, because claims are cleared before activation.

Replay code bounds: the rest is fine

  • Bodies are deduplicated by hash across execution and EXTCODE* reads. Empty code counts as nothing. The body-count bound and the byte bound are each checked with >, so a prefix exactly at either limit is allowed. EXTCODEHASH is correctly not counted.
  • GetCode on a 7702-delegated target returns the 23-byte designator, which is what EXTCODESIZE/EXTCODECOPY read, so that is what gets counted.
  • Only the replayer passes the bounds. The mempool's defaults of 0 leave its behaviour unchanged.

Still acknowledged in the PR body

  • The tests-focil-devnet@v0.2.0 engine fixtures are rejected for missing membership. That is correct under execution-apis#911, and they are deliberately left out of KNOWN_FAILURES.

I only read the code and didn't build or run tests. I reviewed fa72b5e8..cfe5bf17. The "39 fixtures pass" result is your figure; I didn't check it.

Comment on lines +166 to +170
if (pc == 0 && _codeBodies is not null)
{
LoadCode(env.CodeInfo.CodeSpan);
if (Violated) 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.

High (DoS on the attester path, new in cfe5bf1): pc == 0 fires on every opcode at pc 0, not only when a frame starts, so a loop that jumps back to 0 re-hashes the whole code body on every iteration.

StartOperation runs for each instruction, so a JUMP to a JUMPDEST at offset 0 re-enters this branch. LoadCode runs ValueKeccak.Compute(code) before the HashSet.Add dedup. The dedup stops the count from growing, but not the hashing.

  • A sender whose code starts JUMPDEST PUSH0 JUMP costs 11 gas per iteration. Under FocilProfile2MaxVerifyGas = 2**18 that is about 23,800 iterations.
  • Bogota inherits Amsterdam's 64 KiB MaxCodeSize, which is under MAX_REPLAY_CODE_BYTES. That is about 1.5 GB of Keccak per replay, roughly 2–3 s.
  • The per-IL budget caps replays at about 4 such candidates per position, ×16 positions. That is minutes of hashing per block, all inside AreEligible's lock, synchronously in BranchProcessor and NewPayloadHandler.
  • The includer pays nothing. The prefix just runs out of gas, the candidate is ineligible, and it never lands on chain. Many senders can share one 64 KiB body through 7702 delegation, so the deploy happens once.
  • Before this commit the tracer did no hashing here, so this commit introduces the cost.

Two fixes, and both together are cheap:

  1. Remember the CodeInfo instances already counted (HashSet<CodeInfo>(ReferenceEqualityComparer.Instance)) and return before hashing on a repeat. (CodeInfo overrides Equals, so the reference comparer matters.)
  2. Use env.CodeInfo.CodeHash when it is stamped (!= default), and fall back to ValueKeccak.Compute only for initcode. The EXTCODESIZE/EXTCODECOPY path already has codeHash from state.GetCodeHash(target), so it can pass that in rather than rehashing the body it just loaded.

Please add a regression test: a self-verify prefix whose code is a JUMPDEST PUSH0 JUMP loop on a large body. Assert the hash count (for example through a small seam) or the elapsed time, plus a case that re-enters the same body through a self-CALL.

Fix this →

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.

Fixed in 9c9a98a: bodies counted only on entry into new code, deduped by instance before hashing, keyed on the known code hash; a loop to pc 0 or self-call never rehashes. Covered by Simulate_ReplayCodeLimits_HashEachBodyOnce. EELS had the same issue, fixed in execution-specs#3777 (5969c74bd).

A loop jumping back to offset 0 rehashed the whole body on every iteration. Bodies are now counted when the executing code changes, deduplicated by instance first, and keyed on the stamped or already-known code hash.
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 51s —— View job


PR review

  • Gather context: one new commit since the last review (9c9a98a7, "count a replay code body on entry only, without rehashing")
  • Review the FrameTxValidationTracer change and the new regression test
  • Post findings

Verdict: mergeable. The High finding from the last review (a loop back to pc 0 re-hashed the whole code body on every iteration) is fixed. I found nothing new at Critical, High or Medium.

Last review's High: fixed

  • Change detection replaces pc == 0. The tracer now enters the counting branch only when env.CodeInfo differs by reference from _executingCode. A JUMP back to offset 0 inside the same body costs one reference comparison.
  • Instances are deduplicated before any hashing. _enteredCode uses ReferenceEqualityComparer, which matters because CodeInfo overrides Equals. The pc == 0 condition is still required when the code changes, so returning to a caller at pc != 0 updates _executingCode without counting the caller again. That is correct, because the caller was counted when it was entered.
  • The stamped hash is used. CodeInfo.CodeHash is stamped whenever an ICodeCache stores the instance. The replayer's IReadOnlyTxProcessingEnvFactory scope uses the normal cache; NoopCodeCache appears only in the witness envs. So deployed code is never hashed, and only initcode falls back to ValueKeccak. Initcode costs at least one CREATE each time, so the gas bounds that hashing.
  • Re-entry through a self-call. If the code cache returns the same instance, nothing happens. If it returns a new instance, the stamped hash makes _codeBodies.Add a no-op. Either way there is no rehash, and the body is not counted twice.
  • EXTCODESIZE/EXTCODECOPY. The hash already read through GetCodeHash is reused, and _codeBodies.Contains short-circuits before GetCode on a repeat read. The byte count still uses the designator length for a 7702-delegated target, as before.
  • Semantics are unchanged. Empty code still counts as nothing, the two bounds are still checked with >, and the mempool defaults (0/0) still leave both sets null.
  • Regression test. Simulate_ReplayCodeLimits_HashEachBodyOnce covers a JUMPDEST PUSH0 JUMP loop and a self-STATICCALL loop on a 48 KiB body, and asserts at most one hash. It covers both paths I asked for.

Low (optional)

  • CodeHashComputations is a counter only the tests read, kept on the production path (inline). This is the same pattern as StateReconstructions, which you removed in 478be65. The cost is one increment on the initcode path only, so this doesn't block merge.
  • HashEachBodyOnce catches OperationCanceledException when (tracer.Violated) and then asserts Violated is false, so that catch can never let the test pass. It is harmless. You could drop the try, or keep it as a guard so that a violation shows up as a clear assertion failure.

Still acknowledged in the PR body

  • The tests-focil-devnet@v0.2.0 engine fixtures are rejected for missing membership. That is correct under execution-apis#911, and they are deliberately left out of KNOWN_FAILURES.

I only read the code and didn't build or run tests. I reviewed cfe5bf17..9c9a98a7.

private bool _codeReadPending;

/// <summary>Bodies hashed because their code carried no stamped hash, which only initcode should need.</summary>
internal int CodeHashComputations { get; private 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.

Low (optional): CodeHashComputations is a counter only the tests read, kept on the production path. This is the same pattern as StateReconstructions, which was removed in 478be65. The cost is one increment, and only on the unstamped (initcode) path, so this doesn't block. If you want the class free of test-only state, the test could assert on elapsed time or a hashing seam instead.

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.

Kept the counter: the code-entry path reads no state and ValueKeccak/CodeInfo are static/sealed, so there is no seam a test double could observe, and a timing assert would be flaky. Dropped the unreachable catch in 6c74323.

…nclusion checks

Brings Profile 2 and the Bogota engine API to ethereum/EIPs#12394 and #12396,
ethereum/execution-apis#911, ethereum/consensus-specs#5698 and
ethereum/execution-specs#3777.

- engine_newPayloadV6 and engine_newPayloadWithWitnessV6 take one inclusion
  list per committee position and omissionClaims {transactionHash,
  evaluationIndex}, replacing the flat list and its membership bitvectors.
  Each list is checked against its byte cap before deduplication; malformed
  lists or claims answer -32602.
- Inclusion is checked apart from execution. A VALID payload is answered at
  once with inclusionCheckId and the check's verdict, null while it is
  pending; engine_getInclusionListVerdictV1 polls it and answers -38007 for an
  unknown check. A check that cannot be decided is forgotten, not completed.
- engine_forkchoiceUpdatedV5 takes PayloadAttributesV5.inclusionLists and
  returns PayloadStatusV1. The retained-list answers and the queued-copy
  tracking kept for per-call lists are removed.
- engine_getPayloadV7 returns omissionClaims; the builder fills each list's
  VERIFY budget separately.
- The VERIFY fill runs per committee position over the separate lists.
- The pipeline records the gas used before each transaction whenever
  Profile 2 is active, for claims judged after execution.
- MAX_INCLUSION_LIST_CLAIMS is renamed MAX_OMISSION_CLAIMS.
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 1m 59s —— View job


PR review

  • Gather context: commits since the last review (6c74323d, the master merge 72cbe904, and b086f5a9)
  • Review the deferred inclusion check (InclusionChecks, InclusionCheckId, engine_getInclusionListVerdictV1)
  • Review separated inclusion lists / omission claims and the builder side
  • Post findings

Verdict: mergeable. b086f5a9 replaces the retained-list design with execution-apis#911's deferred, ID-keyed check. That design is simpler, and I found nothing at Critical, High or Medium. Two optional Lows are below.

Deferred inclusion check: checked and fine

  • Execution validity is never held for the IL. NewPayloadWithInclusionCheck schedules a check only on VALID, when both inputs are present, and ValidateInitialParams guarantees both are present under 7805. The canonical-behind-head shortcut also answers VALID without an IL arm now, so the old SYNCING-on-resend case is gone.
  • InclusionChecks behaves as the spec describes:
    • Start is idempotent per ID.
    • TryGetVerdict never starts a check.
    • A completed verdict is never mutated.
    • The oldest of 256 checks is evicted.
    • An undecidable or faulted check is removed, and only if it is still the entry for its ID (the ReferenceEquals guard), so the CL can resubmit.
    • A check that is evicted while still pending completes on an orphaned object and does no harm.
    • Writing Verdict outside the lock is benign: a reader that runs early just sees "pending".
  • EvaluateAsync reads the right state and gas:
    • It waits for the executed copy to leave the queue before reading state, because VALID is answered before commit.
    • It stamps gas dimensions and per-tx gas onto the freshly rebuilt block, not the tree's shared header.
    • It returns null instead of guessing when state is missing. The existing two-bound fallback still covers a cache miss.
  • InclusionCheckId: the streaming KeccakRlpWriter lengths match the encoder (LengthOfKeccakRlp, LengthOf(byte[]), LengthOf(ulong)). Matches_the_rlp_the_engine_api_defines checks it against an independently built Rlp.Encode of the same structure.
  • Separate lists:
    • InclusionLists.Validate covers exactly 16 lists, no empty entry, and the 8 KiB cap before dedup. It runs on newPayload (-32602) and on FCU attributes (-38003).
    • DecodeLists shares one Transaction per hash, so sender recovery and signature checks run once per envelope.
    • InFillOrder deduplicates each list and sorts it by hash, which gives the (hash, envelope) fill order. A hash collision is the only exception, as the remark notes.
    • Distinct keeps first-occurrence order for ilByHash, so the "first occurrence answers for every copy" rule is unchanged.
  • Builder: InclusionListTxSource keys its cache on the attributes' byte[][][], fills each position on its own, and IsEmpty treats 16 empty lists as no IL.
  • Claims: OmissionClaim requires both fields, a null hash is -32602, and the 4096 cap matches MAX_OMISSION_CLAIMS. ClaimedIndices semantics are unchanged; only the type was renamed.

Low (optional)

  • The pipeline's IL verdict path is now dead in production (inline). Block.InclusionListTransactions is no longer set by the engine API, so these are reachable only from tests:

    • InclusionListSatisfactionChecker's judging half and the flat IsSatisfied overloads;
    • Block.IsInclusionListSatisfied and ProcessingResult.InclusionListUnsatisfied;
    • NotifyInclusionListUnsatisfied;
    • the IL handling in the prewarmer, in RecoverSignatures and in BlockchainProcessor.

    AGENTS.md asks for code a change makes unused to be removed. A follow-up PR is fine, but please note it in the PR body.

  • Checks are not throttled. Each new check ID gets its own Task.Run, and evicted checks keep running. Replays are serialised by the replayer's lock, but decoding, sender recovery and the budget fill all run concurrently. The CL is trusted and the work per check is bounded, so this doesn't block merge. A small SemaphoreSlim would cap it if CLs ever send many IL variants per slot.

Still acknowledged in the PR body

  • The focilTest CI job (tests-focil-devnet@v0.3.0) will fail until those fixtures are refilled for execution-apis#911.

I only read the code and didn't build it or run the tests. I reviewed 6c74323d..b086f5a9, skipping the master merge. "All 96 engine fixtures pass" is your result, not one I checked.


// P2P-decoded blocks legitimately have null IL; IsSatisfied treats null as "not applicable".
return InclusionListValidator.IsSatisfied(processedBlock, suggestedBlock.InclusionListTransactions, worldState, spec, txValidator);
if (suggestedBlock.InclusionListTransactions is null) return true;

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.

Low: production code no longer reaches anything after this line. ExecutionPayload no longer sets Block.InclusionListTransactions; only the Block constructor and test builders do. So outside tests this method always returns true here, and nothing downstream ever fires:

  • the flat InclusionListValidator.IsSatisfied(Block, Transaction[]?, …) overloads;
  • Block.IsInclusionListSatisfied;
  • ProcessingResult.InclusionListUnsatisfied and NotifyInclusionListUnsatisfied in BlockchainProcessor;
  • the IL arm in NewPayloadHandler's verdict mapping (L882/L913);
  • the IL sender warm-up in BlockCachePreWarmer, the IL recovery in RecoverSignatures, and the BlockRef guard in BlockchainProcessor.

The checker now only records GasUsedBeforeTransactions. AGENTS.md says to remove code a change makes unused. That can be a follow-up so this diff doesn't grow, but please say so in the PR body. Otherwise a reader will assume the pipeline still reaches a verdict.

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.

Removed in 7603394: the flat-list checker and everything only it reached are gone.

…drop the flat-list check

- Remove the pipeline's flat inclusion-list check, dead since inclusion is
  checked by identifier: Block.InclusionListTransactions and
  IsInclusionListSatisfied, InclusionListSatisfactionChecker,
  ProcessingResult.InclusionListUnsatisfied, the flat IsSatisfied overloads,
  and the inclusion-list handling in BlockchainProcessor, RecoverSignatures
  and the prewarmers. BranchProcessor records the gas used before each
  transaction itself, once Profile 2 is active.
- engine_getInclusionListV1 lists ready Profile 2 candidate frame
  transactions once Profile 2 is active, each drawn as a run of its own,
  keeping their total VERIFY cost within MAX_VERIFY_GAS_PER_IL and leaving
  out candidates already expired at the parent.
- InclusionChecks evicts a check for a block at or below the finalized one
  first, then a pending check, and a completed verdict only when nothing
  else is left; never the check being added. At most four checks run at
  once, and an evicted pending check is cancelled.
- An omission claim's evaluationIndex must be a canonical QUANTITY
  whatever JsonRpc.StrictHexFormat is set to; either hex case is accepted.
@Marchhill

Copy link
Copy Markdown
Contributor Author

Removed the dead flat-list pipeline check and throttled/cancelled inclusion checks (both Lows from the last review); engine_getInclusionListV1 now lists Profile 2 candidate frame txs within the per-IL VERIFY budget; completed verdicts are kept until finalized; evaluationIndex must be a canonical QUANTITY. All 96 FOCIL/frame engine fixtures pass.

@claude review

@claude

claude Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 1m 32s —— View job


PR review

  • Gather context: one new commit since the last review (7603394c)
  • Review removal of the flat-list pipeline check
  • Review InclusionChecks eviction, throttling and cancellation
  • Review Profile 2 candidates in engine_getInclusionListV1
  • Review CanonicalQuantityConverter
  • Post findings

Verdict: mergeable. Both Lows from the last review are fixed. I found nothing at Critical, High or Medium. There is one new Low, in eviction.

Last review's findings: resolved

  • The dead flat-list pipeline check is removed. I grepped the tree for InclusionListUnsatisfied, Block.InclusionListTransactions, IInclusionListSatisfactionChecker and NotifyInclusionListUnsatisfied. None are left, apart from NewPayloadHandler's private IsInclusionListSatisfied helper, which is a different thing.
    • BranchProcessor now records GasUsedBeforeTransactions itself, under the same NoValidation and IsActive gates the checker used.
    • The verdict mapping in NewPayloadHandler is back to Success-only.
    • The single remaining IsSatisfied overload always gets the committee lists, so Profile2Inputs.InclusionLists is now correctly non-nullable.
  • Checks are throttled and cancelled.
    • _running allows 4 checks at a time.
    • entered makes sure the semaphore is released only by a check that actually acquired it.
    • An evicted check is cancelled through a token linked to _disposed, so it stops whether it is still waiting for a slot or already evaluating.
    • The when (cancellation.IsCancellationRequested) filter covers both cancellation sources.
    • Removing a cancelled check is still guarded by ReferenceEquals, so a newer check with the same ID is never removed.

New since last review: checked and fine

  • Retaining unfinalized verdicts. The eviction order is: a check at or below the finalized block, then the oldest pending check other than added, then the oldest check. This matches the PR body. ulong <= ulong? is false when nothing is finalized, so with no finalized block the first rule never fires. The finalized number is read only when the store is near capacity, and outside the lock. The Count read there is racy, but a wrong read only changes which entry is evicted, so it is harmless.

  • Profile 2 candidates in engine_getInclusionListV1 (InclusionListBuilder):

    • Each candidate is its own reservoir entry. Blob transactions are excluded by Classify.
    • Readiness uses the keyed nonce set or the account nonce at head, the same state AppendableRun uses.
    • The expiry cut-off deadline <= parentTimestamp is conservative under either reading of the deadline, because the target timestamp is always after the parent's.
    • In EncodeTransactionsUpToLimit, the budget is debited only when an entry is actually listed. The total stays within MAX_VERIFY_GAS_PER_IL, so the attester's fill admits every listed candidate, whatever order it processes them in.
    • A frame candidate that doesn't fit is skipped without putting its sender in droppedSenders, which is right, because it is a run of one.
    • Scanning GetPendingTransactions() is a whole-pool pass, but it runs once per slot and only on IL committee duty, so it is off the hot path.
  • CanonicalQuantityConverter rejects:

    • non-string tokens;
    • a missing or uppercase 0X prefix;
    • empty digits;
    • leading zeros (0x0 is still accepted);
    • more than 16 digits;
    • whitespace, because AllowHexSpecifier alone doesn't accept it.

    It accepts either case of hex digit. Its namespace matches the other converters in Nethermind.Core/JsonConverters.

  • Wiring. InclusionChecks gets IBlockFinder through the existing Bind<IBlockFinder, IBlockTree>(). GetInclusionListTransactionsHandler passes FocilProfile2MaxVerifyGas through to the builder.

Low

  • A newly added check whose block is at or below the finalized one can evict itself (inline, InclusionChecks.cs:106).
    • The finalized rule doesn't exclude added, although the remark and the commit message say the added check is never evicted.
    • With a full store, newPayloadV6 for an already-finalized block returns an ID that polling immediately reports as -38007, and each resubmission does the same.
    • A verdict on a finalized block has little value, so this doesn't block merge. The fix is one condition, plus a test case.

Still acknowledged in the PR body

  • The focilTest CI job (tests-focil-devnet@v0.3.0) will fail until those fixtures are refilled for execution-apis#911.

I only read the code; I didn't build it or run tests. I reviewed b086f5a9..7603394c. "All 96 fixtures pass" is your result, not one I checked.

Check? oldestPending = null;
foreach (Check check in _order)
{
if (check.BlockNumber <= finalizedNumber) return check;

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.

Low: the finalized rule can evict the check that is being added. The remark says added is "never the one evicted", and the commit message says "never the check being added". The pending branch excludes added, but this line doesn't.

How it happens:

  • The store is full (256 checks).
  • No older check is at or below the finalized block.
  • The CL calls newPayloadV6 for a block at or below the finalized one, for example after a restart or a resend.

Then added is the first match here. It is removed, and its Evicted token is cancelled. newPayloadV6 still returns its inclusionCheckId, but polling gets -38007 straight away. The CL resubmits, and the same eviction happens on every resubmission for as long as the store stays full. A verdict for a finalized block is not worth much, so this doesn't block merge. Still, the code doesn't do what the doc says.

Suggested change
if (check.BlockNumber <= finalizedNumber) return check;
if (check.BlockNumber <= finalizedNumber && !ReferenceEquals(check, added)) return check;

Please add a case to Eviction_keeps_unfinalized_verdicts where the new check's block is at or below the finalized one.

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.

Fixed in 34cd32b: added is skipped before every eviction rule. Regression case added to Eviction_keeps_unfinalized_verdicts (added check at the finalized block); it fails without the fix.

@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 20s —— View job


PR review

  • Gather context: one new commit since the last review (34cd32ba, "never evict the inclusion check being added under the finalized rule")
  • Review the InclusionChecks.EvictionCandidate change and the new test case
  • Post findings

Verdict: mergeable. The Low from the last review is fixed. I found nothing at Critical, High or Medium.

Last review's finding: resolved

A newly added check could evict itself under the finalized rule.

  • The fix. EvictionCandidate now skips added before it applies any rule (InclusionChecks.cs:106), so the remark "never the one evicted" is now true for all three rules. The pending rule no longer needs its own ReferenceEquals guard, and the fix removes it.
  • The _order.First!.Value fallback can't return added. Eviction only runs when Count > Capacity, so the store holds at least 257 entries. added is the last of them, appended by AddLast.
  • The new test case exercises the bug. With (0UL, 0UL, "pending") the store already holds blocks 1, 3 and 2, so none of them is at or below finalized block 0. Only added, at block 0, matches the finalized rule. Without the fix it would remove itself, and the assertion TryGetVerdict(KeccakA) is True would fail. With the fix the pending check at block 3 is evicted, which matches the documented order.
  • The assertion isn't racy. For block 0 the evaluator returns true, and only undecided (null) verdicts are removed, so added stays in the store whether or not its run has finished by the time the test polls.

Still acknowledged in the PR body

  • The focilTest CI job (tests-focil-devnet@v0.3.0) will fail until those fixtures are refilled for execution-apis#911.

I only read the code and didn't build or run tests. I reviewed 7603394c..34cd32ba.

…ofile-2-focil-eligibility

# Conflicts:
#	src/Nethermind/Nethermind.AuRa.Test/AuraBlockProcessorTests.cs
#	src/Nethermind/Nethermind.Blockchain.Test/BlockProcessorTests.cs
#	src/Nethermind/Nethermind.Blockchain.Test/ReorgTests.cs
#	src/Nethermind/Nethermind.Blockchain.Test/Validators/InclusionListValidatorTests.cs
#	src/Nethermind/Nethermind.Consensus.Test/BlockCachePreWarmerTests.cs
#	src/Nethermind/Nethermind.Consensus.Test/Processing/BranchProcessorVerdictTests.cs
#	src/Nethermind/Nethermind.Consensus/Processing/BlockCachePreWarmer.cs
#	src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockProductionTransactionsExecutor.cs
#	src/Nethermind/Nethermind.Consensus/Processing/BranchProcessor.cs
#	src/Nethermind/Nethermind.Consensus/Processing/IInclusionListSatisfactionChecker.cs
#	src/Nethermind/Nethermind.Consensus/Processing/InclusionListSatisfactionChecker.cs
#	src/Nethermind/Nethermind.Consensus/Validators/InclusionListValidator.cs
#	src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V7.cs
#	src/Nethermind/Nethermind.Merge.Plugin.Test/NewPayloadHandlerRaceConditionTests.cs
#	src/Nethermind/Nethermind.Merge.Plugin.Test/SszRest/SszCodecTests.cs
#	src/Nethermind/Nethermind.Merge.Plugin.Test/SszRest/SszMiddlewareTests.cs
#	src/Nethermind/Nethermind.Merge.Plugin/Data/ExecutionPayloadV3.cs
#	src/Nethermind/Nethermind.Merge.Plugin/Data/GetPayloadV6Result.cs
#	src/Nethermind/Nethermind.Merge.Plugin/EngineRpcModule.cs
#	src/Nethermind/Nethermind.Merge.Plugin/Handlers/NewPayloadHandler.cs
#	src/Nethermind/Nethermind.Merge.Plugin/SszRest/SszCodec.cs
…ofile-2-focil-eligibility

# Conflicts:
#	src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V7.cs
#	src/Nethermind/Nethermind.Merge.Plugin.Test/NewPayloadHandlerRaceConditionTests.cs
#	src/Nethermind/Nethermind.Merge.Plugin/Handlers/NewPayloadHandler.cs
…ofile-2-focil-eligibility

# Conflicts:
#	src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V7.cs
#	src/Nethermind/Nethermind.Specs/SpecNameParser.cs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configuration devops eip evm taiko related to the taiko alethia rollup xdc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants