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.
Data flow is fundamental to zkVM applications. The host provides private inputs, the guest processes them, and outputs are committed to the journal (public) or written back to the host (private).
Data Flow Overview
Host Guest Receipt
| | |
|--- write() ---------> read() (private input) |
| | |
| process() |
| | |
| <------- write() ------| (private output) |
| | |
| commit() ----------------> journal
- Private inputs: Provided by host, read by guest, not in receipt
- Private outputs: Written by guest, received by host, not in receipt
- Public outputs: Committed to journal, included in receipt, verifiable by anyone
Basic Reading
The guest uses env::read() to receive data from the host:
// Guest code
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
// Read primitive types
let a: u64 = env::read();
let b: u32 = env::read();
let flag: bool = env::read();
}
// Host code
use risc0_zkvm::ExecutorEnv;
let env = ExecutorEnv::builder()
.write(&17u64).unwrap()
.write(&42u32).unwrap()
.write(&true).unwrap()
.build()
.unwrap();
The order of write() calls on the host must exactly match the order of read() calls in the guest.
Reading Complex Types
Use serde for structured data:
// Shared types (in a common crate)
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Input {
value: u64,
name: String,
data: Vec<u8>,
}
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let input: Input = env::read();
println!("Received: {} with value {}", input.name, input.value);
}
// Host
let input = Input {
value: 42,
name: "example".to_string(),
data: vec![1, 2, 3],
};
let env = ExecutorEnv::builder()
.write(&input).unwrap()
.build()
.unwrap();
For performance-critical code, use read_slice():
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
// Read length first
let len: usize = env::read();
// Allocate and read data
let mut buffer = vec![0u8; len];
env::read_slice(&mut buffer);
}
// Host
let data = vec![1u8, 2, 3, 4, 5];
let env = ExecutorEnv::builder()
.write(&data.len()).unwrap()
.write_slice(&data)
.build()
.unwrap();
read_slice() is significantly faster than read() for large data because it avoids deserialization overhead. Use it when performance matters.
Reading Frames
Frames include length headers for efficient large data transfer:
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let data = env::read_frame();
// Process data
}
// Host
let payload = b"large data payload";
let env = ExecutorEnv::builder()
.write_frame(payload)
.build()
.unwrap();
Committing to the Journal
The journal contains public outputs included in the receipt:
Basic Commits
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let result: u64 = 42;
// Commit to journal (public output)
env::commit(&result);
}
// Host - extract from receipt
let receipt = prover.prove(env, GUEST_ELF).unwrap().receipt;
let result: u64 = receipt.journal.decode().unwrap();
assert_eq!(result, 42);
Committing Multiple Values
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let a = 17u64;
let b = 23u64;
let product = a * b;
// Commit multiple values
env::commit(&a);
env::commit(&b);
env::commit(&product);
}
// Host - decode in order
let a: u64 = receipt.journal.decode().unwrap();
let b: u64 = receipt.journal.decode().unwrap();
let product: u64 = receipt.journal.decode().unwrap();
Committing Structures
// Shared
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Output {
result: u64,
hash: [u8; 32],
success: bool,
}
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let output = Output {
result: 42,
hash: [0; 32],
success: true,
};
env::commit(&output);
}
// Host
let output: Output = receipt.journal.decode().unwrap();
assert!(output.success);
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let data = [1u8, 2, 3, 4, 5];
// Commit raw bytes
env::commit_slice(&data);
}
// Host - reading raw bytes from journal
let mut data = [0u8; 5];
// Note: Reading raw slices from journal requires manual deserialization
Writing Private Output
Use write() to send data back to the host without including it in the receipt:
Basic Private Output
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let intermediate = 42;
// Send to host (private)
env::write(&intermediate);
let result = 84;
// Commit to journal (public)
env::commit(&result);
}
Private outputs are useful for:
- Debugging information
- Intermediate results not needed for verification
- Large data that would make the receipt too big
Writing to stdout/stderr
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
// stdout and stderr are private outputs
env::stdout().write(&"Processing...\n");
env::stderr().write(&"Warning: something\n");
}
// Host - capture stdout/stderr
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();
let env = ExecutorEnv::builder()
.stdout(&mut stdout_buf)
.stderr(&mut stderr_buf)
.build()
.unwrap();
Using File Descriptors
Direct access to I/O streams:
// Guest
use risc0_zkvm::guest::env;
use risc0_zkvm::guest::env::{Read, Write};
risc0_zkvm::guest::entry!(main);
fn main() {
let mut stdin = env::stdin();
let mut stdout = env::stdout();
let mut journal = env::journal();
// Read from stdin
let input: u64 = stdin.read();
// Write to stdout (private)
stdout.write(&(input * 2));
// Write to journal (public)
journal.write(&input);
}
Custom File Descriptors
// Host
use risc0_zkvm::ExecutorEnv;
use std::io::Cursor;
let custom_input = Cursor::new(vec![1, 2, 3, 4]);
let mut custom_output = Vec::new();
let env = ExecutorEnv::builder()
.read_fd(10, custom_input) // Custom FD 10 for reading
.write_fd(11, &mut custom_output) // Custom FD 11 for writing
.build()
.unwrap();
Do not use file descriptor numbers that conflict with standard descriptors (0-3). See risc0_zkvm_platform::fileno for reserved values.
Choosing the Right Method
Use read() and commit() - convenience matters more than performance.
Use read_slice() and commit_slice() to avoid serialization overhead.
For Very Large Data (> 100KB)
Consider using frames or breaking into chunks.
Balance between serialization convenience and performance needs.
Cycle Count Comparison
// Guest - measuring performance
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
// Method 1: Using read()
let start1 = env::cycle_count();
let data1: Vec<u8> = env::read();
let end1 = env::cycle_count();
// Method 2: Using read_slice()
let start2 = env::cycle_count();
let len: usize = env::read();
let mut data2 = vec![0u8; len];
env::read_slice(&mut data2);
let end2 = env::cycle_count();
env::log(&format!("read() cycles: {}", end1 - start1));
env::log(&format!("read_slice() cycles: {}", end2 - start2));
}
Real-World Example
Complete example processing JSON data:
// Guest
use risc0_zkvm::guest::env;
use risc0_zkvm::sha::{Impl, Sha256};
use json::parse;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Output {
critical_value: u32,
data_hash: [u8; 32],
}
risc0_zkvm::guest::entry!(main);
fn main() {
// Read JSON string (private input)
let json_str: String = env::read();
// Log for debugging (private output)
env::log(&format!("Processing {} bytes", json_str.len()));
// Compute hash of input data
let hash = *Impl::hash_bytes(json_str.as_bytes());
// Parse and extract critical data
let data = parse(&json_str).unwrap();
let critical_value = data["critical_data"].as_u32().unwrap();
// Create output structure
let output = Output {
critical_value,
data_hash: hash.into(),
};
// Commit to journal (public output)
env::commit(&output);
}
// Host
use risc0_zkvm::{ExecutorEnv, default_prover};
fn process_json(json_data: &str) -> Output {
// Send JSON to guest
let env = ExecutorEnv::builder()
.write(&json_data.to_string()).unwrap()
.build()
.unwrap();
// Execute and prove
let prover = default_prover();
let receipt = prover.prove(env, GUEST_ELF).unwrap().receipt;
// Extract public output
let output: Output = receipt.journal.decode().unwrap();
// Verify
receipt.verify(GUEST_ID).unwrap();
output
}
Common Patterns
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let value: u64 = env::read();
// Validate input
if value == 0 || value > 1_000_000 {
panic!("Invalid input: {}", value);
}
// Process valid input
let result = value * 2;
env::commit(&result);
}
Pattern: Conditional Output
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let flag: bool = env::read();
let data: Vec<u8> = env::read();
if flag {
// Commit full data
env::commit(&data);
} else {
// Commit only hash
let hash = compute_hash(&data);
env::commit(&hash);
}
}
Pattern: Batching
// Guest
use risc0_zkvm::guest::env;
risc0_zkvm::guest::entry!(main);
fn main() {
let count: usize = env::read();
let mut results = Vec::new();
for _ in 0..count {
let input: u64 = env::read();
let result = process(input);
results.push(result);
}
// Commit all results at once
env::commit(&results);
}
Troubleshooting
”Failed to deserialize”
Ensure types match exactly between host and guest:
// Wrong - type mismatch
env::builder().write(&42u64) // Host writes u64
let x: u32 = env::read(); // Guest reads u32
// Correct
env::builder().write(&42u64) // Host writes u64
let x: u64 = env::read(); // Guest reads u64
The guest tried to read more data than the host provided:
// Wrong
env::builder().write(&42u64) // Host writes one value
let a: u64 = env::read();
let b: u64 = env::read(); // Error: no more data
// Correct
env::builder()
.write(&42u64)
.write(&23u64) // Provide both values
Journal Decode Errors
Verify the order and types when decoding:
// Guest commits: u64, then String
env::commit(&42u64);
env::commit(&"hello".to_string());
// Host must decode in same order
let number: u64 = receipt.journal.decode().unwrap();
let text: String = receipt.journal.decode().unwrap();
Best Practices
Only commit data that verifiers need. Use private outputs for everything else.
When dealing with large data, use _slice variants to reduce cycles.
Check input validity at the start of guest execution to fail fast.
Clearly document the input/output schema for your guest programs.
Add version fields to structures for future compatibility.
See Also