@crisp-e3/contracts 0.18.0-insecure.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -8
- package/contracts/SelfRegistry.sol +101 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pnpm deploy:contracts:full # also deploy Interfold stack (no ZK unless ENABL
|
|
|
39
39
|
|
|
40
40
|
This is the main logic of CRISP - an interfold program for secure voting.
|
|
41
41
|
|
|
42
|
-
It exposes
|
|
42
|
+
It exposes three main functions:
|
|
43
43
|
|
|
44
44
|
- `validate` - that is called when a new E3 instance is requested on Interfold
|
|
45
45
|
(`Interfold.request`).
|
|
@@ -47,10 +47,15 @@ It exposes two main functions:
|
|
|
47
47
|
(`Interfold.publishCiphertextOutput`). This function ensures that the ciphertext output is valid.
|
|
48
48
|
CRISP uses Risc0 as the compute provider for running the FHE program, thus the proof will be a
|
|
49
49
|
Risc0 proof.
|
|
50
|
-
- `
|
|
51
|
-
the
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
- `publishInput` - accepts an input for the E3 instance. Data providers call it on this contract
|
|
51
|
+
directly. In CRISP, the data providers are the voters and the input is the vote itself. The
|
|
52
|
+
function checks the stage and the input window, resolves the voter's eligibility from the census,
|
|
53
|
+
and verifies a Noir proof over nine public inputs, which is what establishes that the ciphertext
|
|
54
|
+
was encrypted correctly under the committee public key
|
|
55
|
+
(`examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol:493-554`, paths from the
|
|
56
|
+
repository root). The verifier is the one the round's census selects: `CRISPVerifier.sol` for a
|
|
57
|
+
census posted as a Merkle root, `CRISPOnchainVerifier.sol` for one read from token balances on
|
|
58
|
+
chain. Both files declare a contract named `HonkVerifier`, which is why they are named by file.
|
|
59
|
+
The Greco relations that proof checks are built by
|
|
60
|
+
`crates/zk-helpers/src/circuits/threshold/user_data_encryption/` and proved by the circuits under
|
|
61
|
+
`circuits/bin/threshold/`. See the Greco [paper](https://eprint.iacr.org/2024/594).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-only
|
|
2
|
+
//
|
|
3
|
+
// This file is provided WITHOUT ANY WARRANTY;
|
|
4
|
+
// without even the implied warranty of MERCHANTABILITY
|
|
5
|
+
// or FITNESS FOR A PARTICULAR PURPOSE.
|
|
6
|
+
pragma solidity >=0.8.27;
|
|
7
|
+
|
|
8
|
+
/// @notice An open self-registration census for `CensusMode.ONCHAIN` rounds.
|
|
9
|
+
/// @dev Anyone may register themselves, once, and registration is permanent. The contract answers
|
|
10
|
+
/// the `IVotesToken` surface `CRISPProgram._eligibility` reads: `getPastVotes` returns 1 for a
|
|
11
|
+
/// registered account and 0 otherwise. It has no `decimals()`, so the voting-power divisor
|
|
12
|
+
/// derives to 1, and no `clock()`, so the round snapshot falls back to block numbers — both
|
|
13
|
+
/// fallbacks `CRISPProgram` already implements.
|
|
14
|
+
///
|
|
15
|
+
/// `getPastVotes` deliberately ignores the timepoint. An honest checkpointed answer would fix the
|
|
16
|
+
/// electorate at the round's snapshot, which is taken in the transaction that requests the round —
|
|
17
|
+
/// before anyone knew there was a round to register for. Ignoring it is what lets a voter register
|
|
18
|
+
/// during the input window and vote in the same round. The cost is that the electorate is not
|
|
19
|
+
/// fixed per round: an input's eligibility depends on when it is published, not on one snapshot
|
|
20
|
+
/// every input shares. That is a deliberate trade for low-stakes polls.
|
|
21
|
+
///
|
|
22
|
+
/// Registration is open and unpriced, so one person can register any number of addresses. Use
|
|
23
|
+
/// this census only where one-address-one-vote is not worth attacking — a meme poll, a temperature
|
|
24
|
+
/// check — never where the outcome carries value. Pair it with `CreditMode.CONSTANT` and credits
|
|
25
|
+
/// of 1: every registrant carries the same weight, and `minVotingPower` of 1 is exactly what a
|
|
26
|
+
/// registered account reports.
|
|
27
|
+
///
|
|
28
|
+
/// Registrants are enumerable so mask submitters can draw a target: a mask is written to someone
|
|
29
|
+
/// else's slot, and without a public list of who is eligible there is nobody to mask. The list is
|
|
30
|
+
/// append-only, which keeps indices stable for random selection.
|
|
31
|
+
contract SelfRegistry {
|
|
32
|
+
/// @notice One entry per registered account, in registration order. Append-only.
|
|
33
|
+
address[] private _registrants;
|
|
34
|
+
|
|
35
|
+
/// @notice Whether an account has registered.
|
|
36
|
+
mapping(address account => bool) public isRegistered;
|
|
37
|
+
|
|
38
|
+
/// @notice An account registered for voting.
|
|
39
|
+
/// @param account The account that registered.
|
|
40
|
+
/// @param index Its position in the registrant list.
|
|
41
|
+
event Registered(address indexed account, uint256 index);
|
|
42
|
+
|
|
43
|
+
/// @notice The account has already registered; registration is permanent and single-shot.
|
|
44
|
+
error AlreadyRegistered(address account);
|
|
45
|
+
|
|
46
|
+
/// @notice Register the caller as an eligible voter.
|
|
47
|
+
/// @dev Permanent: there is no deregistration. Removing an entry would shift or hole the
|
|
48
|
+
/// registrant list that mask submitters index into, and would let a voter provably leave the
|
|
49
|
+
/// electorate mid-round — a slot that cannot be masked any more is a receipt.
|
|
50
|
+
function register() external {
|
|
51
|
+
if (isRegistered[msg.sender]) revert AlreadyRegistered(msg.sender);
|
|
52
|
+
|
|
53
|
+
isRegistered[msg.sender] = true;
|
|
54
|
+
_registrants.push(msg.sender);
|
|
55
|
+
|
|
56
|
+
emit Registered(msg.sender, _registrants.length - 1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// @notice The voting power of an account: 1 when registered, 0 otherwise.
|
|
60
|
+
/// @dev The timepoint is ignored — see the contract-level note. The parameter stays in the
|
|
61
|
+
/// signature because `CRISPProgram` calls through `IVotesToken`.
|
|
62
|
+
/// @param account The account to read.
|
|
63
|
+
/// @return One for a registered account, zero otherwise.
|
|
64
|
+
function getPastVotes(address account, uint256) external view returns (uint256) {
|
|
65
|
+
return isRegistered[account] ? 1 : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// @notice How many accounts have registered.
|
|
69
|
+
/// @return The registrant count.
|
|
70
|
+
function totalRegistrants() external view returns (uint256) {
|
|
71
|
+
return _registrants.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// @notice The registrant at one index of the append-only list.
|
|
75
|
+
/// @param index The position, in registration order.
|
|
76
|
+
/// @return The registered account.
|
|
77
|
+
function registrantAt(uint256 index) external view returns (address) {
|
|
78
|
+
return _registrants[index];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// @notice A page of the registrant list, for clients drawing mask targets.
|
|
82
|
+
/// @dev Clamped rather than reverting past the end, so a caller can page with a fixed size and
|
|
83
|
+
/// not race registrations between reading the count and reading the page.
|
|
84
|
+
/// @param start The first index to read.
|
|
85
|
+
/// @param count How many entries to read at most.
|
|
86
|
+
/// @return page The registrants in `[start, min(start + count, total))`.
|
|
87
|
+
function registrants(uint256 start, uint256 count) external view returns (address[] memory page) {
|
|
88
|
+
uint256 total = _registrants.length;
|
|
89
|
+
if (start >= total) return new address[](0);
|
|
90
|
+
|
|
91
|
+
// Compared by subtraction, not by `start + count > total`: the addition runs under checked
|
|
92
|
+
// arithmetic, so a large `count` would revert before the clamp it exists to trigger.
|
|
93
|
+
// `total - start` cannot underflow — `start < total` is established above.
|
|
94
|
+
uint256 end = count > total - start ? total : start + count;
|
|
95
|
+
page = new address[](end - start);
|
|
96
|
+
|
|
97
|
+
for (uint256 i = start; i < end; i++) {
|
|
98
|
+
page[i - start] = _registrants[i];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crisp-e3/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"contracts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@zk-kit/lazy-imt.sol": "2.0.0-beta.12",
|
|
32
32
|
"poseidon-solidity": "^0.0.5",
|
|
33
33
|
"solady": "^0.1.13",
|
|
34
|
-
"@interfold/contracts": "0.
|
|
34
|
+
"@interfold/contracts": "0.13.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@nomicfoundation/hardhat-keystore": "3.0.3",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"typechain": "^8.3.0",
|
|
54
54
|
"typescript": "5.8.3",
|
|
55
55
|
"viem": "2.30.6",
|
|
56
|
-
"@crisp-e3/
|
|
57
|
-
"@crisp-e3/
|
|
56
|
+
"@crisp-e3/sdk": "^0.19.0",
|
|
57
|
+
"@crisp-e3/zk-inputs": "^0.19.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"compile": "hardhat compile",
|
|
@@ -66,10 +66,13 @@
|
|
|
66
66
|
"deploy:contracts:mock": "export USE_MOCKS=true PRINT_ENV_VARS=true && pnpm deploy:contracts",
|
|
67
67
|
"deploy:contracts:full": "export DEPLOY_INTERFOLD=true && pnpm deploy:contracts",
|
|
68
68
|
"deploy:contracts:full:mock": "export DEPLOY_INTERFOLD=true USE_MOCKS=true PRINT_ENV_VARS=true && pnpm deploy:contracts",
|
|
69
|
+
"governance:builder": "hardhat run deploy/create-governance-builder.ts",
|
|
69
70
|
"test": "hardhat test mocha",
|
|
70
71
|
"check:test-legs": "node scripts/check-test-legs.mjs",
|
|
71
|
-
"test:unit": "pnpm check:test-legs && hardhat test mocha tests/census-mode.test.ts tests/crisp.journal.test.ts tests/input-leaf.test.ts tests/interfold-binding.test.ts tests/tally.decoding.test.ts",
|
|
72
|
+
"test:unit": "pnpm check:test-legs && hardhat test mocha tests/census-mode.test.ts tests/crisp.journal.test.ts tests/input-leaf.test.ts tests/interfold-binding.test.ts tests/self-registry.test.ts tests/tally.decoding.test.ts",
|
|
72
73
|
"test:input-tree": "hardhat test mocha tests/input-tree-e2e.test.ts",
|
|
74
|
+
"test:input-tree:poisoning": "hardhat test mocha --grep \"accepts an input whose bytes|lets an honest mask follow\" -- tests/input-tree-e2e.test.ts",
|
|
75
|
+
"test:input-tree:canonical": "hardhat test mocha --grep \"^(?!.*(?:accepts an input whose bytes|lets an honest mask follow))\" -- tests/input-tree-e2e.test.ts",
|
|
73
76
|
"test:ballots": "hardhat test mocha tests/crisp.contracts.test.ts tests/onchain-census.test.ts",
|
|
74
77
|
"verify": "hardhat run deploy/verify.ts",
|
|
75
78
|
"updateSubmissionWindow": "hardhat ciphernode:window",
|