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ście | ELF sBPF v1 |
| wyjście | crate z powłoką w C; rand-guest build zamienia go w kontener obrazu |
| gdzie działa | poza ł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 |
| repozytorium | sbpf2rv/ w repozytorium circuits |
| przypięty toolchain | Rust 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.
hcwiąże przetłumaczony kod. Kod w C jest kompilowany do obrazu, więchcobrazu go obejmuje.- Straż ELF wiąże
hcz 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 samymhc. Dlatego wygenerowanyprogram.ckoń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ę zatrzymaniemBadElf, status 2, nad stanem wstępnym. Dowód, który weryfikuje się względem tegohc, jest więc przebiegiem tego programu. H_PUBwiąż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żenie | czym jest | w czasie działania |
|---|---|---|
UnknownSyscall | skrót wywołania systemowego, którego środowisko nie implementuje | pułapka UnknownSyscall |
Cpi | wywołanie między programami (sol_invoke_signed*) | ta sama pułapka UnknownSyscall; CPI nie jest zaimplementowane |
RegisterOutOfRange | rejestr r11..r15 albo rejestr callx powyżej r10 | pułapka BadInsn |
UnknownOpcode | kod operacji bez klasy w v1, w tym rodzina sdiv/srem obecna tylko w v2 oraz hor64 | pułapka BadInsn |
JumpOutOfText | skok, wywołanie lub przejście dalej poza tekst funkcji | pułapka BadJump |
BadCallImmSrc | call 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.
callxspoza przeskanowanego zbioru kończy się pułapkąBadJump, podczas gdy interpreter może uruchomić pod tym adresem prawdziwy kod.- 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.
- 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.
| obraz | słowa programu | limit | hc |
|---|---|---|---|
| SPL Token, przetłumaczony | 65 096 | 65 535 (439 zapasu) | 8ca905ae…758b |
sbpf.bin, interpreter | 8317 | 65 535 | 059a7086…f476 |
| wektor | status | instrukcje sBPF | cykle przetłumaczonego | cykle sbpf.bin | poziom (oba) |
|---|---|---|---|---|---|
Transfer 250 | 1 | 143 | 765 851 | 694 498 | 20 |
Transfer więcej niż saldo | 0 | 133 | 748 836 | 679 814 | 20 |
MintTo 250 | 1 | 120 | 708 648 | 634 423 | 20 |
MintTo przez błędny podmiot uprawniony | 0 | 134 | 696 694 | 627 241 | 20 |
Burn 250 | 1 | 131 | 710 245 | 637 038 | 20 |
Burn więcej niż saldo | 0 | 121 | 695 434 | 624 781 | 20 |
Transfer z dwoma kontami | 0 | 69 | 645 015 | 569 656 | 20 |
| zbyt wiele kont (szkielet odrzuca) | 2 | 0 | 303 933 | 304 616 | 20 |
Na co idą cykle w Transfer:
| etap | przetłumaczony | sbpf.bin |
|---|---|---|
| odczyt obu taśm | 302 881 | 302 880 |
| załadowanie ELF | 177 012 | 173 859 |
| sprawdzenie regionu kont | 103 829 | 103 824 |
| wyzerowanie stosu i sterty sBPF | 49 181 | 49 181 |
| hasze wyjścia i wejścia, słowa publiczne, spoiwo | 51 091 | 42 723 |
| straż ELF | 72 949 | — |
| sam program sBPF | 8908 | 22 031 |
| razem | 765 851 | 694 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 programu | 740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a |
| obraz | 65,096 słów; hc 8ca905ae… w zapisie rand-guest, ae05a98c… w zapisie portfela |
| publiczne wejście | 27,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łata | 9.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_v6na 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órerand callpobiera 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
--publiczapisuje słowa ELF i ich skrót, arand_getProgramPublicje udostępnia. Łańcuch nie zapisuje, jak obraz został z niego zbudowany: opublikuj dane wejściowe kompilacji, aby inni mogli odtworzyćhc.