Rand Protocol
Documentación
Programas RISC-V

El transpilador de Solana

sbpf2rv traduce un programa sBPF de Solana al ISA RISC-V de Rand: qué es, el modelo de confianza y su guardia de ELF, qué provoca una trampa y qué se rechaza, las cifras medidas, y un recorrido completo que traduce el programa real SPL Token y coincide palabra por palabra con el intérprete sBPF.

sbpf2rv toma un programa Solana compilado (un ELF de sBPF, el .so que produce cargo build-sbf) y lo convierte en un programa de Rand. Escribe C, y rand-guest build compila ese C en una imagen RV32IM. La imagen publica las mismas ocho palabras de salida que el guest intérprete de sBPF de Rand, sbpf.bin, para el mismo ELF y la misma instrucción. Esta página muestra todo el recorrido con el programa SPL Token real.

Qué es

entradaun ELF de sBPF v1
salidaun crate shim en C; rand-guest build lo convierte en un contenedor de imagen
dónde se ejecutafuera de la cadena, en la máquina del desarrollador; la cadena solo ve la imagen
qué debe coincidirlas ocho palabras de salida de sbpf.bin, con las mismas dos cintas de entrada
repositoriosbpf2rv/ en el repositorio de circuits
toolchain fijadoRust 1.98.1, clang 23.1.1, el crate cc 1.4.6

El programa traducido conserva la ABI del intérprete. Lee las mismas cintas: el ELF en la cinta pública y la instrucción serializada (cuentas, luego datos de la instrucción) en la cinta privada. Ejecuta alrededor del programa el mismo arnés sbpf-core: decodifica la entrada, carga el ELF, comprueba la región de cuentas y calcula el hash de la entrada y de la salida. Lo único que se sustituye por código nativo es la propia ejecución de sBPF. Por eso sus salidas coinciden con las del intérprete palabra por palabra.

Modelo de confianza

Tres elementos vinculan una prueba de un programa traducido.

  1. hc vincula el código traducido. El C se compila dentro de la imagen, de modo que el hc de la imagen lo cubre.
  2. La guarda del ELF vincula hc a un único programa. El shim carga el programa desde el ELF que hay en la cinta, así que, sin una comprobación, una misma imagen podría ejecutar bajo el mismo hc otro ELF elegido por quien la invoca. Por ello, el program.c generado termina con un digest del programa cargado a partir del cual se tradujo: texto, datos de solo lectura, sus direcciones y el punto de entrada. Antes de ejecutar nada, el shim calcula de la misma manera (Poseidon2) el hash del programa cargado desde la cinta y lo compara. Cualquier otro programa se detiene con BadElf, estado 2, sobre el estado previo. Así, una prueba que verifica contra este hc es una ejecución de ese programa.
  3. H_PUB vincula los bytes del ELF. El ELF viaja en la cinta pública, y el digest de entradas públicas de la prueba lo cubre palabra por palabra.

Que hc sea la traducción fiel de un ELF dado se comprueba reconstruyendo. Se ejecuta el sbpf2rv fijado sobre el ELF, se compila con el toolchain fijado y se compara hc. La compilación es reproducible por este motivo.

La cadena no registra el ELF de origen ni su hash. Almacena la imagen y su identificador de programa. Quien despliega y quiere que el ELF sea conocido lo publica junto al programa.

La guarda vincula el programa cargado, no el archivo. Dos ELF que difieren solo donde el cargador no mira (relleno de la cabecera, tablas de símbolos) se cargan como el mismo programa y se ejecutan de forma idéntica en ambos lados. Un ELF con un solo byte de .rodata modificado, incluso uno que la transferencia nunca lee, es rechazado por la traducción y ejecutado por el intérprete.

Qué provoca un trap y qué se rechaza

En la traducción no se rechaza nada. Un ELF mal formado se rechaza con el mensaje propio del cargador, pero ninguna instrucción de un ELF bien formado se rechaza. El escáner informa como advertencias de lo que un programa podría encontrar, y el programa traducido provoca un trap en tiempo de ejecución exactamente donde lo haría el intérprete:

