@crisp-e3/contracts 0.16.0 → 0.17.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.
@@ -12,9 +12,12 @@ 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 { HonkVerifier } from "./CRISPVerifier.sol";
15
+ import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
16
+ import { IHonkVerifier } from "./interfaces/IHonkVerifier.sol";
17
+ import { IVotesToken } from "./interfaces/IVotesToken.sol";
18
+ import { IERC6372Clock } from "./interfaces/IERC6372Clock.sol";
16
19
 
17
- contract CRISPProgram is IE3Program, Ownable {
20
+ contract CRISPProgram is IE3Program, Ownable, EIP712 {
18
21
  using InternalLazyIMT for LazyIMTData;
19
22
 
20
23
  /// @notice Enum to represent credit modes
@@ -43,7 +46,12 @@ contract CRISPProgram is IE3Program, Ownable {
43
46
  /// @notice Derived from token balances by the coordinator. The default.
44
47
  TOKEN,
45
48
  /// @notice Supplied by the requester via `getCensus(uint256 e3Id) returns (address[])`.
46
- BY_REQUESTER
49
+ BY_REQUESTER,
50
+ /// @notice Read from the token by this contract, one input at a time. No list is enumerated
51
+ /// and no root is posted, so there is no census producer to trust. `publishInput` calls
52
+ /// `getPastVotes` for the slot and passes the result to the circuit as a public input, which
53
+ /// is why this mode uses the `crisp_onchain` verifier rather than the `crisp` one.
54
+ ONCHAIN
47
55
  }
48
56
 
49
57
  /// @notice Struct to store all data related to a voting round
@@ -55,6 +63,16 @@ contract CRISPProgram is IE3Program, Ownable {
55
63
  uint256 numOptions;
56
64
  CreditMode creditMode;
57
65
  CensusMode censusMode;
66
+ /// @notice The token that voting power is read from. Only used by `CensusMode.ONCHAIN`.
67
+ address token;
68
+ /// @notice The smallest voting power that may cast an input. Only used by
69
+ /// `CensusMode.ONCHAIN`, where eligibility is checked per input instead of by a census.
70
+ uint256 minVotingPower;
71
+ /// @notice Credits given to each eligible voter under `CreditMode.CONSTANT`.
72
+ uint256 credits;
73
+ /// @notice The timepoint that voting power is read at, in the ERC-6372 clock units of the
74
+ /// token. Recorded when the round is requested, so it is the same for every input.
75
+ uint48 snapshot;
58
76
  }
59
77
 
60
78
  // Constants
@@ -75,7 +93,17 @@ contract CRISPProgram is IE3Program, Ownable {
75
93
  IInterfold public interfold;
76
94
  IRiscZeroVerifier public risc0Verifier;
77
95
  bytes32 public imageId;
78
- HonkVerifier private immutable honkVerifier;
96
+ /// @notice Verifies ballots for the census modes that prove membership of a Merkle tree.
97
+ IHonkVerifier private immutable honkVerifier;
98
+ /// @notice Verifies ballots for `CensusMode.ONCHAIN`, whose circuit has no Merkle inputs and
99
+ /// takes voting power as a public input instead.
100
+ IHonkVerifier private immutable onchainHonkVerifier;
101
+
102
+ /// @notice The EIP-712 type of the message a voter signs to authorise one ballot.
103
+ /// @dev The digest binds the signature to the round, the slot, and this exact ciphertext. The
104
+ /// chain id and this contract address come from the EIP-712 domain, so a signature cannot be
105
+ /// replayed onto another round, another slot, another ballot, or another deployment.
106
+ bytes32 private constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 e3Id,address slot,bytes32 ciphertextCommitment)");
79
107
 
80
108
  // Mappings
81
109
  mapping(uint256 e3Id => RoundData) e3Data;
@@ -94,6 +122,16 @@ contract CRISPProgram is IE3Program, Ownable {
94
122
  /// @notice A requester-supplied census names who may vote, not how much each vote weighs, so it
95
123
  /// only has meaning when every voter carries the same credits.
96
124
  error CensusModeRequiresConstantCredits();
125
+ /// @notice `CensusMode.ONCHAIN` reads voting power from a token, so it cannot run without one
126
+ /// that answers `getPastVotes`.
127
+ error CensusModeRequiresToken();
128
+ /// @notice An `ONCHAIN` round with constant credits must grant a non-zero allowance, or it
129
+ /// bounds every ballot to zero and can only accept masks.
130
+ error InvalidCredits();
131
+ /// @notice The slot holds less voting power than the round requires, so it cannot be written to.
132
+ /// @dev Raised for mask inputs as well as real votes. Under `CensusMode.ONCHAIN` this check
133
+ /// replaces the Merkle membership proof that gates both branches in the other modes.
134
+ error SlotNotEligible();
97
135
  error InvalidCensusMode();
98
136
  error SlotIsEmpty();
99
137
  error MerkleRootNotSet();
@@ -111,17 +149,38 @@ contract CRISPProgram is IE3Program, Ownable {
111
149
  /// @param _risc0Verifier The RISC Zero verifier address
112
150
  /// @param _honkVerifier The honk verifier address
113
151
  /// @param _imageId The image ID for the guest program
114
- constructor(IInterfold _interfold, IRiscZeroVerifier _risc0Verifier, HonkVerifier _honkVerifier, bytes32 _imageId) Ownable(msg.sender) {
152
+ constructor(
153
+ IInterfold _interfold,
154
+ IRiscZeroVerifier _risc0Verifier,
155
+ IHonkVerifier _honkVerifier,
156
+ IHonkVerifier _onchainHonkVerifier,
157
+ bytes32 _imageId
158
+ ) Ownable(msg.sender) EIP712("CRISP", "1") {
115
159
  if (address(_interfold) == address(0)) revert InterfoldAddressZero();
116
160
  if (address(_risc0Verifier) == address(0)) revert Risc0VerifierAddressZero();
117
161
  if (address(_honkVerifier) == address(0)) revert InvalidHonkVerifier();
162
+ if (address(_onchainHonkVerifier) == address(0)) revert InvalidHonkVerifier();
118
163
 
119
164
  interfold = _interfold;
120
165
  risc0Verifier = _risc0Verifier;
121
166
  honkVerifier = _honkVerifier;
167
+ onchainHonkVerifier = _onchainHonkVerifier;
122
168
  imageId = _imageId;
123
169
  }
124
170
 
171
+ /// @notice The digest a voter signs to authorise one ballot.
172
+ /// @dev Computed for every input, and for mask inputs as well as real votes. The circuit ignores
173
+ /// it on the mask branch, but the contract must not skip it: a digest that were computed only
174
+ /// for real votes would let an observer tell the two apart on-chain, which is exactly what mask
175
+ /// inputs exist to prevent.
176
+ /// @param e3Id The E3 the ballot belongs to.
177
+ /// @param slot The slot address the ballot is written to.
178
+ /// @param ciphertextCommitment The commitment to the ballot ciphertext.
179
+ /// @return The EIP-712 digest.
180
+ function ballotDigest(uint256 e3Id, address slot, bytes32 ciphertextCommitment) public view returns (bytes32) {
181
+ return _hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, e3Id, slot, ciphertextCommitment)));
182
+ }
183
+
125
184
  /// @notice Sets the Merkle root for an E3 program. Can only be set once.
126
185
  /// @param _e3Id The E3 program ID
127
186
  /// @param _root The Merkle root to set.
@@ -199,34 +258,9 @@ contract CRISPProgram is IE3Program, Ownable {
199
258
  if (msg.sender != address(interfold) && msg.sender != owner()) revert CallerNotAuthorized();
200
259
  if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized();
201
260
 
202
- // Scoped so the decoded values do not outlive their use: `validate` is close enough to the
203
- // stack limit that holding all six of them alongside the parameters exceeds it.
204
- {
205
- // One decode, every field required. `censusMode` is read as a uint and range-checked rather
206
- // than decoded straight into the enum, so an unrecognised value gives a named error instead
207
- // of a bare panic.
208
- (, , uint256 numOptions, CreditMode creditMode, , uint256 rawCensusMode) = abi.decode(
209
- customParams,
210
- (address, uint256, uint256, CreditMode, uint256, uint256)
211
- );
212
- // The circuit asserts `num_options <= MAX_OPTIONS`, so a round configured above it accepts no
213
- // ballot at all. Reject at request time rather than stranding a round nobody can vote in.
214
- if (numOptions < 2 || numOptions > MAX_VOTE_OPTIONS) revert InvalidNumOptions();
215
- if (rawCensusMode > uint256(type(CensusMode).max)) revert InvalidCensusMode();
216
-
217
- // Rejected here rather than by the coordinator, so a combination that can never work costs
218
- // nothing: this reverts in the same transaction that requests the E3, before any fee is paid.
219
- if (CensusMode(rawCensusMode) == CensusMode.BY_REQUESTER && creditMode != CreditMode.CONSTANT) {
220
- revert CensusModeRequiresConstantCredits();
221
- }
222
-
223
- // we need to know the number of options for decoding the tally
224
- e3Data[e3Id].numOptions = numOptions;
225
- // we want to save the credit mode so it can be verified on chain by everyone
226
- e3Data[e3Id].creditMode = creditMode;
227
- // recorded so anyone can verify which electorate the round was requested against
228
- e3Data[e3Id].censusMode = CensusMode(rawCensusMode);
229
- }
261
+ // Delegated to its own frame rather than scoped inline: `validate` is close enough to the
262
+ // stack limit that holding the six decoded values alongside the parameters exceeds it.
263
+ _initRound(e3Id, customParams);
230
264
 
231
265
  e3Data[e3Id].paramsHash = keccak256(e3ProgramParams);
232
266
 
@@ -236,6 +270,79 @@ contract CRISPProgram is IE3Program, Ownable {
236
270
  return ENCRYPTION_SCHEME_ID;
237
271
  }
238
272
 
273
+ /// @notice Decode the round configuration and record it.
274
+ /// @dev One decode, every field required. `censusMode` is read as a uint and range-checked
275
+ /// rather than decoded straight into the enum, so an unrecognised value gives a named error
276
+ /// instead of a bare panic.
277
+ /// @param e3Id The E3 being configured.
278
+ /// @param customParams The ABI-encoded round configuration.
279
+ function _initRound(uint256 e3Id, bytes calldata customParams) internal {
280
+ (address token, uint256 minVotingPower, uint256 numOptions, CreditMode creditMode, uint256 credits, uint256 rawCensusMode) = abi.decode(
281
+ customParams,
282
+ (address, uint256, uint256, CreditMode, uint256, uint256)
283
+ );
284
+
285
+ // The circuit asserts `num_options <= MAX_OPTIONS`, so a round configured above it accepts no
286
+ // ballot at all. Reject at request time rather than stranding a round nobody can vote in.
287
+ if (numOptions < 2 || numOptions > MAX_VOTE_OPTIONS) revert InvalidNumOptions();
288
+ if (rawCensusMode > uint256(type(CensusMode).max)) revert InvalidCensusMode();
289
+
290
+ // Rejected here rather than by the coordinator, so a combination that can never work costs
291
+ // nothing: this reverts in the same transaction that requests the E3, before any fee is paid.
292
+ if (CensusMode(rawCensusMode) == CensusMode.BY_REQUESTER && creditMode != CreditMode.CONSTANT) {
293
+ revert CensusModeRequiresConstantCredits();
294
+ }
295
+
296
+ // ONCHAIN reads every voter's power from this token, so a round without one accepts no ballot
297
+ // at all. Same reasoning as the numOptions bound: fail before the fee is paid.
298
+ if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN && token == address(0)) {
299
+ revert CensusModeRequiresToken();
300
+ }
301
+
302
+ // An ONCHAIN round hands `credits` to the circuit as the voting-power bound, so zero credits
303
+ // bound every ballot to zero: only a mask would be accepted, and the round would tally
304
+ // nothing. Checked for ONCHAIN only — the Merkle modes take the bound from the census leaf,
305
+ // where `credits` never reaches the circuit and the contract has nothing to check.
306
+ if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN && creditMode == CreditMode.CONSTANT && credits == 0) {
307
+ revert InvalidCredits();
308
+ }
309
+
310
+ RoundData storage round = e3Data[e3Id];
311
+ // we need to know the number of options for decoding the tally
312
+ round.numOptions = numOptions;
313
+ // we want to save the credit mode so it can be verified on chain by everyone
314
+ round.creditMode = creditMode;
315
+ // recorded so anyone can verify which electorate the round was requested against
316
+ round.censusMode = CensusMode(rawCensusMode);
317
+ round.token = token;
318
+ round.minVotingPower = minVotingPower;
319
+ round.credits = credits;
320
+
321
+ // The snapshot is taken here rather than supplied by the requester. This function runs in the
322
+ // transaction that requests the E3, so `clock() - 1` is the last finalized timepoint of the
323
+ // round, and a requester cannot name a timepoint that suits it. Recording it once also makes
324
+ // every input of the round read the same electorate.
325
+ if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN) {
326
+ // Checked before any call is attempted. A call to an address with no code succeeds and
327
+ // returns nothing, so `clock()` fails while decoding the empty return data rather than
328
+ // inside the call — and a decode failure is not what `try/catch` is there to catch. An EOA
329
+ // would otherwise be refused by a bare panic instead of a named error.
330
+ if (token.code.length == 0) revert CensusModeRequiresToken();
331
+
332
+ uint48 snapshot = _previousTimepoint(token);
333
+
334
+ // Probe the exact call every input will make. `_previousTimepoint` swallows a missing
335
+ // `clock()` and falls back to block numbers, which is right for a token that predates
336
+ // ERC-6372 but also lets an address that is not a votes token pass validation — and then
337
+ // every `publishInput` reverts inside `getPastVotes`, after the fee is paid.
338
+ try IVotesToken(token).getPastVotes(address(0), snapshot) returns (uint256) {} catch {
339
+ revert CensusModeRequiresToken();
340
+ }
341
+
342
+ round.snapshot = snapshot;
343
+ }
344
+ }
345
+
239
346
  /// @inheritdoc IE3Program
