Rand Protocol
Dokumentation
RISC-V-Programme

Der Solana-Transpiler

sbpf2rv übersetzt ein Solana-sBPF-Programm in die RISC-V ISA von Rand: was er ist, das Vertrauensmodell und sein ELF-Guard, was einen Trap auslöst und was abgelehnt wird, die gemessenen Zahlen sowie eine vollständige Schritt-für-Schritt-Anleitung, die das echte SPL-Token-Programm übersetzt und dem sBPF-Interpreter Wort für Wort entspricht.

sbpf2rv nimmt ein kompiliertes Solana-Programm (eine sBPF-ELF-Datei, die .so, die cargo build-sbf erzeugt) und wandelt es in ein Rand-Programm um. Es schreibt C, und rand-guest build kompiliert dieses C zu einem RV32IM-Image. Das Image veröffentlicht für dieselbe ELF-Datei und dieselbe Instruktion die gleichen acht Ausgabewörter wie der sBPF-Interpreter-Guest von Rand, sbpf.bin. Diese Seite zeigt den gesamten Weg am echten SPL-Token-Programm.

Was es ist

Eingabeeine sBPF-v1-ELF-Datei
Ausgabeein C-Shim-Crate; rand-guest build macht daraus einen Image-Container
Wo es läuftoff chain, auf dem Rechner des Entwicklers; die Chain sieht nur das Image
Was es erfüllen mussdie acht Ausgabewörter von sbpf.bin, auf denselben zwei Eingabebändern
Repositorysbpf2rv/ im circuits-Repository
Fixierte ToolchainRust 1.98.1, clang 23.1.1, die cc-Crate 1.4.6

Das übersetzte Programm behält die ABI des Interpreters bei. Es liest dieselben Bänder: die ELF-Datei auf dem öffentlichen Band und die serialisierte Instruktion (zuerst die Konten, dann die Instruktionsdaten) auf dem privaten Band. Es führt um das Programm herum denselben sbpf-core-Harness aus: Er dekodiert die Eingabe, lädt die ELF-Datei, prüft die Kontenregion und hasht Eingabe und Ausgabe. Nur die sBPF-Ausführung selbst wird durch nativen Code ersetzt. Deshalb stimmen seine Ausgaben Wort für Wort mit denen des Interpreters überein.

Vertrauensmodell

Drei Dinge binden einen Beweis eines übersetzten Programms.

  1. hc bindet den übersetzten Code. Das C wird in das Image kompiliert, daher deckt hc des Images es ab.
  2. Der ELF-Guard bindet hc an ein Programm. Der Shim lädt das Programm aus der ELF-Datei auf dem Band. Ohne Prüfung könnte also ein Image unter demselben hc eine andere, vom Aufrufer gewählte ELF-Datei ausführen. Die erzeugte program.c endet deshalb mit einem Digest des geladenen Programms, aus dem sie übersetzt wurde: Text, schreibgeschützte Daten, deren Adressen und der Einstiegspunkt. Bevor irgendetwas läuft, hasht der Shim das auf dem Band geladene Programm auf dieselbe Weise (Poseidon2) und vergleicht. Jedes andere Programm hält mit BadElf, Status 2, über dem Vorzustand an. Ein Beweis, der gegen dieses hc verifiziert, ist also ein Lauf genau dieses Programms.
  3. H_PUB bindet die Bytes der ELF-Datei. Die ELF-Datei läuft über das öffentliche Band, und der Digest der öffentlichen Eingaben des Beweises deckt sie Wort für Wort ab.

Ob hc die getreue Übersetzung einer gegebenen ELF-Datei ist, wird durch Neuaufbau geprüft. Führen Sie das fixierte sbpf2rv auf der ELF-Datei aus, bauen Sie mit der fixierten Toolchain und vergleichen Sie hc. Aus diesem Grund ist der Build reproduzierbar.

Die Chain speichert weder die Quell-ELF-Datei noch deren Hash. Sie speichert das Image und seine Programm-ID. Wer als Deployer die ELF-Datei bekannt machen will, veröffentlicht sie neben dem Programm.

Der Guard bindet das geladene Programm, nicht die Datei. Zwei ELF-Dateien, die sich nur dort unterscheiden, wo der Loader nicht hinsieht (Header-Padding, Symboltabellen), laden zum selben Programm und laufen auf beiden Seiten identisch. Eine ELF-Datei mit einem geänderten .rodata-Byte, selbst einem, das der Transfer nie liest, wird von der Übersetzung abgelehnt und vom Interpreter ausgeführt.

Was einen Trap auslöst und was abgelehnt wird

