跳到主要内容

构建私密投票 dApp

私密投票是机密智能合约的另一头牌应用。每张选票加密,观察者追不到谁投了什么——但最终计票是公开、可审计的。

设计

  • 选民注册——一个合格选民授权名单,或代币门控模型。
  • 投票阶段——每个选民提交加密选票。没人看到单张选票。
  • 计票阶段——截止后合约从加密状态计算票数并发布总数。

合约

contracts/PrivateVoting.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract PrivateVoting {
enum Choice { None, Yes, No, Abstain }

address public immutable creator;
uint256 public immutable votingDeadline;
string public proposal;

mapping(address => bool) public isEligible;
mapping(address => Choice) private ballots; // 每选民加密
mapping(address => bool) public hasVoted;

// 计票后公开
uint256 public yesCount;
uint256 public noCount;
uint256 public abstainCount;
bool public tallied;

event VoterRegistered(address voter);
event VoteCast(address indexed voter); // 不 emit choice
event ResultsPublished(uint256 yes, uint256 no, uint256 abstain);

constructor(string memory _proposal, uint256 _votingDuration) {
creator = msg.sender;
proposal = _proposal;
votingDeadline = block.timestamp + _votingDuration;
}

function registerVoter(address voter) external {
require(msg.sender == creator, "not creator");
require(block.timestamp < votingDeadline, "voting closed");
isEligible[voter] = true;
emit VoterRegistered(voter);
}

function vote(Choice choice) external {
require(block.timestamp < votingDeadline, "voting closed");
require(isEligible[msg.sender], "not eligible");
require(!hasVoted[msg.sender], "already voted");
require(choice != Choice.None, "invalid choice");

ballots[msg.sender] = choice;
hasVoted[msg.sender] = true;
emit VoteCast(msg.sender);
}

// 截止后任何人可触发计票
function tally(address[] calldata voters) external {
require(block.timestamp >= votingDeadline, "voting still open");
require(!tallied, "already tallied");

for (uint256 i = 0; i < voters.length; i++) {
Choice c = ballots[voters[i]];
if (c == Choice.Yes) yesCount++;
else if (c == Choice.No) noCount++;
else if (c == Choice.Abstain) abstainCount++;
}
tallied = true;
emit ResultsPublished(yesCount, noCount, abstainCount);
}

// 选民可事后验证自己的投票(签名查询)
function myVote() external view returns (Choice) {
return ballots[msg.sender];
}
}

这个设计的注意点

计票函数遍历调用方传入的选民列表。 因为合约自身无法便宜地遍历 mapping。实践中由创建者或可信方提交列表。

投票选择加密存储。 只有投票人能读自己的选票(通过签名查询)。其他人——包括计票方——都看不到单张选择,只看到聚合数。

公开参与、私密选择。 hasVoted 是公开 bool,任何人都能看到参与了,但怎么投的看不到。

防胁迫

机密性防止外部观察者知道某人投了什么。但胁迫者仍可能要求投票人通过签名查询揭示,投票人可能配合。

极致抗胁迫场景,考虑一种方案:允许在截止前重新投票(只有最后一票算数)——被胁迫的投票人可以在胁迫者面前假装投一种,事后重投。或者用 MACI 风格方法,OXN 因加密可以比公开 EVM 更高效地实现。

测试

it("keeps individual ballots private", async function () {
await voting.connect(creator).registerVoter(alice.address);
await voting.connect(creator).registerVoter(bob.address);

await voting.connect(alice).vote(1); // Yes
await voting.connect(bob).vote(2); // No

// 互相看不到对方的投票
expect(await voting.connect(alice).myVote()).to.equal(1);
expect(await voting.connect(bob).myVote()).to.equal(2);

// 第三方只看到参与,不看到选择
expect(await voting.hasVoted(alice.address)).to.equal(true);
expect(await voting.hasVoted(bob.address)).to.equal(true);
});

下一步