Skip to content

fix(debug): honour the tracer's Execute↔Trace swap on the EIP-7928 BAL path - #12972

Open
hudem1 wants to merge 6 commits into
masterfrom
fix/debug-bal-changeable-adapter
Open

fix(debug): honour the tracer's Execute↔Trace swap on the EIP-7928 BAL path#12972
hudem1 wants to merge 6 commits into
masterfrom
fix/debug-bal-changeable-adapter

Conversation

@hudem1

@hudem1 hudem1 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #12723 (follow-up from #12721, part of #12599 glamsterdam devnet-8).

Changes

Under EIP-7928 the BlockAccessListManager runs transactions through its own per-worker tx-processor adapters, built from the block-processing scope's TransactionProcessorAdapterFactory. Every scope registers that factory (production, trace, proof, simulate) except debug, which registered ChangeableTransactionProcessorAdapter directly — so its BAL-pool workers stayed on Execute and GethStyleTracer's runtime swap to TraceTransactionProcessorAdapter was bypassed on the BAL path.

Consequence: on an Amsterdam block a debug_traceCall whose sender has deployed code (a state-overridden contract, or any call that relies on skipped sender validation) was rejected with EIP-3607 sender has deployed code instead of being traced — Execute validates, Trace skips validation.

The fix mirrors what every other scope does, adapted for debug's shared-mutable adapter:

  • ChangeableTransactionProcessorAdapter.ForProcessor(processor) returns a per-worker adapter that mirrors the shared adapter's current mode (Execute vs Trace), re-read on every call — required because the sequential BAL manager builds its worker adapter once.
  • DebugModuleFactory registers TransactionProcessorAdapterFactory = changeable.ForProcessor, so the BAL pool's workers track the same changeable mode the tracer swaps. The scoped ChangeableTransactionProcessorAdapter registration stays for the non-BAL path and as the tracer's swap target.
  • Updated the TransactionProcessorAdapterFactory <remarks> that described debug as the un-fixed exception.

Types of changes

  • Bugfix (a non-breaking change that fixes an issue)

Testing

  • Yes — added DebugTraceCallBalTests.debug_traceCall_from_contract_sender_traces_on_bal_path: traces a contract-sender call through the EIP-7928 BAL path on an Amsterdam chain.
    • Fails on master with InvalidTransactionException: … failed with error sender has deployed code.
    • Passes with this change.
    • The test documents the two conditions needed to exercise the bug: a non-genesis head (BAL is disabled on genesis) and a non-default tracer (callTracer) so the buffered/eager path runs the transaction instead of the lazily-streamed default struct-log path.
  • Broader debug suites (DebugRpcModuleTests, DebugModuleTests, DebugSimulateTests) pass — no regressions.

Documentation

No user-facing changes.

🤖 Generated with Claude Code

hudem1 and others added 2 commits August 24, 2026 18:11
…L path

Part of #12599fixes #12723 (follow-up from #12721).

Under EIP-7928 the block-access-list pool runs transactions through its own
per-worker tx-processor adapters, built from the scope's
TransactionProcessorAdapterFactory. Every scope registers that factory except
debug, which registered ChangeableTransactionProcessorAdapter directly — so its
BAL workers stayed on Execute and GethStyleTracer's runtime swap to
TraceTransactionProcessorAdapter was bypassed. A debug_traceCall from a
state-overridden contract sender on an Amsterdam block was therefore rejected
with EIP-3607 "sender has deployed code" instead of being traced.

Debug now registers ChangeableTransactionProcessorAdapter.ForProcessor as its
factory: each worker mirrors the shared changeable adapter's current mode
(re-read per call) onto its own processor, so the BAL pool honours the swap.

Adds a regression test tracing a contract sender through the BAL path.

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

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hudem1's task in 7m 43s —— View job


Review: fix(debug): honour the tracer's Execute↔Trace swap on the EIP-7928 BAL path

  • Read PR diff
  • Traced the DI wiring (DebugModuleFactoryBalTxProcessorFactory → BAL worker pool)
  • Verified scope/instance identity and thread-safety of the shared-mode read
  • Reviewed the added regression test against repo test conventions
  • Posted findings

