Rand Protocol
Documentação
Programas RISC-V

O transpilador para Solana

O sbpf2rv traduz um programa sBPF da Solana para a ISA RISC-V da Rand: o que é, o modelo de confiança e sua proteção de ELF, o que gera trap e o que é recusado, os números medidos e um passo a passo completo que traduz o programa SPL Token real e coincide palavra por palavra com o interpretador sBPF.

sbpf2rv pega um programa Solana compilado (um ELF sBPF, o .so que cargo build-sbf produz) e o transforma em um programa Rand. Ele escreve C, e rand-guest build compila esse C em uma imagem RV32IM. A imagem publica as mesmas oito palavras de saída que o guest interpretador sBPF da Rand, sbpf.bin, para o mesmo ELF e a mesma instrução. Esta página mostra o caminho completo no programa SPL Token real.

O que é

entradaum ELF sBPF v1
saídaum crate C shim; rand-guest build o transforma em um contêiner de imagem
onde executafora da cadeia, na máquina do desenvolvedor; a cadeia só vê a imagem
o que deve corresponderas oito palavras de saída de sbpf.bin, nas mesmas duas fitas de entrada
repositóriosbpf2rv/ no repositório circuits
toolchain fixadaRust 1.98.1, clang 23.1.1, o crate cc 1.4.6

O programa traduzido mantém a ABI do interpretador. Ele lê as mesmas fitas: o ELF na fita pública e a instrução serializada (contas, depois dados da instrução) na fita privada. Ele executa o mesmo harness sbpf-core em torno do programa: decodifica a entrada, carrega o ELF, verifica a região de contas e calcula o hash da entrada e da saída. Apenas a execução sBPF em si é substituída por código nativo. É por isso que suas saídas são iguais às do interpretador, palavra por palavra.

Modelo de confiança

Três elementos vinculam uma prova de um programa traduzido.

  1. hc vincula o código traduzido. O C é compilado na imagem, portanto o hc da imagem o cobre.
  2. A guarda do ELF vincula hc a um único programa. O shim carrega o programa a partir do ELF na fita; sem uma verificação, uma mesma imagem poderia executar outro ELF, escolhido pelo chamador, sob o mesmo hc. Por isso o program.c gerado termina com um digest do programa carregado a partir do qual foi traduzido: texto, dados somente leitura, seus endereços e o ponto de entrada. Antes de executar qualquer coisa, o shim calcula o hash do programa carregado da fita da mesma forma (Poseidon2) e compara. Qualquer outro programa para com BadElf, status 2, sobre o estado anterior. Assim, uma prova que verifica contra este hc é uma execução desse programa.
  3. H_PUB vincula os bytes do ELF. O ELF trafega na fita pública, e o digest de entradas públicas da prova o cobre palavra por palavra.

Se hc é a tradução fiel de um dado ELF, verifica-se reconstruindo. Execute o sbpf2rv fixado sobre o ELF, compile com a toolchain fixada e compare hc. A compilação é reprodutível por essa razão.

A cadeia não registra o ELF de origem nem o seu hash. Ela armazena a imagem e o seu id de programa. Um implantador que queira tornar o ELF conhecido o publica junto ao programa.

A guarda vincula o programa carregado, não o arquivo. Dois ELFs que diferem apenas onde o carregador não olha (preenchimento de cabeçalho, tabelas de símbolos) carregam o mesmo programa e executam de forma idêntica nos dois lados. Um ELF com um byte de .rodata alterado, mesmo que a transferência nunca o leia, é recusado pela tradução e executado pelo interpretador.

O que gera trap e o que é recusado

Nada é recusado na tradução. Um ELF malformado é recusado com a mensagem do próprio carregador, mas nenhuma instrução de um ELF bem formado o é. O scanner relata como avisos o que um programa poderia atingir, e o programa traduzido gera trap em tempo de execução exatamente onde o interpretador o faria:

