@crisp-e3/contracts 0.18.0-insecure.0 → 0.18.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 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 two main functions:
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
- - `validateInput` - validate the input data that is submitted to the E3 instance. It is called by
51
- the Interfold contract when a new input is published (`Interfold.publishInput`). In CRISP, the
52
- data providers (the ones submitting the inputs) are the voters, and the input submitted is the
53
- vote itself. The logic checks that gating conditions are satisfied and that the ciphertext is
54
- constructed correctly using
55
- [Greco](https://github.com/gnosisguild/interfold/tree/main/circuits/crates/libs/greco). See the
56
- Greco [paper](https://eprint.iacr.org/2024/594).
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.18.0-insecure.0",
3
+ "version": "0.18.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.6.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/zk-inputs": "^0.18.0-insecure.0",
57
- "@crisp-e3/sdk": "^0.18.0-insecure.0"
56
+ "@crisp-e3/sdk": "^0.18.0",
57
+ "@crisp-e3/zk-inputs": "^0.18.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",