Skip to content

Commit

Permalink
feat: impl debug_traceTransaction (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
grw-ms authored Oct 11, 2023
1 parent 9012529 commit f806bb9
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 67 deletions.
39 changes: 38 additions & 1 deletion SUPPORTED_APIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The `status` options are:
| [`DEBUG`](#debug-namespace) | [`debug_traceCall`](#debug_tracecall) | `SUPPORTED` | Performs a call and returns structured traces of the execution |
| `DEBUG` | `debug_traceBlockByHash` | `NOT IMPLEMENTED`<br />[GitHub Issue #63](https://github.com/matter-labs/era-test-node/issues/63) | Returns structured traces for operations within the block of the specified block hash |
| `DEBUG` | `debug_traceBlockByNumber` | `NOT IMPLEMENTED`<br />[GitHub Issue #64](https://github.com/matter-labs/era-test-node/issues/64) | Returns structured traces for operations within the block of the specified block number |
| `DEBUG` | `debug_traceTransaction` | `NOT IMPLEMENTED`<br />[GitHub Issue #65](https://github.com/matter-labs/era-test-node/issues/65) | Returns a structured trace of the execution of the specified transaction |
| [`DEBUG`](#debug-namespace) | [`debug_traceTransaction`](#debug_tracetransaction) | `SUPPORTED` | Returns a structured trace of the execution of the specified transaction |
| `ETH` | `eth_accounts` | `SUPPORTED` | Returns a list of addresses owned by client |
| [`ETH`](#eth-namespace) | [`eth_chainId`](#eth_chainid) | `SUPPORTED` | Returns the currently configured chain id <br />_(default is `260`)_ |
| `ETH` | `eth_coinbase` | `NOT IMPLEMENTED` | Returns the client coinbase address |
Expand Down Expand Up @@ -337,6 +337,43 @@ curl --request POST \
}'
```

### `debug_traceTransaction`

[source](src/debug.rs)

Returns call traces for the transaction with given hash.

Currently only transactions executed on the dev node itself (ie, not from upstream when using fork mode) can be traced.

The third argument mirrors the [`TraceConfig` of go-ethereum](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#traceconfig), but with the restriction that the only supported tracer is `CallTracer`. Memory, Stack and Storage traces are not supported.

#### Arguments

- `tx_hash: H256`

- `options: TracerConfig` (optional)

#### Status

`SUPPORTED`

#### Example

```bash
curl --request POST \
--url http://localhost:8011/ \
--header 'content-type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": "2",
"method": "debug_traceTransaction",
"params": [
"0xd3a94ff697a573cb174ecce05126e952ecea6dee051526a3e389747ff86b0d99",
{ "tracer": "callTracer", "tracerConfig": { "onlyTopCall": true } }
]
}'
```

## `NETWORK NAMESPACE`

### `net_version`
Expand Down
46 changes: 45 additions & 1 deletion e2e-tests/test/debug-apis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import * as hre from "hardhat";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import { RichAccounts } from "../helpers/constants";
import { deployContract, expectThrowsAsync, getTestProvider } from "../helpers/utils";
import { BigNumber } from "ethers";

const provider = getTestProvider();

describe("debug namespace", function () {
describe("debug_traceCall", function () {
it("Should return error if block is not 'latest' or unspecified", async function () {
expectThrowsAsync(async () => {
await provider.send("debug_traceCall", [{ to: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, "earliest"]);
Expand Down Expand Up @@ -73,3 +74,46 @@ describe("debug namespace", function () {
expect(output_number.toNumber()).to.equal(12);
});
});

describe("debug_traceTransaction", function () {
it("Should return null if txn hash is unknown", async function () {
const result = await provider.send("debug_traceTransaction", [
"0xd3a94ff697a573cb174ecce05126e952ecea6dee051526a3e389747ff86b0d99",
]);
expect(result).to.equal(null);
});

it("Should trace prior transactions", async function () {
const wallet = new Wallet(RichAccounts[0].PrivateKey);
const deployer = new Deployer(hre, wallet);

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);

const txReceipt = await greeter.setGreeting("Luke Skywalker");
const trace = await provider.send("debug_traceTransaction", [txReceipt.hash]);

// call should be successful
expect(trace.error).to.equal(null);
expect(trace.calls.length).to.equal(1);

// gas limit should match
expect(BigNumber.from(trace.gas).toNumber()).to.equal(txReceipt.gasLimit.toNumber());
});

it("Should respect only_top_calls option", async function () {
const wallet = new Wallet(RichAccounts[0].PrivateKey);
const deployer = new Deployer(hre, wallet);

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);

const txReceipt = await greeter.setGreeting("Luke Skywalker");
const trace = await provider.send("debug_traceTransaction", [
txReceipt.hash,
{ tracer: "callTracer", tracerConfig: { onlyTopCall: true } },
]);

// call should be successful
expect(trace.error).to.equal(null);
expect(trace.calls.length).to.equal(0);
});
});
157 changes: 102 additions & 55 deletions src/debug.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
use crate::{
fork::ForkSource,
node::{InMemoryNodeInner, MAX_TX_SIZE},
utils::not_implemented,
utils::{create_debug_output, not_implemented},
};
use jsonrpc_core::{BoxFuture, Result};
use once_cell::sync::OnceCell;
use std::sync::{Arc, RwLock};
use vm::{
constants::ETH_CALL_GAS_LIMIT, CallTracer, ExecutionResult, HistoryDisabled, TxExecutionMode,
Vm,
};
use vm::{constants::ETH_CALL_GAS_LIMIT, CallTracer, HistoryDisabled, TxExecutionMode, Vm};
use zksync_basic_types::H256;
use zksync_core::api_server::web3::backend_jsonrpc::{
error::into_jsrpc_error, namespaces::debug::DebugNamespaceT,
};
use zksync_state::StorageView;
use zksync_types::{
api::{BlockId, BlockNumber, DebugCall, DebugCallType, ResultDebugCall, TracerConfig},
api::{BlockId, BlockNumber, DebugCall, ResultDebugCall, TracerConfig},
l2::L2Tx,
transaction_request::CallRequest,
PackedEthSignature, Transaction, CONTRACT_DEPLOYER_ADDRESS,
PackedEthSignature, Transaction,
};
use zksync_web3_decl::error::Web3Error;

Expand Down Expand Up @@ -122,71 +119,46 @@ impl<S: Send + Sync + 'static + ForkSource + std::fmt::Debug> DebugNamespaceT
.unwrap_or_default()
};

let calltype = if l2_tx.recipient_account() == CONTRACT_DEPLOYER_ADDRESS {
DebugCallType::Create
} else {
DebugCallType::Call
};
let debug =
create_debug_output(&l2_tx, &tx_result, call_traces).map_err(into_jsrpc_error)?;

let result = match &tx_result.result {
ExecutionResult::Success { output } => DebugCall {
gas_used: tx_result.statistics.gas_used.into(),
output: output.clone().into(),
r#type: calltype,
from: l2_tx.initiator_account(),
to: l2_tx.recipient_account(),
gas: l2_tx.common_data.fee.gas_limit,
value: l2_tx.execute.value,
input: l2_tx.execute.calldata().into(),
error: None,
revert_reason: None,
calls: call_traces.into_iter().map(Into::into).collect(),
},
ExecutionResult::Revert { output } => DebugCall {
gas_used: tx_result.statistics.gas_used.into(),
output: Default::default(),
r#type: calltype,
from: l2_tx.initiator_account(),
to: l2_tx.recipient_account(),
gas: l2_tx.common_data.fee.gas_limit,
value: l2_tx.execute.value,
input: l2_tx.execute.calldata().into(),
error: None,
revert_reason: Some(output.to_string()),
calls: call_traces.into_iter().map(Into::into).collect(),
},
ExecutionResult::Halt { reason } => {
return Err(into_jsrpc_error(Web3Error::SubmitTransactionError(
reason.to_string(),
vec![],
)))
}
};

Ok(result)
Ok(debug)
})
}

fn trace_transaction(
&self,
_tx_hash: H256,
_options: Option<TracerConfig>,
tx_hash: H256,
options: Option<TracerConfig>,
) -> BoxFuture<Result<Option<DebugCall>>> {
not_implemented("debug_traceTransaction")
let only_top = options.is_some_and(|o| o.tracer_config.only_top_call);
let inner = Arc::clone(&self.node);
Box::pin(async move {
let inner = inner
.read()
.map_err(|_| into_jsrpc_error(Web3Error::InternalError))?;

Ok(inner
.tx_results
.get(&tx_hash)
.map(|tx| tx.debug_info(only_top)))
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{
deps::system_contracts::bytecode_from_slice, http_fork_source::HttpForkSource,
node::InMemoryNode, testing,
deps::system_contracts::bytecode_from_slice,
http_fork_source::HttpForkSource,
node::{InMemoryNode, TransactionResult},
testing::{self, LogBuilder},
};
use ethers::abi::{short_signature, AbiEncode, HumanReadableParser, ParamType, Token};
use zksync_basic_types::{Address, Nonce, U256};
use zksync_basic_types::{Address, Nonce, H160, U256};
use zksync_types::{
api::{CallTracerConfig, SupportedTracers},
api::{CallTracerConfig, SupportedTracers, TransactionReceipt},
transaction_request::CallRequestBuilder,
utils::deployed_address_create,
};
Expand Down Expand Up @@ -354,4 +326,79 @@ mod tests {
// the contract subcall should have reverted
assert!(contract_call.revert_reason.is_some());
}

#[tokio::test]
async fn test_trace_transaction() {
let node = InMemoryNode::<HttpForkSource>::default();
let inner = node.get_inner();
{
let mut writer = inner.write().unwrap();
writer.tx_results.insert(
H256::repeat_byte(0x1),
TransactionResult {
info: testing::default_tx_execution_info(),
receipt: TransactionReceipt {
logs: vec![LogBuilder::new()
.set_address(H160::repeat_byte(0xa1))
.build()],
..Default::default()
},
debug: testing::default_tx_debug_info(),
},
);
}
let result = DebugNamespaceImpl::new(inner)
.trace_transaction(H256::repeat_byte(0x1), None)
.await
.unwrap()
.unwrap();
assert_eq!(result.calls.len(), 1);
}

#[tokio::test]
async fn test_trace_transaction_only_top() {
let node = InMemoryNode::<HttpForkSource>::default();
let inner = node.get_inner();
{
let mut writer = inner.write().unwrap();
writer.tx_results.insert(
H256::repeat_byte(0x1),
TransactionResult {
info: testing::default_tx_execution_info(),
receipt: TransactionReceipt {
logs: vec![LogBuilder::new()
.set_address(H160::repeat_byte(0xa1))
.build()],
..Default::default()
},
debug: testing::default_tx_debug_info(),
},
);
}
let result = DebugNamespaceImpl::new(inner)
.trace_transaction(
H256::repeat_byte(0x1),
Some(TracerConfig {
tracer: SupportedTracers::CallTracer,
tracer_config: CallTracerConfig {
only_top_call: true,
},
}),
)
.await
.unwrap()
.unwrap();
assert!(result.calls.is_empty());
}

#[tokio::test]
async fn test_trace_transaction_not_found() {
let node = InMemoryNode::<HttpForkSource>::default();
let inner = node.get_inner();
let result = DebugNamespaceImpl::new(inner)
.trace_transaction(H256::repeat_byte(0x1), None)
.await
.unwrap();
assert!(result.is_none());
}
}
Loading

0 comments on commit f806bb9

Please sign in to comment.