@crisp-e3/contracts 0.14.0 → 0.16.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.
@@ -10,6 +10,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
10
10
  import { IE3Program } from "@interfold/contracts/contracts/interfaces/IE3Program.sol";
11
11
  import { IInterfold } from "@interfold/contracts/contracts/interfaces/IInterfold.sol";
12
12
  import { E3 } from "@interfold/contracts/contracts/interfaces/IE3.sol";
13
+ import { Risc0ComputeProof } from "@interfold/contracts/contracts/lib/Risc0ComputeProof.sol";
13
14
  import { LazyIMTData, InternalLazyIMT } from "@zk-kit/lazy-imt.sol/InternalLazyIMT.sol";
14
15
  import { HonkVerifier } from "./CRISPVerifier.sol";
15
16
 
@@ -24,6 +25,27 @@ contract CRISPProgram is IE3Program, Ownable {
24
25
  CUSTOM
25
26
  }
26
27
 
28
+ /// @notice Where the eligible voter set for a round comes from.
29
+ /// @dev Two sources with opposite economics. TOKEN derives the electorate from balances at a
30
+ /// snapshot: the coordinator enumerates holders, which is expensive and needs an indexer, but it
31
+ /// is the only way to answer "everyone holding this token". BY_REQUESTER asks the requesting
32
+ /// contract, which already knows its own membership — a game roster, an allowlisted cohort, a
33
+ /// committee — so nothing is enumerated and no indexer is involved.
34
+ ///
35
+ /// Declared explicitly rather than inferred. A coordinator that probed every requester and
36
+ /// silently fell back on failure would turn a broken census provider into a token vote with the
37
+ /// wrong electorate, and nothing would error.
38
+ ///
39
+ /// Required, not optional: params must carry it. Making it defaultable would mean a caller that
40
+ /// forgot it silently got token discovery, which is the same silent-wrong-electorate failure one
41
+ /// level up.
42
+ enum CensusMode {
43
+ /// @notice Derived from token balances by the coordinator. The default.
44
+ TOKEN,
45
+ /// @notice Supplied by the requester via `getCensus(uint256 e3Id) returns (address[])`.
46
+ BY_REQUESTER
47
+ }
48
+
27
49
  /// @notice Struct to store all data related to a voting round