240
347
  function publishInput(uint256 e3Id, bytes memory data) external {
241
348
  E3 memory e3 = interfold.getE3(e3Id);
@@ -256,9 +363,6 @@ contract CRISPProgram is IE3Program, Ownable {
256
363
  revert E3NotAcceptingInputs(e3Id);
257
364
  }
258
365
 
259
- // We need to ensure that the CRISP admin set the merkle root of the census.
260
- if (e3Data[e3Id].merkleRoot == 0) revert MerkleRootNotSet();
261
-
262
366
  if (data.length == 0) revert EmptyInputData();
263
367
 
264
368
  (bytes memory noirProof, address slotAddress, bytes32 encryptedVoteCommitment, bytes memory encryptedVote) = abi.decode(
@@ -266,26 +370,81 @@ contract CRISPProgram is IE3Program, Ownable {
266
370
  (bytes, address, bytes32, bytes)
267
371
  );
268
372
 
373
+ // The two census families differ here and nowhere else. A Merkle round proves membership
374
+ // inside the circuit against a posted root. An ONCHAIN round reads the power from the token
375
+ // and gives it to the circuit, so the eligibility check has to happen here instead.
376
+ (bytes32 eligibility, IHonkVerifier verifier) = _eligibility(e3Id, slotAddress);
377
+
269
378
  (uint40 voteIndex, bytes32 previousEncryptedVoteCommitment) = _processVote(e3Id, slotAddress, encryptedVoteCommitment);
270
379
 
271
380
  // Set the public inputs for the proof. Order must match Noir circuit.
272
- bytes32[] memory noirPublicInputs = new bytes32[](7);
381
+ bytes32[] memory noirPublicInputs = new bytes32[](9);
273
382
  noirPublicInputs[0] = previousEncryptedVoteCommitment;
274
- noirPublicInputs[1] = bytes32(e3Data[e3Id].merkleRoot);
275
- noirPublicInputs[2] = bytes32(uint256(uint160(slotAddress)));
276
- noirPublicInputs[3] = bytes32(uint256(previousEncryptedVoteCommitment == bytes32(0) ? 1 : 0));
277
- noirPublicInputs[4] = bytes32(e3Data[e3Id].numOptions);
278
- noirPublicInputs[5] = encryptedVoteCommitment;
279
- noirPublicInputs[6] = e3.committeePublicKey;
383
+ // A Keccak digest does not fit in one field element, so it enters the circuit as its two
384
+ // 16-byte halves. The circuit rebuilds the 32 bytes with `digest_from_halves`.
385
+ {
386
+ uint256 digest = uint256(ballotDigest(e3Id, slotAddress, encryptedVoteCommitment));
387
+ noirPublicInputs[1] = bytes32(digest >> 128);
388
+ noirPublicInputs[2] = bytes32(digest & type(uint128).max);
389
+ }
390
+ noirPublicInputs[3] = bytes32(uint256(uint160(slotAddress)));
391
+ noirPublicInputs[4] = eligibility;
392
+ noirPublicInputs[5] = bytes32(uint256(previousEncryptedVoteCommitment == bytes32(0) ? 1 : 0));
393
+ noirPublicInputs[6] = bytes32(e3Data[e3Id].numOptions);
394
+ noirPublicInputs[7] = encryptedVoteCommitment;
395
+ noirPublicInputs[8] = e3.committeePublicKey;
280
396
 
281
397
  // Check if the ciphertext was encrypted correctly
282
- if (!honkVerifier.verify(noirProof, noirPublicInputs)) {
398
+ if (!verifier.verify(noirProof, noirPublicInputs)) {
283
399
  revert InvalidNoirProof();
284
400
  }
285
401
 
286
402
  emit InputPublished(e3Id, encryptedVote, voteIndex);
287
403
  }
288
404
 
405
+ /// @notice Resolve the eligibility public input and the verifier for a round.
406
+ /// @dev Returns the value that occupies index 4 of the circuit public inputs. The `crisp` and
407
+ /// `crisp_onchain` circuits agree on every other position, so this one value and the verifier
408
+ /// address are the whole difference between the two paths.
409
+ /// @param e3Id The E3 the input belongs to.
410
+ /// @param slotAddress The slot the input is written to.
411
+ /// @return eligibility The Merkle root of the census, or the voting power of the slot.
412
+ /// @return verifier The verifier that matches the circuit of this round.
413
+ function _eligibility(uint256 e3Id, address slotAddress) internal view returns (bytes32 eligibility, IHonkVerifier verifier) {
414
+ RoundData storage round = e3Data[e3Id];
415
+
416
+ if (round.censusMode != CensusMode.ONCHAIN) {
417
+ // We need to ensure that the CRISP admin set the merkle root of the census.
418
+ if (round.merkleRoot == 0) revert MerkleRootNotSet();
419
+ return (bytes32(round.merkleRoot), honkVerifier);
420
+ }
421
+
422
+ uint256 power = IVotesToken(round.token).getPastVotes(slotAddress, round.snapshot);
423
+
424
+ // A slot with no power is never writable, whatever the round configured. Without this floor a
425
+ // round with `minVotingPower == 0` would let anyone mask any address, including addresses that
426
+ // never held the token.
427
+ uint256 threshold = round.minVotingPower == 0 ? 1 : round.minVotingPower;
428
+ if (power < threshold) revert SlotNotEligible();
429
+
430
+ // Eligibility comes from the power at the snapshot. The weight the circuit enforces comes from
431
+ // the credit mode, so a CONSTANT round gives every eligible slot the same credits.
432
+ return (bytes32(round.creditMode == CreditMode.CONSTANT ? round.credits : power), onchainHonkVerifier);
433
+ }
434
+
435
+ /// @notice The last finalized timepoint of a token, in its ERC-6372 clock units.
436
+ /// @dev Falls back to block numbers when the token has no `clock()`, which matches the default
437
+ /// ERC20Votes clock.
438
+ /// @param token The token to read the clock of.
439
+ /// @return The timepoint before the current one.
440
+ function _previousTimepoint(address token) internal view returns (uint48) {
441
+ try IERC6372Clock(token).clock() returns (uint48 current) {
442
+ return current == 0 ? 0 : current - 1;
443
+ } catch {
444
+ return uint48(block.number - 1);
445
+ }
446
+ }
447
+
289
448
  /// @notice Decode the tally from the plaintext output
290
449
  /// @param e3Id The E3 program ID
291
450
  /// @return votes - an array of vote counts for each option
@@ -7,85 +7,85 @@ pragma solidity >=0.8.21;
7
7
 
8
8
  uint256 constant N = 2097152;
9
9
  uint256 constant LOG_N = 21;
10
- uint256 constant NUMBER_OF_PUBLIC_INPUTS = 15;
11
- uint256 constant VK_HASH = 0x0a7642f3693429035e756c034137ba93ed0bc3e7d3f370e24143d7e52a7aabc4;
10
+ uint256 constant NUMBER_OF_PUBLIC_INPUTS = 17;
11
+ uint256 constant VK_HASH = 0x1d0e6ec8f2fe720a1f528f02a66b66c1479c1648afaaedec06d89b889a0ff0c2;
12
12
  library HonkVerificationKey {
13
13
  function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) {
14
14
  Honk.VerificationKey memory vk = Honk.VerificationKey({
15
15
  circuitSize: uint256(2097152),
16
16
  logCircuitSize: uint256(21),
17
- publicInputsSize: uint256(15),
17
+ publicInputsSize: uint256(17),
18
18
  ql: Honk.G1Point({
19
- x: uint256(0x14493dc1077b2f351879feecaa600ca60f7ce233869e11f274bb6fc7d31427dd),
20
- y: uint256(0x03269bd3e0e7fcd095c3a6eaddf8e30e33ca383497034c7e028ddc372df027da)
19
+ x: uint256(0x2089549d27c8b4fa58dd4ed5276c42ccc9830ee5cd64cf877df0301a02d54e62),
20
+ y: uint256(0x01137e39f6b1ec6101fad7a4472102bfe11e7534e55e58109734c4b927efe6a6)
21
21
  }),
22
22
  qr: Honk.G1Point({
23
- x: uint256(0x1fa18580baf7ded1c161e13ca6a20c5670ef984a3396979faf7e0a656387e527),
24
- y: uint256(0x2e94ddf45aecdaee626a6eeded33d0feda0b05fe0681f3ee3d6340a4a8c10896)
23
+ x: uint256(0x0c9d534606fb88b2814caaec28c8f0ccd9f903b2236fe399ce723cb7a89cff54),
24
+ y: uint256(0x12d1f3a930e1c9be768e98d5ed2c5c74b3bd1bce2f50ce042cde7270f8032df1)
25
25
  }),
26
26
  qo: Honk.G1Point({
27
- x: uint256(0x092b0d4c826cbe5e07c1e19020272e116ff633dc953763c357f5fd841722123e),
28
- y: uint256(0x2c32eb8ab851dfe95645d6e6c15d5074c4e3d179d58aa0ed636f7dfafabecede)
27
+ x: uint256(0x18424976826978ddb710bfdc21c64a296884c27b0d8dadf6ac7681445dde8d8c),
28
+ y: uint256(0x1690625dd2a2a6afa259cfe9dc7601058bd3275e8fcea82e008caa319c2710b4)
29
29
  }),
30
30
  q4: Honk.G1Point({
31
- x: uint256(0x25306c50d5acbe8b63f31114c2a33abd494ee69eaf0d83dfd5a2a77c6c7694dc),
32
- y: uint256(0x1a86fb908a0a332216ad4e0651be7f73f720d402f00ce3e8b32e94b89f25087f)
31
+ x: uint256(0x025aeec2f923a237df2c5ddfe9bc1af74bdaf41d2fe47aa4c0cb12dc4cafac76),
32
+ y: uint256(0x0a3ecdb966ed757ad3acc3e2ad670b804b582686b85bdad85ebe18713cddd85e)
33
33
  }),
34
34
  qm: Honk.G1Point({
35
- x: uint256(0x26516743e498c0fd06b97618fb55478fd29807b4d6305ca3bb2d7b506264fd6f),
36
- y: uint256(0x10bb22b28f6420cea07563e6114b960dd9bdbf203ad39a4bed6b42d014fff8f9)
35
+ x: uint256(0x1b562d7113fa8ac2dec528dfcec26f0c0b1290f9a0fa7cd08ee8492b7f8ed657),
36
+ y: uint256(0x188c0bdd795dda2f441f73f4a93919cf28417e039c491966c4305582edfcf63f)
37
37
  }),
38
38
  qc: Honk.G1Point({
39
- x: uint256(0x14e3747585344848708d74e5cac292bd806b04496ce2a1f19b832be2b06a46a0),
40
- y: uint256(0x24e01d1273374ee4f50c390758c45738b0dee956af2038acbd53b58d4b74779a)
39
+ x: uint256(0x0b824186ff62956e929ad489895b6f94e44fedde51b9c7ada98f43339fa875e2),
40
+ y: uint256(0x288402862aac013f44d9af847a2a1b318e8f3c026d50818ed5467c252505f8b8)
41
41
  }),
42
42
  qLookup: Honk.G1Point({
43
43
  x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000),
44
44
  y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000)
45
45
  }),
46
46
  qArith: Honk.G1Point({
47
- x: uint256(0x176c5cf9eb8c27900484c632289e86dfbfab9d4bbc5d927e2fe3a79b36479290),
48
- y: uint256(0x2fc22055c186e3bcd7b55e48688c899b5a5c804e3d7403f520776666dbb3f23e)
47
+ x: uint256(0x1eec43a65b0287ccdadfdee7b5c7f1edd764f78af3be146380f9285dc23935ac),
48
+ y: uint256(0x00334d21a1e3fdb81573ecfedf177b7a266b994f7c03e603d22c1d50a14c1177)
49
49
  }),
50
50
  qDeltaRange: Honk.G1Point({
51
- x: uint256(0x2d0e8c12eebb1f0a1604752470e237658a92f612da2abc65a791cc777a35a341),
52
- y: uint256(0x23ebdb221e2fc512e6bce94cc3701880ee87e340c16e2fcce619ee8e97949e56)
51
+ x: uint256(0x1fbf72c051dd975693c7581ba456e27c8779526d6226300b89fdcb2f46236846),
52
+ y: uint256(0x15c7e84bd025a07f602a5ced777d9a6dc519fa77e263fd6ae739a872ced3403b)
53
53
  }),
54
54
  qElliptic: Honk.G1Point({
55
55
  x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000),
56
56
  y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000)
57
57
  }),
58
58
  qMemory: Honk.G1Point({
59
- x: uint256(0x0f8820a8b43ed7f9a6e0aa355bf3ef04dc47c88a221a6c41a4f253e50783fab4),
60
- y: uint256(0x2de4b7c386bedbf5694b899eb29dd3c1be6e7691ecc10d4074966cfe8efb0421)
59
+ x: uint256(0x047852a9a4fee8460c8a70000bd59b1e9fa7b3a8ebe89a4a9bd74f8b667ec0e3),
60
+ y: uint256(0x206a2850fbba93adfaaf19b47f64be4050e7e3d0934e3bb3c152c6d0ebf9bc6e)
61
61
  }),
62
62
  qNnf: Honk.G1Point({
63
- x: uint256(0x19a8398c7b609f27c334c656127ebdc34db4982bb1f1bbec186873c113d2f0b1),
64
- y: uint256(0x29c36f8a702f73fec6a32d581fdc17e59c4999625d7c9147e7eac48df66b1740)
63
+ x: uint256(0x1bbbf2123e3d8fb40a55063fc262031ed631ba08b2403fc66be28284d8bf81f0),
64
+ y: uint256(0x22ed7e7b495a40b67a9131d0ec78104425c718cfcb4a1d539cc0efb438863481)
65
65
  }),
66
66
  qPoseidon2External: Honk.G1Point({
67
- x: uint256(0x00fd3df42aead1d3fe2aedac899b5a5a1d33072137bbf9cd98933a7f939ca4af),
68
- y: uint256(0x0daa73db37e213845ad80630816c01724f21e0151759b18358a51651bbc5ef49)
67
+ x: uint256(0x0545a1bebaefe1aed53b541c02ff5f8dc6ea9fcf3790df43723cec27fad71211),
68
+ y: uint256(0x0f6e3454a6b2e0f9ef6e3c3c420d2212f9b9611dcc56c93cb3f7b3a843b8b061)
69
69
  }),
70
70
  qPoseidon2Internal: Honk.G1Point({
71
- x: uint256(0x0d04ebec0a4f4c5343b72c528f4de039629eab96223847720e5fb619b1a8f10d),
72
- y: uint256(0x128f4178942625efadd7fe3adbaeb337fd961488236802d50ece4a7552e96370)
71
+ x: uint256(0x1dc8084a81e94401164ab0cc7dd600fdcccf393611a00c881b100f125a7c2cae),
72
+ y: uint256(0x1b45db8582b61bd1f9938b966fd2c62007a4e6b29bd12d93300fc87fc248541c)
73
73
  }),
74
74
  s1: Honk.G1Point({
75
- x: uint256(0x0774647b2dca67d5649beb99b794a2eb00df29cf5f41a651f1dbd4ef6e9efbf7),
76
- y: uint256(0x2f0f9a00cfdd4915a84eeecae14991cf8aed2f0efc17685988b2c5513a868150)
75
+ x: uint256(0x0055c092383ecaa42ba772ccd0dc288af43851c2230e18ce204b0b921c565ebb),
76
+ y: uint256(0x29586aa1c092712c4d7337d3017e2ca1c0b460b5020366e3a218f5dbdb65dac3)
77
77
  }),
78
78
  s2: Honk.G1Point({
79
- x: uint256(0x07fb7e3aefe3a10bf887114d2d23a0e6e625695d1edc4507aaa362cf13cbe95b),
80
- y: uint256(0x27c70c4226f232a5649e44053b2b71db25553c54f57ade536e7a60a7e4321836)
79
+ x: uint256(0x03c15d176f3295a287398c9bd47ebcbae4206b787ae7f99a0b4b845ba42c5b8f),
80
+ y: uint256(0x06046a4ecd73c3a079fdd9b1aadb3ec8a10d81bbdfff4fe64491b5d73850c951)
81
81
  }),
82
82
  s3: Honk.G1Point({
83
- x: uint256(0x28cd82d09235cab51c5909d625fd077e6b1c19e3f9d74963ebd6be11d165c8fa),
84
- y: uint256(0x1f13e6a87683c7105f1ad021f09f02db97654f78c52d549b8b927794f5fdde58)
83
+ x: uint256(0x2ab36d90282916edcc8344a83329989d6ab32ce72e50a8c5fb71bdc96cc13bba),
84
+ y: uint256(0x1f91bde663a11f2dd1c2189801544154219b96fc15c228a2afaf94c2fd525614)
85
85
  }),
86
86
  s4: Honk.G1Point({
87
- x: uint256(0x2706b7dbb9755b086936581aa3d9c43e6e24f1dc44e8d6268d1ae9e80b52015e),
88
- y: uint256(0x237ab989a520291d485957047de8e083cfd74afb501593c9429c9d40b5b8a070)
87
+ x: uint256(0x20e8b44228a8581f3b89daf0afd122fb5eb9b54235e5f3006f4da3ced24a4bff),
88
+ y: uint256(0x121da33cdfeb99d9117a23a70ca53f0409a753f28e8217d6297f2d890507fa06)
89
89
  }),
90
90
  t1: Honk.G1Point({
91
91
  x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000),
@@ -104,28 +104,28 @@ library HonkVerificationKey {
104
104
  y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000)
105
105
  }),
