Rand Protocol
Documentación
Programas RISC-V

Desplegar un programa en Rust

Paso a paso, desde un crate de Rust no_std hasta un programa confidencial desplegado: instale la cadena de herramientas fijada, escriba el guest, compílelo y verifíquelo con rand-guest, ejecútelo en el emulador, despliéguelo y llámelo.

This page takes a small Rust program from source to a deployed program on Rand. Every rand-guest command below was run, and its output is pasted, not summarised. The runs were made on 2026-09-19 at circuits commit 7ef3220. The testnet is now chain 20, whose toolchain, like chains 18 and 19’s, is built at aeacf31 (fullnode v0.6.8 pins the same commit as v0.6.7), and the outputs have not been re-run there. La ISA RISC-V explica la máquina, las llamadas al sistema y el formato de archivo.

1. Instalar la cadena de herramientas

hc depende del compilador exacto, por lo que la cadena de herramientas está fijada.

herramientaversiónmotivo
Rust1.98.1compila el guest y el propio rand-guest
el target riscv32im-unknown-none-elfpara 1.98.1el target del guest
un checkout de circuitsrandprotocol/zkp-circuits en aeacf31 para las cadenas 18 a 20contiene rand-guest, guest-sdk y el script del enlazador
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31                      # branch feat/v067-circuits, what fullnode v0.6.7 and v0.6.8 pin
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"

rand-guest tiene cinco subcomandos: build, check, pack, run e info.

El commit importa porque hc es una función del código fuente y de la cadena de herramientas: un guest recompilado contra otro commit de guest-sdk puede obtener un hc distinto. Entre 7ef3220 y aeacf31 el directorio rand-guest no cambió, y guest-sdk/src/lib.rs sí (paso 2).

2. Escribir el guest

Un guest es un crate binario no_std, no_main que depende de guest-sdk. Debe estar dentro del checkout de circuits: rand-guest build localiza el checkout buscando guest-sdk/ por encima del directorio del guest, y rechaza un guest que esté fuera de uno.

Este guest lee un umbral, un recuento y esa cantidad de saldos privados. Publica 1 y el excedente si los saldos cubren el umbral. Nunca publica los saldos.

circuits/
  guest-sdk/
  rand-guest/
  my-guests/
    threshold/
      Cargo.toml
      src/main.rs

my-guests/threshold/Cargo.toml:

[package]
name = "threshold"
version = "0.1.0"
edition = "2021"

[dependencies]
guest-sdk = { path = "../../guest-sdk" }

[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1

[workspace]

El [workspace] vacío mantiene el crate fuera de cualquier workspace superior, como hace el guest fib incluido en el repositorio.

my-guests/threshold/src/main.rs:

#![no_std]
#![no_main]

use guest_sdk::{halt, read_input, write_output};

/// Input 0 is a threshold, input 1 a count n, inputs 2..2+n are private balances.
/// Output 0 is 1 if the balances sum to at least the threshold, else 0.
/// Output 1 is the surplus when they do. The balances themselves are never published.
#[no_mangle]
pub extern "C" fn main() -> ! {
    let threshold = read_input(0);
    let n = read_input(1);
    let mut sum: u32 = 0;
    for i in 0..n {
        sum = sum.wrapping_add(read_input(2 + i));
    }
    if sum >= threshold {
        write_output(0, 1);
        write_output(1, sum - threshold);
    }
    halt();
}

guest-sdk exporta read_input, read_public, write_output, poseidon2, keccak, keccak256, sha256_compress, sha256 y halt, y suministra _start, que fija el puntero de pila y llama a main. No hay asignador de memoria ni punto flotante.

En aeacf31, tres de esas firmas difieren de las de 7ef3220, y hay una exportación nueva:

funciónen 7ef3220en aeacf31
keccakfn keccak(ptr: *mut u32)fn keccak(state: &mut [u32; 50])
sha256_compressfn sha256_compress(ptr: *mut u32)fn sha256_compress(buf: &mut [u32; 24])
poseidon2fn poseidon2(ptr: *mut u32, n: usize)unsafe fn poseidon2(ptr: *mut u32, n: usize)
poseidon2_lenno existefn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8], syscall 7

