Skip to content

Merge master into eip8141-frame-txs-devnet7 - #12802

Merged
AnkushinDaniil merged 56 commits into
eip8141-frame-txs-devnet7from
daniil/merge-master-into-devnet7
Aug 13, 2026
Merged

Merge master into eip8141-frame-txs-devnet7#12802
AnkushinDaniil merged 56 commits into
eip8141-frame-txs-devnet7from
daniil/merge-master-into-devnet7

Conversation

@AnkushinDaniil

Copy link
Copy Markdown
Contributor

Forward-ports current master (54 commits) into the frame-tx devnet base.

This advances the shared devnet base so it stops trailing master and picks up the repo-wide fixes (SSH.NET advisory pin, EIP-8037 execution-gas rename, BAL primary-ctor refactor). It is a base bump, not a behaviour change to the frame-tx feature set.

Conflicts resolved by history, not mechanically:

  • TxPool.cs: took master. The _transactionSnapshot cache the base carried came from perf(engine): overlap the newPayload transactions-root computation with the serial prefix #12515 and master has since reverted it; keeping it would not compile and its concurrency rationale no longer applies to master's non-caching GetPendingTransactions.
  • BlockProcessorTests.cs: took master. BlockAccessListManager is now a primary constructor taking BalTxProcessorFactory; the old call is dead.
  • GethLikeCallTracerTests.cs: kept both sides (the frame EIP-7708 empty-callstack test and master's Amsterdam two-dimensional gas tests) and moved the frame test onto the new three-argument NativeCallTracer(tx, spec, options) ctor.

Post-merge adaptation: frame-tx processor now calls Eip8037BlockGasInclusionCheck.CalculateBlockExecutionGas (renamed from CalculateBlockRegularGas in #12600, identical body). Three unused usings surfaced by IDE0005 dropped so lint stays green.

Supersedes #12800 (that PR only pinned SSH.NET and dropped the same usings; both are included here).

Verified locally on the merge tip before pushing:

  • full Nethermind.slnx build: 0 warnings, 0 errors
  • Nethermind.TxPool.Test: 718 passed, 0 failed
  • Nethermind.Evm.Test (GethLikeCallTracer + FrameTx): 131 passed, 0 failed
  • Nethermind.Blockchain.Test (BlockProcessor + FrameTx): 67 passed, 0 failed

kamilchodola and others added 30 commits August 3, 2026 09:30
…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.
Co-authored-by: rubo <rubo@users.noreply.github.com>
* 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
* 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
Update Dockerfiles

Co-authored-by: rubo <rubo@users.noreply.github.com>
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.
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

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
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.
* 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
…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.
… 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(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>
…2635)

* 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>
…ut (#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(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.
…lock (#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>
…ns (#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)
…er) (#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>
…V1 (#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

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>
…or 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

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

* test(jsonrpc): pin the whole-byte leading-zero root case
…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
* 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
…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 pass, which would reject every well-formed response.

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

---------

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

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AnkushinDaniil's task in 9m 33s —— View job


Review: merge of master into eip8141-frame-txs-devnet7

  • Gather context — isolated the merge commit's real content from the 54 inherited master commits
  • Audit conflict resolutions (TxPool.cs, BlockProcessorTests.cs, GethLikeCallTracerTests.cs)
  • Audit post-merge adaptations (EIP-8037 rename, dropped usings)
  • Evil-merge detection: 22 files differ from both parents; checked each for silently-dropped devnet code
  • Sweep for devnet call sites broken by master's API tightening
  • Verify build/tests locallydotnet build is not in this job's allowed tools, so I could not reproduce the author's build/test run. Findings below are from source reading, and each is traced to the exact method bodies.

What I verified as correct

The three documented conflict resolutions are right, and two of them are better than the PR body claims:

File PR body says Actually
TxPool.cs took master ✅ Correct. Master genuinely has no _transactionSnapshot (removed by #12685); all EIP-8141 logic — _expiringFrameTxCount, RemoveExpiredFrameTransactions, the frame filters, blob-pool Inserted/Removed wiring and its Dispose unsubscribe, ContainsTx dual-pool lookup — survives intact, and master's TryRecoverAddress reorg fix (#12757) is picked up.
BlockProcessorTests.cs took master ✅ Result is better than described — it kept both sides. Installs_predeploy_once_and_captures_it_in_bal (EIP-8141/8250) is still there, re-pointed at the new BalTxProcessorFactory primary ctor, alongside master's new parallel-validation test.
GethLikeCallTracerTests.cs kept both ✅ Correct, and the NativeCallTracer(tx, spec, options) migration is complete — the only production construction site (GethLikeNativeTracerFactory.cs:33) already passes releaseSpec.

Also checked and clean:

  • TraceSimpleTransferActionStart ordering in TransactionProcessor.cs — the devnet side carried the pre-Fix EIP-7708 tracing with logs #12577 placement (log before action-start, with a now-stale "keep tracer event order aligned" comment). The merge correctly took master's Fix EIP-7708 tracing with logs #12577 fix that hoists the action-start above ReportLog. Dropping the devnet comment is right, not a loss.
  • EIP-8037 rename — all 5 CalculateBlockRegularGasCalculateBlockExecutionGas call sites migrated, including the frame-tx one. No stale references to CumulativeRegularGasUsed, TryConsumeStateAndRegularGas, or RegularDimensionExceeded remain.
  • GasConsumed gained a 6th GasRefund member as a defaulted positional parameter, so the devnet's existing 5-arg constructions still bind.
  • Infra (Directory.Packages.props, .slnx, .github/, scripts/, nix/, Dockerfiles, AGENTS.md) is byte-identical to the master parent — no accidental devnet-side infra edits. The SSH.NET pin arrives via master's chore: update .NET packages #12791, so Green devnet7 CI: pin SSH.NET past the advisory, drop unused usings #12800 is indeed subsumed.
  • The one packages.lock.json delta vs master (Nethermind.Evm.Precompiles under Nethermind.TxPool) is the devnet's own dependency, correctly retained.
  • The three dropped usings are genuinely unused after the merge.

Findings

🔴 High — RlpReader.DecodeAddress() is no longer null-tolerant, and six frame-tx call sites still use it

Master's #12579 (bbe3b7e, Reject invalid fixed-size header RLP) changed the contract:

// before (devnet parent a9e547c)
public Address? DecodeAddress()                       // 0x80 → null

// after (merge tip)
public Address  DecodeAddress()    { ReadAddressPrefix(allowNull: false); ... }   // 0x80 → throws RlpException
public Address? DecodeAddressOrNull() { ... }                                     // 0x80 → null

ReadAddressPrefix(allowNull: false)TryReadFixedSizePrefix returns false for Rlp.EmptyByteArrayByte, and ThrowAddressDecodeException fires (RlpReader.cs:1037-1064).

Master migrated its own null-tolerant readers to DecodeAddressOrNull() — that is the bulk of the receipt-decoder churn in this PR. The six call sites the devnet branch added were not migrated, and there is no compile error, because Address? x = <Address> is a legal (merely redundant) assignment.

1a. TxFrameDecoder.cs:29 — a frame with an empty target can no longer be decoded

Address? target = decoderContext.DecodeAddress();

The file's own XML doc states the contract this breaks:

"An empty target byte string decodes to null (resolves to the transaction sender)."

TxFrame.Target is Address? and the encoder writes writer.Encode(item.Target) unconditionally. So a frame tx containing a sender-targeted frame now round-trips as: encode → 0x80 → decode → RlpException. Any such transaction arriving over devp2p or inside a block body is rejected as malformed rather than executed. Failure scenario: peer sends a well-formed type-6 tx whose first frame has target = b''; TxFrameDecoder.DecodeInternal throws, the tx/block is dropped.

1b. TxFrameSignatureDecoder.cs:31 — a frame signature with an empty signer can no longer be decoded

Address? signer = decoderContext.DecodeAddress();

TxFrameSignature.Signer is Address?, and FrameTxValidation.cs:389,392 explicitly branches on signature.Signer is null when counting intrinsic calldata tokens — so a null signer is a deliberately supported decoded state, not a defensive nullable. Same encode/decode asymmetry, same RlpException.

Both should be DecodeAddressOrNull().

Neither site is touched by this PR's diff, which is exactly why it slipped: the breaking change is on master's side of the merge, in a method the devnet files call. I found no test covering an empty target or empty signer, so a green CI run does not clear this.

Fix this →

🟠 Medium — nullable TxReceipt.Payer decoded with the strict reader (3 sites)

ReceiptStorageDecoder.cs:101, CompactReceiptStorageDecoder.cs:71, ReceiptMessageDecoder.cs:109 all do txReceipt.Payer = ctx.DecodeAddress();. Payer is Address?; the encoders write it unconditionally (ReceiptStorageDecoder.cs:190 / :303, Rlp.LengthOf((Address?)null) == 1). BlockReceiptsTracer.cs:168 assigns Payer from _frameTxPayer, which is null unless the processor went through ReportFrameTxReceipt — so a null payer on a TxType.FrameTx receipt is reachable, and it produces a self-inflicted RlpException when the receipt is read back from the DB. Inline comment posted on the ReceiptMessageDecoder hunk.

Separated from the High finding only because the existing test FrameTxReceiptDecoderTests always sets Payer = TestItem.AddressA, so today's happy paths do populate it; 1a/1b have no such backstop.

🟡 Low — dead null-check in FrameTxDecoder.cs:46

transaction.SenderAddress = decoderContext.DecodeAddress() ?? ThrowMissingSender();

DecodeAddress() can no longer return null, so ThrowMissingSender() (line 178, "frame transaction sender must be a 20-byte address") is unreachable and a missing sender now surfaces as the generic "Unexpected RLP prefix…". Either switch to DecodeAddressOrNull() to keep the specific message, or delete ThrowMissingSender and rely on the strict reader deliberately.

🟡 Low — stale local name after the EIP-8037 rename

TransactionProcessorBase.FrameTx.cs:366: the comment above was updated to CalculateBlockExecutionGas, but the local is still ulong blockRegularGas. Cosmetic; worth one keystroke while the rename is fresh.

🟡 Low — PR description overstates two resolutions

BlockProcessorTests.cs is described as "took master" when it in fact kept both sides (good — the frame predeploy test survived). Worth correcting so a future reader auditing this merge doesn't go looking for a test that was never dropped.


Verdict

The merge mechanics are careful and the conflict resolutions are correct — including the two that reason about history (#12685's snapshot revert, #12577's tracer ordering) rather than resolving mechanically. The problem is the class of breakage a merge can't surface as a conflict: master tightened RlpReader.DecodeAddress()'s null contract and migrated its own callers, and the frame-tx callers that depend on the old null-tolerant behaviour were left behind with no compile error and no test coverage. Fixing the six sites is a small, mechanical change; the accompanying null-target / null-signer / null-payer round-trip tests are what stop it recurring on the next master merge.

2 findings need a fix before merge (1 High covering 2 call sites, 1 Medium covering 3); 3 Low are optional.
· branch daniil/merge-master-into-devnet7

@AnkushinDaniil
AnkushinDaniil marked this pull request as draft August 13, 2026 13:57
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (eip8141-frame-txs-devnet7@a9e547c). Learn more about missing BASE report.

Additional details and impacted files
@@                     Coverage Diff                      @@
##             eip8141-frame-txs-devnet7   #12802   +/-   ##
============================================================
  Coverage                             ?        0           
============================================================
  Files                                ?        0           
  Lines                                ?        0           
  Branches                             ?        0           
============================================================
  Hits                                 ?        0           
  Misses                               ?        0           
  Partials                             ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…rNull

Master #12579 split DecodeAddress into a non-null form that throws on 0x80
and a nullable DecodeAddressOrNull. The frame target and frame-signature
signer are optional, so the merge left them on the throwing overload.
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

EVM Opcode Benchmark Diff

Aggregated runs: base=3, pr=3
Noisy rerun opcodes: MULMOD

No significant regressions or improvements detected.

@AnkushinDaniil
AnkushinDaniil marked this pull request as ready for review August 13, 2026 15:57
@AnkushinDaniil
AnkushinDaniil merged commit 560dae3 into eip8141-frame-txs-devnet7 Aug 13, 2026
465 of 466 checks passed
@AnkushinDaniil
AnkushinDaniil deleted the daniil/merge-master-into-devnet7 branch August 13, 2026 15:57
@claude

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AnkushinDaniil's task in 3m 50s —— View job


Review: merge of master into eip8141-frame-txs-devnet7

  • Fetch the merged tree (560dae3) and re-read the affected sources
  • Re-verify the prior review's High finding (RlpReader.DecodeAddress() null contract)
  • Re-verify the Medium (TxReceipt.Payer) and Low findings
  • Sweep for other strict-vs-null-tolerant reader mismatches in devnet-only decoders
  • Build / run testsdotnet build is not in this job's allowed tools. Everything below is traced to exact lines in the merged tree.

Note: this PR is already merged (560dae3), so what follows is a re-verification plus follow-up items, not a merge gate.


Status of the earlier findings

🔴 High (TxFrameDecoder / TxFrameSignatureDecoder) — ✅ fixed before merge. Both sites now use the null-tolerant reader in the merged tree:

src/Nethermind/Nethermind.Serialization.Rlp/TxFrameDecoder.cs:29
    Address? target = decoderContext.DecodeAddressOrNull();
src/Nethermind/Nethermind.Serialization.Rlp/TxFrameSignatureDecoder.cs:31
    Address? signer = decoderContext.DecodeAddressOrNull();

That restores the documented contract ("an empty target byte string decodes to null") and the signature.Signer is null branch in FrameTxValidation.cs. Good catch-and-fix.

🟠 Medium (TxReceipt.Payer) — ❌ still present, all three sites unchanged:

File Line
src/Nethermind/Nethermind.Serialization.Rlp/ReceiptStorageDecoder.cs 101
src/Nethermind/Nethermind.Serialization.Rlp/CompactReceiptStorageDecoder.cs 71
src/Nethermind/Nethermind.Serialization.Rlp/ReceiptMessageDecoder.cs 109

All read txReceipt.Payer = ...DecodeAddress();. TxReceipt.Payer is Address? (TransactionReceipt.cs:94), and every encoder writes it unconditionally — writer.Encode(item.Payer) at ReceiptStorageDecoder.cs:190 and CompactReceiptStorageDecoder.cs:175, sized by Rlp.LengthOf(item.Payer) (which is 1, i.e. 0x80, for null). Post-merge DecodeAddress() is ReadAddressPrefix(allowNull: false)RlpException on 0x80 (RlpReader.cs:360-364, 1056-1063). So the encode/decode pair is asymmetric: the writer accepts null, the reader rejects what it wrote.

Reachability — narrower than I described in the earlier pass, and worth stating precisely. TransactionProcessorBase.FrameTx.cs:428-446 is the only place a FrameTx receipt is marked, and it calls ReportFrameTxReceipt(payer, …) immediately before MarkAsSuccess/MarkAsFailed; payer there is a non-nullable Address. I also checked that BlockReceiptsTracer is handed to the processor unwrapped in block processing (no CancellationTxTracer wrapping in Nethermind.Consensus), so tracer is IFrameTxReceiptTracer holds and _frameTxPayer is populated on every in-block path. No currently-reachable production failure — I was overreaching in the earlier comment when I called it reachable.

What remains is a latent round-trip hazard, and it's the kind that surfaces later rather than now: any future path that builds a FrameTx receipt through a tracer that doesn't implement IFrameTxReceiptTracer (a composite, a plugin tracer, a test double) writes a null payer to the DB and then throws on read-back — a self-inflicted RlpException on a receipt this node itself wrote. The fix is one word per site, so the cost/benefit is lopsided:

txReceipt.Payer = decoderContext.DecodeAddressOrNull();

Confirmed there is no test backstop: FrameTxReceiptDecoderTests.cs:204 always sets Payer = TestItem.AddressA, and :61 only asserts that regular receipts decode a null payer (they skip the frame block entirely). A null-payer FrameTx round-trip is untested in all three codecs.

🟡 Low — FrameTxDecoder.cs:46, dead null-check — still present:

transaction.SenderAddress = decoderContext.DecodeAddress() ?? ThrowMissingSender();

DecodeAddress() no longer returns null, so ThrowMissingSender() (:178, "frame transaction sender must be a 20-byte address") is unreachable and a missing sender now surfaces as the generic "Unexpected RLP prefix of 128…". Either switch to DecodeAddressOrNull() to keep the specific message, or drop ThrowMissingSender and keep the strict reader deliberately.

🟡 Low — TransactionProcessorBase.FrameTx.cs:365, stale local name — still present: the comment above and the callee both say CalculateBlockExecutionGas, the local is still ulong blockRegularGas. Cosmetic.


Fresh sweep this pass

  • Checked every devnet-only decoder for the other strict/null-tolerant pairs master introduced in Reject invalid fixed-size header RLP #12579DecodeKeccak() vs DecodeKeccakOrNull(), DecodeBloom() vs DecodeBloomOrNull(). No mismatches in TxFrameDecoder, TxFrameSignatureDecoder, FrameTxDecoder, or the three receipt decoders. DecodeAddress was the only tightened reader with stragglers.
  • DecodeFrameReceipts / EncodeFrameReceipts (ReceiptStorageDecoder.cs:198-245) are symmetric — status, gas, logs, all non-nullable. No equivalent hazard.

Verdict

The merge itself holds up: the conflict resolutions reason about history rather than resolving mechanically, and the High finding was fixed before merge. Two things are worth a small follow-up PR — the three Payer sites (latent, not currently reachable, one-word fix each) and the now-dead ThrowMissingSender. Both deserve the round-trip tests that would have caught them: a null-Payer FrameTx receipt through storage/compact/message codecs, and — since the TxFrameDecoder/TxFrameSignatureDecoder fix landed without them — a null-Target and null-Signer frame round-trip, so the next master merge can't silently re-break the same contract.

Fix this →
· branch daniil/merge-master-into-devnet7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.