يتيح هذا البرنامج لعضو في قائمة ثابتة أن يدلي بصوت واحد دون أن يُظهر أيَّ الأعضاء هو. القائمة شجرة Merkle من تجزئات Keccak-256، تُبنى بالطريقة نفسها التي تُبنى بها قائمة airdrop على Ethereum، فتنتجها الأدوات نفسها. البرنامج منشور على chain 18، ويستدعيه القسم الأخير بأعضاء تجريبيين يمكنك استخدامهم بنفسك.
تضم Example programs الإعداد المشترك بين صفحات الأمثلة الأربعة: نسخة circuits، والمحفظة، ومصدر الحصول على الشيفرات المصدرية.
1. ما الذي يُثبته
لدى المنظِّم قائمة بالأعضاء. يحتفظ كل عضو بسرٍّ من 32 بايت. لا يرى المنظِّم الأسرار أبدًا: يسلّم كل عضو ورقة (leaf) هي keccak256(secret ‖ tag)، ويبني المنظِّم شجرة Merkle فوق الأوراق.
التصويت استدعاء واحد. وهو يُثبت، دون كشف أي شيء آخر، ما يلي:
- أن المستدعي يعرف سرًّا ورقته في الشجرة؛
- أن الخيار المنشور أحد الخيارات المسموح بها؛
- أن المُبطِل (nullifier) المنشور يساوي
keccak256(secret ‖ tag ‖ poll id).
المُبطِل واحد لكل صوت يدلي به السر نفسه في استطلاع واحد، ويختلف في كل استطلاع آخر. ومن يعدّ الأصوات يعدّ كل مُبطِل مرة واحدة. ولا يرتبط المُبطِل بالورقة: فالانتقال من المُبطِل إلى الورقة يتطلب السر.
لا ترفض السلسلة صوتًا ثانيًا. فالبرنامج على Rand بلا تخزين، ولذلك لا يستطيع تذكّر مُبطِل (Programs). يتحقق الاستدعاءان بالسر نفسه كلاهما ويترك كلٌّ منهما إيصالًا. وقاعدة «مُبطِل واحد، صوت واحد» يطبّقها العادّ على الإيصالات. ويستطيع أي أحد إجراء العدّ نفسه، وهذا ما يجعل النتيجة قابلة للتحقق.
2. الواجهة
الشجرة والاستطلاع وعدد الخيارات واحدة في كل صوت، ولذلك توضع في المقطع العام (public segment) الثابت عند النشر. فيسمّي معرّف البرنامج هذا الاستطلاع دون غيره.
| الكلمة العامة | المعنى |
|---|---|
| 0 | عمق الشجرة d، من 1 إلى 20 |
| 1 | معرّف الاستطلاع |
| 2 | عدد الخيارات؛ الخيار هو 0 .. choices − 1 |
| 3 إلى 10 | الجذر، 32 بايت في ثماني كلمات بترتيب little-endian |
| الكلمة الخاصة | المعنى |
|---|---|
| 0 إلى 7 | السر |
| 8 | فهرس ورقة العضو |
| 9 | الخيار |
| 10 فصاعدًا | مسار Merkle: d من العقد الشقيقة، كل منها 8 كلمات، من الورقة صعودًا |
| خانة المخرجات | المعنى |
|---|---|
| 0 | الحالة: 1 تم التصويت، 2 ليس عضوًا، 3 الخيار خارج النطاق، 4 إعداد غير صالح |
| 1 | الخيار |
| 2 إلى 6 | أول 20 بايت من المُبطِل، في خمس كلمات بترتيب little-endian |
| 7 | غير مستخدمة؛ يكتب Rust panic القيمة 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.
يُقطع المسار دون تفرّع على الفهرس. اختارت النسخة الأولى ترتيب النصفين بتبديل مقنَّع، فحوّل المترجم القناع إلى تفرّع من جديد: استغرق التشغيل 9 دورات إضافية عن كل بت 1 في فهرس الورقة. عدد الدورات نفسه غير منشور، لكن حدّ الغاز الذي يعلنه الاستدعاء يُقرَّب منه (القسم 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"))
في الإنتاج يحتفظ المنظِّم بالأوراق لا بالأسرار، ويبني كل ناخب شاهده (witness) بنفسه من الأوراق المنشورة. ويأخذ السكربت ملف أسرار لأن العرض التجريبي يؤدي الدورين معًا.
للاستطلاع التجريبي عمق 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 مع صفوف الملخصات |
| تبديلات Keccak | 12: ورقة واحدة، وعشر عقد، ومُبطِل واحد. والحد الأقصى للاستدعاء 128 |
| تبديلات Poseidon2 | 134 من 2,048، وكلها للبرنامج والمدخلات والملخصات العامة |
| الطبقة | 12 |
كل مستوى إضافي من العمق يكلّف تبديلًا واحدًا ونحو 270 دورة. وشجرة بعمق 20 (مليون عضو) تقيس 5,866 دورة و22 تبديلًا، وهذه هي الطبقة 14، أعلى طبقة يجوز لاستدعاء استخدامها.
منذ chain 18 يدفع الاستدعاء مقابل الغاز، وتبديل Keccak هو التعليمة الباهظة: 192 غازًا مقابل 1 للدورة العادية. وتطبع المحفظة الغاز الدقيق للتشغيل والحد الذي تعلنه له:
gas: 5636 (declaring 6144, tier 12)
كل صف من التشغيل يكلّف 1 غاز، وصف Keccak يكلّف 191 إضافيًا: 3,344 صفًا زائد 12 × 191 يساوي 5,636، وخمساه للتجزئات الاثنتي عشرة. وتحسب المحفظة صفين أكثر مما حسبه rand-guest run. فعلى chain 18 يُربط إثبات الاستدعاء بمعاملته، وينتقل هذا الربط في ثماني كلمات إضافية من المقطع العام، فتضيف صفَّي ملخص. أما الحد المعلن، 6,144، فهو الرقم الدقيق مقرَّبًا إلى أعلى إلى مضاعف لـ 1,024. الحد علني والرقم الدقيق ليس كذلك.
الإثبات الذي يحمل جدول Keccak كبير. وهذا الإثبات 3,346,865 بايت، مقابل حد أقصى 4,194,304 (4 MiB) على chains من 18 إلى 20. وأرقام العمق 20 أعلاه قياسات المحاكي، ولم يُنشأ إثبات بذلك العمق لهذه الصفحة.
7. النشر
يحمل النشر المقطع العام، فتحتسب الرسوم كلماته الإحدى عشرة مع كلمات الشيفرة الـ 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 ملف الشاهد إلى هذه الوسائط:
$ 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، وكلمات المُبطِل الخمس، تحت معرّف برنامج الاقتراع. h_pub هو الملخّص التجزيئي للمقطع العام من الاقتراع، وهو القيمة نفسها التي عرضها program show. أما h_in فهو التزام مملَّح بالكلمات الخاصة التسعين، ولا يكشف شيئًا منها دون الملح.
استغرق الاستدعاء ثلاث دقائق على حاسوب محمول بمعالج Apple Silicon، وبلغ ذروة استهلاك الذاكرة 6.1 GB: 29 ثانية لإثبات الاستدعاء، و8 ثوانٍ لتفويض الإنفاق، و117 ثانية للحزمة التي تدفع الرسم. الرسم، 0.0062 RAND، هو في معظمه بايتات. وتجد الحساب في أربعة برامج على السلسلة 20.
سطر النسخة المدوَّنة. افتراضيًا يختم المحفظة المدخلات الخاصة في ظرف لا يفتحه إلا مفتاح العرض الخاص بالمستدعي، وتنشره مع الاستدعاء. هذا سجلّ للمستدعي لا إفصاح: لا يستطيع أحد غيره فتحه. وإذا أراد الناخب ألّا يبقى أي سجلّ على الإطلاق فعليه تمرير --no-envelope. بعد ذلك لا يستطيع أحد، ولا الناخب نفسه، أن يُظهر أبدًا ما كانت عليه المدخلات.
9. عدّ الأصوات
لا يحتاج العادّ إلى شيء سوى السلسلة:
- الاقتراع. اجلب المقطع العام للبرنامج وتحقق من الجذر مقابل القائمة المنشورة للأوراق (أعد بناء الشجرة بالدالة
levelsمنallowlist.py). ثم أعد بناء برنامج الضيف من المصدر وقارنhcمعcode_hashالخاص بالبرنامج. - الإيصالات. خذ كل إيصال تكون قيمة
programفيه معرّف الاقتراع ويكون ناتجه 0 مساويًا1. أي إيصال بحالة أخرى، أو يحمل3735928559في الخانة 7، ليس صوتًا. - العدّ. جمّع الإيصالات بحسب النواتج من 2 إلى 6. احسب كل مُبطِل مرة واحدة، أي إيصاله الأول بترتيب السلسلة، وأضف ناتجه 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
قبلت السلسلة الصوت الثاني، وهذا ما يجب أن يحدث: فالإثبات صحيح. ويحمل إيصاله كلمات المُبطِل الخمس نفسها التي حملها الإيصال الأول.
يطبّق tally.py القواعد الثلاث. يقرأ استدعاءات البرنامج من المستكشف وكل إيصال من 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 عن رأيه، وليس في الإيصالات الأربعة ما يدل على أن اثنين منها صدرا عن عضو واحد من القائمة سوى المُبطِل نفسه. الاقتراع مفتوح: صوّت بوصفك أيًّا من الأعضاء الثمانية في العرض التوضيحي وشغّل العدّ من جديد.
ما لا يمنحك إياه الإثبات
- صوت واحد لكل سرّ، لا صوت واحد لكل شخص. المنظِّم هو من يقرر من ينال ورقة. والعضو الذي يسجّل ورقتين يملك صوتين.
- سرّ الناخب بجودة عشوائيته. السرّ المشتق من سلسلة يمكن تخمينها يمكن العثور عليه بتجزئة التخمينات ومقارنتها بالأوراق المنشورة. استخدم 32 بايتًا عشوائيًا.
- الاختيار علني. يُظهر الإيصال أي اختيار صوّت له مُبطِل ما. لا أحد يستطيع أن يعرف أي عضو هو هذا المُبطِل، لكن اقتراعًا قليل الناخبين ونسبة مشاركة منشورة هو إحصاء أعداد صغيرة، لا تعمية.
- التوقيت والرسوم بيانات وصفية. حزمة الرسم محمية ولا تسمّي أحدًا، لكن الناخب الذي يموّل محفظته ثم يصوّت بعد دقيقة في ساعة هادئة قد أخبر المراقب بشيء.
- العدّ خارج السلسلة. تخزّن السلسلة الإيصالات. أما عدّها ورفض المُبطِل الثاني فيتولاهما من يقرؤها.