Any program that is RV32IM machine code can be deployed, whatever produced it. This page shows two paths: C through rand-guest build --lang c, and a program you write in assembly and pack yourself. Both end in the same image container that Deploy a Rust program produces, and both deploy with rand program deploy. Set up the circuits checkout and rand-guest first, as in step 1 of that page. For chains 18 to 20 the checkout was at commit aeacf31, which fullnode v0.6.7 and v0.6.8 both pinned; fullnode v0.8.0 (chain 21) pins d217252, constraint set 9, and an image built at either commit is the same words, so it deploys on chain 21 with the v0.8.0 wallet. The outputs below were produced on 2026-09-19 at 7ef3220 and have not been re-run. The rand-guest directory, guest.h and rt.c included, is the same at both commits.
C via clang
What you need
| tool | version | how |
|---|---|---|
| clang | 23.1.1, with a RISC-V backend | brew install llvm on macOS; Apple’s system clang has no RISC-V backend. Or set $CLANG to a 23.1.1 binary |
rust-lld | from Rust 1.98.1 | rustup +1.98.1 component add llvm-tools |
rand-guest refuses any clang whose version is not 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 builds with another one, with a warning that hc will not match published images.
Write the guest
A C guest directory holds only .c files (and an optional .ld). rand-guest supplies guest.h, _start and a small runtime. Do not add your own guest.h or start.S: build --lang c refuses a directory that has either.
This is the committed guests-compiled/c-fib/fib.c:
#include "guest.h"
static uint32_t fib(uint32_t n) {
uint32_t a = 0, b = 1;
for (uint32_t i = 0; i < n; i++) {
uint32_t c = a + b;
a = b;
b = c;
}
return a;
}
void main(void) {
rand_write_output(0, fib(rand_read_input(0)));
rand_halt();
}
guest.h declares rand_read_input, rand_read_public, rand_write_output, rand_poseidon2, rand_keccak, rand_sha256_compress and rand_halt. It has no wrapper for syscall 7, the length-bound POSEIDON2_LEN that the machine has run since constraint set 7, and rand-guest check at aeacf31 refuses an ecall with a7 = 7 (zkp-circuits issue #1). A C or assembly guest for chain 20 or 21 therefore uses syscalls 0 to 6 (syscall 8, MODQ, is constraint set 9’s and is used only by the multisig auth guest).
Build, run, inspect
$ rand-guest build --lang c guests-compiled/c-fib --out c-fib.bin
25 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote c-fib.bin and its .sha256 (25 words, hc 76e723cb4c218f9a7f657f39b9f95ee2353d8fbeb601aaea394afa29928f3aee, program id c213cf5c94679cab4585ed1e81b61736a7d2fe5605dc7884e4acec4144665999)
$ rand-guest run c-fib.bin --input 20 --tier 10
out[0] = 6765
out[1] = 0
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 116
tier 10
tier 10: fits (cycles 126 of 1023, Poseidon2 permutations 10 of 128)
fib(20) = 6765, the same answer as the Rust fib guest.
build --lang c compiles every .c file in the directory (in sorted order), plus the bundled start.S and rt.c, with:
clang --no-default-config --target=riscv32-unknown-none-elf -march=rv32im -mabi=ilp32 -mno-relax
-nostdlib -ffreestanding -fno-builtin -ffunction-sections -fdata-sections -Os
-ffile-prefix-map=<checkout>=/rand-circuits -c
and links them with rust-lld -flavor gnu --gc-sections -T <ld> from the pinned Rust toolchain. --gc-sections keeps only what the guest calls.
The C runtime
-ffreestanding -fno-builtin stop clang from recognising library functions, but not from emitting calls to them. A struct copy becomes memcpy, a large zero initialiser becomes memset, and a 64-bit division by a run-time value becomes a helper call, because RV32IM divides 32-bit values only. rt.c defines these eight:
| function | used for |
|---|---|
memcpy, memmove, memset, memcmp | copies, fills, compares |
__udivdi3, __umoddi3, __divdi3, __moddi3 | 64-bit / and % |
They are byte loops and shift-subtract division. They are slow: in a hot loop, copy and divide in 32-bit words.
A hand-written program
You can also write the machine code yourself: assemble it, link it with the same linker script, and pack the ELF. This program adds two private inputs and publishes the sum.
add.S:
# Reads private inputs 0 and 1, writes their sum to output slot 0, halts.
.section .text._start
.globl _start
_start:
li a7, 2 # READ_INPUT
li a0, 0
ecall # a0 = input 0
mv t0, a0
li a7, 2 # READ_INPUT
li a0, 1
ecall # a0 = input 1
add t1, t0, a0
li a7, 1 # WRITE_OUTPUT slot, word
li a0, 0
mv a1, t1
ecall
li a7, 0 # HALT
ecall
Set each syscall number with a li a7, n directly before its ecall. The checker can then verify the number.
Assemble with the pinned clang, link with rust-lld and guest-sdk/guest.ld, then pack. Run from inside the circuits checkout:
CLANG=/opt/homebrew/opt/llvm/bin/clang
LLD=$(rustc +1.98.1 --print sysroot)/lib/rustlib/$(rustc +1.98.1 -vV | sed -n 's/host: //p')/bin/rust-lld
$CLANG --no-default-config --target=riscv32-unknown-none-elf -march=rv32im -mabi=ilp32 -mno-relax -c add.S -o add.o
$LLD -flavor gnu -T guest-sdk/guest.ld add.o -o add.elf
$ rand-guest pack add.elf --out add.bin
wrote add.bin (80 bytes) and add.bin.sha256
$ rand-guest check add.bin
14 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
$ rand-guest run add.bin --input 40 --input 2
out[0] = 42
out[1] = 0
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 14
tier 10
$ rand-guest info add.bin
form: image container
text 14 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 14 words from base_pc 0x1000
hc 42986ac1f93396d7de75f6757149523e7f1e9c5999d748af9b1915ada8ee14bd
program id 3b34c9d16adbd81a4849148675a914d10e5da0bd5b6aa64967e5f8a5fc75d941
14 words against a cap of 4096: fits
pack takes the ELF’s .text as the text segment and every other allocated PROGBITS section as one data segment. .bss is left out, because RAM starts at zero.
Packing the container by hand
The image container is six header words and then the words themselves (The RISC-V ISA). You can write one without any toolchain. This Python script writes the same 80 bytes that pack wrote above:
import struct
text = [
0x00200893, # li a7, 2 READ_INPUT
0x00000513, # li a0, 0
0x00000073, # ecall a0 = input 0
0x00050293, # mv t0, a0
0x00200893, # li a7, 2 READ_INPUT
0x00100513, # li a0, 1
0x00000073, # ecall a0 = input 1
0x00a28333, # add t1, t0, a0
0x00100893, # li a7, 1 WRITE_OUTPUT
0x00000513, # li a0, 0 slot 0
0x00030593, # mv a1, t1 word
0x00000073, # ecall
0x00000893, # li a7, 0 HALT
0x00000073, # ecall
]
# magic ("RAND") version text_base n_text data_base n_data
header = [0x444E4152, 1, 0x1000, len(text), 0, 0]
with open("add-hand.bin", "wb") as f:
f.write(struct.pack(f"<{len(header) + len(text)}I", *header, *text))
rand-guest info add-hand.bin prints the same hc 42986ac1…14bd, and cmp add-hand.bin add.bin finds no difference. Always run rand-guest check on a hand-packed image: the node refuses any word that does not decode, but only at deploy time.
The wallet also deploys two older forms: a .json file { "base_pc", "words" }, and a raw .bin of little-endian words loaded at base 0. The container is the only form with a data segment.
Deploy and call
Every path above ends the same way. With a rand wallet from fullnode v0.3 or later (on chain 21, the live chain since 2026-10-12, the v0.8.0 rand: an older wallet cannot send there, because chain 21 runs constraint set 9, whose proofs an older wallet cannot make, and, like chain 20, binds every proof and signature to its genesis hash; see Deploy a Rust program):
rand program deploy add.bin
rand call <program id> --input 40 --input 2
The deploy fee is 0.001 RAND + 0.0001 RAND × words: 0.0024 RAND for add.bin’s 14 words and 0.0035 RAND for c-fib.bin’s 25. Both are under 4,096 words, so they fit any chain’s cap: the default 4,096 and chain 21’s 65,535. The call proves locally at tier 10. On chain 15 it cost 0.002 RAND. Since chain 20 the fee is no longer 0.001 + call_fee(tier): the floor is BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, rand call prints gas: <exact> (declaring <limit>, tier <t>) and the fee before it proves, and four calls measured on chain 18 cost 0.0057 to 0.0075 RAND each (Example programs). Every deploy and call since chain 20 also carries two bundle-side proofs, a tier-10 spend authorisation and the tier-14 bundle. Deploy a Rust program describes what each command does.
To let others check what you deployed, publish the source and your circuits commit (aeacf31 for chains 18 to 20). With clang 23.1.1 and Rust 1.98.1, rand-guest build gives the same hc on any machine.