Bei der Übersetzung wird nichts abgelehnt. Eine fehlerhafte ELF-Datei wird mit der eigenen Meldung des Loaders abgelehnt, aber keine Instruktion in einer wohlgeformten ELF-Datei wird es. Der Scanner meldet als Warnungen, worauf ein Programm treffen könnte, und das übersetzte Programm löst zur Laufzeit genau dort einen Trap aus, wo es der Interpreter täte:

WarnungWas es istZur Laufzeit
UnknownSyscallein Syscall-Hash, den die Runtime nicht implementiertTrap UnknownSyscall
Cpiein programmübergreifender Aufruf (sol_invoke_signed*)derselbe Trap UnknownSyscall; CPI ist nicht implementiert
RegisterOutOfRangeein Register r11..r15 oder ein callx-Register über r10Trap BadInsn
UnknownOpcodeein Opcode ohne v1-Klasse, einschließlich der nur in v2 vorhandenen Familie sdiv/srem und hor64Trap BadInsn
JumpOutOfTextein Sprung, Aufruf oder Durchfall außerhalb des Textes der FunktionTrap BadJump
BadCallImmSrcein call imm mit src in 2..=10 (nur in handgebauten ELF-Dateien)Trap BadInsn

Ein Übersetzer, der nicht erreichten Code ablehnt, würde Programme ablehnen, die der Interpreter heute ausführt. SPL Token ist ein solches Programm: Es ruft sol_set_return_data und sol_get_sysvar auf, die der Transfer-Pfad nie erreicht.

Drei Abweichungen werden akzeptiert. Jede lässt die Übersetzung höchstens dort anhalten, wo der Interpreter es vielleicht nicht tut; die Übersetzung schließt nie einen Lauf ab, den der Interpreter anhält.

  1. Ein callx außerhalb der gescannten Menge löst BadJump aus, wo der Interpreter an dieser Adresse echten Code ausführen könnte.
  2. Budgetprüfungen können um einen Basisblock verzögert werden. Damit SPL Token unter die Obergrenze von 65.535 Wörtern passt, überspringen 1.505 seiner 3.546 Blöcke ihre eigene Prüfung des Instruktionslimits. Die Art des Anhaltens kann sich in dem einen Block unterscheiden, der das Limit überschreitet. Der Status und die acht Wörter sind gleich.
  3. Nur das Quellprogramm läuft. Jede andere ELF-Datei wird abgelehnt (der Guard, oben).

Ed25519 und secp256k1. Software-Implementierungen von sol_ed25519_verify und sol_secp256k1_recover existieren in sbpf-rt/, sind aber nicht eingebunden. Der Interpreter löst bei beiden einen Trap aus, daher löst zur Parität auch die Übersetzung einen aus. Jeder Aufruf kostet etwa 14,3 Mio. bzw. 15,7 Mio. Zyklen, weit über der größten Stufe (2²⁰). Sie brauchen Coprozessor-Tabellen.

Die Zahlen

Gemessen an SPL Token (guests-compiled/sbpf/programs/spl_token.so, dem eigenen Test-Fixture des Interpreters), mit aktivem ELF-Guard.

ImageProgrammwörterObergrenzehc
SPL Token, übersetzt65.09665.535 (439 frei)8ca905ae…758b
sbpf.bin, der Interpreter8.31765.535059a7086…f476
VektorStatussBPF-Instr.Zyklen übersetztZyklen sbpf.binStufe (beide)
Transfer 2501143765.851694.49820
Transfer mehr als das Guthaben0133748.836679.81420
MintTo 2501120708.648634.42320
MintTo durch die falsche Autorität0134696.694627.24120
Burn 2501131710.245637.03820
Burn mehr als das Guthaben0121695.434624.78120
Transfer mit zwei Konten069645.015569.65620
zu viele Konten (Harness lehnt ab)20303.933304.61620

Wohin die Zyklen gehen beim Transfer:

Phaseübersetztsbpf.bin
beide Bänder lesen302.881302.880
die ELF-Datei laden177.012173.859
die Kontenregion prüfen103.829103.824
sBPF-Stack und -Heap nullen49.18149.181
Ausgabe- und Eingabe-Hashes, öffentliche Wörter, Glue51.09142.723
der ELF-Guard72.949—
das sBPF-Programm selbst8.90822.031
Gesamt765.851694.498

Die eigene Ausführung des übersetzten Programms ist pro sBPF-Instruktion etwa 2,4- bis 2,5-mal günstiger (etwa 62 Zyklen gegenüber 154). Aber rund 98 % jedes Laufs entfallen auf den festen Harness, und der Guard fügt 73k Zyklen hinzu. Für SPL Token ist das übersetzte Image daher 69k bis 75k Zyklen teurer, und beide bleiben auf Stufe 20.

Die Übersetzung lohnt sich nur bei rechenintensiven Programmen. Bei etwa 90 eingesparten Zyklen pro Instruktion spart ein Aufruf, der 10.000 sBPF-Instruktionen ausführt, etwa 0,9 Mio. Zyklen. SPL Token führt 120 bis 143 aus.

