@crisp-e3/contracts 0.16.0 → 0.18.0-insecure.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/contracts/CRISPProgram.sol +485 -78
- package/contracts/Mocks/MockInterfold.sol +37 -3
- package/contracts/Mocks/MockVotesToken.sol +48 -0
- package/contracts/interfaces/IERC6372Clock.sol +16 -0
- package/contracts/interfaces/IHonkVerifier.sol +21 -0
- package/contracts/interfaces/IVotesToken.sol +23 -0
- package/contracts/verifiers/CRISPOnchainVerifier.sol +2493 -0
- package/contracts/{CRISPVerifier.sol → verifiers/CRISPVerifier.sol} +45 -45
- package/package.json +9 -4
|
@@ -12,9 +12,17 @@ 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 {
|
|
15
|
+
import { SNARK_SCALAR_FIELD } from "@zk-kit/lazy-imt.sol/Constants.sol";
|
|
16
|
+
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
17
|
+
import { IHonkVerifier } from "./interfaces/IHonkVerifier.sol";
|
|
18
|
+
import { IVotesToken } from "./interfaces/IVotesToken.sol";
|
|
19
|
+
import { IERC6372Clock } from "./interfaces/IERC6372Clock.sol";
|
|
20
|
+
|
|
21
|
+
interface IInterfoldProgramRegistry {
|
|
22
|
+
function e3Programs(IE3Program e3Program) external view returns (bool);
|
|
23
|
+
}
|
|
16
24
|
|
|
17
|
-
contract CRISPProgram is IE3Program, Ownable {
|
|
25
|
+
contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
18
26
|
using InternalLazyIMT for LazyIMTData;
|
|
19
27
|
|
|
20
28
|
/// @notice Enum to represent credit modes
|
|
@@ -43,7 +51,12 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
43
51
|
/// @notice Derived from token balances by the coordinator. The default.
|
|
44
52
|
TOKEN,
|
|
45
53
|
/// @notice Supplied by the requester via `getCensus(uint256 e3Id) returns (address[])`.
|
|
46
|
-
BY_REQUESTER
|
|
54
|
+
BY_REQUESTER,
|
|
55
|
+
/// @notice Read from the token by this contract, one input at a time. No list is enumerated
|
|
56
|
+
/// and no root is posted, so there is no census producer to trust. `publishInput` calls
|
|
57
|
+
/// `getPastVotes` for the slot and passes the result to the circuit as a public input, which
|
|
58
|
+
/// is why this mode uses the `crisp_onchain` verifier rather than the `crisp` one.
|
|
59
|
+
ONCHAIN
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
/// @notice Struct to store all data related to a voting round
|
|
@@ -51,10 +64,47 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
51
64
|
uint256 merkleRoot;
|
|
52
65
|
bytes32 paramsHash;
|
|
53
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;
|
|
54
91
|
LazyIMTData votes;
|
|
55
92
|
uint256 numOptions;
|
|
56
93
|
CreditMode creditMode;
|
|
57
94
|
CensusMode censusMode;
|
|
95
|
+
/// @notice The token that voting power is read from. Only used by `CensusMode.ONCHAIN`.
|
|
96
|
+
address token;
|
|
97
|
+
/// @notice The smallest voting power that may cast an input. Only used by
|
|
98
|
+
/// `CensusMode.ONCHAIN`, where eligibility is checked per input instead of by a census.
|
|
99
|
+
uint256 minVotingPower;
|
|
100
|
+
/// @notice Credits given to each eligible voter under `CreditMode.CONSTANT`.
|
|
101
|
+
uint256 credits;
|
|
102
|
+
/// @notice The timepoint that voting power is read at, in the ERC-6372 clock units of the
|
|
103
|
+
/// token. Recorded when the round is requested, so it is the same for every input.
|
|
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;
|
|
58
108
|
}
|
|
59
109
|
|
|
60
110
|
// Constants
|
|
@@ -71,11 +121,24 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
71
121
|
/// (`circuits/lib/src/constants.nr`). A round above this accepts no ballot, because every
|
|
72
122
|
/// vote proof fails. Must stay aligned with the SDK constant of the same name.
|
|
73
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;
|
|
74
127
|
// State variables
|
|
75
128
|
IInterfold public interfold;
|
|
76
129
|
IRiscZeroVerifier public risc0Verifier;
|
|
77
130
|
bytes32 public imageId;
|
|
78
|
-
|
|
131
|
+
/// @notice Verifies ballots for the census modes that prove membership of a Merkle tree.
|
|
132
|
+
IHonkVerifier private immutable honkVerifier;
|
|
133
|
+
/// @notice Verifies ballots for `CensusMode.ONCHAIN`, whose circuit has no Merkle inputs and
|
|
134
|
+
/// takes voting power as a public input instead.
|
|
135
|
+
IHonkVerifier private immutable onchainHonkVerifier;
|
|
136
|
+
|
|
137
|
+
/// @notice The EIP-712 type of the message a voter signs to authorise one ballot.
|
|
138
|
+
/// @dev The digest binds the signature to the round, the slot, and this exact ciphertext. The
|
|
139
|
+
/// chain id and this contract address come from the EIP-712 domain, so a signature cannot be
|
|
140
|
+
/// replayed onto another round, another slot, another ballot, or another deployment.
|
|
141
|
+
bytes32 private constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 e3Id,address slot,bytes32 ciphertextCommitment)");
|
|
79
142
|
|
|
80
143
|
// Mappings
|
|
81
144
|
mapping(uint256 e3Id => RoundData) e3Data;
|
|
@@ -84,6 +147,9 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
84
147
|
error CallerNotAuthorized();
|
|
85
148
|
error E3AlreadyInitialized();
|
|
86
149
|
error InterfoldAddressZero();
|
|
150
|
+
error InterfoldAlreadyBound();
|
|
151
|
+
error InterfoldNotContract();
|
|
152
|
+
error ProgramNotRegistered();
|
|
87
153
|
error Risc0VerifierAddressZero();
|
|
88
154
|
error InvalidHonkVerifier();
|
|
89
155
|
error EmptyInputData();
|
|
@@ -94,7 +160,35 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
94
160
|
/// @notice A requester-supplied census names who may vote, not how much each vote weighs, so it
|
|
95
161
|
/// only has meaning when every voter carries the same credits.
|
|
96
162
|
error CensusModeRequiresConstantCredits();
|
|
163
|
+
/// @notice `CensusMode.ONCHAIN` reads voting power from a token, so it cannot run without one
|
|
164
|
+
/// that answers `getPastVotes`.
|
|
165
|
+
error CensusModeRequiresToken();
|
|
166
|
+
/// @notice An `ONCHAIN` round with constant credits must grant a non-zero allowance, or it
|
|
167
|
+
/// bounds every ballot to zero and can only accept masks.
|
|
168
|
+
error InvalidCredits();
|
|
169
|
+
/// @notice The slot holds less voting power than the round requires, so it cannot be written to.
|
|
170
|
+
/// @dev Raised for mask inputs as well as real votes. Under `CensusMode.ONCHAIN` this check
|
|
171
|
+
/// replaces the Merkle membership proof that gates both branches in the other modes.
|
|
172
|
+
error SlotNotEligible();
|
|
97
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);
|
|
98
192
|
error SlotIsEmpty();
|
|
99
193
|
error MerkleRootNotSet();
|
|
100
194
|
error InvalidNumOptions();
|
|
@@ -104,24 +198,73 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
104
198
|
error InvalidComputeContext();
|
|
105
199
|
|
|
106
200
|
// Events
|
|
107
|
-
event
|
|
108
|
-
|
|
109
|
-
/// @notice
|
|
110
|
-
/// @
|
|
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.
|
|
111
220
|
/// @param _risc0Verifier The RISC Zero verifier address
|
|
112
221
|
/// @param _honkVerifier The honk verifier address
|
|
113
222
|
/// @param _imageId The image ID for the guest program
|
|
114
|
-
constructor(
|
|
115
|
-
|
|
223
|
+
constructor(
|
|
224
|
+
address _initialOwner,
|
|
225
|
+
IRiscZeroVerifier _risc0Verifier,
|
|
226
|
+
IHonkVerifier _honkVerifier,
|
|
227
|
+
IHonkVerifier _onchainHonkVerifier,
|
|
228
|
+
bytes32 _imageId
|
|
229
|
+
) Ownable(_initialOwner) EIP712("CRISP", "1") {
|
|
116
230
|
if (address(_risc0Verifier) == address(0)) revert Risc0VerifierAddressZero();
|
|
117
231
|
if (address(_honkVerifier) == address(0)) revert InvalidHonkVerifier();
|
|
232
|
+
if (address(_onchainHonkVerifier) == address(0)) revert InvalidHonkVerifier();
|
|
118
233
|
|
|
119
|
-
interfold = _interfold;
|
|
120
234
|
risc0Verifier = _risc0Verifier;
|
|
121
235
|
honkVerifier = _honkVerifier;
|
|
236
|
+
onchainHonkVerifier = _onchainHonkVerifier;
|
|
122
237
|
imageId = _imageId;
|
|
123
238
|
}
|
|
124
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
|
+
|
|
255
|
+
/// @notice The digest a voter signs to authorise one ballot.
|
|
256
|
+
/// @dev Computed for every input, and for mask inputs as well as real votes. The circuit ignores
|
|
257
|
+
/// it on the mask branch, but the contract must not skip it: a digest that were computed only
|
|
258
|
+
/// for real votes would let an observer tell the two apart on-chain, which is exactly what mask
|
|
259
|
+
/// inputs exist to prevent.
|
|
260
|
+
/// @param e3Id The E3 the ballot belongs to.
|
|
261
|
+
/// @param slot The slot address the ballot is written to.
|
|
262
|
+
/// @param ciphertextCommitment The commitment to the ballot ciphertext.
|
|
263
|
+
/// @return The EIP-712 digest.
|
|
264
|
+
function ballotDigest(uint256 e3Id, address slot, bytes32 ciphertextCommitment) public view returns (bytes32) {
|
|
265
|
+
return _hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, e3Id, slot, ciphertextCommitment)));
|
|
266
|
+
}
|
|
267
|
+
|
|
125
268
|
/// @notice Sets the Merkle root for an E3 program. Can only be set once.
|
|
126
269
|
/// @param _e3Id The E3 program ID
|
|
127
270
|
/// @param _root The Merkle root to set.
|
|
@@ -133,12 +276,19 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
133
276
|
}
|
|
134
277
|
|
|
135
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.
|
|
136
285
|
/// @param _imageId The new image ID.
|
|
137
286
|
function setImageId(bytes32 _imageId) external onlyOwner {
|
|
138
287
|
imageId = _imageId;
|
|
139
288
|
}
|
|
140
289
|
|
|
141
290
|
/// @notice Set the RISC Zero verifier.
|
|
291
|
+
/// @dev Carries the same in-flight risk as `setImageId`. Change it only between rounds.
|
|
142
292
|
/// @param _risc0Verifier The new RISC Zero verifier address
|
|
143
293
|
function setRisc0Verifier(IRiscZeroVerifier _risc0Verifier) external onlyOwner {
|
|
144
294
|
if (address(_risc0Verifier) == address(0)) revert Risc0VerifierAddressZero();
|
|
@@ -178,6 +328,36 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
178
328
|
numberOfVotes = round.votes.numberOfLeaves;
|
|
179
329
|
}
|
|
180
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
|
+
|
|
181
361
|
/// @notice The census source a round was requested with.
|
|
182
362
|
/// @dev A separate getter rather than a sixth return value on `getRoundData`, whose tuple is
|
|
183
363
|
/// already consumed by the server and the SDK — widening it would break them for a field most
|
|
@@ -199,34 +379,9 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
199
379
|
if (msg.sender != address(interfold) && msg.sender != owner()) revert CallerNotAuthorized();
|
|
200
380
|
if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized();
|
|
201
381
|
|
|
202
|
-
//
|
|
203
|
-
// stack limit that holding
|
|
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
|
-
}
|
|
382
|
+
// Delegated to its own frame rather than scoped inline: `validate` is close enough to the
|
|
383
|
+
// stack limit that holding the six decoded values alongside the parameters exceeds it.
|
|
384
|
+
_initRound(e3Id, customParams);
|
|
230
385
|
|
|
231
386
|
e3Data[e3Id].paramsHash = keccak256(e3ProgramParams);
|
|
232
387
|
|
|
@@ -236,6 +391,104 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
236
391
|
return ENCRYPTION_SCHEME_ID;
|
|
237
392
|
}
|
|
238
393
|
|
|
394
|
+
/// @notice Decode the round configuration and record it.
|
|
395
|
+
/// @dev One decode, every field required. `censusMode` is read as a uint and range-checked
|
|
396
|
+
/// rather than decoded straight into the enum, so an unrecognised value gives a named error
|
|
397
|
+
/// instead of a bare panic.
|
|
398
|
+
/// @param e3Id The E3 being configured.
|
|
399
|
+
/// @param customParams The ABI-encoded round configuration.
|
|
400
|
+
function _initRound(uint256 e3Id, bytes calldata customParams) internal {
|
|
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));
|
|
410
|
+
|
|
411
|
+
// The circuit asserts `num_options <= MAX_OPTIONS`, so a round configured above it accepts no
|
|
412
|
+
// ballot at all. Reject at request time rather than stranding a round nobody can vote in.
|
|
413
|
+
if (numOptions < 2 || numOptions > MAX_VOTE_OPTIONS) revert InvalidNumOptions();
|
|
414
|
+
if (rawCensusMode > uint256(type(CensusMode).max)) revert InvalidCensusMode();
|
|
415
|
+
|
|
416
|
+
// Rejected here rather than by the coordinator, so a combination that can never work costs
|
|
417
|
+
// nothing: this reverts in the same transaction that requests the E3, before any fee is paid.
|
|
418
|
+
if (CensusMode(rawCensusMode) == CensusMode.BY_REQUESTER && creditMode != CreditMode.CONSTANT) {
|
|
419
|
+
revert CensusModeRequiresConstantCredits();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ONCHAIN reads every voter's power from this token, so a round without one accepts no ballot
|
|
423
|
+
// at all. Same reasoning as the numOptions bound: fail before the fee is paid.
|
|
424
|
+
if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN && token == address(0)) {
|
|
425
|
+
revert CensusModeRequiresToken();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// An ONCHAIN round hands `credits` to the circuit as the voting-power bound, so zero credits
|
|
429
|
+
// bound every ballot to zero: only a mask would be accepted, and the round would tally
|
|
430
|
+
// nothing. Checked for ONCHAIN only — the Merkle modes take the bound from the census leaf,
|
|
431
|
+
// where `credits` never reaches the circuit and the contract has nothing to check.
|
|
432
|
+
if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN && creditMode == CreditMode.CONSTANT && credits == 0) {
|
|
433
|
+
revert InvalidCredits();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
RoundData storage round = e3Data[e3Id];
|
|
437
|
+
// we need to know the number of options for decoding the tally
|
|
438
|
+
round.numOptions = numOptions;
|
|
439
|
+
// we want to save the credit mode so it can be verified on chain by everyone
|
|
440
|
+
round.creditMode = creditMode;
|
|
441
|
+
// recorded so anyone can verify which electorate the round was requested against
|
|
442
|
+
round.censusMode = CensusMode(rawCensusMode);
|
|
443
|
+
round.token = token;
|
|
444
|
+
round.minVotingPower = minVotingPower;
|
|
445
|
+
round.credits = credits;
|
|
446
|
+
|
|
447
|
+
// The snapshot is taken here rather than supplied by the requester. This function runs in the
|
|
448
|
+
// transaction that requests the E3, so `clock() - 1` is the last finalized timepoint of the
|
|
449
|
+
// round, and a requester cannot name a timepoint that suits it. Recording it once also makes
|
|
450
|
+
// every input of the round read the same electorate.
|
|
451
|
+
if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN) {
|
|
452
|
+
// Checked before any call is attempted. A call to an address with no code succeeds and
|
|
453
|
+
// returns nothing, so `clock()` fails while decoding the empty return data rather than
|
|
454
|
+
// inside the call — and a decode failure is not what `try/catch` is there to catch. An EOA
|
|
455
|
+
// would otherwise be refused by a bare panic instead of a named error.
|
|
456
|
+
if (token.code.length == 0) revert CensusModeRequiresToken();
|
|
457
|
+
|
|
458
|
+
uint48 snapshot = _previousTimepoint(token);
|
|
459
|
+
|
|
460
|
+
// Probe the exact call every input will make. `_previousTimepoint` swallows a missing
|
|
461
|
+
// `clock()` and falls back to block numbers, which is right for a token that predates
|
|
462
|
+
// ERC-6372 but also lets an address that is not a votes token pass validation — and then
|
|
463
|
+
// every `publishInput` reverts inside `getPastVotes`, after the fee is paid.
|
|
464
|
+
try IVotesToken(token).getPastVotes(address(0), snapshot) returns (uint256) {} catch {
|
|
465
|
+
revert CensusModeRequiresToken();
|
|
466
|
+
}
|
|
467
|
+
|
|
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;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
239
492
|
/// @inheritdoc IE3Program
|
|
240
493
|
function publishInput(uint256 e3Id, bytes memory data) external {
|
|
241
494
|
E3 memory e3 = interfold.getE3(e3Id);
|
|
@@ -256,34 +509,144 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
256
509
|
revert E3NotAcceptingInputs(e3Id);
|
|
257
510
|
}
|
|
258
511
|
|
|
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
512
|
if (data.length == 0) revert EmptyInputData();
|
|
263
513
|
|
|
264
|
-
(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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));
|
|
521
|
+
|
|
522
|
+
// The two census families differ here and nowhere else. A Merkle round proves membership
|
|
523
|
+
// inside the circuit against a posted root. An ONCHAIN round reads the power from the token
|
|
524
|
+
// and gives it to the circuit, so the eligibility check has to happen here instead.
|
|
525
|
+
(bytes32 eligibility, IHonkVerifier verifier) = _eligibility(e3Id, slotAddress);
|
|
526
|
+
|
|
527
|
+
bytes32 parentCommitment = _parentCommitment(e3Id, slotAddress, parentIndexPlusOne);
|
|
268
528
|
|
|
269
|
-
|
|
529
|
+
uint40 voteIndex = _processVote(e3Id, slotAddress, encryptedVoteCommitment, encryptedVote, parentIndexPlusOne);
|
|
270
530
|
|
|
271
531
|
// Set the public inputs for the proof. Order must match Noir circuit.
|
|
272
|
-
bytes32[] memory noirPublicInputs = new bytes32[](
|
|
273
|
-
noirPublicInputs[0] =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
532
|
+
bytes32[] memory noirPublicInputs = new bytes32[](9);
|
|
533
|
+
noirPublicInputs[0] = parentCommitment;
|
|
534
|
+
// A Keccak digest does not fit in one field element, so it enters the circuit as its two
|
|
535
|
+
// 16-byte halves. The circuit rebuilds the 32 bytes with `digest_from_halves`.
|
|
536
|
+
{
|
|
537
|
+
uint256 digest = uint256(ballotDigest(e3Id, slotAddress, encryptedVoteCommitment));
|
|
538
|
+
noirPublicInputs[1] = bytes32(digest >> 128);
|
|
539
|
+
noirPublicInputs[2] = bytes32(digest & type(uint128).max);
|
|
540
|
+
}
|
|
541
|
+
noirPublicInputs[3] = bytes32(uint256(uint160(slotAddress)));
|
|
542
|
+
noirPublicInputs[4] = eligibility;
|
|
543
|
+
noirPublicInputs[5] = bytes32(uint256(parentIndexPlusOne == 0 ? 1 : 0));
|
|
544
|
+
noirPublicInputs[6] = bytes32(e3Data[e3Id].numOptions);
|
|
545
|
+
noirPublicInputs[7] = encryptedVoteCommitment;
|
|
546
|
+
noirPublicInputs[8] = e3.committeePublicKey;
|
|
280
547
|
|
|
281
548
|
// Check if the ciphertext was encrypted correctly
|
|
282
|
-
if (!
|
|
549
|
+
if (!verifier.verify(noirProof, noirPublicInputs)) {
|
|
283
550
|
revert InvalidNoirProof();
|
|
284
551
|
}
|
|
285
552
|
|
|
286
|
-
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;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// @notice Resolve the eligibility public input and the verifier for a round.
|
|
581
|
+
/// @dev Returns the value that occupies index 4 of the circuit public inputs. The `crisp` and
|
|
582
|
+
/// `crisp_onchain` circuits agree on every other position, so this one value and the verifier
|
|
583
|
+
/// address are the whole difference between the two paths.
|
|
584
|
+
/// @param e3Id The E3 the input belongs to.
|
|
585
|
+
/// @param slotAddress The slot the input is written to.
|
|
586
|
+
/// @return eligibility The Merkle root of the census, or the voting power of the slot.
|
|
587
|
+
/// @return verifier The verifier that matches the circuit of this round.
|
|
588
|
+
function _eligibility(uint256 e3Id, address slotAddress) internal view returns (bytes32 eligibility, IHonkVerifier verifier) {
|
|
589
|
+
RoundData storage round = e3Data[e3Id];
|
|
590
|
+
|
|
591
|
+
if (round.censusMode != CensusMode.ONCHAIN) {
|
|
592
|
+
// We need to ensure that the CRISP admin set the merkle root of the census.
|
|
593
|
+
if (round.merkleRoot == 0) revert MerkleRootNotSet();
|
|
594
|
+
return (bytes32(round.merkleRoot), honkVerifier);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
uint256 rawPower = IVotesToken(round.token).getPastVotes(slotAddress, round.snapshot);
|
|
598
|
+
|
|
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.
|
|
602
|
+
uint256 threshold = round.minVotingPower == 0 ? 1 : round.minVotingPower;
|
|
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;
|
|
612
|
+
|
|
613
|
+
// Eligibility comes from the power at the snapshot. The weight the circuit enforces comes from
|
|
614
|
+
// the credit mode, so a CONSTANT round gives every eligible slot the same credits.
|
|
615
|
+
return (bytes32(round.creditMode == CreditMode.CONSTANT ? round.credits : power), onchainHonkVerifier);
|
|
616
|
+
}
|
|
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
|
+
|
|
639
|
+
/// @notice The last finalized timepoint of a token, in its ERC-6372 clock units.
|
|
640
|
+
/// @dev Falls back to block numbers when the token has no `clock()`, which matches the default
|
|
641
|
+
/// ERC20Votes clock.
|
|
642
|
+
/// @param token The token to read the clock of.
|
|
643
|
+
/// @return The timepoint before the current one.
|
|
644
|
+
function _previousTimepoint(address token) internal view returns (uint48) {
|
|
645
|
+
try IERC6372Clock(token).clock() returns (uint48 current) {
|
|
646
|
+
return current == 0 ? 0 : current - 1;
|
|
647
|
+
} catch {
|
|
648
|
+
return uint48(block.number - 1);
|
|
649
|
+
}
|
|
287
650
|
}
|
|
288
651
|
|
|
289
652
|
/// @notice Decode the tally from the plaintext output
|
|
@@ -328,15 +691,32 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
328
691
|
return votes;
|
|
329
692
|
}
|
|
330
693
|
|
|
331
|
-
/// @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.
|
|
332
700
|
/// @param e3Id The E3 program ID
|
|
333
701
|
/// @param slotAddress The slot address
|
|
334
|
-
/// @return The
|
|
702
|
+
/// @return The index of the last published input, or -1 if the slot is empty
|
|
335
703
|
function getSlotIndex(uint256 e3Id, address slotAddress) external view returns (int40) {
|
|
336
704
|
uint40 storedIndexPlusOne = e3Data[e3Id].voteSlots[slotAddress];
|
|
337
705
|
return int40(storedIndexPlusOne) - 1;
|
|
338
706
|
}
|
|
339
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
|
+
|
|
340
720
|
/// @inheritdoc IE3Program
|
|
341
721
|
function verify(
|
|
342
722
|
uint256 e3Id,
|
|
@@ -366,29 +746,56 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
366
746
|
return true;
|
|
367
747
|
}
|
|
368
748
|
|
|
369
|
-
/// @notice
|
|
370
|
-
/// 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.
|
|
371
750
|
function _processVote(
|
|
372
751
|
uint256 e3Id,
|
|
373
752
|
address slotAddress,
|
|
374
|
-
bytes32 encryptedVoteCommitment
|
|
375
|
-
|
|
376
|
-
uint40
|
|
753
|
+
bytes32 encryptedVoteCommitment,
|
|
754
|
+
bytes memory encryptedVote,
|
|
755
|
+
uint40 parentIndexPlusOne
|
|
756
|
+
) internal returns (uint40 voteIndex) {
|
|
757
|
+
RoundData storage round = e3Data[e3Id];
|
|
377
758
|
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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;
|
|
392
799
|
}
|
|
393
800
|
|
|
394
801
|
/// @notice Decode bytes to uint64 array
|