28
50
  struct RoundData {
29
51
  uint256 merkleRoot;
@@ -32,6 +54,7 @@ contract CRISPProgram is IE3Program, Ownable {
32
54
  LazyIMTData votes;
33
55
  uint256 numOptions;
34
56
  CreditMode creditMode;
57
+ CensusMode censusMode;
35
58
  }
36
59
 
37
60
  // Constants
@@ -39,8 +62,15 @@ contract CRISPProgram is IE3Program, Ownable {
39
62
  bytes32 public constant ENCRYPTION_SCHEME_ID = keccak256("fhe.rs:BFV");
40
63
  /// @notice The depth of the input Merkle tree.
41
64
  uint8 public constant TREE_DEPTH = 20;
42
- /// @notice Maximum number of bits allocated for vote counts in the plaintext output per option.
43
- uint256 constant MAX_VOTE_BITS = 50;
65
+ /// @notice Number of leading plaintext coefficients that carry the vote payload.
66
+ /// @dev Must stay aligned with `@crisp-e3/sdk` and `crisp_utils` (`MAX_MSG_NON_ZERO_COEFFS`).
67
+ /// The remaining coefficients up to the BFV degree are zero padding.
68
+ uint256 constant MAX_MSG_NON_ZERO_COEFFS = 100;
69
+ /// @notice Maximum number of vote options a round may configure.
70
+ /// @dev Bounded by the Noir circuit, which asserts `num_options <= MAX_OPTIONS`
71
+ /// (`circuits/lib/src/constants.nr`). A round above this accepts no ballot, because every
72
+ /// vote proof fails. Must stay aligned with the SDK constant of the same name.
73
+ uint256 constant MAX_VOTE_OPTIONS = 10;
44
74
  // State variables
45
75
  IInterfold public interfold;
46
76
  IRiscZeroVerifier public risc0Verifier;
@@ -61,12 +91,17 @@ contract CRISPProgram is IE3Program, Ownable {
61
91
  error InvalidMerkleRoot();
62
92
  error MerkleRootAlreadySet();
63
93
  error InvalidTallyLength();
94
+ /// @notice A requester-supplied census names who may vote, not how much each vote weighs, so it
95
+ /// only has meaning when every voter carries the same credits.
96
+ error CensusModeRequiresConstantCredits();
97
+ error InvalidCensusMode();
64
98
  error SlotIsEmpty();
65
99
  error MerkleRootNotSet();
66
100
  error InvalidNumOptions();
67
101
  error InputDeadlinePassed(uint256 e3Id, uint256 deadline);
68
102
  error KeyNotPublished(uint256 e3Id);
69
103
  error E3NotAcceptingInputs(uint256 e3Id);
104
+ error InvalidComputeContext();
70
105
 
71
106
  // Events
72
107
  event InputPublished(uint256 indexed e3Id, bytes encryptedVote, uint256 index);
@@ -139,10 +174,20 @@ contract CRISPProgram is IE3Program, Ownable {
139
174
  paramsHash = round.paramsHash;
140
175
  numOptions = round.numOptions;
141
176
  creditMode = round.creditMode;
142
- inputRoot = round.votes._root(TREE_DEPTH);
177
+ inputRoot = round.votes._root();
143
178
  numberOfVotes = round.votes.numberOfLeaves;
144
179
  }
145
180
 
181
+ /// @notice The census source a round was requested with.
182
+ /// @dev A separate getter rather than a sixth return value on `getRoundData`, whose tuple is
183
+ /// already consumed by the server and the SDK — widening it would break them for a field most
184
+ /// callers do not want.
185
+ /// @param e3Id The E3 to look up.
186
+ /// @return The census mode recorded at validation.
187
+ function censusModeOf(uint256 e3Id) external view returns (CensusMode) {
188
+ return e3Data[e3Id].censusMode;
189
+ }
190
+
146
191
  /// @inheritdoc IE3Program
147
192
  function validate(
148
193
  uint256 e3Id,
@@ -154,14 +199,34 @@ contract CRISPProgram is IE3Program, Ownable {
154
199
  if (msg.sender != address(interfold) && msg.sender != owner()) revert CallerNotAuthorized();
155
200
  if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized();
156
201
 
157
- // decode custom params to get the number of options
158
- (, , uint256 numOptions, CreditMode creditMode, ) = abi.decode(customParams, (address, uint256, uint256, CreditMode, uint256));
159
- if (numOptions < 2) revert InvalidNumOptions();
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
+ }
160
222
 
161
- // we need to know the number of options for decoding the tally
162
- e3Data[e3Id].numOptions = numOptions;
163
- // we want to save the credit mode so it can be verified on chain by everyone
164
- e3Data[e3Id].creditMode = creditMode;
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
+ }
165
230
 
166
231
  e3Data[e3Id].paramsHash = keccak256(e3ProgramParams);
167
232
 
@@ -237,20 +302,24 @@ contract CRISPProgram is IE3Program, Ownable {
237
302
 
238
303
  uint64[] memory tally = _decodeBytesToUint64Array(e3.plaintextOutput);
239
304
 
240
- uint256 segmentSize = tally.length / numOptions;
241
- uint256 effectiveSize = segmentSize > MAX_VOTE_BITS ? MAX_VOTE_BITS : segmentSize;
305
+ // The payload lives in the first MAX_MSG_NON_ZERO_COEFFS coefficients; the rest of
306
+ // the polynomial is zero padding and must not be read.
307
+ if (tally.length < MAX_MSG_NON_ZERO_COEFFS) revert InvalidTallyLength();
308
+
309
+ uint256 segmentSize = MAX_MSG_NON_ZERO_COEFFS / numOptions;
310
+ // More options than payload coefficients leaves nothing to decode.
311
+ if (segmentSize == 0) return new uint256[](0);
242
312
 
243
313
  votes = new uint256[](numOptions);
244
314
 
245
315
  for (uint256 optIdx = 0; optIdx < numOptions; optIdx++) {
246
316
  uint256 segmentStart = optIdx * segmentSize;
247
- // Read only the last effectiveSize bits (where the value is, MSB first)
248
- uint256 readStart = segmentStart + segmentSize - effectiveSize;
249
317
  uint256 value = 0;
250
318
 
251
- for (uint256 i = 0; i < effectiveSize; i++) {
252
- uint256 weight = 2 ** (effectiveSize - 1 - i);
253
- value += uint256(tally[readStart + i]) * weight;
319
+ // Each segment holds the count in binary, most significant coefficient first.
320
+ for (uint256 i = 0; i < segmentSize; i++) {
321
+ uint256 weight = 2 ** (segmentSize - 1 - i);
322
+ value += uint256(tally[segmentStart + i]) * weight;
254
323
  }
255
324
 
256
325
  votes[optIdx] = value;
@@ -269,17 +338,31 @@ contract CRISPProgram is IE3Program, Ownable {
269
338
  }
270
339
 
271
340
  /// @inheritdoc IE3Program
272
- function verify(uint256 e3Id, bytes32 ciphertextOutputHash, bytes memory proof) external view override returns (bool) {
341
+ function verify(
342
+ uint256 e3Id,
343
+ bytes32 ciphertextOutputHash,
344
+ bytes32 ciphertextCommitment,
345
+ bytes memory proof
346
+ ) external view override returns (bool) {
347
+ E3 memory e3 = interfold.getE3(e3Id);
273
348
  bytes32 paramsHash = getParamsHash(e3Id);
349
+ bytes32 inputRoot = bytes32(e3Data[e3Id].votes._root());
350
+ Risc0ComputeProof.Proof memory computeProof = Risc0ComputeProof.decode(proof);
351
+ if (computeProof.paramsHash != paramsHash || computeProof.inputRoot != inputRoot) revert InvalidComputeContext();
352
+
353
+ bytes memory journal = Risc0ComputeProof.journal(
354
+ bytes32(block.chainid),
355
+ bytes32(uint256(uint160(address(interfold)))),
356
+ bytes32(e3Id),
357
+ e3.encryptionSchemeId,
358
+ e3.committeePublicKey,
359
+ ciphertextOutputHash,
360
+ ciphertextCommitment,
361
+ paramsHash,
362
+ inputRoot
363
+ );
274
364
 
275
- bytes32 inputRoot = bytes32(e3Data[e3Id].votes._root(TREE_DEPTH));
276
- bytes memory journal = new bytes(396); // (32 + 1) * 4 * 3
277
-
278
- _encodeLengthPrefixAndHash(journal, 0, ciphertextOutputHash);
279
- _encodeLengthPrefixAndHash(journal, 132, paramsHash);
280
- _encodeLengthPrefixAndHash(journal, 264, inputRoot);
281
-
282
- risc0Verifier.verify(proof, imageId, sha256(journal));
365
+ risc0Verifier.verify(computeProof.seal, imageId, sha256(journal));
283
366
  return true;
284
367
  }
285
368
 
@@ -308,19 +391,6 @@ contract CRISPProgram is IE3Program, Ownable {
308
391
  }
309
392
  }
310
393
 
311
- /// @notice Encode length prefix and hash
312
- /// @param journal The journal to encode into
313
- /// @param startIndex The start index in the journal
314
- /// @param hashVal The hash value to encode
315
- function _encodeLengthPrefixAndHash(bytes memory journal, uint256 startIndex, bytes32 hashVal) internal pure {
316
- journal[startIndex] = 0x20;
317
- startIndex += 4;
318
-
319
- for (uint256 i = 0; i < 32; i++) {
320
- journal[startIndex + i * 4] = hashVal[i];
321
- }
322
- }
323
-
324
394
  /// @notice Decode bytes to uint64 array
325
395
  /// @param data The bytes to decode (must be multiple of 8)
326
396
  /// @return result Array of uint64 values