Verdict: the fix itself is correct and minimal. No Critical or High findings. All findings below are on the test and on the robustness of the mode-detection contract.


What I verified

  • Same-instance guarantee holds. AddScoped<ITransactionProcessorAdapter, ChangeableTransactionProcessorAdapter>() registers the concrete type AsSelf() + InstancePerLifetimeScope() and then BindScoped maps the interface onto ctx.Resolve<TImpl>() (ContainerBuilderExtensions.cs:273-283, BindScoped at :443). The new TransactionProcessorAdapterFactory registration resolves that same instance, and DisposableScopeOverridableEnv<T> resolves its components once in the tracer scope (OverridableEnvFactory's env is added as a module into tracerLifecycle, not a per-call child scope) — so GethStyleTracer.CurrentAdapter and the BAL pool's factory genuinely point at one object.
  • The re-read is required. TxProcessorWithWorldState builds its adapter once in its constructor (BlockAccessListManager.TxProcessorPool.cs:379) and the sequential path reuses it for every tx (BlockProcessor.ParallelBlockValidationTransactionsExecutor.cs:85), so a snapshot would be wrong.
  • Blast radius is contained. The only consumers of TransactionProcessorAdapterFactory inside the debug scope are BalTxProcessorFactory and BlockProcessingModule's derived ITransactionProcessorAdapter registration — the latter is overridden by debug's own line 29 registration, so the non-BAL path is unchanged. When the tracer has not swapped, IsTracing is false and workers run Execute exactly as before; the behaviour change is strictly the intended one.
  • No adapter-type sniffing elsewhere. Grepped the tree: the new CurrentAdapter is TraceTransactionProcessorAdapter is the only place any code branches on adapter type, so introducing PerProcessorAdapter doesn't perturb anything downstream.
  • Threading. CurrentAdapter is read from BAL workers, but every worker is scheduled after the swap and joined before the finally restore, so the non-volatile read is fine; per-tx isinst cost is noise, and this only ever runs in the debug scope.
  • Not forwarding PrepareForInclusionCheck in PerProcessorAdapter matches ChangeableTransactionProcessorAdapter's own behaviour and only matters for simulate — correct as-is.

Findings

Sev Finding
Medium The added test never disposes TestRpcBlockchain — DBs, processor thread, producer runner and two debug lifetime scopes leak for the fixture run. (inline)
Medium The invariant the fix rests on — mode re-read per call, not snapshotted — is not pinned by the test: the sequential worker pool is built lazily after the swap, so a snapshot implementation would pass too. Suggested a 10-line unit test on ForProcessor. (inline)
Low IsTracing type-sniffing silently falls back to Execute for any unrecognised CurrentAdapter — same class of silent wrong-mode bug, relocated. (inline)
Low await chain.AddBlock() and its comment are redundant — AddBlocksOnStart() already leaves the head at block 3. (inline)
Low New standalone fixture duplicates DebugRpcModuleTests.Context; plus ForTest(new TestRpcBlockchain()) leaves SealEngineType null and IsEip3607Enabled = true is already Amsterdam's value. (inline)

Not verified

I could not compile or run anything — dotnet is not in this job's allowed tools, so the "fails on master / passes with this change" claim and the compile-correctness of the new test file are taken from the PR description and static reading only. Everything above is from source inspection.
• branch fix/debug-bal-changeable-adapter

Comment thread src/Nethermind/Nethermind.JsonRpc.Test/Modules/DebugTraceCallBalTests.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc.Test/Modules/DebugTraceCallBalTests.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc.Test/Modules/DebugTraceCallBalTests.cs Outdated
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

EVM Opcode Benchmark Diff

Aggregated runs: base=1, pr=1

No significant regressions or improvements detected.

…unit test

- Move the BAL debug_traceCall regression into a DebugRpcModuleTests partial that
  reuses Context.Create (disposes the chain) instead of a standalone, undisposed
  fixture; drop the redundant AddBlock (AddBlocksOnStart already gives a
  non-genesis head) and the misleading EIP-3607 "enable" comment.
- Add ChangeableTransactionProcessorAdapterTests pinning the "re-read the mode on
  every call" invariant: the BAL pool builds its worker once, before the tracer
  swaps the mode, so a snapshot implementation would silently keep validating on
  later non-swapping traces.

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

@wurdum wurdum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The only thing, out of the scope of changes:

GethStyleTracer clones the target block with WithReplacedBodyCloned, which carries the original BlockAccessList forward, and BlockchainProcessorFacade.Process forwards options without ForceSequentialBlockAccessList. BranchProcessor adds that flag whenever a tracer is present, so on a real Amsterdam block debug_traceCall takes the parallel BAL executor, where the sequential-retry exception has no handler. Mirroring BranchProcessor in the facade would put this fix on the path users hit.

// Mirror BranchProcessor: traced processing runs the sequential BAL path, since this facade
// bypasses BranchProcessor and therefore its sequential retry.
ProcessingOptions blockOptions = tracer == NullBlockTracer.Instance
    ? options
    : options | ProcessingOptions.ForceSequentialBlockAccessList;
(Block? processedBlock, TxReceipt[] _) = blockProcessor.ProcessOne(block, blockOptions, tracer, spec, token);

…-sniff

Review follow-up (benaadams):

- ChangeableTransactionProcessorAdapter now stores the runtime mode as an explicit
  TransactionProcessorAdapterFactory (CurrentAdapterFactory) that GethStyleTracer
  swaps, instead of inferring the mode from `CurrentAdapter is
  TraceTransactionProcessorAdapter`. A decorated or alternative tracing adapter is
  no longer misclassified as Execute. Each ForProcessor worker applies the current
  factory to its own processor, re-read on every call.
- Remove the substitute-based ChangeableTransactionProcessorAdapterTests: the
  factory shape makes "re-read per call on the worker's own processor" structural
  (no snapshot possible), and test-infrastructure.md forbids Substitute.For for
  components TestBlockchain provides. The production-DI e2e test remains the guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hudem1
hudem1 requested review from MarekM25 and flcl42 as code owners August 25, 2026 13:05
…essorFacade

Review follow-up (wurdum): GethStyleTracer clones the target block with
WithReplacedBodyCloned, carrying the original BlockAccessList forward, and this
facade forwarded options unchanged. BranchProcessor adds
ForceSequentialBlockAccessList whenever a tracer is present (and handles the
parallel BlockAccessListSequentialRetryException); the facade bypasses
BranchProcessor, so on a real Amsterdam block debug_traceCall took the parallel
BAL executor whose sequential-retry exception has no handler here.

Mirror BranchProcessor: force the sequential BAL path when a tracer is present.
This also lands the adapter fix on the path users actually hit.

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

hudem1 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

@wurdum good catch on the parallel BAL path — applied in 51bb387.

BlockchainProcessorFacade.Process now mirrors BranchProcessor: when a tracer is present it adds ProcessingOptions.ForceSequentialBlockAccessList, so a traced block (whose WithReplacedBodyCloned clone carries the original BlockAccessList) runs the sequential BAL path instead of the parallel executor — whose BlockAccessListSequentialRetryException this facade doesn't handle. That also lands the adapter fix on the path real Amsterdam nodes hit.

ProcessingOptions blockOptions = tracer == NullBlockTracer.Instance
    ? options
    : options | ProcessingOptions.ForceSequentialBlockAccessList;
(Block? processedBlock, TxReceipt[] _) = blockProcessor.ProcessOne(block, blockOptions, tracer, spec, token);

Debug suites (DebugRpcModuleTests, DebugModuleTests, DebugSimulateTests) still pass. A dedicated parallel-path integration test would need a ParallelExecution config + parent-reader-pool setup that Context.Create doesn't expose — happy to add one if you'd like it pinned, otherwise this mirrors BranchProcessor's existing (tested) behavior.

IDE0005: Nethermind.JsonRpc is an ancestor of the file's namespace
(Nethermind.JsonRpc.Test.Modules), so ResultWrapper resolves without the using.

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

EIP-7928 BAL path bypasses debug's ChangeableTransactionProcessorAdapter (debug_traceCall/traceTransaction trace via Execute on Amsterdam blocks)

4 participants