توضیحنامهی ۰۵
چرخهی کامل: از تماس NFC تا رای روی زنجیره
این متن یک کاربر، یک رای را، از ابتدا تا انتها ردیابی میکند. هر توضیحنامهی دیگر یک برش را عمیق پوشش میدهد؛ این یکی کل خط لوله را پیمایش میکند و نشان میدهد هر تکهی داده کجا زندگی میکند و کجا میمیرد.
اگر فقط یک توضیحنامه میخوانید، این را دوم بخوانید (بعد از #۱).
سه مرحله
۱. ثبتنام
یکبار
۲. ورود به کیف پول
در هر نشست
۳. رایگیری
در هر پیشنهاد
ثبتنام سنگین است: اسکن NFC، اثبات ZK، نوشتن روی زنجیره. یک بار به ازای هر سند انجام میشود. ورود به کیف پول محلی است: PIN / بیومتریک، بدون تعامل با زنجیره. رایگیری متوسطسنگین است: یک اثبات «query» کوچکتر و یک تراکنش روی زنجیره به ازای هر برگهی رای.
مرحلهی ۱: ثبتنام
گام ۱٫۱ — کاربر کیف پول جمهور را نصب میکند
برنامهی کیف پول از App Store یا Play Store دانلود میشود. در اولین اجرا:
- یک کلید خصوصی BabyJubjub بهصورت محلی تولید میکند (تصادفی، ~ ۳۲ بایت).
- آن را در ذخیرهسازی امن سکو (Keychain اپل / Keystore اندروید) ذخیره میکند که از سوی سیستمعامل رمزگذاری شده.
- قفلگشایی بیومتریک / PIN را راهاندازی میکند.
- گواهی اپلیکیشن (App Attest روی iOS، Play Integrity روی اندروید) را انجام میدهد و کیف پول را با
sso-svcثبت میکند. این در تولید، شبیهسازها، دستگاههای root شده و نسخههای اصلاحشدهی برنامه را مسدود میکند.
دادهی تولیدشده: کلید خصوصی کیف پول. فقط روی این دستگاه زندگی میکند، برای همیشه. دادهی فرستادهشده به خارج از دستگاه: کلید عمومی کیف پول (BabyJubjub {x, y}) + payload گواهی.
گام ۱٫۲ — کاربر MRZ را با دوربین اسکن میکند
کیف پول از دوربین دستگاه برای OCR ناحیهی قابلخواندن ماشینی گذرنامه یا INID خود بهره میبرد. این یک اسکن یکخطی متنی است؛ داده برای مشتقکردن کلید جلسهی NFC نیاز است.
دادهی تولیدشده: رشتهی MRZ (شمارهی سند، تاریخ تولد، انقضا). عمر: در حافظهی برنامه نگهداشته میشود، برای مشتقکردن کلیدهای BAC/PACE بهکاربرده میشود، سپس دور انداخته میشود.
گام ۱٫۳ — کاربر سند را روی تلفن میگذارد (NFC)
کیف پول یک جلسهی NFC امن با تراشه با استفاده از BAC (گذرنامههای قدیمیتر) یا PACE (گذرنامههای جدیدتر / INIDها) برقرار میکند. در این جلسه میخواند:
- DG1 — دادهی MRZ
- DG2 — تصویر چهره
- DG13 (INID) — دادهی شخصی
- DG15 (گذرنامههای دارای احراز فعال) — کلید عمومی احراز
- SOD — فهرست هشهای امضاشده
- برای INID بهطور مشخص، گواهی امضا و گواهی احراز هویت از راه توالی فرمان APDU پردیس/Mav4 خوانده میشوند.
دادهی تولیدشده: کل دامپ تراشه. فقط در حافظهی برنامه زندگی میکند؛ هرگز روی دیسک نوشته نمیشود، هرگز به بیرون از دستگاه فرستاده نمیشود.
گام ۱٫۴ — برنامه مدار ZK مناسب را انتخاب میکند
کیف پول گواهی Document Signer را بررسی میکند تا تعیین کند:
- الگوریتم امضا (RSA در مقابل ECDSA)
- الگوریتم هش (SHA-1 / SHA-256 / SHA-384)
- اندازهی کلید (۲۰۴۸ / ۳۰۷۲ / ۴۰۹۶ بیت) یا منحنی
- نوع سند (TD1 برای INID / کارتهای ID، TD3 برای گذرنامهها)
بر اساس آن، یک شناسهی مدار مثل passport_rsa_2048_sha256_e65537 یا inid_rsa_2048 انتخاب میکند.
گام ۱٫۵ — تولید Witness
کیف پول دادهی تراشه + راز کاربر + ریشهی فعلی ICAO را به WASM مدار انتخابشده میدهد. یک ماژول بومی (witnesscalculator) witness را محاسبه میکند — کل فهرست مقادیر میانی که میلیونها قید مدار را برآورده میکنند. این ۲۰ تا ۶۰ ثانیه طول میکشد.
دادهی تولیدشده: witness (~ ۱۰ تا ۱۰۰ مگابایت، در حافظه). پس از تولید اثبات دور انداخته میشود.
گام ۱٫۶ — تولید اثبات
کیف پول اثباتگر Groth16 (rapidsnark-wrp، ماژول بومی) را فراخوانی میکند. ورودی: witness + کلید اثبات مدار (~ صدها مگابایت، از پیش در برنامه بستهبندی شده). خروجی: یک اثبات ۲۵۶ بایتی و ~ ۲۴ سیگنال عمومی. ۳۰ تا ۹۰ ثانیه CPU تلفن، فنها روشن، باتری تخلیه میشود.
دادهی تولیدشده: اثبات ۲۵۶ بایتی + سیگنالهای عمومی (nullifier، ریشهی ICAO، کد تابعیت، تاریخها). این تنها آرتیفکتهایی هستند که تلفن را ترک خواهند کرد.
گام ۱٫۷ — فرستادن به رلهی ثبتنام
کیف پول اثبات + سیگنالهای عمومی را POST میکند به:
https://api.iranians.vote/integrations/registration-relayer/v1/register
رله یک سرویس Go نازک است. این:
- اثبات را بهعنوان یک بررسی سلامتسنجی خارج از زنجیره راستیآزمایی میکند (ارزان).
- اثبات + سیگنالها را در یک تراکنش راریمو L2 میپیچد.
- تراکنش را با کیف پول دارای موجودی خود امضا میکند (gas RMO).
- به RPC راریمو L2 منتشر میکند.
- هش تراکنش را به کیف پول برمیگرداند.
رله دادهی سند کاربر را نمیبیند — فقط اثبات + سیگنالهای عمومی را. وجود آن برای پرداخت gas است، نه برای دروازهگذاری دسترسی.
دادهی تولیدشده روی زنجیره: یک ورودی درRegistrationSMT(درخت Merkle پراکنده از تعهدات هویت). کاربر اکنون ثبتنام شده. هش گواهی DS هم اگر قبلن نبود بهCertificatesSMTاضافه میشود.
گام ۱٫۸ — برنامه یک علامت «ثبتنامشده» محلی مینویسد
کیف پول ذخیره میکند: تعهد هویت مشتقشده از راز کیف پول، هش تراکنش، و شناسهی مدار استفادهشده. هیچ چیز دربارهی خود سند.
دادهی نابودشده در این گام: MRZ، DG1/DG2/DG13/DG15، SOD، گواهیها، witness. همه در حافظه صفر میشوند.
مرحلهی ۱ کامل شد. کاربر اکنون میتواند وارد شود و رای دهد. هرگز نیاز نیست سند را دوباره اسکن کند، مگر اینکه برنامه را دوباره نصب کند یا کیف پولش را بچرخاند.
مرحلهی ۲: ورود به کیف پول
وقتی کاربر برنامه را باز میکند، کیف پول را با Face ID / Touch ID / PIN قفلگشایی میکند. سیستمعامل کلید خصوصی BabyJubjub را از ذخیرهسازی امن رمزگشایی میکند؛ فقط برای مدت نشست در حافظه زندگی میکند.
برای SSO «ورود با جمهور» (که از سوی تراز، Civic-Compass، شرکای آینده بهکاربرده میشود)، جریان این است:
- طرف متکی (مثلن تراز)
/v1/authorize?client_id=…&code_challenge=…را باز میکند. sso-svcیک nonce برمیگرداند + از راه Universal Link به کیف پول هدایت میکند.- کیف پول
{nonce, client_id, app_attestation}را با کلید BabyJubjub خود امضا میکند. sso-svcامضا + گواهی را راستیآزمایی میکند، یکcodeیکبارمصرف صادر میکند.- RP
code + code_verifierرا برای یک JWT دسترسی + بازنشانی مبادله میکند. subJWT یک subject جفتمدار است —HMAC(secret, wallet_id : client_id)— بنابراین RPهای مختلف شناسههای متفاوتی برای همان کاربر میبینند. بدون همبستگی بین سرویسها.
هیچ تعامل با زنجیره در این مرحله نیست. اعتماد از ثبتنام روی زنجیرهی مرحلهی ۱ ریشه میگیرد، اما خود پروتکل ورود کاملن خارج از زنجیره است.
مرحلهی ۳: رایگیری
گام ۳٫۱ — کاربر یک پیشنهاد را باز میکند
کیف پول پیشنهادهای فعال را از قرارداد رایگیری روی راریمو L2 میگیرد و نمایش میدهد. هر پیشنهاد دارای:
- یک شناسهی عددی
- یک توصیف (خارج از زنجیره، روی IPFS پینشده)
- بیتماسک گزینههای پذیرفتهشده (مثلن
[7]= سه گزینه: بله / نه / ممتنع) - سلکتور که تعیین میکند کدام ادعاها لازماند (
citizenship == IR،is_18_plus، و غیره) - دادهی فهرست سفید (کدهای کشور + کرانههای تاریخ)
گام ۳٫۲ — کاربر یک گزینه را انتخاب میکند
کیف پول انتخاب را بهصورت محلی به شناسهی پیشنهاد گره میزند. هنوز امضایی نیست.
گام ۳٫۳ — تولید اثبات Query
این یک اثبات ZK کوچکتر و سریعتر از اثبات ثبتنام است. سند را دوباره اسکن نمیکند؛ از رازی که از قبل در کیف پول ذخیره شده بهره میبرد.
اثبات میکند:
- «من در
RegistrationSMTدر ریشهی R ثبتنام شدهام.» - «هویت من سلکتور پیشنهاد را برآورده میکند» (تابعیت، سن، و غیره).
- «nullifier من برای
event_id = proposal.idبرابر N است.» - «من به گزینهی X رای میدهم.»
این اثبات حدود ۱۰ تا ۳۰ ثانیه روی تلفن طول میکشد. خروجی: ~ ۲۵۶ بایت + ~ ۲۳ سیگنال عمومی (مدار INID) یا ~ ۲۴ (مدار گذرنامه).
گام ۳٫۴ — فرستادن به رلهی رایگیری
https://api.iranians.vote/integrations/proof-verification-relayer/v3/vote
رله اثبات را در یک تراکنش راریمو L2 میپیچد، با کیف پول دارای موجودی خود امضا میکند، منتشر میکند. قرارداد رایگیری:
- precompile جفتسازی BN254 در نشانی
0x08را فراخوانی میکند. - بررسی میکند که nullifier استفاده نشده.
- شمارش رای برای گزینهی X را افزایش میدهد.
- nullifier را بهعنوان استفادهشده ثبت میکند.
اگر کاربر سعی کند دوباره رای دهد: قرارداد the key already exists برمیگرداند ← رله 400 Bad Request برمیگرداند ← کیف پول صفحهی «قبلاً رای دادهاید» را نشان میدهد.
گام ۳٫۵ — نتیجه
تراکنش در حدود ۲ ثانیه روی راریمو L2 ماین میشود. کاربر رسید خود را میبیند: هش تراکنش، رای ثبتشده، گزینهی انتخابشده.
مرحلهی ۳ برای این پیشنهاد کامل شد. کاربر میتواند مستقل به پیشنهاد بعدی رای دهد — همان کیف پول، nullifier متفاوت.
هر تکهی داده کجا زندگی میکند، در یک جدول
| داده | روی تلفن؟ | به خارج فرستاده میشود؟ | روی زنجیره؟ | عمر |
|---|---|---|---|---|
| کلید خصوصی کیف پول (BabyJubjub) | ✅ Secure enclave | ❌ هرگز | ❌ هرگز | دائمی تا حذف برنامه |
| کلید عمومی کیف پول | ✅ | ✅ در ثبتنام | ❌ | دائمی در DB sso-svc |
| رشتهی MRZ | ✅ در حافظه | ❌ | ❌ | چند ثانیه (نشست NFC) |
| DG1، DG2، DG13، DG15 | ✅ در حافظه | ❌ | ❌ | تا تولید اثبات، سپس صفر |
| SOD + گواهی DS | ✅ در حافظه | ❌ | ❌ | تا تولید اثبات، سپس صفر |
| witness ZK | ✅ در حافظه | ❌ | ❌ | چند ثانیه، سپس صفر |
| اثبات ZK (۲۵۶ بایت) | ✅ مختصراً | ✅ به رله | ✅ بخشی از دادهی تراکنش | دائمی روی زنجیره |
| سیگنالهای عمومی | ✅ مختصراً | ✅ به رله | ✅ بخشی از دادهی تراکنش | دائمی روی زنجیره |
| Nullifier | ✅ مختصراً | ✅ به رله | ✅ ثبتشده در قرارداد | دائمی روی زنجیره |
| هش گواهی DS | ✅ مختصراً | ✅ به رله | ✅ یکبار به ازای هر گواهی در CertificatesSMT | دائمی روی زنجیره |
| تعهد هویت | ✅ مختصراً | ✅ به رله | ✅ یکبار در RegistrationSMT | دائمی روی زنجیره |
| انتخاب رای (گزینهی X) | ✅ مختصراً | ✅ درون اثبات | ✅ شمرده در قرارداد | دائمی روی زنجیره |
| subject جفتمدار (SSO) | ❌ | ✅ در JWT به RP | ❌ | به اندازهی عمر JWT نشست |
الگو ثابت است: دادهی سند هرگز دستگاه را ترک نمیکند؛ فقط اثبات رمزنگارانه و خروجیهای عمومی اعلامشدهی آن میروند. هر چیزی روی زنجیره یا هش است، یا شمارش، یا یک تعهد تصادفیشده.
روایت گامبهگام (نسخهی غیرفنی)
برای زمانی که یک رایدهندهی ایرانی در خارج میپرسد «وقتی گذرنامهام را روی برنامه میگذارم چه میشود؟»، همان داستان بدون اصطلاحات فنی:
صفحهی اول گذرنامهتان را با دوربین اسکن میکنید. گذرنامه را به تلفن میچسبانید — تراشهی درون گذرنامه با تلفن شما از راه رادیوی برد کوتاه حرف میزند.
تلفن شما عکس، نام، تاریخها، و یک امضای دیجیتال کوچک را از تراشه میخواند — مدرکی از سازمان ثبت احوال ایران که سند واقعی است.
سپس تلفن کاری هوشمندانه میکند: یک رسید ریاضی میسازد که میگوید «من یک گذرنامهی واقعی ایرانی دارم، بالای ۱۸ سال هستم، اسم و شمارهام اینجا ظاهر نمیشوند.» این رسید تقریباً به اندازهی یک توییت است. دادهی گذرنامهی شما سپس از تلفن پاک میشود.
رسید روی بلاکچین آپلود میشود — یک دفتر کل عمومی و مقاوم در برابر دستکاری که در دهها کشور اجرا میشود. بلاکچین ریاضی را بررسی میکند، رسید را میپذیرد، و حالا شما ثبتنام شدهاید: یک شهروند واقعی ایرانی، واجد شرایط رای، بدون نامی متصل.
وقتی یک رایگیری انجام میشود، یک رسید کوچکتر تولید میکنید: «من یک کاربر ثبتنامشده هستم، رای من بله است.» بلاکچین آن را میشمارد، شما را بهعنوان رایداده روی این پرسش علامت میزند، و نمیتوانید روی همان پرسش دو بار رای دهید.
هیچکس نام شما را نمیداند. هیچکس نمیتواند حساب شما را بگیرد. هیچکس نمیتواند رای شما را حذف کند.
آن پاراگراف تنها نسخهای از سیستم است که اکثر خوانندگان نیاز خواهند داشت. هر چیزی در این پوشه وجود دارد تا درستبودن آن را پشتیبانی کند.