@crisp-e3/contracts 0.17.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 +13 -8
- package/contracts/CRISPProgram.sol +294 -46
- package/contracts/Mocks/MockInterfold.sol +8 -3
- package/contracts/SelfRegistry.sol +101 -0
- package/contracts/interfaces/IHonkVerifier.sol +4 -2
- package/contracts/interfaces/IVotesToken.sol +6 -0
- package/contracts/{CRISPOnchainVerifier.sol → verifiers/CRISPOnchainVerifier.sol} +3 -3
- package/contracts/{CRISPVerifier.sol → verifiers/CRISPVerifier.sol} +3 -3
- package/package.json +13 -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).
|
|
@@ -12,11 +12,16 @@ import { IInterfold } from "@interfold/contracts/contracts/interfaces/IInterfold
|
|
|
12
12
|
import { E3 } from "@interfold/contracts/contracts/interfaces/IE3.sol";
|
|
13
13
|
import { Risc0ComputeProof } from "@interfold/contracts/contracts/lib/Risc0ComputeProof.sol";
|
|
14
14
|
import { LazyIMTData, InternalLazyIMT } from "@zk-kit/lazy-imt.sol/InternalLazyIMT.sol";
|
|
15
|
+
import { SNARK_SCALAR_FIELD } from "@zk-kit/lazy-imt.sol/Constants.sol";
|
|
15
16
|
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
16
17
|
import { IHonkVerifier } from "./interfaces/IHonkVerifier.sol";
|
|
17
18
|
import { IVotesToken } from "./interfaces/IVotesToken.sol";
|
|
18
19
|
import { IERC6372Clock } from "./interfaces/IERC6372Clock.sol";
|
|
19
20
|
|
|
21
|
+
interface IInterfoldProgramRegistry {
|
|
22
|
+
function e3Programs(IE3Program e3Program) external view returns (bool);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
21
26
|
using InternalLazyIMT for LazyIMTData;
|
|
22
27
|
|
|
@@ -59,6 +64,30 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
59
64
|
uint256 merkleRoot;
|
|
60
65
|
bytes32 paramsHash;
|
|
61
66
|
mapping(address slot => uint40 index) voteSlots;
|
|
67
|
+
/// @notice The proven ciphertext commitment of every input, keyed by slot and tree index.
|
|
68
|
+
/// @dev Keyed by both so a parent lookup for the wrong slot returns zero and is refused: it
|
|
69
|
+
/// costs no more storage than a single-key map and removes a separate same-slot check.
|
|
70
|
+
///
|
|
71
|
+
/// A history rather than one commitment per slot. An input names the entry it extends, and
|
|
72
|
+
/// this contract cannot tell whether the bytes published with an entry deserialize to the
|
|
73
|
+
/// ciphertext its commitment describes — only the Secure Process can, once the input window
|
|
74
|
+
/// closes. Keeping only the newest commitment would therefore let anyone leave a slot whose
|
|
75
|
+
/// head nobody but they can open, and a slot that cannot be masked is a slot whose every later
|
|
76
|
+
/// input is provably its owner voting again. With the history, an entry like that is simply
|
|
77
|
+
/// never extended: the next input names the same parent, and masking continues.
|
|
78
|
+
mapping(address slot => mapping(uint40 index => bytes32 commitment)) inputCommitment;
|
|
79
|
+
/// @notice Leaves already appended to this round's input tree.
|
|
80
|
+
/// @dev A replay guard, not a uniqueness requirement on ballots. The proof constrains the
|
|
81
|
+
/// commitment, not who submits it, so anyone who observes a published input can resubmit the
|
|
82
|
+
/// identical calldata: the proof still verifies and {_processVote} appends again. The tally
|
|
83
|
+
/// does not change — the replay names the same parent as the original, which is no longer the
|
|
84
|
+
/// head, so the Secure Process drops it — but the tree is fixed-depth, so enough replays reach
|
|
85
|
+
/// capacity and every later input reverts, denying the round.
|
|
86
|
+
///
|
|
87
|
+
/// Keyed by the leaf rather than the proof because the leaf is exactly what an append adds.
|
|
88
|
+
/// Two genuinely distinct inputs differ in bytes, commitment, slot or parent, so they differ
|
|
89
|
+
/// here; only a byte-identical resubmission collides.
|
|
90
|
+
mapping(uint256 leaf => bool) appendedLeaf;
|
|
62
91
|
LazyIMTData votes;
|
|
63
92
|
uint256 numOptions;
|
|
64
93
|
CreditMode creditMode;
|
|
@@ -73,6 +102,9 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
73
102
|
/// @notice The timepoint that voting power is read at, in the ERC-6372 clock units of the
|
|
74
103
|
/// token. Recorded when the round is requested, so it is the same for every input.
|
|
75
104
|
uint48 snapshot;
|
|
105
|
+
/// @notice Divides raw token voting power into the units the ballot is encoded in. Only used
|
|
106
|
+
/// by `CensusMode.ONCHAIN`. Never zero for such a round.
|
|
107
|
+
uint256 votingPowerDivisor;
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
// Constants
|
|
@@ -89,6 +121,9 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
89
121
|
/// (`circuits/lib/src/constants.nr`). A round above this accepts no ballot, because every
|
|
90
122
|
/// vote proof fails. Must stay aligned with the SDK constant of the same name.
|
|
91
123
|
uint256 constant MAX_VOTE_OPTIONS = 10;
|
|
124
|
+
/// @notice Largest `decimals` a divisor can be derived from: `10 ** 77` is the last power of ten
|
|
125
|
+
/// that fits in a uint256.
|
|
126
|
+
uint8 constant MAX_DERIVABLE_DECIMALS = 78;
|
|
92
127
|
// State variables
|
|
93
128
|
IInterfold public interfold;
|
|
94
129
|
IRiscZeroVerifier public risc0Verifier;
|
|
@@ -112,6 +147,9 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
112
147
|
error CallerNotAuthorized();
|
|
113
148
|
error E3AlreadyInitialized();
|
|
114
149
|
error InterfoldAddressZero();
|
|
150
|
+
error InterfoldAlreadyBound();
|
|
151
|
+
error InterfoldNotContract();
|
|
152
|
+
error ProgramNotRegistered();
|
|
115
153
|
error Risc0VerifierAddressZero();
|
|
116
154
|
error InvalidHonkVerifier();
|
|
117
155
|
error EmptyInputData();
|
|
@@ -133,6 +171,24 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
133
171
|
/// replaces the Merkle membership proof that gates both branches in the other modes.
|
|
134
172
|
error SlotNotEligible();
|
|
135
173
|
error InvalidCensusMode();
|
|
174
|
+
|
|
175
|
+
/// @notice A token reports more decimals than a divisor can be derived from.
|
|
176
|
+
/// @dev `10 ** (decimals - 1)` must fit in a uint256. Pass an explicit divisor for such a token.
|
|
177
|
+
error UnsupportedTokenDecimals(uint8 decimals);
|
|
178
|
+
|
|
179
|
+
/// @notice An ONCHAIN round's floor is below one ballot unit.
|
|
180
|
+
/// @dev `minVotingPower` must be at least the divisor, so every slot that clears the floor
|
|
181
|
+
/// carries at least one unit of weight. Below that, a slot could be eligible to write yet scale
|
|
182
|
+
/// to zero — able to publish, but unable to carry any weight, which is disenfranchisement that
|
|
183
|
+
/// nothing on-chain would report.
|
|
184
|
+
error MinVotingPowerBelowScale();
|
|
185
|
+
/// @notice An input names a parent this slot never wrote to.
|
|
186
|
+
/// @dev Includes an index belonging to another slot, which the per-slot commitment map reads as
|
|
187
|
+
/// absent. Name no parent instead when there is nothing to extend.
|
|
188
|
+
error UnknownParentInput(uint40 parentIndex);
|
|
189
|
+
|
|
190
|
+
/// @notice Thrown when an input identical to one already published is submitted again.
|
|
191
|
+
error InputAlreadyPublished(uint256 leaf);
|
|
136
192
|
error SlotIsEmpty();
|
|
137
193
|
error MerkleRootNotSet();
|
|
138
194
|
error InvalidNumOptions();
|
|
@@ -142,32 +198,60 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
142
198
|
error InvalidComputeContext();
|
|
143
199
|
|
|
144
200
|
// Events
|
|
145
|
-
event
|
|
146
|
-
|
|
147
|
-
/// @notice
|
|
148
|
-
/// @
|
|
201
|
+
event InterfoldBound(address indexed interfold);
|
|
202
|
+
|
|
203
|
+
/// @notice A ciphertext input was accepted for a round.
|
|
204
|
+
/// @dev Carries the slot and the commitment as well as the bytes. Both are already public — the
|
|
205
|
+
/// slot is a plaintext `publishInput` argument and `getSlotIndex` exposes it — so emitting them
|
|
206
|
+
/// leaks nothing and saves every consumer from parsing transaction calldata. The Secure Process
|
|
207
|
+
/// needs the commitment to check that the published bytes are the ciphertext that was proven.
|
|
208
|
+
event InputPublished(
|
|
209
|
+
uint256 indexed e3Id,
|
|
210
|
+
address indexed slotAddress,
|
|
211
|
+
bytes32 encryptedVoteCommitment,
|
|
212
|
+
bytes encryptedVote,
|
|
213
|
+
uint256 index,
|
|
214
|
+
uint40 parentIndexPlusOne
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
/// @notice Initialize the contract without an Interfold controller.
|
|
218
|
+
/// @dev The owner binds the controller after Interfold registers this program.
|
|
219
|
+
/// @param _initialOwner The account that can configure and bind this program.
|
|
149
220
|
/// @param _risc0Verifier The RISC Zero verifier address
|
|
150
221
|
/// @param _honkVerifier The honk verifier address
|
|
151
222
|
/// @param _imageId The image ID for the guest program
|
|
152
223
|
constructor(
|
|
153
|
-
|
|
224
|
+
address _initialOwner,
|
|
154
225
|
IRiscZeroVerifier _risc0Verifier,
|
|
155
226
|
IHonkVerifier _honkVerifier,
|
|
156
227
|
IHonkVerifier _onchainHonkVerifier,
|
|
157
228
|
bytes32 _imageId
|
|
158
|
-
) Ownable(
|
|
159
|
-
if (address(_interfold) == address(0)) revert InterfoldAddressZero();
|
|
229
|
+
) Ownable(_initialOwner) EIP712("CRISP", "1") {
|
|
160
230
|
if (address(_risc0Verifier) == address(0)) revert Risc0VerifierAddressZero();
|
|
161
231
|
if (address(_honkVerifier) == address(0)) revert InvalidHonkVerifier();
|
|
162
232
|
if (address(_onchainHonkVerifier) == address(0)) revert InvalidHonkVerifier();
|
|
163
233
|
|
|
164
|
-
interfold = _interfold;
|
|
165
234
|
risc0Verifier = _risc0Verifier;
|
|
166
235
|
honkVerifier = _honkVerifier;
|
|
167
236
|
onchainHonkVerifier = _onchainHonkVerifier;
|
|
168
237
|
imageId = _imageId;
|
|
169
238
|
}
|
|
170
239
|
|
|
240
|
+
/// @notice Bind this program to its permanent Interfold controller.
|
|
241
|
+
/// @dev Interfold must register this program before the owner calls this function.
|
|
242
|
+
/// @param _interfold The Interfold controller that registered this program.
|
|
243
|
+
function bindInterfold(IInterfold _interfold) external onlyOwner {
|
|
244
|
+
if (address(interfold) != address(0)) revert InterfoldAlreadyBound();
|
|
245
|
+
if (address(_interfold) == address(0)) revert InterfoldAddressZero();
|
|
246
|
+
if (address(_interfold).code.length == 0) revert InterfoldNotContract();
|
|
247
|
+
if (!IInterfoldProgramRegistry(address(_interfold)).e3Programs(IE3Program(address(this)))) {
|
|
248
|
+
revert ProgramNotRegistered();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
interfold = _interfold;
|
|
252
|
+
emit InterfoldBound(address(_interfold));
|
|
253
|
+
}
|
|
254
|
+
|
|
171
255
|
/// @notice The digest a voter signs to authorise one ballot.
|
|
172
256
|
/// @dev Computed for every input, and for mask inputs as well as real votes. The circuit ignores
|
|
173
257
|
/// it on the mask branch, but the contract must not skip it: a digest that were computed only
|
|
@@ -192,12 +276,19 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
192
276
|
}
|
|
193
277
|
|
|
194
278
|
/// @notice Set the Image ID for the guest program
|
|
279
|
+
/// @dev This value is application state, not protocol state. Interfold snapshots the protocol
|
|
280
|
+
/// ciphertext verifier for each E3 at request time, and that verifier's own `imageId` is
|
|
281
|
+
/// immutable, so changing this value cannot replace a computation the protocol already accepted.
|
|
282
|
+
/// It can still break an E3 that is in flight: `verify` would then check the receipt against a
|
|
283
|
+
/// guest that did not produce it, the round would fail as a compute timeout, and
|
|
284
|
+
/// `FailurePayerLib` bills that to the requester. Change it only between rounds.
|
|
195
285
|
/// @param _imageId The new image ID.
|
|
196
286
|
function setImageId(bytes32 _imageId) external onlyOwner {
|
|
197
287
|
imageId = _imageId;
|
|
198
288
|
}
|
|
199
289
|
|
|
200
290
|
/// @notice Set the RISC Zero verifier.
|
|
291
|
+
/// @dev Carries the same in-flight risk as `setImageId`. Change it only between rounds.
|
|
201
292
|
/// @param _risc0Verifier The new RISC Zero verifier address
|
|
202
293
|
function setRisc0Verifier(IRiscZeroVerifier _risc0Verifier) external onlyOwner {
|
|
203
294
|
if (address(_risc0Verifier) == address(0)) revert Risc0VerifierAddressZero();
|
|
@@ -237,6 +328,36 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
237
328
|
numberOfVotes = round.votes.numberOfLeaves;
|
|
238
329
|
}
|
|
239
330
|
|
|
331
|
+
/// @notice The divisor applied to raw token voting power for a `CensusMode.ONCHAIN` round.
|
|
332
|
+
/// @dev A client must divide by exactly this before proving, because the contract passes the
|
|
333
|
+
/// scaled value to the circuit as public input 4 and the proof is checked against it. Zero for
|
|
334
|
+
/// rounds that are not ONCHAIN, where no scaling happens.
|
|
335
|
+
/// @param e3Id The E3 to look up.
|
|
336
|
+
/// @return The divisor recorded at validation.
|
|
337
|
+
function votingPowerDivisorOf(uint256 e3Id) external view returns (uint256) {
|
|
338
|
+
return e3Data[e3Id].votingPowerDivisor;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// @notice The voting power a slot may spend in an ONCHAIN round, in ballot units.
|
|
342
|
+
/// @dev The value `publishInput` will hand the circuit as public input 4, computed by the same
|
|
343
|
+
/// contract that will check the proof. A client must prove against exactly this: recomputing it
|
|
344
|
+
/// off-chain means re-deriving the snapshot, the divisor and the rounding, and any drift only
|
|
345
|
+
/// shows up as an opaque verifier failure. Returns 0 for a round that is not ONCHAIN, where the
|
|
346
|
+
/// bound comes from the census leaf instead.
|
|
347
|
+
///
|
|
348
|
+
/// Does not apply the eligibility floor — it answers "how much weight", not "may this slot
|
|
349
|
+
/// write". `publishInput` still enforces the floor.
|
|
350
|
+
/// @param e3Id The round.
|
|
351
|
+
/// @param slot The slot address.
|
|
352
|
+
/// @return The spendable voting power, in ballot units.
|
|
353
|
+
function votingPowerOf(uint256 e3Id, address slot) external view returns (uint256) {
|
|
354
|
+
RoundData storage round = e3Data[e3Id];
|
|
355
|
+
if (round.censusMode != CensusMode.ONCHAIN) return 0;
|
|
356
|
+
if (round.creditMode == CreditMode.CONSTANT) return round.credits;
|
|
357
|
+
|
|
358
|
+
return IVotesToken(round.token).getPastVotes(slot, round.snapshot) / round.votingPowerDivisor;
|
|
359
|
+
}
|
|
360
|
+
|
|
240
361
|
/// @notice The census source a round was requested with.
|
|
241
362
|
/// @dev A separate getter rather than a sixth return value on `getRoundData`, whose tuple is
|
|
242
363
|
/// already consumed by the server and the SDK — widening it would break them for a field most
|
|
@@ -277,10 +398,15 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
277
398
|
/// @param e3Id The E3 being configured.
|
|
278
399
|
/// @param customParams The ABI-encoded round configuration.
|
|
279
400
|
function _initRound(uint256 e3Id, bytes calldata customParams) internal {
|
|
280
|
-
(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
401
|
+
(
|
|
402
|
+
address token,
|
|
403
|
+
uint256 minVotingPower,
|
|
404
|
+
uint256 numOptions,
|
|
405
|
+
CreditMode creditMode,
|
|
406
|
+
uint256 credits,
|
|
407
|
+
uint256 rawCensusMode,
|
|
408
|
+
uint256 votingPowerDivisor
|
|
409
|
+
) = abi.decode(customParams, (address, uint256, uint256, CreditMode, uint256, uint256, uint256));
|
|
284
410
|
|
|
285
411
|
// The circuit asserts `num_options <= MAX_OPTIONS`, so a round configured above it accepts no
|
|
286
412
|
// ballot at all. Reject at request time rather than stranding a round nobody can vote in.
|
|
@@ -340,6 +466,26 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
340
466
|
}
|
|
341
467
|
|
|
342
468
|
round.snapshot = snapshot;
|
|
469
|
+
|
|
470
|
+
// Derived only after the code check above. `decimals()` on a codeless address returns empty
|
|
471
|
+
// data, and the failure happens while decoding rather than inside the call, which `try` does
|
|
472
|
+
// not catch — deriving any earlier would refuse an EOA with a bare panic instead of the
|
|
473
|
+
// named error the check above raises.
|
|
474
|
+
uint256 divisor = votingPowerDivisor == 0 ? _defaultVotingPowerDivisor(token) : votingPowerDivisor;
|
|
475
|
+
|
|
476
|
+
// Only CUSTOM credits take the circuit bound from scaled power; a CONSTANT round hands the
|
|
477
|
+
// circuit `credits` and never reads the scaled value, so the floor and the divisor have
|
|
478
|
+
// nothing to agree about there.
|
|
479
|
+
//
|
|
480
|
+
// Where they do meet, the floor is raw and the bound is scaled, so they only agree when the
|
|
481
|
+
// floor is worth at least one ballot unit. Requiring it here means every slot that passes
|
|
482
|
+
// `_eligibility` carries weight, and it costs nothing: this reverts in the transaction that
|
|
483
|
+
// requests the E3, not per input after the fee is paid. It also keeps masks working — they
|
|
484
|
+
// run the same eligibility check as real votes, so a slot that scaled to zero could not be
|
|
485
|
+
// masked without revealing which inputs were masks.
|
|
486
|
+
if (creditMode == CreditMode.CUSTOM && minVotingPower < divisor) revert MinVotingPowerBelowScale();
|
|
487
|
+
|
|
488
|
+
round.votingPowerDivisor = divisor;
|
|
343
489
|
}
|
|
344
490
|
}
|
|
345
491
|
|
|
@@ -365,21 +511,26 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
365
511
|
|
|
366
512
|
if (data.length == 0) revert EmptyInputData();
|
|
367
513
|
|
|
368
|
-
(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
514
|
+
(
|
|
515
|
+
bytes memory noirProof,
|
|
516
|
+
address slotAddress,
|
|
517
|
+
bytes32 encryptedVoteCommitment,
|
|
518
|
+
bytes memory encryptedVote,
|
|
519
|
+
uint40 parentIndexPlusOne
|
|
520
|
+
) = abi.decode(data, (bytes, address, bytes32, bytes, uint40));
|
|
372
521
|
|
|
373
522
|
// The two census families differ here and nowhere else. A Merkle round proves membership
|
|
374
523
|
// inside the circuit against a posted root. An ONCHAIN round reads the power from the token
|
|
375
524
|
// and gives it to the circuit, so the eligibility check has to happen here instead.
|
|
376
525
|
(bytes32 eligibility, IHonkVerifier verifier) = _eligibility(e3Id, slotAddress);
|
|
377
526
|
|
|
378
|
-
|
|
527
|
+
bytes32 parentCommitment = _parentCommitment(e3Id, slotAddress, parentIndexPlusOne);
|
|
528
|
+
|
|
529
|
+
uint40 voteIndex = _processVote(e3Id, slotAddress, encryptedVoteCommitment, encryptedVote, parentIndexPlusOne);
|
|
379
530
|
|
|
380
531
|
// Set the public inputs for the proof. Order must match Noir circuit.
|
|
381
532
|
bytes32[] memory noirPublicInputs = new bytes32[](9);
|
|
382
|
-
noirPublicInputs[0] =
|
|
533
|
+
noirPublicInputs[0] = parentCommitment;
|
|
383
534
|
// A Keccak digest does not fit in one field element, so it enters the circuit as its two
|
|
384
535
|
// 16-byte halves. The circuit rebuilds the 32 bytes with `digest_from_halves`.
|
|
385
536
|
{
|
|
@@ -389,7 +540,7 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
389
540
|
}
|
|
390
541
|
noirPublicInputs[3] = bytes32(uint256(uint160(slotAddress)));
|
|
391
542
|
noirPublicInputs[4] = eligibility;
|
|
392
|
-
noirPublicInputs[5] = bytes32(uint256(
|
|
543
|
+
noirPublicInputs[5] = bytes32(uint256(parentIndexPlusOne == 0 ? 1 : 0));
|
|
393
544
|
noirPublicInputs[6] = bytes32(e3Data[e3Id].numOptions);
|
|
394
545
|
noirPublicInputs[7] = encryptedVoteCommitment;
|
|
395
546
|
noirPublicInputs[8] = e3.committeePublicKey;
|
|
@@ -399,7 +550,31 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
399
550
|
revert InvalidNoirProof();
|
|
400
551
|
}
|
|
401
552
|
|
|
402
|
-
emit InputPublished(e3Id, encryptedVote, voteIndex);
|
|
553
|
+
emit InputPublished(e3Id, slotAddress, encryptedVoteCommitment, encryptedVote, voteIndex, parentIndexPlusOne);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/// @notice The commitment of the entry an input names as its parent.
|
|
557
|
+
/// @dev Zero when the input names none, which is what the circuit reads as `is_first_vote`.
|
|
558
|
+
///
|
|
559
|
+
/// Naming no parent is allowed even when the slot already holds entries, and it has to be: a
|
|
560
|
+
/// slot whose every entry is unusable has nothing to extend, and refusing that here would leave
|
|
561
|
+
/// it permanently unwritable. Nothing is gained by refusing it either — the Secure Process
|
|
562
|
+
/// accepts an entry only when its parent is the one currently selected for the slot, so an input
|
|
563
|
+
/// that skips a usable parent is dropped from the tally wherever this contract lets it through.
|
|
564
|
+
/// @param e3Id The round.
|
|
565
|
+
/// @param slotAddress The slot the input is written to.
|
|
566
|
+
/// @param parentIndexPlusOne The tree index of the parent entry plus one, or zero for none.
|
|
567
|
+
/// @return The parent's commitment, or zero.
|
|
568
|
+
function _parentCommitment(uint256 e3Id, address slotAddress, uint40 parentIndexPlusOne) internal view returns (bytes32) {
|
|
569
|
+
if (parentIndexPlusOne == 0) return bytes32(0);
|
|
570
|
+
|
|
571
|
+
bytes32 commitment = e3Data[e3Id].inputCommitment[slotAddress][parentIndexPlusOne - 1];
|
|
572
|
+
// Zero for an index this slot never wrote to, including one belonging to another slot. The
|
|
573
|
+
// circuit would read it as `is_first_vote` while this contract reads it as an update, so the
|
|
574
|
+
// two would disagree about the same input.
|
|
575
|
+
if (commitment == bytes32(0)) revert UnknownParentInput(parentIndexPlusOne - 1);
|
|
576
|
+
|
|
577
|
+
return commitment;
|
|
403
578
|
}
|
|
404
579
|
|
|
405
580
|
/// @notice Resolve the eligibility public input and the verifier for a round.
|
|
@@ -419,19 +594,48 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
419
594
|
return (bytes32(round.merkleRoot), honkVerifier);
|
|
420
595
|
}
|
|
421
596
|
|
|
422
|
-
uint256
|
|
597
|
+
uint256 rawPower = IVotesToken(round.token).getPastVotes(slotAddress, round.snapshot);
|
|
423
598
|
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
599
|
+
// The floor is compared against RAW power, in the token's own units. `minVotingPower` is a
|
|
600
|
+
// governance setting ("you need N tokens to vote") written the way every other token plugin
|
|
601
|
+
// writes it, so scaling it here would reinterpret a configured value by the divisor.
|
|
427
602
|
uint256 threshold = round.minVotingPower == 0 ? 1 : round.minVotingPower;
|
|
428
|
-
if (
|
|
603
|
+
if (rawPower < threshold) revert SlotNotEligible();
|
|
604
|
+
|
|
605
|
+
// Scaled only for the circuit. It enforces `vote <= voting_power`, and the BFV encoding caps
|
|
606
|
+
// each choice at `2**(100/numOptions) - 1` — about 8.6e9 for three options. Raw power from an
|
|
607
|
+
// 18-decimal token is ~1e18 per token, so handing it over unscaled would put every holder
|
|
608
|
+
// above the cap and collapse token weighting into a flat ceiling. Dividing mirrors what the
|
|
609
|
+
// coordinator does when it builds a Merkle census (`balance / 10**(decimals - 1)`), so both
|
|
610
|
+
// census families encode ballots in the same units and a tally decodes the same way.
|
|
611
|
+
uint256 power = rawPower / round.votingPowerDivisor;
|
|
429
612
|
|
|
430
613
|
// Eligibility comes from the power at the snapshot. The weight the circuit enforces comes from
|
|
431
614
|
// the credit mode, so a CONSTANT round gives every eligible slot the same credits.
|
|
432
615
|
return (bytes32(round.creditMode == CreditMode.CONSTANT ? round.credits : power), onchainHonkVerifier);
|
|
433
616
|
}
|
|
434
617
|
|
|
618
|
+
/// @notice The divisor to apply to raw voting power when the requester does not name one.
|
|
619
|
+
/// @dev Mirrors the coordinator's census scaling (`balance / 10**(decimals - 1)`), so an ONCHAIN
|
|
620
|
+
/// round and a Merkle round over the same token encode ballots in identical units. `decimals()`
|
|
621
|
+
/// is optional on an ERC20, so a token without it is left unscaled rather than rejected — a
|
|
622
|
+
/// requester that needs scaling for such a token passes an explicit divisor.
|
|
623
|
+
/// @param token The token voting power is read from.
|
|
624
|
+
/// @return The divisor, never zero.
|
|
625
|
+
function _defaultVotingPowerDivisor(address token) internal view returns (uint256) {
|
|
626
|
+
try IVotesToken(token).decimals() returns (uint8 dec) {
|
|
627
|
+
// `10 ** 78` does not fit in a uint256, and the exponentiation happens in the success body
|
|
628
|
+
// of the `try`, where a revert is NOT caught — an absurd `decimals` would surface as a bare
|
|
629
|
+
// arithmetic panic instead of a named error, which is the failure mode the code check above
|
|
630
|
+
// exists to avoid. Refused explicitly; such a token can still be used by naming a divisor.
|
|
631
|
+
if (dec > MAX_DERIVABLE_DECIMALS) revert UnsupportedTokenDecimals(dec);
|
|
632
|
+
|
|
633
|
+
return dec > 1 ? 10 ** (uint256(dec) - 1) : 1;
|
|
634
|
+
} catch {
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
435
639
|
/// @notice The last finalized timepoint of a token, in its ERC-6372 clock units.
|
|
436
640
|
/// @dev Falls back to block numbers when the token has no `clock()`, which matches the default
|
|
437
641
|
/// ERC20Votes clock.
|
|
@@ -487,15 +691,32 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
487
691
|
return votes;
|
|
488
692
|
}
|
|
489
693
|
|
|
490
|
-
/// @notice
|
|
694
|
+
/// @notice The index of the last input published to a slot.
|
|
695
|
+
/// @dev The last one *published*, which is not always the one that holds the slot. This contract
|
|
696
|
+
/// cannot tell whether an input's bytes deserialize to the ciphertext its commitment describes,
|
|
697
|
+
/// so the entry at this index may be one the Secure Process will never select. A client naming a
|
|
698
|
+
/// parent must resolve the chain — from the published bytes, or from the CRISP server's
|
|
699
|
+
/// `state/previous-ciphertext` — rather than reading it from here.
|
|
491
700
|
/// @param e3Id The E3 program ID
|
|
492
701
|
/// @param slotAddress The slot address
|
|
493
|
-
/// @return The
|
|
702
|
+
/// @return The index of the last published input, or -1 if the slot is empty
|
|
494
703
|
function getSlotIndex(uint256 e3Id, address slotAddress) external view returns (int40) {
|
|
495
704
|
uint40 storedIndexPlusOne = e3Data[e3Id].voteSlots[slotAddress];
|
|
496
705
|
return int40(storedIndexPlusOne) - 1;
|
|
497
706
|
}
|
|
498
707
|
|
|
708
|
+
/// @notice The commitment this contract recorded for one entry of a slot.
|
|
709
|
+
/// @dev Zero for an index this slot never wrote to. A client names a parent by index and must
|
|
710
|
+
/// prove against exactly the commitment stored for it, so this is how it checks what that will
|
|
711
|
+
/// be before proving.
|
|
712
|
+
/// @param e3Id The round.
|
|
713
|
+
/// @param slotAddress The slot address.
|
|
714
|
+
/// @param index The tree index of the entry.
|
|
715
|
+
/// @return The stored commitment, or zero when there is no such entry for this slot.
|
|
716
|
+
function inputCommitmentOf(uint256 e3Id, address slotAddress, uint40 index) external view returns (bytes32) {
|
|
717
|
+
return e3Data[e3Id].inputCommitment[slotAddress][index];
|
|
718
|
+
}
|
|
719
|
+
|
|
499
720
|
/// @inheritdoc IE3Program
|
|
500
721
|
function verify(
|
|
501
722
|
uint256 e3Id,
|
|
@@ -525,29 +746,56 @@ contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
|
525
746
|
return true;
|
|
526
747
|
}
|
|
527
748
|
|
|
528
|
-
/// @notice
|
|
529
|
-
/// on whether it's the first vote or an override.
|
|
749
|
+
/// @notice Record one input: append its leaf and remember its commitment for later parents.
|
|
530
750
|
function _processVote(
|
|
531
751
|
uint256 e3Id,
|
|
532
752
|
address slotAddress,
|
|
533
|
-
bytes32 encryptedVoteCommitment
|
|
534
|
-
|
|
535
|
-
uint40
|
|
753
|
+
bytes32 encryptedVoteCommitment,
|
|
754
|
+
bytes memory encryptedVote,
|
|
755
|
+
uint40 parentIndexPlusOne
|
|
756
|
+
) internal returns (uint40 voteIndex) {
|
|
757
|
+
RoundData storage round = e3Data[e3Id];
|
|
536
758
|
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
759
|
+
// Append-only. Updating a slot's leaf in place would let anyone who can write to a slot — and
|
|
760
|
+
// the mask path needs no signature — replace the bytes of a vote that was already counted,
|
|
761
|
+
// erasing it. Appending leaves the earlier entry in the tree, so the Secure Process can fall
|
|
762
|
+
// back to it when a later entry is unusable, and nothing is lost.
|
|
763
|
+
uint256 leaf = inputLeaf(encryptedVote, encryptedVoteCommitment, slotAddress, parentIndexPlusOne);
|
|
764
|
+
|
|
765
|
+
// Refuse a byte-identical resubmission. Without this the tree is a free growth surface for
|
|
766
|
+
// anyone replaying a published input, and the round dies at tree capacity rather than at the
|
|
767
|
+
// input deadline.
|
|
768
|
+
if (round.appendedLeaf[leaf]) revert InputAlreadyPublished(leaf);
|
|
769
|
+
round.appendedLeaf[leaf] = true;
|
|
770
|
+
|
|
771
|
+
voteIndex = round.votes.numberOfLeaves;
|
|
772
|
+
round.votes._insert(leaf);
|
|
773
|
+
|
|
774
|
+
round.voteSlots[slotAddress] = voteIndex + 1;
|
|
775
|
+
round.inputCommitment[slotAddress][voteIndex] = encryptedVoteCommitment;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/// @notice Builds the input tree leaf for one published input.
|
|
779
|
+
/// @dev Binds four things the Secure Process must be able to trust:
|
|
780
|
+
///
|
|
781
|
+
/// - the **bytes**, because the Noir proof constrains the commitment and never sees the
|
|
782
|
+
/// serialized ciphertext, so the two can disagree and only the guest can tell;
|
|
783
|
+
/// - the **commitment**, so a submitter cannot pair any commitment with any ciphertext;
|
|
784
|
+
/// - the **slot**, because the tree is append-only and the guest tallies one entry per slot.
|
|
785
|
+
/// Without the slot in the leaf a prover could re-group entries and change which one wins;
|
|
786
|
+
/// - the **parent**, because that is what the guest walks the slot's chain by. An unbound parent
|
|
787
|
+
/// would let a prover re-point entries and select a different one.
|
|
788
|
+
///
|
|
789
|
+
/// SHA-256 rather than Keccak: the zkVM accelerates SHA-256 inline, while its Keccak accelerator
|
|
790
|
+
/// emits a proof assumption the host must prove separately and compose. The extra on-chain cost
|
|
791
|
+
/// is about 67k gas on a transaction that already carries the ciphertext.
|
|
792
|
+
function inputLeaf(
|
|
793
|
+
bytes memory encryptedVote,
|
|
794
|
+
bytes32 commitment,
|
|
795
|
+
address slotAddress,
|
|
796
|
+
uint40 parentIndexPlusOne
|
|
797
|
+
) public pure returns (uint256) {
|
|
798
|
+
return uint256(sha256(abi.encodePacked(sha256(encryptedVote), commitment, slotAddress, parentIndexPlusOne))) % SNARK_SCALAR_FIELD;
|
|
551
799
|
}
|
|
552
800
|
|
|
553
801
|
/// @notice Decode bytes to uint64 array
|
|
@@ -19,6 +19,11 @@ contract MockInterfold {
|
|
|
19
19
|
uint256 public nextE3Id;
|
|
20
20
|
|
|
21
21
|
mapping(uint256 => E3) public e3s;
|
|
22
|
+
mapping(IE3Program => bool) public e3Programs;
|
|
23
|
+
|
|
24
|
+
function registerE3Program(IE3Program program) external {
|
|
25
|
+
e3Programs[program] = true;
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
function request(address program) external {
|
|
24
29
|
_request(program, 2);
|
|
@@ -68,7 +73,7 @@ contract MockInterfold {
|
|
|
68
73
|
encryptionSchemeId: ENCRYPTION_SCHEME_ID,
|
|
69
74
|
e3Program: IE3Program(address(0)),
|
|
70
75
|
paramSet: 0, // Insecure512
|
|
71
|
-
customParams: abi.encode(address(0), nextE3Id, numOptions, 0, 0, 0),
|
|
76
|
+
customParams: abi.encode(address(0), nextE3Id, numOptions, 0, 0, 0, 0),
|
|
72
77
|
decryptionVerifier: IDecryptionVerifier(address(0)),
|
|
73
78
|
pkVerifier: IPkVerifier(address(0)),
|
|
74
79
|
committeePublicKey: committeePublicKey,
|
|
@@ -78,7 +83,7 @@ contract MockInterfold {
|
|
|
78
83
|
ciphertextCommitment: bytes32(0)
|
|
79
84
|
});
|
|
80
85
|
|
|
81
|
-
IE3Program(program).validate(nextE3Id, 0, bytes(""), bytes(""), abi.encode(address(0), nextE3Id, numOptions, 0, 0, 0));
|
|
86
|
+
IE3Program(program).validate(nextE3Id, 0, bytes(""), bytes(""), abi.encode(address(0), nextE3Id, numOptions, 0, 0, 0, 0));
|
|
82
87
|
|
|
83
88
|
nextE3Id++;
|
|
84
89
|
}
|
|
@@ -105,7 +110,7 @@ contract MockInterfold {
|
|
|
105
110
|
encryptionSchemeId: ENCRYPTION_SCHEME_ID,
|
|
106
111
|
e3Program: IE3Program(address(0)),
|
|
107
112
|
paramSet: 0, // Insecure512
|
|
108
|
-
customParams: abi.encode(address(0), 0, 2, 0, 0, 0),
|
|
113
|
+
customParams: abi.encode(address(0), 0, 2, 0, 0, 0, 0),
|
|
109
114
|
decryptionVerifier: IDecryptionVerifier(address(0)),
|
|
110
115
|
pkVerifier: IPkVerifier(address(0)),
|
|
111
116
|
committeePublicKey: committeePublicKey,
|
|
@@ -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
|
+
}
|
|
@@ -7,8 +7,10 @@ pragma solidity >=0.8.27;
|
|
|
7
7
|
|
|
8
8
|
/// @notice The subset of a generated Honk verifier that `CRISPProgram` calls.
|
|
9
9
|
/// @dev Declared as an interface so the census paths can hold verifiers generated from different
|
|
10
|
-
/// circuits.
|
|
11
|
-
/// `
|
|
10
|
+
/// circuits. Every generated verifier declares a contract named `HonkVerifier`, and there is one
|
|
11
|
+
/// per census mode per BFV preset under `contracts/verifiers/<preset>/`, so importing the concrete
|
|
12
|
+
/// types would collide. Deployment resolves the right one by fully qualified name; see
|
|
13
|
+
/// `scripts/verifiers.ts`.
|
|
12
14
|
interface IHonkVerifier {
|
|
13
15
|
/// @notice Verify a folded ballot proof against its public inputs.
|
|
14
16
|
/// @dev Reverts rather than returning false when the public inputs do not match the proof.
|
|
@@ -14,4 +14,10 @@ interface IVotesToken {
|
|
|
14
14
|
/// @param timepoint The timepoint, in the ERC-6372 clock units of the token.
|
|
15
15
|
/// @return The voting power at that timepoint.
|
|
16
16
|
function getPastVotes(address account, uint256 timepoint) external view returns (uint256);
|
|
17
|
+
|
|
18
|
+
/// @notice The token's decimals, used to derive the default voting-power divisor.
|
|
19
|
+
/// @dev Optional: a token without it falls back to a divisor of 1, so the probe must not be
|
|
20
|
+
/// treated as a requirement the way `getPastVotes` is.
|
|
21
|
+
/// @return The number of decimals.
|
|
22
|
+
function decimals() external view returns (uint8);
|
|
17
23
|
}
|
|
@@ -8,7 +8,7 @@ pragma solidity >=0.8.21;
|
|
|
8
8
|
uint256 constant N = 2097152;
|
|
9
9
|
uint256 constant LOG_N = 21;
|
|
10
10
|
uint256 constant NUMBER_OF_PUBLIC_INPUTS = 17;
|
|
11
|
-
uint256 constant VK_HASH =
|
|
11
|
+
uint256 constant VK_HASH = 0x0a12a4d2efe48fe7bee586e156eb7cb5d2d7dc71bccf0fa4b4a9b91c097b4c8c;
|
|
12
12
|
library HonkVerificationKey {
|
|
13
13
|
function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) {
|
|
14
14
|
Honk.VerificationKey memory vk = Honk.VerificationKey({
|
|
@@ -20,8 +20,8 @@ library HonkVerificationKey {
|
|
|
20
20
|
y: uint256(0x01137e39f6b1ec6101fad7a4472102bfe11e7534e55e58109734c4b927efe6a6)
|
|
21
21
|
}),
|
|
22
22
|
qr: Honk.G1Point({
|
|
23
|
-
x: uint256(
|
|
24
|
-
y: uint256(
|
|
23
|
+
x: uint256(0x2ac08692659e39f2a9f8cff82c54221ef67e138279c03ffbe9cba0976cb5c5bb),
|
|
24
|
+
y: uint256(0x18d2a25f4040b02577dcec145c99e389c578119bc9652bd2e07cf1e6ac2edbd9)
|
|
25
25
|
}),
|
|
26
26
|
qo: Honk.G1Point({
|
|
27
27
|
x: uint256(0x18424976826978ddb710bfdc21c64a296884c27b0d8dadf6ac7681445dde8d8c),
|
|
@@ -8,7 +8,7 @@ pragma solidity >=0.8.21;
|
|
|
8
8
|
uint256 constant N = 2097152;
|
|
9
9
|
uint256 constant LOG_N = 21;
|
|
10
10
|
uint256 constant NUMBER_OF_PUBLIC_INPUTS = 17;
|
|
11
|
-
uint256 constant VK_HASH =
|
|
11
|
+
uint256 constant VK_HASH = 0x00d736cd7cf93a94fe0fa02be1bd85f7ea05d3bfa1697f95456c05a7cce354ce;
|
|
12
12
|
library HonkVerificationKey {
|
|
13
13
|
function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) {
|
|
14
14
|
Honk.VerificationKey memory vk = Honk.VerificationKey({
|
|
@@ -20,8 +20,8 @@ library HonkVerificationKey {
|
|
|
20
20
|
y: uint256(0x01137e39f6b1ec6101fad7a4472102bfe11e7534e55e58109734c4b927efe6a6)
|
|
21
21
|
}),
|
|
22
22
|
qr: Honk.G1Point({
|
|
23
|
-
x: uint256(
|
|
24
|
-
y: uint256(
|
|
23
|
+
x: uint256(0x048bf2f40b9f0e61917b3bd9d156debaa531af8449eebbb46c6ba23cb0de4ae2),
|
|
24
|
+
y: uint256(0x25842efe61b33504731c38e50c899a32ec606172705f7a3c74ca1427d83d0fe4)
|
|
25
25
|
}),
|
|
26
26
|
qo: Honk.G1Point({
|
|
27
27
|
x: uint256(0x18424976826978ddb710bfdc21c64a296884c27b0d8dadf6ac7681445dde8d8c),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crisp-e3/contracts",
|
|
3
|
-
"version": "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.
|
|
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/sdk": "^0.
|
|
57
|
-
"@crisp-e3/zk-inputs": "^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,8 +66,16 @@
|
|
|
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",
|
|
71
|
+
"check:test-legs": "node scripts/check-test-legs.mjs",
|
|
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",
|
|
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",
|
|
76
|
+
"test:ballots": "hardhat test mocha tests/crisp.contracts.test.ts tests/onchain-census.test.ts",
|
|
70
77
|
"verify": "hardhat run deploy/verify.ts",
|
|
71
|
-
"updateSubmissionWindow": "hardhat ciphernode:window"
|
|
78
|
+
"updateSubmissionWindow": "hardhat ciphernode:window",
|
|
79
|
+
"check:presets": "node ../../scripts/check-presets.mjs"
|
|
72
80
|
}
|
|
73
81
|
}
|