Runtime

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:

  • Macros that allow 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.

  • print! / println! macros that write to an output log, implemented by the zkVM via an ecall.

  • A #[panic_handler], required by Rust’s core, implemented by the zkVM via an ecall.

  • A #[global_allocator] that operates correctly across the zkVM’s memory models.

  • An assembly entry point for the program that configures zkVM-specific global state and calls the guest program’s main function; this ensures stack, heap, and input/output segments are usable during both passes of 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 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

Read the public output address from well-known location PUBLIC_OUTPUT_ADDRESS_LOCATION.

2

Add byte_index to that address.

3

Emit a wou instruction, whose behavior depends on zkVM execution mode:

  • In the zkVM’s first pass (Harvard-like architecture), wou writes the word to a unique memory address space that 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 are input values known to the verifier, specified as arguments to #[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 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

Read the public input address from well-known location PUBLIC_INPUT_ADDRESS_LOCATION.

2

Add byte_index to that address.

3

Emit a rin instruction, whose behavior depends on zkVM execution mode:

  • In the zkVM’s first pass (Harvard-like architecture), 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 are input values known only to the prover. They are specified as arguments to #[nexus_rt::main] and optionally tagged with #[nexus_rt::private_input]. Arguments default to being private, but developers can explicitly mark them private for clarity.

Example use:

#[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 sequentially reads a single byte from the private input tape via an ecall, returning None after each byte has been read once. The runtime provides 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 run and debug guest programs in a native environment (e.g., cargo run). The Nexus runtime itself cannot run outside the zkVM, but its macros support native handlers—functions compiled and executed only when the guest program is run natively on the host. These functions generate inputs and handle outputs without the zkVM’s presence.

Example:

#![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
}

This allows running the program natively to ensure correctness without relying on 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. To help developers, the Nexus runtime implements particular parts of the Rust runtime:

  • A simple global allocator and implementations for panic and abort, as required by core.

  • Simple print! and println! implementations that wrap zkVM-specific system calls for logging.

In the future, the team plans to enable std for the Nexus zkVM where appropriate by gating functionality dependent on an operating system or incompatible with provable computation. For example, there are no plans to enable multi-threading or general I/O, but certain std functionality useful for the zkVM (e.g., some system calls like exit, algorithms, and data structures) may be supported.

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, it uses 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:

  • The guest program must depend on nexus-rt and provide a nexus_rt::main-decorated function as the entry point. Without these, the zkVM can’t properly load the program and behavior is undefined.

  • 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 (compiled with -fPIC).

The Nexus cargo CLI tool ensures these requirements are met for projects it creates; initializing projects with it is highly recommended.

Further reading: