Rand Protocol
Документация
Программы RISC-V

Транспайлер Solana

sbpf2rv транслирует программу Solana sBPF в RISC-V ISA Rand: что это такое, модель доверия и защита на уровне ELF, что вызывает trap и что отклоняется, измеренные показатели, а также полный пошаговый разбор, в котором транслируется настоящая программа SPL Token и результат совпадает с интерпретатором sBPF слово в слово.

sbpf2rv принимает скомпилированную программу Solana (ELF для sBPF, файл .so, который создаёт cargo build-sbf) и превращает её в программу Rand. Он пишет C, а rand-guest build компилирует этот C в образ RV32IM. Образ публикует те же восемь выходных слов, что и гостевой интерпретатор sBPF в Rand, sbpf.bin, для того же ELF и той же инструкции. Эта страница показывает весь путь на настоящей программе SPL Token.

Что это такое

входELF для sBPF v1
выходкрейт-прослойка на C; rand-guest build превращает его в контейнер образа
где выполняетсявне цепочки, на машине разработчика; цепочка видит только образ
чему должен соответствоватьвосемь выходных слов sbpf.bin на тех же двух входных лентах
репозиторийsbpf2rv/ в репозитории circuits
зафиксированный набор инструментовRust 1.98.1, clang 23.1.1, крейт cc 1.4.6

Транслированная программа сохраняет ABI интерпретатора. Она читает те же ленты: ELF на публичной ленте и сериализованную инструкцию (сначала аккаунты, затем данные инструкции) на приватной. Она запускает тот же обвязочный код sbpf-core вокруг программы: декодирует вход, загружает ELF, проверяет область аккаунтов и хеширует вход и выход. Заменено только само выполнение sBPF, оно стало нативным кодом. Поэтому её выходы совпадают с выходами интерпретатора слово в слово.

Модель доверия

Доказательство транслированной программы опирается на три вещи.

  1. hc привязывает транслированный код. C компилируется в образ, поэтому hc образа покрывает его.
  2. Защита ELF привязывает hc к одной программе. Прослойка загружает программу из ELF на ленте, поэтому без проверки один образ мог бы выполнить под тем же hc другой ELF, выбранный вызывающим. Поэтому сгенерированный program.c завершается дайджестом загруженной программы, из которой он был транслирован: текст, данные только для чтения, их адреса и точка входа. Прежде чем что-либо выполнять, прослойка хеширует загруженную с ленты программу тем же способом (Poseidon2) и сравнивает. Любая другая программа останавливается с BadElf, статус 2, над предсостоянием. Таким образом, доказательство, которое проверяется относительно этого hc, является запуском именно этой программы.
  3. H_PUB привязывает байты ELF. ELF передаётся на публичной ленте, и дайджест публичных входов доказательства покрывает его слово в слово.

Является ли hc верной трансляцией данного ELF, проверяется пересборкой. Запустите зафиксированный sbpf2rv на ELF, соберите зафиксированным набором инструментов и сравните hc. Именно ради этого сборка воспроизводима.

Цепочка не записывает исходный ELF и его хеш. Она хранит образ и его идентификатор программы. Деплойер, который хочет, чтобы ELF был известен, публикует его рядом с программой.

Защита привязывает загруженную программу, а не файл. Два ELF, различающиеся только там, куда загрузчик не смотрит (выравнивание заголовков, таблицы символов), загружаются в одну и ту же программу и одинаково выполняются с обеих сторон. ELF с одним изменённым байтом в .rodata, даже таким, который перевод никогда не читает, отвергается трансляцией и выполняется интерпретатором.

Что приводит к останову и что отвергается

При трансляции ничего не отвергается. Некорректный ELF отвергается с сообщением самого загрузчика, но ни одна инструкция в корректном ELF не отвергается. Сканер сообщает в виде предупреждений о том, на что программа может наткнуться, а транслированная программа останавливается во время выполнения ровно там, где остановился бы интерпретатор:

