Skip to main content

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:
rzup install 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

1
Run guest with debugger flag
2
r0vm --elf target/riscv32im-risc0-zkvm-elf/debug/guest --with-debugger
3
Copy the GDB command
4
The output will show a command like:
5
riscv32im-gdb -ex "target remote 127.0.0.1:35051" /tmp/.tmpABC123.elf
6
Run the GDB command in another terminal
7
Paste and run the command from step 2.
8
Set breakpoints and debug
9
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

CommandDescription
break <location>Set breakpoint at function or line
continueContinue execution
nextStep over (execute next line)
stepStep into (enter function calls)
print <var>Print variable value
backtraceShow call stack
info localsShow local variables
listShow source code
quitExit 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

Scenario: Guest Panics Immediately

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

1
Start with Logs
2
Use env::log() for quick debugging before reaching for more complex tools.
3
Use Debug Builds During Development
4
Enable RISC0_BUILD_DEBUG=1 while developing, switch to release for testing.
5
Write Tests
6
Unit test pure functions and integration test the full guest execution.
7
Profile Before Optimizing
8
Measure with the profiler to find real bottlenecks before optimizing.
9
Keep Debug Symbols
10
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

Toolchain Issues

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