Rand Protocol
ドキュメント
RISC-V プログラム

Solana トランスパイラ

sbpf2rv は、Solana の sBPF プログラムを Rand の RISC-V ISA に変換します。本ページでは、その概要、信頼モデルと ELF ガード、トラップされるものと拒否されるもの、計測した数値、そして実際の SPL Token プログラムを変換し、sBPF インタプリタと 1 ワードずつ一致することを確かめるまでの一連の手順を説明します。

sbpf2rv は、コンパイル済みの Solana プログラム(cargo build-sbf が生成する .so、つまり sBPF ELF)を Rand プログラムに変換します。出力は C で、rand-guest build がその C を RV32IM イメージにコンパイルします。同じ ELF と同じ命令に対して、このイメージは Rand の sBPF インタプリタ guest である sbpf.bin と同じ 8 個の出力ワードを公開します。このページでは、実際の SPL Token プログラムを使って全工程を示します。

概要

入力sBPF v1 ELF
出力C の shim クレート。rand-guest build がこれをイメージコンテナにします
実行場所オフチェーン(開発者のマシン上)。チェーンが見るのはイメージだけです
一致が求められるもの同じ 2 本の入力テープに対する sbpf.bin の 8 個の出力ワード
リポジトリcircuits リポジトリの sbpf2rv/
固定されたツールチェーンRust 1.98.1、clang 23.1.1、cc クレート 1.4.6

変換後のプログラムはインタプリタの ABI を維持します。読み込むテープも同じで、公開テープに ELF、秘密テープにシリアライズされた命令(アカウント、続いて命令データ)を置きます。プログラムの周囲では同じ sbpf-core ハーネスが動作し、入力のデコード、ELF のロード、アカウント領域の検査、入力と出力のハッシュ化を行います。置き換えられるのは sBPF の実行そのものだけで、ネイティブコードが代わりに動きます。そのため、出力はインタプリタと 1 ワードずつ一致します。

信頼モデル

変換後のプログラムの証明は、次の 3 つによって拘束されます。

  1. hc は変換後のコードを拘束します。 C はイメージにコンパイルされるため、イメージの hc がそれを覆います。
  2. ELF ガードは hc を 1 つのプログラムに結び付けます。 shim はテープ上の ELF からプログラムをロードします。検査がなければ、1 つのイメージが同じ hc のまま、呼び出し側が選んだ別の ELF を実行できてしまいます。そこで、生成される program.c の末尾には、変換元となったロード済みプログラムのダイジェストを置きます。対象はテキスト、読み取り専用データ、それらのアドレス、エントリポイントです。shim は何かを実行する前に、テープ上のロード済みプログラムを同じ方法(Poseidon2)でハッシュ化して比較します。一致しないプログラムは、BadElf、ステータス 2 で、事前状態のまま停止します。したがって、この hc に対して検証に通る証明は、そのプログラムの実行を意味します。
  3. H_PUB は ELF のバイト列を拘束します。 ELF は公開テープで渡され、証明の公開入力ダイジェストがそれを 1 ワードずつ覆います。

hc が特定の ELF の忠実な変換であるかどうかは、再ビルドで確認します。固定された sbpf2rv をその ELF に実行し、固定されたツールチェーンでビルドして、hc を比較します。このためにビルドは再現可能になっています。

チェーンは元の ELF もそのハッシュも記録しません。保存するのはイメージとそのプログラム ID です。ELF を第三者に確認可能にしたいデプロイヤーは、プログラムと並べて ELF を公開します。

ガードが拘束するのはファイルではなくロード済みプログラムです。ローダーが参照しない箇所(ヘッダーのパディング、シンボルテーブル)だけが異なる 2 つの ELF は、同じプログラムにロードされ、どちらの側でも同一に動作します。.rodata の 1 バイトが変更された ELF は、転送処理がそのバイトを読まない場合でも、変換後のプログラムでは拒否され、インタプリタでは実行されます。

トラップされるものと拒否されるもの

変換時に拒否されるものはありません。 不正な形式の ELF はローダー自身のメッセージとともに拒否されますが、正しい形式の ELF の中の命令は 1 つも拒否されません。スキャナーはプログラムが到達しうるものを警告として報告し、変換後のプログラムは、インタプリタがトラップするのとまったく同じ場所で実行時にトラップします。

警告内容実行時の動作
UnknownSyscallランタイムが実装していない syscall ハッシュUnknownSyscall トラップ
Cpiクロスプログラム呼び出し(sol_invoke_signed*)同じ UnknownSyscall トラップ。CPI は未実装です
RegisterOutOfRangeレジスタ r11..r15、または r10 を超える callx レジスタBadInsn トラップ
UnknownOpcodev1 のクラスに属さないオペコード。v2 専用の sdiv/srem 系と hor64 を含みますBadInsn トラップ
JumpOutOfText関数のテキスト外へのジャンプ、呼び出し、またはフォールスルーBadJump トラップ
BadCallImmSrcsrc が 2..=10 の call imm(手作業で作った ELF にのみ現れます)BadInsn トラップ

