@maci-protocol/core 0.0.0-ci.01622be

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