sbpf2rv 接收一个已编译的 Solana 程序(sBPF ELF,即 cargo build-sbf 生成的 .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 停机,并保持前状态不变。所以,能够针对该hc通过验证的证明,就是该程序的一次运行。 H_PUB约束 ELF 的字节。 ELF 通过公开带传入,证明的公开输入摘要逐字覆盖它。
hc 是否为给定 ELF 的忠实翻译,通过重新构建来检查。对该 ELF 运行固定版本的 sbpf2rv,用固定的工具链构建,然后比较 hc。构建可复现正是出于这个原因。
链不记录源 ELF 或其哈希,只存储镜像及其程序 id。希望让 ELF 为人所知的部署者,应将其与程序一并发布。
守卫约束的是已加载的程序,而不是文件。两个 ELF 如果只在加载器不查看的地方不同(头部填充、符号表),就会加载为同一个程序,在两侧运行结果一致。若 ELF 的 .rodata 中有一个字节被改动,即使转账从不读取该字节,翻译也会拒绝它,而解释器会运行它。
什么会触发陷阱,什么会被拒绝
翻译阶段不拒绝任何内容。 格式错误的 ELF 会被拒绝,并附带加载器自身的消息,但格式正确的 ELF 中没有任何指令会被拒绝。扫描器把程序可能遇到的情况作为警告报告,翻译后的程序在运行时恰好在解释器会触发陷阱的位置触发陷阱:
| 警告 | 含义 | 运行时 |
|---|---|---|
UnknownSyscall | 运行时未实现的 syscall 哈希 | 触发陷阱 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 的翻译与比对
每条命令都从位于 7ef3220 的 circuits 检出目录根部运行,工具链来自部署 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 从不会执行到的真实 syscall:哈希 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. 一条命令检查全部八个向量
一致性测试会构建翻译后的镜像,在两个镜像上通过 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
镜像从公开磁带(public tape)读取 ELF,因此 ELF 是程序的公开输入,在部署时即已固定。--public 接收 .so 文件本身;钱包会将其按字编码为与第 4 步相同的 27,151 个字。
这需要一条创世配置将 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,创世哈希 ca76c2766ed6090ac6ba5dad86b724dd5d2b80c0edfaed287cd8b129e663ca9a),同样保持链 13 的调用信封(call-envelope)上限,将证明大小上限设为 6 MiB(6,291,456 字节;链 17 至 20 为 4 MiB,链 13 至 16 为 8 MiB),将区块大小上限设为 19 MiB。大小上限已不再是唯一的门槛。链 21 的创世配置与链 20 和链 19 一样设置了 hardening_v6,在此设置下,若部署的程序字数超过一次调用在 tier-14 调用上限下所能容纳的字数,则该部署无效(ProgramUncallable):无公开输入时最多 8,180 个字,有公开输入时则更少。该镜像为 65,096 个字,并带有 27,151 个字的公开输入,因此链 21 会拒绝这次部署,同样也会拒绝调用:每个 SPL 向量都运行在 tier 20,高于 tier-14 调用上限(限制)。链 21 上未启用区块聚合(其创世配置中没有 aggregation 部分);其引导节点列于运行验证者。部署费用将公开字与代码字一并计算:0.001 + 0.0001 × (65,096 + 27,151) = 9.2257 RAND,即下文链 13 上的部署所支付的费用。
此次部署于 2026-09-19 在链 13 上运行,当时链 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
…
| 程序 id | 740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a |
| 镜像 | 65,096 个字;在 rand-guest 的写法中为 hc 8ca905ae…,在钱包的写法中为 ae05a98c… |
| 公开输入 | 27,151 个字(即 ELF),摘要 ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e |
| 费用捆绑包(链 13 的 2 入 2 出捆绑包) | tier 14,1,418,406 字节,证明耗时 94.3 秒 |
| 部署交易(链 13) | 626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530 |
| 锚定高度(链 13) | 400 |
| 费用 | 9.2257 RAND |
自链 14 起,支付用的捆绑包是每笔交易都携带的那个 4 入 4 出隐藏资产捆绑包,针对交易的绑定(binding)生成证明。它仍为 tier 14:在链 14 和 15 上,生产配置下约 100 秒、约 1.42 MB。自链 17 起,捆绑包还携带一个 tier-10 授权证明(拆分授权),因此在链 18 和 19 上,一笔交易的费用捆绑包约为 1.49 MB 的捆绑包证明和约 1.37 MB 的授权证明。
程序 id 绑定了 ELF,因此它不同于 rand-guest info 为裸镜像打印的 id。请调用 deploy 打印的那个 id。
限制
- 尚未证明过对该程序的任何调用,它未部署在链 21 上,且链 21 会同时拒绝部署和调用。 上述部署发生在链 13 上。在链 21 的
hardening_v6下,部署字数超过 tier-14 调用所能容纳的程序是无效的(ProgramUncallable;最多 8,180 个字),而该镜像为 65,096 个字。输入方面是可以容纳的:ELF 是部署的公开输入,由rand call自行获取,10,458 个私有字也在 16,071 的输入上限之内,链 20 和 21 保持该上限。但每个 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 秒后因内存耗尽(OOM)在 65.1 GB 处被终止。根据实测的 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 的操作码会触发陷阱。
- Ed25519 和 secp256k1 系统调用会触发陷阱,直到它们有了协处理器表。
- 链仅将 ELF 记录为公开输入。 使用
--public的部署会存储 ELF 的字及其摘要,rand_getProgramPublic提供这些数据。链不记录镜像是如何由它构建的:请公布构建输入,以便他人重建hc。