avisoo que éem tempo de execução
UnknownSyscallum hash de syscall que o runtime não implementatrap UnknownSyscall
Cpiuma invocação entre programas (sol_invoke_signed*)o mesmo trap UnknownSyscall; CPI não está implementada
RegisterOutOfRangeum registrador r11..r15, ou um registrador de callx acima de r10trap BadInsn
UnknownOpcodeum opcode sem classe v1, incluindo a família sdiv/srem exclusiva da v2 e hor64trap BadInsn
JumpOutOfTextum salto, chamada ou continuação sequencial fora do texto da funçãotrap BadJump
BadCallImmSrcum call imm com src em 2..=10 (somente em ELFs construídos à mão)trap BadInsn

Um tradutor que recusasse código não alcançado recusaria programas que o interpretador executa hoje. O SPL Token é um desses programas: ele chama sol_set_return_data e sol_get_sysvar, que o caminho de Transfer nunca alcança.

Três divergências são aceitas. Cada uma apenas faz a tradução parar onde o interpretador poderia não parar; a tradução nunca conclui uma execução que o interpretador interrompe.

  1. Um callx fora do conjunto examinado gera trap BadJump, onde o interpretador pode executar código real naquele endereço.
  2. As verificações de orçamento podem ser adiadas por um bloco básico. Para caber o SPL Token sob o limite de 65.535 palavras, 1.505 dos seus 3.546 blocos pulam a própria verificação do limite de instruções. O tipo de parada pode diferir dentro do único bloco que cruza o limite. O status e as oito palavras são os mesmos.
  3. Apenas o programa de origem executa. Outro ELF é recusado (a guarda, acima).

Ed25519 e secp256k1. Implementações em software de sol_ed25519_verify e sol_secp256k1_recover existem em sbpf-rt/, mas não estão vinculadas. O interpretador gera trap em ambas, então, por paridade, a tradução também gera. Cada chamada custa cerca de 14,3 M e 15,7 M de ciclos, respectivamente, muito acima do maior nível (2²⁰). Elas precisam de tabelas de coprocessador.

Os números

Medido no SPL Token (guests-compiled/sbpf/programs/spl_token.so, o próprio fixture de teste do interpretador), com a guarda do ELF ativa.

imagempalavras de programalimitehc
SPL Token, traduzido65.09665.535 (439 de folga)8ca905ae…758b
sbpf.bin, o interpretador8.31765.535059a7086…f476
vetorstatusinstruções sBPFciclos traduzidosciclos de sbpf.binnível (ambos)
Transfer 2501143765.851694.49820
Transfer mais que o saldo0133748.836679.81420
MintTo 2501120708.648634.42320
MintTo pela autoridade errada0134696.694627.24120
Burn 2501131710.245637.03820
Burn mais que o saldo0121695.434624.78120
Transfer com duas contas069645.015569.65620
contas demais (o harness recusa)20303.933304.61620

Para onde vão os ciclos no Transfer:

etapatraduzidosbpf.bin
ler as duas fitas302.881302.880
carregar o ELF177.012173.859
verificar a região de contas103.829103.824
zerar a pilha e o heap sBPF49.18149.181
hashes de saída e de entrada, palavras públicas, cola51.09142.723
a guarda do ELF72.949—
o programa sBPF em si8.90822.031
total765.851694.498

A execução do próprio programa traduzido é cerca de 2,4 a 2,5 vezes mais barata por instrução sBPF (cerca de 62 ciclos contra 154). Mas cerca de 98% de cada execução é o harness fixo, e a guarda acrescenta 73 mil ciclos. Assim, para o SPL Token a imagem traduzida é de 69 mil a 75 mil ciclos mais cara, e ambas permanecem no nível 20.

A tradução compensa apenas para programas de computação intensiva. Com cerca de 90 ciclos economizados por instrução, uma chamada que executa 10.000 instruções sBPF economiza cerca de 0,9 M de ciclos. O SPL Token executa de 120 a 143.