Walkthrough: SPL Token, übersetzt und abgeglichen

Jeder Befehl läuft aus dem Stammverzeichnis eines circuits-Checkouts bei 7ef3220, mit der Toolchain aus Deploy a Rust program und clang 23.1.1 aus Deploy a RISC-V program. Lange Ausgaben sind dort gekürzt, wo … steht.

0. Build the tools

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

1. Translate

--out muss ein Verzeichnis innerhalb des circuits-Checkouts sein.

$ 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). Die vier Warnungen sind echte Syscalls, die Transfer nie erreicht: Hash 2720453611 ist sol_set_return_data (drei Aufrufstellen) und 331461893 ist sol_get_sysvar.

2. Build the image

Das Image liegt über der Standardobergrenze von 4.096 Wörtern, übergeben Sie daher --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

Jeder, der diese beiden Schritte mit derselben Toolchain ausführt, erhält dieses hc.

3. Run both, and compare

Der Transfer von 250 benötigt zwei Wortlisten, die tests/parity.rs aus der eigenen Aufrufkodierung des Interpreters aufbaut: 27.151 öffentliche Wörter (die ELF-Datei, wortkodiert) und 10.458 private Wörter (die Konten, dann der Transfer-Diskriminator und der Betrag). rand-guest run nimmt sie als --public und --input entgegen. Gekürzt auf die ersten Wörter jeder Liste:

$ 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

Dieselben zwei Listen gegen den Interpreter-Guest:

$ 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

Alle acht Wörter sind identisch. out[0] = 1 ist der Status (Erfolg). out[1..8] ist der Digest, den die ABI des Interpreters über Eingabe und Ausgabe definiert.

4. Check all eight vectors in one command

Der Paritätstest baut das übersetzte Image, führt jeden Vektor über rand-guest run auf beiden Images aus und schlägt bei jedem Unterschied in den acht Wörtern fehl:

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

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

Das Image liest die ELF vom öffentlichen Tape, die ELF ist also die öffentliche Eingabe des Programms und wird beim Deploy festgelegt. --public nimmt die .so-Datei selbst; die Wallet kodiert sie wortweise in dieselben 27.151 Wörter wie in Schritt 4.

Dafür ist eine Chain nötig, deren Genesis max_program_words auf mindestens 65.096 und max_program_public_words auf mindestens 27.151 setzt. Chain 12 begrenzte einen Deploy auf 4.096 Wörter und kannte keine öffentliche Eingabe, daher lehnte die Wallet dieses Image dort ab, bevor überhaupt bewiesen wurde. Chain 13, die v0.4-Chain, setzte 65.535 und 32.768, und jede Chain seitdem behält beide Werte bei. Chain 20, die Live-Chain (Genesis 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135), behält außerdem die Block- und Call-Envelope-Limits von Chain 13 und halbiert die Obergrenze für Beweise auf 4 MiB (4.194.304 Bytes, seit Chain 17; die Chains 13 bis 16 setzten 8 MiB). Die Größenobergrenzen sind nicht mehr die einzige Hürde. Das Genesis von Chain 20 setzt hardening_v6, und danach ist ein Deploy mit mehr Programmwörtern, als ein Call an der Call-Obergrenze von Tier 14 fassen kann, ungültig (ProgramUncallable): höchstens 8.180 Wörter ohne öffentliche Eingabe, daneben entsprechend weniger. Dieses Image umfasst 65.096 Wörter neben einer öffentlichen Eingabe von 27.151 Wörtern, daher lehnt Chain 20 den Deploy ab, und sie würde auch einen Call ablehnen: Jeder SPL-Vektor läuft auf Tier 20, oberhalb der Call-Obergrenze von Tier 14 (Limits). Die Blockaggregation ist auf Chain 20 abgeschaltet (ihr Genesis hat keinen Abschnitt aggregation); ihre Bootstrap-Knoten sind unter Running a validator aufgeführt. Die Deploy-Gebühr zählt die öffentlichen Wörter zu den Codewörtern: 0.001 + 0.0001 × (65,096 + 27,151) = 9,2257 RAND, die Gebühr, die der unten gezeigte Deploy auf Chain 13 gezahlt hat.

Dieser Deploy lief am 2026-09-19 auf Chain 13, als diese die Live-Chain war (FRI-Produktionsprofil, Genesis 8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3), aus der Genesis-Wallet shielded-1. Jede Chain seitdem (14 bis 19) wurde als frische Chain ohne die Programme von Chain 13 aufgesetzt, und das Image wurde auf keiner davon erneut deployt; die Transaktion und die Höhe unten gehören daher zu Chain 13 und lassen sich im Explorer nicht mehr auflösen. Die Programm-ID ist ein Hash allein aus base_pc, den Wörtern und den öffentlichen Wörtern, sodass dasselbe Image mit derselben ELF auf jeder Chain, die den Deploy zulässt, dieselbe ID erhält. Die Ausgabe der Wallet, mit ausgelassenen Fortschrittszeilen (…):

$ 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
…
Programm-ID740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a
Image65.096 Wörter; hc 8ca905ae… in der Schreibweise von rand-guest, ae05a98c… in der der Wallet
öffentliche Eingabe27.151 Wörter (die ELF), Digest ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e
Gebühren-Bundle (2-in-2-out-Bundle von Chain 13)Tier 14, 1.418.406 Bytes, in 94,3 s bewiesen
Deploy-Tx (Chain 13)626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530
verankert auf Höhe (Chain 13)400
Gebühr9,2257 RAND

Seit Chain 14 ist das zahlende Bundle das eine 4-in-4-out-Bundle mit verborgenen Assets, das jede Transaktion mitführt und das gegen das Binding der Transaktion bewiesen wird. Es ist weiterhin Tier 14: etwa 100 s und etwa 1,42 MB beim Produktionsprofil auf den Chains 14 und 15. Seit Chain 17 trägt ein Bundle außerdem einen Tier-10-Auth-Beweis (geteilte Autorisierung), sodass das Gebühren-Bundle einer Transaktion auf den Chains 18 und 19 aus etwa 1,49 MB Bundle-Beweis und etwa 1,37 MB Auth-Beweis besteht.

Die Programm-ID bindet die ELF, daher unterscheidet sie sich von der ID, die rand-guest info für das bloße Image ausgibt. Verwenden Sie die ID, die deploy ausgegeben hat.

Limits

  • Kein Call an das Programm wurde bewiesen, es ist auf Chain 20 nicht deployt, und Chain 20 lehnt sowohl den Deploy als auch den Call ab. Der Deploy oben lief auf Chain 13. Unter hardening_v6 von Chain 20 ist ein Deploy mit mehr Wörtern, als ein Tier-14-Call fassen kann, ungültig (ProgramUncallable; höchstens 8.180 Wörter), und das Image hat 65.096. Die Eingabeseite würde passen: Die ELF ist die öffentliche Eingabe des Deploys, die rand call selbst abruft, und die 10.458 privaten Wörter passen in die Eingabeobergrenze von 16.071, die Chain 20 beibehält. Aber jeder SPL-Vektor ist Tier 20, und seit fullnode v0.5.6 (2026-09-25) lehnt ein Knoten jeden Call-Beweis oberhalb von Tier 14 ab (CallTierTooHigh), noch bevor er einen Verifier-Key aufbaut. Dieselbe Prüfung begrenzt auch die Hash-Tabellen, die ein Call deklarieren darf: 2¹³ SHA-256-Zeilen (128 Kompressionen) und 2¹² Keccak-Zeilen (128 Permutationen). Ein Tier-20-Call lässt sich nur off-chain beweisen, und ein Tier-20-Beweis existiert noch nicht (nächster Punkt).
  • Bisher wurde kein Beweis auf Tier 20 erstellt. Ein Tier-20-Beweis wurde auf einem Laptop mit 48 GB bei etwa 31 GB abgebrochen und auf einem Droplet mit 64 GB nach 10 m 41 s bei 65,1 GB vom OOM-Killer beendet. Hochgerechnet aus der gemessenen Skalierung von Tier 16/18 (Speicher etwa ×3,9, Zeit etwa ×4,1 je +2 Tiers) benötigt Tier 20 etwa 330 GB und etwa 3,6 h – mehr als das größte Speicher-Droplet von DigitalOcean (m-32vcpu-256gb, 256 GB). Der Prover lief single-threaded: Eine 16-vCPU-Maschine lag bei 99 % eines Kerns. Siehe proving memory.
  • Kein CPI. Cross-Program Invocations lösen einen Trap aus.
  • Nur sBPF v1. Opcodes, die nur in v2 existieren, lösen einen Trap aus.
  • Ed25519- und secp256k1-Syscalls lösen einen Trap aus, bis sie Coprozessor-Tabellen haben.
  • Die Chain zeichnet die ELF nur als öffentliche Eingabe auf. Ein Deploy mit --public speichert die Wörter der ELF und ihren Digest, und rand_getProgramPublic liefert sie aus. Die Chain zeichnet nicht auf, wie das Image daraus gebaut wurde: Veröffentlichen Sie die Build-Eingaben, damit andere hc neu bauen können.

Zuletzt geprüft gegen den fullnode-Commit c0878c3f (Chain 21) am 2026-10-12 und 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. Wo Code und diese Seite voneinander abweichen, hat der Code recht; sagen Sie uns Bescheid.