sbpf2rv는 컴파일된 Solana 프로그램(cargo build-sbf가 만들어 내는 .so, 즉 sBPF ELF)을 받아 Rand 프로그램으로 변환합니다. 변환 결과는 C 코드로 출력되며, rand-guest build가 이 C 코드를 RV32IM 이미지로 컴파일합니다. 이 이미지는 같은 ELF와 같은 명령어에 대해 Rand의 sBPF 인터프리터 게스트인 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)으로 해시하여 비교합니다. 다른 프로그램은 사전 상태에 대해 상태 2의BadElf로 중단됩니다. 따라서 이hc에 대해 검증되는 증명은 그 프로그램의 실행입니다. H_PUB가 ELF의 바이트를 묶습니다. ELF는 공개 테이프로 전달되며, 증명의 공개 입력 다이제스트가 이를 워드 단위로 포괄합니다.
hc가 주어진 ELF의 충실한 변환인지는 다시 빌드하여 확인합니다. 고정된 sbpf2rv를 ELF에 실행하고, 고정된 툴체인으로 빌드한 다음, hc를 비교합니다. 빌드가 재현 가능한 것은 이 때문입니다.
체인은 원본 ELF나 그 해시를 기록하지 않습니다. 이미지와 그 프로그램 id를 저장합니다. ELF가 알려지기를 원하는 배포자는 프로그램과 함께 ELF를 공개합니다.
가드가 묶는 것은 파일이 아니라 로드된 프로그램입니다. 로더가 보지 않는 부분(헤더 패딩, 심볼 테이블)에서만 다른 두 ELF는 같은 프로그램으로 로드되어 양쪽에서 동일하게 실행됩니다. .rodata의 바이트가 하나라도 바뀐 ELF는, 전송이 읽지 않는 바이트라 하더라도, 변환본에서는 거부되고 인터프리터에서는 실행됩니다.
트랩되는 것과 거부되는 것
변환 단계에서 거부되는 것은 없습니다. 형식이 잘못된 ELF는 로더 자체의 메시지와 함께 거부되지만, 올바른 형식의 ELF 안에서는 어떤 명령어도 거부되지 않습니다. 스캐너는 프로그램이 마주칠 수 있는 것을 경고로 보고하며, 변환된 프로그램은 인터프리터가 트랩할 바로 그 지점에서 런타임에 트랩합니다.
| 경고 | 의미 | 런타임 동작 |
|---|---|---|
UnknownSyscall | 런타임이 구현하지 않은 syscall 해시 | UnknownSyscall 트랩 |
Cpi | 크로스 프로그램 호출(sol_invoke_signed*) | 동일한 UnknownSyscall 트랩. CPI는 구현되어 있지 않습니다 |
RegisterOutOfRange | 레지스터 r11..r15, 또는 r10을 넘는 callx 레지스터 | BadInsn 트랩 |
UnknownOpcode | v2 전용 sdiv/srem 계열과 hor64를 포함해, v1 클래스가 없는 opcode | BadInsn 트랩 |
JumpOutOfText | 함수의 텍스트 밖으로 향하는 점프, 호출 또는 fall-through | 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 |
잘못된 authority에 의한 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 스택과 힙 0으로 채우기 | 49,181 | 49,181 |
| 출력 및 입력 해시, 공개 워드, glue | 51,091 | 42,723 |
| ELF 가드 | 72,949 | — |
| sBPF 프로그램 자체 | 8,908 | 22,031 |
| 합계 | 765,851 | 694,498 |
변환된 프로그램 자체의 실행은 sBPF 명령어당 약 2.42.5배 저렴합니다(약 62 사이클 대 154 사이클). 그러나 모든 실행의 약 98%는 고정된 하네스가 차지하며, 가드가 73k 사이클을 더합니다. 따라서 SPL Token에서는 변환된 이미지가 69k75k 사이클 더 비싸고, 둘 다 티어 20에 머뭅니다.
변환이 이득이 되는 것은 연산 부하가 큰 프로그램뿐입니다. 명령어당 약 90 사이클을 절약하므로, sBPF 명령어 10,000개를 실행하는 호출은 약 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
같은 두 목록을 인터프리터 게스트에 넣은 결과:
$ 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 이상으로 설정된 체인이 필요합니다. 체인 12는 배포 한도가 4,096 워드였고 공개 입력을 받지 않았으므로, 지갑은 증명을 시작하기도 전에 이 이미지를 거부했습니다. v0.4 체인인 체인 13은 두 값을 65,535와 32,768로 설정했고, 이후의 모든 체인이 두 값을 유지합니다. 현재 운영 중인 체인 20(제네시스 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135)도 체인 13의 블록 및 호출 envelope 한도를 유지하며, 증명 크기 상한은 절반인 4 MiB(4,194,304바이트, 체인 17부터 적용, 체인 13~16은 8 MiB)로 낮췄습니다. 이제 크기 상한만이 유일한 관문은 아닙니다. 체인 20의 제네시스는 hardening_v6를 설정하며, 이에 따라 tier-14 호출 상한에서 한 번의 호출이 담을 수 있는 것보다 많은 프로그램 워드를 배포하면 유효하지 않은 배포가 됩니다(ProgramUncallable). 공개 입력이 없으면 최대 8,180 워드이고, 공개 입력이 함께 있으면 그보다 적습니다. 이 이미지는 27,151 워드의 공개 입력과 함께 65,096 워드이므로 체인 20은 배포를 거부하며, 호출도 거부할 것입니다. 모든 SPL 벡터가 tier 20에서 실행되어 tier-14 호출 상한을 넘기 때문입니다(한도). 체인 20에서는 블록 집계가 꺼져 있습니다(제네시스에 aggregation 섹션이 없습니다). 부트스트랩 노드는 검증자 실행에 나와 있습니다. 배포 수수료는 코드 워드와 공개 워드를 함께 계산합니다. 0.001 + 0.0001 × (65,096 + 27,151) = 9.2257 RAND이며, 아래 체인 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
…
| program id | 740236918310f8e52bb0c1ef49b2b0e0c018762e289666660685b0694c8dd00a |
| image | 65,096 워드, rand-guest 표기로 hc 8ca905ae…, 지갑 표기로 ae05a98c… |
| public input | 27,151 워드(ELF), 다이제스트 ec57b10ed6f3a3d5fe87ec072f691322ad747377ba2f0a1788bcfe3e7334d67e |
| fee bundle (체인 13의 2-in-2-out 번들) | tier 14, 1,418,406바이트, 94.3초에 증명 |
| deploy tx (체인 13) | 626fc9387ecc1f6b39f44184807303ac2843029d8fb8e28338bf186445927530 |
| anchored at height (체인 13) | 400 |
| fee | 9.2257 RAND |
체인 14부터 수수료를 지불하는 번들은 모든 트랜잭션이 담는 하나의 4-in-4-out hidden-asset 번들이며, 트랜잭션의 바인딩에 대해 증명됩니다. 이 번들은 여전히 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로 호출하십시오.
한도
- 이 프로그램에 대한 호출은 증명된 적이 없고, 체인 20에 배포되어 있지 않으며, 체인 20은 배포와 호출을 모두 거부합니다. 위의 배포는 체인 13에서 이루어졌습니다. 체인 20의
hardening_v6에서는 tier-14 호출이 담을 수 있는 것보다 많은 워드의 배포가 유효하지 않으며(ProgramUncallable, 최대 8,180 워드), 이 이미지는 65,096 워드입니다. 입력 측면은 맞을 것입니다. ELF는 배포의 공개 입력이며rand call이 직접 가져오고, 10,458개의 비공개 워드는 체인 20이 유지하는 입력 상한 16,071에 들어갑니다. 그러나 모든 SPL 벡터는 tier 20이고, fullnode v0.5.6(2026-09-25)부터 노드는 tier 14를 넘는 모든 호출 증명을 검증 키를 만들기 전에 거부합니다(CallTierTooHigh). 같은 검사는 호출이 선언할 수 있는 해시 테이블도 제한합니다. SHA-256 행 2¹³개(압축 128회)와 Keccak 행 2¹²개(순열 128회)입니다. tier-20 호출은 체인 밖에서만 증명할 수 있으며, tier-20 증명은 아직 존재하지 않습니다(다음 항목). - tier 20에서 만들어진 증명은 아직 없습니다. tier-20 증명은 48 GB 노트북에서 약 31 GB에서 중단되었고, 64 GB 드롭릿에서는 10분 41초 후 65.1 GB에서 OOM으로 종료되었습니다. 측정된 tier-16/18 스케일링(2 tier 증가당 메모리 약 ×3.9, 시간 약 ×4.1)으로 외삽하면 tier 20에는 약 330 GB와 약 3.6시간이 필요하며, 이는 DigitalOcean의 최대 메모리 드롭릿(m-32vcpu-256gb, 256 GB)보다 큽니다. 프로버는 단일 스레드로 실행되었고, 16-vCPU 장비에서 코어 하나가 99%를 사용했습니다. 증명 메모리를 참고하십시오.
- CPI 없음. 프로그램 간 호출(Cross-program invocation)은 트랩됩니다.
- sBPF v1만 지원합니다. v2 전용 opcode는 트랩됩니다.
- Ed25519 및 secp256k1 syscall은 코프로세서 테이블이 생기기 전까지 트랩됩니다.
- 체인은 ELF를 공개 입력으로만 기록합니다.
--public으로 배포하면 ELF의 워드와 그 다이제스트가 저장되고,rand_getProgramPublic이 이를 제공합니다. 체인은 이미지가 ELF에서 어떻게 빌드되었는지는 기록하지 않습니다. 다른 사람이hc를 다시 빌드할 수 있도록 빌드 입력을 공개하십시오.