Skip to content

refactor: bundle EIP-7928 BAL per-tx processor construction into BalTxProcessorFactory - #12760

Closed
hudem1 wants to merge 1 commit into
fix/simulate-bal-adapter-behaviorsfrom
refactor/bal-tx-processor-factory
Closed

refactor: bundle EIP-7928 BAL per-tx processor construction into BalTxProcessorFactory#12760
hudem1 wants to merge 1 commit into
fix/simulate-bal-adapter-behaviorsfrom
refactor/bal-tx-processor-factory

Conversation

@hudem1

@hudem1 hudem1 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #12721 (LukaszRozmej's step 2). Stacked on #12721 — rebase onto master once that merges.

Changes

BlockAccessListManager's blockHashProvider, specProvider, codeInfoRepositoryFactory, transactionProcessorFactory, and the tx-adapter factory were only ever used together — in TxProcessorWithWorldState's ctor — to build one processor next to a VirtualMachine. This bundles that into a new BalTxProcessorFactory:

(ITransactionProcessor Processor, ITransactionProcessorAdapter Adapter) Create(IWorldState worldState, bool parallel)
  • BlockAccessListManager drops five ctor params for one (BalTxProcessorFactory); the pool managers and the pooled leaf shrink to (stateProvider, logManager, factory).
  • BalTxProcessorFactory's adapter/processor factories default to ExecuteTransactionProcessorAdapter / TransactionProcessorFactory<EthereumGasPolicy> for the manual construction sites (stateless env, test harnesses); container resolution fills them from the scope, so the single-axis wiring from fix(simulate): route the EIP-7928 BAL path through the simulate tx adapter (#12692) #12721 is preserved and the scoped adapter / BAL-pool adapters still agree.
  • Witness envs now resolve the registered BalTxProcessorFactory instead of threading the individual collaborators.

Net −15 lines despite the new type — the bundling removes more plumbing than it adds.

Types of changes

  • Bugfix
  • New feature
  • Breaking change
  • Optimization
  • Refactoring

Testing

  • Requires testing — [x] existing coverage

No behaviour change (pure construction re-shaping). Green locally: Blockchain BAL/BlockProcessor/Producer/Eip8037/Reorg/Blockhash 269, JsonRpc simulate/trace/proof/debug 402, Merge.Plugin payload/production 885; full solution builds clean.

Documentation

  • Requires documentation update — [x] No
  • Requires Release Notes — [x] No

…rFactory (step 2)

Addresses LukaszRozmej's step-2 review. BlockAccessListManager's blockHashProvider,
specProvider, codeInfoRepositoryFactory, transactionProcessorFactory and the tx-adapter
factory were only ever used together to build one processor. Bundle them into a new
BalTxProcessorFactory.Create(worldState, parallel) => (processor, adapter); the manager
drops five ctor params for one, and the pool managers / leaf shrink to
(stateProvider, logManager, factory).

BalTxProcessorFactory's adapter/processor factories default to Execute / EthereumGasPolicy
for manual construction (stateless envs, tests); container resolution fills them from the
scope, preserving the single-axis wiring from the previous commit. Witness envs now just
resolve the registered factory.

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

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hudem1's task in 10m 30s —— View job


