Rand Protocol
مستندات
برنامه‌های نمونه

یک رأی‌گیری ناشناس

برنامه‌ای محرمانه که عضویت در یک فهرست مجاز Keccak Merkle را بدون افشای هویت عضو اثبات می‌کند و یک انتخاب به همراه یک nullifier منتشر می‌کند که از رأی دوم جلوگیری می‌کند - شامل رابط، guest به زبان Rust، اسکریپت میزبان، بودجه و برنامهٔ فعال روی chain 18.

این برنامه به عضوی از یک فهرست ثابت اجازه می‌دهد یک رأی بدهد، بدون اینکه معلوم شود کدام عضو است. فهرست یک درخت Merkle از هش‌های Keccak-256 است و همان‌طور ساخته می‌شود که فهرست airdrop در Ethereum ساخته می‌شود، پس ابزار یکسانی آن را می‌سازد. برنامه روی زنجیره 18 مستقر شده است و بخش آخر آن را با اعضای نمایشی‌ای فراخوانی می‌کند که خودتان هم می‌توانید از آن‌ها استفاده کنید.

Example programs مقدمات مشترک هر چهار صفحهٔ نمونه را دارد: checkout مربوط به circuits، کیف پول، و محل دریافت سورس‌ها.

1. چه چیزی اثبات می‌شود

یک برگزارکننده فهرستی از اعضا دارد. هر عضو یک راز 32 بایتی دارد. برگزارکننده هرگز رازها را نمی‌بیند: هر عضو یک برگ به او می‌دهد، یعنی keccak256(secret ‖ tag)، و برگزارکننده روی برگ‌ها یک درخت Merkle می‌سازد.

یک رأی یک فراخوانی است. این فراخوانی بدون افشای هیچ چیز دیگری اثبات می‌کند که:

  1. فراخواننده رازی را می‌داند که برگ آن در درخت است؛
  2. انتخاب اعلام‌شده یکی از انتخاب‌های مجاز است؛
  3. ابطال‌گر اعلام‌شده برابر keccak256(secret ‖ tag ‖ poll id) است.

ابطال‌گر برای همهٔ رأی‌هایی که یک راز در یک رأی‌گیری می‌دهد یکسان است و در هر رأی‌گیری دیگر متفاوت. هر کسی که رأی‌ها را می‌شمارد، هر ابطال‌گر را فقط یک بار می‌شمارد. ابطال‌گر به برگ پیوند نمی‌خورد: رفتن از ابطال‌گر به برگ به راز نیاز دارد.

زنجیره رأی دوم را رد نمی‌کند. برنامه در Rand حافظهٔ ذخیره‌سازی ندارد، پس نمی‌تواند ابطال‌گری را به خاطر بسپارد (Programs). دو فراخوانی با یک راز هر دو تأیید می‌شوند و هر دو رسید بر جا می‌گذارند. قاعدهٔ «یک ابطال‌گر، یک رأی» را شمارنده روی رسیدها اعمال می‌کند. هر کسی می‌تواند همان شمارش را اجرا کند، و همین است که نتیجهٔ نهایی را قابل بررسی می‌کند.

2. رابط

درخت، رأی‌گیری و تعداد انتخاب‌ها برای همهٔ رأی‌ها یکسان است، پس در بخش عمومی قرار می‌گیرند که هنگام استقرار ثابت می‌شود. شناسهٔ برنامه آن‌گاه همین رأی‌گیری را نام می‌برد و نه هیچ رأی‌گیری دیگری را.

کلمهٔ عمومیمعنا
0عمق درخت d، از 1 تا 20
1شناسهٔ رأی‌گیری
2تعداد انتخاب‌ها؛ هر انتخاب 0 .. choices − 1 است
3 تا 10ریشه، 32 بایت به صورت هشت کلمهٔ little-endian
کلمهٔ خصوصیمعنا
0 تا 7راز
8اندیس برگِ عضو
9انتخاب
10 به بعدمسیر Merkle: d هم‌نیای هشت‌کلمه‌ای، از برگ به سمت بالا
شکاف خروجیمعنا
0وضعیت: 1 رأی داده شد، 2 عضو نیست، 3 انتخاب خارج از محدوده، 4 تنظیم نادرست
1انتخاب
2 تا 620 بایت اول ابطال‌گر، به صورت پنج کلمهٔ little-endian
7استفاده‌نشده؛ panic در Rust در اینجا 0xdeadbeef می‌نویسد