到達しないコードを拒否する変換器は、インタプリタが現に実行できるプログラムを拒否してしまいます。SPL Token がそのようなプログラムです。sol_set_return_data と sol_get_sysvar を呼び出しますが、Transfer の経路はそれらに到達しません。

3 つの相違点は許容されています。いずれも、変換後のプログラムが、インタプリタなら停止しない場面で停止しうるだけです。インタプリタが停止する実行を、変換後のプログラムが完走することはありません。

  1. スキャンされた集合の外への callx は BadJump でトラップします。インタプリタは、そのアドレスにある実際のコードを実行する場合があります。
  2. 予算検査は 1 基本ブロック分遅延することがあります。 SPL Token を 65,535 ワードの上限内に収めるため、3,546 ブロックのうち 1,505 ブロックは自身の命令数上限の検査を省いています。上限をまたぐ 1 つのブロックの内部では、停止の種別が異なることがあります。ステータスと 8 個のワードは同じです。
  3. 実行されるのは元のプログラムだけです。 別の ELF は拒否されます(上記のガード)。

Ed25519 と secp256k1。 sol_ed25519_verify と sol_secp256k1_recover のソフトウェア実装は sbpf-rt/ にありますが、リンクされていません。インタプリタはどちらでもトラップするため、一致を保つために変換後のプログラムもトラップします。呼び出し 1 回のコストはそれぞれ約 14.3M サイクルと約 15.7M サイクルで、最大のティア(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 アカウント 2 個069645,015569,65620
アカウント数過多(ハーネスが拒否)20303,933304,61620

Transfer におけるサイクルの内訳:

段階変換後sbpf.bin
両テープの読み込み302,881302,880
ELF のロード177,012173,859
アカウント領域の検査103,829103,824
sBPF のスタックとヒープのゼロ化49,18149,181
出力・入力ハッシュ、公開ワード、接着コード51,09142,723
ELF ガード72,949—
sBPF プログラム本体8,90822,031
合計765,851694,498

変換後のプログラム自体の実行は、sBPF 命令 1 つあたり約 2.4〜2.5 倍安くなります(約 62 サイクル対 154 サイクル)。しかし、どの実行でも約 98% は固定のハーネスが占めており、ガードが 73k サイクルを加えます。そのため SPL Token では、変換後のイメージのほうが 69k〜75k サイクル高くなり、どちらもティア 20 にとどまります。

変換が効果を発揮するのは、計算負荷の高いプログラムだけです。 命令 1 つあたり約 90 サイクルの節約なので、sBPF 命令を 10,000 個実行する呼び出しでは約 0.9M サイクルを節約できます。SPL Token が実行する命令は 120〜143 個です。

ウォークスルー: SPL Token を変換して照合する

すべてのコマンドは、7ef3220 の circuits チェックアウトのルートから実行します。ツールチェーンは Deploy a Rust program のもの、clang 23.1.1 は Deploy a RISC-V program のものを使います。長い出力は … で省略しています。

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) です。4 件の警告は、Transfer が到達しない実在の syscall によるものです。ハッシュ 2720453611 は sol_set_return_data(呼び出し箇所 3 つ)で、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

同じツールチェーンでこの 2 つの手順を実行すれば、誰でもこの hc を得られます。

3. 両方を実行して比較する

250 の Transfer には 2 つのワードリストが必要です。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

同じ 2 つのリストをインタプリタ 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

8 個のワードはすべて一致します。out[0] = 1 はステータス(成功)です。out[1..8] は、インタプリタの ABI が入力と出力に対して定義するダイジェストです。

4. 8 つのベクトルを 1 つのコマンドで検証する

パリティテストは、変換後のイメージをビルドし、すべてのベクトルを両方のイメージで rand-guest run により実行して、8 個のワードに 1 つでも差異があれば失敗します。

$ 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 ファイルそのものを渡し、ウォレットがステップ 4 と同じ 27,151 ワードにワードエンコードします。

これには、ジェネシスで max_program_words が 65,096 以上、max_program_public_words が 27,151 以上に設定されたチェーンが必要です。チェーン 12 はデプロイを 4,096 ワードに制限し、公開入力も受け付けませんでした。そのため、ウォレットは証明を始める前にこのイメージを拒否しました。v0.4 のチェーンであるチェーン 13 は 65,535 と 32,768 に設定し、以降のすべてのチェーンが両方の値を維持しています。現行チェーンであるチェーン 20(ジェネシス 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135)も、チェーン 13 のブロック上限とコールエンベロープ上限を維持し、証明サイズの上限を 4 MiB(4,194,304 バイト、チェーン 17 以降。チェーン 13 から 16 は 8 MiB)に半減させています。ただし、サイズ上限だけが関門ではなくなりました。チェーン 20 のジェネシスは hardening_v6 を設定しており、これにより、ティア 14 のコール上限で 1 回のコールに収まる量を超えるプログラムワードのデプロイは無効になります(ProgramUncallable)。上限は公開入力がない場合で 8,180 ワード以下で、公開入力を併せる場合はさらに少なくなります。このイメージは 65,096 ワードで、27,151 ワードの公開入力を伴うため、チェーン 20 はデプロイを拒否し、コールも拒否します。すべての SPL ベクトルはティア 20 で実行され、ティア 14 のコール上限を超えるためです(制限)。チェーン 20 ではブロック集約は無効です(ジェネシスに aggregation セクションがありません)。そのブートストラップノードは Running a validator に記載しています。デプロイ手数料は、公開ワードもコードワードと合わせて数えます。0.001 + 0.0001 × (65,096 + 27,151) = 9.2257 RAND で、これが下のチェーン 13 でのデプロイが支払った手数料です。