106
106
  id1: Honk.G1Point({
107
- x: uint256(0x117f92d3a69b1401f1b3eb19da6bf4dacd4a0c7b0fbc2767112674ae33356634),
108
- y: uint256(0x18d8c2c0d2dab140e273b4607eda1ebb4560dbb0c8299e21e702a20d2252b91a)
107
+ x: uint256(0x165a0d0053cad2cfaf860a8dffc7b857ae52a0830a5d0ff389f126736c8a68af),
108
+ y: uint256(0x1230c74e80658090adff46f20a5ba230b00a36fa1647bbee26dcd4f61c81ac74)
109
109
  }),
110
110
  id2: Honk.G1Point({
111
- x: uint256(0x2cffdc37befb4d7147f9b1e40dc54b529684766c4f6ed722a08af2080129fa8c),
112
- y: uint256(0x0f9ea00342ceb61caea3d52750c757d17b6d4d63fc7d855d4b3fdbad8566cab9)
111
+ x: uint256(0x0cde9685338036c896bf8031dfb4a27df07170d25a17a6f3487ff9dbc2d58d97),
112
+ y: uint256(0x2ba321fb18bb8416deb007188ba7eaf7d9acc32649feb97b99b61a385503e561)
113
113
  }),
114
114
  id3: Honk.G1Point({
115
- x: uint256(0x04b9f40f2cd4123d03663d63758856d3f947c857cdb16e4c45587df95a5239c1),
116
- y: uint256(0x149255bba819e1a349307d4a334143f77028d663bee3a4761177dd5f5610df21)
115
+ x: uint256(0x1803faf5351782f647e62d18cffbd667c89844b034d38e49c4317b6c6c262efc),
116
+ y: uint256(0x00d296c60b3f1ad4f263c6c0cf1c931573e621b89604b5a18add83543af491fb)
117
117
  }),