advertenciaqué esen tiempo de ejecución
UnknownSyscallun hash de syscall que el runtime no implementatrap UnknownSyscall
Cpiuna invocación entre programas (sol_invoke_signed*)el mismo trap UnknownSyscall; CPI no está implementada
RegisterOutOfRangeun registro r11..r15, o un registro de callx superior a r10trap BadInsn
UnknownOpcodeun opcode sin clase en v1, incluida la familia sdiv/srem, exclusiva de v2, y hor64trap BadInsn
JumpOutOfTextun salto, llamada o continuación secuencial fuera del texto de la funcióntrap BadJump
BadCallImmSrcun call imm con src en 2..=10 (solo en ELF construidos a mano)trap BadInsn

Un traductor que rechazara el código no alcanzado rechazaría programas que el intérprete ejecuta hoy. SPL Token es uno de ellos: llama a sol_set_return_data y a sol_get_sysvar, que la ruta de Transfer nunca alcanza.

Se aceptan tres divergencias. Cada una solo hace que la traducción se detenga donde el intérprete quizá no lo haría; la traducción nunca completa una ejecución que el intérprete detiene.

  1. Un callx fuera del conjunto escaneado provoca BadJump, mientras que el intérprete puede ejecutar código real en esa dirección.
  2. Las comprobaciones de presupuesto pueden diferirse un bloque básico. Para que SPL Token quepa bajo el tope de 65.535 palabras, 1.505 de sus 3.546 bloques omiten su propia comprobación del límite de instrucciones. El tipo de detención puede diferir dentro del único bloque que cruza el límite. El estado y las ocho palabras son los mismos.
  3. Solo se ejecuta el programa de origen. Cualquier otro ELF es rechazado (la guarda, más arriba).

Ed25519 y secp256k1. Existen implementaciones por software de sol_ed25519_verify y sol_secp256k1_recover en sbpf-rt/, pero no están enlazadas. El intérprete provoca un trap en ambas, así que, por paridad, la traducción también. Cada llamada cuesta unos 14,3 M y 15,7 M de ciclos respectivamente, muy por encima del nivel más grande (2²⁰). Necesitan tablas de coprocesador.

Las cifras

Medidas sobre SPL Token (guests-compiled/sbpf/programs/spl_token.so, el propio fixture de pruebas del intérprete), con la guarda del ELF activa.

imagenpalabras de programatopehc
SPL Token, traducido65.09665.535 (439 libres)8ca905ae…758b
sbpf.bin, el intérprete8.31765.535059a7086…f476
vectorestadoinstrucciones sBPFciclos traducidociclos sbpf.binnivel (ambos)
Transfer 2501143765.851694.49820
Transfer más que el saldo0133748.836679.81420
MintTo 2501120708.648634.42320
MintTo con la autoridad equivocada0134696.694627.24120
Burn 2501131710.245637.03820
Burn más que el saldo0121695.434624.78120
Transfer con dos cuentas069645.015569.65620
demasiadas cuentas (el arnés rechaza)20303.933304.61620

Adónde van los ciclos en el Transfer:

etapatraducidosbpf.bin
leer ambas cintas302.881302.880
cargar el ELF177.012173.859
comprobar la región de cuentas103.829103.824
poner a cero la pila y el heap de sBPF49.18149.181
hashes de salida y de entrada, palabras públicas, código de enlace51.09142.723
la guarda del ELF72.949—
el programa sBPF en sí8.90822.031
total765.851694.498

La ejecución propia del programa traducido es entre 2,4 y 2,5 veces más barata por instrucción sBPF (unos 62 ciclos frente a 154). Pero cerca del 98 % de cada ejecución es el arnés fijo, y la guarda añade 73 k ciclos. Así que, para SPL Token, la imagen traducida es entre 69 k y 75 k ciclos más cara, y ambas se mantienen en el nivel 20.

La traducción solo compensa en programas con mucho cómputo. Con unos 90 ciclos ahorrados por instrucción, una llamada que ejecuta 10.000 instrucciones sBPF ahorra unos 0,9 M de ciclos. SPL Token ejecuta entre 120 y 143.

Recorrido: SPL Token, traducido y verificado

Todos los comandos se ejecutan desde la raíz de una copia de circuits en 7ef3220, con el toolchain de Deploy a Rust program y clang 23.1.1 de Deploy a RISC-V program. La salida larga se recorta donde aparece ….

