@maci-protocol/core 0.0.0-ci.26f28d6

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.
@@ -0,0 +1,1087 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Poll = void 0;
7
+ const crypto_1 = require("@maci-protocol/crypto");
8
+ const domainobjs_1 = require("@maci-protocol/domainobjs");
9
+ const lean_imt_1 = require("@zk-kit/lean-imt");
10
+ const assert_1 = __importDefault(require("assert"));
11
+ const constants_1 = require("./utils/constants");
12
+ const errors_1 = require("./utils/errors");
13
+ /**
14
+ * A representation of the Poll contract.
15
+ */
16
+ class Poll {
17
+ /**
18
+ * Constructs a new Poll object.
19
+ * @param pollEndTimestamp - The Unix timestamp at which the poll ends.
20
+ * @param coordinatorKeypair - The keypair of the coordinator.
21
+ * @param treeDepths - The depths of the trees used in the poll.
22
+ * @param batchSizes - The sizes of the batches used in the poll.
23
+ * @param maciStateRef - The reference to the MACI state.
24
+ * @param pollId - The poll id
25
+ */
26
+ constructor(pollEndTimestamp, coordinatorKeypair, treeDepths, batchSizes, maciStateRef, voteOptions) {
27
+ this.ballots = [];
28
+ this.messages = [];
29
+ this.commands = [];
30
+ this.encPubKeys = [];
31
+ this.stateCopied = false;
32
+ this.pubKeys = [domainobjs_1.padKey];
33
+ // For message processing
34
+ this.numBatchesProcessed = 0;
35
+ this.sbSalts = {};
36
+ this.resultRootSalts = {};
37
+ this.preVOSpentVoiceCreditsRootSalts = {};
38
+ this.spentVoiceCreditSubtotalSalts = {};
39
+ // For vote tallying
40
+ this.tallyResult = [];
41
+ this.perVOSpentVoiceCredits = [];
42
+ this.numBatchesTallied = 0;
43
+ this.totalSpentVoiceCredits = 0n;
44
+ // message chain hash
45
+ this.chainHash = crypto_1.NOTHING_UP_MY_SLEEVE;
46
+ // batch chain hashes
47
+ this.batchHashes = [crypto_1.NOTHING_UP_MY_SLEEVE];
48
+ // Poll state tree leaves
49
+ this.pollStateLeaves = [domainobjs_1.blankStateLeaf];
50
+ // how many users signed up
51
+ this.numSignups = 0n;
52
+ /**
53
+ * Check if user has already joined the poll by checking if the nullifier is registered
54
+ */
55
+ this.hasJoined = (nullifier) => this.pollNullifiers.get(nullifier) != null;
56
+ /**
57
+ * Join the anonymous user to the Poll (to the tree)
58
+ * @param nullifier - Hashed private key used as nullifier
59
+ * @param pubKey - The poll public key.
60
+ * @param newVoiceCreditBalance - New voice credit balance of the user.
61
+ * @param timestamp - The timestamp of the sign-up.
62
+ * @returns The index of added state leaf
63
+ */
64
+ this.joinPoll = (nullifier, pubKey, newVoiceCreditBalance, timestamp) => {
65
+ const stateLeaf = new domainobjs_1.StateLeaf(pubKey, newVoiceCreditBalance, timestamp);
66
+ if (this.hasJoined(nullifier)) {
67
+ throw new Error("UserAlreadyJoined");
68
+ }
69
+ this.pollNullifiers.set(nullifier, true);
70
+ this.pollStateLeaves.push(stateLeaf.copy());
71
+ this.pollStateTree?.insert(stateLeaf.hash());
72
+ return this.pollStateLeaves.length - 1;
73
+ };
74
+ /**
75
+ * Update a Poll with data from MaciState.
76
+ * This is the step where we copy the state from the MaciState instance,
77
+ * and set the number of signups we have so far.
78
+ * @note It should be called to generate the state for poll joining with numSignups set as
79
+ * the number of signups in the MaciState. For message processing, you should set numSignups as
80
+ * the number of users who joined the poll.
81
+ */
82
+ this.updatePoll = (numSignups) => {
83
+ // there might be occasions where we fetch logs after new signups have been made
84
+ // logs are fetched (and MaciState/Poll created locally).
85
+ // If someone signs up after that and we fetch that record
86
+ // then we won't be able to verify the processing on chain as the data will
87
+ // not match. For this, we must only copy up to the number of signups
88
+ // Copy the state tree, ballot tree, state leaves, and ballot leaves
89
+ // start by setting the number of signups
90
+ this.setNumSignups(numSignups);
91
+ // copy up to numSignups state leaves
92
+ this.pubKeys = this.maciStateRef.pubKeys.slice(0, Number(this.numSignups)).map((x) => x.copy());
93
+ // ensure we have the correct actual state tree depth value
94
+ this.actualStateTreeDepth = Math.max(1, Math.ceil(Math.log2(Number(this.numSignups))));
95
+ this.stateTree = new lean_imt_1.LeanIMT(crypto_1.hashLeanIMT);
96
+ // add all leaves
97
+ this.pubKeys.forEach((pubKey) => {
98
+ this.stateTree?.insert(pubKey.hash());
99
+ });
100
+ // create a poll state tree
101
+ this.pollStateTree = new crypto_1.IncrementalQuinTree(this.actualStateTreeDepth, domainobjs_1.blankStateLeafHash, constants_1.STATE_TREE_ARITY, crypto_1.hash2);
102
+ this.pollStateLeaves.forEach((stateLeaf) => {
103
+ this.pollStateTree?.insert(stateLeaf.hash());
104
+ });
105
+ // Create as many ballots as state leaves
106
+ this.emptyBallotHash = this.emptyBallot.hash();
107
+ this.ballotTree = new crypto_1.IncrementalQuinTree(this.stateTreeDepth, this.emptyBallotHash, constants_1.STATE_TREE_ARITY, crypto_1.hash2);
108
+ this.ballotTree.insert(this.emptyBallotHash);
109
+ // we fill the ballotTree with empty ballots hashes to match the number of signups in the tree
110
+ while (this.ballots.length < this.pubKeys.length) {
111
+ this.ballotTree.insert(this.emptyBallotHash);
112
+ this.ballots.push(this.emptyBallot);
113
+ }
114
+ this.stateCopied = true;
115
+ };
116
+ /**
117
+ * Process one message.
118
+ * @param message - The message to process.
119
+ * @param encPubKey - The public key associated with the encryption private key.
120
+ * @returns A number of variables which will be used in the zk-SNARK circuit.
121
+ */
122
+ this.processMessage = (message, encPubKey, qv = true) => {
123
+ try {
124
+ // Decrypt the message
125
+ const sharedKey = domainobjs_1.Keypair.genEcdhSharedKey(this.coordinatorKeypair.privKey, encPubKey);
126
+ const { command, signature } = domainobjs_1.PCommand.decrypt(message, sharedKey);
127
+ const stateLeafIndex = command.stateIndex;
128
+ // If the state tree index in the command is invalid, do nothing
129
+ if (stateLeafIndex >= BigInt(this.ballots.length) ||
130
+ stateLeafIndex < 1n ||
131
+ stateLeafIndex >= BigInt(this.pollStateTree?.nextIndex || -1)) {
132
+ throw new errors_1.ProcessMessageError(errors_1.ProcessMessageErrors.InvalidStateLeafIndex);
133
+ }
134
+ // The user to update (or not)
135
+ const stateLeaf = this.pollStateLeaves[Number(stateLeafIndex)];
136
+ // The ballot to update (or not)
137
+ const ballot = this.ballots[Number(stateLeafIndex)];
138
+ // If the signature is invalid, do nothing
139
+ if (!command.verifySignature(signature, stateLeaf.pubKey)) {
140
+ throw new errors_1.ProcessMessageError(errors_1.ProcessMessageErrors.InvalidSignature);
141
+ }
142
+ // If the nonce is invalid, do nothing
143
+ if (command.nonce !== ballot.nonce + 1n) {
144
+ throw new errors_1.ProcessMessageError(errors_1.ProcessMessageErrors.InvalidNonce);
145
+ }
146
+ // If the vote option index is invalid, do nothing
147
+ if (command.voteOptionIndex < 0n || command.voteOptionIndex >= BigInt(this.voteOptions)) {
148
+ throw new errors_1.ProcessMessageError(errors_1.ProcessMessageErrors.InvalidVoteOptionIndex);
149
+ }
150
+ const voteOptionIndex = Number(command.voteOptionIndex);
151
+ const originalVoteWeight = ballot.votes[voteOptionIndex];
152
+ // the voice credits left are:
153
+ // voiceCreditsBalance (how many the user has) +
154
+ // voiceCreditsPreviouslySpent (the original vote weight for this option) ** 2 -
155
+ // command.newVoteWeight ** 2 (the new vote weight squared)
156
+ // basically we are replacing the previous vote weight for this
157
+ // particular vote option with the new one
158
+ // but we need to ensure that we are not going >= balance
159
+ // @note that above comment is valid for quadratic voting
160
+ // for non quadratic voting, we simply remove the exponentiation
161
+ const voiceCreditsLeft = qv
162
+ ? stateLeaf.voiceCreditBalance +
163
+ originalVoteWeight * originalVoteWeight -
164
+ command.newVoteWeight * command.newVoteWeight
165
+ : stateLeaf.voiceCreditBalance + originalVoteWeight - command.newVoteWeight;
166
+ // If the remaining voice credits is insufficient, do nothing
167
+ if (voiceCreditsLeft < 0n) {
168
+ throw new errors_1.ProcessMessageError(errors_1.ProcessMessageErrors.InsufficientVoiceCredits);
169
+ }
170
+ // Deep-copy the state leaf and update its attributes
171
+ const newStateLeaf = stateLeaf.copy();
172
+ newStateLeaf.voiceCreditBalance = voiceCreditsLeft;
173
+ // if the key changes, this is effectively a key-change message too
174
+ newStateLeaf.pubKey = command.newPubKey.copy();
175
+ // Deep-copy the ballot and update its attributes
176
+ const newBallot = ballot.copy();
177
+ // increase the nonce
178
+ newBallot.nonce += 1n;
179
+ // we change the vote for this exact vote option
180
+ newBallot.votes[voteOptionIndex] = command.newVoteWeight;
181
+ // calculate the path elements for the state tree given the original state tree (before any changes)
182
+ // changes could effectively be made by this new vote - either a key change or vote change
183
+ // would result in a different state leaf
184
+ const originalStateLeafPathElements = this.pollStateTree?.genProof(Number(stateLeafIndex)).pathElements;
185
+ // calculate the path elements for the ballot tree given the original ballot tree (before any changes)
186
+ // changes could effectively be made by this new ballot
187
+ const originalBallotPathElements = this.ballotTree?.genProof(Number(stateLeafIndex)).pathElements;
188
+ // create a new quinary tree where we insert the votes of the origin (up until this message is processed) ballot
189
+ const vt = new crypto_1.IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, constants_1.VOTE_OPTION_TREE_ARITY, crypto_1.hash5);
190
+ for (let i = 0; i < this.ballots[0].votes.length; i += 1) {
191
+ vt.insert(ballot.votes[i]);
192
+ }
193
+ // calculate the path elements for the vote option tree given the original vote option tree (before any changes)
194
+ const originalVoteWeightsPathElements = vt.genProof(voteOptionIndex).pathElements;
195
+ // we return the data which is then to be used in the processMessage circuit
196
+ // to generate a proof of processing
197
+ return {
198
+ stateLeafIndex: Number(stateLeafIndex),
199
+ newStateLeaf,
200
+ originalStateLeaf: stateLeaf.copy(),
201
+ originalStateLeafPathElements,
202
+ originalVoteWeight,
203
+ originalVoteWeightsPathElements,
204
+ newBallot,
205
+ originalBallot: ballot.copy(),
206
+ originalBallotPathElements,
207
+ command,
208
+ };
209
+ }
210
+ catch (e) {
211
+ if (e instanceof errors_1.ProcessMessageError) {
212
+ throw e;
213
+ }
214
+ else {
215
+ throw new errors_1.ProcessMessageError(errors_1.ProcessMessageErrors.FailedDecryption);
216
+ }
217
+ }
218
+ };
219
+ /**
220
+ * Inserts a Message and the corresponding public key used to generate the
221
+ * ECDH shared key which was used to encrypt said message.
222
+ * @param message - The message to insert
223
+ * @param encPubKey - The public key used to encrypt the message
224
+ */
225
+ this.publishMessage = (message, encPubKey) => {
226
+ (0, assert_1.default)(encPubKey.rawPubKey[0] < crypto_1.SNARK_FIELD_SIZE && encPubKey.rawPubKey[1] < crypto_1.SNARK_FIELD_SIZE, "The public key is not in the correct range");
227
+ message.data.forEach((d) => {
228
+ (0, assert_1.default)(d < crypto_1.SNARK_FIELD_SIZE, "The message data is not in the correct range");
229
+ });
230
+ // store the encryption pub key
231
+ this.encPubKeys.push(encPubKey);
232
+ // store the message locally
233
+ this.messages.push(message);
234
+ // add the message hash to the message tree
235
+ const messageHash = message.hash(encPubKey);
236
+ // update chain hash
237
+ this.updateChainHash(messageHash);
238
+ // Decrypt the message and store the Command
239
+ // step 1. we generate the shared key
240
+ const sharedKey = domainobjs_1.Keypair.genEcdhSharedKey(this.coordinatorKeypair.privKey, encPubKey);
241
+ try {
242
+ // step 2. we decrypt it
243
+ const { command } = domainobjs_1.PCommand.decrypt(message, sharedKey);
244
+ // step 3. we store it in the commands array
245
+ this.commands.push(command);
246
+ }
247
+ catch (e) {
248
+ // if there is an error we store an empty command
249
+ const keyPair = new domainobjs_1.Keypair();
250
+ const command = new domainobjs_1.PCommand(0n, keyPair.pubKey, 0n, 0n, 0n, 0n, 0n);
251
+ this.commands.push(command);
252
+ }
253
+ };
254
+ /**
255
+ * Updates message chain hash
256
+ * @param messageHash hash of message with encPubKey
257
+ */
258
+ this.updateChainHash = (messageHash) => {
259
+ this.chainHash = (0, crypto_1.hash2)([this.chainHash, messageHash]);
260
+ if (this.messages.length % this.batchSizes.messageBatchSize === 0) {
261
+ this.batchHashes.push(this.chainHash);
262
+ this.currentMessageBatchIndex += 1;
263
+ }
264
+ };
265
+ /**
266
+ * Create circuit input for pollJoining
267
+ * @param args Poll joining circuit inputs
268
+ * @returns stringified circuit inputs
269
+ */
270
+ this.joiningCircuitInputs = ({ maciPrivKey, stateLeafIndex, pollPubKey, }) => {
271
+ // calculate the path elements for the state tree given the original state tree
272
+ const { siblings, index } = this.stateTree.generateProof(Number(stateLeafIndex));
273
+ const siblingsLength = siblings.length;
274
+ // The index must be converted to a list of indices, 1 for each tree level.
275
+ // The circuit tree depth is this.stateTreeDepth, so the number of siblings must be this.stateTreeDepth,
276
+ // even if the tree depth is actually 3. The missing siblings can be set to 0, as they
277
+ // won't be used to calculate the root in the circuit.
278
+ const indices = [];
279
+ for (let i = 0; i < this.stateTreeDepth; i += 1) {
280
+ // eslint-disable-next-line no-bitwise
281
+ indices.push(BigInt((index >> i) & 1));
282
+ if (i >= siblingsLength) {
283
+ siblings[i] = BigInt(0);
284
+ }
285
+ }
286
+ const siblingsArray = siblings.map((sibling) => [sibling]);
287
+ // Create nullifier from private key
288
+ const inputNullifier = BigInt(maciPrivKey.asCircuitInputs());
289
+ const nullifier = (0, crypto_1.poseidon)([inputNullifier, this.pollId]);
290
+ // Get state tree's root
291
+ const stateRoot = this.stateTree.root;
292
+ // Set actualStateTreeDepth as number of initial siblings length
293
+ const actualStateTreeDepth = BigInt(siblingsLength);
294
+ const circuitInputs = {
295
+ privKey: maciPrivKey.asCircuitInputs(),
296
+ pollPubKey: pollPubKey.asCircuitInputs(),
297
+ siblings: siblingsArray,
298
+ indices,
299
+ nullifier,
300
+ stateRoot,
301
+ actualStateTreeDepth,
302
+ pollId: this.pollId,
303
+ };
304
+ return (0, crypto_1.stringifyBigInts)(circuitInputs);
305
+ };
306
+ /**
307
+ * Create circuit input for pollJoined
308
+ * @param args Poll joined circuit inputs
309
+ * @returns stringified circuit inputs
310
+ */
311
+ this.joinedCircuitInputs = ({ maciPrivKey, stateLeafIndex, voiceCreditsBalance, joinTimestamp, }) => {
312
+ // calculate the path elements for the state tree given the original state tree
313
+ const { pathElements, pathIndices } = this.pollStateTree.genProof(Number(stateLeafIndex));
314
+ // Get poll state tree's root
315
+ const stateRoot = this.pollStateTree.root;
316
+ const elementsLength = pathIndices.length;
317
+ for (let i = 0; i < this.stateTreeDepth; i += 1) {
318
+ if (i >= elementsLength) {
319
+ pathElements[i] = [0n];
320
+ pathIndices[i] = 0;
321
+ }
322
+ }
323
+ const circuitInputs = {
324
+ privKey: maciPrivKey.asCircuitInputs(),
325
+ pathElements: pathElements.map((item) => item.toString()),
326
+ voiceCreditsBalance: voiceCreditsBalance.toString(),
327
+ joinTimestamp: joinTimestamp.toString(),
328
+ pathIndices: pathIndices.map((item) => item.toString()),
329
+ actualStateTreeDepth: BigInt(this.actualStateTreeDepth),
330
+ stateRoot,
331
+ };
332
+ return (0, crypto_1.stringifyBigInts)(circuitInputs);
333
+ };
334
+ /**
335
+ * Pad last unclosed batch
336
+ */
337
+ this.padLastBatch = () => {
338
+ if (this.messages.length % this.batchSizes.messageBatchSize !== 0) {
339
+ this.batchHashes.push(this.chainHash);
340
+ }
341
+ };
342
+ /**
343
+ * This method checks if there are any unprocessed messages in the Poll instance.
344
+ * @returns Returns true if the number of processed batches is
345
+ * less than the total number of batches, false otherwise.
346
+ */
347
+ this.hasUnprocessedMessages = () => {
348
+ const batchSize = this.batchSizes.messageBatchSize;
349
+ let totalBatches = this.messages.length <= batchSize ? 1 : Math.floor(this.messages.length / batchSize);
350
+ if (this.messages.length > batchSize && this.messages.length % batchSize > 0) {
351
+ totalBatches += 1;
352
+ }
353
+ return this.numBatchesProcessed < totalBatches;
354
+ };
355
+ /**
356
+ * Process _batchSize messages starting from the saved index. This
357
+ * function will process messages even if the number of messages is not an
358
+ * exact multiple of _batchSize. e.g. if there are 10 messages, index is
359
+ * 8, and _batchSize is 4, this function will only process the last two
360
+ * messages in this.messages, and finally update the zeroth state leaf.
361
+ * Note that this function will only process as many state leaves as there
362
+ * are ballots to prevent accidental inclusion of a new user after this
363
+ * poll has concluded.
364
+ * @param pollId The ID of the poll associated with the messages to
365
+ * process
366
+ * @param quiet - Whether to log errors or not
367
+ * @returns stringified circuit inputs
368
+ */
369
+ this.processMessages = (pollId, qv = true, quiet = true) => {
370
+ (0, assert_1.default)(this.hasUnprocessedMessages(), "No more messages to process");
371
+ const batchSize = this.batchSizes.messageBatchSize;
372
+ if (this.numBatchesProcessed === 0) {
373
+ // Prevent other polls from being processed until this poll has
374
+ // been fully processed
375
+ this.maciStateRef.pollBeingProcessed = true;
376
+ this.maciStateRef.currentPollBeingProcessed = pollId;
377
+ this.padLastBatch();
378
+ this.currentMessageBatchIndex = this.batchHashes.length - 1;
379
+ this.sbSalts[this.currentMessageBatchIndex] = 0n;
380
+ }
381
+ // Only allow one poll to be processed at a time
382
+ if (this.maciStateRef.pollBeingProcessed) {
383
+ (0, assert_1.default)(this.maciStateRef.currentPollBeingProcessed === pollId, "Another poll is currently being processed");
384
+ }
385
+ // The starting index must be valid
386
+ (0, assert_1.default)(this.currentMessageBatchIndex >= 0, "The starting index must be >= 0");
387
+ // ensure we copy the state from MACI when we start processing the
388
+ // first batch
389
+ if (!this.stateCopied) {
390
+ throw new Error("You must update the poll with the correct data first");
391
+ }
392
+ // Generate circuit inputs
393
+ const circuitInputs = (0, crypto_1.stringifyBigInts)(this.genProcessMessagesCircuitInputsPartial(this.currentMessageBatchIndex));
394
+ // we want to store the state leaves at this point in time
395
+ // and the path elements of the state tree
396
+ const currentStateLeaves = [];
397
+ const currentStateLeavesPathElements = [];
398
+ // we want to store the ballots at this point in time
399
+ // and the path elements of the ballot tree
400
+ const currentBallots = [];
401
+ const currentBallotsPathElements = [];
402
+ // we want to store the vote weights at this point in time
403
+ // and the path elements of the vote weight tree
404
+ const currentVoteWeights = [];
405
+ const currentVoteWeightsPathElements = [];
406
+ // loop through the batch of messages
407
+ for (let i = 0; i < batchSize; i += 1) {
408
+ // we process the messages in reverse order
409
+ const idx = this.currentMessageBatchIndex * batchSize - i - 1;
410
+ (0, assert_1.default)(idx >= 0, "The message index must be >= 0");
411
+ let message;
412
+ let encPubKey;
413
+ if (idx < this.messages.length) {
414
+ message = this.messages[idx];
415
+ encPubKey = this.encPubKeys[idx];
416
+ try {
417
+ // check if the command is valid
418
+ const r = this.processMessage(message, encPubKey, qv);
419
+ const index = r.stateLeafIndex;
420
+ // we add at position 0 the original data
421
+ currentStateLeaves.unshift(r.originalStateLeaf);
422
+ currentBallots.unshift(r.originalBallot);
423
+ currentVoteWeights.unshift(r.originalVoteWeight);
424
+ currentVoteWeightsPathElements.unshift(r.originalVoteWeightsPathElements);
425
+ currentStateLeavesPathElements.unshift(r.originalStateLeafPathElements);
426
+ currentBallotsPathElements.unshift(r.originalBallotPathElements);
427
+ // update the state leaves with the new state leaf (result of processing the message)
428
+ this.pollStateLeaves[index] = r.newStateLeaf.copy();
429
+ // we also update the state tree with the hash of the new state leaf
430
+ this.pollStateTree?.update(index, r.newStateLeaf.hash());
431
+ // store the new ballot
432
+ this.ballots[index] = r.newBallot;
433
+ // update the ballot tree
434
+ this.ballotTree?.update(index, r.newBallot.hash());
435
+ }
436
+ catch (e) {
437
+ // if the error is not a ProcessMessageError we throw it and exit here
438
+ // otherwise we continue processing but add the default blank data instead of
439
+ // this invalid message
440
+ if (e instanceof errors_1.ProcessMessageError) {
441
+ // if logging is enabled, and it's not the first message, print the error
442
+ if (!quiet && idx !== 0) {
443
+ // eslint-disable-next-line no-console
444
+ console.log(`Error at message index ${idx} - ${e.message}`);
445
+ }
446
+ // @note we want to send the correct state leaf to the circuit
447
+ // even if a message is invalid
448
+ // this way if a message is invalid we can still generate a proof of processing
449
+ // we also want to prevent a DoS attack by a voter
450
+ // which sends a message that when force decrypted on the circuit
451
+ // results in a valid state index thus forcing the circuit to look
452
+ // for a valid state leaf, and failing to generate a proof
453
+ // gen shared key
454
+ const sharedKey = domainobjs_1.Keypair.genEcdhSharedKey(this.coordinatorKeypair.privKey, encPubKey);
455
+ // force decrypt it
456
+ const { command } = domainobjs_1.PCommand.decrypt(message, sharedKey, true);
457
+ // cache state leaf index
458
+ const stateLeafIndex = command.stateIndex;
459
+ // if the state leaf index is valid then use it
460
+ if (stateLeafIndex < this.pollStateLeaves.length) {
461
+ currentStateLeaves.unshift(this.pollStateLeaves[Number(stateLeafIndex)].copy());
462
+ currentStateLeavesPathElements.unshift(this.pollStateTree.genProof(Number(stateLeafIndex)).pathElements);
463
+ // copy the ballot
464
+ const ballot = this.ballots[Number(stateLeafIndex)].copy();
465
+ currentBallots.unshift(ballot);
466
+ currentBallotsPathElements.unshift(this.ballotTree.genProof(Number(stateLeafIndex)).pathElements);
467
+ // @note we check that command.voteOptionIndex is valid so < voteOptions
468
+ // this might be unnecessary but we do it to prevent a possible DoS attack
469
+ // from voters who could potentially encrypt a message in such as way that
470
+ // when decrypted it results in a valid state leaf index but an invalid vote option index
471
+ if (command.voteOptionIndex < this.voteOptions) {
472
+ currentVoteWeights.unshift(ballot.votes[Number(command.voteOptionIndex)]);
473
+ // create a new quinary tree and add all votes we have so far
474
+ const vt = new crypto_1.IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, constants_1.VOTE_OPTION_TREE_ARITY, crypto_1.hash5);
475
+ // fill the vote option tree with the votes we have so far
476
+ for (let j = 0; j < this.ballots[0].votes.length; j += 1) {
477
+ vt.insert(ballot.votes[j]);
478
+ }
479
+ // get the path elements for the first vote leaf
480
+ currentVoteWeightsPathElements.unshift(vt.genProof(Number(command.voteOptionIndex)).pathElements);
481
+ }
482
+ else {
483
+ currentVoteWeights.unshift(ballot.votes[0]);
484
+ // create a new quinary tree and add all votes we have so far
485
+ const vt = new crypto_1.IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, constants_1.VOTE_OPTION_TREE_ARITY, crypto_1.hash5);
486
+ // fill the vote option tree with the votes we have so far
487
+ for (let j = 0; j < this.ballots[0].votes.length; j += 1) {
488
+ vt.insert(ballot.votes[j]);
489
+ }
490
+ // get the path elements for the first vote leaf
491
+ currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements);
492
+ }
493
+ }
494
+ else {
495
+ // just use state leaf index 0
496
+ currentStateLeaves.unshift(this.pollStateLeaves[0].copy());
497
+ currentStateLeavesPathElements.unshift(this.pollStateTree.genProof(0).pathElements);
498
+ currentBallots.unshift(this.ballots[0].copy());
499
+ currentBallotsPathElements.unshift(this.ballotTree.genProof(0).pathElements);
500
+ // Since the command is invalid, we use a zero vote weight
501
+ currentVoteWeights.unshift(this.ballots[0].votes[0]);
502
+ // create a new quinary tree and add an empty vote
503
+ const vt = new crypto_1.IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, constants_1.VOTE_OPTION_TREE_ARITY, crypto_1.hash5);
504
+ vt.insert(this.ballots[0].votes[0]);
505
+ // get the path elements for this empty vote weight leaf
506
+ currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements);
507
+ }
508
+ }
509
+ else {
510
+ throw e;
511
+ }
512
+ }
513
+ }
514
+ else {
515
+ // Since we don't have a command at that position, use a blank state leaf
516
+ currentStateLeaves.unshift(this.pollStateLeaves[0].copy());
517
+ currentStateLeavesPathElements.unshift(this.pollStateTree.genProof(0).pathElements);
518
+ // since the command is invliad we use the blank ballot
519
+ currentBallots.unshift(this.ballots[0].copy());
520
+ currentBallotsPathElements.unshift(this.ballotTree.genProof(0).pathElements);
521
+ // Since the command is invalid, we use a zero vote weight
522
+ currentVoteWeights.unshift(this.ballots[0].votes[0]);
523
+ // create a new quinary tree and add an empty vote
524
+ const vt = new crypto_1.IncrementalQuinTree(this.treeDepths.voteOptionTreeDepth, 0n, constants_1.VOTE_OPTION_TREE_ARITY, crypto_1.hash5);
525
+ vt.insert(this.ballots[0].votes[0]);
526
+ // get the path elements for this empty vote weight leaf
527
+ currentVoteWeightsPathElements.unshift(vt.genProof(0).pathElements);
528
+ }
529
+ }
530
+ // store the data in the circuit inputs object
531
+ circuitInputs.currentStateLeaves = currentStateLeaves.map((x) => x.asCircuitInputs());
532
+ // we need to fill the array with 0s to match the length of the state leaves
533
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
534
+ for (let i = 0; i < currentStateLeavesPathElements.length; i += 1) {
535
+ while (currentStateLeavesPathElements[i].length < this.stateTreeDepth) {
536
+ currentStateLeavesPathElements[i].push([0n]);
537
+ }
538
+ }
539
+ circuitInputs.currentStateLeavesPathElements = currentStateLeavesPathElements;
540
+ circuitInputs.currentBallots = currentBallots.map((x) => x.asCircuitInputs());
541
+ circuitInputs.currentBallotsPathElements = currentBallotsPathElements;
542
+ circuitInputs.currentVoteWeights = currentVoteWeights;
543
+ circuitInputs.currentVoteWeightsPathElements = currentVoteWeightsPathElements;
544
+ // record that we processed one batch
545
+ this.numBatchesProcessed += 1;
546
+ if (this.currentMessageBatchIndex > 0) {
547
+ this.currentMessageBatchIndex -= 1;
548
+ }
549
+ // ensure newSbSalt differs from currentSbSalt
550
+ let newSbSalt = (0, crypto_1.genRandomSalt)();
551
+ while (this.sbSalts[this.currentMessageBatchIndex] === newSbSalt) {
552
+ newSbSalt = (0, crypto_1.genRandomSalt)();
553
+ }
554
+ this.sbSalts[this.currentMessageBatchIndex] = newSbSalt;
555
+ // store the salt in the circuit inputs
556
+ circuitInputs.newSbSalt = newSbSalt;
557
+ const newStateRoot = this.pollStateTree.root;
558
+ const newBallotRoot = this.ballotTree.root;
559
+ // create a commitment to the state and ballot tree roots
560
+ // this will be the hash of the roots with a salt
561
+ circuitInputs.newSbCommitment = (0, crypto_1.hash3)([newStateRoot, newBallotRoot, newSbSalt]);
562
+ const coordinatorPublicKeyHash = this.coordinatorKeypair.pubKey.hash();
563
+ // If this is the last batch, release the lock
564
+ if (this.numBatchesProcessed * batchSize >= this.messages.length) {
565
+ this.maciStateRef.pollBeingProcessed = false;
566
+ }
567
+ // ensure we pass the dynamic tree depth
568
+ circuitInputs.actualStateTreeDepth = this.actualStateTreeDepth.toString();
569
+ return (0, crypto_1.stringifyBigInts)({
570
+ ...circuitInputs,
571
+ coordinatorPublicKeyHash,
572
+ });
573
+ };
574
+ /**
575
+ * Generates partial circuit inputs for processing a batch of messages
576
+ * @param index - The index of the partial batch.
577
+ * @returns stringified partial circuit inputs
578
+ */
579
+ this.genProcessMessagesCircuitInputsPartial = (index) => {
580
+ const { messageBatchSize } = this.batchSizes;
581
+ (0, assert_1.default)(index <= this.messages.length, "The index must be <= the number of messages");
582
+ // fill the msgs array with a copy of the messages we have
583
+ // plus empty messages to fill the batch
584
+ // @note create a message with state index 0 to add as padding
585
+ // this way the message will look for state leaf 0
586
+ // and no effect will take place
587
+ // create a random key
588
+ const key = new domainobjs_1.Keypair();
589
+ // gen ecdh key
590
+ const ecdh = domainobjs_1.Keypair.genEcdhSharedKey(key.privKey, this.coordinatorKeypair.pubKey);
591
+ // create an empty command with state index 0n
592
+ const emptyCommand = new domainobjs_1.PCommand(0n, key.pubKey, 0n, 0n, 0n, 0n, 0n);
593
+ // encrypt it
594
+ const msg = emptyCommand.encrypt(emptyCommand.sign(key.privKey), ecdh);
595
+ // copy the messages to a new array
596
+ let msgs = this.messages.map((x) => x.asCircuitInputs());
597
+ // pad with our state index 0 message
598
+ while (msgs.length % messageBatchSize > 0) {
599
+ msgs.push(msg.asCircuitInputs());
600
+ }
601
+ // copy the public keys, pad the array with the last keys if needed
602
+ let encPubKeys = this.encPubKeys.map((x) => x.copy());
603
+ while (encPubKeys.length % messageBatchSize > 0) {
604
+ // pad with the public key used to encrypt the message with state index 0 (padding)
605
+ encPubKeys.push(key.pubKey.copy());
606
+ }
607
+ // validate that the batch index is correct, if not fix it
608
+ // this means that the end will be the last message
609
+ let batchEndIndex = index * messageBatchSize;
610
+ if (batchEndIndex > this.messages.length) {
611
+ batchEndIndex = this.messages.length;
612
+ }
613
+ const batchStartIndex = index > 0 ? (index - 1) * messageBatchSize : 0;
614
+ // we only take the messages we need for this batch
615
+ // it slice msgs array from index of first message in current batch to
616
+ // index of last message in current batch
617
+ msgs = msgs.slice(batchStartIndex, index * messageBatchSize);
618
+ // then take the ones part of this batch
619
+ encPubKeys = encPubKeys.slice(batchStartIndex, index * messageBatchSize);
620
+ // cache tree roots
621
+ const currentStateRoot = this.pollStateTree.root;
622
+ const currentBallotRoot = this.ballotTree.root;
623
+ // calculate the current state and ballot root
624
+ // commitment which is the hash of the state tree
625
+ // root, the ballot tree root and a salt
626
+ const currentSbCommitment = (0, crypto_1.hash3)([currentStateRoot, currentBallotRoot, this.sbSalts[index]]);
627
+ const inputBatchHash = this.batchHashes[index - 1];
628
+ const outputBatchHash = this.batchHashes[index];
629
+ return (0, crypto_1.stringifyBigInts)({
630
+ numSignUps: BigInt(this.numSignups),
631
+ batchEndIndex: BigInt(batchEndIndex),
632
+ index: BigInt(batchStartIndex),
633
+ inputBatchHash,
634
+ outputBatchHash,
635
+ msgs,
636
+ actualStateTreeDepth: BigInt(this.actualStateTreeDepth),
637
+ coordPrivKey: this.coordinatorKeypair.privKey.asCircuitInputs(),
638
+ encPubKeys: encPubKeys.map((x) => x.asCircuitInputs()),
639
+ currentStateRoot,
640
+ currentBallotRoot,
641
+ currentSbCommitment,
642
+ currentSbSalt: this.sbSalts[this.currentMessageBatchIndex],
643
+ voteOptions: this.voteOptions,
644
+ });
645
+ };
646
+ /**
647
+ * Process all messages. This function does not update the ballots or state
648
+ * leaves; rather, it copies and then updates them. This makes it possible
649
+ * to test the result of multiple processMessage() invocations.
650
+ * @returns The state leaves and ballots of the poll
651
+ */
652
+ this.processAllMessages = () => {
653
+ const stateLeaves = this.pollStateLeaves.map((x) => x.copy());
654
+ const ballots = this.ballots.map((x) => x.copy());
655
+ // process all messages in one go (batch by batch but without manual intervention)
656
+ while (this.hasUnprocessedMessages()) {
657
+ this.processMessages(this.pollId);
658
+ }
659
+ return { stateLeaves, ballots };
660
+ };
661
+ /**
662
+ * Checks whether there are any untallied ballots.
663
+ * @returns Whether there are any untallied ballots
664
+ */
665
+ this.hasUntalliedBallots = () => this.numBatchesTallied * this.batchSizes.tallyBatchSize < this.ballots.length;
666
+ /**
667
+ * This method tallies a ballots and updates the tally results.
668
+ * @returns the circuit inputs for the TallyVotes circuit.
669
+ */
670
+ this.tallyVotes = () => {
671
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
672
+ if (this.sbSalts[this.currentMessageBatchIndex] === undefined) {
673
+ throw new Error("You must process the messages first");
674
+ }
675
+ const batchSize = this.batchSizes.tallyBatchSize;
676
+ (0, assert_1.default)(this.hasUntalliedBallots(), "No more ballots to tally");
677
+ // calculate where we start tallying next
678
+ const batchStartIndex = this.numBatchesTallied * batchSize;
679
+ // get the salts needed for the commitments
680
+ const currentResultsRootSalt = batchStartIndex === 0 ? 0n : this.resultRootSalts[batchStartIndex - batchSize];
681
+ const currentPerVOSpentVoiceCreditsRootSalt = batchStartIndex === 0 ? 0n : this.preVOSpentVoiceCreditsRootSalts[batchStartIndex - batchSize];
682
+ const currentSpentVoiceCreditSubtotalSalt = batchStartIndex === 0 ? 0n : this.spentVoiceCreditSubtotalSalts[batchStartIndex - batchSize];
683
+ // generate a commitment to the current results
684
+ const currentResultsCommitment = (0, crypto_1.genTreeCommitment)(this.tallyResult, currentResultsRootSalt, this.treeDepths.voteOptionTreeDepth);
685
+ // generate a commitment to the current per VO spent voice credits
686
+ const currentPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment(currentPerVOSpentVoiceCreditsRootSalt, batchStartIndex, true);
687
+ // generate a commitment to the current spent voice credits
688
+ const currentSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment(currentSpentVoiceCreditSubtotalSalt, batchStartIndex, true);
689
+ // the current commitment for the first batch will be 0
690
+ // otherwise calculate as
691
+ // hash([
692
+ // currentResultsCommitment,
693
+ // currentSpentVoiceCreditsCommitment,
694
+ // currentPerVOSpentVoiceCreditsCommitment
695
+ // ])
696
+ const currentTallyCommitment = batchStartIndex === 0
697
+ ? 0n
698
+ : (0, crypto_1.hash3)([
699
+ currentResultsCommitment,
700
+ currentSpentVoiceCreditsCommitment,
701
+ currentPerVOSpentVoiceCreditsCommitment,
702
+ ]);
703
+ const ballots = [];
704
+ const currentResults = this.tallyResult.map((x) => BigInt(x.toString()));
705
+ const currentPerVOSpentVoiceCredits = this.perVOSpentVoiceCredits.map((x) => BigInt(x.toString()));
706
+ const currentSpentVoiceCreditSubtotal = BigInt(this.totalSpentVoiceCredits.toString());
707
+ // loop in normal order to tally the ballots one by one
708
+ for (let i = this.numBatchesTallied * batchSize; i < this.numBatchesTallied * batchSize + batchSize; i += 1) {
709
+ // we stop if we have no more ballots to tally
710
+ if (i >= this.ballots.length) {
711
+ break;
712
+ }
713
+ // save to the local ballot array
714
+ ballots.push(this.ballots[i]);
715
+ // for each possible vote option we loop and calculate
716
+ for (let j = 0; j < this.maxVoteOptions; j += 1) {
717
+ const v = this.ballots[i].votes[j];
718
+ // the vote itself will be a quadratic vote (sqrt(voiceCredits))
719
+ this.tallyResult[j] += v;
720
+ // the per vote option spent voice credits will be the sum of the squares of the votes
721
+ this.perVOSpentVoiceCredits[j] += v * v;
722
+ // the total spent voice credits will be the sum of the squares of the votes
723
+ this.totalSpentVoiceCredits += v * v;
724
+ }
725
+ }
726
+ const emptyBallot = new domainobjs_1.Ballot(this.maxVoteOptions, this.treeDepths.voteOptionTreeDepth);
727
+ // pad the ballots array
728
+ while (ballots.length < batchSize) {
729
+ ballots.push(emptyBallot);
730
+ }
731
+ // generate the new salts
732
+ const newResultsRootSalt = (0, crypto_1.genRandomSalt)();
733
+ const newPerVOSpentVoiceCreditsRootSalt = (0, crypto_1.genRandomSalt)();
734
+ const newSpentVoiceCreditSubtotalSalt = (0, crypto_1.genRandomSalt)();
735
+ // and save them to be used in the next batch
736
+ this.resultRootSalts[batchStartIndex] = newResultsRootSalt;
737
+ this.preVOSpentVoiceCreditsRootSalts[batchStartIndex] = newPerVOSpentVoiceCreditsRootSalt;
738
+ this.spentVoiceCreditSubtotalSalts[batchStartIndex] = newSpentVoiceCreditSubtotalSalt;
739
+ // generate the new results commitment with the new salts and data
740
+ const newResultsCommitment = (0, crypto_1.genTreeCommitment)(this.tallyResult, newResultsRootSalt, this.treeDepths.voteOptionTreeDepth);
741
+ // generate the new spent voice credits commitment with the new salts and data
742
+ const newSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment(newSpentVoiceCreditSubtotalSalt, batchStartIndex + batchSize, true);
743
+ // generate the new per VO spent voice credits commitment with the new salts and data
744
+ const newPerVOSpentVoiceCreditsCommitment = this.genPerVOSpentVoiceCreditsCommitment(newPerVOSpentVoiceCreditsRootSalt, batchStartIndex + batchSize, true);
745
+ // generate the new tally commitment
746
+ const newTallyCommitment = (0, crypto_1.hash3)([
747
+ newResultsCommitment,
748
+ newSpentVoiceCreditsCommitment,
749
+ newPerVOSpentVoiceCreditsCommitment,
750
+ ]);
751
+ // cache vars
752
+ const stateRoot = this.pollStateTree.root;
753
+ const ballotRoot = this.ballotTree.root;
754
+ const sbSalt = this.sbSalts[this.currentMessageBatchIndex];
755
+ const sbCommitment = (0, crypto_1.hash3)([stateRoot, ballotRoot, sbSalt]);
756
+ const ballotSubrootProof = this.ballotTree?.genSubrootProof(batchStartIndex, batchStartIndex + batchSize);
757
+ const votes = ballots.map((x) => x.votes);
758
+ const circuitInputs = (0, crypto_1.stringifyBigInts)({
759
+ stateRoot,
760
+ ballotRoot,
761
+ sbSalt,
762
+ index: BigInt(batchStartIndex),
763
+ numSignUps: BigInt(this.numSignups),
764
+ sbCommitment,
765
+ currentTallyCommitment,
766
+ newTallyCommitment,
767
+ ballots: ballots.map((x) => x.asCircuitInputs()),
768
+ ballotPathElements: ballotSubrootProof.pathElements,
769
+ votes,
770
+ currentResults,
771
+ currentResultsRootSalt,
772
+ currentSpentVoiceCreditSubtotal,
773
+ currentSpentVoiceCreditSubtotalSalt,
774
+ currentPerVOSpentVoiceCredits,
775
+ currentPerVOSpentVoiceCreditsRootSalt,
776
+ newResultsRootSalt,
777
+ newPerVOSpentVoiceCreditsRootSalt,
778
+ newSpentVoiceCreditSubtotalSalt,
779
+ });
780
+ this.numBatchesTallied += 1;
781
+ return circuitInputs;
782
+ };
783
+ this.tallyVotesNonQv = () => {
784
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
785
+ if (this.sbSalts[this.currentMessageBatchIndex] === undefined) {
786
+ throw new Error("You must process the messages first");
787
+ }
788
+ const batchSize = this.batchSizes.tallyBatchSize;
789
+ (0, assert_1.default)(this.hasUntalliedBallots(), "No more ballots to tally");
790
+ // calculate where we start tallying next
791
+ const batchStartIndex = this.numBatchesTallied * batchSize;
792
+ // get the salts needed for the commitments
793
+ const currentResultsRootSalt = batchStartIndex === 0 ? 0n : this.resultRootSalts[batchStartIndex - batchSize];
794
+ const currentSpentVoiceCreditSubtotalSalt = batchStartIndex === 0 ? 0n : this.spentVoiceCreditSubtotalSalts[batchStartIndex - batchSize];
795
+ // generate a commitment to the current results
796
+ const currentResultsCommitment = (0, crypto_1.genTreeCommitment)(this.tallyResult, currentResultsRootSalt, this.treeDepths.voteOptionTreeDepth);
797
+ // generate a commitment to the current spent voice credits
798
+ const currentSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment(currentSpentVoiceCreditSubtotalSalt, batchStartIndex, false);
799
+ // the current commitment for the first batch will be 0
800
+ // otherwise calculate as
801
+ // hash([
802
+ // currentResultsCommitment,
803
+ // currentSpentVoiceCreditsCommitment,
804
+ // ])
805
+ const currentTallyCommitment = batchStartIndex === 0 ? 0n : (0, crypto_1.hashLeftRight)(currentResultsCommitment, currentSpentVoiceCreditsCommitment);
806
+ const ballots = [];
807
+ const currentResults = this.tallyResult.map((x) => BigInt(x.toString()));
808
+ const currentSpentVoiceCreditSubtotal = BigInt(this.totalSpentVoiceCredits.toString());
809
+ // loop in normal order to tally the ballots one by one
810
+ for (let i = this.numBatchesTallied * batchSize; i < this.numBatchesTallied * batchSize + batchSize; i += 1) {
811
+ // we stop if we have no more ballots to tally
812
+ if (i >= this.ballots.length) {
813
+ break;
814
+ }
815
+ // save to the local ballot array
816
+ ballots.push(this.ballots[i]);
817
+ // for each possible vote option we loop and calculate
818
+ for (let j = 0; j < this.maxVoteOptions; j += 1) {
819
+ const v = this.ballots[i].votes[j];
820
+ this.tallyResult[j] += v;
821
+ // the total spent voice credits will be the sum of the votes
822
+ this.totalSpentVoiceCredits += v;
823
+ }
824
+ }
825
+ const emptyBallot = new domainobjs_1.Ballot(this.maxVoteOptions, this.treeDepths.voteOptionTreeDepth);
826
+ // pad the ballots array
827
+ while (ballots.length < batchSize) {
828
+ ballots.push(emptyBallot);
829
+ }
830
+ // generate the new salts
831
+ const newResultsRootSalt = (0, crypto_1.genRandomSalt)();
832
+ const newSpentVoiceCreditSubtotalSalt = (0, crypto_1.genRandomSalt)();
833
+ // and save them to be used in the next batch
834
+ this.resultRootSalts[batchStartIndex] = newResultsRootSalt;
835
+ this.spentVoiceCreditSubtotalSalts[batchStartIndex] = newSpentVoiceCreditSubtotalSalt;
836
+ // generate the new results commitment with the new salts and data
837
+ const newResultsCommitment = (0, crypto_1.genTreeCommitment)(this.tallyResult, newResultsRootSalt, this.treeDepths.voteOptionTreeDepth);
838
+ // generate the new spent voice credits commitment with the new salts and data
839
+ const newSpentVoiceCreditsCommitment = this.genSpentVoiceCreditSubtotalCommitment(newSpentVoiceCreditSubtotalSalt, batchStartIndex + batchSize, false);
840
+ // generate the new tally commitment
841
+ const newTallyCommitment = (0, crypto_1.hashLeftRight)(newResultsCommitment, newSpentVoiceCreditsCommitment);
842
+ // cache vars
843
+ const stateRoot = this.pollStateTree.root;
844
+ const ballotRoot = this.ballotTree.root;
845
+ const sbSalt = this.sbSalts[this.currentMessageBatchIndex];
846
+ const sbCommitment = (0, crypto_1.hash3)([stateRoot, ballotRoot, sbSalt]);
847
+ const ballotSubrootProof = this.ballotTree?.genSubrootProof(batchStartIndex, batchStartIndex + batchSize);
848
+ const votes = ballots.map((x) => x.votes);
849
+ const circuitInputs = (0, crypto_1.stringifyBigInts)({
850
+ stateRoot,
851
+ ballotRoot,
852
+ sbSalt,
853
+ index: BigInt(batchStartIndex),
854
+ numSignUps: BigInt(this.numSignups),
855
+ sbCommitment,
856
+ currentTallyCommitment,
857
+ newTallyCommitment,
858
+ ballots: ballots.map((x) => x.asCircuitInputs()),
859
+ ballotPathElements: ballotSubrootProof.pathElements,
860
+ votes,
861
+ currentResults,
862
+ currentResultsRootSalt,
863
+ currentSpentVoiceCreditSubtotal,
864
+ currentSpentVoiceCreditSubtotalSalt,
865
+ newResultsRootSalt,
866
+ newSpentVoiceCreditSubtotalSalt,
867
+ });
868
+ this.numBatchesTallied += 1;
869
+ return circuitInputs;
870
+ };
871
+ /**
872
+ * This method generates a commitment to the total spent voice credits.
873
+ *
874
+ * This is the hash of the total spent voice credits and a salt, computed as Poseidon([totalCredits, _salt]).
875
+ * @param salt - The salt used in the hash function.
876
+ * @param numBallotsToCount - The number of ballots to count for the calculation.
877
+ * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true.
878
+ * @returns Returns the hash of the total spent voice credits and a salt, computed as Poseidon([totalCredits, _salt]).
879
+ */
880
+ this.genSpentVoiceCreditSubtotalCommitment = (salt, numBallotsToCount, useQuadraticVoting = true) => {
881
+ let subtotal = 0n;
882
+ for (let i = 0; i < numBallotsToCount; i += 1) {
883
+ if (this.ballots.length <= i) {
884
+ break;
885
+ }
886
+ for (let j = 0; j < this.tallyResult.length; j += 1) {
887
+ const v = BigInt(`${this.ballots[i].votes[j]}`);
888
+ subtotal += useQuadraticVoting ? v * v : v;
889
+ }
890
+ }
891
+ return (0, crypto_1.hashLeftRight)(subtotal, salt);
892
+ };
893
+ /**
894
+ * This method generates a commitment to the spent voice credits per vote option.
895
+ *
896
+ * This is the hash of the Merkle root of the spent voice credits per vote option and a salt, computed as Poseidon([root, _salt]).
897
+ * @param salt - The salt used in the hash function.
898
+ * @param numBallotsToCount - The number of ballots to count for the calculation.
899
+ * @param useQuadraticVoting - Whether to use quadratic voting or not. Default is true.
900
+ * @returns Returns the hash of the Merkle root of the spent voice credits per vote option and a salt, computed as Poseidon([root, _salt]).
901
+ */
902
+ this.genPerVOSpentVoiceCreditsCommitment = (salt, numBallotsToCount, useQuadraticVoting = true) => {
903
+ const leaves = Array(this.tallyResult.length).fill(0n);
904
+ for (let i = 0; i < numBallotsToCount; i += 1) {
905
+ // check that is a valid index
906
+ if (i >= this.ballots.length) {
907
+ break;
908
+ }
909
+ for (let j = 0; j < this.tallyResult.length; j += 1) {
910
+ const v = this.ballots[i].votes[j];
911
+ leaves[j] += useQuadraticVoting ? v * v : v;
912
+ }
913
+ }
914
+ return (0, crypto_1.genTreeCommitment)(leaves, salt, this.treeDepths.voteOptionTreeDepth);
915
+ };
916
+ /**
917
+ * Create a deep copy of the Poll object.
918
+ * @returns A new instance of the Poll object with the same properties.
919
+ */
920
+ this.copy = () => {
921
+ const copied = new Poll(BigInt(this.pollEndTimestamp.toString()), this.coordinatorKeypair.copy(), {
922
+ intStateTreeDepth: Number(this.treeDepths.intStateTreeDepth),
923
+ voteOptionTreeDepth: Number(this.treeDepths.voteOptionTreeDepth),
924
+ }, {
925
+ tallyBatchSize: Number(this.batchSizes.tallyBatchSize.toString()),
926
+ messageBatchSize: Number(this.batchSizes.messageBatchSize.toString()),
927
+ }, this.maciStateRef, this.voteOptions);
928
+ copied.pubKeys = this.pubKeys.map((x) => x.copy());
929
+ copied.pollStateLeaves = this.pollStateLeaves.map((x) => x.copy());
930
+ copied.messages = this.messages.map((x) => x.copy());
931
+ copied.commands = this.commands.map((x) => x.copy());
932
+ copied.ballots = this.ballots.map((x) => x.copy());
933
+ copied.encPubKeys = this.encPubKeys.map((x) => x.copy());
934
+ if (this.ballotTree) {
935
+ copied.ballotTree = this.ballotTree.copy();
936
+ }
937
+ copied.currentMessageBatchIndex = this.currentMessageBatchIndex;
938
+ copied.maciStateRef = this.maciStateRef;
939
+ copied.tallyResult = this.tallyResult.map((x) => BigInt(x.toString()));
940
+ copied.perVOSpentVoiceCredits = this.perVOSpentVoiceCredits.map((x) => BigInt(x.toString()));
941
+ copied.numBatchesProcessed = Number(this.numBatchesProcessed.toString());
942
+ copied.numBatchesTallied = Number(this.numBatchesTallied.toString());
943
+ copied.pollId = this.pollId;
944
+ copied.totalSpentVoiceCredits = BigInt(this.totalSpentVoiceCredits.toString());
945
+ copied.sbSalts = {};
946
+ copied.resultRootSalts = {};
947
+ copied.preVOSpentVoiceCreditsRootSalts = {};
948
+ copied.spentVoiceCreditSubtotalSalts = {};
949
+ Object.keys(this.sbSalts).forEach((k) => {
950
+ copied.sbSalts[k] = BigInt(this.sbSalts[k].toString());
951
+ });
952
+ Object.keys(this.resultRootSalts).forEach((k) => {
953
+ copied.resultRootSalts[k] = BigInt(this.resultRootSalts[k].toString());
954
+ });
955
+ Object.keys(this.preVOSpentVoiceCreditsRootSalts).forEach((k) => {
956
+ copied.preVOSpentVoiceCreditsRootSalts[k] = BigInt(this.preVOSpentVoiceCreditsRootSalts[k].toString());
957
+ });
958
+ Object.keys(this.spentVoiceCreditSubtotalSalts).forEach((k) => {
959
+ copied.spentVoiceCreditSubtotalSalts[k] = BigInt(this.spentVoiceCreditSubtotalSalts[k].toString());
960
+ });
961
+ // update the number of signups
962
+ copied.setNumSignups(this.numSignups);
963
+ return copied;
964
+ };
965
+ /**
966
+ * Check if the Poll object is equal to another Poll object.
967
+ * @param p - The Poll object to compare.
968
+ * @returns True if the two Poll objects are equal, false otherwise.
969
+ */
970
+ this.equals = (p) => {
971
+ const result = this.coordinatorKeypair.equals(p.coordinatorKeypair) &&
972
+ this.treeDepths.intStateTreeDepth === p.treeDepths.intStateTreeDepth &&
973
+ this.treeDepths.voteOptionTreeDepth === p.treeDepths.voteOptionTreeDepth &&
974
+ this.batchSizes.tallyBatchSize === p.batchSizes.tallyBatchSize &&
975
+ this.batchSizes.messageBatchSize === p.batchSizes.messageBatchSize &&
976
+ this.maxVoteOptions === p.maxVoteOptions &&
977
+ this.messages.length === p.messages.length &&
978
+ this.encPubKeys.length === p.encPubKeys.length &&
979
+ this.numSignups === p.numSignups;
980
+ if (!result) {
981
+ return false;
982
+ }
983
+ for (let i = 0; i < this.messages.length; i += 1) {
984
+ if (!this.messages[i].equals(p.messages[i])) {
985
+ return false;
986
+ }
987
+ }
988
+ for (let i = 0; i < this.encPubKeys.length; i += 1) {
989
+ if (!this.encPubKeys[i].equals(p.encPubKeys[i])) {
990
+ return false;
991
+ }
992
+ }
993
+ return true;
994
+ };
995
+ /**
996
+ * Set the coordinator's keypair
997
+ * @param serializedPrivateKey - the serialized private key
998
+ */
999
+ this.setCoordinatorKeypair = (serializedPrivateKey) => {
1000
+ this.coordinatorKeypair = new domainobjs_1.Keypair(domainobjs_1.PrivKey.deserialize(serializedPrivateKey));
1001
+ };
1002
+ /**
1003
+ * Set the number of signups to match the ones from the contract
1004
+ * @param numSignups - the number of signups
1005
+ */
1006
+ this.setNumSignups = (numSignups) => {
1007
+ this.numSignups = numSignups;
1008
+ };
1009
+ /**
1010
+ * Get the number of signups
1011
+ * @returns The number of signups
1012
+ */
1013
+ this.getNumSignups = () => this.numSignups;
1014
+ this.pollEndTimestamp = pollEndTimestamp;
1015
+ this.coordinatorKeypair = coordinatorKeypair;
1016
+ this.treeDepths = treeDepths;
1017
+ this.batchSizes = batchSizes;
1018
+ if (voteOptions > constants_1.VOTE_OPTION_TREE_ARITY ** treeDepths.voteOptionTreeDepth) {
1019
+ throw new Error("Vote options cannot be greater than the number of leaves in the vote option tree");
1020
+ }
1021
+ this.voteOptions = voteOptions;
1022
+ this.maxVoteOptions = constants_1.VOTE_OPTION_TREE_ARITY ** treeDepths.voteOptionTreeDepth;
1023
+ this.maciStateRef = maciStateRef;
1024
+ this.pollId = BigInt(maciStateRef.polls.size);
1025
+ this.stateTreeDepth = maciStateRef.stateTreeDepth;
1026
+ this.actualStateTreeDepth = maciStateRef.stateTreeDepth;
1027
+ this.currentMessageBatchIndex = 0;
1028
+ this.pollNullifiers = new Map();
1029
+ this.tallyResult = new Array(this.maxVoteOptions).fill(0n);
1030
+ this.perVOSpentVoiceCredits = new Array(this.maxVoteOptions).fill(0n);
1031
+ // we put a blank state leaf to prevent a DoS attack
1032
+ this.emptyBallot = domainobjs_1.Ballot.genBlankBallot(this.maxVoteOptions, treeDepths.voteOptionTreeDepth);
1033
+ this.ballots.push(this.emptyBallot);
1034
+ }
1035
+ /**
1036
+ * Serialize the Poll object to a JSON object
1037
+ * @returns a JSON object
1038
+ */
1039
+ toJSON() {
1040
+ return {
1041
+ pollEndTimestamp: this.pollEndTimestamp.toString(),
1042
+ treeDepths: this.treeDepths,
1043
+ batchSizes: this.batchSizes,
1044
+ maxVoteOptions: this.maxVoteOptions,
1045
+ voteOptions: this.voteOptions.toString(),
1046
+ messages: this.messages.map((message) => message.toJSON()),
1047
+ commands: this.commands.map((command) => command.toJSON()),
1048
+ ballots: this.ballots.map((ballot) => ballot.toJSON()),
1049
+ encPubKeys: this.encPubKeys.map((encPubKey) => encPubKey.serialize()),
1050
+ currentMessageBatchIndex: this.currentMessageBatchIndex,
1051
+ pubKeys: this.pubKeys.map((leaf) => leaf.toJSON()),
1052
+ pollStateLeaves: this.pollStateLeaves.map((leaf) => leaf.toJSON()),
1053
+ results: this.tallyResult.map((result) => result.toString()),
1054
+ numBatchesProcessed: this.numBatchesProcessed,
1055
+ numSignups: this.numSignups.toString(),
1056
+ chainHash: this.chainHash.toString(),
1057
+ pollNullifiers: [...this.pollNullifiers.keys()].map((nullifier) => nullifier.toString()),
1058
+ batchHashes: this.batchHashes.map((batchHash) => batchHash.toString()),
1059
+ };
1060
+ }
1061
+ /**
1062
+ * Deserialize a json object into a Poll instance
1063
+ * @param json the json object to deserialize
1064
+ * @param maciState the reference to the MaciState Class
1065
+ * @returns a new Poll instance
1066
+ */
1067
+ static fromJSON(json, maciState) {
1068
+ const poll = new Poll(BigInt(json.pollEndTimestamp), new domainobjs_1.Keypair(), json.treeDepths, json.batchSizes, maciState, BigInt(json.voteOptions));
1069
+ // set all properties
1070
+ poll.pollStateLeaves = json.pollStateLeaves.map((leaf) => domainobjs_1.StateLeaf.fromJSON(leaf));
1071
+ poll.ballots = json.ballots.map((ballot) => domainobjs_1.Ballot.fromJSON(ballot));
1072
+ poll.encPubKeys = json.encPubKeys.map((key) => domainobjs_1.PubKey.deserialize(key));
1073
+ poll.messages = json.messages.map((message) => domainobjs_1.Message.fromJSON(message));
1074
+ poll.commands = json.commands.map((command) => domainobjs_1.PCommand.fromJSON(command));
1075
+ poll.tallyResult = json.results.map((result) => BigInt(result));
1076
+ poll.currentMessageBatchIndex = json.currentMessageBatchIndex;
1077
+ poll.numBatchesProcessed = json.numBatchesProcessed;
1078
+ poll.chainHash = BigInt(json.chainHash);
1079
+ poll.batchHashes = json.batchHashes.map((batchHash) => BigInt(batchHash));
1080
+ poll.pollNullifiers = new Map(json.pollNullifiers.map((nullifier) => [BigInt(nullifier), true]));
1081
+ // copy maci state
1082
+ poll.updatePoll(BigInt(json.numSignups));
1083
+ return poll;
1084
+ }
1085
+ }
1086
+ exports.Poll = Poll;
1087
+ //# sourceMappingURL=Poll.js.map