构建一个去中心化投票应用程序 w/ Noir

本文详细介绍了如何使用Noir构建一个去中心化的投票系统,涵盖了零知识证明的关键组成部分和使用库的示例代码。文章结构清晰,包括技术实现的步骤、加密原语的使用、以及对数据源接口(预言机)的介绍,展示了如何保证投票的匿名性和安全性。通过详细的代码示例和分步讲解,本教程适合希望深入掌握区块链隐私技术的开发者。

"构建去中心化投票应用程序 w/ Noir - Three Sigma" 横幅

1. 引言

在本指南中,我们将一步步开发一个去中心化投票系统,使用 Noir,这是一种为构建保护隐私的零知识(ZK)程序而设计的领域特定语言(DSL)。在此过程中,我们将解释一些关键组成部分,例如 Noir 标准库及其预言机集成。

通过遵循本技术指南,你将学习如何开发零知识电路,构建使用 Noir 的去中心化投票系统。

2. 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 文档

3. Noir 中的预言机

预言机简介

在 Noir 中,预言机是允许零知识(ZK)电路安全与外部数据源交互的关键组件。通过预言机,能够将现实世界的数据纳入 ZK 证明,同时不损害证明系统的隐私和完整性。此能力对许多区块链应用程序至关重要,在这些应用中,外部数据(例如资产价格、天气信息或任何链下数据)需要纳入链上操作。

Noir 中预言机的工作原理

Noir 中的预言机本质上是外部世界与 ZK 电路之间的桥梁。它从外部源获取数据,将这些数据输入到电路中,并确保用于证明的数据既有效又真实。使用预言机通常涉及以下步骤:

  1. 请求数据: 预言机从外部源请求特定数据。
  2. 数据验证: 接收到的数据进行验证,以确保其完整性和正确性。
  3. 将数据纳入证明: 然后将已验证的数据用在 ZK 电路中,以在不泄露数据本身的情况下证明某些陈述或条件。

预言机尤其强大,因为它们允许智能合约和 ZK 电路保持确定性,同时仍使用可能随着时间变化的外部数据。

如果你想了解有关 Noir 中预言机的更多信息,可以查看 官方文档.

4. 示例应用程序

引言

在本指南中,我们将探讨开发一个去中心化投票系统,该系统通过先进的加密技术确保隐私和安全性。该系统利用零知识电路来处理和验证投票,确保每个投票既独特又匿名,同时保持整个投票过程的完整性。

我们将首先设置项目环境,安装必要的工具,然后继续开发能够验证投票并计算必要加密证明的电路。接下来,我们将创建一个 Solidity 智能合约,以管理区块链上的投票过程,包括提案创建、投票和结果统计。该合约将与我们的 Noir 电路生成的零知识证明无缝互动,确保系统既健壮又安全。

无论你是经验丰富的区块链开发人员还是零知识证明的新手,本教程将为你提供使用 Noir 和 Solidity 构建强大、注重隐私的应用程序所需的知识和工具。让我们开始吧!

安装

安装 Noirup

在你的终端中运行以下脚本:

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_openingsecret 字段计算新的承诺(computed_vote_commitment)。然后将此承诺与提供的 vote_commitment 进行比较,以确保投票没有被更改,并且打开(实际投票)与选民所作的承诺相对应。

let computed_vote_commitment = std::hash::pedersen_hash([vote_opening, secret]);
assert(vote_commitment == computed_vote_commitment);

我们为什么使用 Pedersen 哈希:Pedersen 哈希在这里使用是因为它具有抗碰撞性和同态性。抗碰撞性确保两个不同输入几乎不可能产生相同的哈希,这对投票的安全性至关重要。同态性特性允许在加密数据上执行某些操作(如求和承诺)而无需解密,保持选民选择的隐私。

我们为什么使用 assertassert 语句对强制计算的承诺与提供的承诺匹配至关重要。这确保投票没有被篡改,并且承诺准确反映选民的原始选择。

投票范围检查

接下来,函数确保 vote_opening 在有效范围内(例如,对于二元选择为 0 或 1)。此步骤对于防止无效票数(例如,在是/否场景中投票为 2)至关重要。

let vote_opening_int = vote_opening as i32;

assert(vote_opening_int >= 0);
assert(vote_opening_int <= 1);

消除器计算

该函数然后使用 rootsecretproposalIdsalt 字段计算消除器。消除器是一个独特的值,可以防止将特定投票与选民链接,同时仍然允许投票被计算。这对维护选民隐私非常重要,同时确保每个投票是唯一的且不能被复制。

let nullifier = std::hash::pedersen_hash([root, secret, proposalId, salt]);

我们为什么使用 Pedersen 哈希:再次,Pedersen 哈希因其加密属性而被使用,确保消除器是唯一和安全的。使用 salt 增加了一些随机性,这对防止同一选民在不同投票之间建立联系至关重要。

Merkle 树完整性检查

该函数从 secret 生成一个 note_commitment,然后使用该承诺连同 indexhash_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 程序

现在项目已设置好,我们可以执行我们的 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 限制,因此该最大电路大小与可以在浏览器中证明的最大电路大小不同。

证明 Noir 程序的执行

使用 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 树的根,确保仅纳入授权票数。
  • verifierUltraVerifier 合约的实例,对于与每笔投票相关的零知识证明进行验证。

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 已经投票,从而防止重复投票。
  • Merkle 证明验证:一种加密证明,验证基于 Merkle 根和其他参数的投票的有效性。
  • 计票:根据投票值(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 以获取完整代码

5. 结论

总之,Noir 提供了一个简化且易于访问的零知识证明开发方法,使开发人员能够构建保护隐私的应用程序,而无需深入的加密知识。通过创建一个去中心化的投票系统,我们展示了 Noir 如何简化与复杂的加密技术的集成,使实施安全和匿名的链上解决方案变得更容易。

这就是我们 Noir 语言深度探讨的第二部分的结束!

请联系 Three Sigma,让我们的专业团队为你在 Web3 领域提供信心满满的指导。依托我们在智能合约安全、经济建模和区块链工程方面的专业知识,我们将帮助你确保项目的未来。

今天就 联系我们,将你的 Web3 设想变为现实!

参考文献:Noir 文档Awesome Noir Repo

  • 原文链接: threesigma.xyz/blog/buil...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Three Sigma
Three Sigma
Three Sigma is a blockchain engineering and auditing firm focused on improving Web3 by working closely with projects in the space.