Rand Protocol
Dokumentacja
Programy RISC-V

Transpiler Solana

sbpf2rv tłumaczy program Solana sBPF na RISC-V ISA Rand - czym jest, model zaufania i jego zabezpieczenie ELF, co powoduje pułapkę, a co jest odrzucane, zmierzone wartości oraz pełny przykład, który tłumaczy rzeczywisty program SPL Token i zgadza się z interpreterem sBPF słowo w słowo.

sbpf2rv bierze skompilowany program Solana (ELF sBPF, czyli plik .so, który tworzy cargo build-sbf) i zamienia go na program Rand. Generuje kod w C, a rand-guest build kompiluje ten kod do obrazu RV32IM. Obraz publikuje te same osiem słów wyjściowych co interpreter sBPF w Rand, czyli guest sbpf.bin, dla tego samego ELF i tej samej instrukcji. Ta strona pokazuje całą ścieżkę na prawdziwym programie SPL Token.

Czym jest

wejścieELF sBPF v1
wyjściecrate z powłoką w C; rand-guest build zamienia go w kontener obrazu
gdzie działapoza łańcuchem, na maszynie programisty; łańcuch widzi tylko obraz
z czym musi się zgadzaćz ośmioma słowami wyjściowymi sbpf.bin, na tych samych dwóch taśmach wejściowych
repozytoriumsbpf2rv/ w repozytorium circuits
przypięty toolchainRust 1.98.1, clang 23.1.1, crate cc 1.4.6

Przetłumaczony program zachowuje ABI interpretera. Czyta te same taśmy: ELF z taśmy publicznej oraz zserializowaną instrukcję (konta, potem dane instrukcji) z taśmy prywatnej. Uruchamia ten sam szkielet sbpf-core wokół programu: dekoduje wejście, ładuje ELF, sprawdza region kont oraz haszuje wejście i wyjście. Zastępowane jest tylko samo wykonanie sBPF, które przejmuje kod natywny. Dlatego jego wyjścia są równe wyjściom interpretera słowo w słowo.

Model zaufania

Dowód dla przetłumaczonego programu opierają na trzech powiązaniach.

  1. hc wiąże przetłumaczony kod. Kod w C jest kompilowany do obrazu, więc hc obrazu go obejmuje.
  2. Straż ELF wiąże hc z jednym programem. Powłoka ładuje program z ELF na taśmie, więc bez sprawdzenia jeden obraz mógłby uruchomić inny, wybrany przez wywołującego ELF pod tym samym hc. Dlatego wygenerowany program.c kończy się skrótem załadowanego programu, z którego powstał: tekstu, danych tylko do odczytu, ich adresów i punktu wejścia. Zanim cokolwiek uruchomi, powłoka haszuje w ten sam sposób (Poseidon2) program załadowany z taśmy i porównuje wyniki. Każdy inny program kończy się zatrzymaniem BadElf, status 2, nad stanem wstępnym. Dowód, który weryfikuje się względem tego hc, jest więc przebiegiem tego programu.
  3. H_PUB wiąże bajty ELF. ELF jedzie na taśmie publicznej, a skrót wejść publicznych dowodu obejmuje go słowo w słowo.

To, czy hc jest wierną translacją danego ELF, sprawdza się przez odbudowanie. Uruchom przypięty sbpf2rv na ELF, zbuduj przypiętym toolchainem i porównaj hc. Właśnie dlatego build jest powtarzalny.

Łańcuch nie zapisuje źródłowego ELF ani jego skrótu. Przechowuje obraz i jego identyfikator programu. Wdrażający, który chce, by ELF był znany, publikuje go obok programu.

Straż wiąże załadowany program, a nie plik. Dwa ELF, które różnią się tylko tam, gdzie loader nie zagląda (dopełnienie nagłówka, tablice symboli), ładują się do tego samego programu i po obu stronach działają identycznie. ELF ze zmienionym jednym bajtem .rodata, nawet takim, którego transfer nigdy nie czyta, jest odrzucany przez translację i uruchamiany przez interpreter.

Co kończy się pułapką, a co jest odrzucane

Podczas translacji nic nie jest odrzucane. Wadliwy ELF jest odrzucany z własnym komunikatem loadera, ale żadna instrukcja w poprawnym ELF nie jest. Skaner zgłasza jako ostrzeżenia to, na co program mógłby natrafić, a przetłumaczony program zatrzymuje się pułapką w czasie działania dokładnie tam, gdzie interpreter:

