Skip to main content

Working with inputs

This guide walks through how a guest program receives inputs during execution. It covers how the ZisK input buffer works, the read primitives available, and how to choose between them depending on your guest program's needs.

Reading inputs in guest programs

Guest programs have no direct access to the outside world during execution. All information they need must be provided through the input buffer, which is the channel through which a guest program receives input data.

Understanding ZisK inputs

The input is a sequential byte buffer. There is no random access, each read advances a cursor forward. Each io::read / io::read_slice call consumes a u64 length header (8 bytes) encoded in little-endian order, followed by the value bytes zero-padded to the next 8-byte boundary. Values must be written in the same order the guest reads them; diverging order causes an error.

Input Buffer
len (u64)
0100000000000000
value + padding
5300000000000000
len (u64)
0200000000000000
value + padding
4200000000000000
len (u64)
0100000000000000
value + padding
5D00000000000000
Guest Program
① io::read::<u8>() → 83
② io::read::<u16>() → 66
③ io::read::<u8>() → 93

Consuming inputs

ziskos::io exposes two primitives for consuming the input buffer. read deserializes the next bytes directly into a typed value. read_slice gives you the raw bytes and leaves interpretation entirely up to you, which is useful for custom formats, zero-copy access, or structures only known at runtime:

FunctionUse whenDescription
read::<T>() -> TYou want automatic deserialization into a typed valueReads the next item and deserializes it into T using bincode. T must implement serde::Deserialize.
read_slice() -> &[u8]You need raw byte access or a custom formatReturns the next chunk of the input buffer as a raw byte slice with no deserialization. Interpretation is entirely up to you.

Both primitives can be mixed freely within the same program, each handling the part of the input it is best suited for.


Working on a real example

In this section you will build a guest program that sums only the prime numbers from a list of u64 values, using an is_prime helper shared through the common crate. You will write it three ways, each time changing how the guest reads the input while keeping the same logic. By the end you will have a concrete feel for each primitive and know when to reach for one over the other.

Set up the project

You have two ways to get a working ZisK project for this guide. Pick whichever fits your situation; the rest of the guide is identical either way.

Clone the examples repository

If you want the finished version of the program this guide builds, or just want to skim a complete project before writing your own, clone the companion examples repo and move into the primes directory:

bash
git clone https://github.com/0xPolygonHermez/zisk.git
cd zisk/examples/primes/guest

Scaffold a new project

To start from an empty project and write the program yourself, use the cargo-zisk CLI to scaffold a new workspace. It handles workspace setup, toolchain configuration, and dependency wiring so you can move straight to writing logic:

bash
cargo-zisk new primes
cd primes/guest

Download the sample inputs this guide uses into a samples/ folder inside the guest/ crate, where the later commands expect them:

bash
mkdir samples
BASE=https://raw.githubusercontent.com/0xPolygonHermez/zisk/refs/heads/main/examples/primes
curl -L -o samples/example-input-struct.bin $BASE/example-input-struct.bin
curl -L -o samples/example-input-multiple.bin $BASE/example-input-multiple.bin
curl -L -o samples/example-input-raw.bin $BASE/example-input-slice.bin

Either path lands you in a project with a guest/ crate where the ZisK program lives and a common/ crate with the shared input types.

Reading a typed struct

Start with the simplest pattern: define a struct that mirrors the input layout and read the entire input in a single call. Every field is deserialized and available before any logic runs.

First, add serde to the common crate dependencies:

common/Cargo.toml
[package]
name = "primes-common"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = "1.0.228"

Then open common/src/lib.rs and define the shared input type together with the is_prime helper the guest will use:

common/src/lib.rs
// Used by the host and guest for serialization and deserialization of input data.
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct InputDTO {
pub values: Vec<u64>,
}

/// Returns `true` if `n` is a prime number, `false` otherwise.
pub fn is_prime(n: &u64) -> bool {
if *n < 2 {
return false;
}
if *n == 2 || *n == 3 {
return true;
}
if *n % 2 == 0 || *n % 3 == 0 {
return false;
}

let mut i = 5;
loop {
if i * i > *n {
return true;
} else if *n % i == 0 || *n % (i + 2) == 0 {
return false;
} else {
i += 6;
}
}
}

Each approach in this guide will have its own binary so they can all live in the project at once. Rename guest/src/main.rs to guest/src/struct.rs and write the program. A single io::read::<InputDTO>() call reads the entire input at once, bincode deserializes the full values vector before any logic runs:

guest/src/struct.rs
#![no_main]
ziskos::entrypoint!(main);

use primes_common::{is_prime, InputDTO};

fn main() {
// Read the input from the guest's standard input.
let input = ziskos::io::read::<InputDTO>();

// Compute the value we want to prove.
let mut result = 0u64;
for value in &input.values {
if is_prime(value) {
result += value;
}
}

// Commit the result as a public output so a verifier can inspect it
// without re-executing the program.
ziskos::io::commit(&result);

println!("sum-primes({:?}) => {result}", input.values);
}

Before building, register the file as a binary target in guest/Cargo.toml as each example in this guide gets its own [[bin]] entry so they can all be built and run side by side:

guest/Cargo.toml
[[bin]]
name = "struct-guest"
path = "src/struct.rs"

Build and run the struct binary, passing the sample input:

bash /sum-primes/guest/
cargo-zisk build --release --bin struct-guest
cargo-zisk run --release --bin struct-guest -i samples/example-input-struct.bin
sum-primes([5, 11, 18, 23, 45]) => 39

