Skip to content

Commit 4a0d3dc

Browse files
committed
add root check, which was missing
1 parent 4c38c45 commit 4a0d3dc

File tree

3 files changed

+94
-28
lines changed

3 files changed

+94
-28
lines changed

README.md

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -962,23 +962,40 @@ Inside the `vote` function, voters will send their:
962962
- **proof** → the cryptographic proof (later built in the frontend)
963963
- **public inputs**`nullifierHash`, `root`, `vote`, and `tree depth`
964964

965-
Before counting votes, we enforce some **rules**:
965+
Before counting votes, we enforce some **rules** in the following order:
966966

967-
#### 1. Prevent Double-Voting 🛑
967+
#### 1. Validate the Root 🔐
968968

969-
- Check if the `_nullifierHash` has already been used.
970-
- If yes → revert the transaction.
971-
- Track used nullifiers with a mapping: `s_nullifierHashes`.
969+
- **Check for empty tree root**: this prevents voting when no one has registered yet.
970+
- **Validate root matches current root state** (`s_tree.root()`): This ensures the proof was generated against the **actual on-chain Merkle tree**, not some arbitrary tree.
972971

973-
👉 Without this safeguard, anyone could replay the same proof/inputs over and over to vote multiple times.
974-
That’s why **nullifiers are the cornerstone** of privacy-preserving voting.
972+
**Why this matters:** Without root validation, an attacker could:
973+
974+
- **Create their own fake Merkle tree** with commitments they control
975+
- Generate a mathematically valid ZK proof against that fake tree
976+
- Submit the proof with their fake root to the contract
977+
- The verifier would accept it (the proof is valid for that root!)
978+
- **Result**: They vote without ever registering on-chain, completely bypassing the allowlist system
979+
980+
> 👉 The root check is what **binds the proof to the actual on-chain registration tree**. Without it, anyone could vote by creating fake trees. This is your **first line of defense**!
975981
976982
#### 2. Verify the Proof 🔒
977983

978984
- Call the `verify()` function on `i_verifier` and pass in the proof + public inputs.
979985
- The verifier expects **public inputs as a `bytes32[]` array**, in **exactly the same order** as in your circuit file.
980986
- If verification fails → revert the transaction.
981987

988+
#### 3. Prevent Double-Voting 🛑
989+
990+
- Check if the `_nullifierHash` has already been used.
991+
- If yes → revert the transaction.
992+
- Track used nullifiers with a mapping: `s_nullifierHashes`.
993+
994+
👉 Without this safeguard, anyone could replay the same proof/inputs over and over to vote multiple times.
995+
That's why **nullifiers are the cornerstone** of privacy-preserving voting.
996+
997+
👉 **Important**: This check happens **after** proof verification. If the proof is invalid, we want to fail fast without wasting gas on state changes.
998+
982999
✅ Once both checks pass:
9831000

9841001
- Increment `s_yesVotes` or `s_noVotes` accordingly
@@ -990,27 +1007,34 @@ That’s why **nullifiers are the cornerstone** of privacy-preserving voting.
9901007
<details>
9911008
<summary>❓ Question 1</summary>
9921009

1010+
What two root validation checks must you perform **before** any other logic, and why are they essential for security?
1011+
1012+
</details>
1013+
1014+
<details>
1015+
<summary>❓ Question 2</summary>
1016+
9931017
Before writing the voting logic, how can you stop a `_nullifierHash` from being reused so no one can vote twice?
9941018
Make sure to revert with the correct error.
9951019

9961020
</details>
9971021

9981022
<details>
999-
<summary>❓ Question 2</summary>
1023+
<summary>❓ Question 3</summary>
10001024

10011025
When passing inputs to the verifier, how do you build the `bytes32[]` array and in what order should you place `_nullifierHash`, `_root`, `_vote`, and `_depth`?
10021026

10031027
</details>
10041028

10051029
<details>
1006-
<summary>❓ Question 3</summary>
1030+
<summary>❓ Question 4</summary>
10071031

10081032
After calling `i_verifier.verify(_proof, publicInputs)`, what condition should you check, and what should happen if it fails?
10091033

10101034
</details>
10111035

10121036
<details>
1013-
<summary>❓ Question 4</summary>
1037+
<summary>❓ Question 5</summary>
10141038