118
118
  id4: Honk.G1Point({
119
- x: uint256(0x2b8aba03c2291ad20411a8f5c9eafa2177e093539e88f085c376496455ce16ab),
120
- y: uint256(0x2b2cda526df47bdb8ff2257b7c3b8eadfcd5c195ad0d619ddcc3a960ece05656)
119
+ x: uint256(0x22136f96d01a39d6cd8808212c2d90f8f539d0c302bbfbe0856ec5858d14026e),
120
+ y: uint256(0x0c8e6eb560ae1fe67f9341f70bc50ad204ee5da5814a40a811d777be4d796ad7)
121
121
  }),
122
122
  lagrangeFirst: Honk.G1Point({
123
123
  x: uint256(0x2a56ce41f6b0be13b9c26747621b821eee81b23a887f299049b14c11e98460d6),
124
124
  y: uint256(0x1aa98f2de3ddda547d8f6de4e725ded5827d6338c78656c0d12ca1aea6ef2c7c)
125
125
  }),
126
126
  lagrangeLast: Honk.G1Point({
127
- x: uint256(0x100c5101d69d23da354afa62b1d0354277b5f12dc6dbd26b1b33eae7973aca5a),
128
- y: uint256(0x2ae821e69f12831c7dd899a431ff8eb56f8637b07fbd23ca8abcaae6ea299326)
127
+ x: uint256(0x054b6343cdc2405ed0b8e6603bd33aad161e52d8b252ff7916e72f9474fb5c92),
128
+ y: uint256(0x0d9a27acd4b305126491136fbce26d1bc29ecf5b067fe121b87d7347cdf3baf0)
129
129
  })
