diff --git a/src/Nethermind/Nethermind.Core/Collections/ArrayPoolList.cs b/src/Nethermind/Nethermind.Core/Collections/ArrayPoolList.cs index c284e060f7e..d1a94880e7b 100644 --- a/src/Nethermind/Nethermind.Core/Collections/ArrayPoolList.cs +++ b/src/Nethermind/Nethermind.Core/Collections/ArrayPoolList.cs @@ -319,4 +319,6 @@ public void Dispose() #endif public Span AsSpan() => _array.AsSpan(0, Count); + + public ReadOnlyMemory AsMemory() => new(_array, 0, Count); } diff --git a/src/Nethermind/Nethermind.Core/CompositeDisposable.cs b/src/Nethermind/Nethermind.Core/CompositeDisposable.cs new file mode 100644 index 00000000000..edbbe598338 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/CompositeDisposable.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; + +namespace Nethermind.Core; + +public class CompositeDisposable : List, IDisposable +{ + public void Dispose() + { + foreach (IDisposable disposable in this) + { + disposable.Dispose(); + } + } +} diff --git a/src/Nethermind/Nethermind.Evm.Test/EvmPooledMemoryTests.cs b/src/Nethermind/Nethermind.Evm.Test/EvmPooledMemoryTests.cs index 29542672d18..48129be9325 100644 --- a/src/Nethermind/Nethermind.Evm.Test/EvmPooledMemoryTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/EvmPooledMemoryTests.cs @@ -217,7 +217,19 @@ public class MyTracer : ITxTracer, IDisposable public bool IsTracingBlockHash { get; } = false; public bool IsTracingAccess { get; } = false; public bool IsTracingFees => false; - public bool IsTracing => IsTracingReceipt || IsTracingActions || IsTracingOpLevelStorage || IsTracingMemory || IsTracingInstructions || IsTracingRefunds || IsTracingCode || IsTracingStack || IsTracingBlockHash || IsTracingAccess || IsTracingFees; + public bool IsTracingLogs => false; + public bool IsTracing => IsTracingReceipt + || IsTracingActions + || IsTracingOpLevelStorage + || IsTracingMemory + || IsTracingInstructions + || IsTracingRefunds + || IsTracingCode + || IsTracingStack + || IsTracingBlockHash + || IsTracingAccess + || IsTracingFees + || IsTracingLogs; public string lastmemline; @@ -241,6 +253,10 @@ public void ReportOperationRemainingGas(long gas) { } + public void ReportLog(LogEntry log) + { + } + public void SetOperationStack(TraceStack stack) { } @@ -320,6 +336,11 @@ public void ReportActionError(EvmExceptionType exceptionType) throw new NotSupportedException(); } + public void ReportActionRevert(long gas, ReadOnlyMemory output) + { + throw new NotSupportedException(); + } + public void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) { throw new NotSupportedException(); diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs index df61919ce62..134cd6bd0a7 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/GasEstimationTests.cs @@ -15,7 +15,6 @@ using Nethermind.Int256; using Nethermind.Logging; using Nethermind.Specs; -using Nethermind.Specs.Forks; using Nethermind.State; using Nethermind.Trie.Pruning; using NSubstitute; diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/GethLikeCallTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/GethLikeCallTracerTests.cs new file mode 100644 index 00000000000..af49a60c100 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/GethLikeCallTracerTests.cs @@ -0,0 +1,569 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Text.Json; +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm.Tracing.GethStyle; +using Nethermind.Evm.Tracing.GethStyle.Custom.Native.Call; +using Nethermind.Serialization.Json; +using Nethermind.Specs; +using Nethermind.State; +using NUnit.Framework; + +namespace Nethermind.Evm.Test.Tracing; + +[TestFixture] +public class GethLikeCallTracerTests : VirtualMachineTestsBase +{ + private static readonly JsonSerializerOptions SerializerOptions = EthereumJsonSerializer.JsonOptionsIndented; + private const string? WithLog = """{"withLog":true}"""; + private const string? OnlyTopCall = """{"onlyTopCall":true}"""; + private const string? WithLogAndOnlyTopCall = """{"withLog":true,"onlyTopCall":true}"""; + + private string ExecuteCallTrace(byte[] code, string? tracerConfig = null) + { + (_, Transaction tx) = PrepareTx(MainnetSpecProvider.CancunActivation, 100000, code); + NativeCallTracer tracer = new(tx, GetGethTraceOptions(tracerConfig)); + + GethLikeTxTrace callTrace = Execute( + tracer, + code, + MainnetSpecProvider.CancunActivation) + .BuildResult(); + return JsonSerializer.Serialize(callTrace.CustomTracerResult?.Value, SerializerOptions) + // fix for windows, can be done better in .NET 9: https://github.com/dotnet/runtime/issues/84117 + .ReplaceLineEndings("\n"); + } + + private static GethTraceOptions GetGethTraceOptions(string? config) => GethTraceOptions.Default with + { + Tracer = NativeCallTracer.CallTracer, + TracerConfig = config is not null ? JsonSerializer.Deserialize(config) : null + }; + + [Test] + public void Test_CallTrace_SingleCall() + { + byte[] code = Prepare.EvmCode + .PushData(SampleHexData1.PadLeft(64, '0')) + .PushData(0) + .Op(Instruction.SSTORE) + .PushData(SampleHexData2.PadLeft(64, '0')) + .PushData(32) + .Op(Instruction.SSTORE) + .Op(Instruction.STOP) + .Done; + + string callTrace = ExecuteCallTrace(code); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0xfebc", + "input": "0x" +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_NestedCalls() + { + byte[] code = CreateNestedCallsCode(); + string callTrace = ExecuteCallTrace(code); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x16644", + "input": "0x", + "calls": [ + { + "type": "CALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x0", + "gas": "0xc350", + "gasUsed": "0x8400", + "input": "0xa01234", + "calls": [ + { + "type": "CREATE", + "from": "0x76e68a8696537e4141926f3e528733af9e237d69", + "to": "0xd75a3a95360e44a3874e691fb48d77855f127069", + "value": "0x0", + "gas": "0x4513", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + }, + { + "type": "DELEGATECALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x1", + "gas": "0xa342", + "gasUsed": "0x8400", + "input": "0x", + "calls": [ + { + "type": "CREATE", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x89aa9b2ce05aaef815f25b237238c0b4ffff6ae3", + "value": "0x0", + "gas": "0x2585", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + } + ] +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_NestedCalls_WithLog() + { + byte[] code = CreateNestedCallsCode(); + string callTrace = ExecuteCallTrace(code, WithLog); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x16644", + "input": "0x", + "logs": [ + { + "address": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "data": "0x", + "topics": [], + "position": "0x2" + } + ], + "calls": [ + { + "type": "CALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x0", + "gas": "0xc350", + "gasUsed": "0x8400", + "input": "0xa01234", + "logs": [ + { + "address": "0x76e68a8696537e4141926f3e528733af9e237d69", + "data": "0x", + "topics": ["0x1f675bff07515f5df96737194ea945c36c41e7b4fcef307b7cd4d0e602a69111","0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760" + ], + "position": "0x1" + } + ], + "calls": [ + { + "type": "CREATE", + "from": "0x76e68a8696537e4141926f3e528733af9e237d69", + "to": "0xd75a3a95360e44a3874e691fb48d77855f127069", + "value": "0x0", + "gas": "0x4513", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + }, + { + "type": "DELEGATECALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x1", + "gas": "0xa342", + "gasUsed": "0x8400", + "input": "0x", + "logs": [ + { + "address": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "data": "0x", + "topics": ["0x1f675bff07515f5df96737194ea945c36c41e7b4fcef307b7cd4d0e602a69111","0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760" + ], + "position": "0x1" + } + ], + "calls": [ + { + "type": "CREATE", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x89aa9b2ce05aaef815f25b237238c0b4ffff6ae3", + "value": "0x0", + "gas": "0x2585", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + } + ] +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_NestedCalls_OnlyTopCall() + { + byte[] code = CreateNestedCallsCode(); + string callTrace = ExecuteCallTrace(code, OnlyTopCall); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x16644", + "input": "0x" +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_NestedCalls_WithLogsAndOnlyTopCall() + { + byte[] code = CreateNestedCallsCode(); + string callTrace = ExecuteCallTrace(code, WithLogAndOnlyTopCall); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x16644", + "input": "0x", + "logs": [ + { + "address": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "data": "0x", + "topics": [], + "position": "0x0" + } + ] +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_NestedCalls_RevertParentCall() + { + byte[] code = CreateNestedCallsCode(true); + string callTrace = ExecuteCallTrace(code, WithLog); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x1664a", + "input": "0x", + "error": "execution reverted", + "calls": [ + { + "type": "CALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x0", + "gas": "0xc350", + "gasUsed": "0x8400", + "input": "0xa01234", + "calls": [ + { + "type": "CREATE", + "from": "0x76e68a8696537e4141926f3e528733af9e237d69", + "to": "0xd75a3a95360e44a3874e691fb48d77855f127069", + "value": "0x0", + "gas": "0x4513", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + }, + { + "type": "DELEGATECALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x1", + "gas": "0xa342", + "gasUsed": "0x8400", + "input": "0x", + "calls": [ + { + "type": "CREATE", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x89aa9b2ce05aaef815f25b237238c0b4ffff6ae3", + "value": "0x0", + "gas": "0x2585", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + } + ] +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_NestedCalls_RevertInternalCall() + { + byte[] code = CreateNestedCallsCode(false, true); + string callTrace = ExecuteCallTrace(code, WithLog); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x16650", + "input": "0x", + "logs": [ + { + "address": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "data": "0x", + "topics": [], + "position": "0x2" + } + ], + "calls": [ + { + "type": "CALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x0", + "gas": "0xc350", + "gasUsed": "0x8406", + "input": "0xa01234", + "error": "execution reverted", + "logs": [ + { + "address": "0x76e68a8696537e4141926f3e528733af9e237d69", + "data": "0x", + "topics": ["0x1f675bff07515f5df96737194ea945c36c41e7b4fcef307b7cd4d0e602a69111","0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760" + ], + "position": "0x1" + } + ], + "calls": [ + { + "type": "CREATE", + "from": "0x76e68a8696537e4141926f3e528733af9e237d69", + "to": "0xd75a3a95360e44a3874e691fb48d77855f127069", + "value": "0x0", + "gas": "0x4513", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + }, + { + "type": "DELEGATECALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x1", + "gas": "0xa33c", + "gasUsed": "0x8406", + "input": "0x", + "error": "execution reverted", + "logs": [ + { + "address": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "data": "0x", + "topics": ["0x1f675bff07515f5df96737194ea945c36c41e7b4fcef307b7cd4d0e602a69111","0x03783fac2efed8fbc9ad443e592ee30e61d65f471140c10ca155e937b435b760" + ], + "position": "0x1" + } + ], + "calls": [ + { + "type": "CREATE", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x89aa9b2ce05aaef815f25b237238c0b4ffff6ae3", + "value": "0x0", + "gas": "0x257f", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + } + ] +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_NestedCalls_RevertAllCalls() + { + byte[] code = CreateNestedCallsCode(true, true); + string callTrace = ExecuteCallTrace(code, WithLog); + const string expectedCallTrace = """ +{ + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x16656", + "input": "0x", + "error": "execution reverted", + "calls": [ + { + "type": "CALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x0", + "gas": "0xc350", + "gasUsed": "0x8406", + "input": "0xa01234", + "error": "execution reverted", + "calls": [ + { + "type": "CREATE", + "from": "0x76e68a8696537e4141926f3e528733af9e237d69", + "to": "0xd75a3a95360e44a3874e691fb48d77855f127069", + "value": "0x0", + "gas": "0x4513", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + }, + { + "type": "DELEGATECALL", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x76e68a8696537e4141926f3e528733af9e237d69", + "value": "0x1", + "gas": "0xa33c", + "gasUsed": "0x8406", + "input": "0x", + "error": "execution reverted", + "calls": [ + { + "type": "CREATE", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0x89aa9b2ce05aaef815f25b237238c0b4ffff6ae3", + "value": "0x0", + "gas": "0x257f", + "gasUsed": "0x26a", + "input": "0x7f000000000000000000000000000000000000000000000000000000000000000060005260036000f3", + "output": "0x000000" + } + ] + } + ] +} +"""; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_SelfDestruct() + { + byte[] code = Prepare.EvmCode + .PushData(TestItem.AddressA) + .Op(Instruction.SELFDESTRUCT) + .Done; + + string callTrace = ExecuteCallTrace(code); + const string expectedCallTrace = """ + { + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x6593", + "input": "0x", + "calls": [ + { + "type": "SELFDESTRUCT", + "from": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "to": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "value": "0xad78ebc5ac6200001", + "gas": "0x0", + "gasUsed": "0x0", + "input": "0x" + } + ] + } + """; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + [Test] + public void Test_CallTrace_SelfDestruct_OnlyTopCall() + { + byte[] code = Prepare.EvmCode + .PushData(TestItem.AddressA) + .Op(Instruction.SELFDESTRUCT) + .Done; + + string callTrace = ExecuteCallTrace(code, OnlyTopCall); + const string expectedCallTrace = """ + { + "type": "CALL", + "from": "0xb7705ae4c6f81b66cdb323c65f4e8133690fc099", + "to": "0x942921b14f1b1c385cd7e0cc2ef7abe5598c8358", + "value": "0x1", + "gas": "0x186a0", + "gasUsed": "0x6593", + "input": "0x" + } + """; + Assert.That(callTrace, Is.EqualTo(expectedCallTrace)); + } + + private byte[] CreateNestedCallsCode(bool revertParentCall = false, bool revertCreateCall = false) + { + byte[] deployedCode = new byte[3]; + + byte[] initCode = Prepare.EvmCode.ForInitOf(deployedCode).Done; + + Prepare createCodePrepare = Prepare.EvmCode + .Create(initCode, 0) + .Log(0, 0, [TestItem.KeccakA, TestItem.KeccakB]); + byte[] createCode = revertCreateCall ? createCodePrepare.Revert(0, 0).Done : createCodePrepare.STOP().Done; + + TestState.CreateAccount(TestItem.AddressC, 1.Ether()); + TestState.InsertCode(TestItem.AddressC, createCode, Spec); + Prepare callCodePrepare = Prepare.EvmCode + .CallWithInput(TestItem.AddressC, 50000, SampleHexData1) + .DelegateCall(TestItem.AddressC, 50000) + .Log(0, 0); + return revertParentCall ? callCodePrepare.Revert(0, 0).Done : callCodePrepare.Done; + } +} diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/GethLikeJavaScriptTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/GethLikeJavaScriptTracerTests.cs index ffb3f1ab1bc..3d84ab5a51d 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/GethLikeJavaScriptTracerTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/GethLikeJavaScriptTracerTests.cs @@ -343,7 +343,7 @@ public void prestate_tracer() public void call_tracer() { using GethLikeBlockJavaScriptTracer tracer = ExecuteBlock( - GetTracer("callTracer"), + GetTracer("callTracer_legacy"), NestedCalls(), MainnetSpecProvider.CancunActivation); GethLikeTxTrace traces = tracer.BuildResult().First(); diff --git a/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs b/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs index 8ebdf5fb439..3f1407b117c 100644 --- a/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs +++ b/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Nethermind.Core; +using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; using Nethermind.Int256; @@ -214,6 +215,34 @@ public Prepare DynamicCallWithInput(Instruction callType, Address address, long return this; } + public Prepare Log(int size, int position, Hash256[]? topics = null) + { + if (topics?.Length > 4) + { + throw new ArgumentException("Too many topics - must be 4 or less"); + } + int numTopics = topics?.Length ?? 0; + if (topics is not null) + { + foreach (Hash256 topic in topics) + { + PushData(topic.Bytes.ToArray()); + } + } + PushData(size); + PushData(position); + Op(Instruction.LOG0 + (byte)numTopics); + return this; + } + + public Prepare Revert(int size, int position) + { + PushData(size); + PushData(position); + Op(Instruction.REVERT); + return this; + } + public Prepare PushData(Address address) { PushData(address.Bytes); diff --git a/src/Nethermind/Nethermind.Evm/Data/JSTracers/callTracer.js b/src/Nethermind/Nethermind.Evm/Data/JSTracers/callTracer_legacy.js similarity index 100% rename from src/Nethermind/Nethermind.Evm/Data/JSTracers/callTracer.js rename to src/Nethermind/Nethermind.Evm/Data/JSTracers/callTracer_legacy.js diff --git a/src/Nethermind/Nethermind.Evm/EvmExceptionExtensions.cs b/src/Nethermind/Nethermind.Evm/EvmExceptionExtensions.cs new file mode 100644 index 00000000000..74aa1b7e233 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/EvmExceptionExtensions.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Evm; + +public static class EvmExceptionExtensions +{ + public static string? GetEvmExceptionDescription(this EvmExceptionType evmExceptionType) => + evmExceptionType switch + { + EvmExceptionType.None => null, + EvmExceptionType.BadInstruction => "invalid instruction", + EvmExceptionType.StackOverflow => "max call depth exceeded", + EvmExceptionType.StackUnderflow => "stack underflow", + EvmExceptionType.OutOfGas => "out of gas", + EvmExceptionType.GasUInt64Overflow => "gas uint64 overflow", + EvmExceptionType.InvalidSubroutineEntry => "invalid jump destination", + EvmExceptionType.InvalidSubroutineReturn => "invalid jump destination", + EvmExceptionType.InvalidJumpDestination => "invalid jump destination", + EvmExceptionType.AccessViolation => "return data out of bounds", + EvmExceptionType.StaticCallViolation => "write protection", + EvmExceptionType.PrecompileFailure => "precompile error", + EvmExceptionType.TransactionCollision => "contract address collision", + EvmExceptionType.NotEnoughBalance => "insufficient balance for transfer", + EvmExceptionType.Other => "error", + EvmExceptionType.Revert => "execution reverted", + EvmExceptionType.InvalidCode => "invalid code: must not begin with 0xef", + _ => "error" + }; +} diff --git a/src/Nethermind/Nethermind.Evm/ExecutionType.cs b/src/Nethermind/Nethermind.Evm/ExecutionType.cs index 95045606b06..f0ce0c67737 100644 --- a/src/Nethermind/Nethermind.Evm/ExecutionType.cs +++ b/src/Nethermind/Nethermind.Evm/ExecutionType.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Runtime.CompilerServices; namespace Nethermind.Evm @@ -11,6 +12,19 @@ public static class ExecutionTypeExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsAnyCreate(this ExecutionType executionType) => executionType is ExecutionType.CREATE or ExecutionType.CREATE2; + + public static Instruction ToInstruction(this ExecutionType executionType) => + executionType switch + { + ExecutionType.TRANSACTION => Instruction.CALL, + ExecutionType.CALL => Instruction.CALL, + ExecutionType.STATICCALL => Instruction.STATICCALL, + ExecutionType.CALLCODE => Instruction.CALLCODE, + ExecutionType.DELEGATECALL => Instruction.DELEGATECALL, + ExecutionType.CREATE => Instruction.CREATE, + ExecutionType.CREATE2 => Instruction.CREATE2, + _ => throw new NotSupportedException($"Execution type {executionType} is not supported.") + }; } // ReSharper disable InconsistentNaming IdentifierTypo diff --git a/src/Nethermind/Nethermind.Evm/Tracing/AlwaysCancelTxTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/AlwaysCancelTxTracer.cs index cd78dec4b70..603cec0f6e2 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/AlwaysCancelTxTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/AlwaysCancelTxTracer.cs @@ -41,6 +41,7 @@ public static AlwaysCancelTxTracer Instance public bool IsTracingBlockHash => true; public bool IsTracingAccess => true; public bool IsTracingFees => true; + public bool IsTracingLogs => true; public void MarkAsSuccess(Address recipient, long gasSpent, byte[] output, LogEntry[] logs, Hash256? stateRoot = null) => throw new OperationCanceledException(ErrorMessage); @@ -52,6 +53,8 @@ public static AlwaysCancelTxTracer Instance public void ReportOperationRemainingGas(long gas) => throw new OperationCanceledException(ErrorMessage); + public void ReportLog(LogEntry log) => throw new OperationCanceledException(ErrorMessage); + public void SetOperationMemorySize(ulong newSize) => throw new OperationCanceledException(ErrorMessage); public void ReportMemoryChange(long offset, in ReadOnlySpan data) => throw new OperationCanceledException(ErrorMessage); @@ -85,6 +88,7 @@ public static AlwaysCancelTxTracer Instance public void ReportActionEnd(long gas, ReadOnlyMemory output) => throw new OperationCanceledException(ErrorMessage); public void ReportActionError(EvmExceptionType exceptionType) => throw new OperationCanceledException(ErrorMessage); + public void ReportActionRevert(long gas, ReadOnlyMemory output) => throw new OperationCanceledException(ErrorMessage); public void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) => throw new OperationCanceledException(ErrorMessage); public void ReportBlockHash(Hash256 blockHash) => throw new OperationCanceledException(ErrorMessage); diff --git a/src/Nethermind/Nethermind.Evm/Tracing/BlockReceiptsTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/BlockReceiptsTracer.cs index 7d93131630e..f319531a9f0 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/BlockReceiptsTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/BlockReceiptsTracer.cs @@ -27,6 +27,7 @@ public class BlockReceiptsTracer : IBlockTracer, ITxTracer, IJournal, ITxTr public bool IsTracingBlockHash => _currentTxTracer.IsTracingBlockHash; public bool IsTracingAccess => _currentTxTracer.IsTracingAccess; public bool IsTracingFees => _currentTxTracer.IsTracingFees; + public bool IsTracingLogs => _currentTxTracer.IsTracingLogs; private IBlockTracer _otherTracer = NullBlockTracer.Instance; @@ -105,6 +106,8 @@ public void ReportOperationError(EvmExceptionType error) => public void ReportOperationRemainingGas(long gas) => _currentTxTracer.ReportOperationRemainingGas(gas); + public void ReportLog(LogEntry log) => + _currentTxTracer.ReportLog(log); public void SetOperationMemorySize(ulong newSize) => _currentTxTracer.SetOperationMemorySize(newSize); @@ -151,7 +154,7 @@ public void ReportActionEnd(long gas, ReadOnlyMemory output) => public void ReportActionError(EvmExceptionType exceptionType) => _currentTxTracer.ReportActionError(exceptionType); - public void ReportActionRevert(long gasLeft, byte[] output) => + public void ReportActionRevert(long gasLeft, ReadOnlyMemory output) => _currentTxTracer.ReportActionRevert(gasLeft, output); public void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) => diff --git a/src/Nethermind/Nethermind.Evm/Tracing/CancellationTxTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/CancellationTxTracer.cs index d594ed05090..6cd4af28425 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/CancellationTxTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/CancellationTxTracer.cs @@ -27,6 +27,7 @@ public class CancellationTxTracer : ITxTracer, ITxTracerWrapper private readonly bool _isTracingBlockHash; private readonly bool _isTracingBlockAccess; private readonly bool _isTracingFees; + private readonly bool _isTracingOpLevelLogs; public ITxTracer InnerTracer => _innerTracer; @@ -113,6 +114,11 @@ public bool IsTracingFees get => _isTracingFees || _innerTracer.IsTracingFees; init => _isTracingFees = value; } + public bool IsTracingLogs + { + get => _isTracingOpLevelLogs || _innerTracer.IsTracingLogs; + init => _isTracingOpLevelLogs = value; + } public void ReportBalanceChange(Address address, UInt256? before, UInt256? after) { @@ -213,6 +219,15 @@ public void ReportOperationRemainingGas(long gas) } } + public void ReportLog(LogEntry log) + { + _token.ThrowIfCancellationRequested(); + if (_innerTracer.IsTracingLogs) + { + _innerTracer.ReportLog(log); + } + } + public void SetOperationStack(TraceStack stack) { _token.ThrowIfCancellationRequested(); @@ -357,7 +372,7 @@ public void ReportActionError(EvmExceptionType evmExceptionType) } } - public void ReportActionRevert(long gasLeft, byte[] output) + public void ReportActionRevert(long gasLeft, ReadOnlyMemory output) { _token.ThrowIfCancellationRequested(); if (_innerTracer.IsTracingActions) diff --git a/src/Nethermind/Nethermind.Evm/Tracing/CompositeTxTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/CompositeTxTracer.cs index 4f4f42d3d54..336766c6650 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/CompositeTxTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/CompositeTxTracer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Nethermind.Core; using Nethermind.Core.Crypto; +using Nethermind.Evm.Tracing.GethStyle.Custom.JavaScript; using Nethermind.Int256; namespace Nethermind.Evm.Tracing; @@ -52,6 +53,7 @@ public CompositeTxTracer(IList txTracers) public bool IsTracingBlockHash { get; } public bool IsTracingAccess { get; } public bool IsTracingFees { get; } + public bool IsTracingLogs { get; } public void ReportBalanceChange(Address address, UInt256? before, UInt256? after) { @@ -185,6 +187,30 @@ public void ReportOperationRemainingGas(long gas) } } + public void ReportOperationLogs(LogEntry log) + { + for (int index = 0; index < _txTracers.Count; index++) + { + ITxTracer innerTracer = _txTracers[index]; + if (innerTracer.IsTracingInstructions && innerTracer.IsTracingLogs) + { + innerTracer.ReportLog(log); + } + } + } + + public void ReportLog(LogEntry log) + { + for (int index = 0; index < _txTracers.Count; index++) + { + ITxTracer innerTracer = _txTracers[index]; + if (innerTracer.IsTracingInstructions) + { + innerTracer.ReportLog(log); + } + } + } + public void SetOperationStack(TraceStack stack) { for (int index = 0; index < _txTracers.Count; index++) @@ -377,7 +403,7 @@ public void ReportActionError(EvmExceptionType evmExceptionType) } } - public void ReportActionRevert(long gasLeft, byte[] output) + public void ReportActionRevert(long gasLeft, ReadOnlyMemory output) { for (int index = 0; index < _txTracers.Count; index++) { diff --git a/src/Nethermind/Nethermind.Evm/Tracing/Debugger/DebugTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/Debugger/DebugTracer.cs index 003741cd843..e37a5ce52b5 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/Debugger/DebugTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/Debugger/DebugTracer.cs @@ -59,6 +59,8 @@ public DebugTracer(ITxTracer tracer) public bool IsTracingStorage => InnerTracer.IsTracingStorage; + public bool IsTracingLogs => InnerTracer.IsTracingLogs; + public bool IsBreakpoitnSet(int depth, int programCounter) => _breakPoints.ContainsKey((depth, programCounter)); public void SetBreakPoint((int depth, int pc) point, Func condition = null) @@ -200,6 +202,9 @@ public void ReportOperationError(EvmExceptionType error) public void ReportOperationRemainingGas(long gas) => InnerTracer.ReportOperationRemainingGas(gas); + public void ReportLog(LogEntry log) + => InnerTracer.ReportLog(log); + public void SetOperationStack(TraceStack stack) => InnerTracer.SetOperationStack(stack); @@ -233,6 +238,9 @@ public void ReportActionEnd(long gas, ReadOnlyMemory output) public void ReportActionError(EvmExceptionType evmExceptionType) => InnerTracer.ReportActionError(evmExceptionType); + public void ReportActionRevert(long gas, ReadOnlyMemory output) + => InnerTracer.ReportActionRevert(gas, output); + public void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) => InnerTracer.ReportActionEnd(gas, deploymentAddress, deployedCode); diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/JavaScript/GethLikeJavaScriptTxTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/JavaScript/GethLikeJavaScriptTxTracer.cs index 45768f7fb73..f9a1b022ed8 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/JavaScript/GethLikeJavaScriptTxTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/JavaScript/GethLikeJavaScriptTxTracer.cs @@ -121,33 +121,10 @@ public override void ReportOperationRemainingGas(long gas) public override void ReportOperationError(EvmExceptionType error) { base.ReportOperationError(error); - _log.error = GetJavaScriptErrorDescription(error); + _log.error = error.GetEvmExceptionDescription(); _tracer.fault(_log, _db); } - private static string? GetJavaScriptErrorDescription(EvmExceptionType evmExceptionType) => - evmExceptionType switch - { - EvmExceptionType.None => null, - EvmExceptionType.BadInstruction => "invalid instruction", - EvmExceptionType.StackOverflow => "max call depth exceeded", - EvmExceptionType.StackUnderflow => "stack underflow", - EvmExceptionType.OutOfGas => "out of gas", - EvmExceptionType.GasUInt64Overflow => "gas uint64 overflow", - EvmExceptionType.InvalidSubroutineEntry => "invalid jump destination", - EvmExceptionType.InvalidSubroutineReturn => "invalid jump destination", - EvmExceptionType.InvalidJumpDestination => "invalid jump destination", - EvmExceptionType.AccessViolation => "return data out of bounds", - EvmExceptionType.StaticCallViolation => "write protection", - EvmExceptionType.PrecompileFailure => "precompile error", - EvmExceptionType.TransactionCollision => "contract address collision", - EvmExceptionType.NotEnoughBalance => "insufficient balance for transfer", - EvmExceptionType.Other => "error", - EvmExceptionType.Revert => "execution reverted", - EvmExceptionType.InvalidCode => "invalid code: must not begin with 0xef", - _ => "error" - }; - public override void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) { base.ReportActionEnd(gas, deploymentAddress, deployedCode); @@ -162,16 +139,16 @@ public override void ReportActionEnd(long gas, ReadOnlyMemory output) InvokeExit(gas, output); } - public void ReportActionRevert(long gasLeft, byte[] output) + public override void ReportActionRevert(long gasLeft, ReadOnlyMemory output) { base.ReportActionError(EvmExceptionType.Revert); - InvokeExit(gasLeft, output, GetJavaScriptErrorDescription(EvmExceptionType.Revert)); + InvokeExit(gasLeft, output, EvmExceptionType.Revert.GetEvmExceptionDescription()); } public override void ReportActionError(EvmExceptionType evmExceptionType) { base.ReportActionError(evmExceptionType); - InvokeExit(0, Array.Empty(), GetJavaScriptErrorDescription(evmExceptionType)); + InvokeExit(0, Array.Empty(), evmExceptionType.GetEvmExceptionDescription()); } private void InvokeExit(long gas, ReadOnlyMemory output, string? error = null) diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracer.cs new file mode 100644 index 00000000000..cbb08962d9a --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracer.cs @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json; +using Nethermind.Core; +using Nethermind.Core.Collections; +using Nethermind.Core.Crypto; +using Nethermind.Core.Extensions; +using Nethermind.Int256; +using Nethermind.Serialization.Json; + +namespace Nethermind.Evm.Tracing.GethStyle.Custom.Native.Call; + +// The callTracer tracks all the call frames executed during a transaction, including depth 0. +// The result will be a nested list of call frames, resembling how the EVM works. +// They form a tree with the top-level call at root and sub-calls as children of the higher levels. +// +// TracerConfig options: +// onlyTopCall (default = false): Only the main (top-level) call will be processed to avoid any extra processing if only the main call info is required. +// withLog (default = false): Logs emitted during each call will also be collected and included in the result. +public sealed class NativeCallTracer : GethLikeNativeTxTracer +{ + public const string CallTracer = "callTracer"; + + private readonly long _gasLimit; + 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 NativeCallTracer( + Transaction? tx, + GethTraceOptions options) : base(options) + { + IsTracingActions = true; + _gasLimit = tx!.GasLimit; + + _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(); + NativeCallTracerCallFrame 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.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(); + NativeCallTracerCallFrame 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.LoggersAddress, + 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, long gasSpent, byte[] output, LogEntry[] logs, Hash256? stateRoot = null) + { + base.MarkAsSuccess(recipient, gasSpent, output, logs, stateRoot); + NativeCallTracerCallFrame firstCallFrame = _callStack[0]; + firstCallFrame.GasUsed = gasSpent; + firstCallFrame.Output = new ArrayPoolList(output.Length, output); + } + + public override void MarkAsFailed(Address recipient, long gasSpent, byte[]? output, string error, Hash256? stateRoot = null) + { + base.MarkAsFailed(recipient, gasSpent, output, error, stateRoot); + NativeCallTracerCallFrame firstCallFrame = _callStack[0]; + firstCallFrame.GasUsed = gasSpent; + if (output is not null) + firstCallFrame.Output = new ArrayPoolList(output.Length, output); + + EvmExceptionType errorType = _error!.Value; + firstCallFrame.Error = errorType.GetEvmExceptionDescription(); + int revertedPrefixLength = TransactionSubstate.RevertedErrorMessagePrefix.Length; + if (errorType == EvmExceptionType.Revert && error.Length > revertedPrefixLength) + { + firstCallFrame.RevertReason = ValidateRevertReason(error[revertedPrefixLength..]); + } + + 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) + { + ClearFailedLogs(childCallFrame, failed); + } + } + + private static string? ValidateRevertReason(string? errorMessage) => + errorMessage?.StartsWith("0x") == false ? errorMessage : null; +} diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerCallFrame.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerCallFrame.cs new file mode 100644 index 00000000000..21bade6992a --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerCallFrame.cs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Text.Json.Serialization; +using Nethermind.Core; +using Nethermind.Core.Collections; +using Nethermind.Int256; + +namespace Nethermind.Evm.Tracing.GethStyle.Custom.Native.Call; + +[JsonConverter(typeof(NativeCallTracerCallFrameConverter))] +public class NativeCallTracerCallFrame : IDisposable +{ + public Instruction Type { get; set; } + + public Address? From { get; set; } + + public long Gas { get; set; } + + public long GasUsed { get; set; } + + public Address? To { get; set; } + + public ArrayPoolList? Input { get; set; } + + public ArrayPoolList? Output { get; set; } + + public string? Error { get; set; } + + public string? RevertReason { get; set; } + + public ArrayPoolList Calls { get; } = new(8); + + public ArrayPoolList? Logs { get; set; } + + public UInt256? Value { get; set; } + + public void Dispose() + { + Input?.Dispose(); + Output?.Dispose(); + Logs?.Dispose(); + foreach (NativeCallTracerCallFrame childCallFrame in Calls) + { + childCallFrame.Dispose(); + } + } +} diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerCallFrameConverter.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerCallFrameConverter.cs new file mode 100644 index 00000000000..37f181cee8e --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerCallFrameConverter.cs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Nethermind.Serialization.Json; + +namespace Nethermind.Evm.Tracing.GethStyle.Custom.Native.Call; + +public class NativeCallTracerCallFrameConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, NativeCallTracerCallFrame 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.AsMemory(), options); + } + + if (value.Output?.Count > 0) + { + writer.WritePropertyName("output"u8); + JsonSerializer.Serialize(writer, value.Output.AsMemory(), 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); + } + + writer.WriteEndObject(); + } + finally + { + ForcedNumberConversion.ForcedConversion.Value = previousValue; + } + } + + public override NativeCallTracerCallFrame Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException(); + } +} diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerConfig.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerConfig.cs new file mode 100644 index 00000000000..1c7ed62fdfb --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerConfig.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Evm.Tracing.GethStyle.Custom.Native.Call; + +public class NativeCallTracerConfig +{ + public bool OnlyTopCall { get; init; } + + public bool WithLog { get; init; } +} diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerLogEntry.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerLogEntry.cs new file mode 100644 index 00000000000..55de0b3693c --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/Call/NativeCallTracerLogEntry.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Crypto; +namespace Nethermind.Evm.Tracing.GethStyle.Custom.Native.Call; + +public class NativeCallTracerLogEntry( + Address address, + byte[] data, + Hash256[] topics, + ulong position) +{ + public Address Address { get; init; } = address; + public byte[] Data { get; init; } = data; + public Hash256[] Topics { get; init; } = topics; + public ulong Position { get; init; } = position; +} diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/FourByte/Native4ByteTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/FourByte/Native4ByteTracer.cs index b15f9b8787b..4397469959a 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/FourByte/Native4ByteTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/FourByte/Native4ByteTracer.cs @@ -30,8 +30,7 @@ public sealed class Native4ByteTracer : GethLikeNativeTxTracer private readonly Dictionary _4ByteIds = new(); private Instruction _op; - public Native4ByteTracer( - GethTraceOptions options) : base(options) + public Native4ByteTracer(GethTraceOptions options) : base(options) { IsTracingActions = true; } diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/GethLikeNativeTracerFactory.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/GethLikeNativeTracerFactory.cs index 50a4c663298..b4066e51c25 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/GethLikeNativeTracerFactory.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/GethLikeNativeTracerFactory.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Nethermind.Core; +using Nethermind.Evm.Tracing.GethStyle.Custom.Native.Call; using Nethermind.Evm.Tracing.GethStyle.Custom.Native.FourByte; using Nethermind.Evm.Tracing.GethStyle.Custom.Native.Prestate; using Nethermind.State; @@ -27,6 +28,7 @@ private static void RegisterNativeTracers() { RegisterTracer(Native4ByteTracer.FourByteTracer, (options, _, _, _) => new Native4ByteTracer(options)); RegisterTracer(NativePrestateTracer.PrestateTracer, (options, block, transaction, worldState) => new NativePrestateTracer(worldState, options, transaction.SenderAddress, transaction.To, block.Beneficiary)); + RegisterTracer(NativeCallTracer.CallTracer, (options, _, transaction, _) => new NativeCallTracer(transaction, options)); } private static void RegisterTracer(string tracerName, GethLikeNativeTracerFactoryDelegate tracerDelegate) diff --git a/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/NativeTracerContext.cs b/src/Nethermind/Nethermind.Evm/Tracing/GethStyle/Custom/Native/NativeTracerContext.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Nethermind/Nethermind.Evm/Tracing/ITxTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/ITxTracer.cs index 622aaaf736d..5f1b2646abb 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/ITxTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/ITxTracer.cs @@ -125,6 +125,15 @@ public interface ITxTracer : IWorldStateTracer, IDisposable /// bool IsTracingFees { get; } + /// + /// Traces operation logs + /// + /// + /// Controls + /// - + /// + bool IsTracingLogs { get; } + bool IsTracing => IsTracingReceipt || IsTracingActions || IsTracingOpLevelStorage @@ -135,7 +144,8 @@ public interface ITxTracer : IWorldStateTracer, IDisposable || IsTracingStack || IsTracingBlockHash || IsTracingAccess - || IsTracingFees; + || IsTracingFees + || IsTracingLogs; /// /// Transaction completed successfully @@ -183,6 +193,14 @@ public interface ITxTracer : IWorldStateTracer, IDisposable /// Depends on void ReportOperationRemainingGas(long gas); + + /// + /// + /// + /// + /// Depends on + void ReportLog(LogEntry log); + /// /// /// @@ -348,7 +366,7 @@ void LoadOperationTransientStorage(Address storageCellAddress, UInt256 storageIn /// /// /// Depends on - void ReportActionRevert(long gasLeft, ReadOnlyMemory output) => ReportActionError(EvmExceptionType.Revert); + void ReportActionRevert(long gasLeft, ReadOnlyMemory output); /// /// diff --git a/src/Nethermind/Nethermind.Evm/Tracing/TxTracer.cs b/src/Nethermind/Nethermind.Evm/Tracing/TxTracer.cs index 676195cde6f..439427fb1c2 100644 --- a/src/Nethermind/Nethermind.Evm/Tracing/TxTracer.cs +++ b/src/Nethermind/Nethermind.Evm/Tracing/TxTracer.cs @@ -25,7 +25,8 @@ protected TxTracer() || IsTracingStack || IsTracingBlockHash || IsTracingAccess - || IsTracingFees; + || IsTracingFees + || IsTracingLogs; } public bool IsTracing { get; protected set; } public virtual bool IsTracingState { get; protected set; } @@ -41,6 +42,7 @@ protected TxTracer() public virtual bool IsTracingAccess { get; protected set; } public virtual bool IsTracingFees { get; protected set; } public virtual bool IsTracingStorage { get; protected set; } + public virtual bool IsTracingLogs { get; protected set; } public virtual void ReportBalanceChange(Address address, UInt256? before, UInt256? after) { } public virtual void ReportCodeChange(Address address, byte[]? before, byte[]? after) { } public virtual void ReportNonceChange(Address address, UInt256? before, UInt256? after) { } @@ -53,6 +55,7 @@ public virtual void MarkAsFailed(Address recipient, long gasSpent, byte[] output public virtual void StartOperation(int pc, Instruction opcode, long gas, in ExecutionEnvironment env) { } public virtual void ReportOperationError(EvmExceptionType error) { } public virtual void ReportOperationRemainingGas(long gas) { } + public virtual void ReportLog(LogEntry log) { } public virtual void SetOperationStack(TraceStack stack) { } public virtual void ReportStackPush(in ReadOnlySpan stackItem) { } public virtual void SetOperationMemory(TraceMemory memoryTrace) { } @@ -65,6 +68,7 @@ public virtual void ReportAction(long gas, UInt256 value, Address from, Address public virtual void ReportActionEnd(long gas, ReadOnlyMemory output) { } public virtual void ReportActionError(EvmExceptionType evmExceptionType) { } public virtual void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) { } + public virtual void ReportActionRevert(long gas, ReadOnlyMemory output) => ReportActionError(EvmExceptionType.Revert); public virtual void ReportBlockHash(Hash256 blockHash) { } public virtual void ReportByteCode(ReadOnlyMemory byteCode) { } public virtual void ReportGasUpdateForVmTrace(long refund, long gasAvailable) { } diff --git a/src/Nethermind/Nethermind.Evm/TransactionSubstate.cs b/src/Nethermind/Nethermind.Evm/TransactionSubstate.cs index 32e40f6a175..bfc253e6844 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionSubstate.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionSubstate.cs @@ -26,7 +26,7 @@ public class TransactionSubstate private const int RevertPrefix = 4; private const int WordSize = EvmPooledMemory.WordSize; - private const string RevertedErrorMessagePrefix = "Reverted "; + public const string RevertedErrorMessagePrefix = "Reverted "; public static readonly byte[] ErrorFunctionSelector = Keccak.Compute("Error(string)").BytesToArray()[..RevertPrefix]; public static readonly byte[] PanicFunctionSelector = Keccak.Compute("Panic(uint256)").BytesToArray()[..RevertPrefix]; @@ -97,53 +97,57 @@ public TransactionSubstate(ReadOnlyMemory output, ); } - private static string EncodeErrorMessage(ReadOnlySpan span) => + public static string EncodeErrorMessage(ReadOnlySpan span) => Utf8.IsValid(span) ? Encoding.UTF8.GetString(span) : span.ToHexString(true); - private string? TryGetErrorMessage(ReadOnlySpan span) + public static string? GetErrorMessage(ReadOnlySpan span) { - try + if (span.Length < RevertPrefix) return null; + ReadOnlySpan prefix = span.TakeAndMove(RevertPrefix); + UInt256 start, length; + + if (prefix.SequenceEqual(PanicFunctionSelector)) { - if (span.Length < RevertPrefix) return null; - ReadOnlySpan prefix = span.TakeAndMove(RevertPrefix); - UInt256 start, length; + if (span.Length < WordSize) return null; - if (prefix.SequenceEqual(PanicFunctionSelector)) + UInt256 panicCode = new(span.TakeAndMove(WordSize), isBigEndian: true); + if (!PanicReasons.TryGetValue(panicCode, out string panicReason)) { - if (span.Length < WordSize) return null; - - UInt256 panicCode = new(span.TakeAndMove(WordSize), isBigEndian: true); - if (!PanicReasons.TryGetValue(panicCode, out string panicReason)) - { - return $"unknown panic code ({panicCode.ToHexString(skipLeadingZeros: true)})"; - } - - return panicReason; + return $"unknown panic code ({panicCode.ToHexString(skipLeadingZeros: true)})"; } - if (span.Length < WordSize * 2) return null; + return panicReason; + } + + if (span.Length < WordSize * 2) return null; - if (prefix.SequenceEqual(ErrorFunctionSelector)) - { - start = new UInt256(span.TakeAndMove(WordSize), isBigEndian: true); - if (start != WordSize) return null; + if (prefix.SequenceEqual(ErrorFunctionSelector)) + { + start = new UInt256(span.TakeAndMove(WordSize), isBigEndian: true); + if (start != WordSize) return null; - length = new UInt256(span.TakeAndMove(WordSize), isBigEndian: true); - if (length > span.Length) return null; + length = new UInt256(span.TakeAndMove(WordSize), isBigEndian: true); + if (length > span.Length) return null; - ReadOnlySpan binaryMessage = span.TakeAndMove((int)length); - return EncodeErrorMessage(binaryMessage); + ReadOnlySpan binaryMessage = span.TakeAndMove((int)length); + return EncodeErrorMessage(binaryMessage); + } - } + start = new UInt256(span.Slice(0, WordSize), isBigEndian: true); + if (UInt256.AddOverflow(start, WordSize, out UInt256 lengthOffset) || lengthOffset > span.Length) return null; - start = new UInt256(span.Slice(0, WordSize), isBigEndian: true); - if (UInt256.AddOverflow(start, WordSize, out UInt256 lengthOffset) || lengthOffset > span.Length) return null; + length = new UInt256(span.Slice((int)start, WordSize), isBigEndian: true); + if (UInt256.AddOverflow(lengthOffset, length, out UInt256 endOffset) || endOffset != span.Length) return null; - length = new UInt256(span.Slice((int)start, WordSize), isBigEndian: true); - if (UInt256.AddOverflow(lengthOffset, length, out UInt256 endOffset) || endOffset != span.Length) return null; + span = span.Slice((int)lengthOffset, (int)length); + return EncodeErrorMessage(span); + } - span = span.Slice((int)lengthOffset, (int)length); - return EncodeErrorMessage(span); + private string? TryGetErrorMessage(ReadOnlySpan span) + { + try + { + return GetErrorMessage(span); } catch (Exception e) // shouldn't happen, just for being safe { diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 64a9c11ea86..ad870107b8f 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -2560,7 +2560,7 @@ private EvmExceptionType InstructionSelfDestruct(EvmState vmState, ref } [SkipLocalsInit] - private static EvmExceptionType InstructionLog(EvmState vmState, ref EvmStack stack, ref long gasAvailable, Instruction instruction) + private EvmExceptionType InstructionLog(EvmState vmState, ref EvmStack stack, ref long gasAvailable, Instruction instruction) where TTracing : struct, IIsTracing { if (!stack.PopUInt256(out UInt256 position)) return EvmExceptionType.StackUnderflow; @@ -2584,6 +2584,11 @@ private static EvmExceptionType InstructionLog(EvmState vmState, ref E topics); vmState.Logs.Add(logEntry); + if (_txTracer.IsTracingLogs) + { + _txTracer.ReportLog(logEntry); + } + return EvmExceptionType.None; } diff --git a/src/Nethermind/Nethermind.Test.Runner/StateTestTxTracer.cs b/src/Nethermind/Nethermind.Test.Runner/StateTestTxTracer.cs index 03c437e3439..2d00f475f49 100644 --- a/src/Nethermind/Nethermind.Test.Runner/StateTestTxTracer.cs +++ b/src/Nethermind/Nethermind.Test.Runner/StateTestTxTracer.cs @@ -32,7 +32,8 @@ public class StateTestTxTracer : ITxTracer, IDisposable public bool IsTracingBlockHash { get; } = false; public bool IsTracingAccess { get; } = false; public bool IsTracingFees => false; - public bool IsTracing => IsTracingReceipt || IsTracingActions || IsTracingOpLevelStorage || IsTracingMemory || IsTracingInstructions || IsTracingRefunds || IsTracingCode || IsTracingStack || IsTracingBlockHash || IsTracingAccess || IsTracingFees; + public bool IsTracingLogs => false; + public bool IsTracing => IsTracingReceipt || IsTracingActions || IsTracingOpLevelStorage || IsTracingMemory || IsTracingInstructions || IsTracingRefunds || IsTracingCode || IsTracingStack || IsTracingBlockHash || IsTracingAccess || IsTracingFees || IsTracingLogs; public void MarkAsSuccess(Address recipient, long gasSpent, byte[] output, LogEntry[] logs, Hash256 stateRoot = null) @@ -153,6 +154,10 @@ public void ReportStorageChange(in ReadOnlySpan key, in ReadOnlySpan { } + public void ReportLog(LogEntry log) + { + } + public void SetOperationStorage(Address address, UInt256 storageIndex, ReadOnlySpan newValue, ReadOnlySpan currentValue) { } @@ -211,6 +216,11 @@ public void ReportActionError(EvmExceptionType exceptionType) throw new NotSupportedException(); } + public void ReportActionRevert(long gas, ReadOnlyMemory output) + { + throw new NotSupportedException(); + } + public void ReportActionEnd(long gas, Address deploymentAddress, ReadOnlyMemory deployedCode) { throw new NotSupportedException();