0. Compilar las herramientas

(cd sbpf2rv && cargo +1.98.1 build --release)
(cd rand-guest && cargo +1.98.1 build --release)

1. Traducir

--out debe ser un directorio dentro de la copia de circuits.

$ sbpf2rv/target/release/sbpf2rv guests-compiled/sbpf/programs/spl_token.so --out sbpf2rv/target/spl-token --name spl-token
entry pc 225: 30 function(s), 3546 block(s), 12061 instruction(s), 30 callx target(s), 0 refusal(s), 4 warning(s)
  fn 225: 2065 block(s)
  …                                                (30 functions in all)
  warning: UnknownSyscall { pc: 7803, hash: 2720453611 }
  warning: UnknownSyscall { pc: 9530, hash: 2720453611 }
  warning: UnknownSyscall { pc: 8698, hash: 2720453611 }
  warning: UnknownSyscall { pc: 11547, hash: 331461893 }
wrote sbpf2rv/target/spl-token/program.c (521757 bytes of C) and the spl-token shim crate: rand-guest build sbpf2rv/target/spl-token

0 refusal(s). Las cuatro advertencias corresponden a syscalls reales que Transfer nunca alcanza: el hash 2720453611 es sol_set_return_data (tres puntos de llamada) y 331461893 es sol_get_sysvar.

2. Compilar la imagen

La imagen supera el tope predeterminado de 4.096 palabras, así que se pasa --max-words 65535.

$ rand-guest/target/release/rand-guest build sbpf2rv/target/spl-token --max-words 65535
…
65096 words against a cap of 65535 (fits); 2 ecall(s) with a non-static a7
OK
wrote sbpf2rv/target/spl-token/image.bin and its .sha256 (65096 words, hc 8ca905ae3c62f503de7de86829040f323e098b53dba5fffe09b42f1aad16758b, program id 65d234ce9ca4d082f487c038abfa76e67f41a0d91e10b9ac388f1e1541d59184)

$ rand-guest/target/release/rand-guest info sbpf2rv/target/spl-token/image.bin
form: image container
text 64819 words at 0x10000; data 102 words (101 non-zero) at 0x4f4cc; prologue 277 words; program 65096 words from base_pc 0xfbac
hc 8ca905ae3c62f503de7de86829040f323e098b53dba5fffe09b42f1aad16758b
program id 65d234ce9ca4d082f487c038abfa76e67f41a0d91e10b9ac388f1e1541d59184
65096 words against a cap of 4096: does not fit

Quien ejecute estos dos pasos con el mismo toolchain obtiene este hc.

3. Ejecutar ambos y comparar

El Transfer de 250 usa dos listas de palabras, construidas por tests/parity.rs a partir de la propia codificación de llamadas del intérprete: 27.151 palabras públicas (el ELF, codificado en palabras) y 10.458 palabras privadas (las cuentas, luego el discriminante de Transfer y el importe). rand-guest run las recibe como --public y --input. Abreviadas a las primeras palabras de cada lista:

$ rand-guest run sbpf2rv/target/spl-token/image.bin \
    --public 108600 1179403647 65794 0 0 17235971 1 2088 …   (27 151 words)
    --input  41825 4 0 65791 0 50529027 50529027 50529027 …  (10 458 words)
out[0] = 1
out[1] = 2892832079
out[2] = 376091303
out[3] = 1311040261
out[4] = 2015764099
out[5] = 3682593600
out[6] = 3311553006
out[7] = 141115266
cycles 765851
tier 20

Las mismas dos listas contra el guest intérprete:

$ rand-guest run guests-compiled/bin/sbpf.bin \
    --public 108600 1179403647 65794 0 0 17235971 1 2088 …   (27 151 words)
    --input  41825 4 0 65791 0 50529027 50529027 50529027 …  (10 458 words)
out[0] = 1
out[1] = 2892832079
out[2] = 376091303
out[3] = 1311040261
out[4] = 2015764099
out[5] = 3682593600
out[6] = 3311553006
out[7] = 141115266
cycles 694498
tier 20

Las ocho palabras son idénticas. out[0] = 1 es el estado (éxito). out[1..8] son el digest que la ABI del intérprete define sobre la entrada y la salida.

4. Comprobar los ocho vectores con un solo comando

