Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Threading;
using Nethermind.Blockchain.Tracing;
using Nethermind.Consensus.Processing;
using Nethermind.Core;
using Nethermind.Core.Specs;
Expand Down Expand Up @@ -31,7 +32,14 @@ IReadOnlyList<IBlockPreprocessorStep> preprocessorSteps

IReleaseSpec spec = specProvider.GetSpec(block.Header);

(Block? processedBlock, TxReceipt[] _) = blockProcessor.ProcessOne(block, options, tracer, spec, token);
// Mirror BranchProcessor: a traced block runs the sequential EIP-7928 BAL path. This facade bypasses
// BranchProcessor, so it also bypasses its parallel BlockAccessListSequentialRetryException handler —
// forcing sequential here keeps traced processing off the unhandled parallel path.
ProcessingOptions blockOptions = tracer == NullBlockTracer.Instance
? options
: options | ProcessingOptions.ForceSequentialBlockAccessList;

(Block? processedBlock, TxReceipt[] _) = blockProcessor.ProcessOne(block, blockOptions, tracer, spec, token);
return processedBlock;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@ public class GethStyleTracer(
Block block = blockTree.FindBlock(blockParameter) ?? throw new InvalidOperationException($"Cannot find block {blockParameter}");
tx.Hash ??= tx.CalculateHash();
block = block.WithReplacedBodyCloned(BlockBody.WithOneTransactionOnly(tx));
ITransactionProcessorAdapter currentAdapter = transactionProcessorAdapter.CurrentAdapter;
transactionProcessorAdapter.CurrentAdapter = new TraceTransactionProcessorAdapter(transactionProcessorAdapter.TransactionProcessor);
TransactionProcessorAdapterFactory previousAdapterFactory = transactionProcessorAdapter.CurrentAdapterFactory;
transactionProcessorAdapter.CurrentAdapterFactory = static processor => new TraceTransactionProcessorAdapter(processor);

try
{
return TraceImpl(block, tx.Hash, cancellationToken, options, ProcessingOptions.TraceTransactions, writer, pipeWriter);
}
finally
{
transactionProcessorAdapter.CurrentAdapter = currentAdapter;
transactionProcessorAdapter.CurrentAdapterFactory = previousAdapterFactory;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,36 @@

namespace Nethermind.Evm.TransactionProcessing
{
public class ChangeableTransactionProcessorAdapter : ITransactionProcessorAdapter
public class ChangeableTransactionProcessorAdapter(ITransactionProcessor transactionProcessor) : ITransactionProcessorAdapter
{
public ITransactionProcessorAdapter CurrentAdapter { get; set; }
public ITransactionProcessor TransactionProcessor { get; }

private ChangeableTransactionProcessorAdapter(ITransactionProcessorAdapter adapter) => CurrentAdapter = adapter;

public ChangeableTransactionProcessorAdapter(ITransactionProcessor transactionProcessor)
: this(new ExecuteTransactionProcessorAdapter(transactionProcessor)) => TransactionProcessor = transactionProcessor;
/// <summary>The current runtime mode: given a processor, builds the adapter that runs it. Swapped by the debug tracer between Execute and Trace.</summary>
public TransactionProcessorAdapterFactory CurrentAdapterFactory { get; set; } = static processor => new ExecuteTransactionProcessorAdapter(processor);
public ITransactionProcessor TransactionProcessor { get; } = transactionProcessor;

public TransactionResult Execute(Transaction transaction, ITxTracer txTracer) =>
CurrentAdapter.Execute(transaction, txTracer);
CurrentAdapterFactory(TransactionProcessor).Execute(transaction, txTracer);
public void SetBlockExecutionContext(in BlockExecutionContext blockExecutionContext)
=> CurrentAdapter.SetBlockExecutionContext(in blockExecutionContext);
=> TransactionProcessor.SetBlockExecutionContext(in blockExecutionContext);

/// <summary>
/// Builds an adapter that runs <paramref name="processor"/> in this adapter's current runtime mode,
/// re-read on every call.
/// </summary>
/// <remarks>
/// Lets the EIP-7928 block-access-list pool's per-worker processors honour the debug tracer's runtime
/// Execute↔Trace swap: the debug scope registers this as its <see cref="TransactionProcessorAdapterFactory"/>,
/// so each worker applies this shared adapter's current mode to its own processor.
/// </remarks>
public ITransactionProcessorAdapter ForProcessor(ITransactionProcessor processor)
=> new PerProcessorAdapter(this, processor);

private sealed class PerProcessorAdapter(ChangeableTransactionProcessorAdapter mode, ITransactionProcessor processor)
: ITransactionProcessorAdapter
{
public TransactionResult Execute(Transaction transaction, ITxTracer txTracer) =>
mode.CurrentAdapterFactory(processor).Execute(transaction, txTracer);
public void SetBlockExecutionContext(in BlockExecutionContext blockExecutionContext)
=> processor.SetBlockExecutionContext(in blockExecutionContext);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ namespace Nethermind.Evm.TransactionProcessing;
/// The single axis for per-transaction adapter selection: the block-processing scope registers the default
/// (<see cref="ExecuteTransactionProcessorAdapter"/>) and derives the scoped <see cref="ITransactionProcessorAdapter"/>
/// from it, so a scope that overrides this factory (block production, trace, proof, simulate) changes both the main
/// path and the EIP-7928 BAL pool's per-worker adapters at once. The one exception is debug, which registers its
/// runtime-mutable <see cref="ChangeableTransactionProcessorAdapter"/> directly and keeps the default behaviour on
/// the BAL path.
/// path and the EIP-7928 BAL pool's per-worker adapters at once. Debug registers its runtime-mutable
/// <see cref="ChangeableTransactionProcessorAdapter"/> as the scoped adapter and a factory
/// (<see cref="ChangeableTransactionProcessorAdapter.ForProcessor"/>) that mirrors that adapter's Execute↔Trace
/// mode onto each worker, so the BAL pool honours the tracer's runtime swap too.
/// </remarks>
public delegate ITransactionProcessorAdapter TransactionProcessorAdapterFactory(ITransactionProcessor transactionProcessor);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Collections.Generic;
using System.Threading.Tasks;
using Nethermind.Blockchain.Find;
using Nethermind.Blockchain.Tracing.GethStyle;
using Nethermind.Core;
using Nethermind.Core.Extensions;
using Nethermind.Core.Test.Builders;
using Nethermind.Evm;
using Nethermind.Facade.Eth.RpcTransaction;
using Nethermind.Int256;
using Nethermind.Specs;
using Nethermind.Specs.Forks;
using NUnit.Framework;

namespace Nethermind.JsonRpc.Test.Modules;

public partial class DebugRpcModuleTests
{
/// <summary>
/// Regression test for #12723: under EIP-7928 the block-access-list pool's per-worker adapters must honour the
/// debug tracer's runtime Execute↔Trace swap. A <c>debug_traceCall</c> from a sender that has deployed code is
/// rejected by Execute (EIP-3607, "sender has deployed code") but allowed by Trace, which skips sender
/// validation; a BAL worker stuck on Execute fails the trace on the sender-code check.
/// </summary>
[Test]
public async Task Debug_traceCall_from_contract_sender_traces_on_bal_path()
{
// Amsterdam activates both EIP-7928 (the BAL pool) and EIP-3607 (reject contract senders).
Assert.That(Amsterdam.Instance.IsEip7928Enabled && Amsterdam.Instance.IsEip3607Enabled, Is.True);
TestSpecProvider specProvider = new(Amsterdam.Instance) { AllowTestChainOverride = false };
using Context ctx = await Context.Create(specProvider);

LegacyTransactionForRpc call = new()
{
From = TestItem.AddressA,
To = TestItem.AddressB,
Value = 0,
Gas = 100_000,
GasPrice = UInt256.Zero,
};
// A non-default tracer forces the buffered (eager) path so the trace actually runs the transaction; the
// default struct-log tracer streams lazily and would never execute it. The sender is given deployed code:
// Execute rejects it (EIP-3607), Trace skips that check.
GethTraceOptions options = new()
{
Tracer = "callTracer",
StateOverrides = new Dictionary<Address, AccountOverride>
{
{ TestItem.AddressA, new AccountOverride { Code = Bytes.FromHexString("0x00") } }
}
};

ResultWrapper<GethLikeTxTrace> result = ctx.DebugRpcModule.debug_traceCall(call, BlockParameter.Latest, options);

Assert.That(result.Result.ResultType, Is.EqualTo(ResultType.Success), () => result.Result.Error ?? string.Empty);
Assert.That(result.Data, Is.Not.Null);
Assert.That(result.Data!.Failed, Is.False, "the contract-sender call must trace under skipped validation");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ private ContainerBuilder ConfigureTracerContainer(ContainerBuilder builder) =>
.AddScoped<BlockchainProcessor.Options>(BlockchainProcessor.Options.NoReceipts)

// So the debug rpc change the adapter sometime.
.AddScoped<ITransactionProcessorAdapter, ChangeableTransactionProcessorAdapter>();
.AddScoped<ITransactionProcessorAdapter, ChangeableTransactionProcessorAdapter>()

// The EIP-7928 BAL pool builds its per-worker adapters from this factory; route them through the
// same ChangeableTransactionProcessorAdapter so they honour the tracer's runtime Execute↔Trace swap.
.AddScoped<TransactionProcessorAdapterFactory, ChangeableTransactionProcessorAdapter>(
static changeable => changeable.ForProcessor);

public IDebugRpcModule Create()
{
Expand Down
Loading