برنامه نویسی

قراردادهای هوشمند خصوصی با Solidity و Circom

ZK به ما اجازه می دهد تا برنامه هایی با داده و اجرای خصوصی ایجاد کنیم. این در را به روی بسیاری از موارد استفاده جدید باز می کند، مانند موردی که در این راهنما ایجاد خواهیم کرد: یک سیستم رای گیری ناشناس و ایمن که Circom و Solidity را ترکیب می کند.

Circom و وابستگی ها

اگر هنوز سیرکام را ندارید با دستورات زیر آن را نصب کنید. من از node v20 استفاده می کنم اما باید با نسخه های دیگر کار کند.

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom
npm install -g snarkjs
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همچنین از کتابخانه‌های circom استفاده می‌کنیم که تابع poseidon که قرار است استفاده کنیم در آن قرار دارد.

git clone https://github.com/iden3/circomlib.git
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

1. ایجاد کلیدهای عمومی

روشی که ما برای رأی گیری ناشناس و ایمن استفاده خواهیم کرد این است که تأیید کنیم که بخشی از یک گروه هستیم بدون اینکه هویت خود را فاش کنیم. به عنوان مثال، من قصد دارم به رئیس جمهور هندوراس رای بدهم تا نشان دهم هندوراسی هستم اما بدون اینکه نشان دهم کدام هندوراسی هستم. ما به این می گوییم “آزمون گنجاندن در یک مجموعه”.

عملی ترین راه برای انجام این کار در zk و بلاک چین از طریق درختان مرکل است. ما رأی دهندگان را به صورت برگ روی درخت قرار می دهیم و می خواهیم نشان دهیم که یکی از آنها هستیم بدون اینکه کدام را فاش کنیم.

درخت عمومی است بنابراین ما از مجموعه ای از کلیدهای عمومی-خصوصی استفاده خواهیم کرد تا هر رأی دهنده بتواند فقط یک بار رأی خود را اجرا کند.

ممکن است از خود بپرسید که آیا می توانیم از کلیدهای عمومی کیف پول اتریوم خود استفاده کنیم (مثلاً متاماسک). در راهنماهای آینده مانند این، همان طور که با نوآر انجام دادم، به آن موضوع خواهم پرداخت. برای رسیدن به آن نقطه به اصول اولیه در این راهنما نیاز دارید. پس با ما همراه باشید و مشترک شوید!

حال بیایید کلیدهای عمومی کلیدهای خصوصی زیر را از طریق مدار ایجاد کنیم privateKeyHasher.circom بعد:

privateKeyHasher.circom

pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template privateKeyHasher() {
    signal input privateKey;
    signal output publicKey;
    component poseidonComponent;
    poseidonComponent = Poseidon(1);
    poseidonComponent.inputs[0] <== privateKey;
    publicKey <== poseidonComponent.out;
    log(publicKey);
}

component main = privateKeyHasher();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

input.json