ostrzeżenieczym jestw czasie działania
UnknownSyscallskrót wywołania systemowego, którego środowisko nie implementujepułapka UnknownSyscall
Cpiwywołanie między programami (sol_invoke_signed*)ta sama pułapka UnknownSyscall; CPI nie jest zaimplementowane
RegisterOutOfRangerejestr r11..r15 albo rejestr callx powyżej r10pułapka BadInsn
UnknownOpcodekod operacji bez klasy w v1, w tym rodzina sdiv/srem obecna tylko w v2 oraz hor64pułapka BadInsn
JumpOutOfTextskok, wywołanie lub przejście dalej poza tekst funkcjipułapka BadJump
BadCallImmSrccall imm z src w 2..=10 (tylko w ELF budowanych ręcznie)pułapka BadInsn

Translator, który odrzucałby kod nieosiągany, odrzucałby programy, które interpreter uruchamia dziś. SPL Token jest takim programem: wywołuje sol_set_return_data i sol_get_sysvar, do których ścieżka Transfer nigdy nie dociera.

Akceptowane są trzy rozbieżności. Każda z nich sprawia tylko, że translacja zatrzymuje się tam, gdzie interpreter mógłby się nie zatrzymać; translacja nigdy nie kończy przebiegu, który interpreter zatrzymuje.

  1. callx spoza przeskanowanego zbioru kończy się pułapką BadJump, podczas gdy interpreter może uruchomić pod tym adresem prawdziwy kod.
  2. Kontrole budżetu mogą być odroczone o jeden blok podstawowy. Aby zmieścić SPL Token poniżej limitu 65 535 słów, 1505 z jego 3546 bloków pomija własną kontrolę limitu instrukcji. Rodzaj zatrzymania może się różnić wewnątrz tego jednego bloku, który przekracza limit. Status i osiem słów są takie same.
  3. Działa tylko program źródłowy. Inny ELF jest odrzucany (straż, wyżej).

Ed25519 i secp256k1. Programowe implementacje sol_ed25519_verify i sol_secp256k1_recover istnieją w sbpf-rt/, ale nie są dołączane. Interpreter zatrzymuje się pułapką na obu, więc dla zgodności translacja też. Jedno wywołanie kosztuje odpowiednio około 14,3 mln i 15,7 mln cykli, czyli znacznie ponad największy poziom (2²⁰). Wymagają tablic koprocesora.

Liczby

Zmierzone na SPL Token (guests-compiled/sbpf/programs/spl_token.so, własny fixture testowy interpretera), ze strażą ELF na miejscu.

obrazsłowa programulimithc
SPL Token, przetłumaczony65 09665 535 (439 zapasu)8ca905ae…758b
sbpf.bin, interpreter831765 535059a7086…f476
wektorstatusinstrukcje sBPFcykle przetłumaczonegocykle sbpf.binpoziom (oba)
Transfer 2501143765 851694 49820
Transfer więcej niż saldo0133748 836679 81420
MintTo 2501120708 648634 42320
MintTo przez błędny podmiot uprawniony0134696 694627 24120
Burn 2501131710 245637 03820
Burn więcej niż saldo0121695 434624 78120
Transfer z dwoma kontami069645 015569 65620
zbyt wiele kont (szkielet odrzuca)20303 933304 61620

Na co idą cykle w Transfer:

etapprzetłumaczonysbpf.bin
odczyt obu taśm302 881302 880
załadowanie ELF177 012173 859
sprawdzenie regionu kont103 829103 824
wyzerowanie stosu i sterty sBPF49 18149 181
hasze wyjścia i wejścia, słowa publiczne, spoiwo51 09142 723
straż ELF72 949—
sam program sBPF890822 031
razem765 851694 498

Własne wykonanie przetłumaczonego programu jest od 2,4 do 2,5 raza tańsze na instrukcję sBPF (około 62 cykli wobec 154). Jednak około 98% każdego przebiegu to stały szkielet, a straż dodaje 73 tys. cykli. Dlatego dla SPL Token przetłumaczony obraz jest od 69 tys. do 75 tys. cykli droższy, a oba pozostają na poziomie 20.

Translacja się opłaca tylko w programach o dużym zapotrzebowaniu na obliczenia. Przy oszczędności około 90 cykli na instrukcję wywołanie wykonujące 10 000 instrukcji sBPF oszczędza około 0,9 mln cykli. SPL Token wykonuje ich od 120 do 143.

Przykład krok po kroku: SPL Token, przetłumaczony i dopasowany

