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.
| herramienta | versión | motivo |
|---|---|---|
| Rust | 1.98.1 | compila el guest y el propio rand-guest |
el target riscv32im-unknown-none-elf | para 1.98.1 | el target del guest |
| un checkout de circuits | randprotocol/zkp-circuits en aeacf31 para las cadenas 18 a 20 | contiene 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ón | en 7ef3220 | en aeacf31 |
|---|---|---|
keccak | fn keccak(ptr: *mut u32) | fn keccak(state: &mut [u32; 50]) |
sha256_compress | fn sha256_compress(ptr: *mut u32) | fn sha256_compress(buf: &mut [u32; 24]) |
poseidon2 | fn poseidon2(ptr: *mut u32, n: usize) | unsafe fn poseidon2(ptr: *mut u32, n: usize) |
poseidon2_len | no existe | fn 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/configni.cargo/config.tomlen él, en ningún directorio superior ni en$CARGO_HOME.buildse niega a continuar y nombra cada archivo, porque cargo lo fusionaría en la compilación. - Ningún indicador de compilador propio.
buildlos establece todos: el script del enlazador,-unaligned-scalar-memy un remapeo de rutas que hace que la imagen sea independiente de la ubicación del checkout. - Como máximo un archivo
.ld.buildusa el único.lddel directorio del guest, oguest-sdk/guest.ldsi 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.
| indicador | valor por defecto | significado |
|---|---|---|
--out <path> | <dir>/image.bin | dónde se escribe la imagen |
--max-words <n> | 4096 | el 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.ld | el script del enlazador |
--lang rust|c | rust | el 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:
| programa | palabras | comisión de despliegue |
|---|---|---|
threshold | 40 | 0.005 RAND |
| un programa en el límite por defecto (cadena 12) | 4,096 | 0.4106 RAND |
| el programa más grande que la cadena 20 despliega sin segmento público | 8,180 | 0.819 RAND |
| un programa en el límite de 65,535 palabras (cadenas 13 a 15) | 65,535 | 6.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
| mensaje | causa |
|---|---|
… is not inside a circuits checkout (no guest-sdk/ above it) | mueve el directorio del guest dentro del checkout |
build nombra un .cargo/config.toml | bórralo o muévelo; no se puede limpiar |
REJECTED con Cap | el 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 = 7 | el 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 Ebreak | la compilación emitió una instrucción que la máquina no tiene; consulta lo que se rechaza |
trap: … de run | un 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 tier | la ejecución supera 2²⁰ filas o su presupuesto de Poseidon2; no se puede probar |