diff --git a/Cargo.lock b/Cargo.lock index 200a056f28251..25e7e194aeb4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14803,6 +14803,7 @@ dependencies = [ name = "pallet-revive" version = "0.1.0" dependencies = [ + "alloy-core", "array-bytes", "assert_matches", "derive_more 0.99.17", @@ -14903,6 +14904,7 @@ dependencies = [ "sp-arithmetic 23.0.0", "sp-core 28.0.0", "sp-crypto-hashing 0.1.0", + "sp-runtime 31.0.1", "sp-weights 27.0.0", "sqlx", "static_init", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index f73db17194bcf..6e0fa19320dd2 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -2307,6 +2307,67 @@ impl_runtime_apis! { key ) } + + fn trace_block( + block: Block, + config: pallet_revive::evm::TracerConfig + ) -> Vec<(u32, pallet_revive::evm::CallTrace)> { + use pallet_revive::tracing::trace; + let mut tracer = config.build(Revive::evm_gas_from_weight); + let mut traces = vec![]; + let (header, extrinsics) = block.deconstruct(); + + Executive::initialize_block(&header); + for (index, ext) in extrinsics.into_iter().enumerate() { + trace(&mut tracer, || { + let _ = Executive::apply_extrinsic(ext); + }); + + if let Some(tx_trace) = tracer.collect_traces().pop() { + traces.push((index as u32, tx_trace)); + } + } + + traces + } + + fn trace_tx( + block: Block, + tx_index: u32, + config: pallet_revive::evm::TracerConfig + ) -> Option { + use pallet_revive::tracing::trace; + let mut tracer = config.build(Revive::evm_gas_from_weight); + let (header, extrinsics) = block.deconstruct(); + + Executive::initialize_block(&header); + for (index, ext) in extrinsics.into_iter().enumerate() { + if index as u32 == tx_index { + trace(&mut tracer, || { + let _ = Executive::apply_extrinsic(ext); + }); + break; + } else { + let _ = Executive::apply_extrinsic(ext); + } + } + + tracer.collect_traces().pop() + } + + fn trace_call( + tx: pallet_revive::evm::GenericTransaction, + config: pallet_revive::evm::TracerConfig) + -> Result + { + use pallet_revive::tracing::trace; + let mut tracer = config.build(Revive::evm_gas_from_weight); + trace(&mut tracer, || { + Self::eth_transact(tx) + })?; + + Ok(tracer.collect_traces().pop().expect("eth_transact succeeded, trace must exist, qed")) + } } } diff --git a/prdoc/pr_7167.prdoc b/prdoc/pr_7167.prdoc new file mode 100644 index 0000000000000..8faae9f8af345 --- /dev/null +++ b/prdoc/pr_7167.prdoc @@ -0,0 +1,16 @@ +title: '[pallet-revive] Add tracing support (2/2)' +doc: +- audience: Runtime Dev + description: |- + - Add debug endpoint to eth-rpc for capturing a block or a single transaction traces + - Use in-memory DB for non-archive node + + See: + - PR #7166 +crates: +- name: pallet-revive-eth-rpc + bump: minor +- name: pallet-revive + bump: minor +- name: asset-hub-westend-runtime + bump: minor diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 8986d61da6bed..795e80e6746cb 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -3406,6 +3406,67 @@ impl_runtime_apis! { key ) } + + fn trace_block( + block: Block, + config: pallet_revive::evm::TracerConfig + ) -> Vec<(u32, pallet_revive::evm::CallTrace)> { + use pallet_revive::tracing::trace; + let mut tracer = config.build(Revive::evm_gas_from_weight); + let mut traces = vec![]; + let (header, extrinsics) = block.deconstruct(); + + Executive::initialize_block(&header); + for (index, ext) in extrinsics.into_iter().enumerate() { + trace(&mut tracer, || { + let _ = Executive::apply_extrinsic(ext); + }); + + if let Some(tx_trace) = tracer.collect_traces().pop() { + traces.push((index as u32, tx_trace)); + } + } + + traces + } + + fn trace_tx( + block: Block, + tx_index: u32, + config: pallet_revive::evm::TracerConfig + ) -> Option { + use pallet_revive::tracing::trace; + let mut tracer = config.build(Revive::evm_gas_from_weight); + let (header, extrinsics) = block.deconstruct(); + + Executive::initialize_block(&header); + for (index, ext) in extrinsics.into_iter().enumerate() { + if index as u32 == tx_index { + trace(&mut tracer, || { + let _ = Executive::apply_extrinsic(ext); + }); + break; + } else { + let _ = Executive::apply_extrinsic(ext); + } + } + + tracer.collect_traces().pop() + } + + fn trace_call( + tx: pallet_revive::evm::GenericTransaction, + config: pallet_revive::evm::TracerConfig) + -> Result + { + use pallet_revive::tracing::trace; + let mut tracer = config.build(Revive::evm_gas_from_weight); + trace(&mut tracer, || { + Self::eth_transact(tx) + })?; + + Ok(tracer.collect_traces().pop().expect("eth_transact succeeded, trace must exist, qed")) + } } impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi< diff --git a/substrate/frame/revive/Cargo.toml b/substrate/frame/revive/Cargo.toml index 4faa9205378fe..09cbf0b49f591 100644 --- a/substrate/frame/revive/Cargo.toml +++ b/substrate/frame/revive/Cargo.toml @@ -17,6 +17,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +alloy-core = { workspace = true, features = ["sol-types"] } codec = { features = ["derive", "max-encoded-len"], workspace = true } derive_more = { workspace = true } environmental = { workspace = true } @@ -77,6 +78,7 @@ xcm-builder = { workspace = true, default-features = true } [features] default = ["std"] std = [ + "alloy-core/std", "codec/std", "environmental/std", "ethabi/std", diff --git a/substrate/frame/revive/rpc/.sqlx/query-2fcbf357b3993c0065141859e5ad8c11bd7800e3e6d22e8383ab9ac8bbec25b1.json b/substrate/frame/revive/rpc/.sqlx/query-2fcbf357b3993c0065141859e5ad8c11bd7800e3e6d22e8383ab9ac8bbec25b1.json new file mode 100644 index 0000000000000..07e69b7d8f10b --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-2fcbf357b3993c0065141859e5ad8c11bd7800e3e6d22e8383ab9ac8bbec25b1.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n\t\t SELECT transaction_index, transaction_hash\n\t\t FROM transaction_hashes\n\t\t WHERE block_hash = $1\n\t\t ", + "describe": { + "columns": [ + { + "name": "transaction_index", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "transaction_hash", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "2fcbf357b3993c0065141859e5ad8c11bd7800e3e6d22e8383ab9ac8bbec25b1" +} diff --git a/substrate/frame/revive/rpc/.sqlx/query-6345c84da6afad02d0fdf4e1657c53e64320c118d39db73f573510235baf4ba0.json b/substrate/frame/revive/rpc/.sqlx/query-6345c84da6afad02d0fdf4e1657c53e64320c118d39db73f573510235baf4ba0.json new file mode 100644 index 0000000000000..498b125292037 --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-6345c84da6afad02d0fdf4e1657c53e64320c118d39db73f573510235baf4ba0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM logs\n WHERE block_hash = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "6345c84da6afad02d0fdf4e1657c53e64320c118d39db73f573510235baf4ba0" +} diff --git a/substrate/frame/revive/rpc/.sqlx/query-d7377b5a09f075668d259d02e3fc7a12048a70a33b96381118d6c24210afce34.json b/substrate/frame/revive/rpc/.sqlx/query-d7377b5a09f075668d259d02e3fc7a12048a70a33b96381118d6c24210afce34.json new file mode 100644 index 0000000000000..e4979066814af --- /dev/null +++ b/substrate/frame/revive/rpc/.sqlx/query-d7377b5a09f075668d259d02e3fc7a12048a70a33b96381118d6c24210afce34.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM transaction_hashes\n WHERE block_hash = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "d7377b5a09f075668d259d02e3fc7a12048a70a33b96381118d6c24210afce34" +} diff --git a/substrate/frame/revive/rpc/Cargo.toml b/substrate/frame/revive/rpc/Cargo.toml index 76f9be56eb40d..b207a6041b9b6 100644 --- a/substrate/frame/revive/rpc/Cargo.toml +++ b/substrate/frame/revive/rpc/Cargo.toml @@ -41,7 +41,6 @@ path = "examples/rust/remark-extrinsic.rs" anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } codec = { workspace = true, features = ["derive"] } -ethabi = { version = "18.0.0" } futures = { workspace = true, features = ["thread-pool"] } hex = { workspace = true } jsonrpsee = { workspace = true, features = ["full"] } @@ -57,6 +56,7 @@ sc-service = { workspace = true, default-features = true } sp-arithmetic = { workspace = true, default-features = true } sp-core = { workspace = true, default-features = true } sp-crypto-hashing = { workspace = true } +sp-runtime = { workspace = true, default-features = true } sp-weights = { workspace = true, default-features = true } sqlx = { version = "0.8.2", features = ["macros", "runtime-tokio", "sqlite"] } subxt = { workspace = true, default-features = true, features = [ @@ -70,6 +70,7 @@ tokio = { workspace = true, features = ["full"] } [dev-dependencies] env_logger = { workspace = true } +ethabi = { version = "18.0.0" } pretty_assertions = { workspace = true } static_init = { workspace = true } substrate-cli-test-utils = { workspace = true } diff --git a/substrate/frame/revive/rpc/examples/js/.solhint.json b/substrate/frame/revive/rpc/examples/js/.solhint.json new file mode 100644 index 0000000000000..83a795a1f4eef --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/.solhint.json @@ -0,0 +1,9 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error", "^0.8.0"], + "gas-custom-errors": "off", + "one-contract-per-file": "off", + "no-empty-blocks": "off" + } +} diff --git a/substrate/frame/revive/rpc/examples/js/bun.lockb b/substrate/frame/revive/rpc/examples/js/bun.lockb index 39a1d0906b70e..3b8ad3d048b07 100755 Binary files a/substrate/frame/revive/rpc/examples/js/bun.lockb and b/substrate/frame/revive/rpc/examples/js/bun.lockb differ diff --git a/substrate/frame/revive/rpc/examples/js/contracts/.solhint.json b/substrate/frame/revive/rpc/examples/js/contracts/.solhint.json deleted file mode 100644 index 2614a969da39b..0000000000000 --- a/substrate/frame/revive/rpc/examples/js/contracts/.solhint.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "solhint:recommended" -} diff --git a/substrate/frame/revive/rpc/examples/js/contracts/Flipper.sol b/substrate/frame/revive/rpc/examples/js/contracts/Flipper.sol index 51aaafcae4288..93b09f08c2723 100644 --- a/substrate/frame/revive/rpc/examples/js/contracts/Flipper.sol +++ b/substrate/frame/revive/rpc/examples/js/contracts/Flipper.sol @@ -20,7 +20,7 @@ contract FlipperCaller { address public flipperAddress; // Constructor to initialize Flipper's address - constructor(address _flipperAddress) { + constructor(address _flipperAddress) public { flipperAddress = _flipperAddress; } diff --git a/substrate/frame/revive/rpc/examples/js/contracts/PiggyBank.sol b/substrate/frame/revive/rpc/examples/js/contracts/PiggyBank.sol index 0c8a4d26f4dc2..89be1b2589dd5 100644 --- a/substrate/frame/revive/rpc/examples/js/contracts/PiggyBank.sol +++ b/substrate/frame/revive/rpc/examples/js/contracts/PiggyBank.sol @@ -6,7 +6,7 @@ contract PiggyBank { uint256 private balance; address public owner; - constructor() { + constructor() public { owner = msg.sender; balance = 0; } @@ -21,7 +21,7 @@ contract PiggyBank { } function withdraw(uint256 withdrawAmount) public returns (uint256 remainingBal) { - require(msg.sender == owner); + require(msg.sender == owner, "You are not the owner"); balance -= withdrawAmount; (bool success, ) = payable(msg.sender).call{value: withdrawAmount}(""); require(success, "Transfer failed"); diff --git a/substrate/frame/revive/rpc/examples/js/contracts/Tracing.sol b/substrate/frame/revive/rpc/examples/js/contracts/Tracing.sol new file mode 100644 index 0000000000000..c7867fc4a0536 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/contracts/Tracing.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract TracingCaller { + event TraceEvent(uint256 value, string message); + address payable public callee; + + constructor(address payable _callee) public payable { + require(_callee != address(0), "Callee address cannot be zero"); + callee = _callee; + } + + function start(uint256 counter) external { + if (counter == 0) { + return; + } + + uint256 paymentAmount = 0.01 ether; + callee.transfer(paymentAmount); + + emit TraceEvent(counter, "before"); + TracingCallee(callee).consumeGas(counter); + emit TraceEvent(counter, "after"); + + try TracingCallee(callee).failingFunction{value: paymentAmount}() { + } catch { + } + + this.start(counter - 1); + } +} + +contract TracingCallee { + event CalleeCalled(uint256 counter); + + function consumeGas(uint256 counter) external { + // burn some gas + for (uint256 i = 0; i < 10; i++) { + uint256(keccak256(abi.encodePacked(i))); + } + + emit CalleeCalled(counter); + } + + function failingFunction() external payable { + require(false, "This function always fails"); + } + + // Enable contract to receive Ether + receive() external payable {} +} + diff --git a/substrate/frame/revive/rpc/examples/js/package.json b/substrate/frame/revive/rpc/examples/js/package.json index e181461cf8615..5b225711dc552 100644 --- a/substrate/frame/revive/rpc/examples/js/package.json +++ b/substrate/frame/revive/rpc/examples/js/package.json @@ -7,12 +7,14 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "prettier": "prettier --write ." + "prettier": "prettier --write .", + "solhint": "solhint \"contracts/**/*.sol\"" }, "dependencies": { "@parity/revive": "^0.0.9", "ethers": "^6.13.5", "solc": "^0.8.28", + "solhint": "^5.0.5", "viem": "^2.22.4" }, "devDependencies": { diff --git a/substrate/frame/revive/rpc/examples/js/src/fixtures/debug_traceCall.json b/substrate/frame/revive/rpc/examples/js/src/fixtures/debug_traceCall.json new file mode 100644 index 0000000000000..b017c97693ddc --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/src/fixtures/debug_traceCall.json @@ -0,0 +1,143 @@ +{ + "from": "0x0000000000000000000000000000000000000000", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000002", + "calls": [ + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xa329e8de0000000000000000000000000000000000000000000000000000000000000002", + "logs": [ + { + "address": "", + "topics": [ + "0xa07465e8ec189714f79f3786a8f616baf78ebd9cb1769bd61f18de45f2567360" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002", + "position": "0x0" + } + ], + "value": "0x0", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xd1b96663", + "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000", + "error": "execution reverted", + "revertReason": "This function always fails", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000001", + "calls": [ + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xa329e8de0000000000000000000000000000000000000000000000000000000000000001", + "logs": [ + { + "address": "", + "topics": [ + "0xa07465e8ec189714f79f3786a8f616baf78ebd9cb1769bd61f18de45f2567360" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "position": "0x0" + } + ], + "value": "0x0", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xd1b96663", + "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000", + "error": "execution reverted", + "revertReason": "This function always fails", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0", + "type": "CALL" + } + ], + "logs": [ + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066265666f72650000000000000000000000000000000000000000000000000000", + "position": "0x1" + }, + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000056166746572000000000000000000000000000000000000000000000000000000", + "position": "0x2" + } + ], + "value": "0x0", + "type": "CALL" + } + ], + "logs": [ + { + "address": "", + "topics": ["0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549"], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066265666f72650000000000000000000000000000000000000000000000000000", + "position": "0x1" + }, + { + "address": "", + "topics": ["0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549"], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000056166746572000000000000000000000000000000000000000000000000000000", + "position": "0x2" + } + ], + "value": "0x0", + "type": "CALL" +} diff --git a/substrate/frame/revive/rpc/examples/js/src/fixtures/trace_block.json b/substrate/frame/revive/rpc/examples/js/src/fixtures/trace_block.json new file mode 100644 index 0000000000000..952c1c5ce084e --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/src/fixtures/trace_block.json @@ -0,0 +1,152 @@ +[ + { + "txHash": "", + "result": { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000002", + "calls": [ + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xa329e8de0000000000000000000000000000000000000000000000000000000000000002", + "logs": [ + { + "address": "", + "topics": [ + "0xa07465e8ec189714f79f3786a8f616baf78ebd9cb1769bd61f18de45f2567360" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002", + "position": "0x0" + } + ], + "value": "0x0", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xd1b96663", + "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000", + "error": "execution reverted", + "revertReason": "This function always fails", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000001", + "calls": [ + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xa329e8de0000000000000000000000000000000000000000000000000000000000000001", + "logs": [ + { + "address": "", + "topics": [ + "0xa07465e8ec189714f79f3786a8f616baf78ebd9cb1769bd61f18de45f2567360" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "position": "0x0" + } + ], + "value": "0x0", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xd1b96663", + "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000", + "error": "execution reverted", + "revertReason": "This function always fails", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0", + "type": "CALL" + } + ], + "logs": [ + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066265666f72650000000000000000000000000000000000000000000000000000", + "position": "0x1" + }, + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000056166746572000000000000000000000000000000000000000000000000000000", + "position": "0x2" + } + ], + "value": "0x0", + "type": "CALL" + } + ], + "logs": [ + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066265666f72650000000000000000000000000000000000000000000000000000", + "position": "0x1" + }, + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000056166746572000000000000000000000000000000000000000000000000000000", + "position": "0x2" + } + ], + "value": "0x0", + "type": "CALL" + } + } +] diff --git a/substrate/frame/revive/rpc/examples/js/src/fixtures/trace_transaction.json b/substrate/frame/revive/rpc/examples/js/src/fixtures/trace_transaction.json new file mode 100644 index 0000000000000..9ef9c7c4dfca7 --- /dev/null +++ b/substrate/frame/revive/rpc/examples/js/src/fixtures/trace_transaction.json @@ -0,0 +1,143 @@ +{ + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000002", + "calls": [ + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xa329e8de0000000000000000000000000000000000000000000000000000000000000002", + "logs": [ + { + "address": "", + "topics": [ + "0xa07465e8ec189714f79f3786a8f616baf78ebd9cb1769bd61f18de45f2567360" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002", + "position": "0x0" + } + ], + "value": "0x0", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xd1b96663", + "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000", + "error": "execution reverted", + "revertReason": "This function always fails", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000001", + "calls": [ + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xa329e8de0000000000000000000000000000000000000000000000000000000000000001", + "logs": [ + { + "address": "", + "topics": [ + "0xa07465e8ec189714f79f3786a8f616baf78ebd9cb1769bd61f18de45f2567360" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "position": "0x0" + } + ], + "value": "0x0", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0xd1b96663", + "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000", + "error": "execution reverted", + "revertReason": "This function always fails", + "value": "0x2386f26fc10000", + "type": "CALL" + }, + { + "from": "", + "gas": "0x42", + "gasUsed": "0x42", + "to": "", + "input": "0x95805dad0000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0", + "type": "CALL" + } + ], + "logs": [ + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066265666f72650000000000000000000000000000000000000000000000000000", + "position": "0x1" + }, + { + "address": "", + "topics": [ + "0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000056166746572000000000000000000000000000000000000000000000000000000", + "position": "0x2" + } + ], + "value": "0x0", + "type": "CALL" + } + ], + "logs": [ + { + "address": "", + "topics": ["0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549"], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066265666f72650000000000000000000000000000000000000000000000000000", + "position": "0x1" + }, + { + "address": "", + "topics": ["0x25d760f35a7a9cb2bffd2ea8756913655b3786c642f300a702e2934062763549"], + "data": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000056166746572000000000000000000000000000000000000000000000000000000", + "position": "0x2" + } + ], + "value": "0x0", + "type": "CALL" +} diff --git a/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts b/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts index c6417e3198bdc..33e38fccd8a81 100644 --- a/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts +++ b/substrate/frame/revive/rpc/examples/js/src/geth-diff.test.ts @@ -5,11 +5,14 @@ import { killProcessOnPort, waitForHealth, polkadotSdkPath, + visit, } from './util.ts' import { afterAll, afterEach, describe, expect, test } from 'bun:test' import { encodeFunctionData, Hex, parseEther, decodeEventLog } from 'viem' import { ErrorsAbi } from '../abi/Errors' import { EventExampleAbi } from '../abi/EventExample' +import { TracingCallerAbi } from '../abi/TracingCaller' +import { TracingCalleeAbi } from '../abi/TracingCallee' import { Subprocess, spawn } from 'bun' import { fail } from 'node:assert' @@ -129,6 +132,41 @@ for (const env of envs) { } })() + const getTracingExampleAddrs = (() => { + let callerAddr: Hex = '0x' + let calleeAddr: Hex = '0x' + return async () => { + if (callerAddr !== '0x') { + return [callerAddr, calleeAddr] + } + calleeAddr = await (async () => { + const hash = await env.serverWallet.deployContract({ + abi: TracingCalleeAbi, + bytecode: getByteCode('TracingCallee', env.evm), + }) + const receipt = await env.serverWallet.waitForTransactionReceipt({ + hash, + }) + return receipt.contractAddress! + })() + + callerAddr = await (async () => { + const hash = await env.serverWallet.deployContract({ + abi: TracingCallerAbi, + args: [calleeAddr], + bytecode: getByteCode('TracingCaller', env.evm), + value: parseEther('10'), + }) + const receipt = await env.serverWallet.waitForTransactionReceipt({ + hash, + }) + return receipt.contractAddress! + })() + + return [callerAddr, calleeAddr] + } + })() + test('triggerAssertError', async () => { try { await env.accountWallet.readContract({ @@ -143,7 +181,10 @@ for (const env of envs) { expect(lastJsonRpcError?.data).toBe( '0x4e487b710000000000000000000000000000000000000000000000000000000000000001' ) - expect(lastJsonRpcError?.message).toBe('execution reverted: assert(false)') + expect(lastJsonRpcError?.message).toBeOneOf([ + 'execution reverted: assert(false)', + 'execution reverted: panic: assertion failed (0x01)', + ]) } }) @@ -158,7 +199,10 @@ for (const env of envs) { } catch (err) { const lastJsonRpcError = jsonRpcErrors.pop() expect(lastJsonRpcError?.code).toBe(3) - expect(lastJsonRpcError?.message).toBe('execution reverted: This is a revert error') + expect(lastJsonRpcError?.message).toBeOneOf([ + 'execution reverted: This is a revert error', + 'execution reverted: revert: This is a revert error', + ]) expect(lastJsonRpcError?.data).toBe( '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001654686973206973206120726576657274206572726f7200000000000000000000' ) @@ -179,9 +223,10 @@ for (const env of envs) { expect(lastJsonRpcError?.data).toBe( '0x4e487b710000000000000000000000000000000000000000000000000000000000000012' ) - expect(lastJsonRpcError?.message).toBe( - 'execution reverted: division or modulo by zero' - ) + expect(lastJsonRpcError?.message).toBeOneOf([ + 'execution reverted: division or modulo by zero', + 'execution reverted: panic: division or modulo by zero (0x12)', + ]) } }) @@ -199,9 +244,10 @@ for (const env of envs) { expect(lastJsonRpcError?.data).toBe( '0x4e487b710000000000000000000000000000000000000000000000000000000000000032' ) - expect(lastJsonRpcError?.message).toBe( - 'execution reverted: out-of-bounds access of an array or bytesN' - ) + expect(lastJsonRpcError?.message).toBeOneOf([ + 'execution reverted: out-of-bounds access of an array or bytesN', + 'execution reverted: panic: array out-of-bounds access (0x32)', + ]) } }) @@ -308,9 +354,10 @@ for (const env of envs) { } catch (err) { const lastJsonRpcError = jsonRpcErrors.pop() expect(lastJsonRpcError?.code).toBe(3) - expect(lastJsonRpcError?.message).toBe( - 'execution reverted: msg.value does not match value' - ) + expect(lastJsonRpcError?.message).toBeOneOf([ + 'execution reverted: msg.value does not match value', + 'execution reverted: revert: msg.value does not match value', + ]) expect(lastJsonRpcError?.data).toBe( '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e6d73672e76616c756520646f6573206e6f74206d617463682076616c75650000' ) @@ -401,5 +448,84 @@ for (const env of envs) { }, }) }) + + test('tracing', async () => { + let [callerAddr, calleeAddr] = await getTracingExampleAddrs() + + const receipt = await (async () => { + const { request } = await env.serverWallet.simulateContract({ + address: callerAddr, + abi: TracingCallerAbi, + functionName: 'start', + args: [2n], + }) + const hash = await env.serverWallet.writeContract(request) + return await env.serverWallet.waitForTransactionReceipt({ hash }) + })() + + const visitor: Parameters[1] = (key, value) => { + switch (key) { + case 'address': + case 'from': + case 'to': { + if (value === callerAddr) { + return '' + } else if (value === calleeAddr) { + return '' + } else if (value == env.serverWallet.account.address.toLowerCase()) { + return '' + } + + return value + } + case 'revertReason': + return value.startsWith('revert: ') ? value.slice('revert: '.length) : value + + case 'gas': + case 'gasUsed': { + return '0x42' + } + case 'txHash': { + return '' + } + default: { + return value + } + } + } + + // test debug_traceTransaction + { + const fixture = await Bun.file('./src/fixtures/trace_transaction.json').json() + const res = await env.debugClient.traceTransaction(receipt.transactionHash, { + withLog: true, + }) + expect(visit(res, visitor)).toEqual(fixture) + } + + // test debug_traceBlock + { + const res = await env.debugClient.traceBlock(receipt.blockNumber, { withLog: true }) + const fixture = await Bun.file('./src/fixtures/trace_block.json').json() + expect(visit(res, visitor)).toEqual(fixture) + } + + // test debug_traceCall + { + const fixture = await Bun.file('./src/fixtures/debug_traceCall.json').json() + const res = await env.debugClient.traceCall( + { + to: callerAddr, + data: encodeFunctionData({ + abi: TracingCallerAbi, + functionName: 'start', + args: [2n], + }), + }, + { withLog: true } + ) + expect(visit(res, visitor)).toEqual(fixture) + } + }) }) } diff --git a/substrate/frame/revive/rpc/examples/js/src/spammer.ts b/substrate/frame/revive/rpc/examples/js/src/spammer.ts index 7ebee80278512..682bfcabb2698 100644 --- a/substrate/frame/revive/rpc/examples/js/src/spammer.ts +++ b/substrate/frame/revive/rpc/examples/js/src/spammer.ts @@ -9,26 +9,27 @@ import { } from './util' import { FlipperAbi } from '../abi/Flipper' -//Run the substate node -console.log('🚀 Start substrate-node...') -killProcessOnPort(9944) -spawn( - [ - './target/debug/substrate-node', - '--dev', - '-l=error,evm=debug,sc_rpc_server=info,runtime::revive=debug', - ], - { - stdout: Bun.file('/tmp/substrate-node.out.log'), - stderr: Bun.file('/tmp/substrate-node.err.log'), - cwd: polkadotSdkPath, - } -) +if (process.env.START_SUBSTRATE_NODE) { + //Run the substate node + console.log('🚀 Start substrate-node...') + killProcessOnPort(9944) + spawn( + [ + './target/debug/substrate-node', + '--dev', + '-l=error,evm=debug,sc_rpc_server=info,runtime::revive=debug', + ], + { + stdout: Bun.file('/tmp/substrate-node.out.log'), + stderr: Bun.file('/tmp/substrate-node.err.log'), + cwd: polkadotSdkPath, + } + ) +} // Run eth-rpc on 8545 -console.log('🚀 Start eth-rpc...') if (process.env.START_ETH_RPC) { - console.log('🔍 Start eth-rpc...') + console.log('🚀 Start eth-rpc...') killProcessOnPort(8545) spawn( [ @@ -43,9 +44,9 @@ if (process.env.START_ETH_RPC) { cwd: polkadotSdkPath, } ) - await waitForHealth('http://localhost:8545').catch() } +await waitForHealth('http://localhost:8545').catch() const env = await createEnv('eth-rpc') const wallet = env.accountWallet diff --git a/substrate/frame/revive/rpc/examples/js/src/util.ts b/substrate/frame/revive/rpc/examples/js/src/util.ts index 3a488da67d801..cf5c08d256106 100644 --- a/substrate/frame/revive/rpc/examples/js/src/util.ts +++ b/substrate/frame/revive/rpc/examples/js/src/util.ts @@ -1,7 +1,17 @@ import { spawnSync } from 'bun' import { resolve } from 'path' import { readFileSync } from 'fs' -import { createWalletClient, defineChain, Hex, http, publicActions } from 'viem' +import { + CallParameters, + createClient, + createWalletClient, + defineChain, + formatTransactionRequest, + type Hex, + hexToNumber, + http, + publicActions, +} from 'viem' import { privateKeyToAccount, nonceManager } from 'viem/accounts' export function getByteCode(name: string, evm: boolean = false): Hex { @@ -39,8 +49,21 @@ export async function createEnv(name: 'geth' | 'eth-rpc') { const gethPort = process.env.GETH_PORT || '8546' const ethRpcPort = process.env.ETH_RPC_PORT || '8545' const url = `http://localhost:${name == 'geth' ? gethPort : ethRpcPort}` + + let id = await (async () => { + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_chainId', id: 1 }), + }) + let { result } = await resp.json() + return hexToNumber(result) + })() + const chain = defineChain({ - id: name == 'geth' ? 1337 : 420420420, + id, name, nativeCurrency: { name: 'Westie', @@ -94,9 +117,43 @@ export async function createEnv(name: 'geth' | 'eth-rpc') { chain, }).extend(publicActions) - return { serverWallet, emptyWallet, accountWallet, evm: name == 'geth' } + const debugClient = createClient({ + chain, + transport, + }).extend((client) => ({ + async traceTransaction(txHash: Hex, tracerConfig: { withLog: boolean }) { + return client.request({ + method: 'debug_traceTransaction' as any, + params: [txHash, { tracer: 'callTracer', tracerConfig } as any], + }) + }, + async traceBlock(blockNumber: bigint, tracerConfig: { withLog: boolean }) { + return client.request({ + method: 'debug_traceBlockByNumber' as any, + params: [ + `0x${blockNumber.toString(16)}`, + { tracer: 'callTracer', tracerConfig } as any, + ], + }) + }, + + async traceCall(args: CallParameters, tracerConfig: { withLog: boolean }) { + return client.request({ + method: 'debug_traceCall' as any, + params: [ + formatTransactionRequest(args), + 'latest', + { tracer: 'callTracer', tracerConfig } as any, + ], + }) + }, + })) + + return { debugClient, emptyWallet, serverWallet, accountWallet, evm: name == 'geth' } } +export type Env = Awaited> + export function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } @@ -140,3 +197,16 @@ export function waitForHealth(url: string) { }, 1000) }) } + +export function visit(obj: any, callback: (key: string, value: any) => any): any { + if (Array.isArray(obj)) { + return obj.map((item) => visit(item, callback)) + } else if (typeof obj === 'object' && obj !== null) { + return Object.keys(obj).reduce((acc, key) => { + acc[key] = visit(callback(key, obj[key]), callback) + return acc + }, {} as any) + } else { + return obj + } +} diff --git a/substrate/frame/revive/rpc/revive_chain.metadata b/substrate/frame/revive/rpc/revive_chain.metadata index 89476924cf007..80f1000125a17 100644 Binary files a/substrate/frame/revive/rpc/revive_chain.metadata and b/substrate/frame/revive/rpc/revive_chain.metadata differ diff --git a/substrate/frame/revive/rpc/src/apis.rs b/substrate/frame/revive/rpc/src/apis.rs new file mode 100644 index 0000000000000..ae85246c55b39 --- /dev/null +++ b/substrate/frame/revive/rpc/src/apis.rs @@ -0,0 +1,24 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +mod debug_apis; +pub use debug_apis::*; + +mod execution_apis; +pub use execution_apis::*; + +mod health_api; +pub use health_api::*; diff --git a/substrate/frame/revive/rpc/src/apis/debug_apis.rs b/substrate/frame/revive/rpc/src/apis/debug_apis.rs new file mode 100644 index 0000000000000..2db05d2f75ee6 --- /dev/null +++ b/substrate/frame/revive/rpc/src/apis/debug_apis.rs @@ -0,0 +1,102 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::*; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +/// Debug Ethererum JSON-RPC apis. +#[rpc(server, client)] +pub trait DebugRpc { + /// Returns the tracing of the execution of a specific block using its number. + /// + /// ## References + /// + /// - er + #[method(name = "debug_traceBlockByNumber")] + async fn trace_block_by_number( + &self, + block: BlockNumberOrTag, + tracer_config: TracerConfig, + ) -> RpcResult>; + + /// Returns a transaction's traces by replaying it. + /// + /// ## References + /// + /// - + #[method(name = "debug_traceTransaction")] + async fn trace_transaction( + &self, + transaction_hash: H256, + tracer_config: TracerConfig, + ) -> RpcResult; + + /// Dry run a call and returns the transaction's traces. + /// + /// ## References + /// + /// - + #[method(name = "debug_traceCall")] + async fn trace_call( + &self, + transaction: GenericTransaction, + block: BlockNumberOrTag, + tracer_config: TracerConfig, + ) -> RpcResult; +} + +pub struct DebugRpcServerImpl { + client: client::Client, +} + +impl DebugRpcServerImpl { + pub fn new(client: client::Client) -> Self { + Self { client } + } +} + +#[async_trait] +impl DebugRpcServer for DebugRpcServerImpl { + async fn trace_block_by_number( + &self, + block: BlockNumberOrTag, + tracer_config: TracerConfig, + ) -> RpcResult> { + log::debug!(target: crate::LOG_TARGET, "trace_block_by_number: {block:?} config: {tracer_config:?}"); + let traces = self.client.trace_block_by_number(block, tracer_config).await?; + Ok(traces) + } + + async fn trace_transaction( + &self, + transaction_hash: H256, + tracer_config: TracerConfig, + ) -> RpcResult { + let trace = self.client.trace_transaction(transaction_hash, tracer_config).await?; + Ok(trace) + } + + async fn trace_call( + &self, + transaction: GenericTransaction, + block: BlockNumberOrTag, + tracer_config: TracerConfig, + ) -> RpcResult { + log::debug!(target: crate::LOG_TARGET, "trace_call: {transaction:?} block: {block:?} config: {tracer_config:?}"); + let trace = self.client.trace_call(transaction, block, tracer_config).await?; + Ok(trace) + } +} diff --git a/substrate/frame/revive/rpc/src/rpc_methods_gen.rs b/substrate/frame/revive/rpc/src/apis/execution_apis.rs similarity index 99% rename from substrate/frame/revive/rpc/src/rpc_methods_gen.rs rename to substrate/frame/revive/rpc/src/apis/execution_apis.rs index 2df644f5692bd..f55209fce5856 100644 --- a/substrate/frame/revive/rpc/src/rpc_methods_gen.rs +++ b/substrate/frame/revive/rpc/src/apis/execution_apis.rs @@ -18,7 +18,7 @@ //! Generated JSON-RPC methods. #![allow(missing_docs)] -use super::*; +use crate::*; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; #[rpc(server, client)] diff --git a/substrate/frame/revive/rpc/src/rpc_health.rs b/substrate/frame/revive/rpc/src/apis/health_api.rs similarity index 99% rename from substrate/frame/revive/rpc/src/rpc_health.rs rename to substrate/frame/revive/rpc/src/apis/health_api.rs index 35c5a588f284d..076d2fb4800a8 100644 --- a/substrate/frame/revive/rpc/src/rpc_health.rs +++ b/substrate/frame/revive/rpc/src/apis/health_api.rs @@ -16,7 +16,7 @@ // limitations under the License. //! Heatlh JSON-RPC methods. -use super::*; +use crate::*; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use sc_rpc_api::system::helpers::Health; diff --git a/substrate/frame/revive/rpc/src/cli.rs b/substrate/frame/revive/rpc/src/cli.rs index e40f3b1d053ce..5844d36a87fff 100644 --- a/substrate/frame/revive/rpc/src/cli.rs +++ b/substrate/frame/revive/rpc/src/cli.rs @@ -18,8 +18,8 @@ use crate::{ client::{connect, native_to_eth_ratio, Client, SubscriptionType, SubstrateBlockNumber}, BlockInfoProvider, BlockInfoProviderImpl, CacheReceiptProvider, DBReceiptProvider, - EthRpcServer, EthRpcServerImpl, ReceiptExtractor, ReceiptProvider, SystemHealthRpcServer, - SystemHealthRpcServerImpl, LOG_TARGET, + DebugRpcServer, DebugRpcServerImpl, EthRpcServer, EthRpcServerImpl, ReceiptExtractor, + ReceiptProvider, SystemHealthRpcServer, SystemHealthRpcServerImpl, LOG_TARGET, }; use clap::Parser; use futures::{pin_mut, FutureExt}; @@ -37,6 +37,8 @@ const DEFAULT_PROMETHEUS_PORT: u16 = 9616; // Default port if --rpc-port is not specified const DEFAULT_RPC_PORT: u16 = 8545; +const IN_MEMORY_DB: &str = "sqlite::memory:"; + // Parsed command instructions from the command line #[derive(Parser, Debug)] #[clap(author, about, version)] @@ -52,8 +54,8 @@ pub struct CliCommand { /// The database used to store Ethereum transaction hashes. /// This is only useful if the node needs to act as an archive node and respond to Ethereum RPC /// queries for transactions that are not in the in memory cache. - #[clap(long, env = "DATABASE_URL")] - pub database_url: Option, + #[clap(long, env = "DATABASE_URL", default_value = IN_MEMORY_DB)] + pub database_url: String, /// If not provided, only new blocks will be indexed #[clap(long)] @@ -97,7 +99,7 @@ fn build_client( tokio_handle: &tokio::runtime::Handle, cache_size: usize, node_rpc_url: &str, - database_url: Option<&str>, + database_url: &str, abort_signal: Signals, ) -> anyhow::Result { let fut = async { @@ -105,22 +107,22 @@ fn build_client( let block_provider: Arc = Arc::new(BlockInfoProviderImpl::new(cache_size, api.clone(), rpc.clone())); + let prune_old_blocks = database_url == IN_MEMORY_DB; + if prune_old_blocks { + log::info!( target: LOG_TARGET, "Using in-memory database, keeping only {cache_size} blocks in memory"); + } + let receipt_extractor = ReceiptExtractor::new(native_to_eth_ratio(&api).await?); - let receipt_provider: Arc = if let Some(database_url) = database_url { - log::info!(target: LOG_TARGET, "🔗 Connecting to provided database"); - Arc::new(( - CacheReceiptProvider::default(), - DBReceiptProvider::new( - database_url, - block_provider.clone(), - receipt_extractor.clone(), - ) - .await?, - )) - } else { - log::info!(target: LOG_TARGET, "🔌 No database provided, using in-memory cache"); - Arc::new(CacheReceiptProvider::default()) - }; + let receipt_provider: Arc = Arc::new(( + CacheReceiptProvider::default(), + DBReceiptProvider::new( + database_url, + block_provider.clone(), + receipt_extractor.clone(), + prune_old_blocks, + ) + .await?, + )); let client = Client::new(api, rpc_client, rpc, block_provider, receipt_provider, receipt_extractor) @@ -187,7 +189,7 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> { tokio_handle, cache_size, &node_rpc_url, - database_url.as_deref(), + &database_url, tokio_runtime.block_on(async { Signals::capture() })?, )?; @@ -232,10 +234,12 @@ fn rpc_module(is_dev: bool, client: Client) -> Result, sc_service: .with_accounts(if is_dev { vec![crate::Account::default()] } else { vec![] }) .into_rpc(); - let health_api = SystemHealthRpcServerImpl::new(client).into_rpc(); + let health_api = SystemHealthRpcServerImpl::new(client.clone()).into_rpc(); + let debug_api = DebugRpcServerImpl::new(client).into_rpc(); let mut module = RpcModule::new(()); module.merge(eth_api).map_err(|e| sc_service::Error::Application(e.into()))?; module.merge(health_api).map_err(|e| sc_service::Error::Application(e.into()))?; + module.merge(debug_api).map_err(|e| sc_service::Error::Application(e.into()))?; Ok(module) } diff --git a/substrate/frame/revive/rpc/src/client.rs b/substrate/frame/revive/rpc/src/client.rs index ae5311deb8e02..9431057353763 100644 --- a/substrate/frame/revive/rpc/src/client.rs +++ b/substrate/frame/revive/rpc/src/client.rs @@ -22,15 +22,17 @@ use crate::{ }, BlockInfoProvider, ReceiptExtractor, ReceiptProvider, TransactionInfo, LOG_TARGET, }; +use codec::{Decode, Encode}; use jsonrpsee::types::{error::CALL_EXECUTION_FAILED_CODE, ErrorObjectOwned}; use pallet_revive::{ evm::{ - extract_revert_message, Block, BlockNumberOrTag, BlockNumberOrTagOrHash, Filter, - GenericTransaction, Log, ReceiptInfo, SyncingProgress, SyncingStatus, TransactionSigned, - H160, H256, U256, + decode_revert_reason, Block, BlockNumberOrTag, BlockNumberOrTagOrHash, CallTrace, Filter, + GenericTransaction, Log, ReceiptInfo, SyncingProgress, SyncingStatus, TracerConfig, + TransactionSigned, TransactionTrace, H160, H256, U256, }, EthTransactError, EthTransactInfo, }; +use sp_runtime::OpaqueExtrinsic; use sp_weights::Weight; use std::{ops::ControlFlow, sync::Arc, time::Duration}; use subxt::{ @@ -108,9 +110,9 @@ pub enum ClientError { /// A [`codec::Error`] wrapper error. #[error(transparent)] CodecError(#[from] codec::Error), - /// Contract reverted + /// Transcact call failed. #[error("contract reverted")] - Reverted(EthTransactError), + TransactError(EthTransactError), /// A decimal conversion failed. #[error("conversion failed")] ConversionFailed, @@ -151,12 +153,16 @@ impl From for ErrorObjectOwned { None, ) }, - ClientError::Reverted(EthTransactError::Data(data)) => { - let msg = extract_revert_message(&data).unwrap_or_default(); + ClientError::TransactError(EthTransactError::Data(data)) => { + let msg = match decode_revert_reason(&data) { + Some(reason) => format!("execution reverted: {reason}"), + None => "execution reverted".to_string(), + }; + let data = format!("0x{}", hex::encode(data)); ErrorObjectOwned::owned::(REVERT_CODE, msg, Some(data)) }, - ClientError::Reverted(EthTransactError::Message(msg)) => + ClientError::TransactError(EthTransactError::Message(msg)) => ErrorObjectOwned::owned::(CALL_EXECUTION_FAILED_CODE, msg, None), _ => ErrorObjectOwned::owned::(CALL_EXECUTION_FAILED_CODE, err.to_string(), None), @@ -570,7 +576,7 @@ impl Client { match result { Err(err) => { log::debug!(target: LOG_TARGET, "Dry run failed {err:?}"); - Err(ClientError::Reverted(err.0)) + Err(ClientError::TransactError(err.0)) }, Ok(result) => Ok(result.0), } @@ -645,7 +651,136 @@ impl Client { let gas_price = runtime_api.call(payload).await?; Ok(*gas_price) } + /// Get the transaction traces for the given block. + pub async fn trace_block_by_number( + &self, + block: BlockNumberOrTag, + tracer_config: TracerConfig, + ) -> Result, ClientError> { + let block_hash = match block { + BlockNumberOrTag::U256(n) => { + let block_number: SubstrateBlockNumber = + n.try_into().map_err(|_| ClientError::ConversionFailed)?; + self.get_block_hash(block_number).await? + }, + BlockNumberOrTag::BlockTag(_) => self.latest_block().await.map(|b| b.hash()), + } + .ok_or(ClientError::BlockNotFound)?; + + let block = self + .rpc + .chain_get_block(Some(block_hash)) + .await? + .ok_or(ClientError::BlockNotFound)?; + + let header = block.block.header; + let parent_hash = header.parent_hash; + let exts = block + .block + .extrinsics + .into_iter() + .filter_map(|e| OpaqueExtrinsic::decode(&mut &e[..]).ok()) + .collect::>(); + + let params = ((header, exts), tracer_config).encode(); + + let bytes = self + .rpc + .state_call("ReviveApi_trace_block", Some(¶ms), Some(parent_hash)) + .await + .inspect_err(|err| { + log::error!(target: LOG_TARGET, "state_call failed with: {err:?}"); + })?; + + let traces = Vec::<(u32, CallTrace)>::decode(&mut &bytes[..])?; + + let mut hashes = self + .receipt_provider + .block_transaction_hashes(&block_hash) + .await + .ok_or(ClientError::EthExtrinsicNotFound)?; + + let traces = traces + .into_iter() + .filter_map(|(index, trace)| { + Some(TransactionTrace { tx_hash: hashes.remove(&(index as usize))?, trace }) + }) + .collect(); + + Ok(traces) + } + + /// Get the transaction traces for the given transaction. + pub async fn trace_transaction( + &self, + transaction_hash: H256, + tracer_config: TracerConfig, + ) -> Result { + let ReceiptInfo { block_hash, transaction_index, .. } = self + .receipt_provider + .receipt_by_hash(&transaction_hash) + .await + .ok_or(ClientError::EthExtrinsicNotFound)?; + + log::debug!(target: LOG_TARGET, "Found eth_tx at {block_hash:?} index: + {transaction_index:?}"); + + let block = self + .rpc + .chain_get_block(Some(block_hash)) + .await? + .ok_or(ClientError::BlockNotFound)?; + + let header = block.block.header; + let parent_hash = header.parent_hash; + let exts = block + .block + .extrinsics + .into_iter() + .filter_map(|e| OpaqueExtrinsic::decode(&mut &e[..]).ok()) + .collect::>(); + + let params = ((header, exts), transaction_index.as_u32(), tracer_config).encode(); + let bytes = self + .rpc + .state_call("ReviveApi_trace_tx", Some(¶ms), Some(parent_hash)) + .await + .inspect_err(|err| { + log::error!(target: LOG_TARGET, "state_call failed with: {err:?}"); + })?; + + let trace = Option::::decode(&mut &bytes[..])?; + trace.ok_or(ClientError::EthExtrinsicNotFound) + } + /// Get the transaction traces for the given block. + pub async fn trace_call( + &self, + transaction: GenericTransaction, + block: BlockNumberOrTag, + tracer_config: TracerConfig, + ) -> Result { + let block_hash = match block { + BlockNumberOrTag::U256(n) => { + let block_number: SubstrateBlockNumber = + n.try_into().map_err(|_| ClientError::ConversionFailed)?; + self.get_block_hash(block_number).await? + }, + BlockNumberOrTag::BlockTag(_) => self.latest_block().await.map(|b| b.hash()), + }; + + let params = (transaction, tracer_config).encode(); + let bytes = self + .rpc + .state_call("ReviveApi_trace_call", Some(¶ms), block_hash) + .await + .inspect_err(|err| { + log::error!(target: LOG_TARGET, "state_call failed with: {err:?}"); + })?; + + Result::::decode(&mut &bytes[..])? + .map_err(ClientError::TransactError) + } /// Get the EVM block for the given hash. pub async fn evm_block( &self, diff --git a/substrate/frame/revive/rpc/src/lib.rs b/substrate/frame/revive/rpc/src/lib.rs index 0a153afa6ddf2..8d6797722d4f2 100644 --- a/substrate/frame/revive/rpc/src/lib.rs +++ b/substrate/frame/revive/rpc/src/lib.rs @@ -44,11 +44,8 @@ pub use receipt_provider::*; mod receipt_extractor; pub use receipt_extractor::*; -mod rpc_health; -pub use rpc_health::*; - -mod rpc_methods_gen; -pub use rpc_methods_gen::*; +mod apis; +pub use apis::*; pub const LOG_TARGET: &str = "eth-rpc"; diff --git a/substrate/frame/revive/rpc/src/receipt_provider.rs b/substrate/frame/revive/rpc/src/receipt_provider.rs index 8f1c20005ef4a..fe8a3e9fb04fa 100644 --- a/substrate/frame/revive/rpc/src/receipt_provider.rs +++ b/substrate/frame/revive/rpc/src/receipt_provider.rs @@ -17,6 +17,7 @@ use jsonrpsee::core::async_trait; use pallet_revive::evm::{Filter, Log, ReceiptInfo, TransactionSigned, H256}; +use std::collections::HashMap; use tokio::join; mod cache; @@ -40,6 +41,9 @@ pub trait ReceiptProvider: Send + Sync { /// Deletes receipts associated with the specified block hash. async fn remove(&self, block_hash: &H256); + /// Return all transaction hashes for the given block hash. + async fn block_transaction_hashes(&self, block_hash: &H256) -> Option>; + /// Get the receipt for the given block hash and transaction index. async fn receipt_by_block_hash_and_index( &self, @@ -68,7 +72,7 @@ impl ReceiptProvider for (Cach } async fn remove(&self, block_hash: &H256) { - self.0.remove(block_hash).await; + join!(self.0.remove(block_hash), self.1.remove(block_hash)); } async fn receipt_by_block_hash_and_index( @@ -92,6 +96,13 @@ impl ReceiptProvider for (Cach self.1.receipts_count_per_block(block_hash).await } + async fn block_transaction_hashes(&self, block_hash: &H256) -> Option> { + if let Some(hashes) = self.0.block_transaction_hashes(block_hash).await { + return Some(hashes); + } + self.1.block_transaction_hashes(block_hash).await + } + async fn receipt_by_hash(&self, hash: &H256) -> Option { if let Some(receipt) = self.0.receipt_by_hash(hash).await { return Some(receipt); diff --git a/substrate/frame/revive/rpc/src/receipt_provider/cache.rs b/substrate/frame/revive/rpc/src/receipt_provider/cache.rs index 87947be7c7227..576c08c0dd03b 100644 --- a/substrate/frame/revive/rpc/src/receipt_provider/cache.rs +++ b/substrate/frame/revive/rpc/src/receipt_provider/cache.rs @@ -70,6 +70,11 @@ impl ReceiptProvider for CacheReceiptProvider { cache.transaction_hashes_by_block_and_index.get(block_hash).map(|v| v.len()) } + async fn block_transaction_hashes(&self, block_hash: &H256) -> Option> { + let cache = self.cache().await; + cache.transaction_hashes_by_block_and_index.get(block_hash).cloned() + } + async fn receipt_by_hash(&self, hash: &H256) -> Option { let cache = self.cache().await; cache.receipts_by_hash.get(hash).cloned() diff --git a/substrate/frame/revive/rpc/src/receipt_provider/db.rs b/substrate/frame/revive/rpc/src/receipt_provider/db.rs index 0f82f5df1ba7d..c471d009022ab 100644 --- a/substrate/frame/revive/rpc/src/receipt_provider/db.rs +++ b/substrate/frame/revive/rpc/src/receipt_provider/db.rs @@ -16,12 +16,15 @@ // limitations under the License. use super::*; -use crate::{Address, AddressOrAddresses, BlockInfoProvider, Bytes, FilterTopic, ReceiptExtractor}; +use crate::{ + Address, AddressOrAddresses, BlockInfoProvider, Bytes, FilterTopic, ReceiptExtractor, + LOG_TARGET, +}; use jsonrpsee::core::async_trait; use pallet_revive::evm::{Filter, Log, ReceiptInfo, TransactionSigned}; use sp_core::{H256, U256}; use sqlx::{query, QueryBuilder, Row, Sqlite, SqlitePool}; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; /// A `[ReceiptProvider]` that stores receipts in a SQLite database. #[derive(Clone)] @@ -32,6 +35,8 @@ pub struct DBReceiptProvider { block_provider: Arc, /// A means to extract receipts from extrinsics. receipt_extractor: ReceiptExtractor, + /// Whether to prune old blocks. + prune_old_blocks: bool, } impl DBReceiptProvider { @@ -40,10 +45,11 @@ impl DBReceiptProvider { database_url: &str, block_provider: Arc, receipt_extractor: ReceiptExtractor, + prune_old_blocks: bool, ) -> Result { let pool = SqlitePool::connect(database_url).await?; sqlx::migrate!().run(&pool).await?; - Ok(Self { pool, block_provider, receipt_extractor }) + Ok(Self { pool, block_provider, receipt_extractor, prune_old_blocks }) } async fn fetch_row(&self, transaction_hash: &H256) -> Option<(H256, usize)> { @@ -68,7 +74,41 @@ impl DBReceiptProvider { #[async_trait] impl ReceiptProvider for DBReceiptProvider { - async fn remove(&self, _block_hash: &H256) {} + async fn remove(&self, block_hash: &H256) { + if !self.prune_old_blocks { + return; + } + + let block_hash = block_hash.as_ref(); + + let delete_transaction_hashes = query!( + r#" + DELETE FROM transaction_hashes + WHERE block_hash = $1 + "#, + block_hash + ) + .execute(&self.pool); + + let delete_logs = query!( + r#" + DELETE FROM logs + WHERE block_hash = $1 + "#, + block_hash + ) + .execute(&self.pool); + + let (tx_result, logs_result) = tokio::join!(delete_transaction_hashes, delete_logs); + + if let Err(err) = tx_result { + log::error!(target: LOG_TARGET, "Error removing transaction hashes for block hash {block_hash:?}: {err:?}"); + } + + if let Err(err) = logs_result { + log::error!(target: LOG_TARGET, "Error removing logs for block hash {block_hash:?}: {err:?}"); + } + } async fn archive(&self, block_hash: &H256, receipts: &[(TransactionSigned, ReceiptInfo)]) { self.insert(block_hash, receipts).await; @@ -282,6 +322,28 @@ impl ReceiptProvider for DBReceiptProvider { Some(count) } + async fn block_transaction_hashes(&self, block_hash: &H256) -> Option> { + let block_hash = block_hash.as_ref(); + let rows = query!( + r#" + SELECT transaction_index, transaction_hash + FROM transaction_hashes + WHERE block_hash = $1 + "#, + block_hash + ) + .map(|row| { + let transaction_index = row.transaction_index as usize; + let transaction_hash = H256::from_slice(&row.transaction_hash); + (transaction_index, transaction_hash) + }) + .fetch_all(&self.pool) + .await + .ok()?; + + Some(rows.into_iter().collect()) + } + async fn receipt_by_block_hash_and_index( &self, block_hash: &H256, @@ -349,18 +411,53 @@ mod tests { pool, block_provider: Arc::new(MockBlockInfoProvider {}), receipt_extractor: ReceiptExtractor::new(1_000_000), + prune_old_blocks: true, } } #[sqlx::test] - async fn test_insert(pool: SqlitePool) { + async fn test_insert_remove(pool: SqlitePool) { let provider = setup_sqlite_provider(pool).await; let block_hash = H256::default(); - let receipts = vec![(TransactionSigned::default(), ReceiptInfo::default())]; + let receipts = vec![( + TransactionSigned::default(), + ReceiptInfo { + logs: vec![Log { block_hash, ..Default::default() }], + ..Default::default() + }, + )]; provider.insert(&block_hash, &receipts).await; let row = provider.fetch_row(&receipts[0].1.transaction_hash).await; assert_eq!(row, Some((block_hash, 0))); + + provider.remove(&block_hash).await; + + let transaction_count: i64 = sqlx::query_scalar( + r#" + SELECT COUNT(*) + FROM transaction_hashes + WHERE block_hash = ? + "#, + ) + .bind(block_hash.as_ref()) + .fetch_one(&provider.pool) + .await + .unwrap(); + assert_eq!(transaction_count, 0); + + let logs_count: i64 = sqlx::query_scalar( + r#" + SELECT COUNT(*) + FROM logs + WHERE block_hash = ? + "#, + ) + .bind(block_hash.as_ref()) + .fetch_one(&provider.pool) + .await + .unwrap(); + assert_eq!(logs_count, 0); } #[sqlx::test] diff --git a/substrate/frame/revive/rpc/src/tests.rs b/substrate/frame/revive/rpc/src/tests.rs index e1ac274d32ea8..375e3a5dd3f8c 100644 --- a/substrate/frame/revive/rpc/src/tests.rs +++ b/substrate/frame/revive/rpc/src/tests.rs @@ -230,7 +230,7 @@ async fn revert_call() -> anyhow::Result<()> { .unwrap_err(); let call_err = unwrap_call_err!(err.source().unwrap()); - assert_eq!(call_err.message(), "execution reverted: This is a require error"); + assert_eq!(call_err.message(), "execution reverted: revert: This is a require error"); assert_eq!(call_err.code(), 3); Ok(()) } diff --git a/substrate/frame/revive/src/evm.rs b/substrate/frame/revive/src/evm.rs index 33660a36aa6ea..f340474f472e0 100644 --- a/substrate/frame/revive/src/evm.rs +++ b/substrate/frame/revive/src/evm.rs @@ -24,46 +24,4 @@ pub use tracing::*; mod gas_encoder; pub use gas_encoder::*; pub mod runtime; - -use crate::alloc::{format, string::*}; - -/// Extract the revert message from a revert("msg") solidity statement. -pub fn extract_revert_message(exec_data: &[u8]) -> Option { - let error_selector = exec_data.get(0..4)?; - - match error_selector { - // assert(false) - [0x4E, 0x48, 0x7B, 0x71] => { - let panic_code: u32 = U256::from_big_endian(exec_data.get(4..36)?).try_into().ok()?; - - // See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require - let msg = match panic_code { - 0x00 => "generic panic", - 0x01 => "assert(false)", - 0x11 => "arithmetic underflow or overflow", - 0x12 => "division or modulo by zero", - 0x21 => "enum overflow", - 0x22 => "invalid encoded storage byte array accessed", - 0x31 => "out-of-bounds array access; popping on an empty array", - 0x32 => "out-of-bounds access of an array or bytesN", - 0x41 => "out of memory", - 0x51 => "uninitialized function", - code => return Some(format!("execution reverted: unknown panic code: {code:#x}")), - }; - - Some(format!("execution reverted: {msg}")) - }, - // revert(string) - [0x08, 0xC3, 0x79, 0xA0] => { - let decoded = ethabi::decode(&[ethabi::ParamKind::String], &exec_data[4..]).ok()?; - if let Some(ethabi::Token::String(msg)) = decoded.first() { - return Some(format!("execution reverted: {}", String::from_utf8_lossy(msg))) - } - Some("execution reverted".to_string()) - }, - _ => { - log::debug!(target: crate::LOG_TARGET, "Unknown revert function selector: {error_selector:?}"); - Some("execution reverted".to_string()) - }, - } -} +pub use alloy_core::sol_types::decode_revert_reason; diff --git a/substrate/frame/revive/src/evm/api/debug_rpc_types.rs b/substrate/frame/revive/src/evm/api/debug_rpc_types.rs index 0857a59fbf3b6..e9518f6b6400b 100644 --- a/substrate/frame/revive/src/evm/api/debug_rpc_types.rs +++ b/substrate/frame/revive/src/evm/api/debug_rpc_types.rs @@ -156,29 +156,23 @@ pub enum CallType { pub struct CallTrace { /// Address of the sender. pub from: H160, - /// Address of the receiver. - pub to: H160, - /// Call input data. - pub input: Vec, - /// Amount of value transferred. - #[serde(skip_serializing_if = "U256::is_zero")] - pub value: U256, - /// Type of call. - #[serde(rename = "type")] - pub call_type: CallType, /// Amount of gas provided for the call. pub gas: Gas, /// Amount of gas used. #[serde(rename = "gasUsed")] pub gas_used: Gas, + /// Address of the receiver. + pub to: H160, + /// Call input data. + pub input: Bytes, /// Return data. - #[serde(flatten, skip_serializing_if = "Bytes::is_empty")] + #[serde(skip_serializing_if = "Bytes::is_empty")] pub output: Bytes, /// The error message if the call failed. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, /// The revert reason, if the call reverted. - #[serde(rename = "revertReason")] + #[serde(rename = "revertReason", skip_serializing_if = "Option::is_none")] pub revert_reason: Option, /// List of sub-calls. #[serde(skip_serializing_if = "Vec::is_empty")] @@ -186,6 +180,11 @@ pub struct CallTrace { /// List of logs emitted during the call. #[serde(skip_serializing_if = "Vec::is_empty")] pub logs: Vec, + /// Amount of value transferred. + pub value: U256, + /// Type of call. + #[serde(rename = "type")] + pub call_type: CallType, } /// A log emitted during a call. @@ -195,12 +194,11 @@ pub struct CallTrace { pub struct CallLog { /// The address of the contract that emitted the log. pub address: H160, - /// The log's data. - #[serde(skip_serializing_if = "Bytes::is_empty")] - pub data: Bytes, /// The topics used to index the log. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub topics: Vec, + /// The log's data. + pub data: Bytes, /// Position of the log relative to subcalls within the same trace /// See for details #[serde(with = "super::hex_serde")] diff --git a/substrate/frame/revive/src/evm/tracing.rs b/substrate/frame/revive/src/evm/tracing.rs index 7466ec1de4877..7eae64db79fb1 100644 --- a/substrate/frame/revive/src/evm/tracing.rs +++ b/substrate/frame/revive/src/evm/tracing.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::{ - evm::{extract_revert_message, CallLog, CallTrace, CallType}, + evm::{decode_revert_reason, CallLog, CallTrace, CallType}, primitives::ExecReturnValue, tracing::Tracer, DispatchError, Weight, @@ -72,7 +72,7 @@ impl Gas> Tracer for CallTracer Gas> Tracer for CallTracer U256 { + let fee = T::WeightPrice::convert(weight); + Self::evm_fee_to_gas(fee) + } + /// Get the block gas limit. pub fn evm_block_gas_limit() -> U256 { let max_block_weight = T::BlockWeights::get() @@ -1334,8 +1340,7 @@ where .max_total .unwrap_or_else(|| T::BlockWeights::get().max_block); - let fee = T::WeightPrice::convert(max_block_weight); - Self::evm_fee_to_gas(fee) + Self::evm_gas_from_weight(max_block_weight) } /// Get the gas price. @@ -1508,5 +1513,35 @@ sp_api::decl_runtime_apis! { address: H160, key: [u8; 32], ) -> GetStorageResult; + + + /// Traces the execution of an entire block and returns call traces. + /// + /// This is intended to be called through `state_call` to replay the block from the + /// parent block. + /// + /// See eth-rpc `debug_traceBlockByNumber` for usage. + fn trace_block( + block: Block, + config: TracerConfig + ) -> Vec<(u32, CallTrace)>; + + /// Traces the execution of a specific transaction within a block. + /// + /// This is intended to be called through `state_call` to replay the block from the + /// parent hash up to the transaction. + /// + /// See eth-rpc `debug_traceTransaction` for usage. + fn trace_tx( + block: Block, + tx_index: u32, + config: TracerConfig + ) -> Option; + + /// Dry run and return the trace of the given call. + /// + /// See eth-rpc `debug_traceCall` for usage. + fn trace_call(tx: GenericTransaction, config: TracerConfig) -> Result; + } } diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index 6a74660702fa8..39ac6fcaf3422 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -4431,8 +4431,10 @@ fn tracing_works_for_transfers() { trace(&mut tracer, || { builder::bare_call(BOB_ADDR).value(10_000_000).build_and_unwrap_result(); }); + + let traces = tracer.collect_traces(); assert_eq!( - tracer.collect_traces(), + traces, vec![CallTrace { from: ALICE_ADDR, to: BOB_ADDR, @@ -4501,20 +4503,18 @@ fn tracing_works() { vec![CallTrace { from: ALICE_ADDR, to: addr, - input: (3u32, addr_callee).encode(), + input: (3u32, addr_callee).encode().into(), call_type: Call, logs: logs.clone(), calls: vec![ CallTrace { from: addr, to: addr_callee, - input: 2u32.encode(), + input: 2u32.encode().into(), output: hex_literal::hex!( "08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000" ).to_vec().into(), - revert_reason: Some( - "execution reverted: This function always fails".to_string() - ), + revert_reason: Some("revert: This function always fails".to_string()), error: Some("execution reverted".to_string()), call_type: Call, ..Default::default() @@ -4522,14 +4522,14 @@ fn tracing_works() { CallTrace { from: addr, to: addr, - input: (2u32, addr_callee).encode(), + input: (2u32, addr_callee).encode().into(), call_type: Call, logs: logs.clone(), calls: vec![ CallTrace { from: addr, to: addr_callee, - input: 1u32.encode(), + input: 1u32.encode().into(), output: Default::default(), error: Some("ContractTrapped".to_string()), call_type: Call, @@ -4538,14 +4538,14 @@ fn tracing_works() { CallTrace { from: addr, to: addr, - input: (1u32, addr_callee).encode(), + input: (1u32, addr_callee).encode().into(), call_type: Call, logs: logs.clone(), calls: vec![ CallTrace { from: addr, to: addr_callee, - input: 0u32.encode(), + input: 0u32.encode().into(), output: 0u32.to_le_bytes().to_vec().into(), call_type: Call, ..Default::default() @@ -4553,7 +4553,7 @@ fn tracing_works() { CallTrace { from: addr, to: addr, - input: (0u32, addr_callee).encode(), + input: (0u32, addr_callee).encode().into(), call_type: Call, calls: vec![ CallTrace {