Każde polecenie uruchamia się z katalogu głównego checkoutu circuits na 7ef3220, z toolchainem z Deploy a Rust program i clang 23.1.1 z Deploy a RISC-V program. Długie wyjście jest ucięte w miejscach oznaczonych ….

0. Zbuduj narzędzia

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

1. Przetłumacz

--out musi być katalogiem wewnątrz checkoutu 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). Cztery ostrzeżenia dotyczą prawdziwych wywołań systemowych, do których Transfer nigdy nie dociera: skrót 2720453611 to sol_set_return_data (trzy miejsca wywołania), a 331461893 to sol_get_sysvar.

2. Zbuduj obraz

Obraz przekracza domyślny limit 4096 słów, więc podaj --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

Każdy, kto wykona te dwa kroki z tym samym toolchainem, otrzyma ten sam hc.

3. Uruchom oba i porównaj

Transfer 250 przyjmuje dwie listy słów, zbudowane przez tests/parity.rs na podstawie własnego kodowania wywołań interpretera: 27 151 słów publicznych (ELF zakodowany w słowach) i 10 458 słów prywatnych (konta, potem dyskryminator Transfer i kwota). rand-guest run przyjmuje je jako --public i --input. Skrócone do pierwszych słów każdej listy:

$ 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

Te same dwie listy na guescie interpretera:

$ 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

Wszystkie osiem słów jest identycznych. out[0] = 1 to status (sukces). out[1..8] to skrót, który ABI interpretera definiuje nad wejściem i wyjściem.

4. Sprawdź wszystkie osiem wektorów jednym poleceniem

Test zgodności buduje przetłumaczony obraz, przepuszcza każdy wektor przez rand-guest run na obu obrazach i kończy się błędem przy jakiejkolwiek różnicy w ośmiu słowach:

$ 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. Wdróż

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

Obraz odczytuje plik ELF z publicznej taśmy, więc ELF jest publicznym wejściem programu, ustalonym przy wdrożeniu. --public przyjmuje sam plik .so; portfel koduje go na słowa w te same 27,151 słów co w kroku 4.

Wymaga to łańcucha, którego genesis ustawia max_program_words na co najmniej 65,096 oraz max_program_public_words na co najmniej 27,151. Chain 12 ograniczał wdrożenie do 4,096 słów i nie przyjmował publicznego wejścia, więc portfel odrzucił tam ten obraz jeszcze przed wygenerowaniem dowodu. Chain 13, łańcuch v0.4, ustawił 65,535 i 32,768, a każdy kolejny łańcuch zachowuje obie wartości. Chain 20, aktualny łańcuch (genesis 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135), zachowuje także limity bloku i koperty wywołania z chain 13 oraz zmniejsza o połowę limit dowodu do 4 MiB (4,194,304 bajtów, od chain 17; chain 13 do 16 ustawiały 8 MiB). Limity rozmiaru nie są już jedyną bramką. Genesis chain 20 ustawia hardening_v6, a przy nim wdrożenie większej liczby słów programu, niż może pomieścić wywołanie przy limicie wywołania poziomu 14, jest nieprawidłowe (ProgramUncallable): co najwyżej 8,180 słów bez publicznego wejścia, a przy publicznym wejściu mniej. Ten obraz ma 65,096 słów obok 27,151-słowowego publicznego wejścia, więc chain 20 odrzuca wdrożenie, a odrzuciłby też wywołanie: każdy wektor SPL działa na poziomie 20, powyżej limitu wywołania poziomu 14 (Limity). Agregacja bloków jest na chain 20 wyłączona (jego genesis nie ma sekcji aggregation); jego węzły startowe są wymienione w Uruchamianie walidatora. Opłata za wdrożenie liczy słowa publiczne razem ze słowami kodu: 0.001 + 0.0001 × (65,096 + 27,151) = 9.2257 RAND, czyli opłata, jaką zapłaciło poniższe wdrożenie na chain 13.

To wdrożenie odbyło się na chain 13 dnia 2026-09-19, gdy chain 13 był aktualnym łańcuchem (produkcyjny profil FRI, genesis 8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3), z portfela genesis shielded-1. Każdy kolejny łańcuch (14 do 19) został utworzony jako nowy łańcuch bez programów chain 13, a obraz nie został na żadnym z nich wdrożony ponownie, więc poniższa transakcja i wysokość należą do chain 13 i nie są już dostępne w eksploratorze. Identyfikator programu jest skrótem wyłącznie z base_pc, słów i słów publicznych, więc ten sam obraz i ten sam ELF otrzymują ten sam identyfikator w każdym łańcuchu, który dopuszcza wdrożenie. Wynik portfela, z pominiętymi liniami postępu (…):

