@maci-protocol/core 0.0.0-ci.00107eb

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