Review: BalTxProcessorFactory bundling (PR #12760)

  • Gather context (diff vs fix/simulate-bal-adapter-behaviors)
  • Analyze BalTxProcessorFactory + DI wiring (Autofac optional-param binding, scope overrides)
  • Check pool/manager construction sites, stateless/witness envs, all 8 test call sites
  • Post review

The bundling itself is a good call — the five collaborators genuinely only travelled together, and (processor, adapter) from one Create(worldState, parallel) is the right seam. I traced construction semantics on both the container and manual paths and found no behavioural change. Details of what I verified are at the bottom.

Findings: 0 Critical, 0 High, 1 Medium, 3 Low.


Medium

1. Seven files now have unused using directives — this will fail the code-lint job.

code-lint.yml ends with ! grep -qP "warning (IDE|CA)\d+" /tmp/build.log, and .editorconfig:97 sets dotnet_diagnostic.IDE0005.severity = warning, so any redundant using fails the job (the suppression at .editorconfig:189 only covers specific Nethermind.Merge.Plugin files).

File Now-unused
Nethermind.Consensus/Processing/BlockAccessListManager.TxProcessorPool.cs:15 Nethermind.Core.Specs
Nethermind.Consensus/Stateless/WitnessCapturingBlockProcessingEnv.cs:13,16 Nethermind.Core.Specs, Nethermind.Evm.TransactionProcessing
Nethermind.Consensus/Stateless/WitnessGeneratingBlockProcessingEnvFactory.cs:16,20 Nethermind.Core.Specs, Nethermind.Evm.TransactionProcessing
Nethermind.Blockchain.Test/BlockAccessListSequentialValidationTests.cs:16 Nethermind.Evm.TransactionProcessing
Nethermind.Blockchain.Test/BlockAccessLists/BlockAccessListManagerTests.cs:16 Nethermind.Evm.TransactionProcessing
Nethermind.Blockchain.Test/Eip8037BlockGasIntegrationTests.cs:15 Nethermind.Evm.TransactionProcessing
Nethermind.Blockchain.Test/BlockhashProviderTests.cs:24 Nethermind.Evm.TransactionProcessing

Verified by enumerating every public type in Nethermind.Core.Specs / Nethermind.Evm.TransactionProcessing and grepping each file for all of them (plus their extension-method receivers) — zero hits remain. In the four test files the last consumer was the ExecuteTransactionProcessorAdapter lambda this PR deleted; in the two witness envs it was ISpecProvider / TransactionProcessorAdapterFactory / ITransactionProcessorFactory; in the pool file it was ISpecProvider. Nethermind.Evm is still needed everywhere (BlockExecutionContext, NoopCodeCache, IBlockhashProvider), and BlockAccessListManager.cs still needs Nethermind.Core.Specs (IReleaseSpec at line 124) — don't strip those.

I could not run dotnet build in this job (command not permitted), so this is a static verification rather than a compiler confirmation — but it is the mechanism that failed CI for exactly this pattern before.

Fix this →


Low

2. adapterFactory demoted from required to silently-defaultedBalTxProcessorFactory.cs:28 (inline). Behaviour is preserved today, but a missing registration now yields a quiet Execute fallback instead of a resolution error, on the exact axis step 1 unified. Suggest making it required and passing it at the four manual sites (which also drops the CreateExecuteAdapter duplicate of BlockProcessingModule.CreateExecuteAdapter). processorFactory was already optional on the base branch, so leaving that one alone is fine.

3. Test duplication — the same 4-arg new BalTxProcessorFactory(Substitute.For<IBlockhashProvider>(), <spec>, LimboLogs.Instance, static worldState => new EthereumCodeInfoRepository(worldState)) now appears 8× across 7 files, differing only in the spec provider (inline). A one-line helper keeps the next ctor change to a single edit.

4. Create has no XML doc; class could be sealedBalTxProcessorFactory.cs:34 (inline). The parallel flag in particular is not self-documenting at the call site.

Also worth a thought (not a finding): BlockAccessListManager's public ctor signature changes shape, which is source-breaking for any out-of-tree plugin constructing it directly. Fine for an internal component mid-refactor-series, but the PR is labelled refactoring-only, so flagging it for visibility.


What I verified as behaviour-preserving

  • Construction order/inputs identical. VirtualMachine is still built once per processor; codeInfoRepositoryFactory still receives the TracedAccessWorldState (Create(WorldState, parallel)codeInfoRepositoryFactory(worldState)), and BlobBaseFeeCalculator.Instance / specProvider / parallel are threaded unchanged.
  • Autofac optional-parameter binding. AddScoped<T>() uses RegisterType<T>(), so ConstructorParameterBinding prefers a registered service and only falls back to the default when nothing can supply the value. Both optional params are therefore injected in every real scope.
  • All adapter-override scopes still reached. BalTxProcessorFactory is InstancePerLifetimeScope, so it is re-created per child scope and picks up that scope's TransactionProcessorAdapterFactory: BuildUp (GlobalWorldStateBlockProducerEnvFactory.cs:31), Trace (ProofModuleFactory, TraceModuleFactory), and simulate's SimulateRequestState-derived one (SimulateReadOnlyBlocksProcessingEnvFactory.cs:70) — relevant given this stack's base branch.
  • AuRa and simulate processor factories still honoured. AuRaPlugin registers ITransactionProcessorFactory → AuRaTransactionProcessorFactory, and simulate decorates it with SkipSenderCodeCheckTransactionProcessorFactory; both resolve through the new factory instead of the manager, in the same scope as before.
  • Witness scopes equivalent. ctx.Resolve<BalTxProcessorFactory>() resolves against the child scope's IComponentContext, so the overridden IWorldState / IBlockhashCache / NoopCodeCache still shape the result exactly as the previous explicit ctx.Resolve<...>() list did.
  • No missed construction site. Every new BlockAccessListManager(...) (2 witness scopes, StatelessBlockProcessingEnv, 8 test sites) and the single DI registration are updated; .AddScoped<BalTxProcessorFactory>() sits in BlockProcessingModule, the only module registering IBlockAccessListManager.
  • Lifetimes/threading. Scoped factory over a singleton ITransactionProcessorFactory — no captive dependency; Create only reads readonly fields and allocates, so concurrent NewProcessor() calls from the pool are as safe as before. No new allocations on any hot path (pool warm-up only).

Verdict: the refactor is sound; please clear the unused usings before merge (CI-blocking) and either address or explicitly wave off finding 2.
· branch refactor/bal-tx-processor-factory

ISpecProvider specProvider,
ILogManager logManager,
CodeInfoRepositoryFactory codeInfoRepositoryFactory,
TransactionProcessorAdapterFactory? adapterFactory = null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low–Medium — adapterFactory goes from required to silently-defaulted.

On the base branch TransactionProcessorAdapterFactory was a required BlockAccessListManager ctor param, so a scope missing that registration failed loudly at resolution. Making it = null with a ?? CreateExecuteAdapter fallback converts that into a silent behavioural default.

That matters because the adapter is exactly the axis step 1 (#12721) unified: block production registers CreateBuildUpAdapter (GlobalWorldStateBlockProducerEnvFactory.cs:31), trace/proof register CreateTraceAdapter, and simulate registers a SimulateRequestState-derived one. Today all of those scopes sit on top of BlockProcessingModule, so Autofac's optional-parameter binding does inject them and behaviour is preserved — I verified that. But if a future scope ever builds a BAL manager without that registration, the pool's per-worker adapters would quietly fall back to Execute while the scoped ITransactionProcessorAdapter stays on BuildUp/Trace — a silent two-axis divergence in a consensus-critical component instead of an exception.

Suggestion: keep processorFactory optional (it already was), but make adapterFactory required and pass it explicitly at the four manual sites (StatelessBlockProcessingEnv + the test helper). That also removes the CreateExecuteAdapter duplicate of BlockProcessingModule.CreateExecuteAdapter.

private readonly TransactionProcessorAdapterFactory _adapterFactory = adapterFactory ?? CreateExecuteAdapter;
private readonly ITransactionProcessorFactory _processorFactory = processorFactory ?? new TransactionProcessorFactory<EthereumGasPolicy>();

public (ITransactionProcessor Processor, ITransactionProcessorAdapter Adapter) Create(IWorldState worldState, bool parallel)

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.

LowCreate is the type's only public member and has no XML doc; per AGENTS.md public members want <summary> (+ <param> for the non-obvious parallel flag, which selects the BAL-backed world state / parallel processor path). Also worth sealeding the class — nothing derives from it, and the repo prefers composition over inheritance.

Comment on lines +1280 to +1281
new BalTxProcessorFactory(Substitute.For<IBlockhashProvider>(), new TestSingleReleaseSpecProvider(Amsterdam.Instance), LimboLogs.Instance,
static worldState => new EthereumCodeInfoRepository(worldState)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low — test duplication. This exact 4-argument BalTxProcessorFactory construction is now repeated 8× across BlockProcessorTests (×6), Eip8037BlockGasIntegrationTests (×2), BlockAccessListSequentialValidationTests, BlockhashProviderTests, BlockAccessListManagerTests, ReorgTests, AuraBlockProcessorTests — differing only in the spec provider. AGENTS.md / test-infrastructure.md ask for shared setup to be factored out; a one-liner helper (e.g. static BalTxProcessorFactory TestBalFactory(ISpecProvider specProvider) => new(Substitute.For<IBlockhashProvider>(), specProvider, LimboLogs.Instance, static worldState => new EthereumCodeInfoRepository(worldState));) would shrink every call site and keep the eight in sync when the ctor changes again.

@hudem1

hudem1 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Superseded — step 2 (BalTxProcessorFactory) was implemented directly on #12721 (dae82e5 + follow-up suggestion commits), so this stacked PR is redundant. The BlockAccessListManager ctor and BalTxProcessorFactory on #12721 match what this PR did. Closing.

@hudem1 hudem1 closed this Aug 10, 2026
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.

1 participant