The loop walks input.values and accumulates the primes into result. One read, one struct, clean and type-safe, but bincode copies every byte into owned Rust values, the full input is in memory before the loop starts. If you need to action each value before reading the next, the following approach gives you more control.

Reading multiple times

This is the second example in the guide, so rather than touching the struct binary you'll add a new one alongside it. The logic stays the same, only the way the input is consumed changes.

Drop the struct entirely. Instead of reading the whole input in one call, read the length header first, then pull each value out one at a time, checking and summing primes as they arrive, so the guest never holds the full vector at once. Create a second program at guest/src/multiple.rs:

guest/src/multiple.rs
#![no_main]
ziskos::entrypoint!(main);

use primes_common::is_prime;

fn main() {
// Read the length from the guest's standard input.
let len = ziskos::io::read::<u64>();

print!("sum-primes([");
// Reading each value from the guest standard inputs and adding it if its prime
let mut result = 0;
for i in 0..len {
let value = ziskos::io::read::<u64>();
if i == 0 {
print!("{value}");
} else {
print!(",{value}");
}
if is_prime(&value) {
result += value;
}
}

// Commit the result as a public output so a verifier can inspect it
// without re-executing the program.
ziskos::io::commit(&result);

println!("]) => {result}");
}

Add a second [[bin]] entry to guest/Cargo.toml for this file:

guest/Cargo.toml
[[bin]]
name = "multiple-guest"
path = "src/multiple.rs"

Build and run the multiple binary:

bash /sum-primes/guest/
cargo-zisk build --release --bin multiple-guest
cargo-zisk run --release --bin multiple-guest -i samples/example-input-multiple.bin
sum-primes([5, 11, 18, 23, 45]) => 39

Same result. Each value is checked the moment it arrives, so the guest never has to hold the full vector in memory before it can start work.

Reading raw bytes

In this last example we'll use read_slice together with rkyv for zero-copy access to a typed struct. rkyv serializes the data in a layout that can be accessed directly from the byte slice, no deserialization step, no allocations, no manual offset arithmetic. First, add rkyv to the common crate alongside the existing serde dependency:

common/Cargo.toml
[dependencies]
serde = "1.0.228"
rkyv = { version = "0.8.16" , features = ["std", "alloc"]}

Then add a parallel InputZeroCopyDTO next to the serde-based one, deriving rkyv's Archive, Serialize, and Deserialize:

common/src/lib.rs
// Re-exported so the guest and host can use the same `rkyv` version as the derive macros below.
pub use rkyv;

// Used by the guest for zero-copy deserialization, avoiding heap allocation overhead in the guest.
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub struct InputZeroCopyDTO {
pub values: Vec<u64>,
}

Now create guest/src/slice.rs. The key difference from the other approaches is that from_bytes gives a typed view of the raw bytes, input.values is accessible directly, with no copy:

guest/src/slice.rs
#![no_main]
ziskos::entrypoint!(main);

use primes_common::{is_prime, rkyv, InputZeroCopyDTO};

fn main() {
// Read the input from the guest's standard input.
let raw_input = ziskos::io::read_slice();

// Zero-copy view: no deserialization, no allocation
let input =
rkyv::api::high::from_bytes::<InputZeroCopyDTO, rkyv::rancor::Error>(raw_input.as_ref())
.unwrap();

// Compute the value we want to prove.
let mut result = 0;
for value in &input.values {
if is_prime(value) {
result += value;
}
}

// Commit the result as a public output so a verifier can inspect it
// without re-executing the program.
ziskos::io::commit(&result);

println!("sum-primes({:?}) => {result}", input.values);
}

Add a third [[bin]] entry to guest/Cargo.toml for this file:

guest/Cargo.toml
[[bin]]
name = "slice-guest"
path = "src/slice.rs"

Build and run the raw binary, passing the rkyv-encoded input as a binary file:

bash /sum-primes/guest/
cargo-zisk build --release --bin slice-guest
cargo-zisk run --release --bin slice-guest -i samples/example-input-slice.bin
sum-primes([5, 11, 18, 23, 45]) => 39

Same result as the previous two approaches. The advantage over manual byte parsing is that rkyv handles field layout and alignment, you get the typed struct interface without the bincode copy cost.

Comparing the three approaches

There is no single best choice. Use read::<T>() when the schema is fixed and having all fields ready at once is more convenient, bincode handles framing but copies every byte into owned values. Use individual reads when you want to process each value as it arrives without allocating the full collection up front. Use read_slice() when you want the raw bytes and intend to interpret them yourself, you control the wire format and decode straight from the slice, so nothing is allocated beyond what you choose.


Summary

You now understand how data flows into a guest program. The input buffer is the only channel from the outside world, everything the guest knows is consumed from there. The choice of primitive determines how much control you have over the read sequence, how much memory is allocated, and how clearly the intent reads in code.

The key invariant to keep in mind: the way the guest reads determines the layout of the input. Change the read pattern, switching from a struct to individual fields, or from typed reads to a raw slice, and the input passed on must change to match.


Next steps

With inputs covered, the natural next step is the other side of the I/O model — how your guest exposes results to the verifier:

  • Committing outputs: learn how outputs are committed to the proof and made available to the verifier as public values.
  • Profiling your program: measure execution cost to find bottlenecks before they become expensive to prove.