이 페이지는 Rand에 직접 작성한 프로그램을 올리는 데 필요한 모든 내용을 다룹니다. 프로그램이 될 수 있는 것, 프로그램이 읽고 게시할 대상을 설계하는 방법, 그리고 Rust와 C로 각각 한 번씩 작성한 완전한 예제입니다. 이어서 테스트, 증명 예산, 배포, 호출, 그리고 누구나 결과를 확인하는 방법을 다룹니다. 아래의 모든 rand-guest 출력은 실제 실행에서 나온 것이며 출력된 그대로 붙여 넣었습니다. 실행은 2026-09-25에 circuits 커밋 7ef3220에서 수행했습니다. 현재 testnet은 chain 20이며, 이 체인의 툴체인은 chain 18 및 19와 마찬가지로 aeacf31(브랜치 feat/v067-circuits, fullnode v0.6.8이 같은 커밋을 고정합니다)에서 빌드되고, 출력은 그곳에서 다시 실행하지 않았습니다. SDK나 체인이 다른 부분은 본문에서 따로 밝힙니다.
두 개의 짧은 튜토리얼이 빠른 경로입니다. Deploy a Rust program과 Deploy a RISC-V program입니다. 이 페이지는 Rust 튜토리얼의 1단계를 마쳤다고 가정합니다. 즉 aeacf31의 randprotocol/zkp-circuits 체크아웃, riscv32im-unknown-none-elf 타깃이 설치된 Rust 1.98.1, rand-guest 바이너리를 갖추고 있어야 하며, C의 경우 clang 23.1.1과 llvm-tools도 필요합니다. The RISC-V ISA는 머신 자체에 대한 레퍼런스입니다. Example programs는 여기서부터 더 큰 프로그램과 chain 18의 가스 계량이 바꾼 점을 이어서 다룹니다. 이 페이지는 Call이 상태 없이 실행하는 RPL-1 프로그램을 다룹니다. chain 20에는 Invoke로 변경되는 RPL-2 프로그램 상태, 셀, 볼트도 있으며(Programs), 완전한 예제는 randprotocol/example-programs에 있습니다.
1. 프로그램이 될 수 있는 것
프로그램은 다음 형태에 맞는 모든 계산입니다.
- 입력으로 32비트 워드를 읽습니다.
- 출력으로 최대 여덟 개의 32비트 워드를 게시합니다.
- 결정적입니다. 같은 입력은 항상 같은 출력을 냅니다.
- 해당 티어의 예산 안에서 정지합니다(7절).
이 한도 안에서는 무엇이든 작성할 수 있습니다. 루프, 재귀, 구조체, 배열, 64비트 산술, 크레이트를 쓸 수 있고, 할당자를 가져오면 힙도 쓸 수 있습니다. 다만 머신은 순수한 RV32IM이며 그 아래에 운영체제는 없습니다.
| 사용할 수 있는 것 | 사용할 수 없는 것 |
|---|---|
MUL/DIV/REM을 포함한 RV32IM 정수 명령어 | 부동소수점 하드웨어(Rust의 소프트웨어 부동소수점은 동작하지만 느립니다) |
기본 링커 스크립트에서 0x1000부터 최대 1 MiB의 RAM, 64 KiB 스택 | 힙(할당자를 제공하는 경우는 예외입니다, 6절) |
| 시스콜로 제공되는 Poseidon2, Keccak-f, SHA-256 | 파일, 네트워크, 시계, 난수, 스레드, std, libc |
no_std Rust 크레이트와 자체 완결적인 C | 한 호출에서 다음 호출로 이어지는 상태 |
| 여덟 개의 공개 출력 워드 | 프로그램 내부에서 RAND나 토큰을 이동하는 방법 |
프로그램을 설계할 때 가장 중요한 것은 마지막 행입니다. 출력은 효과가 아니라 데이터입니다. 호출은 특정 다이제스트를 가진 프로그램이 어떤 비공개 입력에서 실행되어 여덟 개의 워드를 게시했음을 증명합니다. 체인은 증명을 검증하고 여덟 개의 워드를 영수증에 저장합니다. 어떤 잔액도 바꾸지 않으며 아무것도 촉발하지 않습니다. 그 워드가 무엇을 뜻하는지는 프로그램, 호출자, 그리고 영수증을 읽는 사람 사이의 합의로 정해집니다. 체인이 아직 지원하지 않는 항목은 Programs에 정리되어 있습니다.
2. 인터페이스를 먼저 설계하십시오
프로그램에는 세 개의 채널이 있습니다. 코드를 작성하기 전에 각 채널에 무엇을 실을지 정하십시오.
| 채널 | 설정 시점 | 읽는 방법 | 볼 수 있는 대상 | chain 20의 한도 |
|---|---|---|---|---|
| 비공개 입력 | 매 호출 | read_input(i) / rand_read_input(i) | 호출자와, 호출자가 해당 호출의 키를 넘겨준 사람뿐 | 16,071 워드(봉인된 입력 봉투의 상한) |
| 공개 세그먼트 | 배포 시 한 번 | read_public(i) / rand_read_public(i) | 모두. 프로그램 id가 이를 바인딩합니다 | 32,768 워드 |
| 출력 | 실행마다 | write_output(slot, w) / rand_write_output | 영수증을 통해 모두 | 8 워드 |
비공개 입력은 그 자체로는 세계에 대해 아무것도 증명하지 않습니다
비공개 입력은 솔트가 적용된 커밋먼트 H_IN으로 증명에 바인딩되므로 증명 이후에는 바뀔 수 없습니다. 그러나 프로그램은 그 입력이 참인지 알 방법이 없습니다. 호출자는 어떤 숫자든 입력할 수 있습니다. 비공개 입력에 의미를 부여하려면 프로그램 내부에서 공개된 무언가에 묶어야 합니다.
- 앞서 게시된 커밋먼트(공개 세그먼트에 있거나 대역 외로 검증자에게 전달된 것)를 비공개 입력이 열어야 하도록 합니다. 아래의 경매가 이 방식입니다.
- 비공개 리프가 그 아래에서 증명되어야 하는 Merkle 루트를 사용합니다(이를 위한 시스콜이 Poseidon2입니다).
- 검증자가 이미 가지고 있는 다이제스트를 프로그램이 다시 계산해 출력으로 게시하도록 합니다.
이것이 기밀 프로그램 설계의 핵심 기술입니다. 증명은 검증자에게 무엇이 계산되었는지를 알려 줍니다. 공개 데이터에 대한 바인딩은 무엇에 대해 계산되었는지를 알려 줍니다.
데이터를 워드로 인코딩하기
모든 것은 u32로 전달됩니다.
| 데이터 | 인코딩 |
|---|---|
| 바이트열 | 길이 워드 하나, 이어서 워드당 네 바이트를 리틀 엔디언으로, 마지막 워드는 0으로 패딩(지갑이 ELF 공개 입력에 사용하는 형식) |
u64 또는 금액 | 두 워드, 하위 워드 다음에 상위 워드 |
| 부호 있는 정수 | 한 워드의 2의 보수 |
| 큰 결과 | 그 Poseidon2 다이제스트인 8 워드(출력을 모두 채웁니다). 또는 몇 개의 요약 워드와 더 짧은 커밋먼트를 게시합니다 |
명령줄 도구는 입력 워드를 10진수로 받습니다. rand-guest run --input과 rand call --input은 0x…를 받지 않습니다. rand program deploy --public에 쓰는 공개 세그먼트 파일은 10진수와 0x 16진수를 모두 받습니다.
출력 구성하기
- 슬롯 0은 상태 워드입니다. 실패를 포함해 각 결과에 번호를 부여하십시오. 일치하지 않는 오프닝, 너무 많은 항목, 범위를 벗어난 입력 등이 그 예입니다. 검사에 실패하더라도 상태 값과 함께 정상적으로 정지해야 호출자가 무엇이 잘못되었는지 말해 주는 증명을 받을 수 있습니다.
- 각 슬롯은 최대 한 번만 기록합니다. 같은 슬롯에 두 번째로 쓰면 트랩입니다(에뮬레이터에서는
DoubleWrite(0)). 트랩이 발생한 실행은 증명할 수 없습니다. 한 번도 쓰지 않은 슬롯은 0으로 읽힙니다. - Rust에서는 슬롯 7을 비워 두십시오. 패닉이 발생한 Rust 게스트는 슬롯 7에
0xdeadbeef(3,735,928,559)를 쓰고 정지합니다. 패닉 이전에 기록한 출력은 그대로 남으므로, 독자는 다른 슬롯을 믿기 전에 슬롯 7을 확인해야 합니다(6절). - 주어진 입력을 넘어 읽으면 트랩입니다(에뮬레이터에서는
InputIndex(5)). 먼저 개수를 읽고 확인하십시오.
3. 실전 예제: 봉인 입찰 경매
판매자가 낙찰되지 못한 입찰가를 공개하지 않고 차가 입찰(second-price) 경매를 열려고 합니다.
- 각 입찰자는 입찰가와 무작위 128비트 솔트를 정하고, 판매자에게 커밋먼트를 제출합니다. 커밋먼트는
("BID1", bidder index, bid, salt)에 대한 Poseidon2입니다. - 판매자는 최저 가격과 모든 커밋먼트를 공개 세그먼트로 하여 경매 프로그램을 배포합니다. 이제 프로그램 id는 이 경매만을 가리킵니다.
- 각 입찰자는 판매자에게 자신의 입찰가와 솔트를 비공개로 보냅니다.
- 판매자는 모든 입찰가와 솔트를 비공개 입력으로 하여 프로그램을 호출합니다. 프로그램은 각 오프닝을 해당 커밋먼트와 대조하고, 낙찰자를 고른 뒤,
[status, winner, price, bidder count]를 게시합니다.
체인은 누가 얼마에 낙찰되었는지만 알게 됩니다. 낙찰되지 못한 입찰가는 결코 보지 못합니다. 판매자가 입찰가 하나라도 바꾸면 결과 대신 BAD_OPENING을 받습니다. 커밋먼트가 프로그램 id에 고정되어 있기 때문입니다.
모든 파일은 circuits 체크아웃 안에 둡니다. rand-guest build가 게스트의 디렉터리에서 위쪽으로 탐색해 guest-sdk를 찾기 때문입니다.
circuits/
guest-sdk/
rand-guest/
my-guests/
commit/ a Rust guest that computes one commitment
auction-core/ the auction rule as a plain no_std library, tested on the host
auction/ the Rust guest
auction-c/ the same guest in C
auction-c-test/ a host test for the C rule, outside the guest's directory
해시 계산기로서의 에뮬레이터
입찰자에게는 머신이 계산하는 것과 정확히 같은 Goldilocks 필드 위의 Poseidon2가 필요합니다. 가장 간단하고 올바른 도구는 이를 계산하는 작은 게스트를 에뮬레이터에서 실행하는 것입니다. 다이제스트는 여덟 워드이며, 이는 출력 슬롯의 수와 정확히 같습니다.
my-guests/commit/Cargo.toml:
[package]
name = "commit"
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]
my-guests/commit/src/main.rs:
#![no_std]
#![no_main]
use guest_sdk::{halt, poseidon2, read_input, write_output};
/// Inputs: a bidder's index, bid and four salt words.
/// Outputs: the 8-word commitment Poseidon2("BID1", index, bid, salt[0..4]).
#[no_mangle]
pub extern "C" fn main() -> ! {
let mut buf = [0u32; 8];
buf[0] = 0x4249_4431; // "BID1"
for i in 0..6 {
buf[1 + i] = read_input(i as u32);
}
unsafe { poseidon2(buf.as_mut_ptr(), 7) };
for (slot, word) in buf.iter().enumerate() {
write_output(slot as u32, *word);
}
halt();
}
poseidon2(ptr, n)은 n개의 워드를 제자리에서 해시하고 8워드 다이제스트를 ptr..ptr+8에 덮어쓰므로, 해시되는 것은 일곱 워드뿐이지만 버퍼는 최소 여덟 워드를 담아야 합니다. "BID1" 태그는 이 다이제스트를 Poseidon2의 다른 용도와 구분해 줍니다.
aeacf31에서 poseidon2는 원시 포인터를 받기 때문에 unsafe fn이며, 따라서 호출은 unsafe 블록 안에 있습니다. 아래 출력을 생성한 7ef3220에서는 안전한 함수였고 같은 줄을 블록 없이 작성했습니다. 스펀지는 패딩하지 않습니다. [a]와 [a, 0]은 같은 해시를 냅니다. 모든 메시지가 일곱 워드이고 태그로 시작하는 이 경우에는 문제가 되지 않습니다. 길이가 달라질 수 있는 메시지의 경우, aeacf31의 SDK에는 길이를 바인딩하는 poseidon2_len(buf, n)(시스콜 7)이 있지만, 해당 커밋의 rand-guest check가 시스콜 7을 거부하므로 그러한 게스트는 아직 빌드되지 않습니다(zkp-circuits issue #1). 대신 길이를 메시지 안에 넣으십시오.
$ rand-guest build my-guests/commit --out commit.bin
76 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote commit.bin and its .sha256 (76 words, hc 41007891b5c393296fdd76539c93d0b06678bc8a0bd52cfc3b6886dcf0381343, program id 11dabdf740989d43599b296c43342bf3ce890e980f5cc558d15ba8ca9e8e9438)
$ rand-guest run commit.bin --input 0 --input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
out[0] = 3502385358
out[1] = 1187922325
out[2] = 2500412136
out[3] = 2452821097
out[4] = 2045657999
out[5] = 1866863173
out[6] = 678782893
out[7] = 3524569252
cycles 77
tier 10
여기와 이후의 빌드 출력에서는 cargo 줄을 생략했습니다. cargo가 출력하는 unaligned-scalar-mem 경고는 예상된 것입니다(Deploy a Rust program).
입찰자 네 명의 입찰가와 솔트는 다음과 같습니다.
| 입찰자 | 입찰가 | salt (워드 4개) |
|---|---|---|
| 0 | 400 | 2654435769 2135587861 4087136352 1559071514 |
| 1 | 250 | 461845907 3432918353 3864292196 2246822507 |
| 2 | 900 | 3266489909 668265263 374761393 3550635116 |
| 3 | 300 | 4252018373 3042594569 1759714724 625341585 |
입찰자별로 commit.bin을 한 번씩 실행하고, 최저 입찰가(100), 입찰자 수(4), 네 개의 다이제스트를 순서대로 기록하면 34워드로 이루어진 공개 세그먼트 auction-public.txt가 만들어집니다.
100 4 3502385358 1187922325 2500412136 2452821097 2045657999 1866863173 678782893 3524569252 3413403069 1515911705 3633767467 1218237979 474331206 896634170 2889783686 541804293 3111308722 99534202 1917186958 1017842926 3082732585 4215265588 467906960 2929273893 588931287 1295911076 647129560 1384102451 625140897 4060943258 2970944086 698558155
실제 경매에서는 각 입찰자가 자신의 컴퓨터에서 commit.bin을 실행하며, salt는 실제 난수원에서 가져옵니다.
4. Rust로 작성한 프로그램
규칙을 테스트할 수 있는 라이브러리로
syscall이 필요 없는 로직은 별도의 no_std 크레이트로 분리합니다. guest-sdk는 riscv32 외의 타깃에서는 빌드를 거부하므로, guest 크레이트에서는 노트북에서 cargo test를 실행할 수 없습니다. guest-sdk에 의존하지 않는 라이브러리 크레이트는 실행할 수 있습니다.
my-guests/auction-core/Cargo.toml:
[package]
name = "auction-core"
version = "0.1.0"
edition = "2021"
[workspace]
my-guests/auction-core/src/lib.rs:
//! The auction's rule, with no syscalls, so it builds and tests on the host.
#![no_std]
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
/// The highest bidder wins and pays the second-highest bid, or the reserve if higher.
Sold { winner: u32, price: u32 },
/// No bid reached the reserve.
NoSale,
}
/// A second-price (Vickrey) auction. Ties go to the lower index.
pub fn settle(reserve: u32, bids: &[u32]) -> Outcome {
let mut best: Option<(u32, u32)> = None; // (index, bid)
let mut second = 0u32;
for (i, &bid) in bids.iter().enumerate() {
match best {
Some((_, top)) if bid <= top => second = second.max(bid),
Some((_, top)) => {
second = top;
best = Some((i as u32, bid));
}
None => best = Some((i as u32, bid)),
}
}
match best {
Some((winner, top)) if top >= reserve => Outcome::Sold { winner, price: second.max(reserve) },
_ => Outcome::NoSale,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn highest_bid_pays_the_second() {
assert_eq!(settle(100, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 400 });
}
#[test]
fn the_reserve_sets_a_floor() {
assert_eq!(settle(500, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 500 });
assert_eq!(settle(1000, &[400, 250, 900, 300]), Outcome::NoSale);
}
#[test]
fn a_tie_goes_to_the_lower_index() {
assert_eq!(settle(0, &[700, 700]), Outcome::Sold { winner: 0, price: 700 });
}
#[test]
fn no_bids_is_no_sale() {
assert_eq!(settle(0, &[]), Outcome::NoSale);
}
}
$ cd my-guests/auction-core && cargo +1.98.1 test
running 4 tests
test tests::a_tie_goes_to_the_lower_index ... ok
test tests::highest_bid_pays_the_second ... ok
test tests::no_bids_is_no_sale ... ok
test tests::the_reserve_sets_a_floor ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
guest
my-guests/auction/Cargo.toml은 두 크레이트에 경로로 의존합니다.
[package]
name = "auction"
version = "0.1.0"
edition = "2021"
[dependencies]
guest-sdk = { path = "../../guest-sdk" }
auction-core = { path = "../auction-core" }
[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1
[workspace]
my-guests/auction/src/main.rs:
#![no_std]
#![no_main]
use auction_core::{settle, Outcome};
use guest_sdk::{halt, poseidon2, read_input, read_public, write_output};
const MAX_BIDDERS: usize = 64;
const TAG: u32 = 0x4249_4431; // "BID1", the same tag the commit guest uses
// Output slot 0: what happened.
const SOLD: u32 = 1;
const NO_SALE: u32 = 2;
const BAD_OPENING: u32 = 3;
const TOO_MANY: u32 = 4;
/// Public (fixed at deploy): reserve, n, then n 8-word commitments.
/// Private (per call): for each bidder, the bid and four salt words.
/// Outputs: [status, winner, price, n, 0, 0, 0, 0]; a panic sets slot 7 to 0xdeadbeef.
#[no_mangle]
pub extern "C" fn main() -> ! {
let reserve = read_public(0);
let n = read_public(1) as usize;
if n > MAX_BIDDERS {
finish(TOO_MANY, 0, 0, n as u32);
}
let mut bids = [0u32; MAX_BIDDERS];
for i in 0..n {
let base = (5 * i) as u32;
let bid = read_input(base);
let mut buf = [TAG, i as u32, bid, read_input(base + 1), read_input(base + 2),
read_input(base + 3), read_input(base + 4), 0];
unsafe { poseidon2(buf.as_mut_ptr(), 7) };
let committed = 2 + 8 * i as u32;
for w in 0..8 {
if buf[w] != read_public(committed + w as u32) {
finish(BAD_OPENING, i as u32, 0, n as u32);
}
}
bids[i] = bid;
}
match settle(reserve, &bids[..n]) {
Outcome::Sold { winner, price } => finish(SOLD, winner, price, n as u32),
Outcome::NoSale => finish(NO_SALE, 0, 0, n as u32),
}
}
fn finish(status: u32, winner: u32, price: u32, n: u32) -> ! {
write_output(0, status);
write_output(1, winner);
write_output(2, price);
write_output(3, n);
halt();
}
모든 종료 경로는 finish를 거치며, finish는 각 슬롯을 한 번씩 기록한 뒤 halt합니다. 루프의 상한은 배포 시점에 고정되는 공개 세그먼트에서 가져옵니다. 따라서 호출자는 프로그램이 커밋된 입찰보다 적은 수의 입찰을 읽도록 만들 수 없습니다.
빌드와 실행
$ rand-guest build my-guests/auction --out auction.bin
258 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction.bin and its .sha256 (258 words, hc aa04e4c451e986f086e9fcff53b213eb52e593317c32a979d5e24991164f050d, program id 148ab0c4b9ad5115caab4a2ab433e3e13b5e8fbecbd9d8a9d6ba20c64f942c73)
rand-guest run은 공개 세그먼트를 반복되는 --public 플래그로 받습니다. 셸에서는 다음과 같습니다.
PUB=$(for w in $(cat auction-public.txt); do printf -- "--public %s " "$w"; done)
BIDS="--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
--input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507
--input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116
--input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585"
rand-guest run auction.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 641
tier 10
tier 10: fits (cycles 721 of 1023, Poseidon2 permutations 88 of 128)
입찰자 2에게 두 번째로 높은 입찰가인 400에 낙찰(1)되었으며, 집계된 입찰자는 네 명입니다. 실패해야 하는 두 가지 실행은 다음과 같습니다.
| 변경 | 출력 0–3 | 의미 |
|---|---|---|
| 입찰자 1의 입찰가 250을 260으로 교체 | 3 1 0 0 | 입찰자 1에서 BAD_OPENING이 발생하며, 그 외에는 아무것도 공개되지 않음 |
| 최저 입찰가를 100 대신 1,000으로 설정(공개 세그먼트가 달라지므로 프로그램 id도 달라짐) | 2 0 0 4 | NO_SALE: 최저 입찰가에 도달한 입찰이 없음 |
5. C로 작성한 동일한 프로그램
C guest 디렉터리에는 .c 파일과 직접 작성한 헤더가 들어 있습니다. rand-guest build --lang c는 guest.h, _start, 그리고 작은 런타임을 제공합니다(Deploy a RISC-V program). 이 명령은 디렉터리의 모든 .c 파일을 컴파일하므로, 호스트 테스트 파일은 다른 위치에 두어야 합니다.
my-guests/auction-c/auction.h:
#ifndef AUCTION_H
#define AUCTION_H
#include <stdint.h>
/* Outcome of a second-price auction. sold is 0 when no bid reached the reserve. */
struct outcome {
uint32_t sold;
uint32_t winner;
uint32_t price;
};
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n);
#endif
my-guests/auction-c/settle.c:
#include "auction.h"
/* A second-price (Vickrey) auction. Ties go to the lower index. */
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n) {
struct outcome o = {0, 0, 0};
if (n == 0) {
return o;
}
uint32_t winner = 0, top = bids[0], second = 0;
for (uint32_t i = 1; i < n; i++) {
if (bids[i] > top) {
second = top;
top = bids[i];
winner = i;
} else if (bids[i] > second) {
second = bids[i];
}
}
if (top >= reserve) {
o.sold = 1;
o.winner = winner;
o.price = second > reserve ? second : reserve;
}
return o;
}
my-guests/auction-c/main.c:
#include "guest.h"
#include "auction.h"
#define MAX_BIDDERS 64
#define TAG 0x42494431u /* "BID1", the same tag the commit guest uses */
enum { SOLD = 1, NO_SALE = 2, BAD_OPENING = 3, TOO_MANY = 4 };
static uint32_t bids[MAX_BIDDERS];
static void finish(uint32_t status, uint32_t winner, uint32_t price, uint32_t n) {
rand_write_output(0, status);
rand_write_output(1, winner);
rand_write_output(2, price);
rand_write_output(3, n);
rand_halt();
}
/* Public (fixed at deploy): reserve, n, then n 8-word commitments.
* Private (per call): for each bidder, the bid and four salt words.
* Outputs: [status, winner, price, n, 0, 0, 0, 0]. */
void main(void) {
uint32_t reserve = rand_read_public(0);
uint32_t n = rand_read_public(1);
if (n > MAX_BIDDERS) {
finish(TOO_MANY, 0, 0, n);
}
for (uint32_t i = 0; i < n; i++) {
uint32_t base = 5 * i;
uint32_t buf[8] = {TAG, i, rand_read_input(base), rand_read_input(base + 1),
rand_read_input(base + 2), rand_read_input(base + 3),
rand_read_input(base + 4), 0};
bids[i] = buf[2];
rand_poseidon2(buf, 7);
for (uint32_t w = 0; w < 8; w++) {
if (buf[w] != rand_read_public(2 + 8 * i + w)) {
finish(BAD_OPENING, i, 0, n);
}
}
}
struct outcome o = settle(reserve, bids, n);
if (o.sold) {
finish(SOLD, o.winner, o.price, n);
}
finish(NO_SALE, 0, 0, n);
}
bids는 0으로 초기화되는 static이므로 .bss에 배치됩니다. RAM은 이미 0으로 읽히기 때문에 이미지 워드가 늘어나지 않습니다.
호스트에서 규칙 테스트하기
settle.c는 syscall을 사용하지 않으므로 일반 컴파일러로 빌드할 수 있습니다. my-guests/auction-c-test/test_settle.c:
#include <assert.h>
#include <stdio.h>
#include "../auction-c/auction.h"
int main(void) {
uint32_t bids[] = {400, 250, 900, 300};
struct outcome o = settle(100, bids, 4);
assert(o.sold && o.winner == 2 && o.price == 400);
o = settle(500, bids, 4);
assert(o.sold && o.price == 500);
o = settle(1000, bids, 4);
assert(!o.sold);
uint32_t tie[] = {700, 700};
o = settle(0, tie, 2);
assert(o.winner == 0 && o.price == 700);
puts("settle: 4 checks passed");
return 0;
}
$ cc -Wall -o test_settle my-guests/auction-c-test/test_settle.c my-guests/auction-c/settle.c && ./test_settle
settle: 4 checks passed
빌드와 실행
$ rand-guest build --lang c my-guests/auction-c --out auction-c.bin
200 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction-c.bin and its .sha256 (200 words, hc 68605aa8d28a863a4a52a25de07ba159327a0fd0145a9dcd5e5aa6e79ab6bea0, program id 5667c343b8848805a908c9009d426164f39d2f927b2c780529a054f0d66aafc1)
$ rand-guest run auction-c.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 576
tier 10
tier 10: fits (cycles 641 of 1023, Poseidon2 permutations 73 of 128)
출력은 Rust guest의 출력과 일치합니다. 입찰자가 네 명일 때 C 이미지는 58워드 더 작고 65사이클 더 빠릅니다. 7절에서는 입찰자가 64명일 때 균형이 뒤집히는 것을 보여 줍니다. 두 프로그램은 서로 다른 hc와 프로그램 id를 가진 별개의 프로그램입니다. 소스를 공개하는 쪽을 배포하십시오.
6. 언어별 참고 사항
Rust
크레이트. #![no_std], #![no_main], 그리고 halt()로 끝나는 #[no_mangle] pub extern "C" fn main() -> !가 필요합니다. 릴리스 프로파일은 panic = "abort"로 설정합니다. 비어 있는 [workspace] 테이블은 크레이트가 상위 워크스페이스에 포함되지 않도록 합니다. .cargo/config.toml과 직접 지정하는 컴파일러 플래그는 사용하지 마십시오. rand-guest build는 전자를 거부하고 후자를 모두 직접 설정합니다(Deploy a Rust program).
패닉. guest-sdk의 패닉 핸들러는 슬롯 7에 0xdeadbeef를 기록하고 halt합니다. 이 실행은 여전히 유효하며 증명 가능한 실행입니다. 다음 프로그램은 슬롯 0과 1을 기록한 뒤, 요소가 여섯 개인 Vec의 인덱스 999를 읽습니다.
$ rand-guest run heap.bin --input 6 --input 1 --input 2 --input 3 --input 4 --input 5 --input 6
out[0] = 4
out[1] = 1080033280
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 3735928559
cycles 1326
tier 12
슬롯 0과 1에는 실제 값이 들어 있고, 슬롯 7은 실행이 패닉했음을 나타냅니다. 이러한 실행을 제출하는 호출자는 온체인에서 영수증을 받습니다. 영수증을 읽는 쪽은 슬롯 7 = 3,735,928,559인 영수증을 거부해야 하며, 그렇지 않다면 프로그램이 패닉하지 않도록 만들어야 합니다. 모든 경로에서 get(), 검사된 산술, 명시적인 상태 코드를 사용하는 편이 좋습니다.
힙. 기본적으로 할당자는 없습니다. 할당자를 제공하면 alloc을 사용할 수 있습니다. 한 번 실행되고 종료되는 프로그램에는 고정된 아레나 위의 범프 할당자가 적합합니다.
extern crate alloc;
use alloc::vec::Vec;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
/// A bump allocator over a fixed arena: allocation moves a pointer forward, free does nothing.
/// One run is one short-lived process, so nothing needs to be reclaimed.
struct Bump {
arena: UnsafeCell<[u8; ARENA]>,
next: UnsafeCell<usize>,
}
const ARENA: usize = 256 * 1024;
unsafe impl Sync for Bump {}
unsafe impl GlobalAlloc for Bump {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let next = &mut *self.next.get();
let start = (*next + layout.align() - 1) & !(layout.align() - 1);
let end = start + layout.size();
if end > ARENA {
return core::ptr::null_mut();
}
*next = end;
(self.arena.get() as *mut u8).add(start)
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}
#[global_allocator]
static HEAP: Bump = Bump { arena: UnsafeCell::new([0; ARENA]), next: UnsafeCell::new(0) };
아레나는 0으로 채워져 있으므로 .bss에 놓이며 이미지 워드를 늘리지 않습니다. 기본 링커 스크립트는 RAM 1 MiB를 제공하며, 아레나, 정적 변수, 64 KiB 스택이 모두 이 안에 들어가야 합니다. 이 할당자를 사용하면 Vec, collect(), iter().sum()이 모두 빌드되고 check를 통과합니다. 다만 alloc과 그 의존성은 코드를 늘립니다. 이 테스트 프로그램은 1,341워드이고 경매 프로그램은 258워드입니다. 프로그램 크기가 증명 예산을 소모하는 이유는 7절에서 설명합니다.
부동소수점. 이 타깃에는 부동소수점 명령이 없으며, Rust는 f32/f64를 소프트웨어 루틴으로 낮춥니다. 이 루틴은 동작합니다. 9, 2, 7, 4, 5의 평균을 f64로 계산한 결과는 5.4f32의 비트로 반환되었습니다. 다만 각 연산에 많은 사이클이 듭니다. 가능하면 정수나 고정소수점을 사용하십시오.
크레이트. crates.io의 no_std 크레이트는 다른 의존성과 마찬가지로 빌드됩니다. crc 크레이트로 hello, rand의 CRC-32를 에뮬레이터에서 실행하자 2124296908이 출력되었으며, 이는 Python의 zlib.crc32가 반환하는 값과 같습니다. std가 필요한 크레이트는 이 타깃에서 빌드되지 않습니다. rand-guest build는 --locked를 전달하지 않으므로 Cargo.lock을 커밋하십시오. 그렇지 않으면 몇 달 뒤 다시 빌드할 때 더 새로운 크레이트로 해석되어 다른 hc가 만들어질 수 있습니다.
unimp 함정: 라이브러리 코드에서 나오는 Csr 결과. 일부 core 코드는 LLVM이 도달 불가능하다고 판단하는 경로에서 unimp 워드(0xc0001073, csrrw x0, cycle, x0)로 컴파일됩니다. 이 머신에는 CSR이 없으므로 check는 해당 이미지를 거부합니다.
0x00002250 0xc0001073 Csr: CSR instruction: the machine has no CSRs
2613 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED
wrote heap.bin (the loader refuses it: no hc)
이 경우 원인은 길이를 런타임에만 알 수 있는 Vec에 대한 slice::sort_unstable이었습니다. 안정 정렬인 sort()도 동일했으며, 길이가 16으로 고정된 배열에 대한 sort_unstable은 통과했습니다. 이러한 결과의 출처를 찾으려면 guest의 ELF를 디스어셈블하여 check가 출력한 주소 주변의 함수를 확인하십시오.
llvm-objdump -d -C my-guests/heap/target/riscv32im-unknown-none-elf/release/heap | grep -B12 unimp
그런 다음 해당 호출을 직접 작성한 코드(이 프로그램의 경우 짧은 삽입 정렬)로 교체하십시오. guest-sdk의 halt()가 noreturn 보장 대신 loop {}로 끝나는 것도 같은 이유입니다.
C
- 진입점은
void main(void)이며,rand_halt()로 끝납니다. 번들로 제공되는_start가 스택을 설정하고 이 함수를 호출합니다. - libc가 없습니다.
printf,malloc,<string.h>를 사용할 수 없습니다. 런타임은memcpy,memmove,memset,memcmp(clang은 구조체 복사와 큰 초기화에 이 함수들의 호출을 생성합니다)와 네 가지 64비트 나눗셈 도우미를 제공합니다. 이 함수들은 모두 바이트 단위 루프이므로, 성능이 중요한 코드에서는 워드 단위로 복사하고 나누십시오. 힙이 필요하면 Rust에서와 같이static배열 위에 범프 할당자를 작성하십시오. - 정의되지 않은 동작은 검출되지 않습니다. 패닉 핸들러가 없습니다. 범위를 벗어난 쓰기는 조용히 메모리를 손상시키며, 정렬되지 않은 로드나 스토어는 트랩을 일으켜 해당 실행을 증명할 수 없게 됩니다.
main.c가n을MAX_BIDDERS와 대조하듯이, 인덱스를 직접 정한 한도와 대조하여 확인하십시오. - 디렉터리 규칙. 직접 작성한
guest.h나start.S를 추가하지 마십시오.build --lang c는 둘 다 거부합니다. 테스트 파일은 guest 디렉터리 밖에 두십시오. 디렉터리 안의 모든.c파일이 이미지로 컴파일되기 때문입니다. - 고정된 clang.
rand-guest는 23.1.1이 아닌 clang을 거부합니다.RAND_GUEST_CLANG_UNPINNED=1로 이를 무시할 수 있으나, 이 경우hc가 다른 사람의 빌드와 일치하지 않는다는 경고가 표시됩니다.
7. 증명이 가능한가? 예산
prover는 각 실행을 티어 t로 패딩합니다. 티어에는 서로 독립적인 두 가지 예산이 있으며, rand-guest run --tier t는 두 가지를 모두 보고합니다.
- 행: 최대
2ᵗ − 1개이며, 실행된 사이클과 다이제스트 행을 합산합니다. - Poseidon2 순열: 최대
2ᵗ⁻³개입니다.
| 티어 | 행 | Poseidon2 순열 | 해시 테이블이 없을 때의 가스 상한 | chain 20에서의 호출 |
|---|---|---|---|---|
| 10 | 1,023 | 128 | 1,279 | 수락됨 |
| 12 | 4,095 | 512 | 5,119 | 수락됨 |
| 14 | 16,383 | 2,048 | 20,479 | 수락됨 |
| 16 | 65,535 | 8,192 | 81,919 | v0.5.6 이후 거부됨 |
| 18, 20 | 2¹⁸ − 1, 2²⁰ − 1 | 2¹⁵, 2¹⁷ | 327,679, 1,310,719 | v0.5.6 이후 거부됨 |
수수료는 티어가 아니라 가스와 바이트를 따릅니다. chain 15에서는 호출마다 0.001 + call_fee(tier)의 고정 수수료를 냈습니다. 티어 10에서 0.002 RAND, 티어 12에서 0.0021, 티어 14에서 0.0022였습니다. chain 18부터는 모든 실행이 계량됩니다. 일반 행은 1 가스, Poseidon2 absorb 행은 3, Keccak 순열은 192, SHA-256 압축은 64입니다. 증명은 선언된 가스 한도를 공개하고, 회로는 실행의 가스가 이 한도 이하임을 강제하며, 정확한 가스는 비공개로 유지됩니다. 해시 테이블이 없을 때 티어가 선언할 수 있는 상한은 (2ᵗ − 1) + 2ᵗ⁻²이며, 위 표의 열이 이 값입니다. 수수료 하한은 다음과 같습니다.
floor = BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉
BUNDLE_BASE는 0.001 RAND이고, 바이트는 호출 증명과 입력 엔벨로프의 크기입니다. 가격은 가스당 100단위, KiB당 800단위(1단위는 10⁻⁹ RAND)에서 시작하여 블록마다 조금씩 움직입니다. chain 20에서는 각각 10,000단위와 80,000단위를 넘어 오를 수 없으며, 바이트 가격은 호출의 증명과 엔벨로프 바이트만 움직입니다. chain 18과 19에는 상한이 없었습니다. 기본적으로 지갑이 지불하는 금액은 9절에서 확인할 수 있으며, Example programs에는 chain 18에서 측정한 호출 네 건이 건당 0.0057~0.0075 RAND로 정리되어 있습니다.
Tier 14는 호출이 사용할 수 있는 가장 높은 tier입니다. fullnode v0.5.6(2026-09-25)부터 노드는 tier 14를 넘는 호출 증명을 verifier key를 만들기 전에 CallTierTooHigh로 거부합니다. 같은 규칙에 따라 그러한 호출을 담은 블록은 유효하지 않습니다. 이 제한은 메모리가 2–4 GB인 validator를 보호합니다. tier-20 verifier key를 만드는 데에는 수 분과 수 기가바이트가 듭니다. rand call은 증명하기 전에 그러한 호출을 거부하지 않지만, chain 20에서는 먼저 tier를 출력합니다. 호출하기 전에 rand-guest run … --tier 14를 실행하고, 실행이 tier에 맞지 않으면 프로그램의 구조를 바꾸십시오. 배포 전에 rand program deploy --input <word> …(입력이 없는 호출이라면 --check-call)는 염두에 둔 호출을 에뮬레이터로 실행하며, 그 결과가 tier 14 이하에 해당하지 않으면 배포를 거부합니다.
예산을 소모하는 요인
예상 밖인 것은 digest 행과 순열입니다. 코드가 실행되기 전에 머신은 프로그램, 비공개 입력, 공개 세그먼트를 해시하여 hc, H_IN, H_PUB를 만듭니다. 각각 4워드당 약 1회의 순열과 1개의 행이 듭니다. 여기에 더해, 호출하는 POSEIDON2마다 해시하는 4워드당 약 1회의 순열이 듭니다. tier 10의 경매는 다음과 같습니다.
| 항목 | 워드 | 순열 |
|---|---|---|
프로그램 (hc) | 258 | 약 65 |
비공개 입력 (H_IN) | 20 | 약 5 |
공개 세그먼트 (H_PUB) | 34 | 약 9 |
| 커밋먼트 검사 4회, 각 7워드 | 4 × 7 | 8 |
합계, run이 보고하는 값 | 128 중 88 |
행: 641 사이클에 digest 행을 더하면 1,023 중 721이었습니다.
chain 19와 마찬가지로 chain 20에서도 지갑 자체의 집계는 rand-guest run의 집계보다 약간 높습니다. 그곳의 호출 증명은 트랜잭션에 바인딩되고(chain 20에서는 이 바인딩에 genesis 해시도 포함되며, 같은 8워드입니다), 이 바인딩은 공개 세그먼트에 8워드가 더 붙은 형태로 전달됩니다. 공개 세그먼트가 있는 프로그램에서는 digest 행과 순열이 각각 2개 늘고, 없는 프로그램에서는 각각 1개 늘어납니다.
따라서 프로그램의 크기도 예산의 일부입니다. heap 테스트 프로그램은 1,183 사이클만 실행했지만, 1,341워드로 인해 340회의 순열이 들었고 tier 12까지 올라갔습니다. tier 14의 순열 2,048회 기준으로, 약 8,000워드를 크게 넘는 프로그램은 무엇을 하든 호출할 수 없습니다. chain 20에서 정확한 한계는 공개 세그먼트가 없고 입력이 없는 호출의 경우 8,180워드이며, 둘 중 하나라도 있으면 그보다 작습니다. chain 20의 프로그램 상한은 chain 15와 같은 65,535워드이지만, chain 19와 chain 20은 이 한계를 넘는 프로그램의 배포를 거부합니다(ProgramUncallable). chain 15는 아무도 호출할 수 없는 프로그램에 대해서도 배포 수수료를 받았습니다. 그래서 chain 13에서 변환된 ERC-20(11,686워드, tier 16에서 호출)은 chain 20에서 배포도 호출도 할 수 없습니다. 프로그램을 작게 유지하십시오. Rust에서는 opt-level = "s", C에서는 -Os(이미 설정되어 있음)를 쓰고, 의존성을 줄이며, 필요하지 않다면 alloc을 쓰지 마십시오.
경매는 선형으로 증가합니다. 입찰자가 하나 늘 때마다 입력 5워드, 공개 8워드, 검사에 필요한 순열 2회, 약 100 사이클이 추가됩니다. 입찰자가 64명(무작위 입찰, 입력 320워드, 공개 514워드)일 때 두 빌드 모두 같은 낙찰자와 가격을 고르며, 둘 다 tier 14가 필요합니다.
auction.bin tier 14: fits (cycles 6557 of 16383, Poseidon2 permutations 403 of 2048)
auction-c.bin tier 14: fits (cycles 8571 of 16383, Poseidon2 permutations 388 of 2048)
순열은 tier 12의 512회 안에 들어가지만, 행은 4,095개에 들어가지 않습니다. 여기서는 해시가 아니라 사이클이 tier를 결정합니다. 입찰자 64명에서는 Rust 빌드가 더 빠릅니다. 루프가 지배적이 되면 opt-level = 3이 C 빌드의 고정된 -Os를 앞섭니다.
Keccak과 SHA-256
keccak과 sha256_compress는 각각 1개의 행이 들지만, 둘 중 하나라도 쓰는 증명에는 추가 테이블이 붙습니다. aeacf31에서는 둘 다 배열 참조를 받습니다. keccak(&mut state)는 [u32; 50]을, sha256_compress(&mut buf)는 [u32; 24]를 받으며, 7ef3220에서는 raw 포인터를 받았습니다. v0.5.6부터 호출 하나는 Keccak-f 순열을 최대 128회, SHA-256 압축을 최대 128회까지만 쓸 수 있습니다. chain 20에서는 Keccak 순열 1회가 192 gas, SHA-256 압축 1회가 64 gas이기도 합니다.
이 테이블은 증명을 더 크게 만들며, chain 20은 호출 증명을 4,194,304바이트(4 MiB, chain 1719와 같음, chain 1316에서는 상한이 8 MiB)로 제한합니다. chain 18에서 Keccak 테이블을 쓰는 tier-12 호출은 3.35 MB로 측정되어 상한의 대부분을 차지했고, SHA-256 테이블을 쓰는 tier-14 호출은 1.91 MB였으며, 둘 다 쓰지 않으면 1.50 MB였습니다(Example programs). KiB마다 바이트 가격이 부과됩니다. chain 15에서는 무료 2 MiB + 18,432바이트를 넘는 바이트에 대해서만 KiB당 1,000 단위가 부과되었습니다(Fees). 외부 해시와 일치시켜야 하는 경우가 아니라면 Poseidon2를 쓰십시오.
증명에 드는 메모리와 시간
증명 시간과 메모리는 tier가 올라갈수록 가파르게 늘어납니다. tier-10 호출은 노트북에서 몇 초 만에 증명됩니다. chain 20에서는 모든 배포와 모든 호출이 번들 측 증명도 두 개 더 가집니다. tier-10 지출 승인(수 초)과 수수료를 지불하는 tier-14 번들(노트북에서 약 2분)입니다. chain 18에서 측정한 호출 4건은 각각 3분에서 5분 30초가 걸렸고 최대 6.1~6.6 GB를 사용했습니다(Example programs). chain 15에서는 번들이 약 100초, 약 5.7 GB의 tier-14 증명 하나였습니다. 측정된 수치는 Validator and aggregator hardware에 있습니다.
8. 배포
testnet은 chain 20입니다. 배포하려면 fullnode v0.6.8 이상의 rand 지갑, 키, 그리고 약간의 RAND가 필요합니다(Introduction, chain 20의 faucet은 chain 19의 faucet과 마찬가지로 genesis가 허용 목록에 올린 16개 지갑에만 발행합니다). v0.6.7 지갑은 chain 20에서 전송할 수 없습니다. chain 20의 genesis는 binding_domain: 1로 설정되어 있어 모든 증명 바인딩과 서명된 메시지에 genesis 해시가 포함되는데, v0.6.7 지갑은 이를 넣지 않기 때문입니다. 공개 엔드포인트 https://rpc.randprotocol.org만으로 지갑을 동기화하고, 배포하고, 호출하기에 충분합니다(setup). 경매는 공개 세그먼트와 함께 배포합니다.
rand fee deploy 258 --public-words 34 # 0.001 + 0.0001 × (258 + 34) = 0.0302 RAND
rand program deploy auction.bin --public auction-public.txt
program deploy는 프로그램 id, hc, 워드 수, 공개 입력의 워드 수와 digest를 출력합니다. 증명하기 전에 프로그램을 체인의 상한에 대해, 공개 입력을 max_program_public_words(chain 20에서는 32,768)에 대해 검사합니다. 이어서 수수료를 지불하는 가치 0의 자기 이체 번들을 증명하고, 제출한 뒤, 커밋을 기다립니다. chain 20에서는 chain 17 이후와 마찬가지로 증명이 두 개이며, 각각 자체 출력 줄이 있습니다. tier-10 지출 승인(수 초, 약 1.37 MB)과 tier-14 번들(약 2분, 약 1.5 MB)입니다. chain 15에서는 약 100초, 약 1.42 MB의 tier-14 증명 하나였습니다. 배포 수수료에는 gas 항목이 없습니다. 경매는 chain 18~20에 배포된 적이 없습니다. An anonymous vote는 chain 18에서 공개 세그먼트를 포함한 배포를 지갑이 출력한 그대로 보여 줍니다.
프로그램 id는 --public에 따라 달라집니다. rand-guest는 코드만으로 148ab0c4…2c73을 출력했습니다. 공개 세그먼트가 있는 배포는 blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public) 아래에 저장되며, 이는 reserve와 네 개의 커밋먼트도 바인딩합니다. program deploy가 출력하는 id를 사용하십시오. 코드가 같고 커밋먼트가 다른 두 번째 경매는 다른 프로그램 id를 가집니다.
rand program show <auction id> # base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest
code_hash는 rand-guest가 출력한 hc이며, 표기만 다릅니다. rand-guest는 digest의 여덟 워드를 각각 big-endian으로 출력하고, 지갑, rand program show, rand_getProgram은 각 워드의 바이트를 little-endian으로 출력하므로, 8개의 16진수 자리 묶음마다 바이트 순서가 뒤집힙니다. 위의 빌드에서 aa04e4c4…050d(Rust)는 c4e404aa…4f16으로, 68605aa8…bea0(C)는 a85a6068…b69a로 나타납니다.
9. 호출
판매자는 비공개 워드 20개를 모두 담아 호출합니다.
rand call <auction id> --expect-public auction-public.txt \
--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514 \
--input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507 \
--input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116 \
--input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585
지갑은 프로그램의 코드와 공개 세그먼트를 가져와 이것들이 id로 해시되는지 확인합니다. --expect-public은 공개 세그먼트가 그 파일의 워드가 아니면 증명하기 전에 거부하기도 합니다. 그런 다음 지갑은 에뮬레이터에서 호출을 실행하고 정확한 gas, 선언할 한도, tier(실행이 들어가는 가장 작은 tier, 여기서는 tier 10), 수수료를 출력합니다.
gas: <exact> (declaring <limit>, tier <t>)
fee <amount> RAND (incl. two price steps of headroom)
지갑은 입력 워드 20개를 호출자만 열 수 있는 봉투에 봉인하고, 호출을 로컬에서 증명하고, 번들로 수수료를 지불하고, 제출한 뒤, 기다렸다가 영수증을 출력합니다. chain 20에서는 증명이 세 개입니다. 호출, tier-10 지출 승인, tier-14 번들입니다.
호출 비용. chain 15에서 이 호출은 0.001 + call_fee(10) = 0.002 RAND를 지불했습니다. chain 20에서 지갑은 증명이 존재하기 전에 계산되는 7절의 하한을 지불하며, 기본값은 세 가지입니다.
- 선언 한도는 정확한 가스를
2^(tier − 2)의 배수로 올림한 값입니다. tier 10에서는 256, tier 12에서는 1,024, tier 14에서는 4,096입니다.--gas-limit max는 해당 tier의 상한을 선언하고,--gas-limit N은 정확한 수치를 선언합니다. - 증명은 실제 크기가 아니라 체인의 상한인 4,194,304 bytes로 가격이 책정됩니다. chain 20의 호출 증명은 해당 트랜잭션에 결합되어 있어 수수료를 먼저 확정해야 하기 때문입니다.
- 가격 단계 두 번분의 여유, 즉 1.125²배를 둡니다. 호출은 가격이 책정된 뒤 두세 개의 블록이 지나서 반영되기 때문입니다.
--fee는 기본값을 덮어쓰며, rand fee call <tier> --gas <limit> --bytes <B>는 노드에 하한 자체를 질의합니다. 경매의 호출은 chain 18부터 20까지에서 실행된 적이 없으므로, 그 가스 행과 수수료는 여기에 싣지 않았습니다. 익명 투표에는 지갑이 출력한 그대로의 완전한 호출이 chain 18에 있으며, 수수료는 0.0062 RAND입니다.
영수증에는 tier, H_IN, H_PUB, 그리고 출력 [1, 2, 400, 4, 0, 0, 0, 0]이 담겨 있습니다. 에뮬레이터가 출력한 것과 같은 워드입니다. 증명이 바로 그 동일한 실행에 대한 것이기 때문입니다. 네 개의 입찰과 솔트는 체인에 없습니다. --auditor <rand1…>는 입력을 지정한 감사자에게도 봉인하며, --no-envelope는 트랜스크립트를 전혀 게시하지 않습니다. 이는 되돌릴 수 없습니다 (프로그램).
10. 결과 확인
입찰자를 포함한 누구든 판매자를 신뢰하지 않고도 경매를 검증할 수 있습니다.
- 코드. 동일한 circuits 커밋과 Rust 1.98.1(또는 clang 23.1.1)에서
rand-guest build로 공개된 소스를 다시 빌드합니다.hc를rand program show또는rand_getProgram의code_hash와 비교하되, 바이트가 역순으로 표기될 수 있음을 감안합니다(8절). 빌드는 재현 가능하므로 어느 머신에서나 두 값이 일치합니다. - 공개 세그먼트.
rand_getProgramPublic으로 가져오거나rand call --expect-public을 실행합니다. 본인의 커밋먼트가 본인의 인덱스에 있는지 확인합니다. - 영수증.
rand receipt <tx hash>또는rand_getReceipt: status가1이고, 낙찰자와 가격이 맞으며, 슬롯 7이0xdeadbeef가 아니고,program이 경매의 id와 같아야 합니다. - 입력(열람 권한이 있는 경우). 판매자의
rand open-call <tx hash>(또는 감사자의--as-auditor)는 봉인된 입력을 열고,H_IN과 대조하고, 프로그램을 다시 실행하며, 불일치가 있으면 0이 아닌 코드로 종료합니다. v0.6.8 지갑을 사용하십시오. v0.6.8부터는 프로그램의 공개 세그먼트와 호출 바인딩을 사용해 재실행하므로 경매에서도 동작합니다(fullnode issue #116). v0.6.7에서는 공개 세그먼트와 함께 배포된 모든 프로그램에 대해open-call이re-running the program on these inputs failed: PublicIndex(0)와 함께 중단되었습니다.
증명이 보장하는 것은 규칙이 커밋된 입찰의 오프닝에 적용되었다는 점입니다. 증명은 자금을 이동시키지 않습니다. 낙찰자는 일반 rand send로 판매자에게 대금을 지급하며, 영수증은 양측이 함께 근거로 삼는 기록입니다.
문제 해결
| 증상 | 원인 및 해결 |
|---|---|
이전 커밋을 기준으로 작성된 poseidon2(…), keccak(…), sha256_compress(…) 호출에서 빌드가 실패합니다 | aeacf31에서 poseidon2는 unsafe fn이고, 나머지 둘은 각각 &mut [u32; 50]과 &mut [u32; 24]를 받습니다(3절과 7절) |
check가 a7 = 7인 Syscall을 보고합니다 | 게스트가 poseidon2_len을 호출합니다. aeacf31의 rand-guest check는 syscall 7을 목록에 포함하지 않습니다(zkp-circuits issue #1) |
check가 0xc0001073에서 Csr를 보고합니다 | 라이브러리 코드가 unimp로 컴파일되었습니다. llvm-objdump로 해당 함수를 찾아 호출을 교체하십시오(6절) |
run이 어느 tier에도 맞지 않는다고 하거나 tier 14를 초과한다고 합니다 | 행 수 또는 Poseidon2 순열이 너무 많습니다. 프로그램을 줄이거나 해시를 줄이거나, 여러 호출로 작업을 나누고 호출 사이에 다이제스트를 이어 전달하십시오 |
| slot 7 = 3735928559 | Rust 게스트가 패닉했습니다. 그 이전의 출력은 신뢰할 수 없습니다 |
trap: DoubleWrite(n) | 출력 슬롯 n에 두 번 기록했습니다. 모든 종료 경로를 하나의 함수로 통과시키십시오 |
trap: InputIndex(i) | 프로그램이 호출자가 제공하지 않은 비공개 입력을 읽었습니다. 먼저 개수를 읽고 확인하십시오 |
로드 또는 스토어에서 trap: … | 정렬되지 않았거나 범위를 벗어난 주소입니다. C에서는 경계를 넘은 배열 인덱스입니다 |
invalid value '0x…' for '--input' | 명령줄에서 입력 워드는 십진수입니다 |
CallTierTooHigh로 호출이 거부되었습니다 | 증명이 tier 14를 초과합니다. 호출하기 전에 rand-guest run --tier 14로 확인하십시오 |
다시 빌드한 hc가 배포된 값과 다릅니다 | circuits 커밋, 툴체인 또는 의존성 버전이 다릅니다. 세 가지를 모두 고정하고 Cargo.lock을 커밋하십시오. 표기만 다르다면 8자리 16진수 그룹마다 바이트가 역순입니다(8절) |
rand open-call이 PublicIndex(0)와 함께 중단됩니다 | 프로그램에 공개 세그먼트가 있고 지갑이 v0.6.7이며, 그 재실행은 아무것도 전달하지 않습니다(fullnode issue #116). v0.6.8을 사용하십시오 |
… is not inside a circuits checkout | 게스트 디렉터리를 체크아웃 안으로 옮기십시오 (Rust 프로그램 배포) |