$ 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
…
identyfikator programu740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a
obraz65,096 słów; hc 8ca905ae… w zapisie rand-guest, ae05a98c… w zapisie portfela
publiczne wejście27,151 słów (ELF), skrót ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e
pakiet opłaty (pakiet 2-in-2-out z chain 13)poziom 14, 1,418,406 bajtów, dowód w 94.3 s
transakcja wdrożenia (chain 13)626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530
zakotwiczona na wysokości (chain 13)400
opłata9.2257 RAND

Od chain 14 pakiet płacący to jeden pakiet 4-in-4-out z ukrytym aktywem, który niesie każda transakcja, dowodzony względem powiązania (binding) transakcji. Nadal jest to poziom 14: około 100 s i około 1.42 MB przy profilu produkcyjnym na chain 14 i 15. Od chain 17 pakiet niesie także dowód uwierzytelnienia poziomu 10 (podzielona autoryzacja), więc na chain 18 i 19 pakiet opłaty transakcji to około 1.49 MB dowodu pakietu i około 1.37 MB dowodu uwierzytelnienia.

Identyfikator programu wiąże ELF, więc różni się od identyfikatora, który rand-guest info wypisuje dla samego obrazu. Użyj identyfikatora wypisanego przez deploy.

Limity

  • Żadne wywołanie programu nie zostało udowodnione, program nie jest wdrożony na chain 20, a chain 20 odrzuca zarówno wdrożenie, jak i wywołanie. Powyższe wdrożenie odbyło się na chain 13. Przy hardening_v6 na chain 20 wdrożenie większej liczby słów, niż może pomieścić wywołanie poziomu 14, jest nieprawidłowe (ProgramUncallable; co najwyżej 8,180 słów), a obraz ma 65,096. Strona wejścia by się zmieściła: ELF jest publicznym wejściem wdrożenia, które rand call pobiera samo, a 10,458 słów prywatnych mieści się w limicie wejścia 16,071, który chain 20 zachowuje. Ale każdy wektor SPL jest na poziomie 20, a od fullnode v0.5.6 (2026-09-25) węzeł odrzuca każdy dowód wywołania powyżej poziomu 14 (CallTierTooHigh), zanim zbuduje klucz weryfikacyjny. Ta sama kontrola ogranicza tablice haszujące, które wywołanie może zadeklarować: 2¹³ wierszy SHA-256 (128 kompresji) i 2¹² wierszy Keccak (128 permutacji). Wywołanie poziomu 20 można udowodnić tylko poza łańcuchem, a dowód poziomu 20 jeszcze nie istnieje (następny punkt).
  • Nie wykonano jeszcze żadnego dowodu na poziomie 20. Dowód poziomu 20 został zabity na laptopie z 48 GB przy około 31 GB, a na dropletcie z 64 GB został zabity przez OOM przy 65.1 GB po 10 m 41 s. Ekstrapolacja z zmierzonego skalowania poziomów 16/18 (pamięć około ×3.9, czas około ×4.1 na każde +2 poziomy) daje dla poziomu 20 około 330 GB i około 3.6 h — więcej niż największy droplet pamięciowy DigitalOcean (m-32vcpu-256gb, 256 GB). Prover działał jednowątkowo: 16-vCPU maszyna wykorzystywała 99% jednego rdzenia. Zobacz pamięć dowodzenia.
  • Brak CPI. Wywołania międzyprogramowe kończą się pułapką (trap).
  • Tylko sBPF v1. Kody operacji wyłącznie v2 kończą się pułapką (trap).
  • Wywołania systemowe Ed25519 i secp256k1 kończą się pułapką (trap), dopóki nie będą miały tablic koprocesora.
  • Łańcuch zapisuje ELF wyłącznie jako publiczne wejście. Wdrożenie z --public zapisuje słowa ELF i ich skrót, a rand_getProgramPublic je udostępnia. Łańcuch nie zapisuje, jak obraz został z niego zbudowany: opublikuj dane wejściowe kompilacji, aby inni mogli odtworzyć hc.

Ostatnio zweryfikowano względem commitu fullnode c0878c3f (łańcuch 21) dnia 2026-10-12, a takż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. Jeśli kod i ta strona się różnią, rację ma kod; prosimy o zgłoszenie.