このデプロイは、チェーン 13 が現行チェーンだった 2026-09-19 に、チェーン 13(本番 FRI プロファイル、ジェネシス 8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3)上で、ジェネシスウォレット shielded-1 から実行しました。以降のチェーン(14 から 19)はいずれも、チェーン 13 のプログラムを引き継がない新しいチェーンとして切り出されており、このイメージはそのどれにも再デプロイしていません。したがって、以下のトランザクションと高さはチェーン 13 のものであり、エクスプローラーではもう解決できません。プログラム ID は base_pc、ワード、公開ワードのみのハッシュであるため、同じイメージと ELF なら、デプロイを受け付けるどのチェーンでも同じ ID になります。ウォレットの出力は次のとおりです。進行状況の行は省略しています(…)。

$ 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
…
プログラム ID740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a
イメージ65,096 ワード。rand-guest の表記で hc 8ca905ae…、ウォレットの表記で ae05a98c…
公開入力27,151 ワード(ELF)、ダイジェスト ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e
手数料バンドル(チェーン 13 の 2 入力 2 出力バンドル)ティア 14、1,418,406 バイト、証明に 94.3 秒
デプロイ tx(チェーン 13)626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530
アンカーされた高さ(チェーン 13)400
手数料9.2257 RAND

チェーン 14 以降、手数料を支払うバンドルは、すべてのトランザクションが持つ 4 入力 4 出力の隠しアセットバンドル 1 つで、トランザクションのバインディングに対して証明されます。これは引き続きティア 14 で、チェーン 14 と 15 の本番 FRI プロファイルでは約 100 秒、約 1.42 MB です。チェーン 17 以降はバンドルにティア 10 の認証証明(分割認可)も付くため、チェーン 18 と 19 では、トランザクションの手数料バンドルはバンドル証明が約 1.49 MB、認証証明が約 1.37 MB になります。

プログラム ID は ELF にバインドされるため、rand-guest info が素のイメージに対して出力する ID とは異なります。deploy が出力した ID でコールしてください。

制限

  • このプログラムへのコールは証明されておらず、チェーン 20 にはデプロイされておらず、チェーン 20 はデプロイもコールも拒否します。 上記のデプロイはチェーン 13 でのものです。チェーン 20 の hardening_v6 では、ティア 14 のコールに収まる量を超えるワード数のデプロイは無効であり(ProgramUncallable、最大 8,180 ワード)、このイメージは 65,096 ワードです。入力側は収まります。ELF はデプロイの公開入力で、rand call が自分で取得します。また、10,458 の非公開ワードは、チェーン 20 が維持する入力上限 16,071 に収まります。しかし、すべての SPL ベクトルはティア 20 であり、fullnode v0.5.6(2026-09-25)以降、ノードは検証鍵を構築する前に、ティア 14 を超えるコール証明をすべて拒否します(CallTierTooHigh)。同じチェックは、コールが宣言できるハッシュテーブルにも上限を課します。SHA-256 は 2¹³ 行(128 回の圧縮)、Keccak は 2¹² 行(128 回の置換)です。ティア 20 のコールはオフチェーンでしか証明できず、ティア 20 の証明はまだ存在しません(次の項目)。
  • ティア 20 での証明はまだ作成されていません。 ティア 20 の証明は、48 GB のノート PC では約 31 GB の時点で強制終了され、64 GB のドロップレットでは 10 分 41 秒後に 65.1 GB で OOM により強制終了されました。実測したティア 16/18 のスケーリング(2 ティア上がるごとにメモリは約 3.9 倍、時間は約 4.1 倍)から外挿すると、ティア 20 には約 330 GB と約 3.6 時間が必要で、これは DigitalOcean の最大メモリのドロップレット(m-32vcpu-256gb、256 GB)を超えます。証明者はシングルスレッドで動作し、16 vCPU のマシンでも 1 コアが 99% で張り付いていました。証明のメモリを参照してください。
  • CPI はありません。 クロスプログラム呼び出しはトラップします。
  • sBPF v1 のみ対応です。 v2 専用のオペコードはトラップします。
  • Ed25519 と secp256k1 のシステムコールは、コプロセッサテーブルができるまでトラップします。
  • チェーンは ELF を公開入力としてのみ記録します。 --public 付きのデプロイは、ELF のワードとそのダイジェストを保存し、rand_getProgramPublic がそれらを返します。チェーンは、イメージがそこからどのようにビルドされたかを記録しません。他の人が hc を再ビルドできるよう、ビルド入力を公開してください。

fullnode のコミット c0878c3f(チェーン 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。 コードとこのページが食い違う場合は、コードが正です。お知らせください。