یک هش 32 بایتی هشت کلمه است و هشت کلمه همهٔ خروجی موجود است. انتشار 160 بیت جا را برای وضعیت و انتخاب باقی می‌گذارد. دو عضو فقط با یک برخورد هش یک ابطال‌گر 160 بیتی را شریک می‌شوند، و یافتن آن حدود 2⁸⁰ تلاش لازم دارد.

ترتیب بایت‌ها. Keccak روی بایت‌ها کار می‌کند. فراخوان سیستمی KECCAK در ماشین، حالت را به صورت کلمه‌ها می‌گیرد، به طوری که بایت i از حالت در بایت i mod 4 از کلمهٔ i / 4 قرار دارد. پس 32 بایت با خواندن چهار بایت در هر بار، به صورت little-endian، به هشت کلمه تبدیل می‌شود، و ریشه در بخش عمومی همان بایت‌های ریشه است که به این شکل خوانده شده‌اند.

3. مهمان

my-guests/allowlist/src/main.rs، کامل:

#![no_std]
#![no_main]

use guest_sdk::{halt, keccak, read_input, read_public, write_output};

const MAX_DEPTH: u32 = 20;

const OK: u32 = 1;
const NOT_MEMBER: u32 = 2;
const BAD_CHOICE: u32 = 3;
const BAD_SETUP: u32 = 4;

/// The 32 bytes hashed after a secret to make its leaf: the ASCII tag, zero-padded.
const LEAF_TAG: [u32; 8] = tag_words(b"rand-allowlist-leaf-v1");
/// The first 28 bytes hashed after a secret to make its nullifier; the poll id is the last 4.
const NULL_TAG: [u32; 8] = tag_words(b"rand-allowlist-null-v1");

/// An ASCII tag as eight words: byte `i` of the tag is byte `i % 4` of word `i / 4`.
const fn tag_words(tag: &[u8]) -> [u32; 8] {
    let mut w = [0u32; 8];
    let mut i = 0;
    while i < tag.len() {
        w[i / 4] |= (tag[i] as u32) << (8 * (i % 4));
        i += 1;
    }
    w
}

/// Keccak-256 of a 64-byte message, the hash Ethereum calls `keccak256(abi.encodePacked(a, b))`.
/// The message is one rate block, so this is one permutation: the two halves go into the first
/// 16 words of the state, `0x01` follows them, and `0x80` closes the 136-byte rate.
///
/// `a_second` is 0 or 1. When it is 1 the halves trade places. The place is computed as an
/// offset, not chosen by a branch, so both orders execute the same instructions.
fn hash2(a: &[u32; 8], b: &[u32; 8], a_second: u32) -> [u32; 8] {
    let at = 8 * (a_second & 1) as usize;
    let mut st = [0u32; 50];
    st[at..at + 8].copy_from_slice(a);
    st[8 - at..16 - at].copy_from_slice(b);
    st[16] = 0x01;
    st[33] = 0x8000_0000;
    keccak(&mut st);
    let mut out = [0u32; 8];
    out.copy_from_slice(&st[..8]);
    out
}

fn read8(at: u32) -> [u32; 8] {
    let mut w = [0u32; 8];
    for (i, x) in w.iter_mut().enumerate() {
        *x = read_input(at + i as u32);
    }
    w
}

/// Public segment, fixed at deploy: [depth, poll id, number of choices, root (8 words)].
/// Private input: [secret (8 words), leaf index, choice, siblings (8 words per level)].
/// Outputs: [status, choice, nullifier (5 words), 0].
#[no_mangle]
pub extern "C" fn main() -> ! {
    let depth = read_public(0);
    let poll = read_public(1);
    let choices = read_public(2);
    if depth == 0 || depth > MAX_DEPTH {
        write_output(0, BAD_SETUP);
        halt();
    }

    let secret = read8(0);
    let index = read_input(8);
    let choice = read_input(9);

    // Walk from the leaf to the root. Bit `level` of the index says whether the running hash
    // is the right child at that level.
    let mut cur = hash2(&secret, &LEAF_TAG, 0);
    let mut level = 0;
    while level < depth {
        let sib = read8(10 + 8 * level);
        cur = hash2(&cur, &sib, index >> level);
        level += 1;
    }

    let mut diff = 0u32;
    for k in 0..8 {
        diff |= cur[k] ^ read_public(3 + k as u32);
    }

    let mut ctx = NULL_TAG;
    ctx[7] = poll;
    let nullifier = hash2(&secret, &ctx, 0);

    if diff != 0 {
        write_output(0, NOT_MEMBER);
    } else if choice >= choices {
        write_output(0, BAD_CHOICE);
    } else {
        write_output(0, OK);
        write_output(1, choice);
        for k in 0..5 {
            write_output(2 + k as u32, nullifier[k]);
        }
    }
    halt();
}

