Repository navigation
Conversation
…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>
|
Claude finished @Marchhill's task in 1m 43s —— View job PR review
Verdict: not mergeable yet. There is one High finding. The new code-body counting in High
Builder claims only where the candidate is invalid (
|
| if (pc == 0 && _codeBodies is not null) | ||
| { | ||
| LoadCode(env.CodeInfo.CodeSpan); | ||
| if (Violated) return; | ||
| } |
There was a problem hiding this comment.
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 JUMPcosts 11 gas per iteration. UnderFocilProfile2MaxVerifyGas = 2**18that is about 23,800 iterations. - Bogota inherits Amsterdam's 64 KiB
MaxCodeSize, which is underMAX_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 inBranchProcessorandNewPayloadHandler. - 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:
- Remember the
CodeInfoinstances already counted (HashSet<CodeInfo>(ReferenceEqualityComparer.Instance)) and return before hashing on a repeat. (CodeInfooverridesEquals, so the reference comparer matters.) - Use
env.CodeInfo.CodeHashwhen it is stamped (!= default), and fall back toValueKeccak.Computeonly for initcode. TheEXTCODESIZE/EXTCODECOPYpath already hascodeHashfromstate.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.
There was a problem hiding this comment.
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.
|
@claude review |
|
Claude finished @Marchhill's task in 51s —— View job PR review
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
Low (optional)
Still acknowledged in the PR body
I only read the code and didn't build or run tests. I reviewed |
| 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; } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@claude review |
|
Claude finished @Marchhill's task in 1m 59s —— View job PR review
Verdict: mergeable. Deferred inclusion check: checked and fine
Low (optional)
Still acknowledged in the PR body
I only read the code and didn't build it or run the tests. I reviewed |
|
|
||
| // 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; |
There was a problem hiding this comment.
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.InclusionListUnsatisfiedandNotifyInclusionListUnsatisfiedinBlockchainProcessor;- the IL arm in
NewPayloadHandler's verdict mapping (L882/L913); - the IL sender warm-up in
BlockCachePreWarmer, the IL recovery inRecoverSignatures, and theBlockRefguard inBlockchainProcessor.
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.
There was a problem hiding this comment.
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.
|
Removed the dead flat-list pipeline check and throttled/cancelled inclusion checks (both Lows from the last review); @claude review |
|
Claude finished @Marchhill's task in 1m 32s —— View job PR review
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
New since last review: checked and fine
Low
Still acknowledged in the PR body
I only read the code; I didn't build it or run tests. I reviewed |
| Check? oldestPending = null; | ||
| foreach (Check check in _order) | ||
| { | ||
| if (check.BlockNumber <= finalizedNumber) return check; |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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.
|
@claude review |
|
Claude finished @Marchhill's task in 20s —— View job PR review
Verdict: mergeable. The Low from the last review is fixed. I found nothing at Critical, High or Medium. Last review's finding: resolvedA newly added check could evict itself under the finalized rule.
Still acknowledged in the PR body
I only read the code and didn't build or run tests. I reviewed |
…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
…ck by its prototype labels
…ofile-2-focil-eligibility # Conflicts: # src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V7.cs # src/Nethermind/Nethermind.Specs/SpecNameParser.cs
Changes
InclusionListValidatorexcused 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
Eip8369Profile2.Classify): no blobs; a declared prefix of an optionalexpiry_verify, then an optionalrecent_root_verify, thenself_verify,deploy | self_verify,only_verify | payordeploy | only_verify | pay; noATOMIC_BATCH_FLAGin the prefix; no VERIFY frame after it;verify_cost ≤ MAX_VERIFY_GAS_PER_TX, counting the signature cost and the prefix's execution limits.(hash, envelope)order fromMAX_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.min(claim, n)for an omitted candidate whose hash is claimed exactly once, otherwisen. Each candidate is replayed once, at that index alone.Profile2EligibilityReplayer): parent state plus the EIP-7928 changes withblock_access_index ≤ k, underMAX_VERIFY_GAS_PER_TX, the AA-VOPS access rules and the replay code bounds. The deploy frame's factory must be an undelegated contract.kuses the EIP-8037 execution and state gas used beforek, recorded from the execution receipts.Engine API (execution-apis#911)
engine_newPayloadV6andengine_newPayloadWithWitnessV6takeinclusionLists(exactly 16 lists, each within 8 KiB before deduplication, no empty entry) andomissionClaims({transactionHash, evaluationIndex}, at most 4096, the index a canonicalQUANTITYwhateverJsonRpc.StrictHexFormatis). Malformed inputs answer-32602, after the-38005fork check.VALID, newPayloadV6 schedules the check and answers at once withinclusionCheckId(keccak256("engine/inclusion-check/v1" || RLP([blockHash, inclusionLists, claims]))) andinclusionListSatisfied, which isnulluntil 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, andBranchProcessorrecords the per-transaction gas the check needs.engine_getInclusionListVerdictV1(inclusionCheckId)returns the verdict,nullwhile 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_forkchoiceUpdatedV5takesPayloadAttributesV5.inclusionLists(-38003when malformed) and returnsPayloadStatusV1; 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_getPayloadV7returnsomissionClaims. The builder claims the payload length at the first failed attempt to append an admitted candidate, only when the candidate is invalid there.engine_getInclusionListV1also lists ready Profile 2 candidate frame transactions once Profile 2 is active, keeping their total VERIFY cost withinMAX_VERIFY_GAS_PER_ILand leaving out candidates already expired at the parent.Constants
MAX_VERIFY_GAS_PER_IL = 2**20,MAX_VERIFY_GAS_PER_TX = 2**18(alsoBlocks.FocilProfile2MaxVerifyGas, where0lifts 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?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
Eip8369Profile2Tests,InclusionListValidatorTests,Profile2EligibilityReplayerTests,FrameTxValidationPrefixSimulationTests,TransactionsExecutorTests,InclusionListTxSourceTests,PayloadAttributes*Tests,InclusionCheckIdTests,InclusionChecksTests,EngineModuleTests(V7 and Eip8369),SszCodecTestsandSszMiddlewareTests.tests/bogota/eip8369_focil_framesandtests/bogota/eip7805_focil) pass withnethtest -e --forkAlias Bogota=Eip8369Prototype; the runner pollsengine_getInclusionListVerdictV1for eachVALIDpayload.Eip8369Prototypeis a test-only fork: Bogota with EIP-8141, EIP-8250 and EIP-8272.focilTestCI job still runstests-focil-devnet@v0.3.0, whoseengine_newPayloadV6carries a flat list, and fails until that release is refilled against execution-apis#911.Documentation
Requires documentation update
Requires explanation in Release Notes