Input / Output (io)
The io module is the guest's only channel to the outside world.
Inputs arrive as a sequential bytes written by the host and public
outputs are committed into the proof
during execution so they can be consumed after verification.
Overview
ZisK guests execute inside a zkVM that has no operating system, no filesystem, no networking, and no environment. The only data that crosses into the guest is whatever the host wrote into the input region before execution started, and the only data the guest can publish is whatever it commits into the proof's public-output region. The io module is the interface for both directions. It provides four primitives that map directly onto the underlying memory regions:
| Direction | Typed | Raw |
|---|---|---|
| Read | io::read<T>() -> T | io::read_slice<'a>() -> &'a [u8] |
| Commit | io::commit(value: &T) | io::commit_slice(buf: &[u8]) |
Reading inputs
read<T>() -> T
Deserializes the next value from the input stream into T using
bincode. The read order must match the order the values were written.
pub fn read<T: DeserializeOwned>() -> T
Internally this performs two reads from the input stream: first a
u64 length header that says how many bytes the value occupies,
then exactly that many payload bytes which are passed to
bincode::deserialize to reconstruct T.
If no value is available to consume from the input stream, the program blocks waiting for one. It does not fail or return early.
Parameters
| Name | Type | Description |
|---|---|---|
T | type parameter | Target type. Must implement serde::DeserializeOwned. |
Returns
| Type | Description |
|---|---|
T | The deserialized value read from the input stream. |
Example
let data = io::read::<u64>();
let data: Vec<u8> = io::read();
Read order must match write order exactly. A type mismatch produces a deserialization panic; a length mismatch is silent, leaving the next read out of sync
read_slice<'a>() -> &'a [u8]
Returns the payload of the next input frame as a raw byte slice without deserializing it. Use this when the data is already in a binary format the guest can interpret directly, or when bincode adds unwanted overhead.
// In the guest (zkvm): a zero-copy borrow of the input region
pub fn read_slice<'a>() -> &'a [u8]
// Off-target (native execution): an owned copy
pub fn read_slice() -> Box<[u8]>
Internally this performs two reads from the input stream: first a
u64 length header that says how many bytes the value occupies,
then exactly that many payload bytes are read and returned raw.
If no value is available to consume from the input stream, the program blocks waiting for one. It does not fail or return early.
This function was previously read_input_slice. That name still
exists as a deprecated alias and forwards to read_slice; prefer
read_slice in new code.
Returns
| Type | Description |
|---|---|
&[u8] (guest) | Zero-copy borrow of the next input frame. |
Box<[u8]> (native) | Owned copy of the next input frame. |
Example
// Read a 32-byte digest written via stdin.write_slice on the host.
let digest = io::read_slice();
assert_eq!(digest.len(), 32);
// Reinterpret raw bytes without copying.
let words: &[u64] = unsafe {
core::slice::from_raw_parts(
digest.as_ptr() as *const u64,
digest.len() / 8,
)
};
Committing outputs
commit(value: &T) -> ()
Serializes value with bincode and appends the result to the public output as 32-bit slots. Committed values are written into the proof and visible to the verifier. (The buffer is padded with zeros to the next 4-byte boundary if its length is not a multiple of 4).
pub fn commit<T: Serialize>(value: &T)
Parameters
| Name | Type | Description |
|---|---|---|
value | &T | Reference to the value to commit. T must implement serde::Serialize. |
Returns
Nothing. The serialized bytes are packed into the next available 32-bit slots of public values.
Example
let result: u64 = compute(&input);
io::commit(&result);
let summary = MySummary { ok: true, count: 42 };
io::commit(&summary);
The verifier reads public values in the same order they were committed. Two callers reading the same proof must agree on both the order and the type of each commit.
commit_slice(value: &[u8]) -> ()
The raw-bytes counterpart to commit. Writes buf to the public
output stream as a sequence of 32-bit little-endian words (the buffer
is padded with zeros to the next 4-byte boundary if its length is not
a multiple of 4).
pub fn commit_slice(buf: &[u8])
Parameters
| Name | Type | Description |
|---|---|---|
buf | &[u8] | Raw byte slice to write to the public output stream. |
Returns
Nothing. Each consecutive 4-byte chunk of buf is written as a
little-endian u32; the final chunk is zero-padded if buf.len() is
not a multiple of 4.
Example
// Commit a digest without padding
let digest: [u8; 32] = Sha256::digest(&input).into();
io::commit_slice(&digest);
// Commit a array of bytes with padding
let encoded: [u8; 30] = [0u8; 30];
io::commit_slice(&encoded);
The verifier reads public values in the same order they were committed. Two callers reading the same proof must agree on both the order and the type of each commit.
Cursor controls
Both directions are sequential: reads advance an input cursor and commits advance an output cursor. These helpers rewind those cursors — useful in tests or when re-reading the input from the start.
read_input_reset() -> ()
Reset the input cursor to the beginning so the next read / read_slice
starts again from the first value.
pub fn read_input_reset()
write_output_reset() -> ()
Reset the output cursor to slot 0, discarding any previously committed public values.
pub fn write_output_reset()