این صفحه یک برنامهٔ کوچک Rust را از کد منبع تا یک برنامهٔ مستقرشده روی Rand میبرد. هر فرمان rand-guest در زیر اجرا شده است و خروجی آن عیناً درج شده، نه خلاصه. اجراها در تاریخ 2026-09-19 و روی کامیت 7ef3220 از circuits انجام شدهاند. testnet اکنون chain 20 است که زنجیرهٔ ابزار آن، مانند chain 18 و 19، روی aeacf31 ساخته میشود (fullnode نسخهٔ v0.6.8 همان کامیت را پین میکند که v0.6.7 میکرد) و خروجیها در آنجا دوباره اجرا نشدهاند. مجموعهدستورهای RISC-V ماشین، فراخوانهای سیستمی و قالب فایل را توضیح میدهد.
1. نصب زنجیرهٔ ابزار
hc به کامپایلر دقیق وابسته است، پس زنجیرهٔ ابزار پین شده است.
| ابزار | نسخه | دلیل |
|---|---|---|
| Rust | 1.98.1 | guest و خود rand-guest را کامپایل میکند |
هدف riscv32im-unknown-none-elf | برای 1.98.1 | هدف guest |
| یک checkout از circuits | randprotocol/zkp-circuits روی aeacf31 برای chainهای 18 تا 20 | rand-guest، guest-sdk و اسکریپت پیوندده (linker) را در بر دارد |
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 که روی کامیت دیگری از guest-sdk بازسازی شود میتواند hc متفاوتی بگیرد. میان 7ef3220 و aeacf31 پوشهٔ rand-guest تغییری نکرد، ولی guest-sdk/src/lib.rs تغییر کرد (گام 2).
2. نوشتن guest
guest یک crate باینری از نوع no_std و no_main است که به guest-sdk وابسته است. باید درون checkout مربوط به circuits قرار گیرد: rand-guest build با جستوجوی guest-sdk/ بالاتر از پوشهٔ guest، checkout را پیدا میکند و guest بیرون از آن را رد میکند.
این guest یک آستانه، یک تعداد و به همان تعداد موجودی خصوصی میخواند. اگر موجودیها آستانه را پوشش دهند، 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] خالی crate را از هر workspace والد بیرون نگه میدارد، همانطور که guest ثبتشدهٔ 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؛ و _start را فراهم میکند که اشارهگر پشته را تنظیم میکند و main را فرا میخواند. تخصیصدهندهٔ حافظه (allocator) و ممیز شناور وجود ندارد.
در 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 طول پیام را به درون اسفنج (sponge) پیوند میدهد و فراخوان امن است. rand-guest check در aeacf31 همچنان فقط فراخوانهای سیستمی 0 تا 6 را فهرست میکند، پس guest که آن را فرا بخواند به تصویری ساخته میشود که build آن را با پیام ecall with a7 = 7, which is no syscall the machine implements رد میکند (zkp-circuits issue #1). تا زمان رفع این مشکل، guest با unsafe { poseidon2(ptr, n) } روی پیامهای برچسبدار با طول ثابت هش میکند. guest این صفحه از هیچیک از این چهار تابع استفاده نمیکند.
قواعد پوشهٔ guest:
- هیچ
.cargo/configیا.cargo/config.tomlدر آن، در هیچ پوشهٔ والد و در$CARGO_HOMEنباشد.buildرد میکند و نام هر فایل را میآورد، زیرا cargo آن را با ساخت ادغام میکرد. - هیچ پرچم کامپایلر شخصی نباشد.
buildهمه را تنظیم میکند: اسکریپت پیوندده،-unaligned-scalar-memو یک بازنگاشت مسیر که تصویر را از محل checkout مستقل میکند. - حداکثر یک فایل
.ld.buildاز تنها فایل.ldدر پوشهٔ guest استفاده میکند، و اگر نباشد از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 باز هم تصویر را مینویسد و با کد غیرصفر خارج میشود.
| پرچم | پیشفرض | معنا |
|---|---|---|
--out <path> | <dir>/image.bin | محل نوشتن تصویر |
--max-words <n> | 4096 | سقفی که check با آن میسنجد؛ سقف زنجیرهٔ هدف را بدهید (65,535 در chain 20) |
--ld <path> | تنها .ld در پوشه، وگرنه guest-sdk/guest.ld | اسکریپت پیوندده |
--lang rust|c | rust | کامپایلر |
4. اجرا روی شبیهساز
ورودیهای خصوصی را با --input بدهید، هر کدام یک کلمه. --tier t همچنین گزارش میدهد که اجرا در رده (tier) 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 مجموع کم است و همهٔ خروجیها صفر میشوند. trap بهشکل 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 چیزی است که اثبات یک فراخوانی در برابر آن راستیآزمایی میشود. شناسهٔ برنامه چیزی است که زیر آن مستقر و فراخوانی میکنید.
6. استقرار
از کیف پول rand در fullnode نسخهٔ v0.3 یا بالاتر استفاده کنید؛ کیف پولهای قدیمیتر کانتینر تصویر را نمیخوانند. از chain 14 هر اثبات bundle در برابر binding تراکنش خودش راستیآزمایی میشود، پس کیف پولی قدیمیتر از ساخت chain-14 (b3c594c) bundleای را اثبات میکند که زنجیره رد میکند. در chain 20، یعنی زنجیرهٔ زنده، از rand در fullnode نسخهٔ v0.6.8 یا بالاتر استفاده کنید (ناوگان v0.6.8 را اجرا میکند و یک rand ساختهشده روی Linux به انتشار GitHub آن پیوست است). کیف پول v0.6.7 نمیتواند در chain 20 ارسال کند: genesis در chain 20 مقدار binding_domain: 1 را تنظیم میکند، پس هر binding اثبات و هر پیام امضاشده شامل هش genesis است، و کیف پول v0.6.7 تنها شناسهٔ زنجیره را میبندد. نقطهٔ پایانی عمومی https://rpc.randprotocol.org برای همگامسازی کیف پول، استقرار و فراخوانی کافی است (راهاندازی).
rand fee deploy 40 # the floor for a 40-word program
rand program deploy threshold.bin
program deploy شناسهٔ برنامه، تعداد کلمهها و hc را چاپ میکند. سپس با rand_estimateFee کارمزد استقرار را از node میپرسد؛ این فراخوان برنامهای را که از سقف زنجیره بزرگتر باشد پیش از هر اثبات رد میکند. سپس یک bundle انتقال به خود با ارزش صفر را که کارمزد را میپردازد اثبات میکند، ارسال میکند و منتظر commit میماند. این bundle همان bundle دارایی پنهان 4-ورودی و 4-خروجی است که هر تراکنش دارد. از chain 17 این دو اثبات است و کیف پول برای هرکدام یک خط چاپ میکند: مجوز خرج (spend authorisation) در رده 10 (چند ثانیه، حدود 1.37 MB) و bundle در رده 14 (حدود دو دقیقه روی لپتاپ، حدود 1.5 MB). در chain 15 یک اثبات واحد رده 14 با حدود 100 ثانیه و حدود 1.42 MB بود. یک رأی ناشناس استقرار روی chain 18 را همانطور که کیف پول چاپ کرد نشان میدهد.
کارمزد استقرار 0.001 RAND + 0.0001 RAND × words است، در chain 20 مانند پیش. gas بهای فراخوانیها را تعیین میکند، نه استقرارها را:
| برنامه | کلمهها | کارمزد استقرار |
|---|---|---|
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 کلمه است، پس هم در سقف پیشفرض جا میشود و هم در 65,535 زنجیرهٔ chain 20. برنامهای بیش از 4,096 کلمه زنجیرهای میخواهد که genesis آن max_program_words را دستکم به همان اندازه بالا ببرد، مانند chain 20 (مجموعهدستورهای RISC-V). Chain 20، مانند chain 19، کران دوم و پایینتری دارد: استقرار برنامهای را که هیچ فراخوان رده 14 نتواند در خود جا دهد رد میکند (ProgramUncallable)، یعنی بیش از 8,180 کلمه بدون بخش عمومی و کمتر از آن با بخش عمومی. rand program deploy --input <word> … (یا --check-call برای فراخوانی بدون ورودی) فراخوانی مدنظر شما را نخست در شبیهساز اجرا میکند و اگر در رده 14 یا پایینتر نیفتد استقرار را رد میکند.
رکورد را بررسی کنید:
rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
code_hash آن همان hc است که rand-guest چاپ کرد، با املای متفاوت. rand-guest هر یک از هشت کلمهٔ digest را big-endian چاپ میکند، و کیف پول، rand program show و rand_getProgram بایتهای هر کلمه را little-endian چاپ میکنند. پس a491df99…3665 بهشکل 99df91a4…707c ظاهر میشود: هر گروه هشترقمی هگز بایتبهبایت وارونه است. شناسهٔ برنامه در هر دو یکسان نوشته میشود.
7. فراخوانی
rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
--input 1000 --input 4 --input 400 --input 250 --input 300 --input 75
کیف پول کد را میگیرد، فراخوانی را در شبیهساز اجرا میکند و پیش از اثبات هر چیز gas: <exact> (declaring <limit>, tier <t>) و کارمزد را چاپ میکند. سپس رونوشت ورودی را مهروموم میکند، اجرا را بهصورت محلی اثبات میکند، کارمزد را از طریق یک bundle میپردازد و رسید را چاپ میکند. در chain 20 این سه اثبات است: فراخوانی، مجوز خرج رده 10 و bundle رده 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 که هر واحد 10⁻⁹ RAND است)، بهعلاوهٔ بایتهای اثبات و envelope به نرخ byte price (۸۰۰ واحد در هر KiB در genesis). در chain 20 این دو نرخ نمیتوانند از ۱۰٬۰۰۰ و ۸۰٬۰۰۰ واحد بالاتر بروند. گاز دقیق خصوصی میماند. بهطور پیشفرض، کیف پول آن را رو به بالا و به مضرب 2^(tier − 2) گرد میکند و اعلام میکند، اثبات را با سقف ۴٬۱۹۴٬۳۰۴ بایتی زنجیره قیمتگذاری میکند، چون کارمزد پیش از وجود اثبات ثابت میشود، و دو گام قیمتی هم برای حاشیهٔ اطمینان اضافه میکند. چهار فراخوانی اندازهگیریشده در chain 18 هر کدام بین 0.0057 تا 0.0075 RAND هزینه داشتند (Example programs). این فراخوانی روی chain 18 تا 20 انجام نشده است، بنابراین عددی برای آن ذکر نمیشود.
زنجیره tier، سقف گاز اعلامشده، H_IN و هشت کلمهٔ خروجی [1, 25, 0, 0, 0, 0, 0, 0] را میبیند. هرگز موجودیها را نمیبیند.
Programs دربارهٔ رسیدها، input envelopeها و rand open-call توضیح میدهد. یک Call مانند این بدون حالت (stateless) است؛ در chain 20 یک برنامه میتواند از طریق Invoke سلولها و یک vault هم نگه دارد (Program state (RPL-2)).
بگذارید دیگران hc شما را بررسی کنند
این build بازتولیدپذیر است. build محیط کامپایلر را پاکسازی میکند (RUSTFLAGS، بازنویسیهای CARGO_*، wrapperهای RUSTC و موارد دیگر)، فایلهای پیکربندی cargo را رد میکند و مسیر checkout را به /rand-circuits بازنگاشت میکند. همان سورس با Rust 1.98.1 روی هر ماشینی همان hc را میدهد. برای اینکه دیگران بتوانند آنچه را مستقر کردهاید تأیید کنند، سورس guest و commit مربوط به circuits خود را منتشر کنید (aeacf31 برای guestی که برای chain 18 تا 20 build شده است). آنگاه هر کس میتواند دوباره build کند و hc را با code_hash که rand_getProgram برمیگرداند مقایسه کند. زنجیره فقط کلمات را ذخیره میکند، نه سورس را.
عیبیابی
| message | cause |
|---|---|
… is not inside a circuits checkout (no guest-sdk/ above it) | پوشهٔ guest را به داخل checkout منتقل کنید |
build یک .cargo/config.toml را نام میبرد | آن را حذف یا جابهجا کنید؛ پاکسازیشدنی نیست |
REJECTED همراه با Cap | برنامه از --max-words بزرگتر است؛ آن را تا سقف زنجیرهٔ هدف بالا ببرید یا برنامه را کوچک کنید (opt-level = "s") |
REJECTED همراه با Syscall و a7 = 7 | guest فراخوانی poseidon2_len را انجام میدهد؛ rand-guest check در aeacf31 هنوز syscall 7 را نمیشناسد (zkp-circuits issue #1) |
REJECTED همراه با Fence، Csr یا Ebreak | build دستورالعملی تولید کرده که ماشین ندارد؛ what is refused را ببینید |
trap: … از run | خطای زمان اجرا، مانند آدرس نامتراز یا شمارهٔ syscall پیادهسازینشده |
run میگوید اجرا در هیچ tier نمیگنجد | اجرا از 2²⁰ ردیف یا از بودجهٔ Poseidon2 خود بیشتر است؛ نمیتوان آن را اثبات کرد |