Documentation Index
Fetch the complete documentation index at: https://mintlify.com/risc0/risc0/llms.txt
Use this file to discover all available pages before exploring further.
Debugging guest programs requires specialized tools since the code runs in a RISC-V virtual machine. This guide covers debugging techniques, tools, and best practices for RISC Zero zkVM development.
Logging and Output
Basic Logging
The simplest debugging approach is using env::log():
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
env::log("Starting guest execution");
let value: u64 = env::read();
env::log(&format!("Received value: {}", value));
let result = value * 2;
env::log(&format!("Computed result: {}", result));
env::commit(&result);
env::log("Execution complete");
}
Log messages are printed to the console during execution and don’t affect the proof.
Structured Logging
For more complex debugging, use structured logging:
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let inputs: Vec<u64> = env::read();
env::log(&format!("Input count: {}", inputs.len()));
for (i, value) in inputs.iter().enumerate() {
env::log(&format!(" [{}] = {}", i, value));
}
let sum: u64 = inputs.iter().sum();
env::log(&format!("Sum: {}", sum));
env::commit(&sum);
}
Logs are printed during execution but don’t affect the receipt or proof. They’re purely for development and debugging.
Using GDB
GDB (GNU Debugger) provides full debugging capabilities including breakpoints, backtraces, and variable inspection.
Installation
Install the RISC-V version of GDB:
Requires rzup version >= 0.5.0. Update if needed: curl -L https://risczero.com/install | bash
Enabling Debug Symbols
Configure your guest’s Cargo.toml to include debug symbols:
[profile.dev]
debug = true
[profile.release]
debug = true # Enable for release builds too
Or use a custom profile:
[profile.debug-guest]
inherits = "release"
debug = true
opt-level = 2
Starting a Debug Session
Run guest with debugger flag
r0vm --elf target/riscv32im-risc0-zkvm-elf/debug/guest --with-debugger
The output will show a command like:
riscv32im-gdb -ex "target remote 127.0.0.1:35051" /tmp/.tmpABC123.elf
Run the GDB command in another terminal
Paste and run the command from step 2.
Set breakpoints and debug
Once GDB is attached, set breakpoints and continue execution.
GDB Usage Example
# Start GDB (from the command output)
$ riscv32im-gdb -ex "target remote 127.0.0.1:35051" /tmp/.tmpULKkyS.elf
Reading symbols from /tmp/.tmpULKkyS.elf...
Remote debugging using 127.0.0.1:35051
0xc0000000 in ?? ()
# Set a breakpoint at main
(gdb) break main
Breakpoint 1 at 0x200d38: file src/main.rs, line 10.
# Continue execution
(gdb) continue
Continuing.
Breakpoint 1, main () at src/main.rs:10
10 let a: u64 = env::read();
# Print variables
(gdb) print a
$1 = 0
# Step to next line
(gdb) next
11 let b: u64 = env::read();
# Print updated variable
(gdb) print a
$2 = 17
# Show backtrace
(gdb) backtrace
#0 main () at src/main.rs:11
#1 0x200ff4 in risc0_zkvm::guest::entry
#2 0x201000 in _start
# Continue to next breakpoint or end
(gdb) continue
Common GDB Commands
| Command | Description |
|---|
break <location> | Set breakpoint at function or line |
continue | Continue execution |
next | Step over (execute next line) |
step | Step into (enter function calls) |
print <var> | Print variable value |
backtrace | Show call stack |
info locals | Show local variables |
list | Show source code |
quit | Exit GDB |
Cycle Counting
Measure performance by counting execution cycles:
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let start = env::cycle_count();
// Operation to measure
let result = expensive_function();
let end = env::cycle_count();
let elapsed = end - start;
env::log(&format!("Function took {} cycles", elapsed));
env::commit(&result);
}
fn expensive_function() -> u64 {
// Simulated expensive operation
(0..1000).sum()
}
Cycle counts are provided by the host and are not checked by the zkVM circuit. Use them only for development and optimization, not for security-critical logic.
Comparing Implementations
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let data: Vec<u64> = env::read();
// Method 1: Using iterator
let start1 = env::cycle_count();
let sum1: u64 = data.iter().sum();
let cycles1 = env::cycle_count() - start1;
// Method 2: Using loop
let start2 = env::cycle_count();
let mut sum2 = 0u64;
for &x in &data {
sum2 += x;
}
let cycles2 = env::cycle_count() - start2;
env::log(&format!("Iterator: {} cycles", cycles1));
env::log(&format!("Loop: {} cycles", cycles2));
env::commit(&sum1);
}
Profiling
Generate detailed performance profiles:
Enabling the Profiler
// Host code
use risc0_zkvm::ExecutorEnv;
let env = ExecutorEnv::builder()
.enable_profiler("/tmp/profile.pb")
.build()
.unwrap();
Or set an environment variable:
export RISC0_PPROF_OUT=/tmp/profile.pb
cargo run
Analyzing Profiles
The profiler generates data in pprof format. Analyze it with:
# Install pprof tools
go install github.com/google/pprof@latest
# Generate a report
pprof -http=:8080 /tmp/profile.pb
This opens an interactive web interface showing:
- Call graphs
- Flame graphs
- Function-level cycle counts
- Hotspot identification
Debug Builds vs Release Builds
Debug Builds
Use debug builds during development:
export RISC0_BUILD_DEBUG=1
cargo build
Benefits:
- Full debug symbols
- Better error messages
- Easier to debug
Drawbacks:
- Much larger binaries
- 10-100x slower execution
- Not suitable for production
Release Builds
Use release builds for production and performance testing:
unset RISC0_BUILD_DEBUG
cargo build --release
Benefits:
- Optimized code
- Smaller binaries
- Faster execution
Drawbacks:
- Limited debug info (unless configured)
- Harder to debug
Hybrid Approach
Get the best of both worlds:
[profile.release]
opt-level = 3
debug = true # Keep debug symbols
Debugging Panics
Understanding Panic Output
When a guest panics:
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let a: u64 = env::read();
let b: u64 = env::read();
if a == 1 || b == 1 {
panic!("Trivial factors: a={}, b={}", a, b);
}
let result = a * b;
env::commit(&result);
}
The host will show:
Error: Guest panicked: Trivial factors: a=1, b=23
Custom Error Types
For better error handling:
use risc0_zkvm::guest::env;
#[derive(Debug)]
enum ComputeError {
InvalidInput(String),
Overflow,
DivisionByZero,
}
risc0_zkvm::guest::entry!(main);
fn main() {
match compute() {
Ok(result) => env::commit(&result),
Err(e) => panic!("Computation failed: {:?}", e),
}
}
fn compute() -> Result<u64, ComputeError> {
let a: u64 = env::read();
let b: u64 = env::read();
if a == 0 || b == 0 {
return Err(ComputeError::InvalidInput(
"Inputs must be non-zero".to_string()
));
}
a.checked_mul(b)
.ok_or(ComputeError::Overflow)
}
Testing Guest Code
Unit Testing
Test guest logic outside the zkVM:
// Guest code in lib.rs
pub fn compute_factors(n: u64) -> Option<(u64, u64)> {
if n < 2 {
return None;
}
for i in 2..=(n as f64).sqrt() as u64 {
if n % i == 0 {
return Some((i, n / i));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_factors() {
assert_eq!(compute_factors(15), Some((3, 5)));
assert_eq!(compute_factors(17), None);
assert_eq!(compute_factors(1), None);
}
}
Integration Testing
Test the full host-guest interaction:
// tests/integration_test.rs
use risc0_zkvm::{ExecutorEnv, default_prover};
use my_methods::{COMPUTE_ELF, COMPUTE_ID};
#[test]
fn test_guest_execution() {
let env = ExecutorEnv::builder()
.write(&17u64).unwrap()
.write(&23u64).unwrap()
.build()
.unwrap();
let prover = default_prover();
let receipt = prover.prove(env, COMPUTE_ELF).unwrap().receipt;
let result: u64 = receipt.journal.decode().unwrap();
assert_eq!(result, 391);
receipt.verify(COMPUTE_ID).unwrap();
}
#[test]
#[should_panic(expected = "Trivial factors")]
fn test_guest_panic() {
let env = ExecutorEnv::builder()
.write(&1u64).unwrap() // Invalid input
.write(&23u64).unwrap()
.build()
.unwrap();
let prover = default_prover();
prover.prove(env, COMPUTE_ELF).unwrap();
}
Common Debugging Scenarios
Problem: Guest panics before processing input
Solution: Check if you’re reading before providing input:
// Wrong - no input provided
let env = ExecutorEnv::builder().build().unwrap();
// Guest tries to read
let value: u64 = env::read(); // Panic!
Scenario: Wrong Journal Output
Problem: Journal contains unexpected data
Solution: Log all commits to debug:
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let result = 42u64;
env::log(&format!("About to commit: {}", result));
env::commit(&result);
env::log("Commit successful");
}
Scenario: Unexpected Cycle Count
Problem: Guest uses more cycles than expected
Solution: Profile sections of code:
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let start_total = env::cycle_count();
let start = env::cycle_count();
let data: Vec<u64> = env::read();
env::log(&format!("Read: {} cycles", env::cycle_count() - start));
let start = env::cycle_count();
let processed = process(data);
env::log(&format!("Process: {} cycles", env::cycle_count() - start));
let start = env::cycle_count();
env::commit(&processed);
env::log(&format!("Commit: {} cycles", env::cycle_count() - start));
env::log(&format!("Total: {} cycles", env::cycle_count() - start_total));
}
Best Practices
Use env::log() for quick debugging before reaching for more complex tools.
Use Debug Builds During Development
Enable RISC0_BUILD_DEBUG=1 while developing, switch to release for testing.
Unit test pure functions and integration test the full guest execution.
Profile Before Optimizing
Measure with the profiler to find real bottlenecks before optimizing.
Configure release profiles to retain debug symbols for production debugging.
Troubleshooting Build Issues
Guest Build Fails
Check the guest build output:
export RISC0_GUEST_LOGFILE=/tmp/guest-build.log
cargo build
cat /tmp/guest-build.log
Verify RISC Zero toolchain installation:
rzup --version
rzup list
rzup install rust
Debugging Build Scripts
Add debug output to build.rs:
fn main() {
println!("cargo:warning=Building guest programs...");
risc0_build::embed_methods();
println!("cargo:warning=Guest build complete");
}
See Also