diff --git a/src/Nethermind.Arbitrum/ArbitrumPlugin.cs b/src/Nethermind.Arbitrum/ArbitrumPlugin.cs index c9728e902..d6a8670cc 100644 --- a/src/Nethermind.Arbitrum/ArbitrumPlugin.cs +++ b/src/Nethermind.Arbitrum/ArbitrumPlugin.cs @@ -16,11 +16,13 @@ using Nethermind.Arbitrum.Modules; using Nethermind.Arbitrum.Precompiles; using Nethermind.Arbitrum.Stylus; +using Nethermind.Arbitrum.Tracing; using Nethermind.Blockchain; using Nethermind.Config; using Nethermind.Consensus; using Nethermind.Consensus.Processing; using Nethermind.Consensus.Producers; +using Nethermind.Consensus.Tracing; using Nethermind.Consensus.Validators; using Nethermind.Core; using Nethermind.Core.Container; @@ -229,7 +231,8 @@ protected override void Load(ContainerBuilder builder) // Rpcs .AddSingleton() - .Bind, ArbitrumEthModuleFactory>(); + .Bind, ArbitrumEthModuleFactory>() + .AddScoped(); if (blocksConfig.BuildBlocksOnMainState) builder.AddSingleton(); diff --git a/src/Nethermind.Arbitrum/Execution/Receipts/ArbitrumBlockReceiptTracer.cs b/src/Nethermind.Arbitrum/Execution/Receipts/ArbitrumBlockReceiptTracer.cs index 61609cce1..fa70ceccc 100644 --- a/src/Nethermind.Arbitrum/Execution/Receipts/ArbitrumBlockReceiptTracer.cs +++ b/src/Nethermind.Arbitrum/Execution/Receipts/ArbitrumBlockReceiptTracer.cs @@ -3,16 +3,18 @@ using Nethermind.Arbitrum.Config; using Nethermind.Arbitrum.Execution.Transactions; +using Nethermind.Arbitrum.Tracing; using Nethermind.Blockchain.Tracing; using Nethermind.Core; using Nethermind.Core.Crypto; -using Nethermind.Evm.TransactionProcessing; +using Nethermind.Evm.Tracing; +using Nethermind.Int256; namespace Nethermind.Arbitrum.Execution.Receipts; public class ArbitrumBlockReceiptTracer( ArbitrumTxExecutionContext txExecContext, - IArbitrumConfig arbitrumConfig) : BlockReceiptsTracer + IArbitrumConfig arbitrumConfig) : BlockReceiptsTracer, IArbitrumTxTracer { protected override TxReceipt BuildReceipt(Address recipient, long spentGas, byte statusCode, LogEntry[] logEntries, Hash256? stateRoot) { @@ -40,4 +42,37 @@ protected override TxReceipt BuildReceipt(Address recipient, long spentGas, byte return txReceipt; } + + public void CaptureArbitrumTransfer(Address? from, Address? to, UInt256 value, bool before, BalanceChangeReason reason) + { + IArbitrumTxTracer? arbitrumTxTracer = InnerTracer?.GetTracer(); + arbitrumTxTracer?.CaptureArbitrumTransfer(from, to, value, before, reason); + } + + public void CaptureArbitrumStorageGet(UInt256 index, int depth, bool before) + { + if (InnerTracer is IArbitrumTxTracer arbitrumTxTracer) arbitrumTxTracer.CaptureArbitrumStorageGet(index, depth, before); + } + + public void CaptureArbitrumStorageSet(UInt256 index, ValueHash256 value, int depth, bool before) + { + if (InnerTracer is IArbitrumTxTracer arbitrumTxTracer) arbitrumTxTracer.CaptureArbitrumStorageSet(index, value, depth, before); + } + + public void CaptureStylusHostio(string name, ReadOnlySpan args, ReadOnlySpan outs, ulong startInk, ulong endInk) + { + if (InnerTracer is IArbitrumTxTracer arbitrumTxTracer) arbitrumTxTracer.CaptureStylusHostio(name, args, outs, startInk, endInk); + } + + /// + /// Reports change of code for address + /// + /// + /// + /// + /// Depends on + public new void ReportCodeChange(Address address, byte[]? before, byte[]? after) + { + base.ReportCodeChange(address, before!, after!); + } } diff --git a/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeBlockTracer.cs b/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeBlockTracer.cs index 673f92aae..4adcc3815 100644 --- a/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeBlockTracer.cs +++ b/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeBlockTracer.cs @@ -14,3 +14,11 @@ public class ArbitrumGethLikeBlockTracer(GethTraceOptions options) protected override GethLikeTxTrace OnEnd(ArbitrumGethLikeTxTracer txTracer) => txTracer.BuildResult(); } + +public class ArbitrumGethLikeNativeBlockTracer(GethTraceOptions options) + : BlockTracerBase(options.TxHash) +{ + protected override ArbitrumNativeCallTracer OnStart(Transaction? tx) => new(tx, options); + + protected override GethLikeTxTrace OnEnd(ArbitrumNativeCallTracer txTracer) => txTracer.BuildResult(); +} diff --git a/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeTxTracer.cs b/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeTxTracer.cs index 9c8954357..2c02cfd99 100644 --- a/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeTxTracer.cs +++ b/src/Nethermind.Arbitrum/Tracing/ArbitrumGethLikeTxTracer.cs @@ -1,7 +1,18 @@ using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Blockchain.Tracing.GethStyle.Custom; +using Nethermind.Blockchain.Tracing.GethStyle.Custom.Native; +using Nethermind.Blockchain.Tracing.GethStyle.Custom.Native.Call; using Nethermind.Core; +using Nethermind.Core.Collections; using Nethermind.Core.Crypto; +using Nethermind.Evm; +using Nethermind.Evm.TransactionProcessing; using Nethermind.Int256; +using Nethermind.Serialization.Json; +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using Nethermind.Core.Extensions; namespace Nethermind.Arbitrum.Tracing; @@ -52,3 +63,393 @@ public void CaptureStylusHostio(string name, ReadOnlySpan args, ReadOnlySp { } } + +[JsonConverter(typeof(ArbitrumNativeCallTracerCallFrameConverter))] +public class ArbitrumNativeCallFrame : NativeCallTracerCallFrame +{ + public List BeforeEvmTransfers { get; } = []; + public List AfterEvmTransfers { get; } = []; +} + +public class ArbitrumNativeCallTracerCallFrameConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, ArbitrumNativeCallFrame value, JsonSerializerOptions options) + { + NumberConversion? previousValue = ForcedNumberConversion.ForcedConversion.Value; + try + { + writer.WriteStartObject(); + + ForcedNumberConversion.ForcedConversion.Value = NumberConversion.Hex; + writer.WritePropertyName("type"u8); + JsonSerializer.Serialize(writer, value.Type.GetName(), options); + + writer.WritePropertyName("from"u8); + JsonSerializer.Serialize(writer, value.From, options); + + if (value.To is not null) + { + writer.WritePropertyName("to"u8); + JsonSerializer.Serialize(writer, value.To, options); + } + + if (value.Value is not null) + { + writer.WritePropertyName("value"u8); + JsonSerializer.Serialize(writer, value.Value, options); + } + + writer.WritePropertyName("gas"u8); + JsonSerializer.Serialize(writer, value.Gas, options); + + writer.WritePropertyName("gasUsed"u8); + JsonSerializer.Serialize(writer, value.GasUsed, options); + + writer.WritePropertyName("input"u8); + if (value.Input is null || value.Input.Count == 0) + { + writer.WriteStringValue("0x"u8); + } + else + { + JsonSerializer.Serialize(writer, value.Input.AsReadOnlyMemory(), options); + } + + if (value.Output?.Count > 0) + { + writer.WritePropertyName("output"u8); + JsonSerializer.Serialize(writer, value.Output.AsReadOnlyMemory(), options); + } + + if (value.Error is not null) + { + writer.WritePropertyName("error"u8); + JsonSerializer.Serialize(writer, value.Error, options); + } + + if (value.RevertReason is not null) + { + writer.WritePropertyName("revertReason"u8); + JsonSerializer.Serialize(writer, value.RevertReason, options); + } + + if (value.Logs?.Count > 0) + { + writer.WritePropertyName("logs"u8); + JsonSerializer.Serialize(writer, value.Logs.AsMemory(), options); + } + + if (value.Calls?.Count > 0) + { + writer.WritePropertyName("calls"u8); + JsonSerializer.Serialize(writer, value.Calls.AsMemory(), options); + } + + if (value.BeforeEvmTransfers?.Count > 0) + { + writer.WritePropertyName("beforeEvmTransfers"u8); + JsonSerializer.Serialize(writer, value.BeforeEvmTransfers, options); + } + + if (value.AfterEvmTransfers?.Count > 0) + { + writer.WritePropertyName("afterEvmTransfers"u8); + JsonSerializer.Serialize(writer, value.AfterEvmTransfers, options); + } + + writer.WriteEndObject(); + } + finally + { + ForcedNumberConversion.ForcedConversion.Value = previousValue; + } + } + + public override ArbitrumNativeCallFrame Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException(); + } +} + +public sealed class ArbitrumNativeCallTracer : GethLikeNativeTxTracer, IArbitrumTxTracer +{ + public const string CallTracer = "callTracer"; + + private readonly long _gasLimit; + private readonly Hash256? _txHash; + private readonly NativeCallTracerConfig _config; + private readonly ArrayPoolList _callStack = new(1024); + private readonly CompositeDisposable _disposables = new(); + + private EvmExceptionType? _error; + private long _remainingGas; + private bool _resultBuilt = false; + + public ArbitrumNativeCallTracer( + Transaction? tx, + GethTraceOptions options) : base(options) + { + IsTracingActions = true; + _gasLimit = tx!.GasLimit; + _txHash = tx.Hash; + + _config = options.TracerConfig?.Deserialize(EthereumJsonSerializer.JsonOptions) ?? new NativeCallTracerConfig(); + + if (_config.WithLog) + { + IsTracingLogs = true; + } + } + + protected override GethLikeTxTrace CreateTrace() => new(_disposables); + + public override GethLikeTxTrace BuildResult() + { + GethLikeTxTrace result = base.BuildResult(); + ArbitrumNativeCallFrame firstCallFrame = _callStack[0]; + + Debug.Assert(_callStack.Count == 1, $"Unexpected frames on call stack, expected only master frame, found {_callStack.Count} frames."); + + _callStack.RemoveAt(0); + _disposables.Add(firstCallFrame); + + result.TxHash = _txHash; + result.CustomTracerResult = new GethLikeCustomTrace { Value = firstCallFrame }; + + _resultBuilt = true; + + return result; + } + + public override void Dispose() + { + base.Dispose(); + for (int i = _resultBuilt ? 1 : 0; i < _callStack.Count; i++) + { + _callStack[i].Dispose(); + } + + _callStack.Dispose(); + } + + public override void ReportAction(long gas, UInt256 value, Address from, Address to, ReadOnlyMemory input, ExecutionType callType, bool isPrecompileCall = false) + { + base.ReportAction(gas, value, from, to, input, callType, isPrecompileCall); + + if (_config.OnlyTopCall && Depth > 0) + return; + + Instruction callOpcode = callType.ToInstruction(); + ArbitrumNativeCallFrame callFrame = new() + { + Type = callOpcode, + From = from, + To = to, + Gas = Depth == 0 ? _gasLimit : gas, + Value = callOpcode == Instruction.STATICCALL ? null : value, + Input = input.Span.ToPooledList() + }; + _callStack.Add(callFrame); + } + + public override void ReportLog(LogEntry log) + { + base.ReportLog(log); + + if (_config.OnlyTopCall && Depth > 0) + return; + + NativeCallTracerCallFrame callFrame = _callStack[^1]; + + NativeCallTracerLogEntry callLog = new( + log.Address, + log.Data, + log.Topics, + (ulong)callFrame.Calls.Count); + + callFrame.Logs ??= new ArrayPoolList(8); + callFrame.Logs.Add(callLog); + } + + public override void ReportOperationRemainingGas(long gas) + { + base.ReportOperationRemainingGas(gas); + _remainingGas = gas > 0 ? gas : 0; + } + + public override void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) + { + OnExit(gas, deployedCode); + base.ReportActionEnd(gas, deploymentAddress, deployedCode); + } + + public override void ReportActionEnd(long gas, ReadOnlyMemory output) + { + OnExit(gas, output); + base.ReportActionEnd(gas, output); + } + + public override void ReportActionError(EvmExceptionType evmExceptionType) + { + _error = evmExceptionType; + OnExit(_remainingGas, null, _error); + base.ReportActionError(evmExceptionType); + } + + public override void ReportActionRevert(long gas, ReadOnlyMemory output) + { + _error = EvmExceptionType.Revert; + OnExit(gas, output, _error); + base.ReportActionRevert(gas, output); + } + + public override void ReportSelfDestruct(Address address, UInt256 balance, Address refundAddress) + { + base.ReportSelfDestruct(address, balance, refundAddress); + if (!_config.OnlyTopCall && _callStack.Count > 0) + { + NativeCallTracerCallFrame callFrame = new NativeCallTracerCallFrame + { + Type = Instruction.SELFDESTRUCT, + From = address, + To = refundAddress, + Value = balance + }; + _callStack[^1].Calls.Add(callFrame); + } + } + + public override void MarkAsSuccess(Address recipient, GasConsumed gasSpent, byte[] output, LogEntry[] logs, Hash256? stateRoot = null) + { + base.MarkAsSuccess(recipient, gasSpent, output, logs, stateRoot); + NativeCallTracerCallFrame firstCallFrame = _callStack[0]; + firstCallFrame.GasUsed = gasSpent.SpentGas; + firstCallFrame.Output = new ArrayPoolList(output); + } + + public override void MarkAsFailed(Address recipient, GasConsumed gasSpent, byte[] output, string? error, Hash256? stateRoot = null) + { + base.MarkAsFailed(recipient, gasSpent, output, error, stateRoot); + NativeCallTracerCallFrame firstCallFrame = _callStack[0]; + firstCallFrame.GasUsed = gasSpent.SpentGas; + if (output is not null) + firstCallFrame.Output = new ArrayPoolList(output); + + EvmExceptionType errorType = _error!.Value; + firstCallFrame.Error = errorType.GetEvmExceptionDescription(); + if (errorType == EvmExceptionType.Revert && error is not TransactionSubstate.Revert) + { + firstCallFrame.RevertReason = ValidateRevertReason(error); + } + + if (_config.WithLog) + { + ClearFailedLogs(firstCallFrame, false); + } + } + + private void OnExit(long gas, ReadOnlyMemory? output, EvmExceptionType? error = null) + { + if (!_config.OnlyTopCall && Depth > 0) + { + NativeCallTracerCallFrame callFrame = _callStack[^1]; + + int size = _callStack.Count; + if (size > 1) + { + _callStack.RemoveAt(size - 1); + callFrame.GasUsed = callFrame.Gas - gas; + + ProcessOutput(callFrame, output, error); + + _callStack[^1].Calls.Add(callFrame); + } + } + } + + private static void ProcessOutput(NativeCallTracerCallFrame callFrame, ReadOnlyMemory? output, EvmExceptionType? error) + { + if (error is not null) + { + callFrame.Error = error.Value.GetEvmExceptionDescription(); + if (callFrame.Type is Instruction.CREATE or Instruction.CREATE2) + { + callFrame.To = null; + } + + if (error == EvmExceptionType.Revert && output?.Length != 0) + { + ArrayPoolList? outputList = output?.Span.ToPooledList(); + callFrame.Output = outputList; + + if (outputList?.Count >= 4) + { + ProcessRevertReason(callFrame, output!.Value); + } + } + } + else + { + callFrame.Output = output?.Span.ToPooledList(); + } + } + + private static void ProcessRevertReason(NativeCallTracerCallFrame callFrame, ReadOnlyMemory output) + { + ReadOnlySpan span = output.Span; + string errorMessage; + try + { + errorMessage = TransactionSubstate.GetErrorMessage(span)!; + } + catch + { + errorMessage = TransactionSubstate.EncodeErrorMessage(span); + } + callFrame.RevertReason = ValidateRevertReason(errorMessage); + } + + private static void ClearFailedLogs(NativeCallTracerCallFrame callFrame, bool parentFailed) + { + bool failed = callFrame.Error is not null || parentFailed; + if (failed) + { + callFrame.Logs = null; + } + + foreach (NativeCallTracerCallFrame childCallFrame in callFrame.Calls.AsSpan()) + { + ClearFailedLogs(childCallFrame, failed); + } + } + + private static string? ValidateRevertReason(string? errorMessage) => + errorMessage?.StartsWith("0x") == false ? errorMessage : null; + + public void CaptureArbitrumTransfer(Address? from, Address? to, UInt256 value, bool before, BalanceChangeReason reason) + { + if (_callStack.Count == 0) + return; + + ArbitrumTransfer transfer = new(reason.ToString(), from, to, value); + + ArbitrumNativeCallFrame callFrame = _callStack[^1]; + + if (before) + callFrame.BeforeEvmTransfers.Add(transfer); + else + callFrame.AfterEvmTransfers.Add(transfer); + } + + public void CaptureArbitrumStorageGet(UInt256 index, int depth, bool before) + { + } + + public void CaptureArbitrumStorageSet(UInt256 index, ValueHash256 value, int depth, bool before) + { + } + + public void CaptureStylusHostio(string name, ReadOnlySpan args, ReadOnlySpan outs, ulong startInk, ulong endInk) + { + } +} diff --git a/src/Nethermind.Arbitrum/Tracing/ArbitrumGethStyleTracer.cs b/src/Nethermind.Arbitrum/Tracing/ArbitrumGethStyleTracer.cs new file mode 100644 index 000000000..1ac69e384 --- /dev/null +++ b/src/Nethermind.Arbitrum/Tracing/ArbitrumGethStyleTracer.cs @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Blockchain; +using Nethermind.Blockchain.Blocks; +using Nethermind.Blockchain.Find; +using Nethermind.Blockchain.Receipts; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Blockchain.Tracing.GethStyle.Custom.JavaScript; +using Nethermind.Blockchain.Tracing.GethStyle.Custom.Native; +using Nethermind.Consensus.Processing; +using Nethermind.Consensus.Tracing; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Evm.State; +using Nethermind.Evm.Tracing; +using Nethermind.Evm.TransactionProcessing; +using Nethermind.State.OverridableEnv; +using System.IO.Abstractions; +using Nethermind.Core.Extensions; +using Nethermind.Crypto; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.Arbitrum.Tracing; + +public class ArbitrumGethStyleTracer( + IReceiptStorage receiptStorage, + IBlockTree blockTree, + IBadBlockStore badBlockStore, + ISpecProvider specProvider, + ChangeableTransactionProcessorAdapter transactionProcessorAdapter, + IFileSystem fileSystem, + IOverridableEnv blockProcessingEnv +) : IGethStyleTracer +{ + public GethLikeTxTrace Trace(Hash256 blockHash, int txIndex, GethTraceOptions options, CancellationToken cancellationToken) + { + Block block = blockTree.FindBlock(blockHash, BlockTreeLookupOptions.None) ?? throw new InvalidOperationException($"No historical block found for {blockHash}"); + if (txIndex > block.Transactions.Length - 1) throw new InvalidOperationException($"Block {blockHash} has only {block.Transactions.Length} transactions and the requested tx index was {txIndex}"); + + return TraceImpl(block, block.Transactions[txIndex].Hash, cancellationToken, options)!; + } + + public GethLikeTxTrace Trace(Rlp blockRlp, Hash256 txHash, GethTraceOptions options, CancellationToken cancellationToken) + { + return TraceBlockImpl(GetBlockToTrace(blockRlp), options with { TxHash = txHash }, cancellationToken).FirstOrDefault()!; + } + + public GethLikeTxTrace Trace(Block block, Hash256 txHash, GethTraceOptions options, CancellationToken cancellationToken) + { + return TraceBlockImpl(block, options with { TxHash = txHash }, cancellationToken).FirstOrDefault()!; + } + + public GethLikeTxTrace? Trace(BlockParameter blockParameter, Transaction tx, GethTraceOptions options, CancellationToken cancellationToken) + { + 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); + + try + { + return TraceImpl(block, tx.Hash, cancellationToken, options, ProcessingOptions.TraceTransactions); + } + finally + { + transactionProcessorAdapter.CurrentAdapter = currentAdapter; + } + } + + public GethLikeTxTrace Trace(Hash256 txHash, GethTraceOptions traceOptions, CancellationToken cancellationToken) + { + Hash256? blockHash = receiptStorage.FindBlockHash(txHash); + if (blockHash is null) + { + return null!; + } + + Block? block = blockTree.FindBlock(blockHash, BlockTreeLookupOptions.RequireCanonical); + if (block is null) + { + return null!; + } + + return TraceImpl(block, txHash, cancellationToken, traceOptions)!; + } + + public GethLikeTxTrace Trace(long blockNumber, int txIndex, GethTraceOptions options, CancellationToken cancellationToken) + { + Block block = blockTree.FindBlock(blockNumber, BlockTreeLookupOptions.RequireCanonical) ?? throw new InvalidOperationException($"No historical block found for {blockNumber}"); + if (txIndex > block.Transactions.Length - 1) throw new InvalidOperationException($"Block {blockNumber} has only {block.Transactions.Length} transactions and the requested tx index was {txIndex}"); + + return TraceImpl(block, block.Transactions[txIndex].Hash, cancellationToken, options)!; + } + + public GethLikeTxTrace Trace(long blockNumber, Transaction tx, GethTraceOptions options, CancellationToken cancellationToken) + { + Block block = blockTree.FindBlock(blockNumber, BlockTreeLookupOptions.RequireCanonical) ?? throw new InvalidOperationException($"No historical block found for {blockNumber}"); + if (tx.Hash is null) throw new InvalidOperationException("Cannot trace transactions without tx hash set."); + + block = block.WithReplacedBodyCloned(BlockBody.WithOneTransactionOnly(tx)); + using var scope = blockProcessingEnv.BuildAndOverride(block.Header, options.StateOverrides); + IBlockTracer blockTracer = CreateOptionsTracer(block.Header, options with { TxHash = tx.Hash }, scope.Component.WorldState, specProvider); + try + { + scope.Component.BlockchainProcessor.Process(block, ProcessingOptions.Trace, blockTracer.WithCancellation(cancellationToken), cancellationToken); + return blockTracer.BuildResult().SingleOrDefault()!; + } + catch + { + blockTracer.TryDispose(); + throw; + } + } + + public IReadOnlyCollection TraceBlock(BlockParameter blockParameter, GethTraceOptions options, CancellationToken cancellationToken) + { + var block = blockTree.FindBlock(blockParameter); + + return TraceBlockImpl(block, options, cancellationToken); + } + + public IReadOnlyCollection TraceBlock(Rlp blockRlp, GethTraceOptions options, CancellationToken cancellationToken) + { + return TraceBlockImpl(GetBlockToTrace(blockRlp), options, cancellationToken); + } + + public IReadOnlyCollection TraceBlock(Block block, GethTraceOptions options, CancellationToken cancellationToken) + { + return TraceBlockImpl(block, options, cancellationToken); + } + + public IEnumerable TraceBlockToFile(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(blockHash); + ArgumentNullException.ThrowIfNull(options); + + var block = blockTree.FindBlock(blockHash) ?? throw new InvalidOperationException($"No historical block found for {blockHash}"); + var parent = FindParent(block); + + using var scope = blockProcessingEnv.BuildAndOverride(parent, options.StateOverrides); + var tracer = new GethLikeBlockFileTracer(block, options, fileSystem); + scope.Component.BlockchainProcessor.Process(block, ProcessingOptions.Trace, tracer.WithCancellation(cancellationToken), cancellationToken); + + return tracer.FileNames; + } + + public IEnumerable TraceBadBlockToFile(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(blockHash); + ArgumentNullException.ThrowIfNull(options); + + var block = badBlockStore + .GetAll() + .FirstOrDefault(b => b.Hash == blockHash) + ?? throw new InvalidOperationException($"No historical block found for {blockHash}"); + var parent = FindParent(block); + using var scope = blockProcessingEnv.BuildAndOverride(parent, options.StateOverrides); + var tracer = new GethLikeBlockFileTracer(block, options, fileSystem); + scope.Component.BlockchainProcessor.Process(block, ProcessingOptions.Trace, tracer.WithCancellation(cancellationToken), cancellationToken); + + return tracer.FileNames; + } + + private GethLikeTxTrace? TraceImpl(Block block, Hash256? txHash, CancellationToken cancellationToken, GethTraceOptions options, + ProcessingOptions processingOptions = ProcessingOptions.Trace) + { + ArgumentNullException.ThrowIfNull(txHash); + + // Previously, when the processing options is not `TraceTransaction`, the base block is the parent of the block + // which is set by the `BranchProcessor`, which mean the state override probably does not take affect. + // However, when it is `TraceTransaction`, it applies `ForceSameBlock` to `BlockchainProcessor`, which will send the same + // block as the baseBlock, which is important as the stateroot of the baseblock is modified in `BuildAndOverride`. + // + // Wild stuff! + BlockHeader baseBlockHeader = block.Header; + if ((processingOptions & ProcessingOptions.ForceSameBlock) == 0) + { + baseBlockHeader = FindParent(block)!; + } + + using var scope = blockProcessingEnv.BuildAndOverride(baseBlockHeader, options.StateOverrides); + IBlockTracer tracer = CreateOptionsTracer(block.Header, options with { TxHash = txHash }, scope.Component.WorldState, specProvider); + + try + { + scope.Component.BlockchainProcessor.Process(block, processingOptions, tracer.WithCancellation(cancellationToken), cancellationToken); + return tracer.BuildResult().SingleOrDefault(); + } + catch + { + tracer.TryDispose(); + throw; + } + } + + public static IBlockTracer CreateOptionsTracer(BlockHeader block, GethTraceOptions options, IWorldState worldState, ISpecProvider specProvider) => + options switch + { + { Tracer: var t } when GethLikeNativeTracerFactory.IsNativeTracer(t) => new GethLikeBlockNativeTracer(options.TxHash, (b, tx) => GethLikeNativeTracerFactory.CreateTracer(options, b, tx, worldState)), + { Tracer.Length: > 0 } => new GethLikeBlockJavaScriptTracer(worldState, specProvider.GetSpec(block), options), + _ => new GethLikeBlockMemoryTracer(options), + }; + + private IReadOnlyCollection TraceBlockImpl(Block? block, GethTraceOptions options, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(block); + + var parent = FindParent(block); + using var scope = blockProcessingEnv.BuildAndOverride(parent, options.StateOverrides); + + //IBlockTracer tracer = CreateOptionsTracer(block.Header, options, scope.Component.WorldState, specProvider); + + IBlockTracer tracer = new ArbitrumGethLikeNativeBlockTracer(options); + + try + { + scope.Component.BlockchainProcessor.Process(block, ProcessingOptions.Trace, tracer.WithCancellation(cancellationToken), cancellationToken); + return new GethLikeTxTraceCollection(tracer.BuildResult()); + } + catch + { + tracer.TryDispose(); + throw; + } + } + + private BlockHeader? FindParent(Block block) + { + BlockHeader? parent = null; + + if (!block.IsGenesis) + { + parent = blockTree.FindParentHeader(block.Header, BlockTreeLookupOptions.None); + + if (parent?.Hash is null) + throw new InvalidOperationException("Cannot trace blocks with invalid parents"); + } + + return parent; + } + + private static Block GetBlockToTrace(Rlp blockRlp) + { + Block block = Rlp.Decode(blockRlp); + if (block.TotalDifficulty is null) + { + block.Header.TotalDifficulty = 1; + } + + return block; + } + + public record BlockProcessingComponents(IWorldState WorldState, BlockchainProcessorFacade BlockchainProcessor); +}