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 é
| entrada | um ELF sBPF v1 |
| saída | um crate C shim; rand-guest build o transforma em um contêiner de imagem |
| onde executa | fora da cadeia, na máquina do desenvolvedor; a cadeia só vê a imagem |
| o que deve corresponder | as oito palavras de saída de sbpf.bin, nas mesmas duas fitas de entrada |
| repositório | sbpf2rv/ no repositório circuits |
| toolchain fixada | Rust 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.
hcvincula o código traduzido. O C é compilado na imagem, portanto ohcda imagem o cobre.- A guarda do ELF vincula
hca 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 mesmohc. Por isso oprogram.cgerado 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 comBadElf, status 2, sobre o estado anterior. Assim, uma prova que verifica contra estehcé uma execução desse programa. H_PUBvincula 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:
| aviso | o que é | em tempo de execução |
|---|---|---|
UnknownSyscall | um hash de syscall que o runtime não implementa | trap UnknownSyscall |
Cpi | uma invocação entre programas (sol_invoke_signed*) | o mesmo trap UnknownSyscall; CPI não está implementada |
RegisterOutOfRange | um registrador r11..r15, ou um registrador de callx acima de r10 | trap BadInsn |
UnknownOpcode | um opcode sem classe v1, incluindo a família sdiv/srem exclusiva da v2 e hor64 | trap BadInsn |
JumpOutOfText | um salto, chamada ou continuação sequencial fora do texto da função | trap BadJump |
BadCallImmSrc | um 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.
- Um
callxfora do conjunto examinado gera trapBadJump, onde o interpretador pode executar código real naquele endereço. - 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.
- 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.
| imagem | palavras de programa | limite | hc |
|---|---|---|---|
| SPL Token, traduzido | 65.096 | 65.535 (439 de folga) | 8ca905ae…758b |
sbpf.bin, o interpretador | 8.317 | 65.535 | 059a7086…f476 |
| vetor | status | instruções sBPF | ciclos traduzidos | ciclos de sbpf.bin | nível (ambos) |
|---|---|---|---|---|---|
Transfer 250 | 1 | 143 | 765.851 | 694.498 | 20 |
Transfer mais que o saldo | 0 | 133 | 748.836 | 679.814 | 20 |
MintTo 250 | 1 | 120 | 708.648 | 634.423 | 20 |
MintTo pela autoridade errada | 0 | 134 | 696.694 | 627.241 | 20 |
Burn 250 | 1 | 131 | 710.245 | 637.038 | 20 |
Burn mais que o saldo | 0 | 121 | 695.434 | 624.781 | 20 |
Transfer com duas contas | 0 | 69 | 645.015 | 569.656 | 20 |
| contas demais (o harness recusa) | 2 | 0 | 303.933 | 304.616 | 20 |
Para onde vão os ciclos no Transfer:
| etapa | traduzido | sbpf.bin |
|---|---|---|
| ler as duas fitas | 302.881 | 302.880 |
| carregar o ELF | 177.012 | 173.859 |
| verificar a região de contas | 103.829 | 103.824 |
| zerar a pilha e o heap sBPF | 49.181 | 49.181 |
| hashes de saída e de entrada, palavras públicas, cola | 51.091 | 42.723 |
| a guarda do ELF | 72.949 | — |
| o programa sBPF em si | 8.908 | 22.031 |
| total | 765.851 | 694.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 programa | 740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a |
| imagem | 65.096 palavras; hc 8ca905ae… na grafia de rand-guest, ae05a98c… na da carteira |
| entrada pública | 27.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 |
| taxa | 9,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_v6da 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, querand callbusca 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
--publicarmazena as palavras do ELF e o digest delas, erand_getProgramPublicas 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 ohc.