poseidon2_len incorpora la longitud del mensaje a la esponja y es la llamada segura. rand-guest check en aeacf31 sigue listando solo las syscalls 0 a 6, de modo que un guest que la invoque se compila en una imagen que build rechaza con ecall with a7 = 7, which is no syscall the machine implements (incidencia #1 de zkp-circuits). Hasta que se corrija, un guest calcula hashes con unsafe { poseidon2(ptr, n) } sobre mensajes de longitud fija y etiquetados. El guest de esta página no usa ninguna de las cuatro.

Reglas para el directorio del guest:

  • Ningún .cargo/config ni .cargo/config.toml en él, en ningún directorio superior ni en $CARGO_HOME. build se niega a continuar y nombra cada archivo, porque cargo lo fusionaría en la compilación.
  • Ningún indicador de compilador propio. build los establece todos: el script del enlazador, -unaligned-scalar-mem y un remapeo de rutas que hace que la imagen sea independiente de la ubicación del checkout.
  • Como máximo un archivo .ld. build usa el único .ld del directorio del guest, o guest-sdk/guest.ld si no hay ninguno. Si hay más de uno, indíquelo con --ld.

3. Compilar y comprobar

$ rand-guest build my-guests/threshold --out threshold.bin
   Compiling guest-sdk v0.1.0 (…/circuits/guest-sdk)
warning: unstable feature specified for `-Ctarget-feature`: `unaligned-scalar-mem`
  |
  = note: this feature is not stably supported; its behavior can change in the future

warning: `guest-sdk` (lib) generated 1 warning
   Compiling threshold v0.1.0 (…/circuits/my-guests/threshold)
warning: `threshold` (bin "threshold") generated 1 warning (1 duplicate)
    Finished `release` profile [optimized] target(s) in 0.15s
40 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote threshold.bin and its .sha256 (40 words, hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665, program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853)

La advertencia unaligned-scalar-mem es esperada: la cadena de herramientas desactiva a propósito el acceso no alineado.

build compila con cargo +1.98.1 build --release --target riscv32im-unknown-none-elf, ejecuta check sobre el resultado y escribe el contenedor de la imagen y un archivo de fijación .sha256. Si check rechaza el programa, build escribe igualmente la imagen y termina con un código distinto de cero.

indicadorvalor por defectosignificado
--out <path><dir>/image.bindónde se escribe la imagen
--max-words <n>4096el límite con el que check mide; indique el límite de la cadena de destino (65,535 en la cadena 20)
--ld <path>el único .ld del directorio; si no, guest-sdk/guest.ldel script del enlazador
--lang rust|crustel compilador

4. Ejecutarlo en el emulador

Pase las entradas privadas con --input, una palabra cada una. --tier t también indica si la ejecución cabe en el nivel t.

$ rand-guest run threshold.bin --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75 --tier 10
out[0] = 1
out[1] = 25
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 52
tier 10
tier 10: fits (cycles 66 of 1023, Poseidon2 permutations 14 of 128)

Los saldos suman 1,025, así que el programa publica 1 y un excedente de 25. Con saldos de 400 y 250, la suma no alcanza y todas las salidas son cero. Una trampa imprime trap: <error> y termina con código 2.

5. Inspeccionar la imagen

$ rand-guest info threshold.bin
form: image container
text 40 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 40 words from base_pc 0x1000
hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665
program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
40 words against a cap of 4096: fits

hc es aquello contra lo que se verifica la prueba de una llamada. El identificador de programa es con el que se despliega y se llama.

6. Desplegar

Use la cartera rand de fullnode v0.3 o posterior; las carteras anteriores no leen el contenedor de la imagen. Desde la cadena 14, cada prueba de paquete se verifica contra el binding de su transacción, así que una cartera anterior a la compilación de la cadena 14 (b3c594c) genera la prueba de un paquete que la cadena rechaza. En la cadena 20, la cadena activa, use el rand de fullnode v0.6.8 o posterior (la flota ejecuta v0.6.8, y un rand compilado en Linux se adjunta a su versión en GitHub). Una cartera v0.6.7 no puede enviar en la cadena 20: el génesis de la cadena 20 establece binding_domain: 1, de modo que cada binding de prueba y cada mensaje firmado incluye el hash del génesis, y una cartera v0.6.7 enlaza solo el identificador de cadena. El punto de acceso público https://rpc.randprotocol.org basta para sincronizar la cartera, desplegar y llamar (configuración).

rand fee deploy 40               # the floor for a 40-word program
rand program deploy threshold.bin

program deploy imprime el identificador de programa, el número de palabras y hc. A continuación pregunta al nodo por la comisión de despliegue con rand_estimateFee, que rechaza un programa que supere el límite de la cadena antes de cualquier generación de pruebas. Después genera la prueba de un paquete de autotransferencia de valor cero que paga la comisión, lo envía y espera la confirmación. El paquete es el paquete de activos ocultos de 4 entradas y 4 salidas que lleva toda transacción. Desde la cadena 17 son dos pruebas, y la cartera imprime una línea por cada una: una autorización de gasto de nivel 10 (segundos, unos 1.37 MB) y el paquete de nivel 14 (unos dos minutos en un portátil, unos 1.5 MB). En la cadena 15 era una única prueba de nivel 14 de unos 100 s y unos 1.42 MB. Una votación anónima muestra un despliegue en la cadena 18 tal como lo imprimió la cartera.

La comisión de despliegue es 0.001 RAND + 0.0001 RAND × words, en la cadena 20 como antes. El gas fija el precio de las llamadas, no de los despliegues:

programapalabrascomisión de despliegue
threshold400.005 RAND
un programa en el límite por defecto (cadena 12)4,0960.4106 RAND
el programa más grande que la cadena 20 despliega sin segmento público8,1800.819 RAND
un programa en el límite de 65,535 palabras (cadenas 13 a 15)65,5356.5545 RAND

threshold ocupa menos de 4,096 palabras, así que cabe en el límite por defecto y en el de 65,535 de la cadena 20. Un programa de más de 4,096 palabras necesita una cadena cuyo génesis establezca max_program_words al menos en ese valor, como la cadena 20 (La ISA RISC-V). La cadena 20, como la 19, tiene un segundo límite inferior: se niega a desplegar un programa que ninguna llamada de nivel 14 podría contener (ProgramUncallable), es decir, más de 8,180 palabras sin segmento público y menos con uno. rand program deploy --input <word> … (o --check-call para una llamada sin entradas) ejecuta primero en el emulador la llamada que tiene en mente y se niega a desplegar a menos que quede en el nivel 14 o por debajo.

Compruebe el registro:

rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853

Su code_hash es el hc que imprimió rand-guest, escrito de otra forma. rand-guest imprime cada una de las ocho palabras del resumen en big-endian, y la cartera, rand program show y rand_getProgram imprimen los bytes de cada palabra en little-endian. Así, a491df99…3665 aparece como 99df91a4…707c: cada grupo de ocho dígitos hexadecimales está invertido byte a byte. Ambos escriben igual el identificador de programa.

7. Llamarlo

rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
    --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75

La cartera obtiene el código, ejecuta la llamada en el emulador e imprime gas: <exact> (declaring <limit>, tier <t>) y la comisión antes de generar ninguna prueba. Después sella la transcripción de entrada, genera la prueba de la ejecución localmente, paga la comisión mediante un paquete e imprime el recibo. En la cadena 20 son tres pruebas: la llamada, la autorización de gasto de nivel 10 y el paquete de nivel 14.

La tarifa en la cadena 20 ya no es 0.001 + call_fee(tier), que en la cadena 15 hacía que esta llamada costara 0.002 RAND. El mínimo es BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉: 0.001 RAND, más el límite declarado al precio del gas (100 unidades en génesis, siendo una unidad 10⁻⁹ RAND), más los bytes de la prueba y del sobre al precio por byte (800 unidades por KiB en génesis). En la cadena 20 los dos precios no pueden subir por encima de 10,000 y 80,000 unidades. El gas exacto permanece privado. Por defecto, la billetera lo declara redondeado hacia arriba a un múltiplo de 2^(tier − 2), fija el precio de la prueba en el tope de 4,194,304 bytes de la cadena porque la tarifa se fija antes de que exista la prueba, y añade un margen de dos escalones de precio. Cuatro llamadas medidas en la cadena 18 costaron entre 0.0057 y 0.0075 RAND cada una (Example programs). Esta llamada no se ha realizado en las cadenas 18 a 20, por lo que no se da ninguna cifra.

La cadena ve el tier, el límite de gas declarado, H_IN y las ocho palabras de salida [1, 25, 0, 0, 0, 0, 0, 0]. Nunca ve los saldos.

Programs trata los recibos, los sobres de entrada y rand open-call. Una Call como esta no tiene estado; en la cadena 20 un programa también puede mantener celdas y una bóveda mediante una Invoke (Program state (RPL-2)).

Deja que otros comprueben tu hc

La compilación es reproducible. build limpia el entorno del compilador (RUSTFLAGS, las variables CARGO_* que alteran la configuración, los envoltorios de RUSTC y más), rechaza los archivos de configuración de cargo y reasigna la ruta del checkout a /rand-circuits. El mismo código fuente con Rust 1.98.1 da el mismo hc en cualquier máquina. Para que otros confirmen lo que desplegaste, publica el código fuente del guest y tu commit de circuits (aeacf31 para un guest compilado para las cadenas 18 a 20). Cualquiera podrá entonces recompilar y comparar hc con el code_hash de rand_getProgram. La cadena almacena solo las palabras, no el código fuente.

Solución de problemas

mensajecausa
… is not inside a circuits checkout (no guest-sdk/ above it)mueve el directorio del guest dentro del checkout
build nombra un .cargo/config.tomlbórralo o muévelo; no se puede limpiar
REJECTED con Capel programa supera --max-words; súbelo al tope de la cadena de destino o reduce el programa (opt-level = "s")
REJECTED con Syscall y a7 = 7el guest llama a poseidon2_len; rand-guest check en aeacf31 aún no conoce la syscall 7 (zkp-circuits issue #1)
REJECTED con Fence, Csr o Ebreakla compilación emitió una instrucción que la máquina no tiene; consulta lo que se rechaza
trap: … de runun fallo en tiempo de ejecución, como una dirección desalineada o un número de syscall no implementado
run indica que la ejecución no cabe en ningún tierla ejecución supera 2²⁰ filas o su presupuesto de Poseidon2; no se puede probar

Verificado por última vez con el commit c0878c3f del fullnode (cadena 21) el 2026-10-12, y circuits `7ef3220` (`rand-guest/README.md`, `guest-sdk/`, `guests-compiled/fib`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`; `git diff 7ef3220 aeacf31 -- guest-sdk rand-guest`), read on 2026-09-30; no output on this page was re-run at that commit, fullnode tag v0.6.8 `c9c9bd3c` (`docs/deploy.md` on `binding_domain`, `.github/workflows/ci.yml` circuits pin) and `rand_getLimits` on chain 20, read on 2026-10-01; nothing on this page was deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/zkvm.md` §8a) and `rand_getLimits` on chain 21, read on 2026-10-12; nothing on this page was deployed or called on chain 21. Cuando el código y esta página no coinciden, el código tiene razón; avísenos.