این صفحه همهچیز لازم برای قرار دادن برنامهٔ خودتان روی Rand را پوشش میدهد: یک برنامه چه میتواند باشد، چگونه باید آنچه را میخواند و منتشر میکند طراحی کرد، و یک نمونهٔ کامل که دو بار نوشته شده است، یک بار با Rust و یک بار با C. سپس به آزمایش، بودجهٔ اثبات، استقرار، فراخوانی، و نحوهٔ بررسی نتیجه توسط هر کسی میپردازد. هر خروجی rand-guest در زیر از یک اجرای واقعی آمده و همانطور که چاپ شده درج شده است. اجراها در تاریخ 2026-09-25 روی کامیت 7ef3220 از circuits انجام شدهاند. testnet اکنون زنجیرهٔ 20 است که زنجیرهٔ ابزار آن، مانند زنجیرههای 18 و 19، روی aeacf31 ساخته میشود (شاخهٔ feat/v067-circuits؛ fullnode v0.6.8 همین کامیت را پین میکند)، و خروجیها در آنجا دوباره اجرا نشدهاند. صفحه مشخص میکند که SDK یا زنجیره در کجا تفاوت دارد.
دو آموزش کوتاه مسیر سریع هستند: استقرار یک برنامهٔ Rust و استقرار یک برنامهٔ RISC-V. این صفحه فرض میکند گام 1 از آموزش Rust را انجام دادهاید: یک checkout از randprotocol/zkp-circuits روی aeacf31، Rust 1.98.1 با هدف riscv32im-unknown-none-elf، باینری rand-guest، و برای C، clang 23.1.1 و llvm-tools. معماری مجموعهدستورهٔ RISC-V مرجع خود ماشین است. برنامههای نمونه از همینجا با برنامههای بزرگتر ادامه میدهد و توضیح میدهد سنجش gas در زنجیرهٔ 18 چه چیزی را تغییر داد. این صفحه برنامههای RPL-1 را پوشش میدهد که یک Call آنها را بدون حالت اجرا میکند؛ زنجیرهٔ 20 همچنین حالت برنامهٔ RPL-2، سلولها و یک vault دارد که با Invoke تغییر میکنند (برنامهها)، و نمونههای کامل آن در randprotocol/example-programs است.
1. یک برنامه چه میتواند باشد
برنامه هر محاسبهای است که در این قالب بگنجد:
- کلمههای 32 بیتی را بهعنوان ورودی میخواند.
- حداکثر هشت کلمهٔ 32 بیتی را بهعنوان خروجی منتشر میکند.
- قطعی است: ورودیهای یکسان همیشه خروجیهای یکسان میدهند.
- در چارچوب بودجهٔ سطح خود متوقف میشود (بخش 7).
در درون این محدودیتها میتوانید هر چیزی بنویسید. حلقه، بازگشت، struct، آرایه، حساب 64 بیتی، crate و اگر allocator بیاورید heap در اختیار دارید. با این حال ماشین RV32IM خالص است و هیچ سیستمعاملی زیر آن نیست:
| آنچه دارید | آنچه ندارید |
|---|---|
دستورهای صحیح RV32IM، از جمله MUL/DIV/REM | سختافزار ممیز شناور (ممیز شناور نرمافزاریِ Rust کار میکند، اما کند) |
تا 1 MiB حافظهٔ RAM از 0x1000 در linker script پیشفرض، با پشتهٔ 64 KiB | heap، مگر اینکه allocator فراهم کنید (بخش 6) |
| Poseidon2، Keccak-f و SHA-256 بهصورت syscall | فایل، شبکه، ساعت، تصادفیبودن، thread، std، libc |
crateهای Rust از نوع no_std و C مستقل | حالتی که از یک فراخوانی به فراخوانی بعدی باقی بماند |
| هشت کلمهٔ خروجی عمومی | راهی برای جابهجا کردن RAND یا توکن از داخل برنامه |
ردیف آخر هنگام طراحی برنامه از همه مهمتر است. خروجیها دادهاند، نه اثر. یک فراخوانی ثابت میکند برنامهای با digest مشخص روی چند ورودی خصوصی اجرا شد و هشت کلمه منتشر کرد. زنجیره اثبات را راستیآزمایی میکند و آن هشت کلمه را در یک رسید ذخیره میکند. هیچ موجودیای را تغییر نمیدهد و چیزی را فعال نمیکند. معنای کلمهها میان برنامه، فراخواننده و کسی که رسید را میخواند توافق میشود. برنامهها فهرست مواردی را میدهد که زنجیره هنوز پشتیبانی نمیکند.
2. ابتدا رابط را طراحی کنید
یک برنامه سه کانال دارد. پیش از نوشتن هر کدی تصمیم بگیرید چه چیزی روی هر کدام برود.
| کانال | زمان تعیین | خواندن با | چه کسی میبیند | محدودیت در زنجیرهٔ 20 |
|---|---|---|---|---|
| ورودی خصوصی | در هر فراخوانی | read_input(i) / rand_read_input(i) | فقط فراخواننده و هر کسی که کلید آن فراخوانی را به او بدهد | 16,071 کلمه (سقف پاکت ورودی مهرومومشده) |
| بخش عمومی | یک بار، هنگام استقرار | read_public(i) / rand_read_public(i) | همه؛ شناسهٔ برنامه آن را مقید میکند | 32,768 کلمه |
| خروجیها | در هر اجرا | write_output(slot, w) / rand_write_output | همه، در رسید | 8 کلمه |
ورودیهای خصوصی بهتنهایی دربارهٔ جهان چیزی ثابت نمیکنند
یک ورودی خصوصی با تعهد نمکیشدهٔ H_IN به اثبات مقید میشود، پس پس از اثبات نمیتواند تغییر کند. اما برنامه راهی ندارد که بداند ورودی درست است. فراخواننده میتواند هر عددی را وارد کند. برای اینکه یک ورودی خصوصی معنایی داشته باشد، آن را درون برنامه به چیزی عمومی گره بزنید:
- یک تعهد که پیشتر منتشر شده است (در بخش عمومی، یا بهصورت خارج از باند به راستیآزما داده شده) و ورودی خصوصی باید آن را باز کند، مانند حراج در ادامه؛
- یک ریشهٔ Merkle که یک برگ خصوصی باید زیر آن اثبات شود (Poseidon2 برای این کار syscall است)؛
- یک digest که راستیآزما از قبل دارد و برنامه آن را دوباره محاسبه میکند و بهعنوان خروجی منتشر میکند.
این مهارت اصلی طراحی برای برنامههای محرمانه است. اثبات به راستیآزما میگوید چه چیزی محاسبه شد. اتصال به دادههای عمومی میگوید روی چه چیزی.
کدگذاری داده بهصورت کلمه
همهچیز بهصورت u32 منتقل میشود:
| داده | کدگذاری |
|---|---|
| رشتهٔ بایت | یک کلمهٔ طول، سپس بایتها چهارتا در هر کلمه، little-endian، و کلمهٔ آخر با صفر پر شده (همان شکلی که کیف پول برای ورودی عمومیِ ELF به کار میبرد) |
یک u64 یا یک مقدار مالی | دو کلمه، ابتدا پایین و سپس بالا |
| عدد صحیح علامتدار | مکمل دو در یک کلمه |
| یک نتیجهٔ بزرگ | digest آن با Poseidon2: 8 کلمه، که همهٔ خروجیها را پر میکند؛ یا چند کلمهٔ خلاصه و یک تعهد کوتاهتر منتشر کنید |
ابزارهای خط فرمان کلمههای ورودی را بهصورت دهدهی میگیرند: rand-guest run --input و rand call --input مقدار 0x… را نمیپذیرند. پروندهٔ بخش عمومی برای rand program deploy --public هم دهدهی و هم hex با 0x را میپذیرد.
خروجیها را شکل دهید
- اسلات 0 یک کلمهٔ وضعیت است. به هر پیامد یک عدد بدهید، از جمله شکستها: بازشدنی که مطابقت ندارد، تعداد زیاد آیتم، ورودی خارج از بازه. یک بررسی ناموفق باید باز هم با یک وضعیت بهطور تمیز متوقف شود تا فراخواننده اثباتی بگیرد که میگوید چه چیزی خراب شد.
- هر اسلات حداکثر یک بار نوشته میشود. نوشتن دوم در همان اسلات یک trap است (
DoubleWrite(0)در شبیهساز)، و اجرایی که trap کند قابل اثبات نیست. اسلاتی که هرگز ننویسید صفر خوانده میشود. - در Rust اسلات 7 را آزاد نگه دارید. یک guest در Rust که panic کند مقدار
0xdeadbeef(3,735,928,559) را در اسلات 7 مینویسد و متوقف میشود. خروجیهایی که پیش از panic نوشته است سر جایشان میمانند، پس خواننده باید پیش از باور کردن بقیه، اسلات 7 را بررسی کند (بخش 6). - خواندن فراتر از ورودیهای دادهشده یک trap است (
InputIndex(5)در شبیهساز). ابتدا تعداد را بخوانید و بررسی کنید.
3. نمونهٔ کامل: یک حراج با پیشنهاد مهرومومشده
یک فروشنده میخواهد حراج قیمت دوم برگزار کند بیآنکه پیشنهادهای بازنده را منتشر کند:
- هر پیشنهاددهنده یک پیشنهاد و یک salt تصادفی 128 بیتی انتخاب میکند و یک تعهد به فروشنده میدهد: Poseidon2 روی
("BID1", bidder index, bid, salt). - فروشنده برنامهٔ حراج را با قیمت رزرو و همهٔ تعهدها بهعنوان بخش عمومی آن مستقر میکند. شناسهٔ برنامه اکنون نام همین حراج است و نه چیز دیگر.
- هر پیشنهاددهنده پیشنهاد و salt خود را بهصورت خصوصی برای فروشنده میفرستد.
- فروشنده برنامه را با همهٔ پیشنهادها و saltها بهعنوان ورودی خصوصی فراخوانی میکند. برنامه هر بازشدن را با تعهد آن بررسی میکند، برنده را برمیگزیند و
[status, winner, price, bidder count]را منتشر میکند.
زنجیره میفهمد چه کسی و با چه قیمتی برنده شد. هرگز پیشنهادهای بازنده را نمیبیند. فروشندهای که هر پیشنهادی را تغییر دهد بهجای نتیجه BAD_OPENING میگیرد، چون تعهدها در شناسهٔ برنامه ثابت شدهاند.
همهچیز درون checkout مربوط به circuits قرار دارد، چون rand-guest build با جستوجو به سمت بالا از پوشهٔ guest، 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
شبیهساز بهعنوان ماشینحساب hash
پیشنهاددهندهها به Poseidon2 روی میدان Goldilocks نیاز دارند، دقیقاً همانگونه که ماشین آن را محاسبه میکند. سادهترین ابزار درست یک guest کوچک است که آن را محاسبه کند و روی شبیهساز اجرا شود. یک digest هشت کلمه است، که دقیقاً برابر با تعداد اسلاتهای خروجی است.
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 کلمه را درجا hash میکند و digest هشتکلمهای را روی ptr..ptr+8 مینویسد، پس بافر باید دستکم هشت کلمه جا داشته باشد، هرچند فقط هفت کلمه hash میشود. برچسب "BID1" این digestها را از هر کاربرد دیگر Poseidon2 جدا نگه میدارد.
در aeacf31 تابع poseidon2 یک unsafe fn است، چون یک اشارهگر خام میگیرد، پس فراخوانی در بلوک unsafe قرار میگیرد. در 7ef3220، که خروجیهای زیر با آن تولید شدند، تابعی امن بود و همین خط بدون آن بلوک نوشته میشد. اسفنج padding انجام نمیدهد: [a] و [a, 0] یک hash دارند. اینجا بیضرر است، چون هر پیام هفت کلمه است و با یک برچسب شروع میشود. برای پیامی که طولش میتواند تغییر کند، SDK در aeacf31 تابع poseidon2_len(buf, n) را دارد که طول را مقید میکند (syscall 7)، اما rand-guest check در آن کامیت syscall 7 را رد میکند، پس چنین guestی هنوز build نمیشود (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 از خروجیهای build در اینجا و در ادامه حذف شدهاند. هشدار unaligned-scalar-mem که cargo چاپ میکند انتظار میرود (استقرار یک برنامهٔ Rust).
چهار پیشنهاددهنده، با این پیشنهادها و saltها:
| پیشنهاددهنده | پیشنهاد | نمک (چهار واژه) |
|---|---|---|
| 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) و چهار چکیده بهترتیب، بخش عمومی یعنی auction-public.txt را با 34 واژه میدهد:
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 را روی دستگاه خودش اجرا میکند و نمک از یک منبع تصادفی واقعی میآید.
4. برنامه در Rust
قاعده، بهصورت کتابخانهای که میتوان آزمونش کرد
منطقی را که به فراخوانی سیستمی نیاز ندارد در یک crate مستقل با no_std نگه دارید. guest-sdk برای هر هدفی جز riscv32 از ساخت سر باز میزند، پس یک crate مهمان نمیتواند روی لپتاپ شما cargo test اجرا کند. یک crate کتابخانهای که به 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
مهمان
my-guests/auction/Cargo.toml از طریق مسیر به هر دو crate وابسته است:
[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 میگذرد، که هر خانه را یک بار مینویسد و متوقف میشود. کران حلقه از بخش عمومی میآید که در زمان استقرار ثابت میشود. بنابراین فراخواننده نمیتواند برنامه را وادار کند پیشنهادهای کمتری از تعهدشده بخواند.
ساخت و اجرا
$ 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)
فروخته شد (1) به پیشنهاددهندهٔ 2 با 400، یعنی دومین پیشنهاد بالاتر، و با شمارش چهار پیشنهاددهنده. دو اجرا که باید شکست بخورند:
| تغییر | خروجیهای 0–3 | معنا |
|---|---|---|
| جایگزینی پیشنهاد 250 پیشنهاددهندهٔ 1 با 260 | 3 1 0 0 | BAD_OPENING در پیشنهاددهندهٔ 1؛ چیز دیگری منتشر نمیشود |
| قیمت پایهٔ 1,000 بهجای 100 (بخش عمومی متفاوت، پس شناسهٔ برنامهٔ متفاوت) | 2 0 0 4 | NO_SALE: هیچ پیشنهادی به قیمت پایه نرسید |
5. همان برنامه در C
یک پوشهٔ مهمانِ C شامل فایلهای .c و سرفایلهای خودتان است. rand-guest build --lang c فایل guest.h، تابع _start و یک زماناجرای کوچک را فراهم میکند (استقرار یک برنامهٔ RISC-V). این ابزار همهٔ فایلهای .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 یک static با مقداردهی اولیهٔ صفر است، پس در .bss قرار میگیرد. این کار هیچ واژهای به تصویر اضافه نمیکند، چون RAM از پیش صفر میخواند.
آزمون قاعده روی میزبان
settle.c از هیچ فراخوانی سیستمی استفاده نمیکند، پس کامپایلر معمول شما میتواند آن را بسازد. 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 یکساناند. با چهار پیشنهاددهنده، تصویر C به اندازهٔ 58 واژه کوچکتر و 65 چرخه سریعتر است؛ بخش 7 نشان میدهد که توازن در 64 پیشنهاددهنده برمیگردد. این دو برنامهٔ متفاوتاند با hc و شناسهٔ برنامهٔ متفاوت. هر کدام را که سورس آن را منتشر میکنید مستقر کنید.
6. نکاتی دربارهٔ زبانها
Rust
crate. #![no_std]، #![no_main] و یک #[no_mangle] pub extern "C" fn main() -> ! که با halt() پایان مییابد. پروفایل release مقدار panic = "abort" را تنظیم میکند. یک جدول خالی [workspace] crate را از هر workspace والد بیرون نگه میدارد. .cargo/config.toml و پرچمهای کامپایلر خودتان را کنار بگذارید: rand-guest build اولی را رد میکند و همهٔ دومی را خودش تنظیم میکند (استقرار یک برنامهٔ Rust).
پنیکها. کنترلگر پنیک در guest-sdk مقدار 0xdeadbeef را در خانهٔ 7 مینویسد و متوقف میشود. اجرا همچنان یک اجرای معتبر و اثباتپذیر است. این برنامه پس از نوشتن خانههای 0 و 1، اندیس 999 از یک Vec ششعنصری را میخواند:
$ 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) };
ناحیه صفرشده است، پس در .bss قرار میگیرد و واژهای به تصویر اضافه نمیکند. اسکریپت پیوندده پیشفرض 1 MiB RAM میدهد و ناحیه، متغیرهای static شما و پشتهٔ 64 KiB باید در آن جا شوند. با این تخصیصدهنده، Vec، collect() و iter().sum() همه ساخته میشوند و از check میگذرند. با این حال alloc و وابستگیهایش کد اضافه میکنند: این برنامهٔ آزمون 1,341 واژه است، در برابر 258 برای مزایده. بخش 7 توضیح میدهد چرا اندازهٔ برنامه از بودجهٔ اثبات خرج میکند.
ممیز شناور. هدف دستورهای ممیز شناور ندارد و Rust مقادیر f32/f64 را به روالهای نرمافزاری تبدیل میکند. کار میکنند: میانگین 9، 2، 7، 4 و 5 که در f64 محاسبه شد، بهصورت بیتهای 5.4f32 برگشت. هر کدام چرخههای زیادی خرج میکنند. هرجا میتوانید از اعداد صحیح یا ممیز ثابت استفاده کنید.
crateها. crateهای no_std از crates.io مثل هر وابستگی دیگر ساخته میشوند. CRC-32 مربوط به hello, rand با crate با نام crc، روی شبیهساز، 2124296908 چاپ کرد، همان مقداری که zlib.crc32 پایتون میدهد. crateهایی که به std نیاز دارند برای این هدف ساخته نمیشوند. rand-guest build پرچم --locked را نمیدهد، پس Cargo.lock خود را کامیت کنید: بدون آن، ساخت دوبارهٔ ماهها بعد ممکن است crate جدیدتری را حل کند و 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)
عامل در اینجا slice::sort_unstable روی یک Vec بود که طولش فقط در زمان اجرا معلوم است. sort() پایدار همین کار را کرد، در حالی که sort_unstable روی یک آرایهٔ ثابت 16عنصری گذشت. برای یافتن منبع چنین یافتهای، ELF مهمان را دیساسمبل کنید و تابع پیرامون نشانیای را که check چاپ کرد ببینید:
llvm-objdump -d -C my-guests/heap/target/riscv32im-unknown-none-elf/release/heap | grep -B12 unimp
سپس آن فراخوانی را با کد خودتان جایگزین کنید (برای این برنامه، یک مرتبسازی درجی کوتاه). halt() خودِ guest-sdk به همین دلیل بهجای وعدهٔ 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هر دو را رد میکند. فایلهای آزمون را بیرون از پوشهٔ مهمان نگه دارید، چون هر فایل.cدر آن به تصویر کامپایل میشود. - clang سنجاقشده.
rand-guestهر clang جز 23.1.1 را رد میکند.RAND_GUEST_CLANG_UNPINNED=1آن را نادیده میگیرد، با هشداری کهhcشما با ساخت دیگران یکی نخواهد بود.
7. آیا اثبات میشود؟ بودجه
اثباتکننده هر اجرا را تا یک ردهٔ t پد میکند. هر رده دو بودجهٔ مستقل دارد و rand-guest run --tier t هر دو را گزارش میکند:
- سطرها: حداکثر
2ᵗ − 1، شامل چرخههای اجراشده بهعلاوهٔ سطرهای چکیده؛ - جایگشتهای Poseidon2: حداکثر
2ᵗ⁻³.
| رده | سطرها | جایگشتهای Poseidon2 | سقف گس بدون جدول هش | یک فراخوانی روی زنجیرهٔ 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 رد میشود |
کارمزد از گس و بایت پیروی میکند، نه از رده. در زنجیرهٔ 15 هر فراخوانی یک مبلغ ثابت 0.001 + call_fee(tier) میپرداخت: 0.002 RAND در رده 10، 0.0021 در رده 12 و 0.0022 در رده 14. از زنجیرهٔ 18 هر اجرا اندازهگیری میشود. یک سطر معمولی 1 گس است، یک سطر جذب Poseidon2 برابر 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 واحد به ازای هر گس و 800 واحد به ازای هر KiB شروع شدند (هر واحد 10⁻⁹ RAND است) و در هر بلوک اندکی تغییر میکنند. در زنجیرهٔ 20 این قیمتها نمیتوانند از 10,000 و 80,000 واحد بالاتر بروند و فقط بایتهای اثبات و پاکت فراخوانیها قیمت بایت را جابهجا میکنند؛ در زنجیرههای 18 و 19 سقفی وجود نداشت. بخش 9 نشان میدهد کیف پول بهطور پیشفرض چه میپردازد، و برنامههای نمونه چهار فراخوانی اندازهگیریشده روی زنجیرهٔ 18 دارد، هر کدام با 0.0057 تا 0.0075 RAND.
ترازِ ۱۴ بالاترین ترازی است که یک فراخوانی میتواند به کار ببرد. از fullnode v0.5.6 (2026-09-25) به بعد، یک گره هر اثبات فراخوانی بالاتر از تراز ۱۴ را پیش از ساختن کلید verifier با CallTierTooHigh رد میکند. همین قاعده باعث میشود بلوکی که چنین فراخوانیای در خود دارد نامعتبر باشد. این محدودیت از validatorهایی با ۲ تا ۴ گیگابایت حافظه محافظت میکند: ساختن کلید verifier در تراز ۲۰ دقایقی زمان و گیگابایتها حافظه میبرد. rand call چنین فراخوانیای را پیش از اثبات رد نمیکند، هرچند روی chain 20 ابتدا تراز را چاپ میکند. پیش از فراخوانی، rand-guest run … --tier 14 را اجرا کنید و اگر اجرا در آن نمیگنجد، برنامه را بازساختاردهی کنید. پیش از یک استقرار، rand program deploy --input <word> … (یا برای فراخوانی بدون ورودی --check-call) فراخوانیِ موردنظر شما را در emulator اجرا میکند و تنها در صورتی استقرار را انجام میدهد که تراز آن از ۱۴ بالاتر نرود.
بودجه صرف چه چیزی میشود
سطرها و permutationهای digest غافلگیرکنندهاند. پیش از آنکه کد شما اجرا شود، ماشین برنامه، ورودیهای خصوصی و بخش عمومی را در hc، H_IN و H_PUB هش میکند. هرکدام به ازای هر چهار کلمه حدود یک permutation و یک سطر هزینه دارد. افزون بر این، هر فراخوانی POSEIDON2 به ازای هر چهار کلمهی هششده حدود یک permutation هزینه دارد. برای حراج در تراز ۱۰:
| مورد | کلمهها | permutationها |
|---|---|---|
برنامه (hc) | 258 | حدود 65 |
ورودیهای خصوصی (H_IN) | 20 | حدود 5 |
بخش عمومی (H_PUB) | 34 | حدود 9 |
| چهار بررسی تعهد، هرکدام 7 کلمه | 4 × 7 | 8 |
جمع، آنگونه که run گزارش میکند | 88 از 128 |
سطرها: 641 چرخه بهعلاوهی سطرهای digest به 721 از 1,023 رسید.
روی chain 20، مانند chain 19، شمارش خودِ کیف پول اندکی بیشتر از شمارش rand-guest run است. اثبات فراخوانی در آنجا به تراکنشش بسته میشود (روی chain 20 این اتصال شامل genesis hash نیز میشود، با همان هشت کلمه) و این اتصال بهصورت هشت کلمهی دیگر در بخش عمومی همراه میآید: برای برنامهای با بخش عمومی دو سطر digest و دو permutation بیشتر، و برای برنامهای بدون آن یک سطر و یک permutation بیشتر.
این اندازهی برنامه را بخشی از بودجه میکند. برنامهی آزمون heap فقط 1,183 چرخه اجرا شد، اما 1,341 کلمهاش 340 permutation هزینه داشت و آن را به تراز 12 رساند. در 2,048 permutationِ تراز 14، برنامهای که بسیار بزرگتر از حدود 8,000 کلمه باشد، هر کاری هم بکند، اصلاً قابل فراخوانی نیست. روی chain 20 کران دقیق برای برنامهای بدون بخش عمومی و فراخوانیای بدون ورودی 8,180 کلمه است و با هر یک از این دو کمتر میشود. سقف برنامه روی chain 20 مانند chain 15 برابر 65,535 کلمه است، اما chain 19 و chain 20 استقرار برنامهای بیش از این کران را رد میکنند (ProgramUncallable)، در حالی که chain 15 کارمزد استقرار برنامهای را میگرفت که هیچکس نمیتوانست فراخوانیاش کند. به همین دلیل ERC-20 ترجمهشدهی chain 13 (11,686 کلمه، فراخوانیشده در تراز 16) روی chain 20 نه قابل استقرار است و نه قابل فراخوانی. برنامهها را کوچک نگه دارید: در Rust مقدار opt-level = "s"، در C مقدار -Os (که از پیش تنظیم شده است)، وابستگیهای کم، و بدون alloc مگر آنکه به آن نیاز داشته باشید.
حراج بهصورت خطی رشد میکند: هر شرکتکنندهی اضافی 5 کلمهی ورودی، 8 کلمهی عمومی، 2 permutation برای بررسی و حدود صد چرخه اضافه میکند. با 64 شرکتکننده (پیشنهادهای تصادفی، 320 کلمهی ورودی، 514 کلمهی عمومی)، هر دو ساخت همان برنده و همان قیمت را انتخاب میکنند و هر دو به تراز 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)
permutationها در 512 تراز 12 جا میشدند، اما سطرها در 4,095 آن جا نمیشوند. در اینجا چرخهها، و نه هشکردن، تراز را تعیین میکنند. با 64 شرکتکننده ساخت Rust سریعتر است. opt-level = 3 آن، وقتی حلقه غالب میشود، از -Os ثابتِ ساخت C بهتر عمل میکند.
Keccak و SHA-256
keccak و sha256_compress هرکدام یک سطر هزینه دارند، اما اثباتی که از هرکدام استفاده کند یک جدول اضافی را با خود حمل میکند. در aeacf31 هر دو یک ارجاع آرایه میگیرند، یعنی keccak(&mut state) روی یک [u32; 50] و sha256_compress(&mut buf) روی یک [u32; 24]، در حالی که 7ef3220 یک اشارهگر خام میگرفت. از v0.5.6 به بعد یک فراخوانی حداکثر میتواند از 128 permutation Keccak-f و حداکثر 128 فشردهسازی SHA-256 استفاده کند. روی chain 20 هر permutation Keccak همچنین 192 gas و هر فشردهسازی SHA-256 همچنین 64 gas هزینه دارد.
جدول اثبات را بزرگتر میکند و chain 20 اثبات فراخوانی را به 4,194,304 بایت (4 MiB، مانند chain 17 تا 19؛ این سقف در chain 13 تا 16 برابر 8 MiB بود) محدود میکند. روی chain 18 یک فراخوانی تراز 12 با جدول Keccak برابر 3.35 MB اندازهگیری شد، یعنی بخش عمدهی سقف، و یک فراخوانی تراز 14 با جدول SHA-256 برابر 1.91 MB، در برابر 1.50 MB بدون هیچیک از دو جدول (Example programs). هر KiB به قیمت بایت محاسبه میشود. روی chain 15 تنها بایتهای فراتر از 2 MiB + 18,432 رایگان به نرخ 1,000 واحد به ازای هر KiB محاسبه میشدند (Fees). از Poseidon2 استفاده کنید، مگر آنکه ناچار باشید با یک هش بیرونی سازگار شوید.
حافظه و زمان اثبات
زمان و حافظهی اثبات با افزایش تراز بهشدت رشد میکنند. یک فراخوانی تراز 10 روی یک لپتاپ در چند ثانیه اثبات میشود. روی chain 20 هر استقرار و هر فراخوانی دو اثبات سمت bundle نیز دارد: یک مجوز خرجکردن (spend authorisation) در تراز 10 (چند ثانیه) و bundle تراز 14 که کارمزد را میپردازد (حدود دو دقیقه روی یک لپتاپ). چهار فراخوانی اندازهگیریشده روی chain 18 هرکدام بین سه تا پنج و نیم دقیقه طول کشیدند و به 6.1 تا 6.6 GB رسیدند (Example programs). روی chain 15 bundle یک اثبات تراز 14 واحد بود، با حدود 100 s و حدود 5.7 GB. Validator and aggregator hardware اعداد اندازهگیریشده را دارد.
8. استقرار
testnet همان chain 20 است. استقرار به یک کیف پول rand از fullnode v0.6.8 یا بالاتر، یک کلید و مقداری RAND نیاز دارد (Introduction؛ faucet در chain 20، مانند chain 19، تنها برای 16 کیف پولی که genesis آنها را در فهرست مجاز قرار داده است ضرب میکند). یک کیف پول v0.6.7 نمیتواند روی chain 20 ارسال کند: genesis در chain 20 مقدار binding_domain: 1 را تنظیم میکند، بنابراین هر اتصال اثبات و هر پیام امضاشده شامل genesis hash میشود و کیف پول 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 شناسهی برنامه، hc، تعداد کلمهها، و تعداد کلمهها و digest ورودی عمومی را چاپ میکند. پیش از آنکه چیزی را اثبات کند، برنامه را با سقف زنجیره و ورودی عمومی را با max_program_public_words (32,768 روی chain 20) بررسی میکند. سپس یک bundle انتقال-به-خود با مقدار صفر را که کارمزد را میپردازد اثبات میکند، ارسال میکند و منتظر commit میماند. روی chain 20، مانند chain 17 به بعد، این دو اثبات است که هرکدام خط خروجی خود را دارند: یک مجوز خرجکردن در تراز 10 (چند ثانیه، حدود 1.37 MB) و bundle تراز 14 (حدود دو دقیقه، حدود 1.5 MB). روی chain 15 این یک اثبات تراز 14 با حدود 100 s و حدود 1.42 MB بود. کارمزد استقرار جزء gas ندارد. حراج روی chain 18 تا 20 مستقر نشده است؛ An anonymous vote استقراری با بخش عمومی را روی chain 18 همانطور که کیف پول چاپ کرد نشان میدهد.
شناسهی برنامه با --public تغییر میکند. rand-guest برای کد بهتنهایی 148ab0c4…2c73 را چاپ کرد. استقراری با بخش عمومی زیر blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public) ذخیره میشود که ذخیره و چهار تعهد را نیز به هم میبندد. از شناسهای استفاده کنید که program deploy چاپ میکند. حراج دومی با همان کد و تعهدهای متفاوت، شناسهی برنامهی متفاوتی است.
rand program show <auction id> # base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest
code_hash همان hc است که rand-guest چاپ کرد، با املایی دیگر. rand-guest هر یک از هشت کلمهی digest را بهصورت big-endian چاپ میکند و کیف پول، rand program show و rand_getProgram بایتهای هر کلمه را بهصورت little-endian چاپ میکنند، پس هر گروه هشت رقم هگز بایتمعکوس میشود. برای ساختهای بالا، 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
کیف پول کد و بخش عمومی برنامه را میگیرد و بررسی میکند که با شناسه هش میشوند. --expect-public نیز پیش از اثبات، اگر بخش عمومی همان کلمههای آن فایل نباشد، رد میکند. سپس کیف پول فراخوانی را در emulator اجرا میکند و gas دقیق، سقفی که اعلام خواهد کرد، تراز (کوچکترینی که اجرا در آن میگنجد، در اینجا تراز 10) و کارمزد را چاپ میکند:
gas: <exact> (declaring <limit>, tier <t>)
fee <amount> RAND (incl. two price steps of headroom)
کیف پول 20 کلمهی ورودی را در پاکتی مهر میکند که فقط فراخوان میتواند بگشاید، اجرا را محلی اثبات میکند، کارمزد را از طریق یک bundle میپردازد، ارسال میکند، منتظر میماند و رسید را چاپ میکند. روی chain 20 این سه اثبات است: فراخوانی، مجوز خرجکردن در تراز 10 و bundle تراز 14.
فراخوانی چه هزینهای دارد. روی chain 15 این فراخوانی 0.001 + call_fee(10) = 0.002 RAND میپرداخت. روی chain 20 کیف پول کفی را میپردازد که در بخش 7 آمد و پیش از وجود اثبات محاسبه میشود، با سه مقدار پیشفرض:
- حد اعلامشده همان گس دقیق است که به بالاترین مضرب
2^(tier − 2)گرد شده است: ۲۵۶ در tier 10، ۱٬۰۲۴ در tier 12، ۴٬۰۹۶ در tier 14.--gas-limit maxسقف tier را اعلام میکند و--gas-limit Nیک مقدار دقیق را؛ - هزینهٔ اثبات بر اساس سقف زنجیره، یعنی 4,194,304 بایت، محاسبه میشود نه اندازهٔ واقعی آن، زیرا اثبات یک فراخوانی در 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] است: همان واژههایی که شبیهساز چاپ کرد، زیرا اثبات مربوط به همان اجراست. چهار پیشنهاد و saltها روی زنجیره نیستند. --auditor <rand1…> ورودیها را برای یک ممیز مشخص نیز مهروموم میکند و --no-envelope هیچ رونوشتی منتشر نمیکند، کاری که قابل بازگشت نیست (Programs).
۱۰. بررسی نتیجه
یک پیشنهاددهنده یا هر کس دیگری میتواند حراج را بدون اعتماد به فروشنده بررسی کند:
- کد. سورس منتشرشده را با
rand-guest buildدر همان commit مدارها، با Rust 1.98.1 (یا clang 23.1.1) بازسازی کنید.hcرا باcode_hashحاصل ازrand program showیاrand_getProgramمقایسه کنید و املای بایتمعکوس را در نظر بگیرید (بخش ۸). ساخت بازتولیدپذیر است، بنابراین این دو روی هر دستگاهی یکساناند. - بخش عمومی. آن را با
rand_getProgramPublicبگیرید یاrand call --expect-publicرا اجرا کنید. بررسی کنید که تعهد خودتان در اندیس شما قرار دارد. - رسید.
rand receipt <tx hash>یاrand_getReceipt: وضعیت1، برنده و قیمت، شکاف ۷ برابر0xdeadbeefنباشد، وprogramبا شناسهٔ حراج برابر باشد. - ورودیها، اگر حق دسترسی به آنها را دارید. دستور
rand open-call <tx hash>فروشنده (یا--as-auditorممیز) ورودیهای مهرومومشده را باز میکند، آنها را باH_INمطابقت میدهد، برنامه را دوباره اجرا میکند و در صورت هر ناهمخوانی با کد غیرصفر خارج میشود. از کیف پول 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(…) که برای commit قدیمیتری نوشته شده است شکست میخورد | در aeacf31 تابع poseidon2 یک unsafe fn است و دو تابع دیگر &mut [u32; 50] و &mut [u32; 24] میگیرند (بخشهای ۳ و ۷) |
check خطای Syscall با a7 = 7 گزارش میکند | guest تابع poseidon2_len را فراخوانی میکند؛ rand-guest check در aeacf31 فراخوانی سیستمی ۷ را فهرست نمیکند (zkp-circuits issue #1) |
check خطای Csr در 0xc0001073 گزارش میکند | کد کتابخانه به unimp کامپایل شده است؛ تابع را با llvm-objdump پیدا کنید و فراخوانی را جایگزین کنید (بخش ۶) |
run میگوید اجرا در هیچ tier نمیگنجد یا به بیش از tier 14 نیاز دارد | ردیفها یا جایگشتهای Poseidon2 بیش از حد است؛ برنامه را کوچک کنید، کمتر هش کنید یا کار را میان چند فراخوانی تقسیم کنید و یک digest را میان آنها منتقل کنید |
| slot 7 = 3735928559 | guest زبان Rust دچار panic شده است؛ خروجیهای پیش از آن قابل اتکا نیستند |
trap: DoubleWrite(n) | شکاف خروجی n دو بار نوشته شده است؛ هر خروج را از یک تابع عبور دهید |
trap: InputIndex(i) | برنامه ورودی خصوصیای را خوانده که فراخواننده فراهم نکرده است؛ ابتدا یک شمارنده را بخوانید و بررسی کنید |
trap: … در یک load یا store | آدرسی ناهمتراز یا خارج از محدوده؛ در C، اندیس آرایهای فراتر از حدش |
invalid value '0x…' for '--input' | واژههای ورودی در خط فرمان بهصورت اعشاری (دهدهی) هستند |
فراخوانیای با CallTierTooHigh رد شد | اثبات بالاتر از tier 14 است؛ پیش از فراخوانی با rand-guest run --tier 14 بررسی کنید |
hc بازسازیشده با مقدار مستقرشده تفاوت دارد | commit مدارها، toolchain یا نسخههای وابستگیها متفاوت است؛ هر سه را ثابت کنید و Cargo.lock را commit کنید. اگر فقط املا تفاوت دارد، هر گروه هشترقمی هگز بایتمعکوس است (بخش ۸) |
rand open-call با PublicIndex(0) متوقف میشود | برنامه بخش عمومی دارد و کیف پول v0.6.7 است که اجرای دوبارهٔ آن چیزی ارسال نمیکند (fullnode issue #116)؛ از v0.6.8 استفاده کنید |
… is not inside a circuits checkout | پوشهٔ guest را به داخل checkout ببرید (Deploy a Rust program) |