本文详细介绍了如何使用Noir构建一个去中心化的投票系统,涵盖了零知识证明的关键组成部分和使用库的示例代码。文章结构清晰,包括技术实现的步骤、加密原语的使用、以及对数据源接口(预言机)的介绍,展示了如何保证投票的匿名性和安全性。通过详细的代码示例和分步讲解,本教程适合希望深入掌握区块链隐私技术的开发者。
在本指南中,我们将一步步开发一个去中心化投票系统,使用 Noir,这是一种为构建保护隐私的零知识(ZK)程序而设计的领域特定语言(DSL)。在此过程中,我们将解释一些关键组成部分,例如 Noir 标准库及其预言机集成。
通过遵循本技术指南,你将学习如何开发零知识电路,构建使用 Noir 的去中心化投票系统。
Noir 的加密原语库提供了安全应用程序开发的基本工具,包括密码、哈希函数、椭圆曲线操作、数字签名。
此外,它还包含诸如 BigInt、黑盒函数、BN254、容器、is_unconstrained、Merkle 树、选项、Zeroed 等实用程序。
// 使用哈希函数的示例
let data = "Noir data";
let hash_value = sha256(data);
assert(hash_value == expected_hash, "哈希值应匹配");
// 执行椭圆曲线操作的示例
let g = ec::G1::generator();
let private_key = 12345u64;
let public_key = g * private_key; // 曲线上的标量乘法
// 验证 Schnorr 签名的示例
let message = "为此消息签名";
let signature = schnorr::sign(private_key, message);
let is_valid = schnorr::verify(public_key, message, signature);
assert(is_valid, "Schnorr 签名验证失败");
// 验证 ECDSA 签名的示例
let ecdsa_signature = ecdsa::sign(private_key, message);
let ecdsa_is_valid = ecdsa::verify(public_key, message, ecdsa_signature);
assert(ecdsa_is_valid, "ECDSA 签名验证失败");
// 验证 EdDSA 签名的示例
let eddsa_signature = eddsa::sign(private_key, message);
let eddsa_is_valid = eddsa::verify(public_key, message, eddsa_signature);
assert(eddsa_is_valid, "EdDSA 签名验证失败");
// BigInt 示例
let big_number = BigInt::from(12345678901234567890u64);
// 黑盒函数的示例
let commitment = black_box::commit("秘密数据");
assert(verify_commitment(commitment, "秘密数据"), "承诺验证失败");
// BN254 示例
let point = bn254::G1::generator();
let scalar = 12345u64;
let result_point = point * scalar;
// 容器的示例
let mut vec = Vec::new(); // Vec
let mut bounded_vec = BoundedVec::<u32, 3>::new(); // BoundedVec
let mut map = HashMap::new(); // HashMap
// is_Unconstrained 示例
let x = unconstrained();
assert(is_unconstrained(x), "x 应该是不受限制的");
// Merkle 树的示例
let leaves = vec![hash1, hash2, hash3, hash4];
let tree = MerkleTree::new(leaves);
let proof = tree.generate_proof(0); // 生成第一个叶子的证明
assert(tree.verify_proof(proof, hash1), "Merkle 证明验证失败");
// 选项的示例
let maybe_value: Option<u32> = Some(42u32);
match maybe_value {
Some(value) => log("值存在: {}", value),
None => log("没有值存在"),
}
// Zeroed 示例
let mut counter = zeroed::<u32>(); // 将计数器初始化为 0
counter = counter + 1;
要了解有关 Noir 标准库的更多信息,请查看 Noir 文档
在 Noir 中,预言机是允许零知识(ZK)电路安全与外部数据源交互的关键组件。通过预言机,能够将现实世界的数据纳入 ZK 证明,同时不损害证明系统的隐私和完整性。此能力对许多区块链应用程序至关重要,在这些应用中,外部数据(例如资产价格、天气信息或任何链下数据)需要纳入链上操作。
Noir 中的预言机本质上是外部世界与 ZK 电路之间的桥梁。它从外部源获取数据,将这些数据输入到电路中,并确保用于证明的数据既有效又真实。使用预言机通常涉及以下步骤:
预言机尤其强大,因为它们允许智能合约和 ZK 电路保持确定性,同时仍使用可能随着时间变化的外部数据。
如果你想了解有关 Noir 中预言机的更多信息,可以查看 官方文档.
在本指南中,我们将探讨开发一个去中心化投票系统,该系统通过先进的加密技术确保隐私和安全性。该系统利用零知识电路来处理和验证投票,确保每个投票既独特又匿名,同时保持整个投票过程的完整性。
我们将首先设置项目环境,安装必要的工具,然后继续开发能够验证投票并计算必要加密证明的电路。接下来,我们将创建一个 Solidity 智能合约,以管理区块链上的投票过程,包括提案创建、投票和结果统计。该合约将与我们的 Noir 电路生成的零知识证明无缝互动,确保系统既健壮又安全。
无论你是经验丰富的区块链开发人员还是零知识证明的新手,本教程将为你提供使用 Noir 和 Solidity 构建强大、注重隐私的应用程序所需的知识和工具。让我们开始吧!
在你的终端中运行以下脚本:
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
注意: Noir 的默认后端(Barretenberg)目前不提供 Windows 二进制文件。因此,目前无法本机安装 Noir。然而,可以通过使用 Windows 子系统 Linux (WSL) 来实现。如果你是 Windows 用户,请参阅此 指南。
随意使用任何版本,只需牢记 Nargo 和 NoirJS 包应保持同步。例如,Nargo 0.31.x 与
noir_js@0.31.x
匹配,等。在本指南中,我们将锁定版本为 0.31.0。
通过运行以下命令创建一个新的 Nargo 项目:
nargo new circuits
注意:你可以根据需要为项目命名,但在生产中,通常做法是将项目文件夹命名为 circuits,以便在代码库中与其他文件夹(如:contracts、scripts 和 test)之间保持清晰。
我们将开发一个处理投票承诺、验证投票并计算消除器的电路,以确保投票的隐私和完整性。该电路还将包括一个测试,以通过模拟有效场景来验证其正确性。
main()
函数main()
函数是我们电路的核心。它处理投票承诺,验证投票的完整性,并计算一个消除器,以确保每个投票都是唯一和匿名的。该函数利用加密技术,如 Pedersen 哈希和 Merkle 树验证,以保持投票过程的安全性和隐私。
fn main(
root: pub Field,
index: Field,
hash_path: [Field; 2],
secret: Field,
proposalId: pub Field,
vote_commitment: pub Field,
vote_opening: Field,
salt: Field
) -> pub Field {
该函数接受多个参数:
root: pub Field
:这是 Merkle 树的公共根。Merkle 树是一种数据结构,可以有效且安全地验证大数据集内容。在此上下文中,它用于验证投票是否属于有效的已注册投票树,从而确保整个投票过程的完整性。index: Field
:在 Merkle 树中存储笔记承诺(投票的承诺)的索引。它帮助定位特定的投票,在验证投票是已认可和注册的投票时至关重要。hash_path: [Field; 2]
:Merkle 证明,这是一个哈希数组,用于从笔记承诺重新计算 Merkle 树的根。该证明用于验证从叶子(投票承诺)到根的路径,确保投票没有被篡改。secret: Field
:表示选民秘密的私有字段。该秘密与选民选择结合使用,以创建一个使用 Pedersen 哈希的独特且安全的承诺。这防止任何人在不暴露实际投票的情况下确定实际投票,同时允许系统验证承诺的完整性。proposalId: pub Field
:表示所投票提案的 ID 的公共字段。它确保投票与正确的提案关联,防止投票混淆或错误归属。vote_commitment: pub Field
:投票的承诺,这是一个公共哈希,表示选民的选择和秘密。此承诺确保投票保持隐秘,但仍可验证为真实。vote_opening: Field
:选民做出的实际投票(例如,0 或 1)。它用来验证承诺,以确保投票与选民承诺的一致,而无需揭示投票内容。salt: Field
:用于生成消除器的随机字段,提供额外隐私,防止将投票与特定选民联系在一起,即使他们在多次投票时使用相同的秘密和提案 ID。该函数首先使用 vote_opening
和 secret
字段计算新的承诺(computed_vote_commitment
)。然后将此承诺与提供的 vote_commitment
进行比较,以确保投票没有被更改,并且打开(实际投票)与选民所作的承诺相对应。
let computed_vote_commitment = std::hash::pedersen_hash([vote_opening, secret]);
assert(vote_commitment == computed_vote_commitment);
我们为什么使用 Pedersen 哈希:Pedersen 哈希在这里使用是因为它具有抗碰撞性和同态性。抗碰撞性确保两个不同输入几乎不可能产生相同的哈希,这对投票的安全性至关重要。同态性特性允许在加密数据上执行某些操作(如求和承诺)而无需解密,保持选民选择的隐私。
我们为什么使用 assert
:assert
语句对强制计算的承诺与提供的承诺匹配至关重要。这确保投票没有被篡改,并且承诺准确反映选民的原始选择。
接下来,函数确保 vote_opening
在有效范围内(例如,对于二元选择为 0 或 1)。此步骤对于防止无效票数(例如,在是/否场景中投票为 2)至关重要。
let vote_opening_int = vote_opening as i32;
assert(vote_opening_int >= 0);
assert(vote_opening_int <= 1);
该函数然后使用 root
、secret
、proposalId
和 salt
字段计算消除器。消除器是一个独特的值,可以防止将特定投票与选民链接,同时仍然允许投票被计算。这对维护选民隐私非常重要,同时确保每个投票是唯一的且不能被复制。
let nullifier = std::hash::pedersen_hash([root, secret, proposalId, salt]);
我们为什么使用 Pedersen 哈希:再次,Pedersen 哈希因其加密属性而被使用,确保消除器是唯一和安全的。使用 salt
增加了一些随机性,这对防止同一选民在不同投票之间建立联系至关重要。
该函数从 secret
生成一个 note_commitment
,然后使用该承诺连同 index
和 hash_path
重新计算 Merkle 根(check_root
)。它断言此计算的根与提供的 root
相匹配,以验证 Merkle 树的完整性。
let note_commitment = std::hash::pedersen_hash([secret]);
let check_root = std::merkle::compute_merkle_root(note_commitment, index, hash_path);
assert(root == check_root);
我们为什么要计算 Merkle 根:Merkle 根的计算至关重要,因为它确保投票被包括在有效和经过认证的投票集中。这有助于验证没有未授权投票被插入到投票过程中,从而维护投票系统的完整性。
最后,函数返回计算出的 nullifier
,这是维持选民匿名性的关键组件,同时确保投票是唯一且已计算。
nullifier
}
如果每一步都正确,你的 main.nr
文件应如下所示:
fn main(
root: pub Field,
index: Field,
hash_path: [Field; 2],
secret: Field,
proposalId: pub Field,
vote_commitment: pub Field,
vote_opening: Field,
salt: Field
) -> pub Field {
let computed_vote_commitment = std::hash::pedersen_hash([vote_opening, secret]);
assert(vote_commitment == computed_vote_commitment);
let vote_opening_int = vote_opening as i32;
assert(vote_opening_int >= 0);
assert(vote_opening_int <= 1);
let nullifier = std::hash::pedersen_hash([root, secret, proposalId, salt]);
let note_commitment = std::hash::pedersen_hash([secret]);
let check_root = std::merkle::compute_merkle_root(note_commitment, index, hash_path);
assert(root == check_root);
nullifier
}
进入 circuits
目录并通过运行以下命令为你的 Noir 程序构建输入/输出文件:
cd circuits
nargo check
将在你的项目目录中生成 Prover.toml
文件,以便指定程序的输入值。
现在项目已设置好,我们可以执行我们的 Noir 程序。
在 Prover.toml 文件中填写执行的输入值:
hash_path = [\
"0x1efa9d6bb4dfdf86063cc77efdec90eb9262079230f1898049efad264835b6c8",\
"0x2a653551d87767c545a2a11b29f0581a392b4e177a87c8e3eb425c51a26a8c77"\
]
index = "0"
proposalId = "0"
root = "0x215597bacd9c7e977dfc170f320074155de974be494579d2586e5b268fa3b629"
salt = "0xabc123"
secret = "1"
vote_commitment = "0x07ebfbf4df29888c6cd6dca13d4bb9d1a923013ddbbcbdc3378ab8845463297b"
vote_opening = "1"
以下是每个输入参数的解释:
hash_path
:用于通过从叶子(投票承诺)重新计算 Merkle 根以验证特定投票承诺是 Merkle 树的一部分的哈希数组。index
:指定投票承诺在 Merkle 树中的位置。在此情况下,0
表示这是第一个位置。proposalId
:所投票提案的唯一标识符。在这里,0
表示第一个或主要提案。root
:表示所有票数承诺的合并代表证。这对于验证投票过程的完整性至关重要。salt
:用于创建 nullifier
的随机值,确保投票是匿名的且不可链接,即使在多次使用相同秘密的情况下。secret
:选民仅知道的私有值,用于与 vote_opening
一起生成安全的 vote_commitment
,确保唯一性和安全性。vote_commitment
:表示选民的选择和秘密的加密哈希,用于验证投票是真实的而不揭示实际选择。vote_opening
:实际投票(例如,0
表示“否”或 1
表示“是”),与 secret
一起用于生成 vote_commitment
并验证投票的一致性。现在让我们执行我们的 Noir 程序:
nargo execute circuits
每个证明后端为处理 Noir 程序提供自己的工具,例如生成、验证证明以及创建验证智能合约。
你可以在 Awesome Noir 中找到兼容的证明后端的完整列表。
为了我们的目的,我们将安装 bb
,这是来自 Barretenberg 证明后端 的 CLI 工具,由 Aztec Labs 提供。
Barretenberg 是一个用于创建和验证证明的库。你可以使用 Barretenberg 标准库或名为 ACIR 的 IR 定义用于证明的电路。
该工具将使用 ACIR 和见证值作为输入生成证明。
在你的终端中运行以下脚本:
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/master/barretenberg/cpp/installation/install | bash
安装与你的 Noir 版本兼容的 bb;因为我们使用 Noir v0.31.0,请运行:
bbup -v 0.41.0
注意:目前该二进制文件下载了可用于证明最大电路大小的 SRS。在撰写时,该最大电路大小参数在代码中已设置为 2^{23}。由于 WASM 限制,因此该最大电路大小与可以在浏览器中证明的最大电路大小不同。
使用 Barretenberg,我们将在运行以下命令的情况下证明我们 Noir 程序的有效执行:
bb prove -b ./target/circuits.json -w ./target/circuits.gz -o ./target/proof
生成的证明将写入 ./target/proof。
一旦生成了证明,我们可以通过验证证明文件来验证我们的 Noir 程序的正确执行。
通过运行以下命令来计算 Noir 程序的验证密钥:
bb write_vk -b ./target/circuits.json -o ./target/vk
然后通过运行以下命令来验证你的证明:
bb verify -k ./target/vk -p ./target/proof
注意: 如果成功,验证将默默完成;如果失败,该命令将记录相应的错误。
我们将开发管理去中心化投票系统的 Solidity 智能合约。Voting
合约是我们系统的核心。它处理提案创建、投票以及计票,同时确保投票过程的安全性和完整性。该合约利用加密技术和 Merkle 树结构来维护选民隐私并防止欺诈。
构造函数使用 Merkle 根和验证合约初始化合约:
constructor(bytes32 _merkleRoot, address _verifier) {
merkleRoot = _merkleRoot;
verifier = UltraVerifier(_verifier);
}
merkleRoot
:表示 Merkle 树的根,确保仅纳入授权票数。verifier
:UltraVerifier
合约的实例,对于与每笔投票相关的零知识证明进行验证。propose()
函数此函数允许用户创建具有描述和投票截止日期的新提案:
function propose(
string memory description,
uint256 deadline
) public returns (uint256) {
proposals[proposalCount] = Proposal(description, deadline, 0, 0);
proposalCount += 1;
return proposalCount;
}
description
:提案的简要描述。deadline
:投票提案的截止时间。castVote()
函数castVote()
函数是合约的最关键部分,处理投票和验证投票的过程:
function castVote(
bytes32[] memory proof,
uint256 proposalId,
uint256 vote,
bytes32 nullifierHash
) public returns (bool) {
require(!nullifiers[nullifierHash], "证明已提交");
require(
block.timestamp < proposals[proposalId].deadline,
"投票时间已结束。"
);
nullifiers[nullifierHash] = true;
bytes32 leaf = keccak256(
bytes.concat(
keccak256(
abi.encode(
merkleRoot,
bytes32(proposalId),
bytes32(vote),
nullifierHash
)
)
)
);
require(MerkleProof.verify(proof, merkleRoot, leaf), "无效证明");
if (vote == 1) {
proposals[proposalId].forVotes += 1;
} else {
proposals[proposalId].againstVotes += 1;
}
return true;
}
nullifierHash
已经投票,从而防止重复投票。1
表示“赞成”,0
表示“反对”),合约更新提案的投票计数。castVote()
函数的第二前图攻击为了防止第二前图攻击,合约使用特定的叶子编码方法:
bytes32 leaf = keccak256(
bytes.concat(
keccak256(
abi.encode(
merkleRoot,
bytes32(proposalId),
bytes32(vote),
nullifierHash
)
)
)
);
叶子哈希编码:通过将 Merkle 根、提案 ID、投票和消除器哈希一起编码,此方法确保 Merkle 树中的每个叶子都是唯一和安全的。这样可以降低攻击者找到两个输入以相同值哈希的风险,从而保持投票过程的完整性。
以下是完整合约:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
import {UltraVerifier} from "../../circuits/contract/plonk_vk.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract Voting {
UltraVerifier verifier;
struct Proposal {
string description;
uint256 deadline;
uint256 forVotes;
uint256 againstVotes;
}
bytes32 merkleRoot;
uint256 proposalCount;
mapping(uint256 proposalId => Proposal) public proposals;
mapping(bytes32 hash => bool isNullified) nullifiers;
constructor(bytes32 _merkleRoot, address _verifier) {
merkleRoot = _merkleRoot;
verifier = UltraVerifier(_verifier);
}
function propose(
string memory description,
uint256 deadline
) public returns (uint256) {
proposals[proposalCount] = Proposal(description, deadline, 0, 0);
proposalCount += 1;
return proposalCount;
}
/// @param vote - 必须为 "1" 才能算作赞成票
function castVote(
bytes32[] memory proof,
uint256 proposalId,
uint256 vote,
bytes32 nullifierHash
) public returns (bool) {
require(!nullifiers[nullifierHash], "证明已提交");
require(
block.timestamp < proposals[proposalId].deadline,
"投票时间已结束。"
);
nullifiers[nullifierHash] = true;
bytes32 leaf = keccak256(
bytes.concat(
keccak256(
abi.encode(
merkleRoot,
bytes32(proposalId),
bytes32(vote),
nullifierHash
)
)
)
);
require(MerkleProof.verify(proof, merkleRoot, leaf), "无效证明");
if (vote == 1) {
proposals[proposalId].forVotes += 1;
}
// 投票 = 0
else {
proposals[proposalId].againstVotes += 1;
}
return true;
}
}
如果你想添加完整系统,可以包含一些 Foundry 测试。要做到这一点,你应安装 Foundry,按照 此指南 操作。
以下是 Foundry 测试:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.21;
import "../lib/forge-std/src/Test.sol";
import "../src/Voting.sol";
import "../../circuits/contract/plonk_vk.sol";
contract VotingTest is Test {
Voting public voteContract;
UltraVerifier public verifier;
bytes32[] proof;
bytes32[] emptyProof;
uint256 deadline = block.timestamp + 10000000;
bytes32 merkleRoot;
bytes32 nullifierHash;
function readInputs() internal view returns (string memory) {
string memory inputDir = string.concat(vm.projectRoot(), "/data/input");
return vm.readFile(string.concat(inputDir, ".json"));
}
function setUp() public {
string memory inputs = readInputs();
merkleRoot = bytes32(vm.parseJson(inputs, ".merkleRoot"));
nullifierHash = bytes32(vm.parseJson(inputs, ".nullifierHash));
verifier = new UltraVerifier();
voteContract = new Voting(merkleRoot, address(verifier));
voteContract.propose("第一个提案", deadline);
string memory proofFilePath = "./circuits/proofs/foundry_voting.proof";
string memory proofData = vm.readLine(proofFilePath);
proof = abi.decode(vm.parseBytes(proofData), (bytes32[]));
// emptyProof = abi.decode(vm.parseBytes(proofData), (bytes32[]));
}
function test_validVote() public {
voteContract.castVote(proof, 0, 1, nullifierHash);
}
function test_invalidProof() public {
vm.expectRevert();
voteContract.castVote(emptyProof, 0, 1, nullifierHash); // 传递空证明
}
function test_doubleVoting() public {
voteContract.castVote(proof, 0, 1, nullifierHash);
vm.expectRevert("证明已提交");
voteContract.castVote(proof, 0, 1, nullifierHash);
}
function test_changedVote() public {
vm.expectRevert();
voteContract.castVote(proof, 0, 0, nullifierHash); // 尝试对已投赞成的提案进行反对投票
}
}
现在轮到你扩展电路,添加更多测试,并继续学习了!
作为参考,你可以查看 GitHub repo 以获取完整代码。
总之,Noir 提供了一个简化且易于访问的零知识证明开发方法,使开发人员能够构建保护隐私的应用程序,而无需深入的加密知识。通过创建一个去中心化的投票系统,我们展示了 Noir 如何简化与复杂的加密技术的集成,使实施安全和匿名的链上解决方案变得更容易。
这就是我们 Noir 语言深度探讨的第二部分的结束!
请联系 Three Sigma,让我们的专业团队为你在 Web3 领域提供信心满满的指导。依托我们在智能合约安全、经济建模和区块链工程方面的专业知识,我们将帮助你确保项目的未来。
今天就 联系我们,将你的 Web3 设想变为现实!
参考文献:Noir 文档、Awesome Noir Repo
- 原文链接: threesigma.xyz/blog/buil...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!