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, оно стало нативным кодом. Поэтому её выходы совпадают с выходами интерпретатора слово в слово.
Модель доверия
Доказательство транслированной программы опирается на три вещи.
hcпривязывает транслированный код. C компилируется в образ, поэтомуhcобраза покрывает его.- Защита ELF привязывает
hcк одной программе. Прослойка загружает программу из ELF на ленте, поэтому без проверки один образ мог бы выполнить под тем жеhcдругой ELF, выбранный вызывающим. Поэтому сгенерированныйprogram.cзавершается дайджестом загруженной программы, из которой он был транслирован: текст, данные только для чтения, их адреса и точка входа. Прежде чем что-либо выполнять, прослойка хеширует загруженную с ленты программу тем же способом (Poseidon2) и сравнивает. Любая другая программа останавливается сBadElf, статус 2, над предсостоянием. Таким образом, доказательство, которое проверяется относительно этогоhc, является запуском именно этой программы. 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 |
BadCallImmSrc | call imm с src в 2..=10 (только в ELF, собранных вручную) | останов BadInsn |
Транслятор, отвергающий недостижимый код, отвергал бы программы, которые интерпретатор сегодня выполняет. SPL Token как раз такая программа: она вызывает sol_set_return_data и sol_get_sysvar, до которых путь Transfer не доходит.
Допускаются три расхождения. Каждое лишь заставляет трансляцию остановиться там, где интерпретатор, возможно, продолжил бы; трансляция никогда не завершает запуск, который интерпретатор останавливает.
callxза пределами просканированного множества приводит к остановуBadJump, тогда как интерпретатор может выполнить настоящий код по этому адресу.- Проверки бюджета могут откладываться на один базовый блок. Чтобы уместить SPL Token в предел 65 535 слов, 1 505 из его 3 546 блоков пропускают собственную проверку лимита инструкций. Вид останова может отличаться внутри того единственного блока, который пересекает лимит. Статус и восемь слов совпадают.
- Выполняется только исходная программа. Другой 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 096 | 65 535 (439 в запасе) | 8ca905ae…758b |
sbpf.bin, интерпретатор | 8 317 | 65 535 | 059a7086…f476 |
| вектор | статус | инструкции sBPF | такты транслированного | такты sbpf.bin | ярус (оба) |
|---|---|---|---|---|---|
Transfer 250 | 1 | 143 | 765 851 | 694 498 | 20 |
Transfer больше баланса | 0 | 133 | 748 836 | 679 814 | 20 |
MintTo 250 | 1 | 120 | 708 648 | 634 423 | 20 |
MintTo неверным авторизующим лицом | 0 | 134 | 696 694 | 627 241 | 20 |
Burn 250 | 1 | 131 | 710 245 | 637 038 | 20 |
Burn больше баланса | 0 | 121 | 695 434 | 624 781 | 20 |
Transfer с двумя аккаунтами | 0 | 69 | 645 015 | 569 656 | 20 |
| слишком много аккаунтов (обвязка отвергает) | 2 | 0 | 303 933 | 304 616 | 20 |
На что уходят такты в Transfer:
| этап | транслированный | sbpf.bin |
|---|---|---|
| чтение обеих лент | 302 881 | 302 880 |
| загрузка ELF | 177 012 | 173 859 |
| проверка области аккаунтов | 103 829 | 103 824 |
| обнуление стека и кучи sBPF | 49 181 | 49 181 |
| хеши выхода и входа, публичные слова, связующий код | 51 091 | 42 723 |
| защита ELF | 72 949 | — |
| сама программа sBPF | 8 908 | 22 031 |
| всего | 765 851 | 694 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.