سه چیز در آن ارزش الگوبرداری دارد.

یک جایگشت برای هر هش. guest_sdk::keccak256 هر رشتهٔ بایتی را می‌پذیرد و بهایش را با حلقه‌های بایتی می‌پردازد. هر پیام اینجا دقیقاً 64 بایت است، کمتر از نرخ 136 بایتی، پس لایه‌گذاری دو کلمهٔ ثابت است و هش یک فراخوانی KECCAK روی حالتی است که با کپی کلمه‌ها پر شده است. در اصطلاح Ethereum، hash2(a, b, 0) برابر keccak256(abi.encodePacked(a, b)) برای دو bytes32 است.

برگ و ابطال‌گر با گره‌ای از درخت اشتباه گرفته نمی‌شوند. یک گره دو هش فرزند 32 بایتی را هش می‌کند. یک برگ یک راز و یک برچسب ASCII ثابت را هش می‌کند، و یک ابطال‌گر یک راز، برچسبی دیگر و شناسهٔ رأی‌گیری را. برای اینکه یک برگ به جای گره پذیرفته شود، یکی از فرزندانش باید برابر برچسب باشد، که یعنی یافتن یک پیش‌تصویر Keccak.

مسیر بدون انشعاب بر اساس اندیس پیموده می‌شود. نسخهٔ اول ترتیب دو نیمه را با یک swap پوشانده‌شده انتخاب می‌کرد و کامپایلر پوشش را دوباره به یک انشعاب تبدیل کرد: اجرا برای هر بیت 1 در اندیس برگ 9 چرخهٔ بیشتر طول می‌کشید. خود تعداد چرخه‌ها منتشر نمی‌شود، اما حد گازی که یک فراخوانی اعلام می‌کند از روی آن گرد می‌شود (بخش 6)، پس اجرایی که به اندیس وابسته باشد می‌تواند کمی از اندیس را فاش کند. نوشتن هر نیمه در یک آفست محاسبه‌شده این مشکل را برطرف کرد: اکنون هر اندیس برگ همان 3,208 چرخه را اجرا می‌کند.

4. سمت میزبان

برگزارکننده و رأی‌دهندگان به همان Keccak نیاز دارند که ماشین محاسبه می‌کند. allowlist.py یک اسکریپت Python بدون وابستگی است که Keccak-f[1600] خودش را دارد (hashlib.sha3_256 در Python همان SHA-3 است که لایه‌گذاری متفاوتی دارد و خلاصه‌های دیگری می‌دهد). این اسکریپت دو کار انجام می‌دهد:

allowlist.py setup  <depth> <poll id> <choices> <secrets file>   writes public.txt, prints the root
allowlist.py witness <depth> <poll id> <secrets file> <member> <choice>
                                                   prints the private words and the nullifier

کل درخت، به زبان اسکریپت:

LEAF_TAG = b"rand-allowlist-leaf-v1".ljust(32, b"\0")
NULL_TAG = b"rand-allowlist-null-v1".ljust(28, b"\0")
EMPTY = bytes(32)  # an unused leaf: no secret hashes to it

def levels(depth, secrets):
    row = [keccak256(s + LEAF_TAG) for s in secrets]
    row += [EMPTY] * ((1 << depth) - len(row))
    out = [row]
    while len(row) > 1:
        row = [keccak256(row[i] + row[i + 1]) for i in range(0, len(row), 2)]
        out.append(row)
    return out

nullifier = keccak256(secret + NULL_TAG + poll.to_bytes(4, "little"))