Passo a passo: SPL Token, traduzido e conferido

Todos os comandos são executados a partir da raiz de um checkout de circuits em 7ef3220, com a toolchain de Implantar um programa Rust e o clang 23.1.1 de Implantar um programa RISC-V. A saída longa é cortada onde marcada com ….

0. Compile as ferramentas

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

1. Traduza

--out deve ser um diretório dentro do checkout 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). Os quatro avisos são syscalls reais que Transfer nunca alcança: o hash 2720453611 é sol_set_return_data (três pontos de chamada) e 331461893 é sol_get_sysvar.

2. Compile a imagem

A imagem excede o limite padrão de 4.096 palavras, então passe --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

Quem executar estes dois passos com a mesma toolchain obtém este hc.

3. Execute ambos e compare

O Transfer de 250 usa duas listas de palavras, construídas por tests/parity.rs a partir da própria codificação de chamada do interpretador: 27.151 palavras públicas (o ELF, codificado em palavras) e 10.458 palavras privadas (as contas, depois o discriminante e o valor do Transfer). rand-guest run as recebe como --public e --input. Abreviadas às primeiras palavras 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

As mesmas duas listas contra o guest interpretador:

$ 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

As oito palavras são idênticas. out[0] = 1 é o status (sucesso). out[1..8] são o digest que a ABI do interpretador define sobre a entrada e a saída.

4. Confira os oito vetores em um único comando

O teste de paridade compila a imagem traduzida, executa cada vetor por rand-guest run nas duas imagens e falha em qualquer diferença nas oito palavras:

$ 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. Implante

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

A imagem lê o ELF da fita pública, de modo que o ELF é a entrada pública do programa, fixada no deploy. --public recebe o próprio arquivo .so; a carteira o codifica em palavras, no mesmo total de 27.151 palavras do passo 4.

Isso exige uma chain cujo genesis defina max_program_words em pelo menos 65.096 e max_program_public_words em pelo menos 27.151. A chain 12 limitava um deploy a 4.096 palavras e não aceitava entrada pública, de modo que a carteira recusou esta imagem ali, antes de gerar qualquer prova. A chain 13, a chain da v0.4, definiu 65.535 e 32.768, e todas as chains seguintes mantêm ambos os valores. A chain 20, a chain em operação (genesis 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135), mantém também os limites de bloco e de envelope de chamada da chain 13 e reduz pela metade o teto da prova para 4 MiB (4.194.304 bytes, desde a chain 17; as chains 13 a 16 definiam 8 MiB). Os tetos de tamanho deixaram de ser o único critério. O genesis da chain 20 define hardening_v6, e sob ele é inválido (ProgramUncallable) um deploy com mais palavras de programa do que uma chamada comporta no teto de chamada do tier 14: no máximo 8.180 palavras sem entrada pública, e menos ao lado de uma. Esta imagem tem 65.096 palavras ao lado de uma entrada pública de 27.151 palavras; portanto, a chain 20 recusa o deploy e recusaria também uma chamada: todo vetor SPL roda no tier 20, acima do teto de chamada do tier 14 (Limites). A agregação de blocos está desativada na chain 20 (o genesis não tem seção aggregation); seus nós de bootstrap estão listados em Running a validator. A taxa de deploy conta as palavras públicas junto com as palavras de código: 0.001 + 0.0001 × (65,096 + 27,151) = 9,2257 RAND, a taxa que o deploy na chain 13 abaixo pagou.