предупреждениечто этово время выполнения
UnknownSyscallхеш системного вызова, который среда выполнения не реализуетостанов UnknownSyscall
Cpiмежпрограммный вызов (sol_invoke_signed*)тот же останов UnknownSyscall; CPI не реализован
RegisterOutOfRangeрегистр r11..r15 или регистр callx выше r10останов BadInsn
UnknownOpcodeкод операции без класса v1, включая семейство sdiv/srem, существующее только в v2, и hor64останов BadInsn
JumpOutOfTextпереход, вызов или проваливание за пределы текста функцииостанов BadJump
BadCallImmSrccall imm с src в 2..=10 (только в ELF, собранных вручную)останов BadInsn

Транслятор, отвергающий недостижимый код, отвергал бы программы, которые интерпретатор сегодня выполняет. SPL Token как раз такая программа: она вызывает sol_set_return_data и sol_get_sysvar, до которых путь Transfer не доходит.

Допускаются три расхождения. Каждое лишь заставляет трансляцию остановиться там, где интерпретатор, возможно, продолжил бы; трансляция никогда не завершает запуск, который интерпретатор останавливает.

  1. callx за пределами просканированного множества приводит к останову BadJump, тогда как интерпретатор может выполнить настоящий код по этому адресу.
  2. Проверки бюджета могут откладываться на один базовый блок. Чтобы уместить SPL Token в предел 65 535 слов, 1 505 из его 3 546 блоков пропускают собственную проверку лимита инструкций. Вид останова может отличаться внутри того единственного блока, который пересекает лимит. Статус и восемь слов совпадают.
  3. Выполняется только исходная программа. Другой ELF отвергается (защита, выше).

Ed25519 и secp256k1. Программные реализации sol_ed25519_verify и sol_secp256k1_recover есть в sbpf-rt/, но не подключены. Интерпретатор останавливается на обоих, поэтому для паритета трансляция тоже останавливается. Каждый вызов стоит примерно 14,3 млн и 15,7 млн тактов соответственно, что намного больше крупнейшего яруса (2²⁰). Им нужны таблицы сопроцессора.

Числа

Измерено на SPL Token (guests-compiled/sbpf/programs/spl_token.so, собственная тестовая фикстура интерпретатора) при включённой защите ELF.

образслова программыпределhc
SPL Token, транслированный65 09665 535 (439 в запасе)8ca905ae…758b
sbpf.bin, интерпретатор8 31765 535059a7086…f476
векторстатусинструкции sBPFтакты транслированноготакты sbpf.binярус (оба)
Transfer 2501143765 851694 49820
Transfer больше баланса0133748 836679 81420
MintTo 2501120708 648634 42320
MintTo неверным авторизующим лицом0134696 694627 24120
Burn 2501131710 245637 03820
Burn больше баланса0121695 434624 78120
Transfer с двумя аккаунтами069645 015569 65620
слишком много аккаунтов (обвязка отвергает)20303 933304 61620

На что уходят такты в Transfer:

этаптранслированныйsbpf.bin
чтение обеих лент302 881302 880
загрузка ELF177 012173 859
проверка области аккаунтов103 829103 824
обнуление стека и кучи sBPF49 18149 181
хеши выхода и входа, публичные слова, связующий код51 09142 723
защита ELF72 949—
сама программа sBPF8 90822 031
всего765 851694 498

Собственное выполнение транслированной программы примерно в 2,4–2,5 раза дешевле в пересчёте на инструкцию sBPF (около 62 тактов против 154). Но около 98 % каждого запуска приходится на фиксированную обвязку, а защита добавляет 73 тыс. тактов. Поэтому для SPL Token транслированный образ дороже на 69–75 тыс. тактов, и оба остаются на ярусе 20.

Трансляция окупается только для программ с большой вычислительной нагрузкой. При экономии около 90 тактов на инструкцию вызов, выполняющий 10 000 инструкций sBPF, экономит около 0,9 млн тактов. SPL Token выполняет от 120 до 143.

Пошаговый разбор: SPL Token, транслированный и сопоставленный