130
130
  });
131
131
  return vk;
@@ -30,6 +30,35 @@ contract MockInterfold {
30
30
  _request(program, numOptions);
31
31
  }
32
32
 
33
+ /// @notice Request an E3 with caller-supplied program params.
34
+ /// @dev `_request` hardcodes a TOKEN-census round. `CensusMode.ONCHAIN` needs a token, a credit
35
+ /// mode and a census mode that only the caller knows, and the snapshot is taken during
36
+ /// `validate`, so the params have to reach it here rather than being patched afterwards.
37
+ function requestWithParams(address program, uint256 numOptions, bytes memory params) external {
38
+ e3s[nextE3Id] = E3({
39
+ seed: 0,
40
+ committeeSize: IInterfold.CommitteeSize.Minimum,
41
+ requestBlock: 0,
42
+ inputWindow: [uint256(0), uint256(0)],
43
+ encryptionSchemeId: ENCRYPTION_SCHEME_ID,
44
+ e3Program: IE3Program(address(0)),
45
+ paramSet: 0, // Insecure512
46
+ customParams: params,
47
+ decryptionVerifier: IDecryptionVerifier(address(0)),
48
+ pkVerifier: IPkVerifier(address(0)),
49
+ committeePublicKey: committeePublicKey,
50
+ ciphertextOutput: bytes32(0),
51
+ plaintextOutput: plaintextOutput,
52
+ requester: address(0),
53
+ ciphertextCommitment: bytes32(0)
54
+ });
55
+
56
+ IE3Program(program).validate(nextE3Id, 0, bytes(""), bytes(""), params);
57
+
58
+ nextE3Id++;
59
+ numOptions; // silence unused-parameter warning; the count travels inside `params`
60
+ }
61
+
33
62
  function _request(address program, uint256 numOptions) internal {
34
63
  e3s[nextE3Id] = E3({
35
64
  seed: 0,
@@ -0,0 +1,48 @@
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
+ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
9
+ import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
10
+ import { ERC20Votes } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
11
+ import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol";
12
+
13
+ /// @title MockVotesToken
14
+ /// @notice An ERC20Votes token for exercising `CensusMode.ONCHAIN`.
15
+ /// @dev `MockVotingToken` is a plain ERC20 with no `getPastVotes`, which makes it the negative
16
+ /// case for the token probe in `CRISPProgram._initRound`. This one is the positive case.
17
+ ///
18
+ /// Uses the timestamp clock, matching `InterfoldToken`. A round records its snapshot in whatever
19
+ /// units the token reports, so a mock on the default block-number clock would exercise a
20
+ /// different path from the token this mode is built for.
21
+ contract MockVotesToken is ERC20, ERC20Permit, ERC20Votes {
22
+ constructor() ERC20("Mock Votes Token", "MVOTE") ERC20Permit("Mock Votes Token") {
23
+ _mint(msg.sender, 1e24);
24
+ _delegate(msg.sender, msg.sender);
25
+ }
26
+
27
+ function mint(address to, uint256 amount) external {
28
+ _mint(to, amount);
29
+ if (delegates(to) == address(0)) _delegate(to, to);
30
+ }
31
+
32
+ function clock() public view override returns (uint48) {
33
+ return uint48(block.timestamp);
34
+ }
35
+
36
+ /// @dev Declared pure per ERC-6372, so it cannot read `clock()`.
37
+ function CLOCK_MODE() public pure override returns (string memory) {
38
+ return "mode=timestamp";
39
+ }
40
+
41
+ function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
42
+ super._update(from, to, value);
43
+ }
44
+
45
+ function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
46
+ return super.nonces(owner);
47
+ }
48
+ }
@@ -0,0 +1,16 @@
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 The ERC-6372 clock of a token, used to record the snapshot of a round.
9
+ /// @dev Kept separate from `IVotesToken` because it is the optional half of the token surface.
10
+ /// A token that predates ERC-6372 still answers `getPastVotes`, so `CRISPProgram` falls back to
11
+ /// block numbers when this call reverts rather than refusing the round.
12
+ interface IERC6372Clock {
13
+ /// @notice The current timepoint, in the clock units of the token.
14
+ /// @return The timepoint.
15
+ function clock() external view returns (uint48);
16
+ }
@@ -0,0 +1,19 @@
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 The subset of a generated Honk verifier that `CRISPProgram` calls.
9
+ /// @dev Declared as an interface so the census paths can hold verifiers generated from different
10
+ /// circuits. `CRISPVerifier.sol` and `CRISPOnchainVerifier.sol` both declare a contract named
11
+ /// `HonkVerifier`, so importing both concrete types would collide.
12
+ interface IHonkVerifier {
13
+ /// @notice Verify a folded ballot proof against its public inputs.
14
+ /// @dev Reverts rather than returning false when the public inputs do not match the proof.
15
+ /// @param proof The folded proof.
16
+ /// @param publicInputs The public inputs, in the order the fold circuit declares them.
17
+ /// @return True when the proof is valid.
18
+ function verify(bytes calldata proof, bytes32[] calldata publicInputs) external view returns (bool);
19
+ }
@@ -0,0 +1,17 @@
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 The subset of an ERC20Votes token that `CensusMode.ONCHAIN` reads.
9
+ /// @dev Required, not optional. A round that names a token which cannot answer this is rejected
10
+ /// at request time, because every input would otherwise revert here after the fee is paid.
11
+ interface IVotesToken {
12
+ /// @notice The voting power of an account at a past timepoint.
13
+ /// @param account The account to read.
14
+ /// @param timepoint The timepoint, in the ERC-6372 clock units of the token.
15
+ /// @return The voting power at that timepoint.
16
+ function getPastVotes(address account, uint256 timepoint) external view returns (uint256);
17
+ }