Este deploy foi executado na chain 13 em 2026-09-19, quando a chain 13 era a chain em operação (perfil FRI de produção, genesis 8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3), a partir da carteira genesis shielded-1. Todas as chains seguintes (14 a 19) foram criadas como chains novas, sem os programas da chain 13, e a imagem não foi reimplantada em nenhuma delas; por isso a transação e a altura abaixo são as da chain 13 e já não são resolvidas no explorer. O id do programa é um hash apenas de base_pc, das palavras e das palavras públicas, de modo que a mesma imagem e o mesmo ELF recebem o mesmo id em qualquer chain que admita o deploy. A saída da carteira, com as linhas de progresso 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 do programa740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a
imagem65.096 palavras; hc 8ca905ae… na grafia de rand-guest, ae05a98c… na da carteira
entrada pública27.151 palavras (o ELF), digest ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e
bundle de taxa (bundle 2-in-2-out da chain 13)tier 14, 1.418.406 bytes, provado em 94,3 s
tx de deploy (chain 13)626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530
ancorada na altura (chain 13)400
taxa9,2257 RAND

Desde a chain 14, o bundle que paga é o único bundle de ativos ocultos 4-in-4-out que toda transação carrega, provado contra o binding da transação. Continua sendo tier 14: cerca de 100 s e cerca de 1,42 MB no perfil de produção nas chains 14 e 15. Desde a chain 17, um bundle também carrega uma prova de autenticação de tier 10 (autorização dividida); assim, nas chains 18 e 19, o bundle de taxa de uma transação soma cerca de 1,49 MB de prova do bundle e cerca de 1,37 MB de prova de autenticação.

O id do programa vincula o ELF, de modo que difere do id que rand-guest info imprime para a imagem pura. Use o id que o deploy imprimiu.

Limites

  • Nenhuma chamada ao programa foi provada, ele não está implantado na chain 20, e a chain 20 recusa tanto o deploy quanto a chamada. O deploy acima foi na chain 13. Sob o hardening_v6 da chain 20, um deploy com mais palavras do que uma chamada de tier 14 comporta é inválido (ProgramUncallable; 8.180 palavras no máximo), e a imagem tem 65.096. O lado da entrada caberia: o ELF é a entrada pública do deploy, que rand call busca por conta própria, e as 10.458 palavras privadas cabem no teto de entrada de 16.071, que a chain 20 mantém. Mas todo vetor SPL é de tier 20 e, desde o fullnode v0.5.6 (2026-09-25), um nó recusa qualquer prova de chamada acima do tier 14 (CallTierTooHigh), antes de montar uma verifier key. A mesma verificação limita as tabelas de hash que uma chamada pode declarar: 2¹³ linhas de SHA-256 (128 compressões) e 2¹² linhas de Keccak (128 permutações). Uma chamada de tier 20 só pode ser provada fora da chain, e ainda não existe nenhuma prova de tier 20 (próximo item).
  • Ainda não foi gerada nenhuma prova no tier 20. Uma prova de tier 20 foi encerrada em um laptop de 48 GB com cerca de 31 GB e morta por falta de memória (OOM) em um droplet de 64 GB com 65,1 GB, após 10 min 41 s. Extrapolando da escala medida nos tiers 16/18 (memória cerca de ×3,9, tempo cerca de ×4,1 a cada +2 tiers), o tier 20 precisa de cerca de 330 GB e cerca de 3,6 h, mais do que o maior droplet de memória da DigitalOcean (m-32vcpu-256gb, 256 GB). O prover rodou em uma única thread: uma máquina de 16 vCPUs ficou em 99% de um núcleo. Veja memória de prova.
  • Sem CPI. Invocações entre programas (cross-program invocations) causam trap.
  • Somente sBPF v1. Os opcodes exclusivos da v2 causam trap.
  • As syscalls de Ed25519 e secp256k1 causam trap até que tenham tabelas de coprocessador.
  • A chain registra o ELF apenas como entrada pública. Um deploy com --public armazena as palavras do ELF e o digest delas, e rand_getProgramPublic as fornece. A chain não registra como a imagem foi construída a partir dele: publique as entradas de build para que outros possam reconstruir o hc.

Última verificação em relação ao commit c0878c3f do fullnode (chain 21) em 2026-10-12, e 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. Onde o código e esta página divergirem, o código está certo; avise-nos.