Skip to main content

How to debug Stylus transactions using Cargo Stylus Replay

ALPHA RELEASE, PUBLIC PREVIEW DOCS

Stylus is currently tagged as a release-candidate. The code has been audited, and testing in production environments is underway. Caution should be taken when used in production scenarios. This documentation is currently in public preview.

To provide feedback, click the Request an update button at the top of this document, join the Arbitrum Discord, or reach out to our team directly by completing this form.

Debugging smart contracts can be challenging, especially when dealing with complex transactions. The cargo-stylus-replay 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-replay crate, which provides essential functionalities for developers:

  1. 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.
  2. Debugging with GDB: Replay and debug the execution of a Stylus transaction using the GNU Debugger (GDB). 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

Note: Currently, this has only been tested on Linux, x86 systems with GDB installed.

Requirements

  • Rust (version 1.77 or higher)
  • Crates: cargo-stylus, cargo-stylus-replay, cargo-stylus-check
  • GNU Debugger (GDB)
  • Cast (an Ethereum CLI tool)
  • Local Arbitrum Sepolia node 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 against the Rust source code.

Installation and setup

  1. Install the required crates and GDB: First, let's ensure that the following crates are installed:
cargo install cargo-stylus cargo-stylus-replay cargo-stylus-check

Install GDB if it's not already installed:

sudo apt-get install gdb
  1. 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, within src/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!
  1. 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()"
  1. Replay the transaction with GDB: Now, we can replay the transaction with cargo stylus and GDB 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

GDB will load and set a breakpoint automatically at the user_entrypoint internal Stylus function.

[Detaching after vfork from child process 370003]

Thread 1 "cargo-stylus-re" hit Breakpoint 1, stylus_hello_world::user_entrypoint (len=4) at src/lib.rs:38
38 #[entrypoint]
(gdb)
  1. Debugging with GDB: 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-re" 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>

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

Run the following command to obtain a trace output:

cargo stylus trace --tx=$TX_HASH --endpoint=$RPC_URL

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}]