From 034ce5d90065428bac63a1f1b59be33f97ba90bf Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 10 Oct 2025 15:31:34 +0300 Subject: [PATCH 01/32] Add test to check filtering per toAddress --- .../ts/tests/evm-tracing/traceFilter.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts index 0661f998b..ab63a64db 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts @@ -22,7 +22,7 @@ describe("test trace filter logic", () => { devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); }, 60 * 1000); - it("should support filtering trace per fromAddress", async () => { + it("should support filtering trace per fromAddress/toAddress", async () => { const [alice, bob] = devClients; const txHash = await alice.sendTransaction({ @@ -34,7 +34,7 @@ describe("test trace filter logic", () => { }); const blockNumberHex = txReceipt.blockNumber.toString(16); - const response = await customRpcRequest( + const responsePerFrom = await customRpcRequest( node.meta.rpcUrlHttp, "trace_filter", [ @@ -46,7 +46,22 @@ describe("test trace filter logic", () => { ], ); - expect(response.length).to.equal(1); - expect(txHash).to.equal(response[0].transactionHash); + expect(responsePerFrom.length).to.equal(1); + expect(txHash).to.equal(responsePerFrom[0].transactionHash); + + const responsePerTo = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + toAddress: [bob.account.address], + }, + ], + ); + + expect(responsePerTo.length).to.equal(1); + expect(txHash).to.equal(responsePerTo[0].transactionHash); }); }); From 886da34018ed7275dbf7db274a66e6ed959beb02 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 10 Oct 2025 15:35:52 +0300 Subject: [PATCH 02/32] Add test to check tracing range of blocks --- .../ts/tests/evm-tracing/traceFilter.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts index ab63a64db..1641bcf95 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts @@ -22,6 +22,50 @@ describe("test trace filter logic", () => { devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); }, 60 * 1000); + it("should support tracing range of blocks", async () => { + const [alice, bob] = devClients; + + const firstTxHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + const firstTxReceipt = await publicClient.waitForTransactionReceipt({ + hash: firstTxHash, + }); + + const firstBlockNumber = firstTxReceipt.blockNumber; + const firstBlockNumberHex = firstTxReceipt.blockNumber.toString(16); + + const secondTxHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + const secondTxReceipt = await publicClient.waitForTransactionReceipt({ + hash: secondTxHash, + }); + + const secondBlockNumber = secondTxReceipt.blockNumber; + const secondBlockNumberHex = secondTxReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: firstBlockNumberHex, + toBlock: secondBlockNumberHex, + }, + ], + ); + + expect(BigInt(response.length)).to.equal(secondBlockNumber); + + for (const index in response.length) { + expect(response[index].blockNumber).to.equal(firstBlockNumber + index); + expect(response[index].transactionPosition).to.equal(0); + } + }); + it("should support filtering trace per fromAddress/toAddress", async () => { const [alice, bob] = devClients; From 5e7b764ea54c18c3288b42de57adf6fe402aadb9 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 10 Oct 2025 16:10:05 +0300 Subject: [PATCH 03/32] Add heavy abi contract --- .../e2e-tests/ts/lib/abis/debugTrace/heavy.ts | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 utils/e2e-tests/ts/lib/abis/debugTrace/heavy.ts diff --git a/utils/e2e-tests/ts/lib/abis/debugTrace/heavy.ts b/utils/e2e-tests/ts/lib/abis/debugTrace/heavy.ts new file mode 100644 index 000000000..6a4f49561 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/debugTrace/heavy.ts @@ -0,0 +1,298 @@ +// pragma solidity >=0.8.3; +// +// contract Heavy { +// constructor(bool should_revert) { +// if (should_revert) { +// revert(); +// } +// } +// +// function call_ok() public pure {} +// +// function call_revert() public pure { +// revert(); +// } +// +// function subcalls(address target0, address target1) public pure { +// try Heavy(target0).subsubcalls(target1) {} catch {} +// try Heavy(target0).subsubcalls(target1) {} catch {} +// } +// +// function subsubcalls(address target1) public pure { +// Heavy(target1).call_ok(); +// Heavy(target1).call_revert(); +// } +// +// function heavy_steps(uint256 store_steps, uint256 op_steps) external { +// while (store_steps != 0) { +// assembly { +// sstore(store_steps, store_steps) +// } +// store_steps--; +// } +// +// while (op_steps != 0) { +// op_steps--; +// } +// } +// +// // This part is to trace Wasm memory overflow +// uint256 public a; +// uint256 public b; +// uint256 public c; +// uint256 public d; +// uint256 public e; +// uint256 public f; +// uint256 public g; +// uint256 public h; +// uint256 public i; +// uint256 public j; +// +// function set_and_loop(uint256 loops) public returns (uint256 result) { +// a = 1; +// b = 1; +// c = 1; +// d = 1; +// e = 1; +// f = 1; +// g = 1; +// h = 1; +// i = 1; +// j = 1; +// uint256 count = 0; +// while (i < loops) { +// count += 1; +// } +// return 1; +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "bool", + name: "should_revert", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "a", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "b", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "c", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "call_ok", + outputs: [], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "call_revert", + outputs: [], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "d", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "e", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "f", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "g", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "h", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "store_steps", + type: "uint256", + }, + { + internalType: "uint256", + name: "op_steps", + type: "uint256", + }, + ], + name: "heavy_steps", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "i", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "j", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "loops", + type: "uint256", + }, + ], + name: "set_and_loop", + outputs: [ + { + internalType: "uint256", + name: "result", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target0", + type: "address", + }, + { + internalType: "address", + name: "target1", + type: "address", + }, + ], + name: "subcalls", + outputs: [], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target1", + type: "address", + }, + ], + name: "subsubcalls", + outputs: [], + stateMutability: "pure", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506040516108503803806108508339818101604052810190602e91906070565b80156037575f5ffd5b506096565b5f5ffd5b5f8115159050919050565b6052816040565b8114605b575f5ffd5b50565b5f81519050606a81604b565b92915050565b5f602082840312156082576081603c565b5b5f608d84828501605e565b91505092915050565b6107ad806100a35f395ff3fe608060405234801561000f575f5ffd5b50600436106100fe575f3560e01c8063b582ec5f11610095578063e2179b8e11610064578063e2179b8e14610250578063e5aa3d581461026e578063f34f16101461028c578063ffae15ba146102a8576100fe565b8063b582ec5f146101ec578063b8c9d3651461020a578063c3da42b814610228578063cb30e69614610246576100fe565b80635eaf9bc1116100d15780635eaf9bc11461018c5780636422847b146101965780638a054ac2146101b2578063a885f4e3146101d0576100fe565b80630dbe671f1461010257806313128fdc1461012057806326121ff0146101505780634df7e3d01461016e575b5f5ffd5b61010a6102c6565b6040516101179190610555565b60405180910390f35b61013a6004803603810190610135919061059c565b6102cb565b6040516101479190610555565b60405180910390f35b610158610347565b6040516101659190610555565b60405180910390f35b61017661034d565b6040516101839190610555565b60405180910390f35b610194610353565b005b6101b060048036038101906101ab91906105c7565b610355565b005b6101ba610392565b6040516101c79190610555565b60405180910390f35b6101ea60048036038101906101e5919061065f565b610398565b005b6101f461044f565b6040516102019190610555565b60405180910390f35b610212610455565b60405161021f9190610555565b60405180910390f35b61023061045b565b60405161023d9190610555565b60405180910390f35b61024e610461565b005b610258610465565b6040516102659190610555565b60405180910390f35b61027661046b565b6040516102839190610555565b60405180910390f35b6102a660048036038101906102a1919061068a565b610471565b005b6102b0610537565b6040516102bd9190610555565b60405180910390f35b5f5481565b5f60015f8190555060018081905550600160028190555060016003819055506001600481905550600160058190555060016006819055506001600781905550600160088190555060016009819055505f5f90505b82600854101561033d5760018161033691906106f5565b905061031f565b6001915050919050565b60055481565b60015481565b565b5b5f821461037357818255818061036b90610728565b925050610356565b5b5f811461038e57808061038690610728565b915050610374565b5050565b60035481565b8073ffffffffffffffffffffffffffffffffffffffff16635eaf9bc16040518163ffffffff1660e01b81526004015f6040518083038186803b1580156103dc575f5ffd5b505afa1580156103ee573d5f5f3e3d5ffd5b505050508073ffffffffffffffffffffffffffffffffffffffff1663cb30e6966040518163ffffffff1660e01b81526004015f6040518083038186803b158015610436575f5ffd5b505afa158015610448573d5f5f3e3d5ffd5b5050505050565b60095481565b60075481565b60025481565b5f5ffd5b60065481565b60085481565b8173ffffffffffffffffffffffffffffffffffffffff1663a885f4e3826040518263ffffffff1660e01b81526004016104aa919061075e565b5f6040518083038186803b1580156104c0575f5ffd5b505afa9250505080156104d1575060015b508173ffffffffffffffffffffffffffffffffffffffff1663a885f4e3826040518263ffffffff1660e01b815260040161050b919061075e565b5f6040518083038186803b158015610521575f5ffd5b505afa925050508015610532575060015b505050565b60045481565b5f819050919050565b61054f8161053d565b82525050565b5f6020820190506105685f830184610546565b92915050565b5f5ffd5b61057b8161053d565b8114610585575f5ffd5b50565b5f8135905061059681610572565b92915050565b5f602082840312156105b1576105b061056e565b5b5f6105be84828501610588565b91505092915050565b5f5f604083850312156105dd576105dc61056e565b5b5f6105ea85828601610588565b92505060206105fb85828601610588565b9150509250929050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61062e82610605565b9050919050565b61063e81610624565b8114610648575f5ffd5b50565b5f8135905061065981610635565b92915050565b5f602082840312156106745761067361056e565b5b5f6106818482850161064b565b91505092915050565b5f5f604083850312156106a05761069f61056e565b5b5f6106ad8582860161064b565b92505060206106be8582860161064b565b9150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6106ff8261053d565b915061070a8361053d565b9250828201905080821115610722576107216106c8565b5b92915050565b5f6107328261053d565b91505f8203610744576107436106c8565b5b600182039050919050565b61075881610624565b82525050565b5f6020820190506107715f83018461074f565b9291505056fea26469706673582212203a48031a3a6911e44a2dfe636fa7c6f4de7214d2c0461bd2ff205366af22bb6664736f6c634300081e0033", +} as const; From 686dddecb1ada4dcdf0b53347df0976aab88ad9b Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 10 Oct 2025 16:16:20 +0300 Subject: [PATCH 04/32] Add looper abi contract --- .../ts/lib/abis/debugTrace/looper.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 utils/e2e-tests/ts/lib/abis/debugTrace/looper.ts diff --git a/utils/e2e-tests/ts/lib/abis/debugTrace/looper.ts b/utils/e2e-tests/ts/lib/abis/debugTrace/looper.ts new file mode 100644 index 000000000..fa8dc500f --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/debugTrace/looper.ts @@ -0,0 +1,57 @@ +// pragma solidity >=0.8.3; +// +// contract Looper { +// uint256 public count; +// +// function infinite() public pure { +// while (true) {} +// } +// +// function incrementalLoop(uint256 n) public { +// uint256 i = 0; +// while (i < n) { +// count = count + 1; +// i += 1; +// } +// } +// } + +export default { + abi: [ + { + inputs: [], + name: "count", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "n", + type: "uint256", + }, + ], + name: "incrementalLoop", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "infinite", + outputs: [], + stateMutability: "pure", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506101ed8061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c806306661abd146100435780635bec9e67146100615780636e4709f91461006b575b5f5ffd5b61004b610087565b60405161005891906100e5565b60405180910390f35b61006961008c565b005b6100856004803603810190610080919061012c565b610095565b005b5f5481565b5b600161008d57565b5f5f90505b818110156100c95760015f546100b09190610184565b5f819055506001816100c29190610184565b905061009a565b5050565b5f819050919050565b6100df816100cd565b82525050565b5f6020820190506100f85f8301846100d6565b92915050565b5f5ffd5b61010b816100cd565b8114610115575f5ffd5b50565b5f8135905061012681610102565b92915050565b5f60208284031215610141576101406100fe565b5b5f61014e84828501610118565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61018e826100cd565b9150610199836100cd565b92508282019050808211156101b1576101b0610157565b5b9291505056fea26469706673582212202018b089162ffbb47bf3ea8487f4a122e87b63c21811d615bec9e32546e3f58064736f6c634300081e0033", +} as const; From 1138f5984c1b13e8ea442f4111a229979fb412ff Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 10 Oct 2025 16:43:35 +0300 Subject: [PATCH 05/32] Add tests for trace_filter to check gas used in loops --- .../tests/evm-tracing/traceFilterGasLoop.ts | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts new file mode 100644 index 000000000..20eb5cf86 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts @@ -0,0 +1,141 @@ +// Constants related to used gas in loops can be different on various EVM versions. + +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import looper from "../../lib/abis/debugTrace/looper"; +import { customRpcRequest } from "../../lib/rpcUtils"; +import { encodeFunctionData } from "viem"; + +describe("test trace filter logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=trace", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let looperAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployLooperContractTxHash = await alice.deployContract({ + abi: looper.abi, + bytecode: looper.bytecode, + }); + const deployLooperContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployLooperContractTxHash, + timeout: 18_000, + }); + looperAddress = deployLooperContractTxReceipt.contractAddress!; + }); + + it("should return 21653 gasUsed for 0 loop", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + data: encodeFunctionData({ + abi: looper.abi, + functionName: "incrementalLoop", + args: [0n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response[0].result).to.not.be.undefined; + expect(response[0].result.error).to.not.exist; + expect(response[0].result.gasUsed).to.equal("0x5495"); + }); + + it("should return 106265 gasUsed for 100 loops", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + data: encodeFunctionData({ + abi: looper.abi, + functionName: "incrementalLoop", + args: [100n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response[0].result).to.not.be.undefined; + expect(response[0].result.error).to.not.exist; + expect(response[0].result.gasUsed).to.equal("0x19f19"); + }); + + it("should return 670577 gasUsed for 1000 loops", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + data: encodeFunctionData({ + abi: looper.abi, + functionName: "incrementalLoop", + args: [1000n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response[0].result).to.not.be.undefined; + expect(response[0].result.error).to.not.exist; + expect(response[0].result.gasUsed).to.equal("0xa3b71"); + }); +}); From abed854e7c727d4ca5d3aaf0dd9930d439d3dbce Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 10 Oct 2025 16:52:23 +0300 Subject: [PATCH 06/32] Rename traceFilter tests to traceFilterGeneral --- utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts | 2 +- .../tests/evm-tracing/{traceFilter.ts => traceFilterGeneral.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename utils/e2e-tests/ts/tests/evm-tracing/{traceFilter.ts => traceFilterGeneral.ts} (98%) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts index 20eb5cf86..c9a3d9c67 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts @@ -8,7 +8,7 @@ import looper from "../../lib/abis/debugTrace/looper"; import { customRpcRequest } from "../../lib/rpcUtils"; import { encodeFunctionData } from "viem"; -describe("test trace filter logic", () => { +describe("test trace filter for gas used in loops logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts similarity index 98% rename from utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts rename to utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts index 1641bcf95..d2faeaead 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilter.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts @@ -4,7 +4,7 @@ import * as eth from "../../lib/ethViem"; import { beforeEachWithCleanup } from "../../lib/lifecycle"; import { customRpcRequest } from "../../lib/rpcUtils"; -describe("test trace filter logic", () => { +describe("test trace filter for general logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; From 3bb5c86331bca8adad3c249247291e9d007ed9f9 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Tue, 14 Oct 2025 11:33:19 +0300 Subject: [PATCH 07/32] Add tests that check max count numbder at trace_filter --- .../tests/evm-tracing/traceFilterGeneral.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts index d2faeaead..553e1b3a6 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts @@ -108,4 +108,49 @@ describe("test trace filter for general logic", () => { expect(responsePerTo.length).to.equal(1); expect(txHash).to.equal(responsePerTo[0].transactionHash); }); + + it("should check default max 500 traces request", async () => { + const [alice, bob] = devClients; + + const txHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const responseSuccess = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + count: 500, + }, + ], + ); + + expect(responseSuccess.length).to.equal(1); + expect(txHash).to.equal(responseSuccess[0].transactionHash); + + await customRpcRequest(node.meta.rpcUrlHttp, "trace_filter", [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + count: 501, + }, + ]).then( + () => { + expect.fail("should not succeed"); + }, + (error) => { + expect(error.message).to.eq( + "count (501) can't be greater than maximum (500)", + ); + }, + ); + }); }); From a8896b5a0f577678d9152b4ddad6daf6fe0b28d8 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Tue, 14 Oct 2025 15:12:36 +0300 Subject: [PATCH 08/32] Rename directory containing abis for evm tracing tests --- .../ts/lib/abis/{debugTrace => evmTracing}/callee.ts | 0 .../ts/lib/abis/{debugTrace => evmTracing}/caller.ts | 0 .../e2e-tests/ts/lib/abis/{debugTrace => evmTracing}/heavy.ts | 0 .../ts/lib/abis/{debugTrace => evmTracing}/looper.ts | 0 utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts | 4 ++-- utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts | 2 +- 6 files changed, 3 insertions(+), 3 deletions(-) rename utils/e2e-tests/ts/lib/abis/{debugTrace => evmTracing}/callee.ts (100%) rename utils/e2e-tests/ts/lib/abis/{debugTrace => evmTracing}/caller.ts (100%) rename utils/e2e-tests/ts/lib/abis/{debugTrace => evmTracing}/heavy.ts (100%) rename utils/e2e-tests/ts/lib/abis/{debugTrace => evmTracing}/looper.ts (100%) diff --git a/utils/e2e-tests/ts/lib/abis/debugTrace/callee.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts similarity index 100% rename from utils/e2e-tests/ts/lib/abis/debugTrace/callee.ts rename to utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts diff --git a/utils/e2e-tests/ts/lib/abis/debugTrace/caller.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts similarity index 100% rename from utils/e2e-tests/ts/lib/abis/debugTrace/caller.ts rename to utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts diff --git a/utils/e2e-tests/ts/lib/abis/debugTrace/heavy.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/heavy.ts similarity index 100% rename from utils/e2e-tests/ts/lib/abis/debugTrace/heavy.ts rename to utils/e2e-tests/ts/lib/abis/evmTracing/heavy.ts diff --git a/utils/e2e-tests/ts/lib/abis/debugTrace/looper.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/looper.ts similarity index 100% rename from utils/e2e-tests/ts/lib/abis/debugTrace/looper.ts rename to utils/e2e-tests/ts/lib/abis/evmTracing/looper.ts diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts index 3a6110984..fc34e4ac1 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts @@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it } from "vitest"; import { RunNodeState, runNode } from "../../lib/node"; import * as eth from "../../lib/ethViem"; import { beforeEachWithCleanup } from "../../lib/lifecycle"; -import callee from "../../lib/abis/debugTrace/callee"; -import caller from "../../lib/abis/debugTrace/caller"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts index c9a3d9c67..4b9691ebb 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { RunNodeState, runNode } from "../../lib/node"; import * as eth from "../../lib/ethViem"; import { beforeEachWithCleanup } from "../../lib/lifecycle"; -import looper from "../../lib/abis/debugTrace/looper"; +import looper from "../../lib/abis/evmTracing/looper"; import { customRpcRequest } from "../../lib/rpcUtils"; import { encodeFunctionData } from "viem"; From 30f75be571d09dfc2c520cff24c387ef67f9fa65 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Tue, 14 Oct 2025 18:08:54 +0300 Subject: [PATCH 09/32] Add tests for trace_filter to check heavy contracts logic --- .../ts/tests/evm-tracing/traceFilterHeavy.ts | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts new file mode 100644 index 000000000..8d870ca00 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts @@ -0,0 +1,175 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import heavy from "../../lib/abis/evmTracing/heavy"; +import { customRpcRequest } from "../../lib/rpcUtils"; +import { encodeFunctionData } from "viem"; + +describe("test trace filter for heavy contract logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=trace", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let heavyContracts: { + address: `0x${string}`; + blockNumberHex: string; + txHash: `0x${string}`; + }[] = []; + + beforeEach(async () => { + const [alice, _] = devClients; + + for (let index = 0; index < 4; index++) { + let shouldRevert = false; + let gas; + + if (index == 3) { + shouldRevert = true; + gas = 150_000n; // should be increased for revert logic. + } + + const deployHeavyContractTxHash = await alice.deployContract({ + abi: heavy.abi, + bytecode: heavy.bytecode, + gas: gas, + args: [shouldRevert], + }); + + const deployHeavyContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployHeavyContractTxHash, + timeout: 18_000, + }); + + heavyContracts.push({ + address: deployHeavyContractTxReceipt.contractAddress!, + blockNumberHex: deployHeavyContractTxReceipt.blockNumber.toString(16), + txHash: deployHeavyContractTxReceipt.transactionHash, + }); + } + }); + + it("should be able to replay deployed contract", async () => { + const [alice, _] = devClients; + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: heavyContracts[0]!.blockNumberHex, + toBlock: heavyContracts[0]!.blockNumberHex, + }, + ], + ); + + expect(response.length).to.equal(1); + + expect(response[0].action).to.include({ + creationMethod: "create", + from: alice.account.address.toLocaleLowerCase(), + gas: "0x66fa5", + value: "0x0", + }); + + expect(response[0]).to.include({ + blockNumber: 1, + subtraces: 0, + transactionHash: heavyContracts[0]!.txHash, + transactionPosition: 0, + type: "create", + }); + }); + + it("should be able to replay reverted contract", async () => { + const [alice, _] = devClients; + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: heavyContracts[3]!.blockNumberHex, + toBlock: heavyContracts[3]!.blockNumberHex, + }, + ], + ); + + expect(response.length).to.equal(1); + expect(response[0].action.creationMethod).to.equal("create"); + expect(response[0].action.from).to.equal( + alice.account.address.toLocaleLowerCase(), + ); + expect(response[0].action.gas).to.equal("0xf576"); + expect(response[0].action.init).to.be.a("string"); + expect(response[0].action.value).to.equal("0x0"); + expect(response[0].blockHash).to.be.a("string"); + expect(response[0].blockNumber).to.equal(4); + expect(response[0].result).to.equal(undefined); + expect(response[0].error).to.equal("Reverted"); + expect(response[0].subtraces).to.equal(0); + expect(response[0].traceAddress.length).to.equal(0); + expect(response[0].transactionHash).to.equal(heavyContracts[3]!.txHash); + expect(response[0].transactionPosition).to.equal(0); + expect(response[0].type).to.equal("create"); + }); + + it("should be able to trace sub-call with reverts", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: heavyContracts[0]!.address, + data: encodeFunctionData({ + abi: heavy.abi, + functionName: "subcalls", + args: [heavyContracts[1]!.address, heavyContracts[2]!.address], + }), + gas: 1000_000n, + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "trace_filter", + [ + { + fromBlock: blockNumberHex, + toBlock: blockNumberHex, + }, + ], + ); + + expect(response.length).to.equal(7); + expect(response[0].subtraces).to.equal(2); + expect(response[0].traceAddress).to.deep.equal([]); + expect(response[1].subtraces).to.equal(2); + expect(response[1].traceAddress).to.deep.equal([0]); + expect(response[2].subtraces).to.equal(0); + expect(response[2].traceAddress).to.deep.equal([0, 0]); + expect(response[3].subtraces).to.equal(0); + expect(response[3].traceAddress).to.deep.equal([0, 1]); + expect(response[4].subtraces).to.equal(2); + expect(response[4].traceAddress).to.deep.equal([1]); + expect(response[5].subtraces).to.equal(0); + expect(response[5].traceAddress).to.deep.equal([1, 0]); + expect(response[6].subtraces).to.equal(0); + expect(response[6].traceAddress).to.deep.equal([1, 1]); + }); +}); From 6126724d9a9a943ce0584bbf33b693b1a35ae378 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 15 Oct 2025 17:59:01 +0300 Subject: [PATCH 10/32] Separate debugTrace related tests to separate files --- .../ts/tests/evm-tracing/debugTrace.ts | 169 ------------------ .../ts/tests/evm-tracing/debugTraceBlockBy.ts | 92 ++++++++++ .../ts/tests/evm-tracing/debugTraceCall.ts | 94 ++++++++++ .../evm-tracing/debugTraceTransaction.ts | 89 +++++++++ 4 files changed, 275 insertions(+), 169 deletions(-) delete mode 100644 utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts create mode 100644 utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockBy.ts create mode 100644 utils/e2e-tests/ts/tests/evm-tracing/debugTraceCall.ts create mode 100644 utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts deleted file mode 100644 index fc34e4ac1..000000000 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTrace.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { RunNodeState, runNode } from "../../lib/node"; -import * as eth from "../../lib/ethViem"; -import { beforeEachWithCleanup } from "../../lib/lifecycle"; -import callee from "../../lib/abis/evmTracing/callee"; -import caller from "../../lib/abis/evmTracing/caller"; -import { encodeFunctionData } from "viem"; -import { customRpcRequest } from "../../lib/rpcUtils"; - -describe("test debug trace logic", () => { - let node: RunNodeState; - let publicClient: eth.PublicClientWebSocket; - let devClients: eth.DevClientsWebSocket; - beforeEachWithCleanup(async (cleanup) => { - node = runNode( - { - args: ["--tracing-mode=debug", "--dev", "--tmp"], - }, - cleanup.push, - ); - - await node.waitForBoot; - - publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); - devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); - }, 60 * 1000); - - let calleeAddress: `0x${string}`; - let callerAddress: `0x${string}`; - - beforeEach(async () => { - const [alice, _] = devClients; - - const deployCalleeContractTxHash = await alice.deployContract({ - abi: callee.abi, - bytecode: callee.bytecode, - }); - const deployCalleeContractTxReceipt = - await publicClient.waitForTransactionReceipt({ - hash: deployCalleeContractTxHash, - timeout: 18_000, - }); - calleeAddress = deployCalleeContractTxReceipt.contractAddress!; - - const deployCallerContractTxHash = await alice.deployContract({ - abi: caller.abi, - bytecode: caller.bytecode, - }); - const deployCallerContractTxReceipt = - await publicClient.waitForTransactionReceipt({ - hash: deployCallerContractTxHash, - timeout: 18_000, - }); - callerAddress = deployCallerContractTxReceipt.contractAddress!; - }); - - describe("debug_traceCall tests", () => { - it("should trace nested contract calls", async () => { - const [alice, bob] = devClients; - - const dummyTx = await alice.sendTransaction({ - to: bob.account.address, - value: 1000n, - }); - await publicClient.waitForTransactionReceipt({ hash: dummyTx }); - - const callParams = { - to: callerAddress, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "someAction", - args: [calleeAddress, 7n], - }), - }; - - const response = await customRpcRequest( - node.meta.rpcUrlHttp, - "debug_traceCall", - [callParams, "latest"], - ); - - const logs: any[] = []; - for (const log of response.structLogs) { - if (logs.length === 1) { - logs.push(log); - } - if (log.op === "RETURN") { - logs.push(log); - } - } - expect(logs).to.be.lengthOf(2); - expect(logs[0].depth).to.be.equal(2); - expect(logs[1].depth).to.be.equal(1); - }); - }); - - describe("debug_traceTransaction tests", () => { - it("should trace nested contract calls", async () => { - const [alice, _] = devClients; - - const txHash = await alice.sendTransaction({ - to: callerAddress, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "someAction", - args: [calleeAddress, 7n], - }), - }); - await publicClient.waitForTransactionReceipt({ hash: txHash }); - - const response = await customRpcRequest( - node.meta.rpcUrlHttp, - "debug_traceTransaction", - [txHash], - ); - - const logs: any[] = []; - for (const log of response.structLogs) { - if (logs.length === 1) { - logs.push(log); - } - if (log.op === "RETURN") { - logs.push(log); - } - } - expect(logs).to.be.lengthOf(2); - expect(logs[0].depth).to.be.equal(2); - expect(logs[1].depth).to.be.equal(1); - }); - }); - - describe("debug_traceBlockByNumber and debug_traceBlockByHash tests", () => { - it("should trace block by number and hash", async () => { - const [alice, _] = devClients; - - const txHash = await alice.sendTransaction({ - to: callerAddress, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "someAction", - args: [calleeAddress, 7n], - }), - }); - const txReceipt = await publicClient.waitForTransactionReceipt({ - hash: txHash, - }); - const blockNumberHex = txReceipt.blockNumber.toString(16); - const blockHash = txReceipt.blockHash; - - const responseByNumber = await customRpcRequest( - node.meta.rpcUrlHttp, - "debug_traceBlockByNumber", - [blockNumberHex, { tracer: "callTracer" }], - ); - - expect(responseByNumber.length).to.equal(1); - expect(txHash).to.equal(responseByNumber[0].txHash); - - const responseByHash = await customRpcRequest( - node.meta.rpcUrlHttp, - "debug_traceBlockByHash", - [blockHash, { tracer: "callTracer" }], - ); - - expect(responseByHash.length).to.equal(1); - expect(txHash).to.equal(responseByHash[0].txHash); - }); - }); -}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockBy.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockBy.ts new file mode 100644 index 000000000..a81d08516 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockBy.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("test debug trace block by number or hash logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let calleeAddress: `0x${string}`; + let callerAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + callerAddress = deployCallerContractTxReceipt.contractAddress!; + }); + + it("should trace block by number and hash", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + const blockHash = txReceipt.blockHash; + + const responseByNumber = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByNumber", + [blockNumberHex, { tracer: "callTracer" }], + ); + + expect(responseByNumber.length).to.equal(1); + expect(txHash).to.equal(responseByNumber[0].txHash); + + const responseByHash = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByHash", + [blockHash, { tracer: "callTracer" }], + ); + + expect(responseByHash.length).to.equal(1); + expect(txHash).to.equal(responseByHash[0].txHash); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCall.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCall.ts new file mode 100644 index 000000000..d00fab0b1 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCall.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("test debug trace call logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let calleeAddress: `0x${string}`; + let callerAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + callerAddress = deployCallerContractTxReceipt.contractAddress!; + }); + + it("should trace nested contract calls", async () => { + const [alice, bob] = devClients; + + const dummyTx = await alice.sendTransaction({ + to: bob.account.address, + value: 1000n, + }); + await publicClient.waitForTransactionReceipt({ hash: dummyTx }); + + const callParams = { + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }; + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceCall", + [callParams, "latest"], + ); + + const logs: any[] = []; + for (const log of response.structLogs) { + if (logs.length === 1) { + logs.push(log); + } + if (log.op === "RETURN") { + logs.push(log); + } + } + expect(logs).to.be.lengthOf(2); + expect(logs[0].depth).to.be.equal(2); + expect(logs[1].depth).to.be.equal(1); + }); +}); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts new file mode 100644 index 000000000..f6becb751 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("test debug trace transaction logic", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let calleeAddress: `0x${string}`; + let callerAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + callerAddress = deployCallerContractTxReceipt.contractAddress!; + }); + + it("should trace nested contract calls", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash], + ); + + const logs: any[] = []; + for (const log of response.structLogs) { + if (logs.length === 1) { + logs.push(log); + } + if (log.op === "RETURN") { + logs.push(log); + } + } + expect(logs).to.be.lengthOf(2); + expect(logs[0].depth).to.be.equal(2); + expect(logs[1].depth).to.be.equal(1); + }); +}); From 021dfcd3c38c3b7775920a78946f4731072b4862 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Thu, 16 Oct 2025 14:49:32 +0300 Subject: [PATCH 11/32] Extend debugTraceTransaction tests with the one that test big responses --- .../evm-tracing/debugTraceTransaction.ts | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index f6becb751..0fe1bbe9e 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -1,9 +1,10 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { RunNodeState, runNode } from "../../lib/node"; import * as eth from "../../lib/ethViem"; import { beforeEachWithCleanup } from "../../lib/lifecycle"; import callee from "../../lib/abis/evmTracing/callee"; import caller from "../../lib/abis/evmTracing/caller"; +import heavy from "../../lib/abis/evmTracing/heavy"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; @@ -25,37 +26,34 @@ describe("test debug trace transaction logic", () => { devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); }, 60 * 1000); - let calleeAddress: `0x${string}`; - let callerAddress: `0x${string}`; - - beforeEach(async () => { + it("should trace nested contract calls", async () => { const [alice, _] = devClients; const deployCalleeContractTxHash = await alice.deployContract({ abi: callee.abi, bytecode: callee.bytecode, }); + const deployCalleeContractTxReceipt = await publicClient.waitForTransactionReceipt({ hash: deployCalleeContractTxHash, timeout: 18_000, }); - calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const calleeAddress = deployCalleeContractTxReceipt.contractAddress!; const deployCallerContractTxHash = await alice.deployContract({ abi: caller.abi, bytecode: caller.bytecode, }); + const deployCallerContractTxReceipt = await publicClient.waitForTransactionReceipt({ hash: deployCallerContractTxHash, timeout: 18_000, }); - callerAddress = deployCallerContractTxReceipt.contractAddress!; - }); - it("should trace nested contract calls", async () => { - const [alice, _] = devClients; + const callerAddress = deployCallerContractTxReceipt.contractAddress!; const txHash = await alice.sendTransaction({ to: callerAddress, @@ -86,4 +84,47 @@ describe("test debug trace transaction logic", () => { expect(logs[0].depth).to.be.equal(2); expect(logs[1].depth).to.be.equal(1); }); + + it("should not trace call that would produce too big responses", async () => { + const [alice, _] = devClients; + + const deployHeavyContractTxHash = await alice.deployContract({ + abi: heavy.abi, + bytecode: heavy.bytecode, + args: [false], + }); + + const deployHeavyContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployHeavyContractTxHash, + timeout: 18_000, + }); + + const heavyAddress = deployHeavyContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: heavyAddress, + gasLimit: "0x800000", + value: 0n, + data: encodeFunctionData({ + abi: heavy.abi, + functionName: "heavy_steps", + args: [100n, 1000n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + await customRpcRequest(node.meta.rpcUrlHttp, "debug_traceTransaction", [ + txHash, + ]).then( + () => { + expect.fail("trace should be reverted but it worked instead"); + }, + (error) => { + expect(error.message).to.eq( + "replayed transaction generated too much data. try disabling memory or storage?", + ); + }, + ); + }); }); From 73691490ff7f38573a7150d1160c01be4787ef5a Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 09:58:54 +0300 Subject: [PATCH 12/32] Extend debugTraceTransaction tests with the one that prevents wasm memory overflow --- .../evm-tracing/debugTraceTransaction.ts | 42 +++++++++++++++++++ .../ts/tests/evm-tracing/traceFilterHeavy.ts | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index 0fe1bbe9e..507538bd5 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -85,6 +85,48 @@ describe("test debug trace transaction logic", () => { expect(logs[1].depth).to.be.equal(1); }); + it("should prevent wasm memory overflow", async () => { + const [alice, _] = devClients; + + const deployHeavyContractTxHash = await alice.deployContract({ + abi: heavy.abi, + bytecode: heavy.bytecode, + args: [false], + }); + + const deployHeavyContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployHeavyContractTxHash, + timeout: 18_000, + }); + + const heavyAddress = deployHeavyContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: heavyAddress, + gas: 1_000_000n, + data: encodeFunctionData({ + abi: heavy.abi, + functionName: "set_and_loop", + args: [10n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + await customRpcRequest(node.meta.rpcUrlHttp, "debug_traceTransaction", [ + txHash, + ]).then( + () => { + expect.fail("trace should be reverted but it worked instead"); + }, + (error) => { + expect(error.message).to.eq( + "replayed transaction generated too much data. try disabling memory or storage?", + ); + }, + ); + }); + it("should not trace call that would produce too big responses", async () => { const [alice, _] = devClients; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts index 8d870ca00..cf0b0a136 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts @@ -138,7 +138,7 @@ describe("test trace filter for heavy contract logic", () => { functionName: "subcalls", args: [heavyContracts[1]!.address, heavyContracts[2]!.address], }), - gas: 1000_000n, + gas: 1_000_000n, }); const txReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash, From 4d1be3ab19472eadc82484c49eef375411a76cae Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 10:07:06 +0300 Subject: [PATCH 13/32] Extend debugTraceTransaction tests with the one that checks using optional disable params --- .../evm-tracing/debugTraceTransaction.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index 507538bd5..229aabc5e 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -85,6 +85,36 @@ describe("test debug trace transaction logic", () => { expect(logs[1].depth).to.be.equal(1); }); + it("should use optional disable parameters", async () => { + const [alice, bob] = devClients; + + const txHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [ + txHash, + { disableMemory: true, disableStack: true, disableStorage: true }, + ], + ); + + const logs: any[] = []; + for (const log of response.structLogs) { + const hasStorage = Object.hasOwn(log, "storage"); + const hasMemory = Object.hasOwn(log, "memory"); + const hasStack = Object.hasOwn(log, "stack"); + if (hasStorage || hasMemory || hasStack) { + logs.push(log); + } + } + expect(logs.length).to.be.equal(0); + }); + it("should prevent wasm memory overflow", async () => { const [alice, _] = devClients; From d135f0922158a50fa6d13a76e9ff0d25362efa8f Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 10:14:15 +0300 Subject: [PATCH 14/32] Extend debugTraceTransaction tests with the one that checks Blockscout format --- .../ts/lib/helpers/blockscout_tracer.min.json | 3 + .../evm-tracing/debugTraceTransaction.ts | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 utils/e2e-tests/ts/lib/helpers/blockscout_tracer.min.json diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout_tracer.min.json b/utils/e2e-tests/ts/lib/helpers/blockscout_tracer.min.json new file mode 100644 index 000000000..e81fe875d --- /dev/null +++ b/utils/e2e-tests/ts/lib/helpers/blockscout_tracer.min.json @@ -0,0 +1,3 @@ +{ + "body": "// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`\n{\n // The call stack of the EVM execution.\n callStack: [{}],\n\n // step is invoked for every opcode that the VM executes.\n step(log, db) {\n // Capture any errors immediately\n const error = log.getError();\n\n if (error !== undefined) {\n this.fault(log, db);\n } else {\n this.success(log, db);\n }\n },\n\n // fault is invoked when the actual execution of an opcode fails.\n fault(log, db) {\n // If the topmost call already reverted, don't handle the additional fault again\n if (this.topCall().error === undefined) {\n this.putError(log);\n }\n },\n\n putError(log) {\n if (this.callStack.length > 1) {\n this.putErrorInTopCall(log);\n } else {\n this.putErrorInBottomCall(log);\n }\n },\n\n putErrorInTopCall(log) {\n // Pop off the just failed call\n const call = this.callStack.pop();\n this.putErrorInCall(log, call);\n this.pushChildCall(call);\n },\n\n putErrorInBottomCall(log) {\n const call = this.bottomCall();\n this.putErrorInCall(log, call);\n },\n\n putErrorInCall(log, call) {\n call.error = log.getError();\n\n // Consume all available gas and clean any leftovers\n if (call.gasBigInt !== undefined) {\n call.gasUsedBigInt = call.gasBigInt;\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n },\n\n topCall() {\n return this.callStack[this.callStack.length - 1];\n },\n\n bottomCall() {\n return this.callStack[0];\n },\n\n pushChildCall(childCall) {\n const topCall = this.topCall();\n\n if (topCall.calls === undefined) {\n topCall.calls = [];\n }\n\n topCall.calls.push(childCall);\n },\n\n pushGasToTopCall(log) {\n const topCall = this.topCall();\n\n if (topCall.gasBigInt === undefined) {\n topCall.gasBigInt = log.getGas();\n }\n topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost();\n },\n\n success(log, db) {\n const op = log.op.toString();\n\n this.beforeOp(log, db);\n\n switch (op) {\n case 'CREATE':\n this.createOp(log);\n break;\n case 'CREATE2':\n this.create2Op(log);\n break;\n case 'SELFDESTRUCT':\n this.selfDestructOp(log, db);\n break;\n case 'CALL':\n case 'CALLCODE':\n case 'DELEGATECALL':\n case 'STATICCALL':\n this.callOp(log, op);\n break;\n case 'REVERT':\n this.revertOp();\n break;\n }\n },\n\n beforeOp(log, db) {\n /**\n * Depths\n * 0 - `ctx`. Never shows up in `log.getDepth()`\n * 1 - first level of `log.getDepth()`\n *\n * callStack indexes\n *\n * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)\n * 1 - first callOp inside of `ctx`\n */\n const logDepth = log.getDepth();\n const callStackDepth = this.callStack.length;\n\n if (logDepth < callStackDepth) {\n // Pop off the last call and get the execution results\n const call = this.callStack.pop();\n\n const ret = log.stack.peek(0);\n\n if (!ret.equals(0)) {\n if (call.type === 'create' || call.type === 'create2') {\n call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));\n call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));\n } else {\n call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength));\n }\n } else if (call.error === undefined) {\n call.error = 'internal failure';\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n\n this.pushChildCall(call);\n }\n else {\n this.pushGasToTopCall(log);\n }\n },\n\n createOp(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n create2Op(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create2',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n selfDestructOp(log, db) {\n const contractAddress = log.contract.getAddress();\n\n this.pushChildCall({\n type: 'selfdestruct',\n from: toHex(contractAddress),\n to: toHex(toAddress(log.stack.peek(0).toString(16))),\n gasBigInt: log.getGas(),\n gasUsedBigInt: log.getCost(),\n valueBigInt: db.getBalance(contractAddress)\n });\n },\n\n callOp(log, op) {\n const to = toAddress(log.stack.peek(1).toString(16));\n\n // Skip any pre-compile invocations, those are just fancy opcodes\n if (!isPrecompiled(to)) {\n this.callCustomOp(log, op, to);\n }\n },\n\n callCustomOp(log, op, to) {\n const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);\n\n const inputOffset = log.stack.peek(2 + stackOffset).valueOf();\n const inputLength = log.stack.peek(3 + stackOffset).valueOf();\n const inputEnd = inputOffset + inputLength;\n\n const call = {\n type: 'call',\n callType: op.toLowerCase(),\n from: toHex(log.contract.getAddress()),\n to: toHex(to),\n input: toHex(log.memory.slice(inputOffset, inputEnd)),\n outputOffset: log.stack.peek(4 + stackOffset).valueOf(),\n outputLength: log.stack.peek(5 + stackOffset).valueOf()\n };\n\n switch (op) {\n case 'CALL':\n case 'CALLCODE':\n call.valueBigInt = bigInt(log.stack.peek(2));\n break;\n case 'DELEGATECALL':\n // value inherited from scope during call sequencing\n break;\n case 'STATICCALL':\n // by definition static calls transfer no value\n call.valueBigInt = bigInt.zero;\n break;\n default:\n throw \"Unknown custom call op \" + op;\n }\n\n this.callStack.push(call);\n },\n\n revertOp() {\n this.topCall().error = 'execution reverted';\n },\n\n // result is invoked when all the opcodes have been iterated over and returns\n // the final result of the tracing.\n result(ctx, db) {\n const result = this.ctxToResult(ctx, db);\n const filtered = this.filterNotUndefined(result);\n const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence;\n return this.encodeCallSequence(callSequence);\n },\n\n ctxToResult(ctx, db) {\n var result;\n\n switch (ctx.type) {\n case 'CALL':\n result = this.ctxToCall(ctx);\n break;\n case 'CREATE':\n result = this.ctxToCreate(ctx, db);\n break;\n case 'CREATE2':\n result = this.ctxToCreate2(ctx, db);\n break;\n }\n\n return result;\n },\n\n ctxToCall(ctx) {\n const result = {\n type: 'call',\n callType: 'call',\n from: toHex(ctx.from),\n to: toHex(ctx.to),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed),\n input: toHex(ctx.input)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrOutput(result, ctx);\n\n return result;\n },\n\n putErrorOrOutput(result, ctx) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error;\n } else {\n result.output = toHex(ctx.output);\n }\n },\n\n ctxToCreate(ctx, db) {\n const result = {\n type: 'create',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n ctxToCreate2(ctx, db) {\n const result = {\n type: 'create2',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n putBottomChildCalls(result) {\n const bottomCall = this.bottomCall();\n const bottomChildCalls = bottomCall.calls;\n\n if (bottomChildCalls !== undefined) {\n result.calls = bottomChildCalls;\n }\n },\n\n putErrorOrCreatedContract(result, ctx, db) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error\n } else {\n result.createdContractAddressHash = toHex(ctx.to);\n result.createdContractCode = toHex(db.getCode(ctx.to));\n }\n },\n\n error(ctx) {\n var error;\n\n const bottomCall = this.bottomCall();\n const bottomCallError = bottomCall.error;\n\n if (bottomCallError !== undefined) {\n error = bottomCallError;\n } else {\n const ctxError = ctx.error;\n\n if (ctxError !== undefined) {\n error = ctxError;\n }\n }\n\n return error;\n },\n\n filterNotUndefined(call) {\n for (var key in call) {\n if (call[key] === undefined) {\n delete call[key];\n }\n }\n\n if (call.calls !== undefined) {\n for (var i = 0; i < call.calls.length; i++) {\n call.calls[i] = this.filterNotUndefined(call.calls[i]);\n }\n }\n\n return call;\n },\n\n // sequence converts the finalized calls from a call tree to a call sequence\n sequence(call, callSequence, availableValueBigInt, traceAddress) {\n const subcalls = call.calls;\n delete call.calls;\n\n call.traceAddress = traceAddress;\n\n if (call.type === 'call' && call.callType === 'delegatecall') {\n call.valueBigInt = availableValueBigInt;\n }\n\n var newCallSequence = callSequence.concat([call]);\n\n if (subcalls !== undefined) {\n for (var i = 0; i < subcalls.length; i++) {\n const nestedSequenced = this.sequence(\n subcalls[i],\n newCallSequence,\n call.valueBigInt,\n traceAddress.concat([i])\n );\n newCallSequence = nestedSequenced.callSequence;\n }\n }\n\n return {\n callSequence: newCallSequence\n };\n },\n\n encodeCallSequence(calls) {\n for (var i = 0; i < calls.length; i++) {\n this.encodeCall(calls[i]);\n }\n\n return calls;\n },\n\n encodeCall(call) {\n this.putValue(call);\n this.putGas(call);\n this.putGasUsed(call);\n\n return call;\n },\n\n putValue(call) {\n const valueBigInt = call.valueBigInt;\n delete call.valueBigInt;\n\n call.value = '0x' + valueBigInt.toString(16);\n },\n\n putGas(call) {\n const gasBigInt = call.gasBigInt;\n delete call.gasBigInt;\n\n if (gasBigInt === undefined) {\n gasBigInt = bigInt.zero;\n }\n\n call.gas = '0x' + gasBigInt.toString(16);\n },\n\n putGasUsed(call) {\n const gasUsedBigInt = call.gasUsedBigInt;\n delete call.gasUsedBigInt;\n\n if (gasUsedBigInt === undefined) {\n gasUsedBigInt = bigInt.zero;\n }\n\n call.gasUsed = '0x' + gasUsedBigInt.toString(16);\n }\n}\n" +} diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index 229aabc5e..205abd884 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -5,6 +5,7 @@ import { beforeEachWithCleanup } from "../../lib/lifecycle"; import callee from "../../lib/abis/evmTracing/callee"; import caller from "../../lib/abis/evmTracing/caller"; import heavy from "../../lib/abis/evmTracing/heavy"; +import BS_TRACER from "../../lib/helpers/blockscout_tracer.min.json"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; @@ -199,4 +200,61 @@ describe("test debug trace transaction logic", () => { }, ); }); + + it("should format as request (Blockscout)", async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + + const calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + + const callerAddress = deployCallerContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER.body }], + ); + + const entries = response; + expect(entries).to.be.lengthOf(2); + const resCaller = entries[0]; + const resCallee = entries[1]; + expect(resCaller.callType).to.be.equal("call"); + expect(resCallee.type).to.be.equal("call"); + expect(resCallee.from).to.be.equal(resCaller.to); + expect(resCaller.traceAddress).to.be.empty; + expect(resCallee.traceAddress.length).to.be.eq(1); + expect(resCallee.traceAddress[0]).to.be.eq(0); + }); }); From 9cefe9ffa67bc6c7336f5315e698c9296eb3fda3 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 10:22:19 +0300 Subject: [PATCH 15/32] Extend Blockscout format test with V2 format check --- .../lib/helpers/blockscout_tracer_v2.min.json | 3 +++ .../evm-tracing/debugTraceTransaction.ts | 20 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 utils/e2e-tests/ts/lib/helpers/blockscout_tracer_v2.min.json diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout_tracer_v2.min.json b/utils/e2e-tests/ts/lib/helpers/blockscout_tracer_v2.min.json new file mode 100644 index 000000000..41e5e3b9a --- /dev/null +++ b/utils/e2e-tests/ts/lib/helpers/blockscout_tracer_v2.min.json @@ -0,0 +1,3 @@ +{ + "body": "// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`\n{\n // The call stack of the EVM execution.\n callStack: [{}],\n\n // step is invoked for every opcode that the VM executes.\n step(log, db) {\n // Capture any errors immediately\n const error = log.getError();\n\n if (error !== undefined) {\n this.fault(log, db);\n } else {\n this.success(log, db);\n }\n },\n\n // fault is invoked when the actual execution of an opcode fails.\n fault(log, db) {\n // If the topmost call already reverted, don't handle the additional fault again\n if (this.topCall().error === undefined) {\n this.putError(log);\n }\n },\n\n putError(log) {\n if (this.callStack.length > 1) {\n this.putErrorInTopCall(log);\n } else {\n this.putErrorInBottomCall(log);\n }\n },\n\n putErrorInTopCall(log) {\n // Pop off the just failed call\n const call = this.callStack.pop();\n this.putErrorInCall(log, call);\n this.pushChildCall(call);\n },\n\n putErrorInBottomCall(log) {\n const call = this.bottomCall();\n this.putErrorInCall(log, call);\n },\n\n putErrorInCall(log, call) {\n call.error = log.getError();\n\n // Consume all available gas and clean any leftovers\n if (call.gasBigInt !== undefined) {\n call.gasUsedBigInt = call.gasBigInt;\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n },\n\n topCall() {\n return this.callStack[this.callStack.length - 1];\n },\n\n bottomCall() {\n return this.callStack[0];\n },\n\n pushChildCall(childCall) {\n const topCall = this.topCall();\n\n if (topCall.calls === undefined) {\n topCall.calls = [];\n }\n\n topCall.calls.push(childCall);\n },\n\n pushGasToTopCall(log) {\n const topCall = this.topCall();\n\n if (topCall.gasBigInt === undefined) {\n topCall.gasBigInt = log.getGas();\n }\n topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost();\n },\n\n success(log, db) {\n const op = log.op.toString();\n\n this.beforeOp(log, db);\n\n switch (op) {\n case 'CREATE':\n this.createOp(log);\n break;\n case 'CREATE2':\n this.create2Op(log);\n break;\n case 'SELFDESTRUCT':\n this.selfDestructOp(log, db);\n break;\n case 'CALL':\n case 'CALLCODE':\n case 'DELEGATECALL':\n case 'STATICCALL':\n this.callOp(log, op);\n break;\n case 'REVERT':\n this.revertOp();\n break;\n }\n },\n\n beforeOp(log, db) {\n /**\n * Depths\n * 0 - `ctx`. Never shows up in `log.getDepth()`\n * 1 - first level of `log.getDepth()`\n *\n * callStack indexes\n *\n * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)\n * 1 - first callOp inside of `ctx`\n */\n const logDepth = log.getDepth();\n const callStackDepth = this.callStack.length;\n\n if (logDepth < callStackDepth) {\n // Pop off the last call and get the execution results\n const call = this.callStack.pop();\n\n const ret = log.stack.peek(0);\n\n if (!ret.equals(0)) {\n if (call.type === 'create' || call.type === 'create2') {\n call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));\n call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));\n } else {\n call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength));\n }\n } else if (call.error === undefined) {\n call.error = 'internal failure';\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n\n this.pushChildCall(call);\n }\n else {\n this.pushGasToTopCall(log);\n }\n },\n\n createOp(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n create2Op(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create2',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n selfDestructOp(log, db) {\n const contractAddress = log.contract.getAddress();\n\n this.pushChildCall({\n type: 'selfdestruct',\n from: toHex(contractAddress),\n to: toHex(toAddress(log.stack.peek(0).toString(16))),\n gasBigInt: log.getGas(),\n gasUsedBigInt: log.getCost(),\n valueBigInt: db.getBalance(contractAddress)\n });\n },\n\n callOp(log, op) {\n const to = toAddress(log.stack.peek(1).toString(16));\n\n // Skip any pre-compile invocations, those are just fancy opcodes\n if (!isPrecompiled(to)) {\n this.callCustomOp(log, op, to);\n }\n },\n\n callCustomOp(log, op, to) {\n const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);\n\n const inputOffset = log.stack.peek(2 + stackOffset).valueOf();\n const inputLength = log.stack.peek(3 + stackOffset).valueOf();\n const inputEnd = inputOffset + inputLength;\n\n const call = {\n type: 'call',\n callType: op.toLowerCase(),\n from: toHex(log.contract.getAddress()),\n to: toHex(to),\n input: toHex(log.memory.slice(inputOffset, inputEnd)),\n outputOffset: log.stack.peek(4 + stackOffset).valueOf(),\n outputLength: log.stack.peek(5 + stackOffset).valueOf()\n };\n\n switch (op) {\n case 'CALL':\n case 'CALLCODE':\n call.valueBigInt = bigInt(log.stack.peek(2));\n break;\n case 'DELEGATECALL':\n // value inherited from scope during call sequencing\n break;\n case 'STATICCALL':\n // by definition static calls transfer no value\n call.valueBigInt = bigInt.zero;\n break;\n default:\n throw 'Unknown custom call op ' + op;\n }\n\n this.callStack.push(call);\n },\n\n revertOp() {\n this.topCall().error = 'execution reverted';\n },\n\n // result is invoked when all the opcodes have been iterated over and returns\n // the final result of the tracing.\n result(ctx, db) {\n const result = this.ctxToResult(ctx, db);\n const filtered = this.filterNotUndefined(result);\n const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence;\n return this.encodeCallSequence(callSequence);\n },\n\n ctxToResult(ctx, db) {\n var result;\n\n switch (ctx.type) {\n case 'CALL':\n result = this.ctxToCall(ctx);\n break;\n case 'CREATE':\n result = this.ctxToCreate(ctx, db);\n break;\n case 'CREATE2':\n result = this.ctxToCreate2(ctx, db);\n break;\n }\n\n return result;\n },\n\n ctxToCall(ctx) {\n const result = {\n type: 'call',\n callType: 'call',\n from: toHex(ctx.from),\n to: toHex(ctx.to),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed),\n input: toHex(ctx.input)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrOutput(result, ctx);\n\n return result;\n },\n\n putErrorOrOutput(result, ctx) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error;\n } else {\n result.output = toHex(ctx.output);\n }\n },\n\n ctxToCreate(ctx, db) {\n const result = {\n type: 'create',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n ctxToCreate2(ctx, db) {\n const result = {\n type: 'create2',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n putBottomChildCalls(result) {\n const bottomCall = this.bottomCall();\n const bottomChildCalls = bottomCall.calls;\n\n if (bottomChildCalls !== undefined) {\n result.calls = bottomChildCalls;\n }\n },\n\n putErrorOrCreatedContract(result, ctx, db) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error\n } else {\n result.createdContractAddressHash = toHex(ctx.to);\n if (toHex(ctx.input) != '0x') {\n result.createdContractCode = toHex(db.getCode(ctx.to));\n } else {\n result.createdContractCode = '0x';\n }\n }\n },\n\n error(ctx) {\n var error;\n\n const bottomCall = this.bottomCall();\n const bottomCallError = bottomCall.error;\n\n if (bottomCallError !== undefined) {\n error = bottomCallError;\n } else {\n const ctxError = ctx.error;\n\n if (ctxError !== undefined) {\n error = ctxError;\n }\n }\n\n return error;\n },\n\n filterNotUndefined(call) {\n for (var key in call) {\n if (call[key] === undefined) {\n delete call[key];\n }\n }\n\n if (call.calls !== undefined) {\n for (var i = 0; i < call.calls.length; i++) {\n call.calls[i] = this.filterNotUndefined(call.calls[i]);\n }\n }\n\n return call;\n },\n\n // sequence converts the finalized calls from a call tree to a call sequence\n sequence(call, callSequence, availableValueBigInt, traceAddress) {\n const subcalls = call.calls;\n delete call.calls;\n\n call.traceAddress = traceAddress;\n\n if (call.type === 'call' && call.callType === 'delegatecall') {\n call.valueBigInt = availableValueBigInt;\n }\n\n var newCallSequence = callSequence.concat([call]);\n\n if (subcalls !== undefined) {\n for (var i = 0; i < subcalls.length; i++) {\n const nestedSequenced = this.sequence(\n subcalls[i],\n newCallSequence,\n call.valueBigInt,\n traceAddress.concat([i])\n );\n newCallSequence = nestedSequenced.callSequence;\n }\n }\n\n return {\n callSequence: newCallSequence\n };\n },\n\n encodeCallSequence(calls) {\n for (var i = 0; i < calls.length; i++) {\n this.encodeCall(calls[i]);\n }\n\n return calls;\n },\n\n encodeCall(call) {\n this.putValue(call);\n this.putGas(call);\n this.putGasUsed(call);\n\n return call;\n },\n\n putValue(call) {\n const valueBigInt = call.valueBigInt;\n delete call.valueBigInt;\n\n call.value = '0x' + valueBigInt.toString(16);\n },\n\n putGas(call) {\n const gasBigInt = call.gasBigInt;\n delete call.gasBigInt;\n\n if (gasBigInt === undefined) {\n gasBigInt = bigInt.zero;\n }\n\n call.gas = '0x' + gasBigInt.toString(16);\n },\n\n putGasUsed(call) {\n const gasUsedBigInt = call.gasUsedBigInt;\n delete call.gasUsedBigInt;\n\n if (gasUsedBigInt === undefined) {\n gasUsedBigInt = bigInt.zero;\n }\n\n call.gasUsed = '0x' + gasUsedBigInt.toString(16);\n }\n}\n" +} diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index 205abd884..0ee9862eb 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -6,6 +6,7 @@ import callee from "../../lib/abis/evmTracing/callee"; import caller from "../../lib/abis/evmTracing/caller"; import heavy from "../../lib/abis/evmTracing/heavy"; import BS_TRACER from "../../lib/helpers/blockscout_tracer.min.json"; +import BS_TRACER_V2 from "../../lib/helpers/blockscout_tracer_v2.min.json"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; @@ -201,7 +202,7 @@ describe("test debug trace transaction logic", () => { ); }); - it("should format as request (Blockscout)", async () => { + it("should format as request (Blockscout, BlockscoutV2)", async () => { const [alice, _] = devClients; const deployCalleeContractTxHash = await alice.deployContract({ @@ -256,5 +257,22 @@ describe("test debug trace transaction logic", () => { expect(resCaller.traceAddress).to.be.empty; expect(resCallee.traceAddress.length).to.be.eq(1); expect(resCallee.traceAddress[0]).to.be.eq(0); + + const responseV2 = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + const entriesV2 = responseV2; + expect(entriesV2).to.be.lengthOf(2); + const resCallerV2 = entriesV2[0]; + const resCalleeV2 = entriesV2[1]; + expect(resCallerV2.callType).to.be.equal("call"); + expect(resCalleeV2.type).to.be.equal("call"); + expect(resCalleeV2.from).to.be.equal(resCallerV2.to); + expect(resCallerV2.traceAddress).to.be.empty; + expect(resCalleeV2.traceAddress.length).to.be.eq(1); + expect(resCalleeV2.traceAddress[0]).to.be.eq(0); }); }); From 40887184018f27a336cc6c7fe4bceacdc1082a4a Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 16:59:23 +0300 Subject: [PATCH 16/32] Extend debugTraceTransaction tests with the one that checks intermediate state --- .../ts/lib/abis/evmTracing/incrementor.ts | 69 +++++++++++++++++ .../evm-tracing/debugTraceTransaction.ts | 77 ++++++++++++++++++- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 utils/e2e-tests/ts/lib/abis/evmTracing/incrementor.ts diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/incrementor.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/incrementor.ts new file mode 100644 index 000000000..3cdb3e4f8 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/incrementor.ts @@ -0,0 +1,69 @@ +// pragma solidity >=0.8.3; +// +// contract Incrementor { +// uint256 public count; +// +// constructor() { +// count = 0; +// } +// +// function incr() public { +// count = count + 1; +// } +// +// function incr(uint256 num) public returns (uint256) { +// count = count + num; +// return count; +// } +// } + +export default { + abi: [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "count", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "incr", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "num", + type: "uint256", + }, + ], + name: "incr", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b505f5f819055506101f1806100225f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c806306661abd14610043578063119fbbd41461006157806321b13c481461006b575b5f5ffd5b61004b61009b565b60405161005891906100e9565b60405180910390f35b6100696100a0565b005b61008560048036038101906100809190610130565b6100b5565b60405161009291906100e9565b60405180910390f35b5f5481565b60015f546100ae9190610188565b5f81905550565b5f815f546100c39190610188565b5f819055505f549050919050565b5f819050919050565b6100e3816100d1565b82525050565b5f6020820190506100fc5f8301846100da565b92915050565b5f5ffd5b61010f816100d1565b8114610119575f5ffd5b50565b5f8135905061012a81610106565b92915050565b5f6020828403121561014557610144610102565b5b5f6101528482850161011c565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610192826100d1565b915061019d836100d1565b92508282019050808211156101b5576101b461015b565b5b9291505056fea2646970667358221220836c30d0514664def3debcdd06557178b40a286f683281477d734ff6edbfa46064736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index 0ee9862eb..b9ed4583c 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -5,9 +5,10 @@ import { beforeEachWithCleanup } from "../../lib/lifecycle"; import callee from "../../lib/abis/evmTracing/callee"; import caller from "../../lib/abis/evmTracing/caller"; import heavy from "../../lib/abis/evmTracing/heavy"; +import incrementor from "../../lib/abis/evmTracing/incrementor"; import BS_TRACER from "../../lib/helpers/blockscout_tracer.min.json"; import BS_TRACER_V2 from "../../lib/helpers/blockscout_tracer_v2.min.json"; -import { encodeFunctionData } from "viem"; +import { encodeFunctionData, hexToNumber } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; describe("test debug trace transaction logic", () => { @@ -275,4 +276,78 @@ describe("test debug trace transaction logic", () => { expect(resCalleeV2.traceAddress.length).to.be.eq(1); expect(resCalleeV2.traceAddress[0]).to.be.eq(0); }); + + it("should replay over an intermediate state", async () => { + const [alice, _] = devClients; + + const deployIncrementorContractTxHash = await alice.deployContract({ + abi: incrementor.abi, + bytecode: incrementor.bytecode, + }); + + const deployIncrementorContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployIncrementorContractTxHash, + timeout: 18_000, + }); + + const incrementorAddress = + deployIncrementorContractTxReceipt.contractAddress!; + + // In our case, the total number of transactions === the max value of the incrementer. + // If we trace the last transaction of the block, should return the total number of + // transactions we executed (10). + // If we trace the 5th transaction, should return 5 and so on. + // + // So we set 5 different target txs for a single block: the 1st, 3 intermediate, and + // the last. + const totalTxs = 10; + const targets = [1, 2, 5, 8, 10]; + const txsPromises: any[] = []; + + const nonce = await publicClient.getTransactionCount({ + address: alice.account.address, + }); + + // Create 10 transactions in a block. + for (let numTxs = 0; numTxs < totalTxs; numTxs++) { + const txsPromise = alice + .sendTransaction({ + to: incrementorAddress, + data: encodeFunctionData({ + abi: incrementor.abi, + functionName: "incr", + args: [1n], + }), + gas: 100_000n, + nonce: nonce + numTxs, + }) + .then((txHash) => + publicClient.waitForTransactionReceipt({ + hash: txHash, + timeout: 18_000, + }), + ); + + txsPromises.push(txsPromise); + } + + const txsReceipts = await Promise.all(txsPromises); + + // Trace 5 target transactions on it. + for (const target of targets) { + const index = target - 1; + + const intermediateTx = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txsReceipts[index].transactionHash], + ); + + const evmResult = hexToNumber( + ("0x" + intermediateTx.returnValue) as `0x${string}`, + ); + expect(evmResult).to.equal(target); + } + }); }); From 7636fbc799cece8a43b0bf766380b0ad54cf4ab6 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 17:25:47 +0300 Subject: [PATCH 17/32] Extend debugTraceTransaction tests with the one that checks out of gas execution --- .../evm-tracing/debugTraceTransaction.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index b9ed4583c..938ef882f 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -4,6 +4,7 @@ import * as eth from "../../lib/ethViem"; import { beforeEachWithCleanup } from "../../lib/lifecycle"; import callee from "../../lib/abis/evmTracing/callee"; import caller from "../../lib/abis/evmTracing/caller"; +import looper from "../../lib/abis/evmTracing/looper"; import heavy from "../../lib/abis/evmTracing/heavy"; import incrementor from "../../lib/abis/evmTracing/incrementor"; import BS_TRACER from "../../lib/helpers/blockscout_tracer.min.json"; @@ -118,6 +119,39 @@ describe("test debug trace transaction logic", () => { expect(logs.length).to.be.equal(0); }); + it("should trace correctly out of gas transaction execution", async () => { + const [alice, _] = devClients; + + const deployLooperContractTxHash = await alice.deployContract({ + abi: looper.abi, + bytecode: looper.bytecode, + }); + const deployLooperContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployLooperContractTxHash, + timeout: 18_000, + }); + const looperAddress = deployLooperContractTxReceipt.contractAddress!; + + const txHash = await alice.sendTransaction({ + to: looperAddress, + gas: 1_000_000n, + data: "0x5bec9e67", + gasLimit: "0x100000", + value: 0n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + expect(response.length).to.be.eq(1); + expect(response[0].error).to.be.equal("out of gas"); + }); + it("should prevent wasm memory overflow", async () => { const [alice, _] = devClients; From 7de1ccfd6643ca9a6a6b72e2876b0def8648f92d Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 17:34:28 +0300 Subject: [PATCH 18/32] Extend debugTraceTransaction tests with the one that checks precompiles tracing --- .../evm-tracing/debugTraceTransaction.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index 938ef882f..3f36e213b 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -6,12 +6,16 @@ import callee from "../../lib/abis/evmTracing/callee"; import caller from "../../lib/abis/evmTracing/caller"; import looper from "../../lib/abis/evmTracing/looper"; import heavy from "../../lib/abis/evmTracing/heavy"; +import evmSwap from "../../lib/abis/evmSwap"; import incrementor from "../../lib/abis/evmTracing/incrementor"; import BS_TRACER from "../../lib/helpers/blockscout_tracer.min.json"; import BS_TRACER_V2 from "../../lib/helpers/blockscout_tracer_v2.min.json"; import { encodeFunctionData, hexToNumber } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; +const evmToNativeSwapPrecompileAddress = + "0x0000000000000000000000000000000000000900"; + describe("test debug trace transaction logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; @@ -152,6 +156,29 @@ describe("test debug trace transaction logic", () => { expect(response[0].error).to.be.equal("out of gas"); }); + it("should trace correctly precompiles", async () => { + const [alice, _] = devClients; + + const txHash = await alice.writeContract({ + abi: evmSwap.abi, + address: evmToNativeSwapPrecompileAddress, + functionName: "swap", + args: [ + "0x7700000000000000000000000000000000000000000000000000000000000077", + ], + value: 1_000_000n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + expect(response.length).to.be.eq(1); + }); + it("should prevent wasm memory overflow", async () => { const [alice, _] = devClients; From 76079518e5270c302a8ce96d7931ddd235b5516a Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 17:47:55 +0300 Subject: [PATCH 19/32] Add checking Blockscout format for out of gas and precompiles --- .../evm-tracing/debugTraceTransaction.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index 3f36e213b..a04e43ced 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -149,11 +149,20 @@ describe("test debug trace transaction logic", () => { const response = await customRpcRequest( node.meta.rpcUrlHttp, "debug_traceTransaction", - [txHash, { tracer: BS_TRACER_V2.body }], + [txHash, { tracer: BS_TRACER.body }], ); expect(response.length).to.be.eq(1); expect(response[0].error).to.be.equal("out of gas"); + + const responseV2 = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + expect(responseV2.length).to.be.eq(1); + expect(responseV2[0].error).to.be.equal("out of gas"); }); it("should trace correctly precompiles", async () => { @@ -173,10 +182,18 @@ describe("test debug trace transaction logic", () => { const response = await customRpcRequest( node.meta.rpcUrlHttp, "debug_traceTransaction", - [txHash, { tracer: BS_TRACER_V2.body }], + [txHash, { tracer: BS_TRACER.body }], ); expect(response.length).to.be.eq(1); + + const responseV2 = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: BS_TRACER_V2.body }], + ); + + expect(responseV2.length).to.be.eq(1); }); it("should prevent wasm memory overflow", async () => { From 28c00f505e6ff3022ecd73c7a54871f306b7dfd1 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 17 Oct 2025 17:52:41 +0300 Subject: [PATCH 20/32] Extend debugTraceTransaction tests with the one that checks transfer tracing --- .../tests/evm-tracing/debugTraceTransaction.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts index a04e43ced..c6a048acc 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts @@ -93,6 +93,24 @@ describe("test debug trace transaction logic", () => { expect(logs[1].depth).to.be.equal(1); }); + it("should trace correctly transfers", async () => { + const [alice, bob] = devClients; + + const txHash = await alice.sendTransaction({ + to: bob.account.address, + value: 1_000_000n, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash], + ); + + expect(response.gas).to.be.eq("0x5208"); // 21_000 gas for a transfer. + }); + it("should use optional disable parameters", async () => { const [alice, bob] = devClients; From 66329881d96e652363fd73ea871744b847257bcd Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Tue, 21 Oct 2025 11:54:55 +0300 Subject: [PATCH 21/32] Add debugTraceTransactionCallTracer tests --- .../debugTraceTransactionCallTracer.ts | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts new file mode 100644 index 000000000..51fc7c126 --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts @@ -0,0 +1,207 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callee from "../../lib/abis/evmTracing/callee"; +import caller from "../../lib/abis/evmTracing/caller"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("test debug trace transaction logic related to call tracer", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let calleeAddress: `0x${string}`; + let callerAddress: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCalleeContractTxHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + const deployCalleeContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCalleeContractTxHash, + timeout: 18_000, + }); + calleeAddress = deployCalleeContractTxReceipt.contractAddress!; + + const deployCallerContractTxHash = await alice.deployContract({ + abi: caller.abi, + bytecode: caller.bytecode, + }); + const deployCallerContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallerContractTxHash, + timeout: 18_000, + }); + callerAddress = deployCallerContractTxReceipt.contractAddress!; + }); + + it("should format as request (Call)", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(Object.keys(response).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + expect(response.type).to.be.equal("CALL"); + const calls = response.calls; + expect(calls.length).to.be.eq(1); + const nested_call = calls[0]; + expect(response.to).to.be.equal(nested_call.from); + expect(nested_call.type).to.be.equal("CALL"); + }); + + it("should format as request (Create)", async () => { + const [alice, _] = devClients; + + const txHash = await alice.deployContract({ + abi: callee.abi, + bytecode: callee.bytecode, + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(Object.keys(response).sort()).to.deep.equal([ + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + + expect(response.type).to.be.equal("CREATE"); + }); + + it("should trace block by number and hash", async () => { + const [alice, _] = devClients; + + const totalTxs = 3; + const txsPromises: any[] = []; + + const nonce = await publicClient.getTransactionCount({ + address: alice.account.address, + }); + + for (let numTxs = 0; numTxs < totalTxs; numTxs++) { + const txsPromise = alice + .sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), + gas: 100_000n, + nonce: nonce + numTxs, + }) + .then((txHash) => + publicClient.waitForTransactionReceipt({ + hash: txHash, + timeout: 18_000, + }), + ); + + txsPromises.push(txsPromise); + } + + const txsReceipts = await Promise.all(txsPromises); + + const blockNumberHex = txsReceipts[0].blockNumber.toString(16); + const blockHash = txsReceipts[0].blockHash; + + // Trace block by number. + const responseByNumber = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByNumber", + [blockNumberHex, { tracer: "callTracer" }], + ); + expect(responseByNumber.length).to.be.equal(3); + responseByNumber.forEach((trace: { [key: string]: any }, index: number) => { + expect(trace["txHash"]).to.be.equal(txsReceipts[index].transactionHash); + expect(trace["result"].calls.length).to.be.equal(1); + expect(Object.keys(trace["result"]).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + }); + + // Trace block by hash (actually the rpc method is an alias of `debug_traceBlockByNumber`). + const responseByHash = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceBlockByNumber", + [blockHash, { tracer: "callTracer" }], + ); + expect(responseByHash.length).to.be.equal(3); + responseByHash.forEach((trace: { [key: string]: any }, index: number) => { + expect(trace["txHash"]).to.be.equal(txsReceipts[index].transactionHash); + expect(trace["result"].calls.length).to.be.equal(1); + expect(Object.keys(trace["result"]).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + }); + }); +}); From e388b6c430ccc02268fb899620eab95a32d2c343 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 22 Oct 2025 13:12:50 +0300 Subject: [PATCH 22/32] Add debugTraceTransactionCallList tests --- .../ts/lib/abis/evmTracing/callForwarder.ts | 108 +++++++++++++++ .../ts/lib/abis/evmTracing/multiplyBy7.ts | 33 +++++ .../debugTraceTransactionCallList.ts | 125 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 utils/e2e-tests/ts/lib/abis/evmTracing/callForwarder.ts create mode 100644 utils/e2e-tests/ts/lib/abis/evmTracing/multiplyBy7.ts create mode 100644 utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallList.ts diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/callForwarder.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/callForwarder.ts new file mode 100644 index 000000000..730702bb9 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/callForwarder.ts @@ -0,0 +1,108 @@ +// pragma solidity >=0.8.3; +// +// contract CallForwarder { +// function call( +// address target, +// bytes memory data +// ) public returns (bool, bytes memory) { +// return target.call(data); +// } +// +// function callRange(address first, address last) public { +// require(first < last, "invalid range"); +// while (first < last) { +// first.call(""); +// first = address(uint160(first) + 1); +// } +// } +// +// function delegateCall( +// address target, +// bytes memory data +// ) public returns (bool, bytes memory) { +// return target.delegatecall(data); +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "call", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "first", + type: "address", + }, + { + internalType: "address", + name: "last", + type: "address", + }, + ], + name: "callRange", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "delegateCall", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b5061076d8061001c5f395ff3fe608060405234801561000f575f5ffd5b506004361061003f575f3560e01c80630382943f146100435780631b8b921d1461005f57806356e7b7aa14610090575b5f5ffd5b61005d6004803603810190610058919061032e565b6100c1565b005b610079600480360381019061007491906104a8565b6101e0565b604051610087929190610596565b60405180910390f35b6100aa60048036038101906100a591906104a8565b610252565b6040516100b8929190610596565b60405180910390f35b8073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161061012f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101269061061e565b60405180910390fd5b5b8073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1610156101dc578173ffffffffffffffffffffffffffffffffffffffff1660405161018790610669565b5f604051808303815f865af19150503d805f81146101c0576040519150601f19603f3d011682016040523d82523d5f602084013e6101c5565b606091505b5050506001826101d591906106aa565b9150610130565b5050565b5f60608373ffffffffffffffffffffffffffffffffffffffff16836040516102089190610721565b5f604051808303815f865af19150503d805f8114610241576040519150601f19603f3d011682016040523d82523d5f602084013e610246565b606091505b50915091509250929050565b5f60608373ffffffffffffffffffffffffffffffffffffffff168360405161027a9190610721565b5f60405180830381855af49150503d805f81146102b2576040519150601f19603f3d011682016040523d82523d5f602084013e6102b7565b606091505b50915091509250929050565b5f604051905090565b5f5ffd5b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6102fd826102d4565b9050919050565b61030d816102f3565b8114610317575f5ffd5b50565b5f8135905061032881610304565b92915050565b5f5f60408385031215610344576103436102cc565b5b5f6103518582860161031a565b92505060206103628582860161031a565b9150509250929050565b5f5ffd5b5f5ffd5b5f601f19601f8301169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b6103ba82610374565b810181811067ffffffffffffffff821117156103d9576103d8610384565b5b80604052505050565b5f6103eb6102c3565b90506103f782826103b1565b919050565b5f67ffffffffffffffff82111561041657610415610384565b5b61041f82610374565b9050602081019050919050565b828183375f83830152505050565b5f61044c610447846103fc565b6103e2565b90508281526020810184848401111561046857610467610370565b5b61047384828561042c565b509392505050565b5f82601f83011261048f5761048e61036c565b5b813561049f84826020860161043a565b91505092915050565b5f5f604083850312156104be576104bd6102cc565b5b5f6104cb8582860161031a565b925050602083013567ffffffffffffffff8111156104ec576104eb6102d0565b5b6104f88582860161047b565b9150509250929050565b5f8115159050919050565b61051681610502565b82525050565b5f81519050919050565b5f82825260208201905092915050565b5f5b83811015610553578082015181840152602081019050610538565b5f8484015250505050565b5f6105688261051c565b6105728185610526565b9350610582818560208601610536565b61058b81610374565b840191505092915050565b5f6040820190506105a95f83018561050d565b81810360208301526105bb818461055e565b90509392505050565b5f82825260208201905092915050565b7f696e76616c69642072616e6765000000000000000000000000000000000000005f82015250565b5f610608600d836105c4565b9150610613826105d4565b602082019050919050565b5f6020820190508181035f830152610635816105fc565b9050919050565b5f81905092915050565b50565b5f6106545f8361063c565b915061065f82610646565b5f82019050919050565b5f61067382610649565b9150819050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6106b4826102d4565b91506106bf836102d4565b9250828201905073ffffffffffffffffffffffffffffffffffffffff8111156106eb576106ea61067d565b5b92915050565b5f6106fb8261051c565b610705818561063c565b9350610715818560208601610536565b80840191505092915050565b5f61072c82846106f1565b91508190509291505056fea264697066735822122061744ef3cd320f8504db2e240b6131dd71c7811dbb3c105ee2c3014e04b1635864736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/multiplyBy7.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/multiplyBy7.ts new file mode 100644 index 000000000..8daa85e39 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/multiplyBy7.ts @@ -0,0 +1,33 @@ +// pragma solidity >=0.8.3; +// +// contract MultiplyBy7 { +// function multiply(uint256 a) public pure returns (uint256 d) { +// return a * 7; +// } +// } + +export default { + abi: [ + { + inputs: [ + { + internalType: "uint256", + name: "a", + type: "uint256", + }, + ], + name: "multiply", + outputs: [ + { + internalType: "uint256", + name: "d", + type: "uint256", + }, + ], + stateMutability: "pure", + type: "function", + }, + ], + bytecode: + "0x6080604052348015600e575f5ffd5b506101a08061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063c6888fa11461002d575b5f5ffd5b610047600480360381019061004291906100a9565b61005d565b60405161005491906100e3565b60405180910390f35b5f60078261006b9190610129565b9050919050565b5f5ffd5b5f819050919050565b61008881610076565b8114610092575f5ffd5b50565b5f813590506100a38161007f565b92915050565b5f602082840312156100be576100bd610072565b5b5f6100cb84828501610095565b91505092915050565b6100dd81610076565b82525050565b5f6020820190506100f65f8301846100d4565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61013382610076565b915061013e83610076565b925082820261014c81610076565b91508282048414831517610163576101626100fc565b5b509291505056fea26469706673582212204f32ea69d8b2260c7fffb56c84905516c1c665b20b41698aaea81eb3df977c0a64736f6c634300081e0033", +} as const; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallList.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallList.ts new file mode 100644 index 000000000..12d48668b --- /dev/null +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallList.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import callForwarder from "../../lib/abis/evmTracing/callForwarder"; +import multiplyBy7 from "../../lib/abis/evmTracing/multiplyBy7"; +import { encodeFunctionData } from "viem"; +import { customRpcRequest } from "../../lib/rpcUtils"; + +describe("test debug trace transaction logic related to call tracer", () => { + let node: RunNodeState; + let publicClient: eth.PublicClientWebSocket; + let devClients: eth.DevClientsWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode( + { + args: ["--tracing-mode=debug", "--dev", "--tmp"], + }, + cleanup.push, + ); + + await node.waitForBoot; + + publicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + devClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + let callForwarderAddress: `0x${string}`; + let multiplyBy7Address: `0x${string}`; + + beforeEach(async () => { + const [alice, _] = devClients; + + const deployCallForwarderContractTxHash = await alice.deployContract({ + abi: callForwarder.abi, + bytecode: callForwarder.bytecode, + }); + const deployCallForwarderContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployCallForwarderContractTxHash, + timeout: 18_000, + }); + callForwarderAddress = + deployCallForwarderContractTxReceipt.contractAddress!; + + const deployMultiplyBy7ContractTxHash = await alice.deployContract({ + abi: multiplyBy7.abi, + bytecode: multiplyBy7.bytecode, + }); + const deployMultiplyBy7ContractTxReceipt = + await publicClient.waitForTransactionReceipt({ + hash: deployMultiplyBy7ContractTxHash, + timeout: 18_000, + }); + multiplyBy7Address = deployMultiplyBy7ContractTxReceipt.contractAddress!; + }); + + it("should correctly trace subcall", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callForwarderAddress, + data: encodeFunctionData({ + abi: callForwarder.abi, + functionName: "call", + args: [ + multiplyBy7Address, + encodeFunctionData({ + abi: multiplyBy7.abi, + functionName: "multiply", + args: [42n], + }), + ], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(response.from).to.be.eq(alice.account.address.toLowerCase()); + expect(response.to).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls.length).to.be.eq(1); + expect(response.calls[0].from).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls[0].to).to.be.eq(multiplyBy7Address.toLowerCase()); + expect(response.calls[0].type).to.be.eq("CALL"); + }); + + it("should correctly trace delegatecall subcall", async () => { + const [alice, _] = devClients; + + const txHash = await alice.sendTransaction({ + to: callForwarderAddress, + data: encodeFunctionData({ + abi: callForwarder.abi, + functionName: "delegateCall", + args: [ + multiplyBy7Address, + encodeFunctionData({ + abi: multiplyBy7.abi, + functionName: "multiply", + args: [42n], + }), + ], + }), + }); + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + const response = await customRpcRequest( + node.meta.rpcUrlHttp, + "debug_traceTransaction", + [txHash, { tracer: "callTracer" }], + ); + + expect(response.from).to.be.eq(alice.account.address.toLowerCase()); + expect(response.to).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls.length).to.be.eq(1); + expect(response.calls[0].from).to.be.eq(callForwarderAddress.toLowerCase()); + expect(response.calls[0].to).to.be.eq(multiplyBy7Address.toLowerCase()); + expect(response.calls[0].type).to.be.eq("DELEGATECALL"); + }); +}); From 5e5981935d0cf738bb32cc3f8ad8ba51bd5415fd Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 22 Oct 2025 13:18:26 +0300 Subject: [PATCH 23/32] Improve namings and descritpion at trace_filter related tests --- .../ts/tests/evm-tracing/traceFilterGeneral.ts | 2 +- .../e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts | 2 +- .../{traceFilterGasLoop.ts => traceFilterUsedGas.ts} | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) rename utils/e2e-tests/ts/tests/evm-tracing/{traceFilterGasLoop.ts => traceFilterUsedGas.ts} (91%) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts index 553e1b3a6..a80b5fcde 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts @@ -4,7 +4,7 @@ import * as eth from "../../lib/ethViem"; import { beforeEachWithCleanup } from "../../lib/lifecycle"; import { customRpcRequest } from "../../lib/rpcUtils"; -describe("test trace filter for general logic", () => { +describe("`trace_filter` tests to verify general logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts index cf0b0a136..cf227fc9e 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts @@ -6,7 +6,7 @@ import heavy from "../../lib/abis/evmTracing/heavy"; import { customRpcRequest } from "../../lib/rpcUtils"; import { encodeFunctionData } from "viem"; -describe("test trace filter for heavy contract logic", () => { +describe("`trace_filter` tests to verify some heavy logic in contracts", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterUsedGas.ts similarity index 91% rename from utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts rename to utils/e2e-tests/ts/tests/evm-tracing/traceFilterUsedGas.ts index 4b9691ebb..b236f3966 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGasLoop.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterUsedGas.ts @@ -1,4 +1,4 @@ -// Constants related to used gas in loops can be different on various EVM versions. +// Constants related to used gas can be different on various EVM versions. import { beforeEach, describe, expect, it } from "vitest"; import { RunNodeState, runNode } from "../../lib/node"; @@ -8,7 +8,7 @@ import looper from "../../lib/abis/evmTracing/looper"; import { customRpcRequest } from "../../lib/rpcUtils"; import { encodeFunctionData } from "viem"; -describe("test trace filter for gas used in loops logic", () => { +describe("`trace_filter` tests to verify used gas logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; @@ -43,7 +43,7 @@ describe("test trace filter for gas used in loops logic", () => { looperAddress = deployLooperContractTxReceipt.contractAddress!; }); - it("should return 21653 gasUsed for 0 loop", async () => { + it("should return 21653 `gasUsed` for 0 loop", async () => { const [alice, _] = devClients; const txHash = await alice.sendTransaction({ @@ -75,7 +75,7 @@ describe("test trace filter for gas used in loops logic", () => { expect(response[0].result.gasUsed).to.equal("0x5495"); }); - it("should return 106265 gasUsed for 100 loops", async () => { + it("should return 106265 `gasUsed` for 100 loops", async () => { const [alice, _] = devClients; const txHash = await alice.sendTransaction({ @@ -107,7 +107,7 @@ describe("test trace filter for gas used in loops logic", () => { expect(response[0].result.gasUsed).to.equal("0x19f19"); }); - it("should return 670577 gasUsed for 1000 loops", async () => { + it("should return 670577 `gasUsed` for 1000 loops", async () => { const [alice, _] = devClients; const txHash = await alice.sendTransaction({ From 55ee0d4af321c58133e17de3f27a4acfca46af51 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 22 Oct 2025 13:20:40 +0300 Subject: [PATCH 24/32] Improve namings and descriptions at debug_traceCall test --- .../evm-tracing/{debugTraceCall.ts => debugTraceCallGeneral.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename utils/e2e-tests/ts/tests/evm-tracing/{debugTraceCall.ts => debugTraceCallGeneral.ts} (97%) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCall.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts similarity index 97% rename from utils/e2e-tests/ts/tests/evm-tracing/debugTraceCall.ts rename to utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts index d00fab0b1..41aaa360a 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCall.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts @@ -7,7 +7,7 @@ import caller from "../../lib/abis/evmTracing/caller"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; -describe("test debug trace call logic", () => { +describe("`debug_traceCall` tests to verify general logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; From 427b821cd86592dfb18f9127a06b03a5f2f57da2 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 22 Oct 2025 13:27:59 +0300 Subject: [PATCH 25/32] Improve namings and descriptions at debug_traceTransaction related tests --- .../{debugTraceBlockBy.ts => debugTraceBlockByGeneral.ts} | 2 +- .../ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts | 2 +- ...debugTraceTransaction.ts => debugTraceTransactionGeneral.ts} | 2 +- ...eTransactionCallList.ts => debugTraceTransactionSubcalls.ts} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename utils/e2e-tests/ts/tests/evm-tracing/{debugTraceBlockBy.ts => debugTraceBlockByGeneral.ts} (96%) rename utils/e2e-tests/ts/tests/evm-tracing/{debugTraceTransaction.ts => debugTraceTransactionGeneral.ts} (99%) rename utils/e2e-tests/ts/tests/evm-tracing/{debugTraceTransactionCallList.ts => debugTraceTransactionSubcalls.ts} (98%) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockBy.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockByGeneral.ts similarity index 96% rename from utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockBy.ts rename to utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockByGeneral.ts index a81d08516..5006be99f 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockBy.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceBlockByGeneral.ts @@ -7,7 +7,7 @@ import caller from "../../lib/abis/evmTracing/caller"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; -describe("test debug trace block by number or hash logic", () => { +describe("`debug_traceBlockByNumber` and `debug_traceBlockByHash` tests to verify general logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts index 51fc7c126..8662ce578 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts @@ -7,7 +7,7 @@ import caller from "../../lib/abis/evmTracing/caller"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; -describe("test debug trace transaction logic related to call tracer", () => { +describe("`debug_traceTransaction` tests to verify `callTracer` usage logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts similarity index 99% rename from utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts rename to utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts index c6a048acc..53ccfe457 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransaction.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts @@ -16,7 +16,7 @@ import { customRpcRequest } from "../../lib/rpcUtils"; const evmToNativeSwapPrecompileAddress = "0x0000000000000000000000000000000000000900"; -describe("test debug trace transaction logic", () => { +describe("`debug_traceTransaction` tests to verify general logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallList.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionSubcalls.ts similarity index 98% rename from utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallList.ts rename to utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionSubcalls.ts index 12d48668b..5fa4a0496 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallList.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionSubcalls.ts @@ -7,7 +7,7 @@ import multiplyBy7 from "../../lib/abis/evmTracing/multiplyBy7"; import { encodeFunctionData } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; -describe("test debug trace transaction logic related to call tracer", () => { +describe("`debug_traceTransaction` tests to verify subcalls logic", () => { let node: RunNodeState; let publicClient: eth.PublicClientWebSocket; let devClients: eth.DevClientsWebSocket; From a798ac5d8594e490461d8277728e7a5355c5f8c0 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 22 Oct 2025 18:17:50 +0300 Subject: [PATCH 26/32] Fix blockNumber usage in checks --- .../ts/tests/evm-tracing/traceFilterHeavy.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts index cf227fc9e..7fe88ed3f 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterHeavy.ts @@ -4,7 +4,7 @@ import * as eth from "../../lib/ethViem"; import { beforeEachWithCleanup } from "../../lib/lifecycle"; import heavy from "../../lib/abis/evmTracing/heavy"; import { customRpcRequest } from "../../lib/rpcUtils"; -import { encodeFunctionData } from "viem"; +import { encodeFunctionData, hexToNumber } from "viem"; describe("`trace_filter` tests to verify some heavy logic in contracts", () => { let node: RunNodeState; @@ -26,7 +26,7 @@ describe("`trace_filter` tests to verify some heavy logic in contracts", () => { let heavyContracts: { address: `0x${string}`; - blockNumberHex: string; + blockNumberHex: `0x${string}`; txHash: `0x${string}`; }[] = []; @@ -57,7 +57,9 @@ describe("`trace_filter` tests to verify some heavy logic in contracts", () => { heavyContracts.push({ address: deployHeavyContractTxReceipt.contractAddress!, - blockNumberHex: deployHeavyContractTxReceipt.blockNumber.toString(16), + blockNumberHex: deployHeavyContractTxReceipt.blockNumber.toString( + 16, + ) as `0x${string}`, txHash: deployHeavyContractTxReceipt.transactionHash, }); } @@ -87,7 +89,7 @@ describe("`trace_filter` tests to verify some heavy logic in contracts", () => { }); expect(response[0]).to.include({ - blockNumber: 1, + blockNumber: hexToNumber(heavyContracts[0]!.blockNumberHex), subtraces: 0, transactionHash: heavyContracts[0]!.txHash, transactionPosition: 0, @@ -118,7 +120,9 @@ describe("`trace_filter` tests to verify some heavy logic in contracts", () => { expect(response[0].action.init).to.be.a("string"); expect(response[0].action.value).to.equal("0x0"); expect(response[0].blockHash).to.be.a("string"); - expect(response[0].blockNumber).to.equal(4); + expect(response[0].blockNumber).to.equal( + hexToNumber(heavyContracts[3]!.blockNumberHex), + ); expect(response[0].result).to.equal(undefined); expect(response[0].error).to.equal("Reverted"); expect(response[0].subtraces).to.equal(0); From fd2f0474e9971332d126a4708b299abc9a71cafa Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 24 Oct 2025 15:16:22 +0300 Subject: [PATCH 27/32] Fix call naming at callee contract --- utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts | 6 +++--- utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts index bdad3dfa6..3fa9b2040 100644 --- a/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/callee.ts @@ -3,7 +3,7 @@ // contract TraceCallee { // uint256 public store; // -// function addtwo(uint256 value) external returns (uint256 result) { +// function addSeven(uint256 value) external returns (uint256 result) { // uint256 x = 7; // store = value; // return value + x; @@ -20,7 +20,7 @@ export default { type: "uint256", }, ], - name: "addtwo", + name: "addSeven", outputs: [ { internalType: "uint256", @@ -46,5 +46,5 @@ export default { }, ], bytecode: - "0x6080604052348015600e575f5ffd5b506101cb8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c8063975057e714610038578063fd63983b14610056575b5f5ffd5b610040610086565b60405161004d91906100c3565b60405180910390f35b610070600480360381019061006b919061010a565b61008b565b60405161007d91906100c3565b60405180910390f35b5f5481565b5f5f60079050825f8190555080836100a39190610162565b915050919050565b5f819050919050565b6100bd816100ab565b82525050565b5f6020820190506100d65f8301846100b4565b92915050565b5f5ffd5b6100e9816100ab565b81146100f3575f5ffd5b50565b5f81359050610104816100e0565b92915050565b5f6020828403121561011f5761011e6100dc565b5b5f61012c848285016100f6565b91505092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61016c826100ab565b9150610177836100ab565b925082820190508082111561018f5761018e610135565b5b9291505056fea264697066735822122021bf5f7d11f3386a1ddb70401a730984be0c92eb7804616e5ef6fc9ef1d6d6e864736f6c634300081e0033", + "0x6080604052348015600e575f5ffd5b506101cb8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c80631ba6484614610038578063975057e714610068575b5f5ffd5b610052600480360381019061004d91906100e2565b610086565b60405161005f919061011c565b60405180910390f35b6100706100a6565b60405161007d919061011c565b60405180910390f35b5f5f60079050825f81905550808361009e9190610162565b915050919050565b5f5481565b5f5ffd5b5f819050919050565b6100c1816100af565b81146100cb575f5ffd5b50565b5f813590506100dc816100b8565b92915050565b5f602082840312156100f7576100f66100ab565b5b5f610104848285016100ce565b91505092915050565b610116816100af565b82525050565b5f60208201905061012f5f83018461010d565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61016c826100af565b9150610177836100af565b925082820190508082111561018f5761018e610135565b5b9291505056fea26469706673582212209aa1341b3eba47419a335ba344ab0e24f3ae67f3e68677260ec0784c164cd05d64736f6c634300081e0033", } as const; diff --git a/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts b/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts index a66e8d2c5..549167f8f 100644 --- a/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts +++ b/utils/e2e-tests/ts/lib/abis/evmTracing/caller.ts @@ -6,7 +6,7 @@ // // function someAction(address addr, uint256 number) public { // callee = TraceCallee(addr); -// store = callee.addtwo(number); +// store = callee.addSeven(number); // } // } @@ -45,5 +45,5 @@ export default { }, ], bytecode: - "0x6080604052348015600e575f5ffd5b506102c68061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c8063398f722314610038578063975057e714610054575b5f5ffd5b610052600480360381019061004d91906101eb565b610072565b005b61005c610154565b6040516100699190610238565b60405180910390f35b815f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fd63983b826040518263ffffffff1660e01b815260040161010a9190610238565b6020604051808303815f875af1158015610126573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061014a9190610265565b6001819055505050565b60015481565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6101878261015e565b9050919050565b6101978161017d565b81146101a1575f5ffd5b50565b5f813590506101b28161018e565b92915050565b5f819050919050565b6101ca816101b8565b81146101d4575f5ffd5b50565b5f813590506101e5816101c1565b92915050565b5f5f604083850312156102015761020061015a565b5b5f61020e858286016101a4565b925050602061021f858286016101d7565b9150509250929050565b610232816101b8565b82525050565b5f60208201905061024b5f830184610229565b92915050565b5f8151905061025f816101c1565b92915050565b5f6020828403121561027a5761027961015a565b5b5f61028784828501610251565b9150509291505056fea264697066735822122019f0f10cf26630f53914cf389a6d6eab4acc8bea5cd28adaa7572a62ba7030eb64736f6c634300081e0033", + "0x6080604052348015600e575f5ffd5b506102c68061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610034575f3560e01c8063398f722314610038578063975057e714610054575b5f5ffd5b610052600480360381019061004d91906101eb565b610072565b005b61005c610154565b6040516100699190610238565b60405180910390f35b815f5f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16631ba64846826040518263ffffffff1660e01b815260040161010a9190610238565b6020604051808303815f875af1158015610126573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061014a9190610265565b6001819055505050565b60015481565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6101878261015e565b9050919050565b6101978161017d565b81146101a1575f5ffd5b50565b5f813590506101b28161018e565b92915050565b5f819050919050565b6101ca816101b8565b81146101d4575f5ffd5b50565b5f813590506101e5816101c1565b92915050565b5f5f604083850312156102015761020061015a565b5b5f61020e858286016101a4565b925050602061021f858286016101d7565b9150509250929050565b610232816101b8565b82525050565b5f60208201905061024b5f830184610229565b92915050565b5f8151905061025f816101c1565b92915050565b5f6020828403121561027a5761027961015a565b5b5f61028784828501610251565b9150509291505056fea264697066735822122063e98df9ee71802baca2f482bdaadad21254526b1716f2dea23f395c7d9c055e64736f6c634300081e0033", } as const; From d6e81599591d70cd580db3972caae637677793f0 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Fri, 24 Oct 2025 15:50:26 +0300 Subject: [PATCH 28/32] Improve checks related with RETURN opcode --- .../ts/tests/evm-tracing/debugTraceCallGeneral.ts | 13 ++++++------- .../evm-tracing/debugTraceTransactionGeneral.ts | 13 ++++++------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts index 41aaa360a..c1dce6d36 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceCallGeneral.ts @@ -79,14 +79,13 @@ describe("`debug_traceCall` tests to verify general logic", () => { ); const logs: any[] = []; - for (const log of response.structLogs) { - if (logs.length === 1) { - logs.push(log); + response.structLogs.forEach((item: any, index: number) => { + if (item.op === "RETURN") { + logs.push(item); + logs.push(response.structLogs[index + 1]); } - if (log.op === "RETURN") { - logs.push(log); - } - } + }); + expect(logs).to.be.lengthOf(2); expect(logs[0].depth).to.be.equal(2); expect(logs[1].depth).to.be.equal(1); diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts index 53ccfe457..975e7820c 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts @@ -80,14 +80,13 @@ describe("`debug_traceTransaction` tests to verify general logic", () => { ); const logs: any[] = []; - for (const log of response.structLogs) { - if (logs.length === 1) { - logs.push(log); - } - if (log.op === "RETURN") { - logs.push(log); + response.structLogs.forEach((item: any, index: number) => { + if (item.op === "RETURN") { + logs.push(item); + logs.push(response.structLogs[index + 1]); } - } + }); + expect(logs).to.be.lengthOf(2); expect(logs[0].depth).to.be.equal(2); expect(logs[1].depth).to.be.equal(1); From d496342472250750e78ccec6a5e25b37d311d8e0 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Tue, 28 Oct 2025 21:20:14 +0300 Subject: [PATCH 29/32] Move blockscout fixtures to separate dir and add README --- utils/e2e-tests/ts/lib/helpers/blockscout/README.md | 8 ++++++++ .../tracer.min.json} | 0 .../tracer_v2.min.json} | 0 .../ts/tests/evm-tracing/debugTraceTransactionGeneral.ts | 4 ++-- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 utils/e2e-tests/ts/lib/helpers/blockscout/README.md rename utils/e2e-tests/ts/lib/helpers/{blockscout_tracer.min.json => blockscout/tracer.min.json} (100%) rename utils/e2e-tests/ts/lib/helpers/{blockscout_tracer_v2.min.json => blockscout/tracer_v2.min.json} (100%) diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout/README.md b/utils/e2e-tests/ts/lib/helpers/blockscout/README.md new file mode 100644 index 000000000..bbc8939ca --- /dev/null +++ b/utils/e2e-tests/ts/lib/helpers/blockscout/README.md @@ -0,0 +1,8 @@ +# Blockscout test helpers + +The directory contains a list of fixtures containing blockscout tracer code. + +## Source data + +- [tracer_v2](tracer_v2.min.json) - [source code](https://github.com/moonbeam-foundation/moonbeam/blob/d47da4eee70f60bd835c39f9e6098e1e4e0c9347/test/helpers/tracer/blockscout_tracer_v2.min.json) +- [tracer](tracer.min.json) - [source code](https://github.com/moonbeam-foundation/moonbeam/blob/d47da4eee70f60bd835c39f9e6098e1e4e0c9347/test/helpers/tracer/blockscout_tracer.min.json) diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout_tracer.min.json b/utils/e2e-tests/ts/lib/helpers/blockscout/tracer.min.json similarity index 100% rename from utils/e2e-tests/ts/lib/helpers/blockscout_tracer.min.json rename to utils/e2e-tests/ts/lib/helpers/blockscout/tracer.min.json diff --git a/utils/e2e-tests/ts/lib/helpers/blockscout_tracer_v2.min.json b/utils/e2e-tests/ts/lib/helpers/blockscout/tracer_v2.min.json similarity index 100% rename from utils/e2e-tests/ts/lib/helpers/blockscout_tracer_v2.min.json rename to utils/e2e-tests/ts/lib/helpers/blockscout/tracer_v2.min.json diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts index 975e7820c..97657403c 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionGeneral.ts @@ -8,8 +8,8 @@ import looper from "../../lib/abis/evmTracing/looper"; import heavy from "../../lib/abis/evmTracing/heavy"; import evmSwap from "../../lib/abis/evmSwap"; import incrementor from "../../lib/abis/evmTracing/incrementor"; -import BS_TRACER from "../../lib/helpers/blockscout_tracer.min.json"; -import BS_TRACER_V2 from "../../lib/helpers/blockscout_tracer_v2.min.json"; +import BS_TRACER from "../../lib/helpers/blockscout/tracer.min.json"; +import BS_TRACER_V2 from "../../lib/helpers/blockscout/tracer_v2.min.json"; import { encodeFunctionData, hexToNumber } from "viem"; import { customRpcRequest } from "../../lib/rpcUtils"; From b751319df001223ad0d512676412265c30ce941d Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Tue, 28 Oct 2025 22:05:11 +0300 Subject: [PATCH 30/32] Fix blocks number calculation at traceFilterGeneral --- utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts index a80b5fcde..d1f7ca904 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts @@ -58,7 +58,9 @@ describe("`trace_filter` tests to verify general logic", () => { ], ); - expect(BigInt(response.length)).to.equal(secondBlockNumber); + expect(BigInt(response.length)).to.equal( + secondBlockNumber - firstBlockNumber + 1n, + ); for (const index in response.length) { expect(response[index].blockNumber).to.equal(firstBlockNumber + index); From 533294424871051ee15eddc429881033e50409ed Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 29 Oct 2025 14:23:58 +0300 Subject: [PATCH 31/32] Use single transaction to check tracing block by number and hash at debugTraceTransactionCallTracer --- .../debugTraceTransactionCallTracer.ts | 107 +++++++----------- 1 file changed, 43 insertions(+), 64 deletions(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts index 8662ce578..0876f98de 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/debugTraceTransactionCallTracer.ts @@ -124,39 +124,19 @@ describe("`debug_traceTransaction` tests to verify `callTracer` usage logic", () it("should trace block by number and hash", async () => { const [alice, _] = devClients; - const totalTxs = 3; - const txsPromises: any[] = []; - - const nonce = await publicClient.getTransactionCount({ - address: alice.account.address, + const txHash = await alice.sendTransaction({ + to: callerAddress, + data: encodeFunctionData({ + abi: caller.abi, + functionName: "someAction", + args: [calleeAddress, 7n], + }), }); - - for (let numTxs = 0; numTxs < totalTxs; numTxs++) { - const txsPromise = alice - .sendTransaction({ - to: callerAddress, - data: encodeFunctionData({ - abi: caller.abi, - functionName: "someAction", - args: [calleeAddress, 7n], - }), - gas: 100_000n, - nonce: nonce + numTxs, - }) - .then((txHash) => - publicClient.waitForTransactionReceipt({ - hash: txHash, - timeout: 18_000, - }), - ); - - txsPromises.push(txsPromise); - } - - const txsReceipts = await Promise.all(txsPromises); - - const blockNumberHex = txsReceipts[0].blockNumber.toString(16); - const blockHash = txsReceipts[0].blockHash; + const txReceipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const blockNumberHex = txReceipt.blockNumber.toString(16); + const blockHash = txReceipt.blockHash; // Trace block by number. const responseByNumber = await customRpcRequest( @@ -164,22 +144,23 @@ describe("`debug_traceTransaction` tests to verify `callTracer` usage logic", () "debug_traceBlockByNumber", [blockNumberHex, { tracer: "callTracer" }], ); - expect(responseByNumber.length).to.be.equal(3); - responseByNumber.forEach((trace: { [key: string]: any }, index: number) => { - expect(trace["txHash"]).to.be.equal(txsReceipts[index].transactionHash); - expect(trace["result"].calls.length).to.be.equal(1); - expect(Object.keys(trace["result"]).sort()).to.deep.equal([ - "calls", - "from", - "gas", - "gasUsed", - "input", - "output", - "to", - "type", - "value", - ]); - }); + + expect(responseByNumber.length).to.be.equal(1); + expect(responseByNumber[0]["txHash"]).to.be.equal( + txReceipt.transactionHash, + ); + expect(responseByNumber[0]["result"].calls.length).to.be.equal(1); + expect(Object.keys(responseByNumber[0]["result"]).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); // Trace block by hash (actually the rpc method is an alias of `debug_traceBlockByNumber`). const responseByHash = await customRpcRequest( @@ -187,21 +168,19 @@ describe("`debug_traceTransaction` tests to verify `callTracer` usage logic", () "debug_traceBlockByNumber", [blockHash, { tracer: "callTracer" }], ); - expect(responseByHash.length).to.be.equal(3); - responseByHash.forEach((trace: { [key: string]: any }, index: number) => { - expect(trace["txHash"]).to.be.equal(txsReceipts[index].transactionHash); - expect(trace["result"].calls.length).to.be.equal(1); - expect(Object.keys(trace["result"]).sort()).to.deep.equal([ - "calls", - "from", - "gas", - "gasUsed", - "input", - "output", - "to", - "type", - "value", - ]); - }); + expect(responseByHash.length).to.be.equal(1); + expect(responseByHash[0]["txHash"]).to.be.equal(txReceipt.transactionHash); + expect(responseByHash[0]["result"].calls.length).to.be.equal(1); + expect(Object.keys(responseByHash[0]["result"]).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); }); }); From 9c9d1d508ca0472be0033cc93dce3f0540d8cce7 Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov Date: Wed, 29 Oct 2025 14:38:31 +0300 Subject: [PATCH 32/32] Check transaction position if it's a part of a corresponding block at traceFilterGeneral --- .../ts/tests/evm-tracing/traceFilterGeneral.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts index d1f7ca904..f459d6663 100644 --- a/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts +++ b/utils/e2e-tests/ts/tests/evm-tracing/traceFilterGeneral.ts @@ -63,8 +63,14 @@ describe("`trace_filter` tests to verify general logic", () => { ); for (const index in response.length) { - expect(response[index].blockNumber).to.equal(firstBlockNumber + index); - expect(response[index].transactionPosition).to.equal(0); + const blockNumber = response[index].blockNumber; + expect(blockNumber).to.equal(firstBlockNumber + index); + if ( + blockNumber == firstBlockNumber || + blockNumber == secondBlockNumberHex + ) { + expect(response[index].transactionPosition).to.equal(0); + } } });