Runtime
Note: This page assumes familiarity with the host-guest model described in the SDK Quick Start.
What’s a Runtime?
In the vast majority of modern software development, a developer never writes code that directly interacts with the platform it’s running on. Instead, the platforms for which developers write code offer runtimes: easy-to-use (and often standardized) APIs that provide high-level functionality while abstracting away most platform-specific details. Prominent examples include POSIX and 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.
The Nexus Runtime
The zkVM runtime environment is most similar to a bare-metal or embedded environment, though it has properties specific to being a zkVM. As is typical for embedded platforms, interaction with the platform (zkVM) is done via raw 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:
- A set of macros that allows developers to work easily with input and output, which are memory-mapped and directly managed by the zkVM.
- A
main
macro that ensures the guest program’smain
function can be correctly located and run by the zkVM. - A
print!
/println!
macro that allows developers to write to an output log, which is implemented by the zkVM via anecall
. - A
#[panic_handler]
, which is required by Rust’score
and is implemented by the zkVM via anecall
. - A
#[global_allocator]
that functions well and correctly in each of the zkVM’s memory models. - An assembly entry point for the program. This configures zkVM-specific global state and eventually calls the guest program’s
main
function. This is responsible for ensuring that the stack, heap, and input/output memory segments are usable during both passes of zkVM execution. - Well-known locations that correspond to particular memory-mapped values provided or read by the zkVM.
The rest of this section discusses these components in more detail and with examples. For even more detail, see the VM specification.
Notable Macros & Functions
Main
The nexus_rt::main
attribute marks the entry point of the guest program. It is a procedural macro that:
- Ensures that the main function can be located and run by the runtime’s entry-point assembly. It does this by re-exporting the main function with a non-mangled, well-known name referenced by the runtime’s entry-point assembly.
- Generates code that automatically reads in all inputs and writes to the output. Unless specified otherwise, inputs default to private.
- Re-orders the function’s attributes to ensure that input type specifications are evaluated before the
main
macro generates code that reads them.
Example use:
The 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:
- Reading the public output address from well-known location
PUBLIC_OUTPUT_ADDRESS_LOCATION
. - Adding
byte_index
to that address. - Emitting a
wou
instruction, which has behavior dependent on the zkVM’s execution mode:- In the zkVM’s first pass, where a Harvard-like architecture is used,
wou
writes the word to a unique memory address space which only contains public output. - In the second pass, the zkVM replaces the binary’s
wou
instructions with ordinarysw
instructions that write the output values from a location calculated and populated by the zkVM using data collected from the first pass.
- In the zkVM’s first pass, where a Harvard-like architecture is used,
Public Input
Public inputs, which are input values known to the verifier, are specified as arguments to the #[nexus_rt::main]
and tagged with #[nexus_rt::public_input]
.
Example use:
In this example, 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:
- Reading the public input address from well-known location
PUBLIC_INPUT_ADDRESS_LOCATION
. - Adding
byte_index
to that address. - Emitting a
rin
instruction, which has behavior dependent on the zkVM’s execution mode:- In the zkVM’s first pass, where a Harvard-like architecture is used,
rin
reads the word from a unique memory address space which only contains public input. - In the second pass, the zkVM replaces the binary’s
rin
instructions with ordinarylw
instructions that read the input values from a location calculated and populated by the zkVM using data collected from the first pass.
- In the zkVM’s first pass, where a Harvard-like architecture is used,
Private Input
Private inputs, which are input values known only to the prover, are specified as arguments to the #[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.
In this example, both 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.
Host-Native Execution
It is often useful to be able to run and debug guest programs in a native environment (via, e.g., 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:
Running natively, we need to provide an input for x
and handle the output. The following code does just that:
The user can now run the program natively and ensure the program itself works as expected without being impeded by the zkVM’s limited debugging capabilities.
Functionality in a no_std
Environment
The Nexus zkVM has no operating system and cannot implement large portions of the Rust standard library, so guest programs must be no_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.
Memory Allocation
The Nexus runtime implements a simple allocator required for any data structures that rely on heap allocations (i.e., those in alloc
). Currently, we use a naive bump allocator that allocates bottom-up and never deallocates.
Assumptions
Despite the conveniences offered by the Nexus runtime, there are still some requirements for guest programs. These include:
- The guest program must have a dependency on
nexus-rt
and provide anexus_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. - The guest program must be annotated with
no_std
andno_main
when compiled for the zkVM (i.e., for ariscv32
target architecture). - The guest program’s code must be position-independent (i.e., compiled with
-fPIC
).
The Nexus cargo
CLI tool ensures that these requirements are met by projects it creates, so we highly recommend initializing projects with it.
Was this page helpful?