La prueba de paridad compila la imagen traducida, ejecuta cada vector con rand-guest run en ambas imágenes y falla ante cualquier diferencia en las ocho palabras:

$ cd sbpf2rv && cargo +1.98.1 test --test parity the_spl_token -- --nocapture
…
translated SPL Token: 65096 program words (cap 65 535), hc 8ca905ae3c62f503de7de86829040f323e098b53dba5fffe09b42f1aad16758b
transfer 250: Ok(0), status 1; cycles translated 765851, interpreter guest 694498
transfer too much: Ok(1), status 0; cycles translated 748836, interpreter guest 679814
MintTo 250: Ok(0), status 1; cycles translated 708648, interpreter guest 634423
MintTo 250 signed by someone else: Ok(4), status 0; cycles translated 696694, interpreter guest 627241
Burn 250: Ok(0), status 1; cycles translated 710245, interpreter guest 637038
Burn 1 000 001 (balance 1 000 000): Ok(1), status 0; cycles translated 695434, interpreter guest 624781
transfer with two accounts: Ok(47244640256), status 0; cycles translated 645015, interpreter guest 569656
an account count above MAX_ACCOUNTS: Err(BadElf), status 2; cycles translated 303933, interpreter guest 304616
…
test the_spl_token_transfer_translates_and_matches_the_interpreter_word_for_word ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 27.84s

5. Desplegar

rand program deploy sbpf2rv/target/spl-token/image.bin --public spl_token.so

La imagen lee el ELF desde la cinta pública, por lo que el ELF es la entrada pública del programa, fijada en el despliegue. --public toma el propio archivo .so; la wallet lo codifica en palabras, en las mismas 27,151 palabras del paso 4.

Esto requiere una cadena cuyo génesis fije max_program_words en al menos 65,096 y max_program_public_words en al menos 27,151. La chain 12 limitaba un despliegue a 4,096 palabras y no admitía entrada pública, de modo que la wallet rechazó esta imagen allí antes de generar prueba alguna. La chain 13, la cadena de la v0.4, fijó 65,535 y 32,768, y todas las cadenas posteriores conservan ambos valores. La chain 20, la cadena en producción (génesis 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135), conserva además los límites de bloque y de sobre de llamada de la chain 13 y reduce a la mitad el límite de prueba, a 4 MiB (4,194,304 bytes, desde la chain 17; las chains 13 a 16 fijaron 8 MiB). Los límites de tamaño ya no son la única barrera. El génesis de la chain 20 activa hardening_v6, y bajo esa regla es inválido (ProgramUncallable) un despliegue con más palabras de programa de las que una llamada puede contener en el límite de llamada del tier 14: como máximo 8,180 palabras sin entrada pública, y menos si la hay. Esta imagen tiene 65,096 palabras junto a una entrada pública de 27,151 palabras, así que la chain 20 rechaza el despliegue, y rechazaría también una llamada: todos los vectores SPL se ejecutan en el tier 20, por encima del límite de llamada del tier 14 (Límites). La agregación de bloques está desactivada en la chain 20 (su génesis no tiene sección aggregation); sus nodos de arranque se enumeran en Ejecutar un validador. La comisión de despliegue cuenta las palabras públicas junto con las palabras de código: 0.001 + 0.0001 × (65,096 + 27,151) = 9.2257 RAND, la comisión que pagó el despliegue en la chain 13 que se muestra a continuación.

Este despliegue se realizó en la chain 13 el 2026-09-19, cuando la chain 13 era la cadena en producción (perfil FRI de producción, génesis 8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3), desde la wallet de génesis shielded-1. Todas las cadenas posteriores (14 a 19) se crearon como cadenas nuevas, sin los programas de la chain 13, y la imagen no se ha vuelto a desplegar en ninguna de ellas; por eso la transacción y la altura que figuran abajo son las de la chain 13 y ya no se resuelven en el explorador. El id del programa es un hash de base_pc, las palabras y las palabras públicas únicamente, de modo que la misma imagen y el mismo ELF obtienen el mismo id en cualquier cadena que admita el despliegue. La salida de la wallet, con sus líneas de progreso omitidas (…):

