glibc
. Runtimes also provide controlled and abstracted access to platform-specific functionality that developers do need access to, which is especially relevant for the Nexus zkVM.
ecall
s, custom instructions, and memory-mapped I/O. Uniquely to zkVMs, however, the memory model changes fundamentally between executions of the same guest program (see the aside below). The Nexus runtime is a set of libraries and macros that allows developers to write higher-level programs that can run efficiently and correctly on the Nexus zkVM, agnostic to the subtleties of the zkVM’s varying executions.
Aside: the zkVM’s first pass uses a Harvard-like architecture with distinct memory spaces for code, data, inputs, and output. During the first pass, the zkVM collects statistics about exactly how much memory is used in each of these spaces. Before the second pass, the zkVM lays out the program’s memory in a single address space whose size is minimized based on the statistics collected during the first pass. Because proving memory is expensive, this process significantly reduces proof sizes and proving times. The second pass maintains memory protections but uses a single address space for all memory segments.More specifically, the Nexus runtime consists of a few parts:
main
macro that ensures the guest program’s main
function can be correctly located and run by the zkVM.print!
/println!
macro that allows developers to write to an output log, which is implemented by the zkVM via an ecall
.#[panic_handler]
, which is required by Rust’s core
and is implemented by the zkVM via an ecall
.#[global_allocator]
that functions well and correctly in each of the zkVM’s memory models.main
function. This is responsible for ensuring that the stack, heap, and input/output memory segments are usable during both passes of zkVM execution.nexus_rt::main
attribute marks the entry point of the guest program. It is a procedural macro that:
main
macro generates code that reads them.main
macro also handles the program’s public output, which is simply the value returned from main
. The macro internally uses the write_public_output
function to serialize the output and write it to the output tape.
The public output is written by the Nexus runtime’s write_public_output
, which is a function that serializes the program’s output in a COBS representation generated by postcard
.
The serialized bytes are written to the output tape using the Nexus runtime’s write_output!(byte_index, value)
macro, which writes a byte-addressed word to the output tape by:
PUBLIC_OUTPUT_ADDRESS_LOCATION
.byte_index
to that address.wou
instruction, which has behavior dependent on the zkVM’s execution mode:
wou
writes the word to a unique memory address space which only contains public output.wou
instructions with ordinary sw
instructions that write the output values from a location calculated and populated by the zkVM using data collected from the first pass.#[nexus_rt::main]
and tagged with #[nexus_rt::public_input]
.
Example use:
x
is a public input while y
is private.
Public inputs are read by the Nexus runtime’s read_public_input
, which is a function that reads the entire public input tape and deserializes it from a COBS representation produced by postcard
.
The serialized bytes are read using the Nexus runtime’s read_input!(byte_index)
macro, which reads a byte-addressed word from the serialized public input by:
PUBLIC_INPUT_ADDRESS_LOCATION
.byte_index
to that address.rin
instruction, which has behavior dependent on the zkVM’s execution mode:
rin
reads the word from a unique memory address space which only contains public input.rin
instructions with ordinary lw
instructions that read the input values from a location calculated and populated by the zkVM using data collected from the first pass.#[nexus_rt::main]
and optionally tagged with #[nexus_rt::private_input]
.
Arguments default to being private, but developers can still explicitly mark them private for clarity.
x
and y
are private inputs.
Private inputs are read by the Nexus runtime’s read_private_input
, which is a function that sequentially reads a single byte from the private input tape via an ecall
, returning None
after each byte has been read once. We provide no further special handling for private inputs; guest program developers are expected to handle the interpretation of the private input tape themselves.
cargo run
). The Nexus runtime itself cannot run outside the zkVM, but its macros support native handlers, which are functions that are compiled and executed only when the guest program is run natively (on the host). These functions are used to generate inputs and handle outputs without the zkVM’s presence.
Consider the following example:
x
and handle the output. The following code does just that:
no_std
Environmentno_std
. In order to make the guest program developer’s life easier, the Nexus runtime implements particular parts of the Rust runtime. We provide a simple global allocator and an implementation for panic
and abort
, which are required by core
Rust functionality.
We also offer simple print!
and println!
implementations which wrap zkVM-specific system calls in order to enable logging.
In the future, we plan to enable std
for the Nexus zkVM by gating off the functionality dependent on an operating system or incompatible with the idea of provable computation. For example, we have no plans to ever enable multi-threading or general I/O, but we will enable the std
functionality that is useful for the zkVM. This might include certain system calls like exit
, algorithms and data structures, and so on.
alloc
). Currently, we use a naive bump allocator that allocates bottom-up and never deallocates.
nexus-rt
and provide a nexus_rt::main
decorated function as the entry point. If a guest program does not do these things, then the zkVM can’t properly load it, and the program will run with undefined behavior.no_std
and no_main
when compiled for the zkVM (i.e., for a riscv32
target architecture).-fPIC
).cargo
CLI tool ensures that these requirements are met by projects it creates, so we highly recommend initializing projects with it.