10151039
Once the proof is verified, how do you decide whether to increment `s_yesVotes` or `s_noVotes` and then emit the `VoteCast` event?
10161040
_(Hint: `_vote` comes as 0 or 1)_
@@ -1047,10 +1071,13 @@ constructor(address _owner, address _verifier, string memory _question) Ownable(
10471071
10481072
function vote(bytes memory _proof, bytes32 _nullifierHash, bytes32 _root, bytes32 _vote, bytes32 _depth) public {
10491073
/// Checkpoint 6 //////
1050-
if (s_nullifierHashes[_nullifierHash]) {
1051-
revert Voting__NullifierHashAlreadyUsed(_nullifierHash);
1074+
if (_root == bytes32(0)) {
1075+
revert Voting__EmptyTree();
1076+
}
1077+
1078+
if (_root != bytes32(s_tree.root())) {
1079+
revert Voting__InvalidRoot();
10521080
}
1053-
s_nullifierHashes[_nullifierHash] = true;
10541081
10551082
bytes32[] memory publicInputs = new bytes32[](4);
10561083
publicInputs[0] = _nullifierHash;
@@ -1062,6 +1089,11 @@ function vote(bytes memory _proof, bytes32 _nullifierHash, bytes32 _root, bytes3
10621089
revert Voting__InvalidProof();
10631090
}
10641091
1092+
if (s_nullifierHashes[_nullifierHash]) {
1093+
revert Voting__NullifierHashAlreadyUsed(_nullifierHash);
1094+
}
1095+
s_nullifierHashes[_nullifierHash] = true;
1096+
10651097
if (_vote == bytes32(uint256(1))) {
10661098
s_yesVotes++;
10671099
} else {

extension/README.md.args.mjs

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -967,23 +967,40 @@ Inside the \`vote\` function, voters will send their:
967967
- **proof** → the cryptographic proof (later built in the frontend)
968968
- **public inputs** → \`nullifierHash\`, \`root\`, \`vote\`, and \`tree depth\`
969969
970-
Before counting votes, we enforce some **rules**:
970+
Before counting votes, we enforce some **rules** in the following order:
971971
972-
#### 1. Prevent Double-Voting 🛑
972+
#### 1. Validate the Root 🔐
973973
974-
- Check if the \`_nullifierHash\` has already been used.
975-
- If yes → revert the transaction.
976-
- Track used nullifiers with a mapping: \`s_nullifierHashes\`.
974+
- **Check for empty tree root**: this prevents voting when no one has registered yet.
975+
- **Validate root matches current root state** (\`s_tree.root()\`): This ensures the proof was generated against the **actual on-chain Merkle tree**, not some arbitrary tree.
977976
978-
👉 Without this safeguard, anyone could replay the same proof/inputs over and over to vote multiple times.
979-
That’s why **nullifiers are the cornerstone** of privacy-preserving voting.
977+
**Why this matters:** Without root validation, an attacker could:
978+
979+
- **Create their own fake Merkle tree** with commitments they control
980+
- Generate a mathematically valid ZK proof against that fake tree
981+
- Submit the proof with their fake root to the contract
982+
- The verifier would accept it (the proof is valid for that root!)
983+
- **Result**: They vote without ever registering on-chain, completely bypassing the allowlist system
984+
985+
> 👉 The root check is what **binds the proof to the actual on-chain registration tree**. Without it, anyone could vote by creating fake trees. This is your **first line of defense**!
980986
981987
#### 2. Verify the Proof 🔒
982988
983989
- Call the \`verify()\` function on \`i_verifier\` and pass in the proof + public inputs.
984990
- The verifier expects **public inputs as a \`bytes32[]\` array**, in **exactly the same order** as in your circuit file.
985991
- If verification fails → revert the transaction.
986992
993+
#### 3. Prevent Double-Voting 🛑
994+
995+
- Check if the \`_nullifierHash\` has already been used.
996+
- If yes → revert the transaction.
997+
- Track used nullifiers with a mapping: \`s_nullifierHashes\`.
998+
999+
👉 Without this safeguard, anyone could replay the same proof/inputs over and over to vote multiple times.
1000+
That's why **nullifiers are the cornerstone** of privacy-preserving voting.
1001+
1002+
👉 **Important**: This check happens **after** proof verification. If the proof is invalid, we want to fail fast without wasting gas on state changes.
1003+
9871004
✅ Once both checks pass:
9881005
9891006
- Increment \`s_yesVotes\` or \`s_noVotes\` accordingly
@@ -995,27 +1012,34 @@ That’s why **nullifiers are the cornerstone** of privacy-preserving voting.
9951012
<details>
9961013
<summary>❓ Question 1</summary>
9971014
1015+
What two root validation checks must you perform **before** any other logic, and why are they essential for security?
1016+
1017+
</details>
1018+
1019+
<details>
1020+
<summary>❓ Question 2</summary>
1021+
9981022
Before writing the voting logic, how can you stop a \`_nullifierHash\` from being reused so no one can vote twice?
9991023
Make sure to revert with the correct error.
10001024
10011025
</details>
10021026
10031027
<details>
1004-
<summary>❓ Question 2</summary>
1028+
<summary>❓ Question 3</summary>
10051029
10061030
When passing inputs to the verifier, how do you build the \`bytes32[]\` array and in what order should you place \`_nullifierHash\`, \`_root\`, \`_vote\`, and \`_depth\`?
10071031
10081032
</details>
10091033
10101034
<details>
1011-
<summary>❓ Question 3</summary>
1035+
<summary>❓ Question 4</summary>
10121036
10131037
After calling \`i_verifier.verify(_proof, publicInputs)\`, what condition should you check, and what should happen if it fails?
10141038
10151039
</details>
10161040
10171041
<details>
1018-
<summary>❓ Question 4</summary>
1042+
<summary>❓ Question 5</summary>
10191043
10201044
Once the proof is verified, how do you decide whether to increment \`s_yesVotes\` or \`s_noVotes\` and then emit the \`VoteCast\` event?
10211045
_(Hint: \`_vote\` comes as 0 or 1)_
@@ -1052,10 +1076,13 @@ constructor(address _owner, address _verifier, string memory _question) Ownable(
10521076
10531077
function vote(bytes memory _proof, bytes32 _nullifierHash, bytes32 _root, bytes32 _vote, bytes32 _depth) public {
10541078
/// Checkpoint 6 //////
1055-
if (s_nullifierHashes[_nullifierHash]) {
1056-
revert Voting__NullifierHashAlreadyUsed(_nullifierHash);
1079+
if (_root == bytes32(0)) {
1080+
revert Voting__EmptyTree();
1081+
}
1082+
1083+
if (_root != bytes32(s_tree.root())) {
1084+
revert Voting__InvalidRoot();
10571085
}
1058-
s_nullifierHashes[_nullifierHash] = true;
10591086
10601087
bytes32[] memory publicInputs = new bytes32[](4);
10611088
publicInputs[0] = _nullifierHash;
@@ -1067,6 +1094,11 @@ function vote(bytes memory _proof, bytes32 _nullifierHash, bytes32 _root, bytes3
10671094
revert Voting__InvalidProof();
10681095
}
10691096
1097+
if (s_nullifierHashes[_nullifierHash]) {
1098+
revert Voting__NullifierHashAlreadyUsed(_nullifierHash);
1099+
}
1100+
s_nullifierHashes[_nullifierHash] = true;
1101+
10701102
if (_vote == bytes32(uint256(1))) {
10711103
s_yesVotes++;
10721104
} else {

extension/packages/hardhat/contracts/Voting.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//SPDX-License-Identifier: MIT
22
pragma solidity >=0.8.0 <0.9.0;
33

4-
import { LeanIMT, LeanIMTData } from "@zk-kit/lean-imt.sol/LeanIMT.sol";
5-
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
4+
import {LeanIMT, LeanIMTData} from "@zk-kit/lean-imt.sol/LeanIMT.sol";
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
66
/// Checkpoint 6 //////
77
// import {IVerifier} from "./Verifier.sol";
88

@@ -17,6 +17,8 @@ contract Voting is Ownable {
1717
error Voting__NullifierHashAlreadyUsed(bytes32 nullifierHash);
1818
error Voting__InvalidProof();
1919
error Voting__NotAllowedToVote();
20+
error Voting__EmptyTree();
21+
error Voting__InvalidRoot();
2022

2123
///////////////////////
2224
/// State Variables ///

0 commit comments

Comments
 (0)