diff --git a/src/Nethermind b/src/Nethermind index e96d4ecce..859c0ec27 160000 --- a/src/Nethermind +++ b/src/Nethermind @@ -1 +1 @@ -Subproject commit e96d4ecceca05dcf6fefab6e78f72b339ada80d9 +Subproject commit 859c0ec2744be48f9902b058827289edf647a574 diff --git a/src/Nethermind.Arbitrum.Test/Tracing/GasDimensionSerializationTests.cs b/src/Nethermind.Arbitrum.Test/Tracing/GasDimensionSerializationTests.cs new file mode 100644 index 000000000..b660e78e7 --- /dev/null +++ b/src/Nethermind.Arbitrum.Test/Tracing/GasDimensionSerializationTests.cs @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Text.Json; +using Nethermind.Arbitrum.Evm; +using Nethermind.Arbitrum.Tracing; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm; +using Nethermind.Evm.TransactionProcessing; +using Nethermind.Serialization.Json; +using NUnit.Framework; + +namespace Nethermind.Arbitrum.Test.Tracing; + +[TestFixture] +public class GasDimensionSerializationTests +{ + private readonly EthereumJsonSerializer _serializer = new(); + + private static void AssertJsonEquals(string actual, string expected) + { + Assert.That( + JsonElement.DeepEquals( + JsonDocument.Parse(actual).RootElement, + JsonDocument.Parse(expected).RootElement), + $"JSON mismatch.\nActual: {actual}\nExpected: {expected}"); + } + + [Test] + public void Serialize_DimensionLogWithAllFields_IncludesAllProperties() + { + DimensionLog log = new() + { + Pc = 123, + Op = "ADD", + Depth = 1, + OneDimensionalGasCost = 3, + Computation = 5, + StateAccess = 100, + StateGrowth = 20000, + HistoryGrowth = 50 + }; + + string json = _serializer.Serialize(log); + + const string expected = """ + { + "pc": 123, + "op": "ADD", + "depth": 1, + "cost": 3, + "cpu": 5, + "rw": 100, + "growth": 20000, + "history": 50 + } + """; + + AssertJsonEquals(json, expected); + } + + [Test] + public void Serialize_DimensionLogWithZeroOptional_OmitsZeroFields() + { + DimensionLog log = new() + { + Pc = 0, + Op = "ADD", + Depth = 1, + OneDimensionalGasCost = 3, + Computation = 5, + StateAccess = 0, + StateGrowth = 0, + HistoryGrowth = 0 + }; + + string json = _serializer.Serialize(log); + + const string expected = """ + { + "pc": 0, + "op": "ADD", + "depth": 1, + "cost": 3, + "cpu": 5 + } + """; + + AssertJsonEquals(json, expected); + } + + [Test] + public void Serialize_TxGasDimensionResult_ProducesExpectedJson() + { + Hash256 txHash = TestItem.KeccakA; + Transaction tx = Build.A.Transaction.WithHash(txHash).TestObject; + Block block = Build.A.Block.WithNumber(100).WithTimestamp(1704067200).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, GethTraceOptions.Default with { Tracer = TxGasDimensionLoggerTracer.TracerName }); + + MultiGas gasBefore = default; + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 10); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.ADD, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.SetIntrinsicGas(21000); + tracer.SetPosterGas(1000); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(22003, 22003), [], []); + + GethLikeTxTrace result = tracer.BuildResult(); + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + string json = _serializer.Serialize(dimensionResult); + + string expected = $$""" + { + "gasUsed": 22003, + "gasUsedForL1": 1000, + "gasUsedForL2": 21003, + "intrinsicGas": 21000, + "adjustedRefund": 0, + "rootIsPrecompile": false, + "rootIsPrecompileAdjustment": 0, + "rootIsStylus": false, + "rootIsStylusAdjustment": 0, + "failed": false, + "txHash": "{{txHash}}", + "blockTimestamp": 1704067200, + "blockNumber": 100, + "status": 1, + "dim": [ + { + "pc": 0, + "op": "ADD", + "depth": 1, + "cost": 10, + "cpu": 10 + } + ] + } + """; + + AssertJsonEquals(json, expected); + } + + [Test] + public void Serialize_TxGasDimensionByOpcodeResult_ProducesExpectedJson() + { + Hash256 txHash = TestItem.KeccakA; + Transaction tx = Build.A.Transaction.WithHash(txHash).TestObject; + Block block = Build.A.Block.WithNumber(100).WithTimestamp(1704067200).TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, GethTraceOptions.Default with { Tracer = TxGasDimensionByOpcodeTracer.TracerName }); + + MultiGas gasBefore = default; + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 10); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.ADD, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(21003, 21003), [], []); + + GethLikeTxTrace result = tracer.BuildResult(); + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)result.CustomTracerResult!.Value; + string json = _serializer.Serialize(dimensionResult); + + string expected = $$""" + { + "gasUsed": 21003, + "gasUsedForL1": 0, + "gasUsedForL2": 21003, + "intrinsicGas": 0, + "adjustedRefund": 0, + "rootIsPrecompile": false, + "rootIsPrecompileAdjustment": 0, + "rootIsStylus": false, + "rootIsStylusAdjustment": 0, + "failed": false, + "txHash": "{{txHash}}", + "blockTimestamp": 1704067200, + "blockNumber": 100, + "status": 1, + "dimensions": { + "ADD": { + "gas1d": 10, + "cpu": 10 + } + } + } + """; + + AssertJsonEquals(json, expected); + } + + [Test] + public void Serialize_TxGasDimensionByOpcodeResultWithMultipleOpcodes_PreservesUppercaseKeys() + { + Hash256 txHash = TestItem.KeccakA; + Transaction tx = Build.A.Transaction.WithHash(txHash).TestObject; + Block block = Build.A.Block.WithNumber(100).WithTimestamp(1704067200).TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, GethTraceOptions.Default with { Tracer = TxGasDimensionByOpcodeTracer.TracerName }); + + MultiGas gasBefore = default; + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 10); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.ADD, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.BeginGasDimensionCapture(pc: 1, Instruction.SSTORE, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(26003, 26003), [], []); + + GethLikeTxTrace result = tracer.BuildResult(); + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)result.CustomTracerResult!.Value; + string json = _serializer.Serialize(dimensionResult); + + string expected = $$""" + { + "gasUsed": 26003, + "gasUsedForL1": 0, + "gasUsedForL2": 26003, + "intrinsicGas": 0, + "adjustedRefund": 0, + "rootIsPrecompile": false, + "rootIsPrecompileAdjustment": 0, + "rootIsStylus": false, + "rootIsStylusAdjustment": 0, + "failed": false, + "txHash": "{{txHash}}", + "blockTimestamp": 1704067200, + "blockNumber": 100, + "status": 1, + "dimensions": { + "ADD": { + "gas1d": 10, + "cpu": 10 + }, + "SSTORE": { + "gas1d": 10, + "cpu": 10 + } + } + } + """; + + AssertJsonEquals(json, expected); + } + + [Test] + public void Serialize_GasDimensionBreakdownWithAllFields_IncludesAllProperties() + { + GasDimensionBreakdown breakdown = new() + { + OneDimensionalGasCost = 100, + Computation = 50, + StateAccess = 30, + StateGrowth = 15, + HistoryGrowth = 5 + }; + + string json = _serializer.Serialize(breakdown); + + const string expected = """ + { + "gas1d": 100, + "cpu": 50, + "rw": 30, + "growth": 15, + "hist": 5 + } + """; + + AssertJsonEquals(json, expected); + } + + [Test] + public void Serialize_GasDimensionBreakdownWithZeroOptional_OmitsZeroFields() + { + GasDimensionBreakdown breakdown = new() + { + OneDimensionalGasCost = 100, + Computation = 50, + StateAccess = 0, + StateGrowth = 0, + HistoryGrowth = 0 + }; + + string json = _serializer.Serialize(breakdown); + + const string expected = """ + { + "gas1d": 100, + "cpu": 50 + } + """; + + AssertJsonEquals(json, expected); + } +} diff --git a/src/Nethermind.Arbitrum.Test/Tracing/GasDimensionTracerIntegrationTests.cs b/src/Nethermind.Arbitrum.Test/Tracing/GasDimensionTracerIntegrationTests.cs new file mode 100644 index 000000000..be26b7061 --- /dev/null +++ b/src/Nethermind.Arbitrum.Test/Tracing/GasDimensionTracerIntegrationTests.cs @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using FluentAssertions; +using Nethermind.Arbitrum.Test.Infrastructure; +using Nethermind.Arbitrum.Tracing; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Core; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm; +using Nethermind.Evm.State; +using Nethermind.Evm.TransactionProcessing; + +namespace Nethermind.Arbitrum.Test.Tracing; + +[TestFixture] +public class GasDimensionTracerIntegrationTests +{ + [Test] + public void TraceTransaction_SimpleTransfer_CapturesGasDimensions() + { + ArbitrumRpcTestBlockchain chain = ArbitrumRpcTestBlockchain.CreateDefault(builder => + { + builder.AddScoped(new ArbitrumTestBlockchainBase.Configuration + { + SuggestGenesisOnStart = true, + FillWithTestDataOnStart = true + }); + }); + + BlockExecutionContext blCtx = new(chain.BlockTree.Head!.Header, chain.SpecProvider.GenesisSpec); + chain.TxProcessor.SetBlockExecutionContext(in blCtx); + + IWorldState worldState = chain.MainWorldState; + using IDisposable _ = worldState.BeginScope(chain.BlockTree.Head!.Header); + + Address sender = TestItem.AddressA; + Address receiver = TestItem.AddressB; + Transaction tx = Build.A.Transaction + .WithTo(receiver) + .WithValue(1_000_000) + .WithGasLimit(21_000) + .WithMaxFeePerGas(1_000_000_000) + .WithMaxPriorityFeePerGas(100_000_000) + .WithNonce(worldState.GetNonce(sender)) + .WithSenderAddress(sender) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Block block = chain.BlockTree.Head!; + TxGasDimensionLoggerTracer tracer = new(tx, block, GethTraceOptions.Default with { Tracer = TxGasDimensionLoggerTracer.TracerName }); + TransactionResult result = chain.TxProcessor.Execute(tx, tracer); + + result.Should().Be(TransactionResult.Ok); + + GethLikeTxTrace trace = tracer.BuildResult(); + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)trace.CustomTracerResult!.Value; + + dimensionResult.GasUsed.Should().Be(21000); + dimensionResult.Status.Should().Be(1); + dimensionResult.Failed.Should().BeFalse(); + } + + [Test] + public void TraceTransaction_ContractCall_CapturesMultipleOpcodes() + { + ArbitrumRpcTestBlockchain chain = ArbitrumRpcTestBlockchain.CreateDefault(builder => + { + builder.AddScoped(new ArbitrumTestBlockchainBase.Configuration + { + SuggestGenesisOnStart = true, + FillWithTestDataOnStart = true + }); + }); + + BlockExecutionContext blCtx = new(chain.BlockTree.Head!.Header, chain.SpecProvider.GenesisSpec); + chain.TxProcessor.SetBlockExecutionContext(in blCtx); + + IWorldState worldState = chain.MainWorldState; + using IDisposable _ = worldState.BeginScope(chain.BlockTree.Head!.Header); + + byte[] runtimeCode = Prepare.EvmCode + .PushData(1) + .PushData(2) + .Op(Instruction.ADD) + .Op(Instruction.POP) + .Op(Instruction.STOP) + .Done; + + Address contractAddress = new("0x0000000000000000000000000000000000000200"); + worldState.CreateAccount(contractAddress, 0); + worldState.InsertCode(contractAddress, runtimeCode, chain.SpecProvider.GenesisSpec); + worldState.Commit(chain.SpecProvider.GenesisSpec); + + Address sender = TestItem.AddressA; + Transaction tx = Build.A.Transaction + .WithTo(contractAddress) + .WithValue(0) + .WithGasLimit(100_000) + .WithMaxFeePerGas(1_000_000_000) + .WithMaxPriorityFeePerGas(100_000_000) + .WithNonce(worldState.GetNonce(sender)) + .WithSenderAddress(sender) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Block block = chain.BlockTree.Head!; + TxGasDimensionLoggerTracer tracer = new(tx, block, GethTraceOptions.Default with { Tracer = TxGasDimensionLoggerTracer.TracerName }); + TransactionResult result = chain.TxProcessor.Execute(tx, tracer); + + result.Should().Be(TransactionResult.Ok); + + GethLikeTxTrace trace = tracer.BuildResult(); + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)trace.CustomTracerResult!.Value; + + dimensionResult.DimensionLogs.Should().HaveCount(5); + dimensionResult.DimensionLogs.Should().Contain(log => log.Op == "ADD"); + dimensionResult.DimensionLogs.Should().Contain(log => log.Op == "POP"); + dimensionResult.DimensionLogs.Should().Contain(log => log.Op == "STOP"); + } + + [Test] + public void TraceTransaction_ByOpcodeTracer_AggregatesByOpcodeType() + { + ArbitrumRpcTestBlockchain chain = ArbitrumRpcTestBlockchain.CreateDefault(builder => + { + builder.AddScoped(new ArbitrumTestBlockchainBase.Configuration + { + SuggestGenesisOnStart = true, + FillWithTestDataOnStart = true + }); + }); + + BlockExecutionContext blCtx = new(chain.BlockTree.Head!.Header, chain.SpecProvider.GenesisSpec); + chain.TxProcessor.SetBlockExecutionContext(in blCtx); + + IWorldState worldState = chain.MainWorldState; + using IDisposable _ = worldState.BeginScope(chain.BlockTree.Head!.Header); + + byte[] runtimeCode = Prepare.EvmCode + .PushData(1) + .PushData(2) + .Op(Instruction.ADD) + .PushData(3) + .Op(Instruction.ADD) + .Op(Instruction.POP) + .Op(Instruction.STOP) + .Done; + + Address contractAddress = new("0x0000000000000000000000000000000000000201"); + worldState.CreateAccount(contractAddress, 0); + worldState.InsertCode(contractAddress, runtimeCode, chain.SpecProvider.GenesisSpec); + worldState.Commit(chain.SpecProvider.GenesisSpec); + + Address sender = TestItem.AddressA; + Transaction tx = Build.A.Transaction + .WithTo(contractAddress) + .WithValue(0) + .WithGasLimit(100_000) + .WithMaxFeePerGas(1_000_000_000) + .WithMaxPriorityFeePerGas(100_000_000) + .WithNonce(worldState.GetNonce(sender)) + .WithSenderAddress(sender) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Block block = chain.BlockTree.Head!; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, GethTraceOptions.Default with { Tracer = TxGasDimensionByOpcodeTracer.TracerName }); + TransactionResult result = chain.TxProcessor.Execute(tx, tracer); + + result.Should().Be(TransactionResult.Ok); + + GethLikeTxTrace trace = tracer.BuildResult(); + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)trace.CustomTracerResult!.Value; + + dimensionResult.Dimensions.Should().ContainKey("ADD"); + GasDimensionBreakdown addBreakdown = dimensionResult.Dimensions["ADD"]; + addBreakdown.OneDimensionalGasCost.Should().Be(6); + } + + [Test] + public void TraceTransaction_FailedTransaction_SetsFailed() + { + ArbitrumRpcTestBlockchain chain = ArbitrumRpcTestBlockchain.CreateDefault(builder => + { + builder.AddScoped(new ArbitrumTestBlockchainBase.Configuration + { + SuggestGenesisOnStart = true, + FillWithTestDataOnStart = true + }); + }); + + BlockExecutionContext blCtx = new(chain.BlockTree.Head!.Header, chain.SpecProvider.GenesisSpec); + chain.TxProcessor.SetBlockExecutionContext(in blCtx); + + IWorldState worldState = chain.MainWorldState; + using IDisposable _ = worldState.BeginScope(chain.BlockTree.Head!.Header); + + byte[] runtimeCode = Prepare.EvmCode + .PushData(0) + .PushData(0) + .Op(Instruction.REVERT) + .Done; + + Address contractAddress = new("0x0000000000000000000000000000000000000202"); + worldState.CreateAccount(contractAddress, 0); + worldState.InsertCode(contractAddress, runtimeCode, chain.SpecProvider.GenesisSpec); + worldState.Commit(chain.SpecProvider.GenesisSpec); + + Address sender = TestItem.AddressA; + Transaction tx = Build.A.Transaction + .WithTo(contractAddress) + .WithValue(0) + .WithGasLimit(100_000) + .WithMaxFeePerGas(1_000_000_000) + .WithMaxPriorityFeePerGas(100_000_000) + .WithNonce(worldState.GetNonce(sender)) + .WithSenderAddress(sender) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Block block = chain.BlockTree.Head!; + TxGasDimensionLoggerTracer tracer = new(tx, block, GethTraceOptions.Default with { Tracer = TxGasDimensionLoggerTracer.TracerName }); + TransactionResult result = chain.TxProcessor.Execute(tx, tracer); + + result.Should().Be(TransactionResult.Ok); + + GethLikeTxTrace trace = tracer.BuildResult(); + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)trace.CustomTracerResult!.Value; + + dimensionResult.Failed.Should().BeTrue(); + dimensionResult.Status.Should().Be(0); + } +} diff --git a/src/Nethermind.Arbitrum.Test/Tracing/TxGasDimensionByOpcodeTracerTests.cs b/src/Nethermind.Arbitrum.Test/Tracing/TxGasDimensionByOpcodeTracerTests.cs new file mode 100644 index 000000000..908147608 --- /dev/null +++ b/src/Nethermind.Arbitrum.Test/Tracing/TxGasDimensionByOpcodeTracerTests.cs @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using FluentAssertions; +using Nethermind.Arbitrum.Evm; +using Nethermind.Arbitrum.Tracing; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm; +using Nethermind.Evm.TransactionProcessing; + +namespace Nethermind.Arbitrum.Test.Tracing; + +[TestFixture] +public class TxGasDimensionByOpcodeTracerTests +{ + private static GethTraceOptions DefaultOptions => GethTraceOptions.Default with { Tracer = TxGasDimensionByOpcodeTracer.TracerName }; + + [Test] + public void CaptureGasDimension_SameOpcodeTwice_AggregatesGas() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gas1Before = default; + MultiGas gas1After = default; + gas1After.Increment(ResourceKind.Computation, 5); + + MultiGas gas2Before = default; + gas2Before.Increment(ResourceKind.Computation, 5); + MultiGas gas2After = default; + gas2After.Increment(ResourceKind.Computation, 12); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.ADD, depth: 1, gas1Before); + tracer.EndGasDimensionCapture(gas1After); + tracer.BeginGasDimensionCapture(pc: 5, Instruction.ADD, depth: 1, gas2Before); + tracer.EndGasDimensionCapture(gas2After); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(21006, 21006), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)result.CustomTracerResult!.Value; + dimensionResult.Dimensions.Should().HaveCount(1); + dimensionResult.Dimensions.Should().ContainKey("ADD"); + + GasDimensionBreakdown addBreakdown = dimensionResult.Dimensions["ADD"]; + // gasCost = gas1After.Total - gas1Before.Total + gas2After.Total - gas2Before.Total = 5 + 7 = 12 + addBreakdown.OneDimensionalGasCost.Should().Be(12); + // Computation = gas1After.Computation - gas1Before.Computation + gas2After.Computation - gas2Before.Computation = 5 + 7 = 12 + addBreakdown.Computation.Should().Be(12); + } + + [Test] + public void CaptureGasDimension_DifferentOpcodes_CreatesSeparateEntries() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gasEmpty = default; + MultiGas gasComputation = default; + gasComputation.Increment(ResourceKind.Computation, 3); + MultiGas gasStorage = default; + gasStorage.Increment(ResourceKind.StorageAccess, 100); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.ADD, depth: 1, gasEmpty); + tracer.EndGasDimensionCapture(gasComputation); + tracer.BeginGasDimensionCapture(pc: 1, Instruction.SLOAD, depth: 1, gasComputation); + tracer.EndGasDimensionCapture(gasStorage); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(23103, 23103), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)result.CustomTracerResult!.Value; + dimensionResult.Dimensions.Should().HaveCount(2); + dimensionResult.Dimensions.Should().ContainKey("ADD"); + dimensionResult.Dimensions.Should().ContainKey("SLOAD"); + + // ADD: 3 (gasComputation.Total - gasEmpty.Total) + dimensionResult.Dimensions["ADD"].OneDimensionalGasCost.Should().Be(3); + // SLOAD: 100 - 3 = 97 (gasStorage.Total - gasComputation.Total) + dimensionResult.Dimensions["SLOAD"].OneDimensionalGasCost.Should().Be(97); + } + + [Test] + public void BuildResult_AfterCaptures_DimensionKeysAreUppercase() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gasBefore = default; + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 1); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.ADD, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.BeginGasDimensionCapture(pc: 1, Instruction.SSTORE, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.BeginGasDimensionCapture(pc: 2, Instruction.PUSH1, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(26006, 26006), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)result.CustomTracerResult!.Value; + foreach (string key in dimensionResult.Dimensions.Keys) + key.Should().Be(key.ToUpperInvariant(), $"Key {key} should be uppercase"); + + dimensionResult.Dimensions.Should().ContainKey("ADD"); + dimensionResult.Dimensions.Should().ContainKey("SSTORE"); + dimensionResult.Dimensions.Should().ContainKey("PUSH1"); + } + + [Test] + public void IsTracingGasDimension_Always_ReturnsTrue() + { + Transaction tx = Build.A.Transaction.TestObject; + Block block = Build.A.Block.TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, DefaultOptions); + + tracer.IsTracingGasDimension.Should().BeTrue(); + } + + [Test] + public void SetIntrinsicAndPosterGas_WithValidValues_UpdatesL1L2Gas() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, DefaultOptions); + + tracer.SetIntrinsicGas(21000); + tracer.SetPosterGas(3000); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(28000, 28000), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)result.CustomTracerResult!.Value; + dimensionResult.IntrinsicGas.Should().Be(21000); + dimensionResult.GasUsed.Should().Be(28000); + dimensionResult.GasUsedForL1.Should().Be(3000); + dimensionResult.GasUsedForL2.Should().Be(25000); + } + + [Test] + public void BuildResult_WithFailure_SetsFailedTrue() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionByOpcodeTracer tracer = new(tx, block, DefaultOptions); + + tracer.MarkAsFailed(Address.Zero, new GasConsumed(21000, 21000), [], "execution reverted"); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionByOpcodeResult dimensionResult = (TxGasDimensionByOpcodeResult)result.CustomTracerResult!.Value; + dimensionResult.Failed.Should().BeTrue(); + dimensionResult.Status.Should().Be(0); + } +} diff --git a/src/Nethermind.Arbitrum.Test/Tracing/TxGasDimensionLoggerTracerTests.cs b/src/Nethermind.Arbitrum.Test/Tracing/TxGasDimensionLoggerTracerTests.cs new file mode 100644 index 000000000..af37f7ab8 --- /dev/null +++ b/src/Nethermind.Arbitrum.Test/Tracing/TxGasDimensionLoggerTracerTests.cs @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using FluentAssertions; +using Nethermind.Arbitrum.Evm; +using Nethermind.Arbitrum.Tracing; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm; +using Nethermind.Evm.TransactionProcessing; + +namespace Nethermind.Arbitrum.Test.Tracing; + +[TestFixture] +public class TxGasDimensionLoggerTracerTests +{ + private static GethTraceOptions DefaultOptions => GethTraceOptions.Default with { Tracer = TxGasDimensionLoggerTracer.TracerName }; + + [Test] + public void CaptureGasDimension_SingleOpcode_RecordsCorrectDelta() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gasBefore = default; + gasBefore.Increment(ResourceKind.Computation, 10); + + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 15); + gasAfter.Increment(ResourceKind.StorageAccess, 100); + + tracer.BeginGasDimensionCapture(pc: 5, Instruction.ADD, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(21000, 21000), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + dimensionResult.DimensionLogs.Should().HaveCount(1); + + DimensionLog log = dimensionResult.DimensionLogs[0]; + log.Pc.Should().Be(5); + log.Op.Should().Be("ADD"); + log.Depth.Should().Be(1); + log.OneDimensionalGasCost.Should().Be(105); // gasAfter.Total - gasBefore.Total = 115 - 10 = 105 + log.Computation.Should().Be(5); + log.StateAccess.Should().Be(100); + log.StateGrowth.Should().Be(0); + log.HistoryGrowth.Should().Be(0); + } + + [Test] + public void CaptureGasDimension_MultipleOpcodes_AccumulatesLogs() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gas1Before = default; + gas1Before.Increment(ResourceKind.Computation, 0); + MultiGas gas1After = default; + gas1After.Increment(ResourceKind.Computation, 3); + + MultiGas gas2Before = default; + gas2Before.Increment(ResourceKind.Computation, 3); + MultiGas gas2After = default; + gas2After.Increment(ResourceKind.Computation, 6); + gas2After.Increment(ResourceKind.StorageAccess, 100); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.PUSH1, depth: 1, gas1Before); + tracer.EndGasDimensionCapture(gas1After); + tracer.BeginGasDimensionCapture(pc: 2, Instruction.SLOAD, depth: 1, gas2Before); + tracer.EndGasDimensionCapture(gas2After); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(23103, 23103), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + dimensionResult.DimensionLogs.Should().HaveCount(2); + + dimensionResult.DimensionLogs[0].Op.Should().Be("PUSH1"); + dimensionResult.DimensionLogs[0].OneDimensionalGasCost.Should().Be(3); + + dimensionResult.DimensionLogs[1].Op.Should().Be("SLOAD"); + dimensionResult.DimensionLogs[1].OneDimensionalGasCost.Should().Be(103); // 106 - 3 = 103 + dimensionResult.DimensionLogs[1].StateAccess.Should().Be(100); + } + + [Test] + public void SetIntrinsicGas_Called_StoresValue() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + tracer.SetIntrinsicGas(21000); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(21000, 21000), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + dimensionResult.IntrinsicGas.Should().Be(21000); + } + + [Test] + public void SetPosterGas_Called_StoresValueAndAffectsL1L2Split() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + tracer.SetPosterGas(5000); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(25000, 25000), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + dimensionResult.GasUsed.Should().Be(25000); + dimensionResult.GasUsedForL1.Should().Be(5000); + dimensionResult.GasUsedForL2.Should().Be(20000); + } + + [Test] + public void BuildResult_AfterCaptures_ReturnsCorrectStructure() + { + Hash256 txHash = TestItem.KeccakA; + Transaction tx = Build.A.Transaction.WithHash(txHash).TestObject; + Block block = Build.A.Block.WithNumber(123).WithTimestamp(1704067200).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gasBefore = default; + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 10); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.STOP, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.SetIntrinsicGas(21000); + tracer.SetPosterGas(1000); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(22000, 22000), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + result.TxHash.Should().Be(txHash); + result.CustomTracerResult.Should().NotBeNull(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + dimensionResult.TxHash.Should().Be(txHash.ToString()); + dimensionResult.GasUsed.Should().Be(22000); + dimensionResult.GasUsedForL1.Should().Be(1000); + dimensionResult.GasUsedForL2.Should().Be(21000); + dimensionResult.IntrinsicGas.Should().Be(21000); + dimensionResult.BlockNumber.Should().Be(123); + dimensionResult.BlockTimestamp.Should().Be(1704067200); + dimensionResult.Status.Should().Be(1); + dimensionResult.Failed.Should().BeFalse(); + dimensionResult.DimensionLogs.Should().HaveCount(1); + } + + [Test] + public void BuildResult_WithFailure_SetsFailedTrue() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + tracer.MarkAsFailed(Address.Zero, new GasConsumed(21000, 21000), [], "execution reverted"); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + dimensionResult.Failed.Should().BeTrue(); + dimensionResult.Status.Should().Be(0); + } + + [Test] + public void BuildResult_AfterCaptures_OpcodeNamesAreUppercase() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gasBefore = default; + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 1); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.ADD, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.BeginGasDimensionCapture(pc: 1, Instruction.SSTORE, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.BeginGasDimensionCapture(pc: 2, Instruction.CALL, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.BeginGasDimensionCapture(pc: 3, Instruction.PUSH1, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(21000, 21000), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + foreach (DimensionLog log in dimensionResult.DimensionLogs) + log.Op.Should().Be(log.Op.ToUpperInvariant(), $"Opcode {log.Op} should be uppercase"); + + dimensionResult.DimensionLogs[0].Op.Should().Be("ADD"); + dimensionResult.DimensionLogs[1].Op.Should().Be("SSTORE"); + dimensionResult.DimensionLogs[2].Op.Should().Be("CALL"); + dimensionResult.DimensionLogs[3].Op.Should().Be("PUSH1"); + } + + [Test] + public void IsTracingGasDimension_Always_ReturnsTrue() + { + Transaction tx = Build.A.Transaction.TestObject; + Block block = Build.A.Block.TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + tracer.IsTracingGasDimension.Should().BeTrue(); + } + + [Test] + public void CaptureGasDimension_AllResourceKinds_RecordsCorrectly() + { + Transaction tx = Build.A.Transaction.WithHash(TestItem.KeccakA).TestObject; + Block block = Build.A.Block.WithNumber(100).TestObject; + TxGasDimensionLoggerTracer tracer = new(tx, block, DefaultOptions); + + MultiGas gasBefore = default; + MultiGas gasAfter = default; + gasAfter.Increment(ResourceKind.Computation, 10); + gasAfter.Increment(ResourceKind.StorageAccess, 20); + gasAfter.Increment(ResourceKind.StorageGrowth, 30); + gasAfter.Increment(ResourceKind.HistoryGrowth, 40); + + tracer.BeginGasDimensionCapture(pc: 0, Instruction.SSTORE, depth: 1, gasBefore); + tracer.EndGasDimensionCapture(gasAfter); + tracer.MarkAsSuccess(Address.Zero, new GasConsumed(26000, 26000), [], []); + GethLikeTxTrace result = tracer.BuildResult(); + + TxGasDimensionResult dimensionResult = (TxGasDimensionResult)result.CustomTracerResult!.Value; + DimensionLog log = dimensionResult.DimensionLogs[0]; + log.Computation.Should().Be(10); + log.StateAccess.Should().Be(20); + log.StateGrowth.Should().Be(30); + log.HistoryGrowth.Should().Be(40); + } +} diff --git a/src/Nethermind.Arbitrum/ArbitrumPlugin.cs b/src/Nethermind.Arbitrum/ArbitrumPlugin.cs index c9728e902..75ede7a86 100644 --- a/src/Nethermind.Arbitrum/ArbitrumPlugin.cs +++ b/src/Nethermind.Arbitrum/ArbitrumPlugin.cs @@ -37,6 +37,8 @@ using Nethermind.JsonRpc.Modules.Eth; using Nethermind.Serialization.Rlp; using Nethermind.Specs.ChainSpecStyle; +using Nethermind.Arbitrum.Tracing; +using Nethermind.Blockchain.Tracing.GethStyle.Custom.Native; namespace Nethermind.Arbitrum; @@ -67,6 +69,14 @@ public Task Init(INethermindApi api) if (_specHelper.Enabled) _jsonRpcConfig.EnabledModules = _jsonRpcConfig.EnabledModules.Append(Name).ToArray(); + // Register Arbitrum-specific tracers + GethLikeNativeTracerFactory.RegisterTracer( + TxGasDimensionLoggerTracer.TracerName, + static (options, block, tx, _) => new TxGasDimensionLoggerTracer(tx, block, options)); + GethLikeNativeTracerFactory.RegisterTracer( + TxGasDimensionByOpcodeTracer.TracerName, + static (options, block, tx, _) => new TxGasDimensionByOpcodeTracer(tx, block, options)); + return Task.CompletedTask; } diff --git a/src/Nethermind.Arbitrum/Evm/ArbitrumGasPolicy.cs b/src/Nethermind.Arbitrum/Evm/ArbitrumGasPolicy.cs index e12a02a11..351d4cb7a 100644 --- a/src/Nethermind.Arbitrum/Evm/ArbitrumGasPolicy.cs +++ b/src/Nethermind.Arbitrum/Evm/ArbitrumGasPolicy.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Runtime.CompilerServices; +using Nethermind.Arbitrum.Tracing; using Nethermind.Core; using Nethermind.Core.Extensions; using Nethermind.Core.Specs; @@ -23,6 +24,7 @@ public struct ArbitrumGasPolicy : IGasPolicy private MultiGas _accumulated; private MultiGas _retained; private ulong _initialGas; + private IArbitrumTxTracer? _tracer; /// /// Returns a readonly copy of the accumulated multi-gas breakdown. @@ -38,6 +40,16 @@ public readonly MultiGas GetTotalAccumulated() return underflow ? _accumulated.SaturatingSub(_retained) : result; } + /// + /// Sets the tracer for gas dimension capture. + /// The tracer stores before-state and computes gas dimension logs. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetTracer(ref ArbitrumGasPolicy gas, IArbitrumTxTracer? tracer) + { + gas._tracer = tracer; + } + /// /// Applies the final transaction refund to the accumulated MultiGas. /// Called at the transaction end after calculating the capped refund. @@ -317,6 +329,29 @@ public static void ConsumeDataCopyGas(ref ArbitrumGasPolicy gas, bool isExternal gas._accumulated.Increment(wordResource, (ulong)dataCost); } + /// + /// Hook called before instruction execution for gas dimension tracing. + /// Delegates to the tracer to capture pre-execution gas state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void OnBeforeInstructionTrace(in ArbitrumGasPolicy gas, int pc, Instruction instruction, int depth) + { + IArbitrumTxTracer? tracer = gas._tracer; + // Depth is 0-based from VmState.Env.CallDepth, convert to 1-based for Nitro compatibility + tracer?.BeginGasDimensionCapture(pc, instruction, depth + 1, gas.GetAccumulated()); + } + + /// + /// Hook called after instruction execution for gas dimension tracing. + /// Delegates to the tracer to capture post-execution gas state and emit dimension log. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void OnAfterInstructionTrace(in ArbitrumGasPolicy gas) + { + IArbitrumTxTracer? tracer = gas._tracer; + tracer?.EndGasDimensionCapture(gas.GetAccumulated()); + } + /// /// Returns the maximum of two gas values. /// Used for MinimalGas calculation in IntrinsicGas. diff --git a/src/Nethermind.Arbitrum/Evm/ArbitrumVirtualMachine.cs b/src/Nethermind.Arbitrum/Evm/ArbitrumVirtualMachine.cs index 71e10927d..64e783a09 100644 --- a/src/Nethermind.Arbitrum/Evm/ArbitrumVirtualMachine.cs +++ b/src/Nethermind.Arbitrum/Evm/ArbitrumVirtualMachine.cs @@ -375,9 +375,16 @@ public StylusEvmResult StylusCreate(ReadOnlyMemory initCode, in UInt256 en protected override CallResult RunByteCode(scoped ref EvmStack stack, scoped ref ArbitrumGasPolicy gas) { - return StylusCode.IsStylusProgram(VmState.Env.CodeInfo.CodeSpan) - ? RunWasmCode(ref gas) - : base.RunByteCode(ref stack, ref gas); + if (StylusCode.IsStylusProgram(VmState.Env.CodeInfo.CodeSpan)) + return RunWasmCode(ref gas); + + // Set the tracer on the gas struct for gas dimension capture. + // The tracer is used by ArbitrumGasPolicy hooks (OnBeforeInstructionTrace/OnAfterInstructionTrace) + // called from the base RunByteCode loop. + IArbitrumTxTracer? arbTracer = TxTracer.GetTracer(); + ArbitrumGasPolicy.SetTracer(ref gas, arbTracer); + + return base.RunByteCode(ref stack, ref gas); } protected override OpCode[] GenerateOpCodes(IReleaseSpec spec) diff --git a/src/Nethermind.Arbitrum/Execution/ArbitrumTransactionProcessor.cs b/src/Nethermind.Arbitrum/Execution/ArbitrumTransactionProcessor.cs index 42dd4ca56..c6a66be7e 100644 --- a/src/Nethermind.Arbitrum/Execution/ArbitrumTransactionProcessor.cs +++ b/src/Nethermind.Arbitrum/Execution/ArbitrumTransactionProcessor.cs @@ -66,7 +66,7 @@ protected override TransactionResult BuyGas(Transaction tx, IReleaseSpec spec, I { TransactionResult result = base.BuyGas(tx, spec, tracer, opts, in effectiveGasPrice, out premiumPerGas, out senderReservedGasPayment, out blobBaseFee); - IArbitrumTxTracer arbTracer = tracer as IArbitrumTxTracer ?? ArbNullTxTracer.Instance; + IArbitrumTxTracer arbTracer = tracer.GetTracer() ?? ArbNullTxTracer.Instance; if (result && arbTracer.IsTracingActions) { arbTracer.CaptureArbitrumTransfer(tx.SenderAddress, null, senderReservedGasPayment, true, @@ -82,7 +82,7 @@ public override TransactionResult Warmup(Transaction transaction, ITxTracer txTr protected override TransactionResult Execute(Transaction tx, ITxTracer tracer, ExecutionOptions opts) { _currentOpts = opts; - IArbitrumTxTracer arbTracer = tracer as IArbitrumTxTracer ?? ArbNullTxTracer.Instance; + IArbitrumTxTracer arbTracer = tracer.GetTracer() ?? ArbNullTxTracer.Instance; Snapshot snapshot = WorldState.TakeSnapshot(); @@ -166,7 +166,23 @@ protected override void PayFees(Transaction tx, BlockHeader header, IReleaseSpec } protected override TransactionResult CalculateAvailableGas(Transaction tx, in IntrinsicGas intrinsicGas, out ArbitrumGasPolicy gasAvailable) - => GasChargingHook(tx, intrinsicGas.Standard, out gasAvailable); + { + // Capture intrinsic gas for gas dimension tracers + if (_tracingInfo?.Tracer.IsTracingGasDimension == true) + { + ArbitrumGasPolicy standardGas = intrinsicGas.Standard; + long calculatedGas = ArbitrumGasPolicy.GetRemainingGas(in standardGas); + _tracingInfo.Tracer.SetIntrinsicGas(calculatedGas); + } + + TransactionResult result = GasChargingHook(tx, intrinsicGas.Standard, out gasAvailable); + + // Capture L1 poster gas for txGasDimensionLogger tracer (set by GasChargingHook) + if (_tracingInfo?.Tracer.IsTracingGasDimension == true) + _tracingInfo.Tracer.SetPosterGas(TxExecContext.PosterGas); + + return result; + } protected override GasConsumed Refund(Transaction tx, BlockHeader header, IReleaseSpec spec, ExecutionOptions opts, in TransactionSubstate substate, in ArbitrumGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, ArbitrumGasPolicy floorGas) diff --git a/src/Nethermind.Arbitrum/Properties/scripts/generate-system-test-config.sh b/src/Nethermind.Arbitrum/Properties/scripts/generate-system-test-config.sh index f80ce9081..1609f110a 100755 --- a/src/Nethermind.Arbitrum/Properties/scripts/generate-system-test-config.sh +++ b/src/Nethermind.Arbitrum/Properties/scripts/generate-system-test-config.sh @@ -4,7 +4,7 @@ set -e # Parse arguments ARBOS_VERSION=${1:-40} -ACCOUNTS_FILE=${2:-"src/Nethermind.Arbitrum/Properties/accounts/default.json"} +ACCOUNTS_FILE=${2:-"src/Nethermind.Arbitrum/Properties/accounts/defaults.json"} CONFIG_NAME=${3:-"arbitrum-system-test"} MAX_CODE_SIZE=${4:-"0x6000"} TEMPLATE_FILE="src/Nethermind.Arbitrum/Properties/chainspec/system-test-chainspec.template" diff --git a/src/Nethermind.Arbitrum/Tracing/GasDimensionTracerBase.cs b/src/Nethermind.Arbitrum/Tracing/GasDimensionTracerBase.cs new file mode 100644 index 000000000..8fdae25d8 --- /dev/null +++ b/src/Nethermind.Arbitrum/Tracing/GasDimensionTracerBase.cs @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Arbitrum.Evm; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Blockchain.Tracing.GethStyle.Custom.Native; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Evm; +using Nethermind.Evm.TransactionProcessing; +using Nethermind.Int256; + +namespace Nethermind.Arbitrum.Tracing; + +/// +/// Base class for gas dimension tracers. Provides shared state management and Begin/End capture pattern. +/// +public abstract class GasDimensionTracerBase : GethLikeNativeTxTracer, IArbitrumTxTracer +{ + protected readonly Transaction? _transaction; + protected readonly Block? _block; + + protected ulong _gasUsed; + protected ulong _intrinsicGas; + protected ulong _posterGas; + protected bool _failed; + + // Before-state for gas dimension capture (stored between Begin/End calls) + private int _beforePc; + private Instruction _beforeOpcode; + private int _beforeDepth; + private MultiGas _beforeGas; + + protected GasDimensionTracerBase(Transaction? transaction, Block? block, GethTraceOptions options) + : base(options) + { + _transaction = transaction; + _block = block; + IsTracingActions = true; + } + + public bool IsTracingGasDimension => true; + + public override void MarkAsSuccess(Address recipient, GasConsumed gasSpent, byte[] output, LogEntry[] logs, Hash256? stateRoot = null) + { + base.MarkAsSuccess(recipient, gasSpent, output, logs, stateRoot); + _gasUsed = (ulong)gasSpent.SpentGas; + _failed = false; + } + + public override void MarkAsFailed(Address recipient, GasConsumed gasSpent, byte[] output, string? error, Hash256? stateRoot = null) + { + base.MarkAsFailed(recipient, gasSpent, output, error, stateRoot); + _gasUsed = (ulong)gasSpent.SpentGas; + _failed = true; + } + + public void BeginGasDimensionCapture(int pc, Instruction opcode, int depth, in MultiGas gasBefore) + { + _beforePc = pc; + _beforeOpcode = opcode; + _beforeDepth = depth; + _beforeGas = gasBefore; + } + + public void EndGasDimensionCapture(in MultiGas gasAfter) + { + long gasCost = (long)(gasAfter.Total - _beforeGas.Total); + OnGasDimensionCaptured(_beforePc, _beforeOpcode, _beforeDepth, in _beforeGas, in gasAfter, gasCost); + } + + /// + /// Called when a gas dimension capture completes. Derived classes implement this to handle the captured data. + /// + protected abstract void OnGasDimensionCaptured( + int pc, + Instruction opcode, + int depth, + in MultiGas gasBefore, + in MultiGas gasAfter, + long gasCost); + + public void SetIntrinsicGas(long intrinsicGas) => _intrinsicGas = (ulong)intrinsicGas; + + public void SetPosterGas(ulong posterGas) => _posterGas = posterGas; + + public void CaptureArbitrumTransfer(Address? from, Address? to, UInt256 value, bool before, BalanceChangeReason reason) + { + } + + 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) + { + } + + /// + /// Computes L1 and L2 gas usage from total gas used and poster gas. + /// L1 gas is the poster gas (L1 data posting cost), L2 gas is everything else. + /// + protected (ulong gasUsedForL1, ulong gasUsedForL2) ComputeL1L2Gas() + { + ulong gasUsedForL1 = _posterGas; + ulong gasUsedForL2 = _gasUsed > gasUsedForL1 ? _gasUsed - gasUsedForL1 : 0; + return (gasUsedForL1, gasUsedForL2); + } +} diff --git a/src/Nethermind.Arbitrum/Tracing/IArbitrumTxTracer.cs b/src/Nethermind.Arbitrum/Tracing/IArbitrumTxTracer.cs index 9b65f476b..6fb840f1b 100644 --- a/src/Nethermind.Arbitrum/Tracing/IArbitrumTxTracer.cs +++ b/src/Nethermind.Arbitrum/Tracing/IArbitrumTxTracer.cs @@ -1,5 +1,7 @@ +using Nethermind.Arbitrum.Evm; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Evm; using Nethermind.Evm.Tracing; using Nethermind.Int256; @@ -7,10 +9,45 @@ namespace Nethermind.Arbitrum.Tracing; public interface IArbitrumTxTracer : ITxTracer { + /// + /// Whether this tracer captures per-opcode gas dimension breakdown. + /// + bool IsTracingGasDimension => false; + void CaptureArbitrumTransfer(Address? from, Address? to, UInt256 value, bool before, BalanceChangeReason reason); void CaptureArbitrumStorageGet(UInt256 index, int depth, bool before); void CaptureArbitrumStorageSet(UInt256 index, ValueHash256 value, int depth, bool before); void CaptureStylusHostio(string name, ReadOnlySpan args, ReadOnlySpan outs, ulong startInk, ulong endInk); + + /// + /// Called at the start of opcode execution to capture "before" gas state. + /// Used by gas policy hooks for stateful dimension capture. + /// + /// Program counter + /// The opcode being executed + /// Current call depth (1-based) + /// MultiGas snapshot before opcode execution + void BeginGasDimensionCapture(int pc, Instruction opcode, int depth, in MultiGas gasBefore) + { } + + /// + /// Called at the end of opcode execution to capture "after" gas state and emit dimension log. + /// Uses the "before" state captured by BeginGasDimensionCapture. + /// Scalar gas cost is computed as gasAfter.Total - gasBefore.Total. + /// + /// MultiGas snapshot after opcode execution + void EndGasDimensionCapture(in MultiGas gasAfter) + { } + + /// + /// Sets the intrinsic gas for gas dimension logging. + /// + void SetIntrinsicGas(long intrinsicGas) { } + + /// + /// Sets the L1 poster gas for gas dimension logging. + /// + void SetPosterGas(ulong posterGas) { } } diff --git a/src/Nethermind.Arbitrum/Tracing/TxGasDimensionByOpcodeTracer.cs b/src/Nethermind.Arbitrum/Tracing/TxGasDimensionByOpcodeTracer.cs new file mode 100644 index 000000000..068be7913 --- /dev/null +++ b/src/Nethermind.Arbitrum/Tracing/TxGasDimensionByOpcodeTracer.cs @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +// ReSharper disable UnusedAutoPropertyAccessor.Global - Properties are used by JSON serialization + +using System.Text.Json; +using System.Text.Json.Serialization; +using Nethermind.Arbitrum.Evm; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Blockchain.Tracing.GethStyle.Custom; +using Nethermind.Core; +using Nethermind.Evm; +using Nethermind.Serialization.Json; + +namespace Nethermind.Arbitrum.Tracing; + +/// +/// Tracer that aggregates gas dimension breakdown by opcode type. +/// +public sealed class TxGasDimensionByOpcodeTracer(Transaction? transaction, Block? block, GethTraceOptions options) + : GasDimensionTracerBase(transaction, block, options) +{ + public const string TracerName = "txGasDimensionByOpcode"; + + private readonly Dictionary _dimensionsByOpcode = new(); + + public override GethLikeTxTrace BuildResult() + { + GethLikeTxTrace result = base.BuildResult(); + + (ulong gasUsedForL1, ulong gasUsedForL2) = ComputeL1L2Gas(); + + // Convert dictionary to use opcode names as keys + Dictionary dimensionsByName = new(); + foreach (KeyValuePair kvp in _dimensionsByOpcode) + { + string opcodeName = (kvp.Key.GetName() ?? kvp.Key.ToString()).ToUpperInvariant(); + dimensionsByName[opcodeName] = kvp.Value; + } + + TxGasDimensionByOpcodeResult dimensionResult = new() + { + TxHash = _transaction?.Hash?.ToString(), + GasUsed = _gasUsed, + GasUsedForL1 = gasUsedForL1, + GasUsedForL2 = gasUsedForL2, + IntrinsicGas = _intrinsicGas, + AdjustedRefund = 0, // TODO: Track refunds + RootIsPrecompile = false, + RootIsPrecompileAdjustment = 0, + RootIsStylus = false, + RootIsStylusAdjustment = 0, + Failed = _failed, + BlockTimestamp = _block?.Timestamp ?? 0, + BlockNumber = (ulong)(_block?.Number ?? 0), + Status = _failed ? 0UL : 1UL, + Dimensions = dimensionsByName + }; + + result.TxHash = _transaction?.Hash; + result.CustomTracerResult = new GethLikeCustomTrace { Value = dimensionResult }; + + return result; + } + + protected override void OnGasDimensionCaptured( + int pc, + Instruction opcode, + int depth, + in MultiGas gasBefore, + in MultiGas gasAfter, + long gasCost) + { + MultiGas delta = gasAfter.SaturatingSub(in gasBefore); + + // Get or create the breakdown for this opcode + if (!_dimensionsByOpcode.TryGetValue(opcode, out GasDimensionBreakdown? breakdown)) + { + breakdown = new GasDimensionBreakdown(); + _dimensionsByOpcode[opcode] = breakdown; + } + + // Aggregate the gas dimensions + breakdown.OneDimensionalGasCost += (ulong)gasCost; + breakdown.Computation += delta.Get(ResourceKind.Computation); + breakdown.StateAccess += delta.Get(ResourceKind.StorageAccess); + breakdown.StateGrowth += delta.Get(ResourceKind.StorageGrowth); + breakdown.HistoryGrowth += delta.Get(ResourceKind.HistoryGrowth); + } +} + +/// +/// JSON converter that preserves dictionary key casing (bypasses CamelCase policy). +/// Required because EthereumJsonSerializer uses DictionaryKeyPolicy = JsonNamingPolicy.CamelCase. +/// +public sealed class PreserveCaseDictionaryConverter : JsonConverter> +{ + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException("Read not needed for this tracer"); + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (KeyValuePair kvp in value) + { + writer.WritePropertyName(kvp.Key); // Use key as-is, no CamelCase conversion + JsonSerializer.Serialize(writer, kvp.Value, options); + } + writer.WriteEndObject(); + } +} + +/// +/// Aggregated gas dimension breakdown for a single opcode type. +/// +public sealed class GasDimensionBreakdown +{ + [JsonPropertyName("gas1d")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong OneDimensionalGasCost { get; set; } + + [JsonPropertyName("cpu")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong Computation { get; set; } + + [JsonPropertyName("rw")] + [JsonConverter(typeof(ULongRawJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong StateAccess { get; set; } + + [JsonPropertyName("growth")] + [JsonConverter(typeof(ULongRawJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong StateGrowth { get; set; } + + [JsonPropertyName("hist")] + [JsonConverter(typeof(ULongRawJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong HistoryGrowth { get; set; } +} + +public sealed class TxGasDimensionByOpcodeResult : BaseTxGasDimensionResult +{ + [JsonPropertyName("dimensions")] + [JsonConverter(typeof(PreserveCaseDictionaryConverter))] + public Dictionary Dimensions { get; init; } = new(); +} diff --git a/src/Nethermind.Arbitrum/Tracing/TxGasDimensionLoggerTracer.cs b/src/Nethermind.Arbitrum/Tracing/TxGasDimensionLoggerTracer.cs new file mode 100644 index 000000000..0215288d5 --- /dev/null +++ b/src/Nethermind.Arbitrum/Tracing/TxGasDimensionLoggerTracer.cs @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +// ReSharper disable UnusedAutoPropertyAccessor.Global - Properties are used by JSON serialization + +using System.Text.Json.Serialization; +using Nethermind.Arbitrum.Evm; +using Nethermind.Blockchain.Tracing.GethStyle; +using Nethermind.Blockchain.Tracing.GethStyle.Custom; +using Nethermind.Core; +using Nethermind.Evm; +using Nethermind.Serialization.Json; + +namespace Nethermind.Arbitrum.Tracing; + +/// +/// Tracer that captures per-opcode gas dimension breakdown. +/// +public sealed class TxGasDimensionLoggerTracer : GasDimensionTracerBase +{ + public const string TracerName = "txGasDimensionLogger"; + + private readonly List _logs = new(256); + + public TxGasDimensionLoggerTracer(Transaction? transaction, Block? block, GethTraceOptions options) + : base(transaction, block, options) + { + } + + public override GethLikeTxTrace BuildResult() + { + GethLikeTxTrace result = base.BuildResult(); + + (ulong gasUsedForL1, ulong gasUsedForL2) = ComputeL1L2Gas(); + + TxGasDimensionResult dimensionResult = new() + { + TxHash = _transaction?.Hash?.ToString(), + GasUsed = _gasUsed, + GasUsedForL1 = gasUsedForL1, + GasUsedForL2 = gasUsedForL2, + IntrinsicGas = _intrinsicGas, + AdjustedRefund = 0, // TODO: Track refunds + RootIsPrecompile = false, + RootIsPrecompileAdjustment = 0, + RootIsStylus = false, + RootIsStylusAdjustment = 0, + Failed = _failed, + BlockTimestamp = _block?.Timestamp ?? 0, + BlockNumber = (ulong)(_block?.Number ?? 0), + Status = _failed ? 0UL : 1UL, + DimensionLogs = _logs + }; + + result.TxHash = _transaction?.Hash; + result.CustomTracerResult = new GethLikeCustomTrace { Value = dimensionResult }; + + return result; + } + + protected override void OnGasDimensionCaptured( + int pc, + Instruction opcode, + int depth, + in MultiGas gasBefore, + in MultiGas gasAfter, + long gasCost) + { + MultiGas delta = gasAfter.SaturatingSub(in gasBefore); + + DimensionLog log = new() + { + Pc = (ulong)pc, + Op = (opcode.GetName() ?? opcode.ToString()).ToUpperInvariant(), + Depth = depth, + OneDimensionalGasCost = (ulong)gasCost, + Computation = delta.Get(ResourceKind.Computation), + StateAccess = delta.Get(ResourceKind.StorageAccess), + StateGrowth = delta.Get(ResourceKind.StorageGrowth), + HistoryGrowth = delta.Get(ResourceKind.HistoryGrowth) + }; + + _logs.Add(log); + } +} + +public sealed class DimensionLog +{ + [JsonPropertyName("pc")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong Pc { get; init; } + + [JsonPropertyName("op")] + public string Op { get; init; } = string.Empty; + + [JsonPropertyName("depth")] + public int Depth { get; init; } + + [JsonPropertyName("cost")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong OneDimensionalGasCost { get; init; } + + [JsonPropertyName("cpu")] + [JsonConverter(typeof(ULongRawJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong Computation { get; init; } + + [JsonPropertyName("rw")] + [JsonConverter(typeof(ULongRawJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong StateAccess { get; init; } + + [JsonPropertyName("growth")] + [JsonConverter(typeof(ULongRawJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong StateGrowth { get; init; } + + [JsonPropertyName("history")] + [JsonConverter(typeof(ULongRawJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public ulong HistoryGrowth { get; init; } +} + +public abstract class BaseTxGasDimensionResult +{ + [JsonPropertyName("gasUsed")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong GasUsed { get; init; } + + [JsonPropertyName("gasUsedForL1")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong GasUsedForL1 { get; init; } + + [JsonPropertyName("gasUsedForL2")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong GasUsedForL2 { get; init; } + + [JsonPropertyName("intrinsicGas")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong IntrinsicGas { get; init; } + + [JsonPropertyName("adjustedRefund")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong AdjustedRefund { get; init; } + + [JsonPropertyName("rootIsPrecompile")] + public bool RootIsPrecompile { get; init; } + + [JsonPropertyName("rootIsPrecompileAdjustment")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong RootIsPrecompileAdjustment { get; init; } + + [JsonPropertyName("rootIsStylus")] + public bool RootIsStylus { get; init; } + + [JsonPropertyName("rootIsStylusAdjustment")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong RootIsStylusAdjustment { get; init; } + + [JsonPropertyName("failed")] + public bool Failed { get; init; } + + [JsonPropertyName("txHash")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TxHash { get; init; } + + [JsonPropertyName("blockTimestamp")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong BlockTimestamp { get; init; } + + [JsonPropertyName("blockNumber")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong BlockNumber { get; init; } + + [JsonPropertyName("status")] + [JsonConverter(typeof(ULongRawJsonConverter))] + public ulong Status { get; init; } +} + +public sealed class TxGasDimensionResult : BaseTxGasDimensionResult +{ + [JsonPropertyName("dim")] + public List DimensionLogs { get; init; } = []; +}