@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
|
|
43
|
-
|
|
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(
|
|
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
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
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
|
-
|
|
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
|