در محیط واقعی برگزارکننده برگ‌ها را دارد و نه رازها را، و هر رأی‌دهنده شاهد خودش را از برگ‌های منتشرشده می‌سازد. اسکریپت یک فایل رازها می‌گیرد چون نمونهٔ نمایشی هر دو نقش را بازی می‌کند.

رأی‌گیری نمایشی عمق 10 (1,024 برگ)، شناسهٔ رأی‌گیری 1 و سه انتخاب دارد. رازهای هشت عضو آن در demo-secrets.txt است، هر خط یک راز 64 رقمی هگزا، و عمداً عمومی‌اند تا هر کسی بتواند برنامه را فراخوانی کند:

$ python3 allowlist.py setup 10 1 3 demo-secrets.txt
root ce8d6ef78a2ccaab688afa50ff969b0f89b23e7f32d8c3746848b46f773f4750
wrote public.txt
$ cat public.txt
10 1 3 4151217614 2882153610 1358596712 261854975 2134815369 1958991922 1874085992 1346846583

اولین کلمهٔ ریشه، 4151217614، برابر 0xf76e8dce است: چهار بایت اول ریشه یعنی ce 8d 6e f7 که به صورت little-endian خوانده شده‌اند.

5. ساخت و اجرا

$ rand-guest build my-guests/allowlist --max-words 65535
427 words against a cap of 65535 (fits); 0 ecall(s) with a non-static a7
OK
wrote my-guests/allowlist/image.bin and its .sha256 (427 words, hc 420876739f0987ae7f0fbd23a98312ed4452b7e77cf6153e424ed02aca7b5afe, program id 93c780859b60516408099dc45247a21aead612f0c65c667248253533dd932bb3)

عضو 5 به انتخاب 2 رأی می‌دهد. شاهد 90 کلمه است: 8 برای راز، یکی برای اندیس، یکی برای انتخاب و 80 برای مسیر.

$ cd my-guests/allowlist
$ python3 allowlist.py witness 10 1 demo-secrets.txt 5 2 > witness.txt
nullifier b5516c17d7f51034998f837217777deb8bc90886
nullifier words 392974773 873526743 1921224601 3950868247 2248722827
$ rand-guest run image.bin --public $(cat public.txt) --input $(cat witness.txt) --tier 14
out[0] = 1
out[1] = 2
out[2] = 392974773
out[3] = 873526743
out[4] = 1921224601
out[5] = 3950868247
out[6] = 2248722827
out[7] = 0
cycles 3208
tier 12
tier 14: fits (cycles 3342 of 16383, Poseidon2 permutations 134 of 2048)

پنج کلمهٔ ابطال‌گر همان‌هایی هستند که اسکریپت بیرون از ماشین محاسبه کرده بود. شکست‌ها با یک وضعیت به‌طور تمیز متوقف می‌شوند، پس می‌توان آن‌ها را هم اثبات کرد. همان شاهد با کلمهٔ اول راز تغییریافته out[0] = 2 می‌دهد و هیچ چیز دیگر، و عضوی که انتخاب 3 از 3 را بخواهد out[0] = 3 می‌گیرد.

6. بودجه

برنامه427 کلمه
چرخه‌ها3,208 برای درخت با عمق 10؛ 3,342 با سطرهای خلاصه
جایگشت‌های Keccak12: یک برگ، ده گره، یک ابطال‌گر. سقف هر فراخوانی 128 است
جایگشت‌های Poseidon2134 از 2,048، همه مربوط به برنامه، ورودی و خلاصه‌های عمومی
رده12

هر سطح عمق اضافه یک جایگشت و حدود 270 چرخه هزینه دارد. درخت با عمق 20 (یک میلیون عضو) 5,866 چرخه و 22 جایگشت اندازه‌گیری می‌شود، که رده 14 است، بالاترین رده‌ای که یک فراخوانی اجازهٔ استفاده از آن را دارد.

از زنجیره 18 به بعد هر فراخوانی گاز می‌پردازد، و جایگشت Keccak گران‌ترین دستور است: 192 گاز در برابر 1 برای یک چرخهٔ معمولی. کیف پول گاز دقیق یک اجرا و حدی را که برای آن اعلام می‌کند چاپ می‌کند:

gas: 5636 (declaring 6144, tier 12)

