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.

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

Reading Input from the Host

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();

Reading Slices (High Performance)

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);

Committing Slices (High Performance)

// 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.

Performance Optimization

Choosing the Right Method

1
For Small Data (< 1KB)
2
Use read() and commit() - convenience matters more than performance.
3
For Large Data (> 1KB)
4
Use read_slice() and commit_slice() to avoid serialization overhead.
5
For Very Large Data (> 100KB)
6
Consider using frames or breaking into chunks.
7
For Structured Data
8
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

Pattern: Input Validation

// 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

“Reading past end of input”

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

1
Keep Journal Small
2
Only commit data that verifiers need. Use private outputs for everything else.
3
Use Slices for Performance
4
When dealing with large data, use _slice variants to reduce cycles.
5
Validate Inputs Early
6
Check input validity at the start of guest execution to fail fast.
7
Document Types
8
Clearly document the input/output schema for your guest programs.
9
Consider Versioning
10
Add version fields to structures for future compatibility.

See Also