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