Cualquier programa que sea código máquina RV32IM puede desplegarse, sea cual sea su origen. Esta página muestra dos caminos: C mediante rand-guest build --lang c, y un programa que usted escribe en ensamblador y empaqueta por su cuenta. Ambos terminan en el mismo contenedor de imagen que produce Desplegar un programa en Rust, y ambos se despliegan con rand program deploy. Configure primero el checkout de circuits y rand-guest, como en el paso 1 de esa página. Para las cadenas 18 a 20, el checkout está en el commit aeacf31, que fullnode v0.6.7 y v0.6.8 fijan ambos. Las salidas que se muestran a continuación se produjeron el 2026-09-19 en 7ef3220 y no se han vuelto a ejecutar. El directorio rand-guest, incluidos guest.h y rt.c, es el mismo en ambos commits.
C mediante clang
Qué necesita
| herramienta | versión | cómo |
|---|---|---|
| clang | 23.1.1, con un backend RISC-V | brew install llvm en macOS; el clang del sistema de Apple no tiene backend RISC-V. O defina $CLANG con un binario 23.1.1 |
rust-lld | de Rust 1.98.1 | rustup +1.98.1 component add llvm-tools |
rand-guest rechaza cualquier clang cuya versión no sea 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 compila con otro, con una advertencia de que hc no coincidirá con las imágenes publicadas.
Escribir el guest
Un directorio de guest en C contiene solo archivos .c (y un .ld opcional). rand-guest aporta guest.h, _start y un pequeño runtime. No añada su propio guest.h ni start.S: build --lang c rechaza un directorio que tenga cualquiera de los dos.
Este es el archivo guests-compiled/c-fib/fib.c confirmado en el repositorio:
#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 declara rand_read_input, rand_read_public, rand_write_output, rand_poseidon2, rand_keccak, rand_sha256_compress y rand_halt. No tiene wrapper para la syscall 7, el POSEIDON2_LEN con límite de longitud que la máquina ejecuta desde el conjunto de restricciones 7, y rand-guest check en aeacf31 rechaza un ecall con a7 = 7 (zkp-circuits issue #1). Por tanto, un guest en C o en ensamblador para la cadena 20 usa las syscalls 0 a 6.
Compilar, ejecutar, inspeccionar
$ 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, la misma respuesta que el guest fib en Rust.
build --lang c compila todos los archivos .c del directorio (en orden alfabético), además de los start.S y rt.c incluidos, con:
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
y los enlaza con rust-lld -flavor gnu --gc-sections -T <ld> de la toolchain de Rust fijada. --gc-sections conserva solo lo que el guest llama.
El runtime de C
-ffreestanding -fno-builtin impiden que clang reconozca funciones de biblioteca, pero no que emita llamadas a ellas. Una copia de struct se convierte en memcpy, un inicializador de ceros grande se convierte en memset, y una división de 64 bits por un valor en tiempo de ejecución se convierte en una llamada a una función auxiliar, porque RV32IM solo divide valores de 32 bits. rt.c define estas ocho:
| función | se usa para |
|---|---|
memcpy, memmove, memset, memcmp | copias, rellenos, comparaciones |
__udivdi3, __umoddi3, __divdi3, __moddi3 | / y % de 64 bits |
Son bucles de bytes y división por desplazamiento y resta. Son lentas: en un bucle caliente, copie y divida en palabras de 32 bits.
Un programa escrito a mano
También puede escribir usted mismo el código máquina: ensamblarlo, enlazarlo con el mismo linker script y empaquetar el ELF. Este programa suma dos entradas privadas y publica la suma.
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
Defina cada número de syscall con un li a7, n justo antes de su ecall. Así el verificador de comprobación puede validar el número.
Ensamble con el clang fijado, enlace con rust-lld y guest-sdk/guest.ld, y luego empaquete. Ejecute desde dentro del checkout de circuits:
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 toma el .text del ELF como segmento de texto y cada una de las demás secciones PROGBITS asignadas como un único segmento de datos. .bss se omite, porque la RAM empieza en cero.
Empaquetar el contenedor a mano
El contenedor de imagen consta de seis palabras de cabecera y después las palabras mismas (La ISA RISC-V). Puede escribir uno sin ninguna toolchain. Este script de Python escribe los mismos 80 bytes que escribió pack más arriba:
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 imprime el mismo hc 42986ac1…14bd, y cmp add-hand.bin add.bin no encuentra ninguna diferencia. Ejecute siempre rand-guest check sobre una imagen empaquetada a mano: el nodo rechaza cualquier palabra que no se decodifique, pero solo en el momento del despliegue.
La wallet también despliega dos formas más antiguas: un archivo .json { "base_pc", "words" } y un .bin sin formato de palabras little-endian cargadas en la base 0. El contenedor es la única forma con un segmento de datos.
Desplegar y llamar
Todos los caminos anteriores terminan igual. Con una wallet rand de fullnode v0.3 o posterior (en la cadena 20, la cadena activa, v0.6.8 o posterior: una wallet v0.6.7 no puede enviar allí, porque la cadena 20 vincula cada prueba y cada firma a su hash de génesis; véase Desplegar un programa en Rust):
rand program deploy add.bin
rand call <program id> --input 40 --input 2
La comisión de despliegue es 0.001 RAND + 0.0001 RAND × words: 0.0024 RAND para las 14 palabras de add.bin y 0.0035 RAND para las 25 de c-fib.bin. Ambos están por debajo de 4,096 palabras, de modo que caben en el límite de cualquier cadena: el predeterminado de 4,096 y el de 65,535 de la cadena 20. La llamada se prueba localmente en el nivel 10. En la cadena 15 costó 0.002 RAND. En la cadena 20 la comisión ya no es 0.001 + call_fee(tier): el mínimo es BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, rand call imprime gas: <exact> (declaring <limit>, tier <t>) y la comisión antes de generar la prueba, y cuatro llamadas medidas en la cadena 18 costaron de 0.0057 a 0.0075 RAND cada una (Programas de ejemplo). Cada despliegue y cada llamada en la cadena 20 llevan además dos pruebas del lado del bundle: una autorización de gasto de nivel 10 y el bundle de nivel 14. Desplegar un programa en Rust describe lo que hace cada comando.
Para que otros puedan comprobar lo que usted desplegó, publique el código fuente y su commit de circuits (aeacf31 para las cadenas 18 a 20). Con clang 23.1.1 y Rust 1.98.1, rand-guest build da el mismo hc en cualquier máquina.