sbpf2rv 會讀取已編譯的 Solana 程式(即 cargo build-sbf 產生的 sBPF ELF,副檔名為 .so),並將其轉換為 Rand 程式。它輸出 C 代碼,再由 rand-guest build 將該 C 代碼編譯為 RV32IM 映像。對於同一個 ELF 和同一條指令,該映像所發佈的八個輸出字,與 Rand 的 sBPF 直譯器 guest(sbpf.bin)所發佈的相同。本頁以真實的 SPL Token 程式展示完整流程。
簡介
| 輸入 | 一個 sBPF v1 ELF |
| 輸出 | 一個 C shim crate;rand-guest build 會將其製成映像容器 |
| 運行位置 | 鏈下,在開發者的機器上;鏈上只會看到映像 |
| 必須吻合的對象 | sbpf.bin 在相同兩條輸入磁帶上的八個輸出字 |
| 代碼庫 | circuits 代碼庫中的 sbpf2rv/ |
| 固定版本的工具鏈 | Rust 1.98.1、clang 23.1.1、cc crate 1.4.6 |
轉譯後的程式沿用直譯器的 ABI。它讀取相同的輸入磁帶:公開磁帶載有 ELF,私密磁帶載有序列化後的指令(先是帳戶,再是指令數據)。它在程式外圍運行同一套 sbpf-core 測試框架:解碼輸入、載入 ELF、檢查帳戶區域,並對輸入和輸出計算哈希。被替換成原生代碼的,只有 sBPF 的執行本身。因此,它的輸出與直譯器逐字相同。
信任模型
轉譯後程式的證明由三項要素約束。
hc約束轉譯後的代碼。 C 代碼已編譯進映像,因此映像的hc涵蓋這些代碼。- ELF 守衛將
hc約束於單一程式。 shim 從磁帶上的 ELF 載入程式,若不加檢查,同一個映像便可在相同的hc下運行由調用者任選的另一個 ELF。因此,生成的program.c末尾附有一個摘要,對應其轉譯來源的已載入程式:文本、唯讀數據、它們的地址以及入口點。在運行任何內容之前,shim 會以同樣方式(Poseidon2)對磁帶上已載入的程式計算哈希並作比較。任何其他程式都會以BadElf、狀態 2 中止,並停留在前狀態(pre-state)。所以,只要證明能針對此hc通過驗證,它就是該程式的一次運行。 H_PUB約束 ELF 的位元組。 ELF 經由公開磁帶傳送,證明的公開輸入摘要逐字涵蓋它。
hc 是否忠實轉譯自某個 ELF,可透過重新構建來檢驗:對該 ELF 運行固定版本的 sbpf2rv,以固定版本的工具鏈構建,再比較 hc。正因如此,構建過程是可重現的。
鏈上不會記錄源 ELF 或其哈希,只會儲存映像及其程式 id。希望公開 ELF 的部署者,可將其與程式一併發佈。
守衛約束的是已載入的程式,而非檔案本身。兩個 ELF 若只在載入器不會檢視之處有所不同(標頭填充、符號表),便會載入為同一個程式,並在兩邊運行得完全一致。ELF 中只要有一個 .rodata 位元組被改動,即使轉賬從不讀取該位元組,轉譯結果也會拒絕它,而直譯器則會運行它。
哪些會觸發陷阱,哪些會被拒絕
轉譯時不會拒絕任何東西。 格式錯誤的 ELF 會以載入器自身的訊息拒絕,但格式正確的 ELF 中,沒有任何一條指令會被拒絕。掃描器會把程式可能遇到的情況以警告報告,而轉譯後的程式會在運行時,於直譯器會觸發陷阱的同一位置觸發陷阱:
| 警告 | 含義 | 運行時 |
|---|---|---|
UnknownSyscall | 運行時未實現的系統調用哈希 | 觸發 UnknownSyscall 陷阱 |
Cpi | 跨程式調用(sol_invoke_signed*) | 同樣的 UnknownSyscall 陷阱;CPI 尚未實現 |
RegisterOutOfRange | 寄存器 r11..r15,或高於 r10 的 callx 寄存器 | 觸發 BadInsn 陷阱 |
UnknownOpcode | 不屬於任何 v1 類別的操作碼,包括僅見於 v2 的 sdiv/srem 系列和 hor64 | 觸發 BadInsn 陷阱 |
JumpOutOfText | 跳轉、調用或順序執行落在函數文本之外 | 觸發 BadJump 陷阱 |
BadCallImmSrc | src 為 2..=10 的 call imm(僅見於手工構建的 ELF) | 觸發 BadInsn 陷阱 |
若轉譯器拒絕從未執行到的代碼,就會拒絕直譯器現時能夠運行的程式。SPL Token 便是這樣的程式:它調用了 sol_set_return_data 和 sol_get_sysvar,而 Transfer 路徑從未執行到這兩者。
有三項差異是可接受的。每一項都只會令轉譯結果在直譯器未必中止之處中止;轉譯結果絕不會完成一次直譯器會中止的運行。
- 掃描集合之外的
callx會觸發BadJump,而直譯器可能會在該地址運行真實的代碼。 - 預算檢查可能順延一個基本塊。 為使 SPL Token 符合 65,535 字的上限,其 3,546 個基本塊中有 1,505 個略過自身的指令數上限檢查。在跨越上限的那一個基本塊內,中止的類別可能不同,但狀態和八個字相同。
- 只有源程式會運行。 其他 ELF 會被拒絕(見上文的守衛)。
Ed25519 和 secp256k1。 sol_ed25519_verify 和 sol_secp256k1_recover 的軟件實現存在於 sbpf-rt/,但並未鏈接。直譯器對兩者都會觸發陷阱,因此為求一致,轉譯結果也會觸發陷阱。每次調用分別約需 14.3M 和 15.7M 個週期,遠高於最大等級(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 |
就每條 sBPF 指令而言,轉譯後程式自身的執行成本低約 2.4 至 2.5 倍(約 62 個週期,對比 154 個)。但每次運行約 98% 是固定的測試框架開銷,而守衛另加 73k 個週期。因此,對 SPL Token 而言,轉譯後的映像反而貴 69k 至 75k 個週期,兩者同樣停留在等級 20。
轉譯只對運算密集的程式有利。 以每條指令節省約 90 個週期計算,一次執行 10,000 條 sBPF 指令的調用可節省約 0.9M 個週期。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. 兩邊運行並比對
250 的 Transfer 需要兩份字列表,由 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
同樣的兩份列表,送入直譯器 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
八個字完全相同。out[0] = 1 是狀態(成功)。out[1..8] 是直譯器的 ABI 對輸入和輸出所定義的摘要。
4. 以一條命令檢查全部八個向量
parity 測試會構建轉譯後的映像,在兩個映像上通過 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 檔案本身;錢包會把它編碼成與步驟 4 相同的 27,151 個字。
這需要一條創世時把 max_program_words 設為至少 65,096、並把 max_program_public_words 設為至少 27,151 的鏈。Chain 12 把一次部署的上限定為 4,096 個字,且不接受公開輸入,因此錢包在該鏈上尚未進行任何證明便拒絕了此映像。Chain 13 即 v0.4 鏈,把兩項上限設為 65,535 和 32,768,其後每條鏈都沿用這兩個數值。現行的 chain 20(創世 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135)同樣沿用 chain 13 的區塊與呼叫信封上限,並把證明上限減半至 4 MiB(4,194,304 位元組,自 chain 17 起;chain 13 至 16 設為 8 MiB)。大小上限已不是唯一的關卡。Chain 20 的創世啟用了 hardening_v6,在此設定下,若部署的程式字數超出一次呼叫在 tier-14 呼叫上限下所能容納的數量,該部署即屬無效(ProgramUncallable):在沒有公開輸入時最多 8,180 個字,附帶公開輸入時則更少。此映像為 65,096 個字,另附 27,151 個字的公開輸入,因此 chain 20 會拒絕這次部署,也會拒絕相應的呼叫:每個 SPL 向量都在 tier 20 運行,高於 tier-14 呼叫上限(限制)。Chain 20 未啟用區塊聚合(其創世沒有 aggregation 一節);其引導節點列於運行驗證者。部署費用把公開字與程式碼字一併計算:0.001 + 0.0001 × (65,096 + 27,151) = 9.2257 RAND,即下文 chain 13 部署所支付的費用。
此次部署於 2026-09-19 在 chain 13 上進行,當時 chain 13 為現行鏈(正式 FRI 配置,創世 8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3),由創世錢包 shielded-1 發出。其後每條鏈(14 至 19)都是不帶 chain 13 程式的全新鏈,此映像至今未在其中任何一條上重新部署,因此下文的交易與高度屬於 chain 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
…
| 程式 id | 740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a |
| 映像 | 65,096 個字;在 rand-guest 的寫法中為 hc 8ca905ae…,在錢包的寫法中為 ae05a98c… |
| 公開輸入 | 27,151 個字(即 ELF),摘要 ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e |
| 費用 bundle(chain 13 的 2 入 2 出 bundle) | tier 14,1,418,406 位元組,證明耗時 94.3 秒 |
| 部署交易(chain 13) | 626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530 |
| 錨定高度(chain 13) | 400 |
| 費用 | 9.2257 RAND |
自 chain 14 起,支付費用的 bundle 是每筆交易都附帶的那個 4 入 4 出隱藏資產 bundle,針對該交易的綁定進行證明。它仍為 tier 14:在 chain 14 和 15 上,以正式配置計約 100 秒、約 1.42 MB。自 chain 17 起,bundle 還附帶一個 tier-10 的授權證明(拆分授權),因此在 chain 18 和 19 上,一筆交易的費用 bundle 約為 1.49 MB 的 bundle 證明加約 1.37 MB 的授權證明。
程式 id 綁定 ELF,因此與 rand-guest info 為裸映像印出的 id 不同。請使用 deploy 印出的 id 進行呼叫。
限制
- 尚未證明任何對此程式的呼叫,它沒有部署在 chain 20 上,而且 chain 20 同時拒絕部署與呼叫。 上述部署是在 chain 13 上進行的。在 chain 20 的
hardening_v6下,部署的字數若超出 tier-14 呼叫所能容納的數量即屬無效(ProgramUncallable;最多 8,180 個字),而此映像有 65,096 個字。輸入方面則可容納:ELF 是部署的公開輸入,由rand call自行取得,而 10,458 個私有字也在 16,071 的輸入上限之內,chain 20 保留此上限。但每個 SPL 向量都是 tier 20,而自 fullnode v0.5.6(2026-09-25)起,節點會在建立驗證金鑰之前,拒絕任何高於 tier 14 的呼叫證明(CallTierTooHigh)。同一項檢查也限制一次呼叫可宣告的哈希表:2¹³ 行 SHA-256(128 次壓縮)和 2¹² 行 Keccak(128 次置換)。tier-20 的呼叫只能在鏈下證明,而目前尚不存在 tier-20 的證明(見下一點)。 - 尚未產生任何 tier 20 的證明。 一次 tier-20 的證明在一部 48 GB 的筆記型電腦上於約 31 GB 時被終止,在一部 64 GB 的 droplet 上則於 10 分 41 秒後在 65.1 GB 時被 OOM 終止。根據實測的 tier-16/18 縮放比例推算(每增加 2 個 tier,記憶體約 ×3.9,時間約 ×4.1),tier 20 需要約 330 GB 和約 3.6 小時,超過 DigitalOcean 最大的記憶體型 droplet(m-32vcpu-256gb,256 GB)。證明者以單執行緒運行:一部 16 vCPU 的機器只有一個核心處於 99% 的使用率。參見證明記憶體。
- 不支援 CPI。 跨程式調用會觸發 trap。
- 僅支援 sBPF v1。 僅 v2 才有的運算碼會觸發 trap。
- Ed25519 和 secp256k1 系統呼叫會觸發 trap,直至它們有協處理器表為止。
- 鏈上只把 ELF 記錄為公開輸入。 帶
--public的部署會儲存 ELF 的字及其摘要,並由rand_getProgramPublic提供。鏈上不記錄映像是如何由它建構出來的:請公開建構輸入,以便他人重新建構hc。