{
    "privateKey": "111"
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما مدار را با دستورات زیر کامپایل و محاسبه می کنیم، شما می توانید نتیجه را در ترمینال ببینید.

circom privateKeyHasher.circom --r1cs --wasm --sym --c
node privateKeyHasher_js/generate_witness.js privateKeyHasher_js/privateKeyHasher.wasm input.json witness.wtns
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نتیجه 4 کلید خصوصی باید به صورت زیر باشد:

کلید خصوصی کلید عمومی
111 13377623690824916797327209540443066247715962236839283896963055328700043345550
222 3370092681714607727019534888747304108045661953819543369463810453568040251648
333 19430878135540641438890585969007029177622584902384053006985767702837167003933
444 2288143249026782941992289125994734798520452235369536663078162770881373549221

آیا انجام این کار از طریق circom ضروری است؟ پاسخ منفی است. با انجام آن در circom، ما محاسبات غیر ضروری زیادی را انجام می دهیم، در حال حاضر این کار را به این روش انجام می دهیم تا اطمینان حاصل کنیم که اجرای الگوریتم هش poseidon که بعداً از آن استفاده خواهیم کرد، سازگار است. این برای پروژه های در حال تولید توصیه نمی شود.

2. ایجاد درخت

اکنون چهار برگ درخت خود را به صورت زیر قرار داده ایم

└─ ???
   ├─ ???
   │  ├─ 13377623690824916797327209540443066247715962236839283896963055328700043345550
   │  └─ 3370092681714607727019534888747304108045661953819543369463810453568040251648
   └─ ???
      ├─ 19430878135540641438890585969007029177622584902384053006985767702837167003933
      └─ 2288143249026782941992289125994734798520452235369536663078162770881373549221
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در ادامه می خواهیم شاخه به شاخه درخت مرکل را تولید کنیم. بیایید به یاد داشته باشیم که درختان مرکل از هش کردن هر یک از برگ ها و شاخه های آن به صورت جفت تا زمانی که به ریشه برسیم تولید می شوند.

برای تولید درخت کامل، تابع زیر را اجرا می کنیم که دو برگ را هش می کند تا ریشه آن تولید شود. ما این کار را در مجموع 3 بار انجام می دهیم زیرا برای به دست آوردن ریشه یک درخت با 4 برگ لازم است: raíz = hash(hash(A, B), hash(C, D)).

hashLeaves.circom

pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template hashLeaves() {
    signal input leftLeaf;
    signal input rightLeaf;
    signal output root;
    component poseidonComponent;
    poseidonComponent = Poseidon(2);
    poseidonComponent.inputs[0] <== leftLeaf;
    poseidonComponent.inputs[1] <== rightLeaf;
    root <== poseidonComponent.out;
    log(root);
}

component main = hashLeaves();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اینها ورودی های لازم برای تولید شاخه اول هستند. به روشی مشابه می توانید شاخه و ریشه دیگر را تولید کنید.

input.json

{
    "leftLeaf": "13377623690824916797327209540443066247715962236839283896963055328700043345550",
    "rightLeaf": "3370092681714607727019534888747304108045661953819543369463810453568040251648"
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانند مرحله قبل، با دستورات زیر مدار کامپایل شده و ریشه دو برگ آن چاپ می شود.

circom hashLeaves.circom --r1cs --wasm --sym --c
node hashLeaves_js/generate_witness.js hashLeaves_js/hashLeaves.wasm input.json witness.wtns
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

درخت کامل ما به این صورت است:

└─ 172702405816516791996779728912308790882282610188111072512380034048458433129
   ├─ 8238706810845716733547504554580992539732197518335350130391048624023669338026
   │  ├─ 13377623690824916797327209540443066247715962236839283896963055328700043345550
   │  └─ 3370092681714607727019534888747304108045661953819543369463810453568040251648
   └─ 11117482755699627218224304590393929490559713427701237904426421590969988571596
      ├─ 19430878135540641438890585969007029177622584902384053006985767702837167003933
      └─ 2288143249026782941992289125994734798520452235369536663078162770881373549221
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

3. اثبات رای ناشناس ایجاد کنید

برای ایجاد رای باید پارامترهای زیر را به مدار منتقل کنیم:

  • privateKey: کلید خصوصی کاربر.
  • root: ریشه درخت به ما اطمینان می دهد که در مجموعه درست عمل می کنیم. علاوه بر این، برای وضوح بیشتر، می توانیم قرارداد و زنجیره ای را که در آن رأی اجرا می شود، اضافه کنیم. این متغیر عمومی و قابل دسترسی برای قرارداد هوشمند خواهد بود.
  • proposalId y vote: رای انتخاب شده توسط کاربر.
  • pathElements y pathIndicies: حداقل اطلاعات مورد نیاز برای بازسازی ریشه، این شامل pathElements، یعنی گره های برگ یا شاخه و pathIndices، که به ما نشان می دهد کدام مسیر را برای هش انتخاب کنیم که 0 نماد گره های سمت چپ و 1 گره های سمت راست است.

proveVote.circom

pragma circom 2.0.0;

include "circomlib/circuits/poseidon.circom";

template switchPosition() {
    signal input in[2];
    signal input s;
    signal output out[2];

    s * (1 - s) === 0;
    out[0] <== (in[1] - in[0])*s + in[0];
    out[1] <== (in[0] - in[1])*s + in[1];
}

template privateKeyHasher() {
    signal input privateKey;
    signal output publicKey;
    component poseidonComponent;
    poseidonComponent = Poseidon(1);
    poseidonComponent.inputs[0] <== privateKey;
    publicKey <== poseidonComponent.out;
}

template nullifierHasher() {
    signal input root;
    signal input privateKey;
    signal input proposalId;
    signal output nullifier;
    component poseidonComponent;
    poseidonComponent = Poseidon(3);
    poseidonComponent.inputs[0] <== root;
    poseidonComponent.inputs[1] <== privateKey;
    poseidonComponent.inputs[2] <== proposalId;
    nullifier <== poseidonComponent.out;
}

template proveVote(levels) {
    signal input privateKey;
    signal input root;
    signal input proposalId;
    signal input vote;
    signal input pathElements[levels];
    signal input pathIndices[levels];
    signal output nullifier;

    signal leaf;
    component hasherComponent;
    hasherComponent = privateKeyHasher();
    hasherComponent.privateKey <== privateKey;
    leaf <== hasherComponent.publicKey;

    component selectors[levels];
    component hashers[levels];

    signal computedPath[levels];

    for (var i = 0; i < levels; i++) {
        selectors[i] = switchPosition();
        selectors[i].in[0] <== i == 0 ? leaf : computedPath[i - 1];
        selectors[i].in[1] <== pathElements[i];
        selectors[i].s <== pathIndices[i];

        hashers[i] = Poseidon(2);
        hashers[i].inputs[0] <== selectors[i].out[0];
        hashers[i].inputs[1] <== selectors[i].out[1];
        computedPath[i] <== hashers[i].out;
    }
    root === computedPath[levels - 1];

    component nullifierComponent;
    nullifierComponent = nullifierHasher();
    nullifierComponent.root <== root;
    nullifierComponent.privateKey <== privateKey;
    nullifierComponent.proposalId <== proposalId;
    nullifier <== nullifierComponent.nullifier;
}

component main {public [root, proposalId, vote]} = proveVote(2);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

input.json

{
    "privateKey": "111",
    "root": "172702405816516791996779728912308790882282610188111072512380034048458433129",
    "proposalId": "0",
    "vote": "1",
    "pathElements": ["3370092681714607727019534888747304108045661953819543369463810453568040251648", "11117482755699627218224304590393929490559713427701237904426421590969988571596"],
    "pathIndices": ["0","0"]
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما آزمایش می کنیم که آیا همه چیز خوب کار می کند:

circom proveVote.circom --r1cs --wasm --sym --c
node proveVote_js/generate_witness.js proveVote_js/proveVote.wasm input.json witness.wtns
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر مشکلی وجود نداشت نباید چیزی در ترمینال چاپ شود.

4. رای زنجیره ای را از Solidity تأیید کنید

با دستورات زیر مراسم اولیه را که به نام the نیز شناخته می شود انجام می دهیم راه اندازی قابل اعتماد.

snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup proveVote.r1cs pot12_final.ptau proveVote_0000.zkey
snarkjs zkey contribute proveVote_0000.zkey proveVote_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey proveVote_0001.zkey verification_key.json
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مرحله بعد، قرارداد تایید کننده را به صورت جامد تولید می کنیم.

snarkjs zkey export solidityverifier proveVote_0001.zkey verifier.sol
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با اجرای این دستور یک قرارداد تایید کننده در فایل ایجاد می شود verifier.sol. اکنون آن قرارداد را روی زنجیره راه اندازی کنید.

سپس قرارداد زنجیره ای زیر را راه اندازی می کند که حاوی منطق رای گیری و تأیید اثبات است. آدرس قرارداد تأیید کننده ای را که به عنوان پارامتر در سازنده راه اندازی کردیم، به آن منتقل کنید.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

interface ICircomVerifier {
    function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) external view returns (bool);
}

contract CircomVoter {
    ICircomVerifier circomVerifier;
    uint public publicInput;

    struct Proposal {
        string description;
        uint deadline;
        uint forVotes;
        uint againstVotes;
    }

    uint merkleRoot;
    uint proposalCount;
    mapping (uint proposalId => Proposal) public proposals;
    mapping (uint nullifier => bool isNullified) public nullifiers;

    constructor(uint _merkleRoot, address circomVeriferAddress) {
        merkleRoot = _merkleRoot;
        circomVerifier = ICircomVerifier(circomVeriferAddress);
    }

    function propose(string memory description, uint deadline) public {
        proposals[proposalCount] = Proposal(description, deadline, 0, 0);
        proposalCount += 1;
    }

    function castVote(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) public {
        circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals);
        uint nullifier = _pubSignals[0];
        uint merkleRootPublicInput = _pubSignals[1];
        uint proposalId = uint(_pubSignals[2]);
        uint vote = uint(_pubSignals[3]);

        require(block.timestamp < proposals[proposalId].deadline, "Voting period is over");
        require(merkleRoot == merkleRootPublicInput, "Invalid merke root");
        require(!nullifiers[nullifier], "Vote already casted");

        nullifiers[nullifier] = true;

        if(vote == 1)
            proposals[proposalId].forVotes += 1;
        else if (vote == 2)
            proposals[proposalId].againstVotes += 1;
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون با فراخوانی تابع، اولین پیشنهاد برای رای گیری را ایجاد کنید propose(). برای مثال می‌توانید رأی دادن را با آن انجام دهید ¿Comemos pizza? به عنوان توضیحات و با 1811799232 به عنوان مهلتی برای انقضای آن در سال 2027.

اکنون یک تست با فرمت لازم برای تایید آن در Remix ایجاد می کنیم.

snarkjs groth16 prove proveVote_0001.zkey witness.wtns proof.json public.json
snarkjs groth16 verify verification_key.json public.json proof.json
snarkjs generatecall
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید نتیجه ترمینال را به عنوان پارامتر در ریمیکس پاس کنیم و با بدست آوردن داده های پروپوزال 0 از طریق نگاشت خواهیم دید که چگونه رای اجرا شده است. proposals.

آرای ناشناس با Circom و Solidity

متذکر می شویم که رای ما بدون فاش کردن اینکه فرستنده کیست شمارش شد. سعی کنید دوباره همان رای را بدهید، خواهید دید که امکان پذیر نیست، معامله معکوس می شود. این به این دلیل است که ما قبلاً رأی را باطل کرده ایم تا هر رأی دهنده فقط بتواند یک رأی بدهد.

منابع و اسناد رسمی:

با تشکر از خواندن این راهنما!

من را در dev.to و YouTube برای همه چیزهایی که به توسعه بلاک چین به زبان اسپانیایی مرتبط است دنبال کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا