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 ecalls, 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’s main 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 an ecall.
  • A #[panic_handler], which is required by Rust’s core and is implemented by the zkVM via an ecall.
  • 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:

#[nexus_rt::main]
fn main(x: u32, y: u32) -> bool {
    x > y
}

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:

  1. Reading the public output address from well-known location PUBLIC_OUTPUT_ADDRESS_LOCATION.
  2. Adding byte_index to that address.
  3. 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 ordinary sw instructions that write the output values from a location calculated and populated by the zkVM using data collected from the first pass.

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:

#[nexus_rt::main]
#[nexus_rt::public_input(x)]
fn main(x: u32, y: u32) -> bool {
    x > y
}

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:

  1. Reading the public input address from well-known location PUBLIC_INPUT_ADDRESS_LOCATION.
  2. Adding byte_index to that address.
  3. 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 ordinary lw instructions that read the input values from a location calculated and populated by the zkVM using data collected from the first pass.

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.

#[nexus_rt::main]
#[nexus_rt::private_input(y)]
fn main(x: u32, y: u32) -> bool {
    x > y
}

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:

#[nexus_rt::main]
#[nexus_rt::public_input(x)]
fn main(x: u32) -> u32 {
    x ^ 0xdeadbeef
}

Running natively, we need to provide an input for x and handle the output. The following code does just that:

#![cfg_attr(target_arch = "riscv32", no_std, no_main)]

#[cfg(not(target_arch = "riscv32"))]
fn native_input_handler() -> Option<u32> {
    let mut input = String::new();

    std::io::stdin().read_line(&mut input).ok()?;
    
    Some(input.trim().parse().ok()?)
}

#[cfg(not(target_arch = "riscv32"))]
fn native_output_handler(output: &u32) -> Option<()> {
    println!("Output: {}", output);

    Some(())
}

#[nexus_rt::main]
#[cfg_attr(target_arch = "riscv32", nexus_rt::public_input(x))]
#[cfg_attr(not(target_arch = "riscv32"), nexus_rt::custom_input(x, native_input_handler))]
#[cfg_attr(not(target_arch = "riscv32"), nexus_rt::custom_input(y, native_input_handler))]
#[cfg_attr(not(target_arch = "riscv32"), nexus_rt::custom_output(native_output_handler))]
fn main(x: u32, y: u32) -> u32 {
    x ^ 0xdeadbeef
}

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 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.
  • The guest program must be annotated with no_std and no_main when compiled for the zkVM (i.e., for a riscv32 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.