هر سطر از اجرا 1 گاز هزینه دارد و هر سطر Keccak 191 گاز بیشتر: 3,344 سطر به‌علاوهٔ 12 × 191 برابر 5,636 است، و دو پنجم آن مربوط به دوازده هش است. کیف پول دو سطر بیشتر از آنچه rand-guest run شمرد می‌شمارد. در زنجیره 18 اثبات فراخوانی به تراکنش خودش بسته می‌شود، و این پیوند به صورت هشت کلمهٔ اضافی در بخش عمومی حمل می‌شود که دو سطر خلاصه اضافه می‌کند. حد اعلام‌شده، 6,144، همان عدد دقیق است که به بالا به مضرب 1,024 گرد شده است. حد عمومی است و عدد دقیق نه.

اثباتی که جدول Keccak را حمل می‌کند بزرگ است. این یکی 3,346,865 بایت است، در برابر سقف 4,194,304 (4 MiB) در زنجیره‌های 18 تا 20. ارقام عمق 20 در بالا اندازه‌گیری‌های شبیه‌ساز هستند و برای این صفحه در آن عمق اثباتی ساخته نشده است.

7. استقرار

استقرار بخش عمومی را هم حمل می‌کند، پس کارمزد 11 کلمهٔ آن را به همراه 427 کلمهٔ کد می‌شمارد: 0.001 + 0.0001 × 438 = 0.0448 RAND.

$ rand fee deploy 427 --public-words 11
0.0448 RAND
$ rand program deploy image.bin --public public.txt
program id: ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 (427 words, hc 73760842ae87099f23bd0f7fed1283a9e7b752443e15f67c2ad04e42fe5a7bca, public input 11 words, digest 2296cfca8db9d38bd0b7579056c9dea3c8d8005df7b42e2b1e954a75fed67510)
proving the spend authorisation on this machine (tier 10; seconds)…
authorisation proved in 22.9s: tier 10, 1370827 bytes
proving the bundle (tier 14; about a minute and a half on a laptop)…
proved in 190.2s: tier 14, 1497544 bytes
submitted deploy 05f8bfcbf9fccedbd53c3ba41bcc32dc8070ed3433364fc591b4f00699438de6
  0 RAND out, 59.9552 RAND change, fee 0.0448 RAND, anchored at height 97632, proved in 190.2s (bundle) + 22.9s (auth)

شناسه‌ای که deploy چاپ می‌کند، ad2f719f…b970، همان نیست که rand-guest build چاپ کرد. با وجود بخش عمومی، شناسه برابر blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public) است، پس به ریشه، شناسهٔ رأی‌گیری و تعداد انتخاب‌ها بسته می‌شود. رأی‌گیری دوم روی همان اعضا یک استقرار دوم از همان کد با شناسهٔ رأی‌گیری متفاوت است، و شناسهٔ برنامه و ابطال‌گرهای متفاوتی می‌گیرد.

hc هم به شکل دیگری نوشته می‌شود: rand-guest هر یک از هشت کلمهٔ خود را big-endian چاپ می‌کند و کیف پول بایت‌های هر کلمه را little-endian، پس 42087673 به 73760842 تبدیل شد. این همان خلاصه است.

$ rand program show ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970
{
  "base_pc": 4096,
  "code_hash": "73760842ae87099f23bd0f7fed1283a9e7b752443e15f67c2ad04e42fe5a7bca",
  "deployed_at": 97821,
  "id": "ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970",
  "public_digest": "2296cfca8db9d38bd0b7579056c9dea3c8d8005df7b42e2b1e954a75fed67510",
  "public_words_len": 11,
  "words_len": 427
}

هر کسی می‌تواند بخش عمومی را دوباره بخواند. rand_getProgramPublic کلمه‌ها را به صورت بایت‌های little-endian برمی‌گرداند، پس ریشه همان‌طور که چاپ شده بود ظاهر می‌شود:

$ curl -s https://rpc.randprotocol.org -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"rand_getProgramPublic","params":["ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970"]}'
{"id":1,"jsonrpc":"2.0","result":"0a0000000100000003000000ce8d6ef78a2ccaab688afa50ff969b0f89b23e7f32d8c3746848b46f773f4750"}

8. رأی دادن

