이 페이지는 작은 Rust 프로그램을 소스에서 Rand에 배포된 프로그램까지 옮기는 과정을 다룹니다. 아래의 모든 rand-guest 명령은 실제로 실행했으며, 출력은 요약하지 않고 그대로 붙여 넣었습니다. 실행은 2026-09-19에 circuits 커밋 7ef3220에서 이루어졌습니다. 현재 testnet은 chain 20이며, 이 체인의 툴체인은 chain 18, 19와 마찬가지로 aeacf31에서 빌드됩니다(fullnode v0.6.8은 v0.6.7과 같은 커밋을 고정합니다). 출력은 해당 체인에서 다시 실행하지 않았습니다. RISC-V ISA에서 머신, 시스템 콜, 파일 형식을 설명합니다.
1. 툴체인 설치
hc는 컴파일러의 정확한 버전에 의존하므로 툴체인을 고정합니다.
| 도구 | 버전 | 용도 |
|---|---|---|
| Rust | 1.98.1 | 게스트와 rand-guest 자체를 컴파일합니다 |
riscv32im-unknown-none-elf 타깃 | 1.98.1용 | 게스트의 타깃입니다 |
| circuits 체크아웃 | chain 18~20은 aeacf31의 randprotocol/zkp-circuits | rand-guest, guest-sdk, 링커 스크립트를 포함합니다 |
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31 # branch feat/v067-circuits, what fullnode v0.6.7 and v0.6.8 pin
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"
rand-guest에는 다섯 개의 하위 명령이 있습니다. build, check, pack, run, info입니다.
커밋이 중요한 이유는 hc가 소스와 툴체인의 함수이기 때문입니다. 다른 guest-sdk 커밋에 맞춰 다시 빌드한 게스트는 다른 hc를 가질 수 있습니다. 7ef3220과 aeacf31 사이에서 rand-guest 디렉터리는 바뀌지 않았고, guest-sdk/src/lib.rs는 바뀌었습니다(2단계 참조).
2. 게스트 작성
게스트는 guest-sdk에 의존하는 no_std, no_main 바이너리 크레이트입니다. 반드시 circuits 체크아웃 안에 있어야 합니다. rand-guest build는 게스트 디렉터리 위쪽에서 guest-sdk/를 찾아 체크아웃의 위치를 파악하며, 체크아웃 밖에 있는 게스트는 거부합니다.
이 게스트는 임계값, 개수, 그리고 그 개수만큼의 비공개 잔액을 읽습니다. 잔액의 합이 임계값을 충족하면 1과 초과분을 공개합니다. 잔액 자체는 공개하지 않습니다.
circuits/
guest-sdk/
rand-guest/
my-guests/
threshold/
Cargo.toml
src/main.rs
my-guests/threshold/Cargo.toml:
[package]
name = "threshold"
version = "0.1.0"
edition = "2021"
[dependencies]
guest-sdk = { path = "../../guest-sdk" }
[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1
[workspace]
비어 있는 [workspace]는 커밋된 fib 게스트와 마찬가지로 이 크레이트를 상위 워크스페이스에서 분리합니다.
my-guests/threshold/src/main.rs:
#![no_std]
#![no_main]
use guest_sdk::{halt, read_input, write_output};
/// Input 0 is a threshold, input 1 a count n, inputs 2..2+n are private balances.
/// Output 0 is 1 if the balances sum to at least the threshold, else 0.
/// Output 1 is the surplus when they do. The balances themselves are never published.
#[no_mangle]
pub extern "C" fn main() -> ! {
let threshold = read_input(0);
let n = read_input(1);
let mut sum: u32 = 0;
for i in 0..n {
sum = sum.wrapping_add(read_input(2 + i));
}
if sum >= threshold {
write_output(0, 1);
write_output(1, sum - threshold);
}
halt();
}
guest-sdk는 read_input, read_public, write_output, poseidon2, keccak, keccak256, sha256_compress, sha256, halt를 내보내고, 스택 포인터를 설정한 뒤 main을 호출하는 _start를 제공합니다. 할당자와 부동소수점은 없습니다.
aeacf31에서는 이 가운데 세 함수의 시그니처가 7ef3220과 다르며, 새로 내보내는 함수가 하나 있습니다.
| 함수 | 7ef3220에서 | aeacf31에서 |
|---|---|---|
keccak | fn keccak(ptr: *mut u32) | fn keccak(state: &mut [u32; 50]) |
sha256_compress | fn sha256_compress(ptr: *mut u32) | fn sha256_compress(buf: &mut [u32; 24]) |
poseidon2 | fn poseidon2(ptr: *mut u32, n: usize) | unsafe fn poseidon2(ptr: *mut u32, n: usize) |
poseidon2_len | 없음 | fn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8], 시스템 콜 7 |
poseidon2_len은 메시지 길이를 스펀지에 결합하며, 안전한 호출입니다. aeacf31의 rand-guest check는 여전히 시스템 콜 0~6만 나열하므로, 이 함수를 호출하는 게스트는 build가 ecall with a7 = 7, which is no syscall the machine implements라며 거부하는 이미지로 빌드됩니다(zkp-circuits issue #1). 이 문제가 수정될 때까지 게스트는 길이가 고정되고 태그가 붙은 메시지에 대해 unsafe { poseidon2(ptr, n) }로 해시합니다. 이 페이지의 게스트는 네 함수를 모두 사용하지 않습니다.
게스트 디렉터리에 대한 규칙은 다음과 같습니다.
- 게스트 디렉터리, 모든 상위 디렉터리,
$CARGO_HOME에.cargo/config또는.cargo/config.toml이 없어야 합니다. cargo가 이 파일을 빌드에 병합하므로build는 거부하고 해당 파일을 각각 지목합니다. - 자체 컴파일러 플래그를 쓰지 않습니다.
build가 링커 스크립트,-unaligned-scalar-mem, 이미지를 체크아웃 위치와 무관하게 만드는 경로 재매핑 등 모든 플래그를 설정합니다. .ld파일은 최대 하나입니다.build는 게스트 디렉터리에 있는 하나의.ld파일을 사용하며, 없으면guest-sdk/guest.ld를 사용합니다. 둘 이상이면--ld로 지정합니다.
3. 빌드와 검사
$ rand-guest build my-guests/threshold --out threshold.bin
Compiling guest-sdk v0.1.0 (…/circuits/guest-sdk)
warning: unstable feature specified for `-Ctarget-feature`: `unaligned-scalar-mem`
|
= note: this feature is not stably supported; its behavior can change in the future
warning: `guest-sdk` (lib) generated 1 warning
Compiling threshold v0.1.0 (…/circuits/my-guests/threshold)
warning: `threshold` (bin "threshold") generated 1 warning (1 duplicate)
Finished `release` profile [optimized] target(s) in 0.15s
40 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote threshold.bin and its .sha256 (40 words, hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665, program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853)
unaligned-scalar-mem 경고는 예상된 것입니다. 툴체인이 비정렬 접근을 의도적으로 끕니다.
build는 cargo +1.98.1 build --release --target riscv32im-unknown-none-elf로 컴파일하고, 결과에 check를 실행한 뒤, 이미지 컨테이너와 .sha256 핀을 기록합니다. check가 프로그램을 거부하더라도 build는 이미지를 기록하고 0이 아닌 값으로 종료합니다.
| 플래그 | 기본값 | 의미 |
|---|---|---|
--out <path> | <dir>/image.bin | 이미지가 저장되는 위치 |
--max-words <n> | 4096 | check가 기준으로 삼는 상한. 대상 체인의 상한을 지정합니다(chain 20에서는 65,535) |
--ld <path> | 디렉터리에 있는 하나의 .ld, 없으면 guest-sdk/guest.ld | 링커 스크립트 |
--lang rust|c | rust | 컴파일러 |
4. 에뮬레이터에서 실행
비공개 입력은 --input으로 전달하며, 워드 하나당 하나씩입니다. --tier t는 해당 실행이 티어 t에 들어맞는지도 함께 보고합니다.
$ rand-guest run threshold.bin --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75 --tier 10
out[0] = 1
out[1] = 25
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 52
tier 10
tier 10: fits (cycles 66 of 1023, Poseidon2 permutations 14 of 128)
잔액의 합이 1,025이므로 프로그램은 1과 초과분 25를 공개합니다. 잔액이 400과 250이면 합이 부족하며 모든 출력이 0입니다. 트랩이 발생하면 trap: <error>를 출력하고 2로 종료합니다.
5. 이미지 검사
$ rand-guest info threshold.bin
form: image container
text 40 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 40 words from base_pc 0x1000
hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665
program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
40 words against a cap of 4096: fits
hc는 호출의 증명이 검증되는 기준입니다. 프로그램 id는 배포하고 호출할 때 사용하는 식별자입니다.
6. 배포
fullnode v0.3 이상의 rand 지갑을 사용하십시오. 이전 지갑은 이미지 컨테이너를 읽지 못합니다. chain 14부터 모든 번들 증명은 해당 트랜잭션의 바인딩에 대해 검증되므로, chain-14 빌드(b3c594c)보다 오래된 지갑은 체인이 거부하는 번들을 증명합니다. 현재 운영 중인 chain 20에서는 fullnode v0.6.8 이상의 rand를 사용하십시오(플릿은 v0.6.8을 실행하며, Linux에서 빌드한 rand가 해당 GitHub 릴리스에 첨부되어 있습니다). v0.6.7 지갑은 chain 20에서 전송할 수 없습니다. chain 20의 제네시스가 binding_domain: 1을 설정하므로 모든 증명 바인딩과 서명된 메시지에 제네시스 해시가 포함되는데, v0.6.7 지갑은 체인 id만 바인딩하기 때문입니다. 공개 엔드포인트 https://rpc.randprotocol.org만으로 지갑 동기화, 배포, 호출을 수행할 수 있습니다(설정).
rand fee deploy 40 # the floor for a 40-word program
rand program deploy threshold.bin
program deploy는 프로그램 id, 워드 수, hc를 출력합니다. 그다음 rand_estimateFee로 노드에 배포 수수료를 조회하는데, 이 호출은 증명을 수행하기 전에 체인의 상한을 초과하는 프로그램을 거부합니다. 이어서 수수료를 지불하는 가치 0의 자기 전송 번들을 증명하여 제출하고, 커밋을 기다립니다. 이 번들은 모든 트랜잭션이 포함하는 4-in-4-out 은닉 자산 번들입니다. chain 17부터는 두 개의 증명으로 구성되며 지갑은 각각에 대해 한 줄씩 출력합니다. 티어 10 지출 승인(수 초, 약 1.37 MB)과 티어 14 번들(노트북에서 약 2분, 약 1.5 MB)입니다. chain 15에서는 약 100초, 약 1.42 MB의 티어 14 증명 하나였습니다. 익명 투표에서 chain 18의 배포를 지갑이 출력한 그대로 볼 수 있습니다.
배포 수수료는 0.001 RAND + 0.0001 RAND × words이며, chain 20에서도 같습니다. 가스는 배포가 아니라 호출에 부과됩니다.
| 프로그램 | 워드 | 배포 수수료 |
|---|---|---|
threshold | 40 | 0.005 RAND |
| 기본 상한의 프로그램(chain 12) | 4,096 | 0.4106 RAND |
| 공개 세그먼트 없이 chain 20이 배포하는 가장 큰 프로그램 | 8,180 | 0.819 RAND |
| 65,535 워드 상한의 프로그램(chain 13~15) | 65,535 | 6.5545 RAND |
threshold는 4,096 워드 미만이므로 기본 상한과 chain 20의 65,535 모두에 들어맞습니다. 4,096 워드를 넘는 프로그램은 제네시스가 max_program_words를 그 이상으로 설정한 체인(예: chain 20)이 필요합니다(RISC-V ISA). chain 20은 chain 19와 마찬가지로 더 낮은 두 번째 한계를 갖습니다. 어떤 티어 14 호출도 담을 수 없는 프로그램은 배포를 거부하며(ProgramUncallable), 그 크기는 공개 세그먼트가 없으면 8,180 워드 초과이고, 있으면 그보다 작습니다. rand program deploy --input <word> …(입력이 없는 호출이면 --check-call)는 염두에 둔 호출을 먼저 에뮬레이터로 실행하고, 결과가 티어 14 이하가 아니면 배포를 거부합니다.
레코드를 확인합니다.
rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
code_hash는 rand-guest가 출력한 hc와 같은 값이며 표기만 다릅니다. rand-guest는 다이제스트의 여덟 워드를 각각 빅 엔디안으로 출력하고, 지갑, rand program show, rand_getProgram은 각 워드의 바이트를 리틀 엔디안으로 출력합니다. 그래서 a491df99…3665는 99df91a4…707c로 나타납니다. 8자리 16진수 그룹마다 바이트 순서가 뒤집힌 것입니다. 프로그램 id는 두 도구에서 표기가 같습니다.
7. 호출
rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
--input 1000 --input 4 --input 400 --input 250 --input 300 --input 75
지갑은 코드를 가져와 에뮬레이터에서 호출을 실행하고, 증명을 수행하기 전에 gas: <exact> (declaring <limit>, tier <t>)와 수수료를 출력합니다. 이어서 입력 트랜스크립트를 봉인하고, 실행을 로컬에서 증명하고, 번들로 수수료를 지불한 뒤, 영수증을 출력합니다. chain 20에서는 호출, 티어 10 지출 승인, 티어 14 번들의 세 가지 증명이 수행됩니다.
chain 20의 수수료는 더 이상 0.001 + call_fee(tier)가 아닙니다. 이 공식에 따르면 chain 15에서 이 호출의 수수료는 0.002 RAND였습니다. 하한은 BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉입니다. 즉 0.001 RAND에, 선언한 한도에 gas price를 곱한 값(genesis 시점에 100 units, 1 unit은 10⁻⁹ RAND)과, 증명 및 envelope 바이트에 byte price를 곱한 값(genesis 시점에 KiB당 800 units)을 더한 금액입니다. chain 20에서 두 가격은 각각 10,000 units와 80,000 units를 넘어 오를 수 없습니다. 정확한 gas는 비공개로 유지됩니다. 기본적으로 지갑은 gas를 2^(tier − 2)의 배수로 올림하여 선언하고, 증명이 만들어지기 전에 수수료가 확정되므로 증명을 체인의 상한인 4,194,304바이트로 계산하여 가격을 책정하며, 가격 단계 두 번만큼의 여유분을 더합니다. chain 18에서 측정한 네 번의 호출은 각각 0.00570.0075 RAND가 들었습니다(Example programs). 이 호출은 chain 1820에서 실행된 적이 없으므로 수치를 제시하지 않습니다.
체인은 tier, 선언한 gas 한도, H_IN, 그리고 여덟 개의 출력 워드 [1, 25, 0, 0, 0, 0, 0, 0]를 확인합니다. 잔액은 전혀 확인하지 못합니다.
Programs에서는 영수증, 입력 envelope, rand open-call을 다룹니다. 이와 같은 Call은 상태를 갖지 않습니다. chain 20에서는 프로그램이 Invoke를 통해 cell과 vault를 보관할 수도 있습니다(Program state (RPL-2)).
다른 사람이 hc를 확인할 수 있게 하기
빌드는 재현 가능합니다. build는 컴파일러 환경(RUSTFLAGS, CARGO_* 재정의, RUSTC 래퍼 등)을 정리하고, cargo 설정 파일을 거부하며, 체크아웃 경로를 /rand-circuits로 다시 매핑합니다. 같은 소스를 Rust 1.98.1로 빌드하면 어느 머신에서든 같은 hc가 나옵니다. 배포한 내용을 다른 사람이 확인할 수 있도록 guest의 소스와 circuits 커밋(chain 18~20용으로 빌드한 guest는 aeacf31)을 공개하십시오. 그러면 누구든 다시 빌드하여 hc를 rand_getProgram의 code_hash와 비교할 수 있습니다. 체인에는 소스가 아니라 워드만 저장됩니다.
문제 해결
| 메시지 | 원인 |
|---|---|
… is not inside a circuits checkout (no guest-sdk/ above it) | guest 디렉터리를 체크아웃 안으로 옮기십시오 |
build가 .cargo/config.toml을 지목함 | 삭제하거나 옮기십시오. 정리할 수 없습니다 |
Cap과 함께 REJECTED | 프로그램이 --max-words를 초과했습니다. 대상 체인의 상한까지 값을 올리거나 프로그램을 줄이십시오(opt-level = "s") |
Syscall 및 a7 = 7과 함께 REJECTED | guest가 poseidon2_len을 호출합니다. aeacf31의 rand-guest check는 아직 syscall 7을 인식하지 못합니다(zkp-circuits issue #1) |
Fence, Csr 또는 Ebreak와 함께 REJECTED | 빌드가 머신에 없는 명령어를 생성했습니다. what is refused를 참조하십시오 |
run에서 trap: … | 정렬되지 않은 주소나 구현되지 않은 syscall 번호 같은 실행 중 오류입니다 |
run이 어떤 tier에도 맞지 않는다고 출력함 | 실행이 2²⁰ 행 또는 Poseidon2 예산을 초과했습니다. 증명할 수 없습니다 |