@crisp-e3/contracts 0.14.0 → 0.15.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.
|
@@ -24,6 +24,27 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
24
24
|
CUSTOM
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/// @notice Where the eligible voter set for a round comes from.
|
|
28
|
+
/// @dev Two sources with opposite economics. TOKEN derives the electorate from balances at a
|
|
29
|
+
/// snapshot: the coordinator enumerates holders, which is expensive and needs an indexer, but it
|
|
30
|
+
/// is the only way to answer "everyone holding this token". BY_REQUESTER asks the requesting
|
|
31
|
+
/// contract, which already knows its own membership — a game roster, an allowlisted cohort, a
|
|
32
|
+
/// committee — so nothing is enumerated and no indexer is involved.
|
|
33
|
+
///
|
|
34
|
+
/// Declared explicitly rather than inferred. A coordinator that probed every requester and
|
|
35
|
+
/// silently fell back on failure would turn a broken census provider into a token vote with the
|
|
36
|
+
/// wrong electorate, and nothing would error.
|
|
37
|
+
///
|
|
38
|
+
/// Required, not optional: params must carry it. Making it defaultable would mean a caller that
|
|
39
|
+
/// forgot it silently got token discovery, which is the same silent-wrong-electorate failure one
|
|
40
|
+
/// level up.
|
|
41
|
+
enum CensusMode {
|
|
42
|
+
/// @notice Derived from token balances by the coordinator. The default.
|
|
43
|
+
TOKEN,
|
|
44
|
+
/// @notice Supplied by the requester via `getCensus(uint256 e3Id) returns (address[])`.
|
|
45
|
+
BY_REQUESTER
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
/// @notice Struct to store all data related to a voting round
|
|
28
49
|
struct RoundData {
|
|
29
50
|
uint256 merkleRoot;
|
|
@@ -32,6 +53,7 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
32
53
|
LazyIMTData votes;
|
|
33
54
|
uint256 numOptions;
|
|
34
55
|
CreditMode creditMode;
|
|
56
|
+
CensusMode censusMode;
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
// Constants
|
|
@@ -39,8 +61,15 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
39
61
|
bytes32 public constant ENCRYPTION_SCHEME_ID = keccak256("fhe.rs:BFV");
|
|
40
62
|
/// @notice The depth of the input Merkle tree.
|
|
41
63
|
uint8 public constant TREE_DEPTH = 20;
|
|
42
|
-
/// @notice
|
|
43
|
-
|
|
64
|
+
/// @notice Number of leading plaintext coefficients that carry the vote payload.
|
|
65
|
+
/// @dev Must stay aligned with `@crisp-e3/sdk` and `crisp_utils` (`MAX_MSG_NON_ZERO_COEFFS`).
|
|
66
|
+
/// The remaining coefficients up to the BFV degree are zero padding.
|
|
67
|
+
uint256 constant MAX_MSG_NON_ZERO_COEFFS = 100;
|
|
68
|
+
/// @notice Maximum number of vote options a round may configure.
|
|
69
|
+
/// @dev Bounded by the Noir circuit, which asserts `num_options <= MAX_OPTIONS`
|
|
70
|
+
/// (`circuits/lib/src/constants.nr`). A round above this accepts no ballot, because every
|
|
71
|
+
/// vote proof fails. Must stay aligned with the SDK constant of the same name.
|
|
72
|
+
uint256 constant MAX_VOTE_OPTIONS = 10;
|
|
44
73
|
// State variables
|
|
45
74
|
IInterfold public interfold;
|
|
46
75
|
IRiscZeroVerifier public risc0Verifier;
|
|
@@ -61,6 +90,10 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
61
90
|
error InvalidMerkleRoot();
|
|
62
91
|
error MerkleRootAlreadySet();
|
|
63
92
|
error InvalidTallyLength();
|
|
93
|
+
/// @notice A requester-supplied census names who may vote, not how much each vote weighs, so it
|
|
94
|
+
/// only has meaning when every voter carries the same credits.
|
|
95
|
+
error CensusModeRequiresConstantCredits();
|
|
96
|
+
error InvalidCensusMode();
|
|
64
97
|
error SlotIsEmpty();
|
|
65
98
|
error MerkleRootNotSet();
|
|
66
99
|
error InvalidNumOptions();
|
|
@@ -143,6 +176,16 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
143
176
|
numberOfVotes = round.votes.numberOfLeaves;
|
|
144
177
|
}
|
|
145
178
|
|
|
179
|
+
/// @notice The census source a round was requested with.
|
|
180
|
+
/// @dev A separate getter rather than a sixth return value on `getRoundData`, whose tuple is
|
|
181
|
+
/// already consumed by the server and the SDK — widening it would break them for a field most
|
|
182
|
+
/// callers do not want.
|
|
183
|
+
/// @param e3Id The E3 to look up.
|
|
184
|
+
/// @return The census mode recorded at validation.
|
|
185
|
+
function censusModeOf(uint256 e3Id) external view returns (CensusMode) {
|
|
186
|
+
return e3Data[e3Id].censusMode;
|
|
187
|
+
}
|
|
188
|
+
|
|
146
189
|
/// @inheritdoc IE3Program
|
|
147
190
|
function validate(
|
|
148
191
|
uint256 e3Id,
|
|
@@ -154,14 +197,34 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
154
197
|
if (msg.sender != address(interfold) && msg.sender != owner()) revert CallerNotAuthorized();
|
|
155
198
|
if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized();
|
|
156
199
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
200
|
+
// Scoped so the decoded values do not outlive their use: `validate` is close enough to the
|
|
201
|
+
// stack limit that holding all six of them alongside the parameters exceeds it.
|
|
202
|
+
{
|
|
203
|
+
// One decode, every field required. `censusMode` is read as a uint and range-checked rather
|
|
204
|
+
// than decoded straight into the enum, so an unrecognised value gives a named error instead
|
|
205
|
+
// of a bare panic.
|
|
206
|
+
(, , uint256 numOptions, CreditMode creditMode, , uint256 rawCensusMode) = abi.decode(
|
|
207
|
+
customParams,
|
|
208
|
+
(address, uint256, uint256, CreditMode, uint256, uint256)
|
|
209
|
+
);
|
|
210
|
+
// The circuit asserts `num_options <= MAX_OPTIONS`, so a round configured above it accepts no
|
|
211
|
+
// ballot at all. Reject at request time rather than stranding a round nobody can vote in.
|
|
212
|
+
if (numOptions < 2 || numOptions > MAX_VOTE_OPTIONS) revert InvalidNumOptions();
|
|
213
|
+
if (rawCensusMode > uint256(type(CensusMode).max)) revert InvalidCensusMode();
|
|
214
|
+
|
|
215
|
+
// Rejected here rather than by the coordinator, so a combination that can never work costs
|
|
216
|
+
// nothing: this reverts in the same transaction that requests the E3, before any fee is paid.
|
|
217
|
+
if (CensusMode(rawCensusMode) == CensusMode.BY_REQUESTER && creditMode != CreditMode.CONSTANT) {
|
|
218
|
+
revert CensusModeRequiresConstantCredits();
|
|
219
|
+
}
|
|
160
220
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
221
|
+
// we need to know the number of options for decoding the tally
|
|
222
|
+
e3Data[e3Id].numOptions = numOptions;
|
|
223
|
+
// we want to save the credit mode so it can be verified on chain by everyone
|
|
224
|
+
e3Data[e3Id].creditMode = creditMode;
|
|
225
|
+
// recorded so anyone can verify which electorate the round was requested against
|
|
226
|
+
e3Data[e3Id].censusMode = CensusMode(rawCensusMode);
|
|
227
|
+
}
|
|
165
228
|
|
|
166
229
|
e3Data[e3Id].paramsHash = keccak256(e3ProgramParams);
|
|
167
230
|
|
|
@@ -237,20 +300,24 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
237
300
|
|
|
238
301
|
uint64[] memory tally = _decodeBytesToUint64Array(e3.plaintextOutput);
|
|
239
302
|
|
|
240
|
-
|
|
241
|
-
|
|
303
|
+
// The payload lives in the first MAX_MSG_NON_ZERO_COEFFS coefficients; the rest of
|
|
304
|
+
// the polynomial is zero padding and must not be read.
|
|
305
|
+
if (tally.length < MAX_MSG_NON_ZERO_COEFFS) revert InvalidTallyLength();
|
|
306
|
+
|
|
307
|
+
uint256 segmentSize = MAX_MSG_NON_ZERO_COEFFS / numOptions;
|
|
308
|
+
// More options than payload coefficients leaves nothing to decode.
|
|
309
|
+
if (segmentSize == 0) return new uint256[](0);
|
|
242
310
|
|
|
243
311
|
votes = new uint256[](numOptions);
|
|
244
312
|
|
|
245
313
|
for (uint256 optIdx = 0; optIdx < numOptions; optIdx++) {
|
|
246
314
|
uint256 segmentStart = optIdx * segmentSize;
|
|
247
|
-
// Read only the last effectiveSize bits (where the value is, MSB first)
|
|
248
|
-
uint256 readStart = segmentStart + segmentSize - effectiveSize;
|
|
249
315
|
uint256 value = 0;
|
|
250
316
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
317
|
+
// Each segment holds the count in binary, most significant coefficient first.
|
|
318
|
+
for (uint256 i = 0; i < segmentSize; i++) {
|
|
319
|
+
uint256 weight = 2 ** (segmentSize - 1 - i);
|
|
320
|
+
value += uint256(tally[segmentStart + i]) * weight;
|
|
254
321
|
}
|
|
255
322
|
|
|
256
323
|
votes[optIdx] = value;
|
|
@@ -269,15 +336,21 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
269
336
|
}
|
|
270
337
|
|
|
271
338
|
/// @inheritdoc IE3Program
|
|
272
|
-
function verify(
|
|
339
|
+
function verify(
|
|
340
|
+
uint256 e3Id,
|
|
341
|
+
bytes32 ciphertextOutputHash,
|
|
342
|
+
bytes32 ciphertextCommitment,
|
|
343
|
+
bytes memory proof
|
|
344
|
+
) external view override returns (bool) {
|
|
273
345
|
bytes32 paramsHash = getParamsHash(e3Id);
|
|
274
346
|
|
|
275
347
|
bytes32 inputRoot = bytes32(e3Data[e3Id].votes._root(TREE_DEPTH));
|
|
276
|
-
bytes memory journal = new bytes(
|
|
348
|
+
bytes memory journal = new bytes(528); // (32 + 1) * 4 * 4
|
|
277
349
|
|
|
278
350
|
_encodeLengthPrefixAndHash(journal, 0, ciphertextOutputHash);
|
|
279
|
-
_encodeLengthPrefixAndHash(journal, 132,
|
|
280
|
-
_encodeLengthPrefixAndHash(journal, 264,
|
|
351
|
+
_encodeLengthPrefixAndHash(journal, 132, ciphertextCommitment);
|
|
352
|
+
_encodeLengthPrefixAndHash(journal, 264, paramsHash);
|
|
353
|
+
_encodeLengthPrefixAndHash(journal, 396, inputRoot);
|
|
281
354
|
|
|
282
355
|
risc0Verifier.verify(proof, imageId, sha256(journal));
|
|
283
356
|
return true;
|
|
@@ -20,6 +20,16 @@ contract MockInterfold {
|
|
|
20
20
|
mapping(uint256 => E3) public e3s;
|
|
21
21
|
|
|
22
22
|
function request(address program) external {
|
|
23
|
+
_request(program, 2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// @notice Request an E3 with a caller-supplied option count, so tests can
|
|
27
|
+
/// cover tallies with more than two options.
|
|
28
|
+
function requestWithOptions(address program, uint256 numOptions) external {
|
|
29
|
+
_request(program, numOptions);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _request(address program, uint256 numOptions) internal {
|
|
23
33
|
e3s[nextE3Id] = E3({
|
|
24
34
|
seed: 0,
|
|
25
35
|
committeeSize: IInterfold.CommitteeSize.Minimum,
|
|
@@ -28,7 +38,7 @@ contract MockInterfold {
|
|
|
28
38
|
encryptionSchemeId: bytes32(0),
|
|
29
39
|
e3Program: IE3Program(address(0)),
|
|
30
40
|
paramSet: 0, // Insecure512
|
|
31
|
-
customParams: abi.encode(address(0), nextE3Id,
|
|
41
|
+
customParams: abi.encode(address(0), nextE3Id, numOptions, 0, 0, 0),
|
|
32
42
|
decryptionVerifier: IDecryptionVerifier(address(0)),
|
|
33
43
|
pkVerifier: IPkVerifier(address(0)),
|
|
34
44
|
committeePublicKey: committeePublicKey,
|
|
@@ -38,7 +48,7 @@ contract MockInterfold {
|
|
|
38
48
|
ciphertextCommitment: bytes32(0)
|
|
39
49
|
});
|
|
40
50
|
|
|
41
|
-
IE3Program(program).validate(nextE3Id, 0, bytes(""), bytes(""), abi.encode(address(0), nextE3Id,
|
|
51
|
+
IE3Program(program).validate(nextE3Id, 0, bytes(""), bytes(""), abi.encode(address(0), nextE3Id, numOptions, 0, 0, 0));
|
|
42
52
|
|
|
43
53
|
nextE3Id++;
|
|
44
54
|
}
|
|
@@ -65,7 +75,7 @@ contract MockInterfold {
|
|
|
65
75
|
encryptionSchemeId: bytes32(0),
|
|
66
76
|
e3Program: IE3Program(address(0)),
|
|
67
77
|
paramSet: 0, // Insecure512
|
|
68
|
-
customParams: abi.encode(address(0), 0, 2, 0, 0),
|
|
78
|
+
customParams: abi.encode(address(0), 0, 2, 0, 0, 0),
|
|
69
79
|
decryptionVerifier: IDecryptionVerifier(address(0)),
|
|
70
80
|
pkVerifier: IPkVerifier(address(0)),
|
|
71
81
|
committeePublicKey: committeePublicKey,
|
|
@@ -8,7 +8,19 @@ pragma solidity ^0.8.27;
|
|
|
8
8
|
import { IRiscZeroVerifier, Receipt } from "risc0/IRiscZeroVerifier.sol";
|
|
9
9
|
|
|
10
10
|
contract MockRISC0Verifier is IRiscZeroVerifier {
|
|
11
|
-
|
|
11
|
+
bytes32 public expectedJournalDigest;
|
|
12
|
+
|
|
13
|
+
error UnexpectedJournalDigest(bytes32 actual, bytes32 expected);
|
|
14
|
+
|
|
15
|
+
function setExpectedJournalDigest(bytes32 journalDigest) external {
|
|
16
|
+
expectedJournalDigest = journalDigest;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function verify(bytes calldata, bytes32, bytes32 journalDigest) public view override {
|
|
20
|
+
if (expectedJournalDigest != bytes32(0) && journalDigest != expectedJournalDigest) {
|
|
21
|
+
revert UnexpectedJournalDigest(journalDigest, expectedJournalDigest);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
function verifyIntegrity(Receipt calldata receipt) external view override {}
|
|
14
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crisp-e3/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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.5.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@nomicfoundation/hardhat-keystore": "3.0.3",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"typechain": "^8.3.0",
|
|
54
54
|
"typescript": "5.8.3",
|
|
55
55
|
"viem": "2.30.6",
|
|
56
|
-
"@crisp-e3/zk-inputs": "^0.
|
|
57
|
-
"@crisp-e3/sdk": "^0.
|
|
56
|
+
"@crisp-e3/zk-inputs": "^0.15.0",
|
|
57
|
+
"@crisp-e3/sdk": "^0.15.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"compile": "hardhat compile",
|