$ rand program deploy <image.bin> --public spl_token.so
program id: 740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a (65096 words, hc ae05a98c03f5623c68e87dde320f0429538b093efeffa5db1a2fb4098b7516ad, public input 27151 words, digest ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e)
…
proved in 94.3s: tier 14, 1418406 bytes
submitted deploy 626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530
…
id del programa740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a
imagen65,096 palabras; hc 8ca905ae… en la grafía de rand-guest, ae05a98c… en la de la wallet
entrada pública27,151 palabras (el ELF), digest ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e
paquete de comisión (paquete 2-in-2-out de la chain 13)tier 14, 1,418,406 bytes, probado en 94.3 s
tx de despliegue (chain 13)626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530
anclada en la altura (chain 13)400
comisión9.2257 RAND

Desde la chain 14, el paquete de pago es el paquete único de activos ocultos 4-in-4-out que lleva toda transacción, probado contra el binding de la transacción. Sigue siendo tier 14: unos 100 s y unos 1.42 MB con el perfil de producción en las chains 14 y 15. Desde la chain 17, un paquete lleva además una prueba de autenticación de tier 10 (autorización dividida), de modo que en las chains 18 y 19 el paquete de comisión de una transacción son unos 1.49 MB de prueba del paquete y unos 1.37 MB de prueba de autenticación.

El id del programa vincula el ELF, por lo que difiere del id que imprime rand-guest info para la imagen sin el ELF. Llame al id que imprimió deploy.

Límites

  • No se ha probado ninguna llamada al programa, no está desplegado en la chain 20, y la chain 20 rechaza tanto el despliegue como la llamada. El despliegue anterior se hizo en la chain 13. Bajo hardening_v6 de la chain 20, un despliegue de más palabras de las que cabe en una llamada de tier 14 es inválido (ProgramUncallable; 8,180 palabras como máximo), y la imagen tiene 65,096. El lado de la entrada sí cabría: el ELF es la entrada pública del despliegue, que rand call obtiene por sí mismo, y las 10,458 palabras privadas caben en el límite de entrada de 16,071, que la chain 20 conserva. Pero todos los vectores SPL son de tier 20, y desde fullnode v0.5.6 (2026-09-25) un nodo rechaza cualquier prueba de llamada por encima del tier 14 (CallTierTooHigh), antes de construir una clave de verificador. La misma comprobación limita las tablas hash que puede declarar una llamada: 2¹³ filas de SHA-256 (128 compresiones) y 2¹² filas de Keccak (128 permutaciones). Una llamada de tier 20 solo puede probarse fuera de la cadena, y todavía no existe ninguna prueba de tier 20 (siguiente punto).
  • Todavía no se ha generado ninguna prueba de tier 20. Una prueba de tier 20 fue terminada en un portátil de 48 GB con unos 31 GB, y el sistema la mató por falta de memoria (OOM) en un droplet de 64 GB con 65.1 GB tras 10 m 41 s. Extrapolando el escalado medido en los tiers 16/18 (memoria aproximadamente ×3.9, tiempo aproximadamente ×4.1 por cada +2 tiers), el tier 20 necesita unos 330 GB y unas 3.6 h, más que el droplet de mayor memoria de DigitalOcean (m-32vcpu-256gb, 256 GB). El probador se ejecutó con un solo hilo: una máquina de 16 vCPU se quedó al 99% de un núcleo. Véase memoria de prueba.
  • Sin CPI. Las invocaciones entre programas provocan un trap.
  • Solo sBPF v1. Los opcodes exclusivos de v2 provocan un trap.
  • Las syscalls de Ed25519 y secp256k1 provocan un trap hasta que tengan tablas de coprocesador.
  • La cadena registra el ELF solo como entrada pública. Un despliegue con --public almacena las palabras del ELF y su digest, y rand_getProgramPublic las sirve. La cadena no registra cómo se construyó la imagen a partir de él: publique las entradas de compilación para que otros puedan reconstruir hc.

Verificado por última vez con el commit c0878c3f del fullnode (cadena 21) el 2026-10-12, y circuits `7ef3220` (`sbpf2rv/README.md`, `docs/superpowers/specs/2026-09-18-sbpf-to-rv32-translator-design.md`); steps 1, 2 and the parity test re-run on 2026-09-19. Cuando el código y esta página no coinciden, el código tiene razón; avísenos.