How to debug Stylus transactions using Cargo Stylus Replay
Debugging smart contracts can be challenging, especially when dealing with complex transactions. The cargo-stylus
crate simplifies the debugging process by allowing developers to replay Stylus transactions. This tool leverages GDB to provide an interactive debugging experience, enabling developers to set breakpoints, inspect state changes, and trace the execution flow step-by-step. This capability is crucial for identifying and resolving issues, ensuring that smart contracts function correctly and efficiently.
Overview
Cargo Stylus is a tool designed to simplify the development and debugging process for smart contracts written in Rust for the Stylus execution environment. One of its powerful features is the cargo stylus
subcommand, which provides essential functionalities for developers:
- Trace transactions: Perform trace calls against Stylus transactions using Ethereum nodes'
debug_traceTransaction
RPC. This feature enables developers to analyze the execution flow and state changes of their transactions in a detailed manner. - Debugging with GDB or LLDB: Replay and debug the execution of a Stylus transaction using a debugger. This allows developers to set breakpoints, inspect variables, and step through the transaction execution line by line, providing an in-depth understanding of the transaction's behavior.
Replaying transactions
Requirements
- Rust (version 1.77 or higher)
- Crate:
cargo-stylus
- GNU Debugger (GDB) (Linux) or LLDB (MacOS)
- Cast (an Ethereum CLI tool)
- Arbitrum RPC Provider with tracing endpoints enabled or a local Stylus dev node
cargo stylus replay
allows users to debug the execution of a Stylus transaction using GDB or LLDB against the Rust source code.
Installation and setup
- Install the required crates and debugger: First, let's ensure that the following crates are installed:
cargo install cargo-stylus
If on Linux, install GDB if it's not already installed:
sudo apt-get install gdb
If on MacOS, install LLDB if it's not already installed:
xcode-select --install
- Deploy your Stylus contract: For this guide, we demonstrate how to debug the execution of the
increment()
method in the stylus-hello-world smart contract. In Rust, it looks something like this, withinsrc/lib.rs
:
#[external]
impl Counter {
...
/// Increments number and updates its value in storage.
pub fn increment(&mut self) {
let number = self.number.get();
self.set_number(number + U256::from(1));
}
...
}
Set your RPC endpoint to a node with tracing enabled and your private key:
export RPC_URL=...
export PRIV_KEY=...
and deploy your contract:
cargo stylus deploy --private-key=$PRIV_KEY --endpoint=$RPC_URL
You should see an output similar to:
contract size: 4.0 KB
wasm size: 12.1 KB
contract size: 4.0 KB
deployed code at address: 0x2c8d8a1229252b07e73b35774ad91c0b973ecf71
wasm already activated!
- Send a transaction: First, set the address of the deployed contract as an environment variable:
export ADDR=0x2c8d8a1229252b07e73b35774ad91c0b973ecf71
And send a transaction using Cast
:
cast send --rpc-url=$RPC_URL --private-key=$PRIV_KEY $ADDR "increment()"
- Replay the transaction with the debugger: Now, we can replay the transaction with cargo stylus and the debugger to inspect each step of it against our source code. Make sure GDB is installed and that you are on a Linux, x86 system. Also, you should set the transaction hash as an environment variable:
export TX_HASH=0x18b241841fa0a59e02d3c6d693750ff0080ad792204aac7e5d4ce9e20c466835
And replay the transaction:
cargo stylus replay --tx=$TX_HASH --endpoint=$RPC_URL --use-native-tracer
Options:
--tx: Specifies the transaction hash to replay.
--endpoint: Specifies the RPC endpoint for fetching transaction data.
--use-native-tracer: Uses the native Stylus tracer instead of the default JS tracer. The native tracer has broader support from RPC providers.
Note: The --use-native-tracer
flag uses stylusTracer
instead of jsTracer
, which is required for tracing Stylus transactions on most RPC providers. See more details below.
The debugger will load and set a breakpoint automatically at the user_entrypoint
internal Stylus function. While the examples below showcase GDB commands, you can find the LLDB equivalents here.
[Detaching after vfork from child process 370003]
Thread 1 "cargo-stylus" hit Breakpoint 1, stylus_hello_world::user_entrypoint (len=4) at src/lib.rs:38
38 #[entrypoint]
(gdb)
- Debugging: Now, set a breakpoint at the
increment()
method:
(gdb) b stylus_hello_world::Counter::increment
Breakpoint 2 at 0x7ffff7e4ee33: file src/lib.rs, line 69.
Then, type c
to continue the execution and you will reach that line where increment
is called:
(gdb) c
Once you reach the increment
method, inspect the state:
Thread 1 "cargo-stylus" hit Breakpoint 2, stylus_hello_world::Counter::increment (self=0x7fffffff9ae8) at src/lib.rs:69
69 let number = self.number.get();
(gdb) p number
Trace a transaction
For traditional tracing, cargo stylus
supports calls to debug_traceTransaction
. To trace a transaction, you can use the following command:
cargo stylus trace [OPTIONS] --tx <TX> --use-native-tracer
Options:
-e, --endpoint <ENDPOINT> RPC endpoint [default: http://localhost:8547]
-t, --tx <TX> Tx to replay
-p, --project <PROJECT> Project path [default: .]
-h, --help Print help
-V, --version Print version
--use-native-tracer Uses the native Stylus tracer instead of the default JS tracer. The native tracer has broader support from RPC providers.
Run the following command to obtain a trace output:
cargo stylus trace --tx=$TX_HASH --endpoint=$RPC_URL --use-native-tracer
This will produce a trace of the functions called and ink left along each method:
[{"args":[0,0,0,4],"endInk":846200000,"name":"user_entrypoint","outs":[],"startInk":846200000},{"args":[],"endInk":846167558,"name":"msg_reentrant","outs":[0,0,0,0],"startInk":846175958},{"args":[],"endInk":846047922,"name":"read_args","outs":[208,157,224,138],"startInk":846061362},{"args":[],"endInk":845914924,"name":"msg_value","outs":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"startInk":845928364},{"args":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"endInk":227196069,"name":"storage_load_bytes32","outs":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"startInk":844944549},{"args":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"endInk":226716083,"name":"storage_cache_bytes32","outs":[],"startInk":226734563},{"args":[0],"endInk":226418732,"name":"storage_flush_cache","outs":[],"startInk":226486805},{"args":[],"endInk":226362319,"name":"write_result","outs":[],"startInk":226403481},{"args":[],"endInk":846200000,"name":"user_returned","outs":[0,0,0,0],"startInk":846200000}]
RPC endpoint compatibility
Both cargo stylus trace
and cargo stylus replay
require an RPC endpoint that supports debug_traceTransaction
.
By default, the jsTracer
type is used, which is not supported by most RPC providers. If the --use-native-tracer
flag is used, the stylusTracer
type is used, which is supported by many RPC providers.
Both jsTracer
and stylusTracer
are available on local nodes, but stylusTracer
is more efficient.
See this list of RPC providers for tracing support.