rand call برای هر کلمهٔ خصوصی یک --input می‌گیرد، به صورت دهدهی. sed فایل witness را به این پرچم‌ها تبدیل می‌کند:

$ rand call ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 \
    $(sed 's/[0-9][0-9]*/--input &/g' witness.txt)
proving the call locally (90 inputs stay private, over the program's 11-word public input)…
gas: 5636 (declaring 6144, tier 12)
fee 0.006191437 RAND (incl. two price steps of headroom)
proved in 29.2s: tier 12, 3346865 bytes, outputs [1, 2, 392974773, 873526743, 1921224601, 3950868247, 2248722827, 0]
proving the spend authorisation on this machine (tier 10; seconds)…
authorisation proved in 8.2s: tier 10, 1367303 bytes
proving the bundle (tier 14; about a minute and a half on a laptop)…
proved in 117.2s: tier 14, 1504260 bytes
submitted call 8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666
  0 RAND out, 59.716708563 RAND change, fee 0.006191437 RAND, anchored at height 98185, proved in 117.2s (bundle) + 8.2s (auth)
{
  "h_in": "ea71e7701e62d49cac37b239bef132178f481835027d62b34da45fd6cf2667c6",
  "h_pub": "2296cfca8db9d38bd0b7579056c9dea3c8d8005df7b42e2b1e954a75fed67510",
  "height": 98335,
  "index": 0,
  "outputs": [
    1,
    2,
    392974773,
    873526743,
    1921224601,
    3950868247,
    2248722827,
    0
  ],
  "program": "ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970",
  "tier": 12,
  "tx": "8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666"
}
input transcript published; open it with `rand open-call 8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666`

رسید همان چیزی است که زنجیره نگه می‌دارد: وضعیت 1، انتخاب 2 و پنج کلمهٔ nullifier، زیر شناسهٔ برنامهٔ رأی‌گیری. h_pub چکیدهٔ بخش عمومی رأی‌گیری است، همان مقداری که program show چاپ کرد. h_in یک تعهد نمک‌دار به ۹۰ کلمهٔ خصوصی است و بدون نمک چیزی را باز نمی‌کند.

این فراخوانی روی یک لپ‌تاپ Apple Silicon سه دقیقه طول کشید و حداکثر مصرف حافظه‌اش 6.1 GB بود: 29 s برای اثبات فراخوانی، 8 s برای مجوز خرج و 117 s برای بستهٔ پرداخت کارمزد. کارمزد، یعنی 0.0062 RAND، بیشتر بابت بایت‌هاست. محاسبهٔ آن در Four programs on chain 20 آمده است.

خط رونوشت. به‌طور پیش‌فرض کیف پول ورودی‌های خصوصی را در پاکتی مهروموم می‌کند که فقط کلید مشاهدهٔ فراخواننده آن را باز می‌کند، و آن را همراه فراخوانی منتشر می‌کند. این سابقه‌ای برای خود فراخواننده است، نه افشا: هیچ‌کس دیگری نمی‌تواند آن را باز کند. رأی‌دهنده‌ای که هیچ سابقه‌ای نمی‌خواهد، --no-envelope را می‌دهد. پس از آن هیچ‌کس، از جمله خود رأی‌دهنده، هرگز نمی‌تواند نشان دهد ورودی‌ها چه بوده‌اند.

۹. شمارش رأی‌ها

شمارنده فقط به زنجیره نیاز دارد:

  1. رأی‌گیری. بخش عمومی برنامه را بگیرید و ریشه را با فهرست منتشرشدهٔ برگ‌ها بسنجید (درخت را با levels در allowlist.py بازسازی کنید). guest را از روی کد منبع بازسازی کنید و hc را با code_hash برنامه مقایسه کنید.
  2. رسیدها. هر رسیدی را بردارید که program آن شناسهٔ رأی‌گیری است و خروجی 0 آن 1 است. رسیدی با هر وضعیت دیگر، یا با 3735928559 در اسلات 7، رأی نیست.
  3. شمارش. بر اساس خروجی‌های 2 تا 6 گروه‌بندی کنید. هر nullifier را یک بار بشمارید، یعنی نخستین رسید آن به ترتیب زنجیره، و خروجی 1 آن را به مجموع بیفزایید.

سه فراخوانی دیگر انجام شد تا شمارنده کاری برای انجام داشته باشد. عضو 5 بار دوم رأی داد، این بار برای انتخاب 0. عضو 2 برای انتخاب 1 رأی داد. عضو 3 برای انتخاب 0 رأی داد، از کیف پولی که فقط از نقطهٔ پایانی RPC عمومی استفاده می‌کرد. این رأی دوم عضو 5 است:

$ python3 allowlist.py witness 10 1 demo-secrets.txt 5 0 > witness.txt
nullifier b5516c17d7f51034998f837217777deb8bc90886
nullifier words 392974773 873526743 1921224601 3950868247 2248722827
$ rand call ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 \
    $(sed 's/[0-9][0-9]*/--input &/g' witness.txt)
…
proved in 31.0s: tier 12, 3345265 bytes, outputs [1, 0, 392974773, 873526743, 1921224601, 3950868247, 2248722827, 0]
…
submitted call 0f596b87f5137a165d6abc691dbe04f7533db619911d81449fea9ede602c40d0

زنجیره رأی دوم را پذیرفت، آن‌چنان که باید: اثبات معتبر است. رسید آن همان پنج کلمهٔ nullifier رسید نخست را دارد.

tally.py این سه قاعده را اعمال می‌کند. فراخوانی‌های برنامه را از explorer و هر رسید را از RPC عمومی می‌خواند، پس نه کیف پول لازم دارد و نه گره:

$ python3 tally.py ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970
8aa13d1f…  height 98335  choice 2  nullifier b5516c17d7f51034…  counted
0f596b87…  height 99194  choice 0  nullifier b5516c17d7f51034…  repeat, dropped
2db517df…  height 99339  choice 1  nullifier 52f2455918bbc811…  counted
fdbcc960…  height 101833  choice 0  nullifier cd86c5454b809914…  counted
tally: choice 0: 1, choice 1: 1, choice 2: 1

تغییر نظر عضو 5 شمرده نشد، و در چهار رسید چیزی جز خود nullifier نشان نمی‌دهد که دو تا از آن‌ها از یک عضو فهرست آمده‌اند. رأی‌گیری باز است: به‌عنوان هر یک از هشت عضو نمونه رأی بدهید و شمارش را دوباره اجرا کنید.

آنچه اثبات به شما نمی‌دهد

  • یک رأی برای هر راز، نه یک رأی برای هر نفر. سازمان‌دهنده تصمیم می‌گیرد چه کسی برگ می‌گیرد. عضوی که دو برگ ثبت کند، دو رأی دارد.
  • راز رأی‌دهنده فقط به اندازهٔ تصادفی بودنش خوب است. رازی که از رشته‌ای قابل حدس ساخته شده باشد را می‌توان با درهم‌سازی حدس‌ها و مقایسه با برگ‌های منتشرشده پیدا کرد. از 32 بایت تصادفی استفاده کنید.
  • انتخاب عمومی است. رسید نشان می‌دهد یک nullifier به کدام انتخاب رأی داده است. هیچ‌کس نمی‌تواند بفهمد آن nullifier کدام عضو است، اما رأی‌گیری‌ای با رأی‌دهندگان اندک و مشارکت منتشرشده، آمار اعداد کوچک است، نه رمزنگاری.
  • زمان‌بندی و کارمزدها فراداده‌اند. بستهٔ کارمزد shielded است و نام هیچ‌کس را نمی‌آورد، اما رأی‌دهنده‌ای که کیف پولش را شارژ می‌کند و یک دقیقه بعد در ساعتی خلوت رأی می‌دهد، چیزی به ناظر گفته است.
  • شمارش خارج از زنجیره است. زنجیره رسیدها را ذخیره می‌کند. شمردن آن‌ها و رد کردن nullifier دوم بر عهدهٔ هر کسی است که آن‌ها را می‌خواند.

آخرین بار با کامیت 48dc04b از fullnode (زنجیرهٔ 18) در تاریخ 2026-09-30، و circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest`), the commit fullnode v0.6.7 vendors; every `rand-guest` and `rand` output on this page was produced on 2026-09-30, the deploy and the call on chain 18 بررسی شد. هرجا کد و این صفحه با هم اختلاف دارند، حق با کد است؛ به ما اطلاع دهید.