قراردادهای هوشمند خصوصی با 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
yvote
: رای انتخاب شده توسط کاربر. -
pathElements
ypathIndicies
: حداقل اطلاعات مورد نیاز برای بازسازی ریشه، این شامل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
.
متذکر می شویم که رای ما بدون فاش کردن اینکه فرستنده کیست شمارش شد. سعی کنید دوباره همان رای را بدهید، خواهید دید که امکان پذیر نیست، معامله معکوس می شود. این به این دلیل است که ما قبلاً رأی را باطل کرده ایم تا هر رأی دهنده فقط بتواند یک رأی بدهد.
منابع و اسناد رسمی:
با تشکر از خواندن این راهنما!
من را در dev.to و YouTube برای همه چیزهایی که به توسعه بلاک چین به زبان اسپانیایی مرتبط است دنبال کنید.