Каждая команда выполняется из корня копии circuits на 7ef3220, с набором инструментов из Развёртывание программы на Rust и clang 23.1.1 из Развёртывание программы RISC-V. Длинный вывод обрезан там, где стоит ….

0. Соберите инструменты

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

1. Трансляция

--out должен быть каталогом внутри копии 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). Четыре предупреждения относятся к настоящим системным вызовам, до которых Transfer не доходит: хеш 2720453611 соответствует sol_set_return_data (три места вызова), а 331461893 соответствует sol_get_sysvar.

2. Сборка образа

Образ превышает предел по умолчанию в 4 096 слов, поэтому передайте --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

Любой, кто выполнит эти два шага с тем же набором инструментов, получит этот hc.

3. Запустите оба и сравните

Transfer на 250 принимает два списка слов, построенных tests/parity.rs из собственного кодирования вызова интерпретатора: 27 151 публичное слово (ELF в словесном кодировании) и 10 458 приватных слов (аккаунты, затем дискриминант Transfer и сумма). rand-guest run принимает их как --public и --input. Сокращённо, первые слова каждого списка:

$ 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

Те же два списка для гостя-интерпретатора:

$ 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

Все восемь слов совпадают. out[0] = 1 — статус (успех). out[1..8] — дайджест, который ABI интерпретатора определяет над входом и выходом.

4. Проверьте все восемь векторов одной командой

Тест паритета собирает транслированный образ, прогоняет каждый вектор через rand-guest run на обоих образах и завершается ошибкой при любом различии в восьми словах:

$ 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. Развёртывание

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

Образ читает ELF из публичной ленты, поэтому ELF — это публичный вход программы, зафиксированный при деплое. --public принимает сам файл .so; кошелёк кодирует его в слова — в те же 27,151 слов, что и на шаге 4.

Для этого нужна цепочка, в genesis которой max_program_words не меньше 65,096, а max_program_public_words не меньше 27,151. Цепочка 12 ограничивала деплой 4,096 словами и не принимала публичного входа, поэтому кошелёк отклонял этот образ ещё до построения доказательства. Цепочка 13, цепочка v0.4, задала 65,535 и 32,768, и каждая последующая цепочка сохраняет оба значения. Цепочка 21, действующая цепочка с 2026-10-12 (fullnode v0.8.0, genesis ca76c2766ed6090ac6ba5dad86b724dd5d2b80c0edfaed287cd8b129e663ca9a), также сохраняет лимит конверта вызова цепочки 13, задаёт предел доказательства в 6 MiB (6,291,456 байт; цепочки с 17 по 20 — 4 MiB, цепочки с 13 по 16 — 8 MiB) и предел блока в 19 MiB. Ограничения по размеру больше не единственный барьер. В genesis цепочки 21, как и цепочек 20 и 19, задан hardening_v6, и при нём деплой программы, содержащей больше слов, чем способен вместить вызов при пределе вызова уровня 14, недопустим (ProgramUncallable): не более 8,180 слов без публичного входа и меньше при его наличии. Этот образ — 65,096 слов при публичном входе в 27,151 слово, поэтому цепочка 21 отклоняет деплой, а отклонила бы и вызов: каждый вектор SPL выполняется на уровне 20, выше предела вызова уровня 14 (Limits). Агрегация блоков на цепочке 21 отключена (в её genesis нет секции aggregation); её bootstrap-узлы перечислены в разделе Running a validator. Комиссия за деплой учитывает публичные слова вместе со словами кода: 0.001 + 0.0001 × (65,096 + 27,151) = 9.2257 RAND — именно столько заплатил приведённый ниже деплой на цепочке 13.

Этот деплой был выполнен на цепочке 13 2026-09-19, когда цепочка 13 была действующей (производственный профиль FRI, genesis 8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3), из genesis-кошелька shielded-1. Каждая последующая цепочка (с 14 по 19) создавалась заново, без программ цепочки 13, и образ ни на одной из них повторно не разворачивался, поэтому приведённые ниже транзакция и высота относятся к цепочке 13 и в обозревателе больше не находятся. Идентификатор программы — это хеш одних лишь base_pc, слов и публичных слов, поэтому один и тот же образ с тем же ELF получает один и тот же идентификатор на любой цепочке, которая допускает деплой. Вывод кошелька, строки прогресса пропущены (…):

$ 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
…
идентификатор программы740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a
образ65,096 слов; hc 8ca905ae… в записи rand-guest, ae05a98c… в записи кошелька
публичный вход27,151 слов (ELF), дайджест ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e
пакет комиссии (пакет 2-in-2-out цепочки 13)уровень 14, 1,418,406 байт, доказан за 94.3 с
транзакция деплоя (цепочка 13)626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530
закреплена на высоте (цепочка 13)400
комиссия9.2257 RAND

Начиная с цепочки 14, оплачивающий пакет — это один пакет 4-in-4-out со скрытым активом, который несёт каждая транзакция и который доказывается относительно привязки транзакции. Это по-прежнему уровень 14: около 100 с и около 1.42 МБ при производственном профиле на цепочках 14 и 15. Начиная с цепочки 17, пакет несёт также доказательство авторизации уровня 10 (раздельная авторизация), поэтому на цепочках 18 и 19 пакет комиссии транзакции — это около 1.49 МБ доказательства пакета и около 1.37 МБ доказательства авторизации.

Идентификатор программы связан с ELF, поэтому он отличается от идентификатора, который rand-guest info печатает для голого образа. Вызывайте программу по идентификатору, который напечатал deploy.

Limits

  • Ни один вызов программы не был доказан, она не развёрнута на цепочке 21, и цепочка 21 отклоняет и деплой, и вызов. Приведённый выше деплой был выполнен на цепочке 13. При hardening_v6 цепочки 21 деплой большего числа слов, чем способен вместить вызов уровня 14, недопустим (ProgramUncallable; не более 8,180 слов), а образ содержит 65,096. Со стороны входа всё поместилось бы: ELF — это публичный вход деплоя, который rand call получает сам, а 10,458 приватных слов укладываются в предел входа 16,071, который сохраняют цепочки 20 и 21. Но каждый вектор SPL относится к уровню 20, а начиная с fullnode v0.5.6 (2026-09-25) узел отклоняет любое доказательство вызова выше уровня 14 (CallTierTooHigh) ещё до построения ключа верификатора. Та же проверка ограничивает хеш-таблицы, которые может объявить вызов: 2¹³ строк SHA-256 (128 сжатий) и 2¹² строк Keccak (128 перестановок). Вызов уровня 20 можно доказать только вне цепочки, а ни одного доказательства уровня 20 пока не существует (следующий пункт).
  • Ни одно доказательство уровня 20 пока не построено. Доказательство уровня 20 было прервано на ноутбуке с 48 ГБ при около 31 ГБ и убито по нехватке памяти (OOM) на дроплете с 64 ГБ при 65.1 ГБ через 10 мин 41 с. По экстраполяции измеренного масштабирования уровней 16/18 (память примерно ×3.9, время примерно ×4.1 на каждые +2 уровня) уровню 20 нужно около 330 ГБ и около 3.6 ч — больше, чем у самого крупного по памяти дроплета DigitalOcean (m-32vcpu-256gb, 256 ГБ). Доказывающий работал в один поток: 16-vCPU машина держала одно ядро на 99%. См. память при доказательстве.
  • Нет CPI. Межпрограммные вызовы завершаются ловушкой.
  • Только sBPF v1. Опкоды, существующие только в v2, завершаются ловушкой.
  • Системные вызовы Ed25519 и secp256k1 завершаются ловушкой, пока для них нет таблиц сопроцессора.
  • Цепочка записывает ELF только как публичный вход. Деплой с --public сохраняет слова ELF и их дайджест, а rand_getProgramPublic их отдаёт. Как образ был собран из ELF, цепочка не записывает: публикуйте входные данные сборки, чтобы другие могли воспроизвести hc.

Последняя проверка по коммиту fullnode c0878c3f (chain 21) от 2026-10-12, а также 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. Если код и эта страница расходятся, прав код; сообщите нам.