@devtion/devcli 1.0.5-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +118 -0
  2. package/dist/index.js +3206 -0
  3. package/dist/types/commands/auth.d.ts +25 -0
  4. package/dist/types/commands/clean.d.ts +6 -0
  5. package/dist/types/commands/contribute.d.ts +139 -0
  6. package/dist/types/commands/finalize.d.ts +51 -0
  7. package/dist/types/commands/index.d.ts +9 -0
  8. package/dist/types/commands/listCeremonies.d.ts +5 -0
  9. package/dist/types/commands/logout.d.ts +6 -0
  10. package/dist/types/commands/observe.d.ts +22 -0
  11. package/dist/types/commands/setup.d.ts +86 -0
  12. package/dist/types/commands/validate.d.ts +8 -0
  13. package/dist/types/index.d.ts +2 -0
  14. package/dist/types/lib/errors.d.ts +60 -0
  15. package/dist/types/lib/files.d.ts +64 -0
  16. package/dist/types/lib/localConfigs.d.ts +110 -0
  17. package/dist/types/lib/prompts.d.ts +104 -0
  18. package/dist/types/lib/services.d.ts +31 -0
  19. package/dist/types/lib/theme.d.ts +42 -0
  20. package/dist/types/lib/utils.d.ts +158 -0
  21. package/dist/types/types/index.d.ts +65 -0
  22. package/package.json +99 -0
  23. package/src/commands/auth.ts +194 -0
  24. package/src/commands/clean.ts +49 -0
  25. package/src/commands/contribute.ts +1090 -0
  26. package/src/commands/finalize.ts +382 -0
  27. package/src/commands/index.ts +9 -0
  28. package/src/commands/listCeremonies.ts +32 -0
  29. package/src/commands/logout.ts +67 -0
  30. package/src/commands/observe.ts +193 -0
  31. package/src/commands/setup.ts +901 -0
  32. package/src/commands/validate.ts +29 -0
  33. package/src/index.ts +66 -0
  34. package/src/lib/errors.ts +77 -0
  35. package/src/lib/files.ts +102 -0
  36. package/src/lib/localConfigs.ts +186 -0
  37. package/src/lib/prompts.ts +748 -0
  38. package/src/lib/services.ts +191 -0
  39. package/src/lib/theme.ts +45 -0
  40. package/src/lib/utils.ts +778 -0
  41. package/src/types/conf.d.ts +16 -0
  42. package/src/types/index.ts +70 -0
package/dist/index.js ADDED
@@ -0,0 +1,3206 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @module @devtion/devcli
5
+ * @version 1.0.5-alpha.0
6
+ * @file All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies
7
+ * @copyright Ethereum Foundation 2022
8
+ * @license MIT
9
+ * @see [Github]{@link https://github.com/privacy-scaling-explorations/p0tion}
10
+ */
11
+ import { createCommand } from 'commander';
12
+ import fs, { readFileSync, createWriteStream, renameSync } from 'fs';
13
+ import { dirname } from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ import { zKey } from 'snarkjs';
16
+ import boxen from 'boxen';
17
+ import { pipeline } from 'node:stream';
18
+ import { promisify } from 'node:util';
19
+ import fetch$1 from 'node-fetch';
20
+ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
21
+ import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract } from '@p0tion/actions';
22
+ import fetch from '@adobe/node-fetch-retry';
23
+ import { request } from '@octokit/request';
24
+ import { SingleBar, Presets } from 'cli-progress';
25
+ import dotenv from 'dotenv';
26
+ import { GithubAuthProvider, getAuth, signOut } from 'firebase/auth';
27
+ import { getDiskInfoSync } from 'node-disk-info';
28
+ import ora from 'ora';
29
+ import { Timer } from 'timer-node';
30
+ import chalk from 'chalk';
31
+ import logSymbols from 'log-symbols';
32
+ import emoji from 'node-emoji';
33
+ import Conf from 'conf';
34
+ import prompts from 'prompts';
35
+ import clear from 'clear';
36
+ import figlet from 'figlet';
37
+ import { Readable } from 'stream';
38
+ import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device';
39
+ import clipboard from 'clipboardy';
40
+ import open from 'open';
41
+ import { Timestamp, onSnapshot } from 'firebase/firestore';
42
+ import readline from 'readline';
43
+
44
+ /**
45
+ * Different custom progress bar types.
46
+ * @enum {string}
47
+ */
48
+ var ProgressBarType;
49
+ (function (ProgressBarType) {
50
+ ProgressBarType["DOWNLOAD"] = "DOWNLOAD";
51
+ ProgressBarType["UPLOAD"] = "UPLOAD";
52
+ })(ProgressBarType || (ProgressBarType = {}));
53
+
54
+ /**
55
+ * Custom theme object.
56
+ */
57
+ var theme = {
58
+ colors: {
59
+ yellow: chalk.yellow,
60
+ magenta: chalk.magenta,
61
+ red: chalk.red,
62
+ green: chalk.green
63
+ },
64
+ text: {
65
+ underlined: chalk.underline,
66
+ bold: chalk.bold,
67
+ italic: chalk.italic
68
+ },
69
+ symbols: {
70
+ success: logSymbols.success,
71
+ warning: logSymbols.warning,
72
+ error: logSymbols.error,
73
+ info: logSymbols.info
74
+ },
75
+ emojis: {
76
+ tada: emoji.get("tada"),
77
+ key: emoji.get("key"),
78
+ broom: emoji.get("broom"),
79
+ pointDown: emoji.get("point_down"),
80
+ eyes: emoji.get("eyes"),
81
+ wave: emoji.get("wave"),
82
+ clipboard: emoji.get("clipboard"),
83
+ fire: emoji.get("fire"),
84
+ clock: emoji.get("hourglass"),
85
+ dizzy: emoji.get("dizzy_face"),
86
+ rocket: emoji.get("rocket"),
87
+ oldKey: emoji.get("old_key"),
88
+ pray: emoji.get("pray"),
89
+ moon: emoji.get("moon"),
90
+ upsideDown: emoji.get("upside_down_face"),
91
+ arrowUp: emoji.get("arrow_up"),
92
+ arrowDown: emoji.get("arrow_down")
93
+ }
94
+ };
95
+
96
+ /** Services */
97
+ const CORE_SERVICES_ERRORS = {
98
+ FIREBASE_DEFAULT_APP_DOUBLE_CONFIG: `Wrong double default configuration for Firebase application`,
99
+ FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `The Github authorization has failed due to lack of association between your account and the CLI`,
100
+ FIREBASE_USER_DISABLED: `The Github account has been suspended by the ceremony coordinator(s), blocking the possibility of contribution. Please, contact them to understand the motivation behind it.`,
101
+ FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials due to network errors. Please, try once again later.`,
102
+ FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network erros. Please, try once again later and make sure your Internet connection is stable.`,
103
+ FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`,
104
+ FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization`,
105
+ AWS_CEREMONY_BUCKET_CREATION: `Unable to create a new bucket for the ceremony. Something went wrong during the creation. Please, repeat the process by providing a new ceremony name of the ceremony.`,
106
+ AWS_CEREMONY_BUCKET_CANNOT_DOWNLOAD_GET_PRESIGNED_URL: `Unable to download the file from the ceremony bucket. This problem could be related to failure when generating the pre-signed url. Please, we kindly ask you to terminate the current session and repeat the process.`
107
+ };
108
+ /** Github */
109
+ const THIRD_PARTY_SERVICES_ERRORS = {
110
+ GITHUB_ACCOUNT_ASSOCIATION_REJECTED: `You have decided not to associate the CLI application with your Github account. This declination will not allow you to make a contribution to any ceremony. In case you made a mistake, you can always repeat the process and accept the association of your Github account with the CLI.`,
111
+ GITHUB_SERVER_TIMEDOUT: `Github's servers are experiencing downtime. Please, try once again later and make sure your Internet connection is stable.`,
112
+ GITHUB_GET_GITHUB_ACCOUNT_INFO: `Something went wrong while retrieving your Github account public information (handle and identifier). Please, try once again later`,
113
+ GITHUB_NOT_AUTHENTICATED: `You are unable to execute the command since you have not authorized this device with your Github account.\n${theme.symbols.info} Please, run the ${theme.text.bold("phase2cli auth")} command and make sure that your account meets the authentication criteria.`,
114
+ GITHUB_GIST_PUBLICATION_FAILED: `Unable to publish the public attestation as gist making the request using your authenticated Github account. Please, verify that you have allowed the 'gist' access permission during the authentication step.`
115
+ };
116
+ /** Command */
117
+ const COMMAND_ERRORS = {
118
+ COMMAND_NOT_COORDINATOR: `Unable to execute the command. In order to perform coordinator functionality you must authenticate with an account having adeguate permissions.`,
119
+ COMMAND_ABORT_PROMPT: `The data submission process was suddenly interrupted. Your previous data has not been saved. We are sorry, you will have to repeat the process again from the beginning.`,
120
+ COMMAND_ABORT_SELECTION: `The data selection process was suddenly interrupted. Your previous data has not been saved. We are sorry, you will have to repeat the process again from the beginning.`,
121
+ COMMAND_SETUP_NO_R1CS: `Unable to retrieve R1CS files from current working directory. Please, run this command from a working directory where the R1CS files are located to continue with the setup process. We kindly ask you to run the command from an empty directory containing only the R1CS and WASM files.`,
122
+ COMMAND_SETUP_NO_WASM: `Unable to retrieve WASM files from current working directory. Please, run this command from a working directory where the WASM files are located to continue with the setup process. We kindly ask you to run the command from an empty directory containing only the WASM and R1CS files.`,
123
+ COMMAND_SETUP_MISMATCH_R1CS_WASM: `The folder contains more R1CS files than WASM files (or vice versa). Please, run this command from a working directory where each R1CS is paired with its corresponding file WASM.`,
124
+ COMMAND_SETUP_DOWNLOAD_PTAU: `Unable to download Powers of Tau file from Hermez Cryptography Phase 1 Trusted Setup. Possible causes may involve an error while making the request (be sure to have a stable internet connection). Please, we kindly ask you to terminate the current session and repeat the process.`,
125
+ COMMAND_SETUP_ABORT: `You chose to abort the setup process.`,
126
+ COMMAND_CONTRIBUTE_NO_OPENED_CEREMONIES: `Unfortunately, there is no ceremony for which you can make a contribution at this time. Please, try again later.`,
127
+ COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA: `Unable to retrieve your data as ceremony participant. Please, terminate the current session and try again later. If the error persists, please contact the ceremony coordinator.`,
128
+ COMMAND_CONTRIBUTE_WRONG_OPTION_CEREMONY: `The ceremony name you provided does not exist or belongs to a ceremony not yet open. Please, double-check your option and retry.`,
129
+ COMMAND_CONTRIBUTE_NO_CURRENT_CONTRIBUTOR_DATA: `Unable to retrieve current circuit contributor information. Please, terminate the current session and try again later. If the error persists, please contact the ceremony coordinator.`,
130
+ COMMAND_CONTRIBUTE_NO_CURRENT_CONTRIBUTOR_CONTRIBUTION: `Unable to retrieve circuit last contribution information. This could happen due to a timeout or some errors while writing the information on the database.`,
131
+ COMMAND_CONTRIBUTE_WRONG_CURRENT_CONTRIBUTOR_CONTRIBUTION_STEP: `Something went wrong when progressing the contribution step of the current circuit contributor. If the error persists, please contact the ceremony coordinator.`,
132
+ COMMAND_CONTRIBUTE_NO_CIRCUIT_DATA: `Unable to retrieve circuit data from the ceremony. Please, terminate the current session and try again later. If the error persists, please contact the ceremony coordinator.`,
133
+ COMMAND_CONTRIBUTE_NO_ACTIVE_TIMEOUT_DATA: `Unable to retrieve your active timeout data. This problem could be related to failure to write timeout data to the database. If the error persists, please contact the ceremony coordinator.`,
134
+ COMMAND_CONTRIBUTE_NO_UNIQUE_ACTIVE_TIMEOUTS: `The number of active timeouts is different from one. This problem could be related to failure to update timeout document in the database. If the error persists, please contact the ceremony coordinator.`,
135
+ COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH: `Unable to retrieve contribution hash from transcript. Possible causes may involve an error while using the logger or unexpected file descriptor termination. Please, terminate the current session and repeat the process.`,
136
+ COMMAND_FINALIZED_NO_CLOSED_CEREMONIES: `Unfortunately, there is no ceremony closed and ready for finalization. Please, try again later.`,
137
+ COMMAND_FINALIZED_NOT_READY_FOR_FINALIZATION: `You are not ready for ceremony finalization. This could happen because the ceremony does not appear closed or you do not have completed every circuit contributions. If the error persists, please contact the operator to check the server logs.`
138
+ };
139
+ /** Config */
140
+ const CONFIG_ERRORS = {
141
+ CONFIG_GITHUB_ERROR: `Configuration error. The Github client id environment variable has not been configured correctly.`,
142
+ CONFIG_FIREBASE_ERROR: `Configuration error. The Firebase environment variable has not been configured correctly`,
143
+ CONFIG_OTHER_ERROR: `Configuration error. One or more config environment variable has not been configured correctly`
144
+ };
145
+ /** Generic */
146
+ const GENERIC_ERRORS = {
147
+ GENERIC_ERROR_RETRIEVING_DATA: `Something went wrong when retrieving the data from the database`,
148
+ GENERIC_COUNTDOWN_EXPIRATION: `Your time to carry out the action has expired`
149
+ };
150
+ /**
151
+ * Print an error string and gracefully terminate the process.
152
+ * @param err <string> - the error string to be shown.
153
+ * @param doExit <boolean> - when true the function terminate the process; otherwise not.
154
+ */
155
+ const showError = (err, doExit) => {
156
+ // Print the error.
157
+ console.error(`${theme.symbols.error} ${err}`);
158
+ // Terminate the process.
159
+ if (doExit)
160
+ process.exit(1);
161
+ };
162
+
163
+ /**
164
+ * Check a directory path.
165
+ * @param directoryPath <string> - the local path of the directory.
166
+ * @returns <boolean> true if the directory at given path exists, otherwise false.
167
+ */
168
+ const directoryExists = (directoryPath) => fs.existsSync(directoryPath);
169
+ /**
170
+ * Write a new file locally.
171
+ * @param localFilePath <string> - the local path of the file.
172
+ * @param data <Buffer> - the content to be written inside the file.
173
+ */
174
+ const writeFile = (localFilePath, data) => fs.writeFileSync(localFilePath, data);
175
+ /**
176
+ * Read a new file from local folder.
177
+ * @param localFilePath <string> - the local path of the file.
178
+ */
179
+ const readFile = (localFilePath) => fs.readFileSync(localFilePath, "utf-8");
180
+ /**
181
+ * Get back the statistics of the provided file.
182
+ * @param localFilePath <string> - the local path of the file.
183
+ * @returns <Stats> - the metadata of the file.
184
+ */
185
+ const getFileStats = (localFilePath) => fs.statSync(localFilePath);
186
+ /**
187
+ * Return the sub-paths for each file stored in the given directory.
188
+ * @param directoryLocalPath <string> - the local path of the directory.
189
+ * @returns <Promise<Array<Dirent>>> - the list of sub-paths of the files contained inside the directory.
190
+ */
191
+ const getDirFilesSubPaths = async (directoryLocalPath) => {
192
+ // Get Dirent sub paths for folders and files.
193
+ const subPaths = await fs.promises.readdir(directoryLocalPath, { withFileTypes: true });
194
+ // Return Dirent sub paths for files only.
195
+ return subPaths.filter((dirent) => dirent.isFile());
196
+ };
197
+ /**
198
+ * Filter all files in a directory by returning only those that match the given extension.
199
+ * @param directoryLocalPath <string> - the local path of the directory.
200
+ * @param fileExtension <string> - the file extension.
201
+ * @returns <Promise<Array<Dirent>>> - return the filenames of the file that match the given extension, if any
202
+ */
203
+ const filterDirectoryFilesByExtension = async (directoryLocalPath, fileExtension) => {
204
+ // Get the sub paths for each file stored in the given directory.
205
+ const cwdFiles = await getDirFilesSubPaths(directoryLocalPath);
206
+ // Filter by extension.
207
+ return cwdFiles.filter((file) => file.name.includes(fileExtension));
208
+ };
209
+ /**
210
+ * Delete a directory specified at a given path.
211
+ * @param directoryLocalPath <string> - the local path of the directory.
212
+ */
213
+ const deleteDir = (directoryLocalPath) => {
214
+ fs.rmSync(directoryLocalPath, { recursive: true, force: true });
215
+ };
216
+ /**
217
+ * Clean a directory specified at a given path.
218
+ * @param directoryLocalPath <string> - the local path of the directory.
219
+ */
220
+ const cleanDir = (directoryLocalPath) => {
221
+ deleteDir(directoryLocalPath);
222
+ fs.mkdirSync(directoryLocalPath);
223
+ };
224
+ /**
225
+ * Create a new directory in a specified path if not exist in that path.
226
+ * @param directoryLocalPath <string> - the local path of the directory.
227
+ */
228
+ const checkAndMakeNewDirectoryIfNonexistent = (directoryLocalPath) => {
229
+ if (!directoryExists(directoryLocalPath))
230
+ fs.mkdirSync(directoryLocalPath);
231
+ };
232
+ /**
233
+ * Write data a local JSON file at a given path.
234
+ * @param localFilePath <string> - the local path of the file.
235
+ * @param data <JSON> - the JSON content to be written inside the file.
236
+ */
237
+ const writeLocalJsonFile = (filePath, data) => {
238
+ fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
239
+ };
240
+
241
+ // Get npm package name.
242
+ const packagePath$4 = `${dirname(fileURLToPath(import.meta.url))}/..`;
243
+ const { name: name$1 } = JSON.parse(readFileSync(packagePath$4.includes(`src/lib/`) ? `${packagePath$4}/../package.json` : `${packagePath$4}/package.json`, "utf8"));
244
+ /**
245
+ * Local Storage.
246
+ * @dev The CLI implementation use the Conf package to create a local storage
247
+ * in the user device (`.config/@p0tion/phase2cli-nodejs/config.json` path) to store the access token.
248
+ */
249
+ const config = new Conf({
250
+ projectName: name$1,
251
+ schema: {
252
+ accessToken: {
253
+ type: "string",
254
+ default: ""
255
+ }
256
+ }
257
+ });
258
+ /**
259
+ * Local Paths.
260
+ * @dev definition of the paths to the local folders containing the CLI-generated artifacts.
261
+ */
262
+ const outputLocalFolderPath = `./${commonTerms.foldersAndPathsTerms.output}`;
263
+ const setupLocalFolderPath = `${outputLocalFolderPath}/${commonTerms.foldersAndPathsTerms.setup}`;
264
+ const contributeLocalFolderPath = `${outputLocalFolderPath}/${commonTerms.foldersAndPathsTerms.contribute}`;
265
+ const finalizeLocalFolderPath = `${outputLocalFolderPath}/${commonTerms.foldersAndPathsTerms.finalize}`;
266
+ const potLocalFolderPath = `${setupLocalFolderPath}/${commonTerms.foldersAndPathsTerms.pot}`;
267
+ const zkeysLocalFolderPath = `${setupLocalFolderPath}/${commonTerms.foldersAndPathsTerms.zkeys}`;
268
+ const wasmLocalFolderPath = `${setupLocalFolderPath}/${commonTerms.foldersAndPathsTerms.wasm}`;
269
+ const contributionsLocalFolderPath = `${contributeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.zkeys}`;
270
+ const contributionTranscriptsLocalFolderPath = `${contributeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.transcripts}`;
271
+ const attestationLocalFolderPath = `${contributeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.attestation}`;
272
+ const finalZkeysLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.zkeys}`;
273
+ const finalPotLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.pot}`;
274
+ const finalTranscriptsLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.transcripts}`;
275
+ const finalAttestationsLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.attestation}`;
276
+ const verificationKeysLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.vkeys}`;
277
+ const verifierContractsLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.verifiers}`;
278
+ const localPaths = {
279
+ output: outputLocalFolderPath,
280
+ setup: setupLocalFolderPath,
281
+ contribute: contributeLocalFolderPath,
282
+ finalize: finalizeLocalFolderPath,
283
+ pot: potLocalFolderPath,
284
+ zkeys: zkeysLocalFolderPath,
285
+ wasm: wasmLocalFolderPath,
286
+ contributions: contributionsLocalFolderPath,
287
+ transcripts: contributionTranscriptsLocalFolderPath,
288
+ attestations: attestationLocalFolderPath,
289
+ finalZkeys: finalZkeysLocalFolderPath,
290
+ finalPot: finalPotLocalFolderPath,
291
+ finalTranscripts: finalTranscriptsLocalFolderPath,
292
+ finalAttestations: finalAttestationsLocalFolderPath,
293
+ verificationKeys: verificationKeysLocalFolderPath,
294
+ verifierContracts: verifierContractsLocalFolderPath
295
+ };
296
+ /**
297
+ * Return the access token, if present.
298
+ * @returns <string | undefined> - the access token if present, otherwise undefined.
299
+ */
300
+ const getLocalAccessToken = () => config.get("accessToken");
301
+ /**
302
+ * Check if the access token exists in the local storage.
303
+ * @returns <boolean>
304
+ */
305
+ const checkLocalAccessToken = () => config.has("accessToken") && !!config.get("accessToken");
306
+ /**
307
+ * Set the access token.
308
+ * @param token <string> - the access token to be stored.
309
+ */
310
+ const setLocalAccessToken = (token) => config.set("accessToken", token);
311
+ /**
312
+ * Delete the stored access token.
313
+ */
314
+ const deleteLocalAccessToken = () => config.delete("accessToken");
315
+ /**
316
+ * Get the complete local file path.
317
+ * @param cwd <string> - the current working directory path.
318
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
319
+ * @returns <string> - the complete local path to the file.
320
+ */
321
+ const getCWDFilePath = (cwd, completeFilename) => `${cwd}/${completeFilename}`;
322
+ /**
323
+ * Get the complete PoT file path.
324
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
325
+ * @returns <string> - the complete PoT path to the file.
326
+ */
327
+ const getPotLocalFilePath = (completeFilename) => `${potLocalFolderPath}/${completeFilename}`;
328
+ /**
329
+ * Get the complete zKey file path.
330
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
331
+ * @returns <string> - the complete zKey path to the file.
332
+ */
333
+ const getZkeyLocalFilePath = (completeFilename) => `${zkeysLocalFolderPath}/${completeFilename}`;
334
+ /**
335
+ * Get the complete contribution file path.
336
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
337
+ * @returns <string> - the complete contribution path to the file.
338
+ */
339
+ const getContributionLocalFilePath = (completeFilename) => `${contributionsLocalFolderPath}/${completeFilename}`;
340
+ /**
341
+ * Get the contribution attestation file path.
342
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
343
+ * @returns <string> - the the contribution attestation path to the file.
344
+ */
345
+ const getAttestationLocalFilePath = (completeFilename) => `${attestationLocalFolderPath}/${completeFilename}`;
346
+ /**
347
+ * Get the transcript file path.
348
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
349
+ * @returns <string> - the the transcript path to the file.
350
+ */
351
+ const getTranscriptLocalFilePath = (completeFilename) => `${contributionTranscriptsLocalFolderPath}/${completeFilename}`;
352
+ /**
353
+ * Get the complete final zKey file path.
354
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
355
+ * @returns <string> - the complete final zKey path to the file.
356
+ */
357
+ const getFinalZkeyLocalFilePath = (completeFilename) => `${finalZkeysLocalFolderPath}/${completeFilename}`;
358
+ /**
359
+ * Get the complete verification key file path.
360
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
361
+ * @returns <string> - the complete final verification key path to the file.
362
+ */
363
+ const getVerificationKeyLocalFilePath = (completeFilename) => `${verificationKeysLocalFolderPath}/${completeFilename}`;
364
+ /**
365
+ * Get the complete verifier contract file path.
366
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
367
+ * @returns <string> - the complete final verifier contract path to the file.
368
+ */
369
+ const getVerifierContractLocalFilePath = (completeFilename) => `${verifierContractsLocalFolderPath}/${completeFilename}`;
370
+ /**
371
+ * Get the final transcript file path.
372
+ * @param completeFilename <string> - the complete filename of the file (name.ext).
373
+ * @returns <string> - the the final transcript path to the file.
374
+ */
375
+ const getFinalTranscriptLocalFilePath = (completeFilename) => `${finalTranscriptsLocalFolderPath}/${completeFilename}`;
376
+
377
+ const packagePath$3 = `${dirname(fileURLToPath(import.meta.url))}`;
378
+ dotenv.config({
379
+ path: packagePath$3.includes(`src/lib`)
380
+ ? `${dirname(fileURLToPath(import.meta.url))}/../../.env`
381
+ : `${dirname(fileURLToPath(import.meta.url))}/.env`
382
+ });
383
+ /**
384
+ * Exchange the Github token for OAuth credential.
385
+ * @param githubToken <string> - the Github token generated through the Device Flow process.
386
+ * @returns <OAuthCredential>
387
+ */
388
+ const exchangeGithubTokenForCredentials = (githubToken) => GithubAuthProvider.credential(githubToken);
389
+ /**
390
+ * Get the information associated to the account from which the token has been generated to
391
+ * create a custom unique identifier for the user.
392
+ * @notice the unique identifier has the following form 'handle-identifier'.
393
+ * @param githubToken <string> - the Github token.
394
+ * @returns <Promise<any>> - the Github (provider) unique identifier associated to the user.
395
+ */
396
+ const getGithubProviderUserId = async (githubToken) => {
397
+ // Ask for user account public information through Github API.
398
+ const response = await request("GET https://api.github.com/user", {
399
+ headers: {
400
+ authorization: `token ${githubToken}`
401
+ }
402
+ });
403
+ if (response && response.status === 200)
404
+ return `${response.data.login}-${response.data.id}`;
405
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
406
+ };
407
+ /**
408
+ * Get the gists associated to the authenticated user account.
409
+ * @param githubToken <string> - the Github token.
410
+ * @param params <Object<number,number>> - the necessary parameters for the request.
411
+ * @returns <Promise<any>> - the Github gists associated with the authenticated user account.
412
+ */
413
+ const getGithubAuthenticatedUserGists = async (githubToken, params) => {
414
+ // Ask for user account public information through Github API.
415
+ const response = await request("GET https://api.github.com/gists{?per_page,page}", {
416
+ headers: {
417
+ authorization: `token ${githubToken}`
418
+ },
419
+ per_page: params.perPage,
420
+ page: params.page
421
+ });
422
+ if (response && response.status === 200)
423
+ return response.data;
424
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
425
+ };
426
+ /**
427
+ * Check whether or not the user has published the gist.
428
+ * @dev gather all the user's gists and check if there is a match with the expected public attestation.
429
+ * @param githubToken <string> - the Github token.
430
+ * @param publicAttestationFilename <string> - the public attestation filename.
431
+ * @returns <Promise<GithubGistFile | undefined>> - return the public attestation gist if and only if has been published.
432
+ */
433
+ const getPublicAttestationGist = async (githubToken, publicAttestationFilename) => {
434
+ const itemsPerPage = 50; // number of gists to fetch x page.
435
+ let gists = []; // The list of user gists.
436
+ let publishedGist; // the published public attestation gist.
437
+ let page = 1; // Page of gists = starts from 1.
438
+ // Get first batch (page) of gists
439
+ let pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page });
440
+ // State update.
441
+ gists = gists.concat(pageGists);
442
+ // Keep going until hitting a blank page.
443
+ while (pageGists.length > 0) {
444
+ // Fetch next page.
445
+ page += 1;
446
+ pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page });
447
+ // State update.
448
+ gists = gists.concat(pageGists);
449
+ }
450
+ // Look for public attestation.
451
+ for (const gist of gists) {
452
+ const numberOfFiles = Object.keys(gist.files).length;
453
+ const publicAttestationCandidateFile = Object.values(gist.files)[0];
454
+ /// @todo improve check by using expected public attestation content (e.g., hash).
455
+ if (numberOfFiles === 1 && publicAttestationCandidateFile.filename === publicAttestationFilename)
456
+ publishedGist = publicAttestationCandidateFile;
457
+ }
458
+ return publishedGist;
459
+ };
460
+ /**
461
+ * Return the Github handle from the provider user id.
462
+ * @notice the provider user identifier must have the following structure 'handle-id'.
463
+ * @param providerUserId <string> - the unique provider user identifier.
464
+ * @returns <string> - the third-party provider handle of the user.
465
+ */
466
+ const getUserHandleFromProviderUserId = (providerUserId) => {
467
+ if (providerUserId.indexOf("-") === -1)
468
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
469
+ return providerUserId.split("-")[0];
470
+ };
471
+ /**
472
+ * Return a custom spinner.
473
+ * @param text <string> - the text that should be displayed as spinner status.
474
+ * @param spinnerLogo <any> - the logo.
475
+ * @returns <Ora> - a new Ora custom spinner.
476
+ */
477
+ const customSpinner = (text, spinnerLogo) => ora({
478
+ text,
479
+ spinner: spinnerLogo
480
+ });
481
+ /**
482
+ * Custom sleeper.
483
+ * @dev to be used in combination with loggers and for workarounds where listeners cannot help.
484
+ * @param ms <number> - sleep amount in milliseconds
485
+ * @returns <Promise<any>>
486
+ */
487
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
488
+ /**
489
+ * Simple loader for task simulation.
490
+ * @param loadingText <string> - spinner text while loading.
491
+ * @param spinnerLogo <any> - spinner logo.
492
+ * @param durationInMs <number> - spinner loading duration in ms.
493
+ * @returns <Promise<void>>.
494
+ */
495
+ const simpleLoader = async (loadingText, spinnerLogo, durationInMs) => {
496
+ // Custom spinner (used as loader).
497
+ const loader = customSpinner(loadingText, spinnerLogo);
498
+ loader.start();
499
+ // nb. simulate execution for requested duration.
500
+ await sleep(durationInMs);
501
+ loader.stop();
502
+ };
503
+ /**
504
+ * Check and return the free aggregated disk space (in KB) for participant machine.
505
+ * @dev this method use the node-disk-info method to retrieve the information about
506
+ * disk availability for all visible disks.
507
+ * nb. no other type of data or operation is performed by this methods.
508
+ * @returns <number> - the free aggregated disk space in kB for the participant machine.
509
+ */
510
+ const estimateParticipantFreeGlobalDiskSpace = () => {
511
+ // Get info about disks.
512
+ const disks = getDiskInfoSync();
513
+ // Get an estimation of available memory.
514
+ let availableDiskSpace = 0;
515
+ for (const disk of disks)
516
+ availableDiskSpace += disk.available;
517
+ // Return the disk space available in KB.
518
+ return availableDiskSpace;
519
+ };
520
+ /**
521
+ * Get seconds, minutes, hours and days from milliseconds.
522
+ * @param millis <number> - the amount of milliseconds.
523
+ * @returns <Timing> - a custom object containing the amount of seconds, minutes, hours and days in the provided millis.
524
+ */
525
+ const getSecondsMinutesHoursFromMillis = (millis) => {
526
+ let delta = millis / 1000;
527
+ const days = Math.floor(delta / 86400);
528
+ delta -= days * 86400;
529
+ const hours = Math.floor(delta / 3600) % 24;
530
+ delta -= hours * 3600;
531
+ const minutes = Math.floor(delta / 60) % 60;
532
+ delta -= minutes * 60;
533
+ const seconds = Math.floor(delta) % 60;
534
+ return {
535
+ seconds: seconds >= 60 ? 59 : seconds,
536
+ minutes: minutes >= 60 ? 59 : minutes,
537
+ hours: hours >= 24 ? 23 : hours,
538
+ days
539
+ };
540
+ };
541
+ /**
542
+ * Gracefully terminate the command execution
543
+ * @params ghUsername <string> - the Github username of the user.
544
+ */
545
+ const terminate = async (ghUsername) => {
546
+ console.log(`\nSee you, ${theme.text.bold(`@${getUserHandleFromProviderUserId(ghUsername)}`)} ${theme.emojis.wave}`);
547
+ process.exit(0);
548
+ };
549
+ /**
550
+ * Publish public attestation using Github Gist.
551
+ * @dev the contributor must have agreed to provide 'gist' access during the execution of the 'auth' command.
552
+ * @param accessToken <string> - the contributor access token.
553
+ * @param publicAttestation <string> - the public attestation.
554
+ * @param ceremonyTitle <string> - the ceremony title.
555
+ * @param ceremonyPrefix <string> - the ceremony prefix.
556
+ * @returns <Promise<string>> - the url where the gist has been published.
557
+ */
558
+ const publishGist = async (token, content, ceremonyTitle, ceremonyPrefix) => {
559
+ // Make request.
560
+ const response = await request("POST /gists", {
561
+ description: `Attestation for ${ceremonyTitle} MPC Phase 2 Trusted Setup ceremony`,
562
+ public: true,
563
+ files: {
564
+ [`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`]: {
565
+ content
566
+ }
567
+ },
568
+ headers: {
569
+ authorization: `token ${token}`
570
+ }
571
+ });
572
+ if (response.status !== 201 || !response.data.html_url)
573
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GIST_PUBLICATION_FAILED, true);
574
+ return response.data.html_url;
575
+ };
576
+ /**
577
+ * Generate a custom url that when clicked allows you to compose a tweet ready to be shared.
578
+ * @param ceremonyName <string> - the name of the ceremony.
579
+ * @param gistUrl <string> - the url of the gist where the public attestation has been shared.
580
+ * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
581
+ * @returns <string> - the ready to share tweet url.
582
+ */
583
+ const generateCustomUrlToTweetAboutParticipation = (ceremonyName, gistUrl, isFinalizing) => isFinalizing
584
+ ? `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremonyName}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE`
585
+ : `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyName}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20contribute%20here:%20https://github.com/privacy-scaling-explorations/p0tion%20You%20can%20view%20my%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP`;
586
+ /**
587
+ * Return a custom progress bar.
588
+ * @param type <ProgressBarType> - the type of the progress bar.
589
+ * @param [message] <string> - additional information to be displayed when downloading/uploading.
590
+ * @returns <SingleBar> - a new custom (single) progress bar.
591
+ */
592
+ const customProgressBar = (type, message) => {
593
+ // Formats.
594
+ const uploadFormat = `${theme.emojis.arrowUp} Uploading ${message} [${theme.colors.magenta("{bar}")}] {percentage}% | {value}/{total} Chunks`;
595
+ const downloadFormat = `${theme.emojis.arrowDown} Downloading ${message} [${theme.colors.magenta("{bar}")}] {percentage}% | {value}/{total} GB`;
596
+ // Define a progress bar showing percentage of completion and chunks downloaded/uploaded.
597
+ return new SingleBar({
598
+ format: type === ProgressBarType.DOWNLOAD ? downloadFormat : uploadFormat,
599
+ hideCursor: true,
600
+ clearOnComplete: true
601
+ }, Presets.legacy);
602
+ };
603
+ /**
604
+ * Download an artifact from the ceremony bucket.
605
+ * @dev this method request a pre-signed url to make a GET request to download the artifact.
606
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
607
+ * @param bucketName <string> - the name of the ceremony artifacts bucket (AWS S3).
608
+ * @param storagePath <string> - the storage path that locates the artifact to be downloaded in the bucket.
609
+ * @param localPath <string> - the local path where the artifact will be downloaded.
610
+ */
611
+ const downloadCeremonyArtifact = async (cloudFunctions, bucketName, storagePath, localPath) => {
612
+ const spinner = customSpinner(`Preparing for downloading the contribution...`, `clock`);
613
+ spinner.start();
614
+ // Request pre-signed url to make GET download request.
615
+ const getPreSignedUrl = await generateGetObjectPreSignedUrl(cloudFunctions, bucketName, storagePath);
616
+ // Make fetch to get info about the artifact.
617
+ // @ts-ignore
618
+ const response = await fetch(getPreSignedUrl);
619
+ if (response.status !== 200 && !response.ok)
620
+ showError(CORE_SERVICES_ERRORS.AWS_CEREMONY_BUCKET_CANNOT_DOWNLOAD_GET_PRESIGNED_URL, true);
621
+ // Extract and prepare data.
622
+ const content = response.body;
623
+ const contentLength = Number(response.headers.get("content-length"));
624
+ const contentLengthInGB = convertBytesOrKbToGb(contentLength, true);
625
+ // Prepare stream.
626
+ const writeStream = createWriteStream(localPath);
627
+ spinner.stop();
628
+ // Prepare custom progress bar.
629
+ const progressBar = customProgressBar(ProgressBarType.DOWNLOAD, `last contribution`);
630
+ const progressBarStep = contentLengthInGB / 100;
631
+ let chunkLengthWritingProgress = 0;
632
+ let completedProgress = progressBarStep;
633
+ // Bootstrap the progress bar.
634
+ progressBar.start(contentLengthInGB < 0.01 ? 0.01 : parseFloat(contentLengthInGB.toFixed(2)).valueOf(), 0);
635
+ // Write chunk by chunk.
636
+ for await (const chunk of content) {
637
+ // Write chunk.
638
+ writeStream.write(chunk);
639
+ // Update current progress.
640
+ chunkLengthWritingProgress += convertBytesOrKbToGb(chunk.length, true);
641
+ // Display the current progress.
642
+ while (chunkLengthWritingProgress >= completedProgress) {
643
+ // Store new completed progress step by step.
644
+ completedProgress += progressBarStep;
645
+ // Display accordingly in the progress bar.
646
+ progressBar.update(contentLengthInGB < 0.01 ? 0.01 : parseFloat(completedProgress.toFixed(2)).valueOf());
647
+ }
648
+ }
649
+ await sleep(2000); // workaround to show bar for small artifacts.
650
+ progressBar.stop();
651
+ };
652
+ /**
653
+ *
654
+ * @param lastZkeyLocalFilePath <string> - the local path of the last contribution.
655
+ * @param nextZkeyLocalFilePath <string> - the local path where the next contribution is going to be stored.
656
+ * @param entropyOrBeacon <string> - the entropy or beacon (only when finalizing) for the contribution.
657
+ * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
658
+ * @param averageComputingTime <number> - the current average contribution computation time.
659
+ * @param transcriptLogger <Logger> - the custom file logger to generate the contribution transcript.
660
+ * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
661
+ * @returns <Promise<number>> - the amount of time spent contributing.
662
+ */
663
+ const handleContributionComputation = async (lastZkeyLocalFilePath, nextZkeyLocalFilePath, entropyOrBeacon, contributorOrCoordinatorIdentifier, averageComputingTime, transcriptLogger, isFinalizing) => {
664
+ // Prepare timer (statistics only).
665
+ const computingTimer = new Timer({ label: "COMPUTING" /* ParticipantContributionStep.COMPUTING */ });
666
+ computingTimer.start();
667
+ // Time format.
668
+ const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(averageComputingTime);
669
+ const spinner = customSpinner(`${isFinalizing ? `Applying beacon...` : `Computing contribution...`} ${averageComputingTime > 0
670
+ ? `${theme.text.bold(`(ETA ${theme.text.bold(`${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}`)}).\n${theme.symbols.warning} This may take longer or less time on your machine! Everything's fine, just be patient and do not stop the computation to avoid starting over again`)}`
671
+ : ``}`, `clock`);
672
+ spinner.start();
673
+ // Discriminate between contribution finalization or computation.
674
+ if (isFinalizing)
675
+ await zKey.beacon(lastZkeyLocalFilePath, nextZkeyLocalFilePath, contributorOrCoordinatorIdentifier, entropyOrBeacon, numExpIterations, transcriptLogger);
676
+ else
677
+ await zKey.contribute(lastZkeyLocalFilePath, nextZkeyLocalFilePath, contributorOrCoordinatorIdentifier, entropyOrBeacon, transcriptLogger);
678
+ computingTimer.stop();
679
+ await sleep(3000); // workaround for file descriptor.
680
+ spinner.stop();
681
+ return computingTimer.ms();
682
+ };
683
+ /**
684
+ * Return the most up-to-date data about the participant document for the given ceremony.
685
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
686
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
687
+ * @param participantId <string> - the unique identifier of the participant.
688
+ * @returns <Promise<DocumentData>> - the most up-to-date participant data.
689
+ */
690
+ const getLatestUpdatesFromParticipant = async (firestoreDatabase, ceremonyId, participantId) => {
691
+ // Fetch participant data.
692
+ const participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(ceremonyId), participantId);
693
+ if (!participant.data())
694
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true);
695
+ return participant.data();
696
+ };
697
+ /**
698
+ * Start or resume a contribution from the last participant contribution step.
699
+ * @notice this method goes through each contribution stage following this order:
700
+ * 1) Downloads the last contribution from previous contributor.
701
+ * 2) Computes the new contribution.
702
+ * 3) Uploads the new contribution.
703
+ * 4) Requests the verification of the new contribution to the coordinator's backend and waits for the result.
704
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
705
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
706
+ * @param ceremony <FirebaseDocumentInfo> - the Firestore document of the ceremony.
707
+ * @param circuit <FirebaseDocumentInfo> - the Firestore document of the ceremony circuit.
708
+ * @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (contributor or coordinator).
709
+ * @param participantContributionStep <ParticipantContributionStep> - the contribution step of the participant (from where to start/resume contribution).
710
+ * @param entropyOrBeaconHash <string> - the entropy or beacon hash (only when finalizing) for the contribution.
711
+ * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
712
+ * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
713
+ */
714
+ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing) => {
715
+ // Extract data.
716
+ const { prefix: ceremonyPrefix } = ceremony.data;
717
+ const { waitingQueue, avgTimings, prefix: circuitPrefix, sequencePosition } = circuit.data;
718
+ const { completedContributions } = waitingQueue; // = current progress.
719
+ console.log(`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}`)}`)} (Contribution Steps)`);
720
+ // Get most up-to-date data from the participant document.
721
+ let participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id);
722
+ const spinner = customSpinner(`${participantData.contributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */
723
+ ? `Preparing to begin the contribution. Please note that the contribution can take a long time depending on the size of the circuits and your internet connection.`
724
+ : `Preparing to resume contribution. Please note that the contribution can take a long time depending on the size of the circuits and your internet connection.`}`, `clock`);
725
+ spinner.start();
726
+ // Compute zkey indexes.
727
+ const lastZkeyIndex = formatZkeyIndex(completedContributions);
728
+ const nextZkeyIndex = formatZkeyIndex(completedContributions + 1);
729
+ // Prepare zKey filenames.
730
+ const lastZkeyCompleteFilename = `${circuitPrefix}_${lastZkeyIndex}.zkey`;
731
+ const nextZkeyCompleteFilename = isFinalizing
732
+ ? `${circuitPrefix}_${finalContributionIndex}.zkey`
733
+ : `${circuitPrefix}_${nextZkeyIndex}.zkey`;
734
+ // Prepare zKey storage paths.
735
+ const lastZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, lastZkeyCompleteFilename);
736
+ const nextZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, nextZkeyCompleteFilename);
737
+ // Prepare zKey local paths.
738
+ const lastZkeyLocalFilePath = isFinalizing
739
+ ? getFinalZkeyLocalFilePath(lastZkeyCompleteFilename)
740
+ : getContributionLocalFilePath(lastZkeyCompleteFilename);
741
+ const nextZkeyLocalFilePath = isFinalizing
742
+ ? getFinalZkeyLocalFilePath(nextZkeyCompleteFilename)
743
+ : getContributionLocalFilePath(nextZkeyCompleteFilename);
744
+ // Generate a custom file logger for contribution transcript.
745
+ const transcriptCompleteFilename = isFinalizing
746
+ ? `${circuit.data.prefix}_${contributorOrCoordinatorIdentifier}_${finalContributionIndex}.log`
747
+ : `${circuit.data.prefix}_${nextZkeyIndex}.log`;
748
+ const transcriptLocalFilePath = isFinalizing
749
+ ? getFinalTranscriptLocalFilePath(transcriptCompleteFilename)
750
+ : getTranscriptLocalFilePath(transcriptCompleteFilename);
751
+ const transcriptLogger = createCustomLoggerForFile(transcriptLocalFilePath);
752
+ // Populate transcript file w/ header.
753
+ transcriptLogger.info(`${isFinalizing ? `Final` : `Contribution`} transcript for ${circuitPrefix} phase 2 contribution.\n${isFinalizing
754
+ ? `Coordinator: ${contributorOrCoordinatorIdentifier}`
755
+ : `Contributor # ${Number(nextZkeyIndex)}`} (${contributorOrCoordinatorIdentifier})\n`);
756
+ // Get ceremony bucket name.
757
+ const bucketName = getBucketName(ceremonyPrefix, String(process.env.CONFIG_CEREMONY_BUCKET_POSTFIX));
758
+ await sleep(3000); // ~3s.
759
+ spinner.stop();
760
+ // Contribution step = DOWNLOADING.
761
+ if (isFinalizing || participantData.contributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */) {
762
+ // Download the latest contribution from bucket.
763
+ await downloadCeremonyArtifact(cloudFunctions, bucketName, lastZkeyStorageFilePath, lastZkeyLocalFilePath);
764
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} correctly downloaded`);
765
+ // Advance to next contribution step (COMPUTING) if not finalizing.
766
+ if (!isFinalizing) {
767
+ spinner.text = `Preparing for contribution computation...`;
768
+ spinner.start();
769
+ await progressToNextContributionStep(cloudFunctions, ceremony.id);
770
+ await sleep(1000);
771
+ // Refresh most up-to-date data from the participant document.
772
+ participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id);
773
+ spinner.stop();
774
+ }
775
+ }
776
+ else
777
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} already downloaded`);
778
+ // Contribution step = COMPUTING.
779
+ if (isFinalizing || participantData.contributionStep === "COMPUTING" /* ParticipantContributionStep.COMPUTING */) {
780
+ // Handle the next contribution computation.
781
+ const computingTime = await handleContributionComputation(lastZkeyLocalFilePath, nextZkeyLocalFilePath, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, avgTimings.contributionComputation, transcriptLogger, isFinalizing);
782
+ // Permanently store on db the contribution hash and computing time.
783
+ spinner.text = `Writing contribution metadata...`;
784
+ spinner.start();
785
+ // Read local transcript file info to get the contribution hash.
786
+ const transcriptContents = readFile(transcriptLocalFilePath);
787
+ const matchContributionHash = transcriptContents.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/);
788
+ if (!matchContributionHash)
789
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true);
790
+ // Format contribution hash.
791
+ const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "");
792
+ // Make request to cloud functions to permanently store the information.
793
+ await permanentlyStoreCurrentContributionTimeAndHash(cloudFunctions, ceremony.id, computingTime, contributionHash);
794
+ // Format computing time.
795
+ const { seconds: computationSeconds, minutes: computationMinutes, hours: computationHours } = getSecondsMinutesHoursFromMillis(computingTime);
796
+ spinner.succeed(`${isFinalizing ? "Contribution" : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`} computation took ${theme.text.bold(`${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits(computationMinutes)}:${convertToDoubleDigits(computationSeconds)}`)}`);
797
+ // Advance to next contribution step (UPLOADING) if not finalizing.
798
+ if (!isFinalizing) {
799
+ spinner.text = `Preparing for uploading the contribution...`;
800
+ spinner.start();
801
+ await progressToNextContributionStep(cloudFunctions, ceremony.id);
802
+ await sleep(1000);
803
+ // Refresh most up-to-date data from the participant document.
804
+ participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id);
805
+ spinner.stop();
806
+ }
807
+ }
808
+ else
809
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} already computed`);
810
+ // Contribution step = UPLOADING.
811
+ if (isFinalizing || participantData.contributionStep === "UPLOADING" /* ParticipantContributionStep.UPLOADING */) {
812
+ spinner.text = `Uploading ${isFinalizing ? "final" : "your"} contribution ${!isFinalizing ? theme.text.bold(`#${nextZkeyIndex}`) : ""} to storage.\n${theme.symbols.warning} This step may take a while based on circuit size and your contribution speed. Everything's fine, just be patient.`;
813
+ spinner.start();
814
+ if (!isFinalizing)
815
+ await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB), ceremony.id, participantData.tempContributionData);
816
+ else
817
+ await multiPartUpload(cloudFunctions, bucketName, nextZkeyStorageFilePath, nextZkeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
818
+ spinner.succeed(`${isFinalizing ? `Contribution` : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`} correctly saved to storage`);
819
+ // Advance to next contribution step (VERIFYING) if not finalizing.
820
+ if (!isFinalizing) {
821
+ spinner.text = `Preparing for requesting contribution verification...`;
822
+ spinner.start();
823
+ await progressToNextContributionStep(cloudFunctions, ceremony.id);
824
+ await sleep(1000);
825
+ // Refresh most up-to-date data from the participant document.
826
+ participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id);
827
+ spinner.stop();
828
+ }
829
+ }
830
+ // Contribution step = VERIFYING.
831
+ if (isFinalizing || participantData.contributionStep === "VERIFYING" /* ParticipantContributionStep.VERIFYING */) {
832
+ // Format verification time.
833
+ const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(avgTimings.verifyCloudFunction);
834
+ process.stdout.write(`${theme.symbols.info} Your contribution is under verification ${avgTimings.verifyCloudFunction > 0
835
+ ? `(~ ${theme.text.bold(`${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}`)})\n${theme.symbols.warning} This step can take up to one hour based on circuit size. Everything's fine, just be patient.`
836
+ : ``}`);
837
+ try {
838
+ // Execute contribution verification.
839
+ await verifyContribution(cloudFunctions, ceremony.id, circuit, bucketName, contributorOrCoordinatorIdentifier, String(process.env.FIREBASE_CF_URL_VERIFY_CONTRIBUTION));
840
+ }
841
+ catch (error) {
842
+ process.stdout.write(`\n${theme.symbols.error} ${theme.text.bold("Unfortunately there was an error with the contribution verification. Please restart phase2cli and try again. If the problem persists, please contact the ceremony coordinator.")}\n`);
843
+ }
844
+ }
845
+ };
846
+
847
+ /**
848
+ * Ask a binary (yes/no or true/false) customizable question.
849
+ * @param question <string> - the question to be answered.
850
+ * @param active <string> - the active option (default yes).
851
+ * @param inactive <string> - the inactive option (default no).
852
+ * @returns <Promise<Answers<string>>>
853
+ */
854
+ const askForConfirmation = async (question, active = "yes", inactive = "no") => prompts({
855
+ type: "toggle",
856
+ name: "confirmation",
857
+ message: theme.text.bold(question),
858
+ initial: false,
859
+ active,
860
+ inactive
861
+ });
862
+ /**
863
+ * Prompt a series of questios to gather input data for the ceremony setup.
864
+ * @param firestore <Firestore> - the instance of the Firestore database.
865
+ * @returns <Promise<CeremonyInputData>> - the necessary information for the ceremony provided by the coordinator.
866
+ */
867
+ const promptCeremonyInputData = async (firestore) => {
868
+ // Get ceremonies prefixes already in use.
869
+ const ceremoniesDocs = fromQueryToFirebaseDocumentInfo(await getAllCollectionDocs(firestore, commonTerms.collections.ceremonies.name)).sort((a, b) => a.data.sequencePosition - b.data.sequencePosition);
870
+ const prefixesAlreadyInUse = ceremoniesDocs.length > 0 ? ceremoniesDocs.map((ceremony) => ceremony.data.prefix) : [];
871
+ // Define questions.
872
+ const questions = [
873
+ {
874
+ type: "text",
875
+ name: "title",
876
+ message: theme.text.bold(`Ceremony name`),
877
+ validate: (title) => {
878
+ if (title.length <= 0)
879
+ return theme.colors.red(`${theme.symbols.error} Please, enter a non-empty string as the name of the ceremony`);
880
+ // Check if the current name matches one of the already used prefixes.
881
+ if (prefixesAlreadyInUse.includes(extractPrefix(title)))
882
+ return theme.colors.red(`${theme.symbols.error} The name is already in use for another ceremony`);
883
+ return true;
884
+ }
885
+ },
886
+ {
887
+ type: "text",
888
+ name: "description",
889
+ message: theme.text.bold(`Short description`),
890
+ validate: (title) => title.length > 0 ||
891
+ theme.colors.red(`${theme.symbols.error} Please, enter a non-empty string as the description of the ceremony`)
892
+ },
893
+ {
894
+ type: "date",
895
+ name: "startDate",
896
+ message: theme.text.bold(`When should the ceremony open for contributions?`),
897
+ validate: (d) => new Date(d).valueOf() > Date.now()
898
+ ? true
899
+ : theme.colors.red(`${theme.symbols.error} Please, enter a date subsequent to current date`)
900
+ }
901
+ ];
902
+ // Prompt questions.
903
+ const { title, description, startDate } = await prompts(questions);
904
+ if (!title || !description || !startDate)
905
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
906
+ // Prompt for questions that depend on the answers to the previous ones.
907
+ const { endDate } = await prompts({
908
+ type: "date",
909
+ name: "endDate",
910
+ message: theme.text.bold(`When should the ceremony stop accepting contributions?`),
911
+ validate: (d) => new Date(d).valueOf() > new Date(startDate).valueOf()
912
+ ? true
913
+ : theme.colors.red(`${theme.symbols.error} Please, enter a date subsequent to starting date`)
914
+ });
915
+ if (!endDate)
916
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
917
+ process.stdout.write("\n");
918
+ // Prompt for timeout mechanism type selection.
919
+ const { timeoutMechanismType } = await prompts({
920
+ type: "select",
921
+ name: "timeoutMechanismType",
922
+ message: theme.text.bold("Select the methodology for deciding to unblock the queue due to contributor disconnection, extreme slow computation, or malicious behavior"),
923
+ choices: [
924
+ {
925
+ title: "Dynamic (self-update approach based on latest contribution time)",
926
+ value: "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */
927
+ },
928
+ {
929
+ title: "Fixed (approach based on a fixed amount of time)",
930
+ value: "FIXED" /* CeremonyTimeoutType.FIXED */
931
+ }
932
+ ],
933
+ initial: 0
934
+ });
935
+ if (timeoutMechanismType !== "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */ && timeoutMechanismType !== "FIXED" /* CeremonyTimeoutType.FIXED */)
936
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
937
+ // Prompt for penalty.
938
+ const { penalty } = await prompts({
939
+ type: "number",
940
+ name: "penalty",
941
+ message: theme.text.bold(`How long should a user have to attend before they can join the waiting queue again after a detected blocking situation? Please, express the value in minutes`),
942
+ validate: (pnlt) => {
943
+ if (pnlt < 1)
944
+ return theme.colors.red(`${theme.symbols.error} Please, enter a penalty at least one minute long`);
945
+ return true;
946
+ }
947
+ });
948
+ if (!penalty)
949
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
950
+ return {
951
+ title,
952
+ description,
953
+ startDate,
954
+ endDate,
955
+ timeoutMechanismType,
956
+ penalty
957
+ };
958
+ };
959
+ /**
960
+ * Prompt a series of questios to gather input about the Circom compiler.
961
+ * @returns <Promise<CircomCompilerData>> - the necessary information for the Circom compiler used for the circuits.
962
+ */
963
+ const promptCircomCompiler = async () => {
964
+ const questions = [
965
+ {
966
+ type: "text",
967
+ name: "version",
968
+ message: theme.text.bold(`Circom compiler version (x.y.z)`),
969
+ validate: (version) => {
970
+ if (version.length <= 0 || !version.match(/^[0-9].[0-9.].[0-9]$/))
971
+ return theme.colors.red(`${theme.symbols.error} Please, provide a valid Circom compiler version (e.g., 2.0.5)`);
972
+ return true;
973
+ }
974
+ },
975
+ {
976
+ type: "text",
977
+ name: "commitHash",
978
+ message: theme.text.bold(`The commit hash of the version of the Circom compiler`),
979
+ validate: (commitHash) => commitHash.length === 40 ||
980
+ theme.colors.red(`${theme.symbols.error} Please,enter a 40-character commit hash (e.g., b7ad01b11f9b4195e38ecc772291251260ab2c67)`)
981
+ }
982
+ ];
983
+ const { version, commitHash } = await prompts(questions);
984
+ if (!version || !commitHash)
985
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
986
+ return {
987
+ version,
988
+ commitHash
989
+ };
990
+ };
991
+ /**
992
+ * Shows a list of circuits for a single option selection.
993
+ * @dev the circuit names are derived from local R1CS files.
994
+ * @param options <Array<string>> - an array of circuits names.
995
+ * @returns Promise<string> - the name of the choosen circuit.
996
+ */
997
+ const promptCircuitSelector = async (options) => {
998
+ const { circuitFilename } = await prompts({
999
+ type: "select",
1000
+ name: "circuitFilename",
1001
+ message: theme.text.bold("Select the R1CS file related to the circuit you want to add to the ceremony"),
1002
+ choices: options.map((option) => ({ title: option, value: option })),
1003
+ initial: 0
1004
+ });
1005
+ if (!circuitFilename)
1006
+ showError(COMMAND_ERRORS.COMMAND_ABORT_SELECTION, true);
1007
+ return circuitFilename;
1008
+ };
1009
+ /**
1010
+ * Shows a list of standard EC2 VM instance types for a single option selection.
1011
+ * @notice the suggested VM configuration type is calculated based on circuit constraint size.
1012
+ * @param constraintSize <number> - the amount of circuit constraints
1013
+ * @returns Promise<string> - the name of the choosen VM type.
1014
+ */
1015
+ const promptVMTypeSelector = async (constraintSize) => {
1016
+ let suggestedConfiguration = 0;
1017
+ // Suggested configuration based on circuit constraint size.
1018
+ if (constraintSize >= 0 && constraintSize <= 1000000)
1019
+ suggestedConfiguration = 1; // t3_large.
1020
+ else if (constraintSize > 1000000 && constraintSize <= 2000000)
1021
+ suggestedConfiguration = 2; // t3_2xlarge.
1022
+ else if (constraintSize > 2000000 && constraintSize <= 5000000)
1023
+ suggestedConfiguration = 3; // c5a_8xlarge.
1024
+ else if (constraintSize > 5000000 && constraintSize <= 30000000)
1025
+ suggestedConfiguration = 4; // c6id_32xlarge.
1026
+ else if (constraintSize > 30000000)
1027
+ suggestedConfiguration = 5; // m6a_32xlarge.
1028
+ const options = [
1029
+ {
1030
+ title: `${vmConfigurationTypes.t3_large.type} (RAM ${vmConfigurationTypes.t3_large.ram} + VCPUs ${vmConfigurationTypes.t3_large.vcpu} = ${vmConfigurationTypes.t3_large.pricePerHour}$ x hour)`,
1031
+ value: vmConfigurationTypes.t3_large.type
1032
+ },
1033
+ {
1034
+ title: `${vmConfigurationTypes.t3_2xlarge.type} (RAM ${vmConfigurationTypes.t3_2xlarge.ram} + VCPUs ${vmConfigurationTypes.t3_2xlarge.vcpu} = ${vmConfigurationTypes.t3_2xlarge.pricePerHour}$ x hour)`,
1035
+ value: vmConfigurationTypes.t3_2xlarge.type
1036
+ },
1037
+ {
1038
+ title: `${vmConfigurationTypes.c5_9xlarge.type} (RAM ${vmConfigurationTypes.c5_9xlarge.ram} + VCPUs ${vmConfigurationTypes.c5_9xlarge.vcpu} = ${vmConfigurationTypes.c5_9xlarge.pricePerHour}$ x hour)`,
1039
+ value: vmConfigurationTypes.c5_9xlarge.type
1040
+ },
1041
+ {
1042
+ title: `${vmConfigurationTypes.c5_18xlarge.type} (RAM ${vmConfigurationTypes.c5_18xlarge.ram} + VCPUs ${vmConfigurationTypes.c5_18xlarge.vcpu} = ${vmConfigurationTypes.c5_18xlarge.pricePerHour}$ x hour)`,
1043
+ value: vmConfigurationTypes.c5_18xlarge.type
1044
+ },
1045
+ {
1046
+ title: `${vmConfigurationTypes.c5a_8xlarge.type} (RAM ${vmConfigurationTypes.c5a_8xlarge.ram} + VCPUs ${vmConfigurationTypes.c5a_8xlarge.vcpu} = ${vmConfigurationTypes.c5a_8xlarge.pricePerHour}$ x hour)`,
1047
+ value: vmConfigurationTypes.c5a_8xlarge.type
1048
+ },
1049
+ {
1050
+ title: `${vmConfigurationTypes.c6id_32xlarge.type} (RAM ${vmConfigurationTypes.c6id_32xlarge.ram} + VCPUs ${vmConfigurationTypes.c6id_32xlarge.vcpu} = ${vmConfigurationTypes.c6id_32xlarge.pricePerHour}$ x hour)`,
1051
+ value: vmConfigurationTypes.c6id_32xlarge.type
1052
+ },
1053
+ {
1054
+ title: `${vmConfigurationTypes.m6a_32xlarge.type} (RAM ${vmConfigurationTypes.m6a_32xlarge.ram} + VCPUs ${vmConfigurationTypes.m6a_32xlarge.vcpu} = ${vmConfigurationTypes.m6a_32xlarge.pricePerHour}$ x hour)`,
1055
+ value: vmConfigurationTypes.m6a_32xlarge.type
1056
+ }
1057
+ ];
1058
+ const { vmType } = await prompts({
1059
+ type: "select",
1060
+ name: "vmType",
1061
+ message: theme.text.bold("Choose your VM type based on your needs (suggested option at first)"),
1062
+ choices: options,
1063
+ initial: suggestedConfiguration
1064
+ });
1065
+ if (!vmType)
1066
+ showError(COMMAND_ERRORS.COMMAND_ABORT_SELECTION, true);
1067
+ return vmType;
1068
+ };
1069
+ /**
1070
+ * Shows a list of disk types for selected VM.
1071
+ * @returns Promise<DiskTypeForVM> - the selected disk type.
1072
+ */
1073
+ const promptVMDiskTypeSelector = async () => {
1074
+ const options = [
1075
+ {
1076
+ title: "GP2",
1077
+ value: "gp2" /* DiskTypeForVM.GP2 */
1078
+ },
1079
+ {
1080
+ title: "GP3",
1081
+ value: "gp3" /* DiskTypeForVM.GP3 */
1082
+ },
1083
+ {
1084
+ title: "IO1",
1085
+ value: "io1" /* DiskTypeForVM.IO1 */
1086
+ },
1087
+ {
1088
+ title: "SC1",
1089
+ value: "sc1" /* DiskTypeForVM.SC1 */
1090
+ },
1091
+ {
1092
+ title: "ST1",
1093
+ value: "st1" /* DiskTypeForVM.ST1 */
1094
+ }
1095
+ ];
1096
+ const { vmDiskType } = await prompts({
1097
+ type: "select",
1098
+ name: "vmDiskType",
1099
+ message: theme.text.bold("Choose your VM disk (volume) type based on your needs (nb. the disk size is automatically computed based on OS + verification minimal space requirements)"),
1100
+ choices: options,
1101
+ initial: 0
1102
+ });
1103
+ if (!vmDiskType)
1104
+ showError(COMMAND_ERRORS.COMMAND_ABORT_SELECTION, true);
1105
+ return vmDiskType;
1106
+ };
1107
+ /**
1108
+ * Show a series of questions about the circuits.
1109
+ * @param constraintSize <number> - the amount of circuit constraints.
1110
+ * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony.
1111
+ * @param needPromptCircomCompiler <boolean> - a boolean value indicating if the questions related to the Circom compiler version and commit hash must be asked.
1112
+ * @param enforceVM <boolean> - a boolean value indicating if the contribution verification could be supported by VM-only approach or not.
1113
+ * @returns Promise<Array<Circuit>> - circuit info prompted by the coordinator.
1114
+ */
1115
+ const promptCircuitInputData = async (constraintSize, timeoutMechanismType, sameCircomCompiler, enforceVM) => {
1116
+ // State data.
1117
+ let circuitConfigurationValues = [];
1118
+ let dynamicTimeoutThreshold = 0;
1119
+ let fixedTimeoutTimeWindow = 0;
1120
+ let circomVersion = "";
1121
+ let circomCommitHash = "";
1122
+ let circuitInputData;
1123
+ let useCfOrVm;
1124
+ let vmDiskType;
1125
+ let vmConfigurationType = "";
1126
+ const questions = [
1127
+ {
1128
+ type: "text",
1129
+ name: "description",
1130
+ message: theme.text.bold(`Short description`),
1131
+ validate: (title) => title.length > 0 ||
1132
+ theme.colors.red(`${theme.symbols.error} Please, enter a non-empty string as the description of the circuit`)
1133
+ },
1134
+ {
1135
+ name: "externalReference",
1136
+ type: "text",
1137
+ message: theme.text.bold(`The external link to the circuit`),
1138
+ validate: (value) => value.length > 0 && value.match(/(https?:\/\/[^\s]+\.circom$)/g)
1139
+ ? true
1140
+ : theme.colors.red(`${theme.symbols.error} Please, provide a valid link to the circuit (e.g., https://github.com/iden3/circomlib/blob/master/circuits/poseidon.circom)`)
1141
+ },
1142
+ {
1143
+ name: "templateCommitHash",
1144
+ type: "text",
1145
+ message: theme.text.bold(`The commit hash of the circuit`),
1146
+ validate: (commitHash) => commitHash.length === 40 ||
1147
+ theme.colors.red(`${theme.symbols.error} Please, provide a valid commit hash (e.g., b7ad01b11f9b4195e38ecc772291251260ab2c67)`)
1148
+ }
1149
+ ];
1150
+ // Prompt for circuit data.
1151
+ const { description, externalReference, templateCommitHash } = await prompts(questions);
1152
+ if (!description || !externalReference || !templateCommitHash)
1153
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1154
+ // Ask for circuit configuration.
1155
+ const { confirmation: needConfiguration } = await askForConfirmation(`Did the circuit template require configuration with parameters?`, `Yes`, `No`);
1156
+ if (needConfiguration === undefined)
1157
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1158
+ if (needConfiguration) {
1159
+ // Ask for values if needed config.
1160
+ const { circuitValues } = await prompts({
1161
+ name: "circuitValues",
1162
+ type: "text",
1163
+ message: theme.text.bold(`Circuit template configuration in a comma-separated list of values`),
1164
+ validate: (value) => (value.split(",").length === 1 && !!value) ||
1165
+ (value.split(`,`).length > 1 && value.includes(",")) ||
1166
+ theme.colors.red(`${theme.symbols.error} Please, provide a correct comma-separated list of values (e.g., 10,2,1,2)`)
1167
+ });
1168
+ if (circuitValues === undefined)
1169
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1170
+ circuitConfigurationValues = circuitValues.split(",");
1171
+ }
1172
+ // Prompt for Circom compiler info (if needed).
1173
+ if (!sameCircomCompiler) {
1174
+ const { version, commitHash } = await promptCircomCompiler();
1175
+ circomVersion = version;
1176
+ circomCommitHash = commitHash;
1177
+ }
1178
+ // Ask for prefered contribution verification method (CF vs VM).
1179
+ if (!enforceVM) {
1180
+ const { confirmation } = await askForConfirmation(`The contribution verification can be performed using Cloud Functions (CF, cheaper for small contributions but limited to 1M constraints) or custom virtual machines (expensive but could scale up to 30M constraints). Be aware about VM costs and if you wanna learn more, please visit the documentation to have a complete overview about cost estimation of the two mechanisms.\nChoose the contribution verification mechanism`, `CF`, // eq. true.
1181
+ `VM` // eq. false.
1182
+ );
1183
+ useCfOrVm = confirmation;
1184
+ }
1185
+ else
1186
+ useCfOrVm = "VM" /* CircuitContributionVerificationMechanism.VM */;
1187
+ if (useCfOrVm === undefined)
1188
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1189
+ if (!useCfOrVm) {
1190
+ // Ask for selecting the specific VM configuration type.
1191
+ vmConfigurationType = await promptVMTypeSelector(constraintSize);
1192
+ // Ask for selecting the specific VM disk (volume) type.
1193
+ vmDiskType = await promptVMDiskTypeSelector();
1194
+ }
1195
+ // Ask for dynamic timeout mechanism data.
1196
+ if (timeoutMechanismType === "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */) {
1197
+ const { dynamicThreshold } = await prompts({
1198
+ type: "number",
1199
+ name: "dynamicThreshold",
1200
+ message: theme.text.bold(`The dynamic timeout requires an acceptance threshold (expressed in %) to avoid disqualifying too many contributors for non-critical issues.\nFor example, suppose we set a threshold at 20%. If the average contribution is 10 minutes, the next contributor has 12 minutes to complete download, computation, and upload (verification is NOT included).\nTherefore, assuming it took 11:30 minutes, the next contributor will have (10 + 11:30) / 2 = 10:45 + 20% = 2:15 + 10:45 = 13 minutes total.\nPlease, set your threshold`),
1201
+ validate: (value) => {
1202
+ if (value === undefined || value < 0 || value > 100)
1203
+ return theme.colors.red(`${theme.symbols.error} Please, provide a valid threshold selecting a value between [0-100]%. We suggest at least 25%.`);
1204
+ return true;
1205
+ }
1206
+ });
1207
+ if (dynamicThreshold === undefined || dynamicThreshold < 0 || dynamicThreshold > 100)
1208
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1209
+ dynamicTimeoutThreshold = dynamicThreshold;
1210
+ circuitInputData = {
1211
+ description,
1212
+ dynamicThreshold: dynamicTimeoutThreshold,
1213
+ compiler: {
1214
+ version: circomVersion,
1215
+ commitHash: circomCommitHash
1216
+ },
1217
+ template: {
1218
+ source: externalReference,
1219
+ commitHash: templateCommitHash,
1220
+ paramsConfiguration: circuitConfigurationValues
1221
+ },
1222
+ verification: {
1223
+ cfOrVm: useCfOrVm
1224
+ ? "CF" /* CircuitContributionVerificationMechanism.CF */
1225
+ : "VM" /* CircuitContributionVerificationMechanism.VM */,
1226
+ vm: {
1227
+ vmConfigurationType,
1228
+ vmDiskType
1229
+ }
1230
+ }
1231
+ };
1232
+ }
1233
+ else {
1234
+ // Ask for fixed timeout mechanism data.
1235
+ const { fixedTimeWindow } = await prompts({
1236
+ type: "number",
1237
+ name: `fixedTimeWindow`,
1238
+ message: theme.text.bold(`The fixed timeout requires a fixed time window for contribution. Your time window in minutes`),
1239
+ validate: (value) => {
1240
+ if (value <= 0)
1241
+ return theme.colors.red(`${theme.symbols.error} Please, provide a time window greater than zero`);
1242
+ return true;
1243
+ }
1244
+ });
1245
+ if (fixedTimeWindow === undefined || fixedTimeWindow <= 0)
1246
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1247
+ fixedTimeoutTimeWindow = fixedTimeWindow;
1248
+ circuitInputData = {
1249
+ description,
1250
+ fixedTimeWindow: fixedTimeoutTimeWindow,
1251
+ compiler: {
1252
+ version: circomVersion,
1253
+ commitHash: circomCommitHash
1254
+ },
1255
+ template: {
1256
+ source: externalReference,
1257
+ commitHash: templateCommitHash,
1258
+ paramsConfiguration: circuitConfigurationValues
1259
+ },
1260
+ verification: {
1261
+ cfOrVm: useCfOrVm
1262
+ ? "CF" /* CircuitContributionVerificationMechanism.CF */
1263
+ : "VM" /* CircuitContributionVerificationMechanism.VM */,
1264
+ vm: {
1265
+ vmConfigurationType,
1266
+ vmDiskType
1267
+ }
1268
+ }
1269
+ };
1270
+ }
1271
+ return circuitInputData;
1272
+ };
1273
+ /**
1274
+ * Prompt for asking if the same circom compiler version has been used for all circuits of the ceremony.
1275
+ * @returns <Promise<boolean>>
1276
+ */
1277
+ const promptSameCircomCompiler = async () => {
1278
+ const { confirmation: sameCircomCompiler } = await askForConfirmation("Did the circuits of the ceremony were compiled with the same version of circom?", "Yes", "No");
1279
+ if (sameCircomCompiler === undefined)
1280
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1281
+ return sameCircomCompiler;
1282
+ };
1283
+ /**
1284
+ * Prompt for asking if the coordinator wanna use a pre-computed zKey for the given circuit.
1285
+ * @returns <Promise<boolean>>
1286
+ */
1287
+ const promptPreComputedZkey = async () => {
1288
+ const { confirmation: wannaUsePreComputedZkey } = await askForConfirmation("Would you like to use a pre-computed zKey for this circuit?", "Yes", "No");
1289
+ if (wannaUsePreComputedZkey === undefined)
1290
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1291
+ return wannaUsePreComputedZkey;
1292
+ };
1293
+ /**
1294
+ * Prompt for asking if the coordinator wants to add a new circuit to the ceremony.
1295
+ * @returns <Promise<boolean>>
1296
+ */
1297
+ const promptCircuitAddition = async () => {
1298
+ const { confirmation: wannaAddNewCircuit } = await askForConfirmation("Want to add another circuit for the ceremony?", "Yes", "No");
1299
+ if (wannaAddNewCircuit === undefined)
1300
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1301
+ return wannaAddNewCircuit;
1302
+ };
1303
+ /**
1304
+ * Shows a list of pre-computed zKeys for a single option selection.
1305
+ * @dev the names are derived from local zKeys files.
1306
+ * @param options <Array<string>> - an array of pre-computed zKeys names.
1307
+ * @returns Promise<string> - the name of the choosen pre-computed zKey.
1308
+ */
1309
+ const promptPreComputedZkeySelector = async (options) => {
1310
+ const { preComputedZkeyFilename } = await prompts({
1311
+ type: "select",
1312
+ name: "preComputedZkeyFilename",
1313
+ message: theme.text.bold("Select the pre-computed zKey file related to the circuit"),
1314
+ choices: options.map((option) => ({ title: option, value: option })),
1315
+ initial: 0
1316
+ });
1317
+ if (!preComputedZkeyFilename)
1318
+ showError(COMMAND_ERRORS.COMMAND_ABORT_SELECTION, true);
1319
+ return preComputedZkeyFilename;
1320
+ };
1321
+ /**
1322
+ * Prompt asking to the coordinator to choose the desired PoT file for the zKey for the circuit.
1323
+ * @param suggestedSmallestNeededPowers <number> - the minimal number of powers necessary for circuit zKey generation.
1324
+ * @returns Promise<number> - the selected amount of powers.
1325
+ */
1326
+ const promptNeededPowersForCircuit = async (suggestedSmallestNeededPowers) => {
1327
+ const question = {
1328
+ name: "choosenPowers",
1329
+ type: "number",
1330
+ message: theme.text.bold(`Specify the amount of Powers of Tau used to generate the pre-computed zKey`),
1331
+ validate: (value) => value >= suggestedSmallestNeededPowers && value <= 28
1332
+ ? true
1333
+ : theme.colors.red(`${theme.symbols.error} Please, provide a valid amount of powers selecting a value between [${suggestedSmallestNeededPowers}-28]. ${suggestedSmallestNeededPowers}`)
1334
+ };
1335
+ // Prompt for circuit data.
1336
+ const { choosenPowers } = await prompts(question);
1337
+ if (choosenPowers === undefined || Number(choosenPowers) < suggestedSmallestNeededPowers)
1338
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1339
+ return choosenPowers;
1340
+ };
1341
+ /**
1342
+ * Shows a list of PoT files for a single option selection.
1343
+ * @dev the names are derived from local PoT files.
1344
+ * @param options <Array<string>> - an array of PoT file names.
1345
+ * @returns Promise<string> - the name of the choosen PoT.
1346
+ */
1347
+ const promptPotSelector = async (options) => {
1348
+ const { potFilename } = await prompts({
1349
+ type: "select",
1350
+ name: "potFilename",
1351
+ message: theme.text.bold("Select the Powers of Tau file choosen for the circuit"),
1352
+ choices: options.map((option) => {
1353
+ console.log(option);
1354
+ return { title: option, value: option };
1355
+ }),
1356
+ initial: 0
1357
+ });
1358
+ if (!potFilename)
1359
+ showError(COMMAND_ERRORS.COMMAND_ABORT_SELECTION, true);
1360
+ return potFilename;
1361
+ };
1362
+ /**
1363
+ * Prompt for asking about ceremony selection.
1364
+ * @dev this method is used to show a list of ceremonies to be selected for both the computation of a contribution and the finalization of a ceremony.
1365
+ * @param ceremoniesDocuments <Array<FirebaseDocumentInfo>> - the list of ceremonies Firestore documents.
1366
+ * @param isFinalizing <boolean> - true when the coordinator must select a ceremony for finalization; otherwise false (participant selects a ceremony for contribution).
1367
+ * @returns Promise<FirebaseDocumentInfo> - the Firestore document of the selected ceremony.
1368
+ */
1369
+ const promptForCeremonySelection = async (ceremoniesDocuments, isFinalizing) => {
1370
+ // Prepare state.
1371
+ const choices = [];
1372
+ // Prepare choices x ceremony.
1373
+ // Data to be shown for selection.
1374
+ // nb. when is not finalizing, extract info to compute the amount of days left for contribute (86400000 ms x day).
1375
+ for (const ceremonyDocument of ceremoniesDocuments)
1376
+ choices.push({
1377
+ title: ceremonyDocument.data.title,
1378
+ description: `${ceremonyDocument.data.description} ${!isFinalizing
1379
+ ? `(${theme.colors.magenta(Math.ceil(Math.abs(Date.now() - ceremonyDocument.data.endDate) / 86400000))} days left)`
1380
+ : ""}`,
1381
+ value: ceremonyDocument
1382
+ });
1383
+ // Prompt for selection.
1384
+ const { ceremony } = await prompts({
1385
+ type: "select",
1386
+ name: "ceremony",
1387
+ message: theme.text.bold(!isFinalizing
1388
+ ? "Which ceremony would you like to contribute to?"
1389
+ : "Which ceremony would you like to finalize?"),
1390
+ choices,
1391
+ initial: 0
1392
+ });
1393
+ if (!ceremony || ceremony === undefined)
1394
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1395
+ return ceremony;
1396
+ };
1397
+ /**
1398
+ * Prompt the participant to type the entropy or the coordinator to type the beacon.
1399
+ * @param isEntropy <boolean> - true when prompting for typing entropy; otherwise false.
1400
+ * @returns <Promise<string>> - the entropy or beacon value.
1401
+ */
1402
+ const promptToTypeEntropyOrBeacon = async (isEntropy = true) => {
1403
+ // Prompt for entropy or beacon.
1404
+ const { entropyOrBeacon } = await prompts({
1405
+ type: "text",
1406
+ name: "entropyOrBeacon",
1407
+ style: `${isEntropy ? `password` : `text`}`,
1408
+ message: theme.text.bold(`Enter ${isEntropy ? `entropy (toxic waste)` : `finalization public beacon`}`),
1409
+ validate: (value) => value.length > 0 ||
1410
+ theme.colors.red(`${theme.symbols.error} Please, provide a valid value for the ${isEntropy ? `entropy (toxic waste)` : `finalization public beacon`}`)
1411
+ });
1412
+ if (!entropyOrBeacon || entropyOrBeacon === undefined)
1413
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1414
+ return entropyOrBeacon;
1415
+ };
1416
+ /**
1417
+ * Prompt for entropy generation or insertion.
1418
+ * @return <Promise<string>> - the entropy.
1419
+ */
1420
+ const promptForEntropy = async () => {
1421
+ // Prompt for entropy generation prefered method.
1422
+ const { confirmation } = await askForConfirmation(`Do you prefer to type your entropy or generate it randomly?`, "Manually", "Randomly");
1423
+ if (confirmation === undefined)
1424
+ showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true);
1425
+ // Auto-generate entropy.
1426
+ if (!confirmation)
1427
+ return autoGenerateEntropy();
1428
+ // Prompt for manual entropy input.
1429
+ return promptToTypeEntropyOrBeacon();
1430
+ };
1431
+
1432
+ const packagePath$2 = `${dirname(fileURLToPath(import.meta.url))}`;
1433
+ dotenv.config({
1434
+ path: packagePath$2.includes(`src/lib`)
1435
+ ? `${dirname(fileURLToPath(import.meta.url))}/../../.env`
1436
+ : `${dirname(fileURLToPath(import.meta.url))}/.env`
1437
+ });
1438
+ /**
1439
+ * Bootstrap services and configs is needed for a new command execution and related services.
1440
+ * @returns <Promise<FirebaseServices>>
1441
+ */
1442
+ const bootstrapCommandExecutionAndServices = async () => {
1443
+ // Clean terminal window.
1444
+ clear();
1445
+ // Print header.
1446
+ console.log(theme.colors.magenta(figlet.textSync("Phase 2 cli", { font: "Ogre" })));
1447
+ // Check configs.
1448
+ if (!process.env.AUTH_GITHUB_CLIENT_ID)
1449
+ showError(CONFIG_ERRORS.CONFIG_GITHUB_ERROR, true);
1450
+ if (!process.env.FIREBASE_API_KEY ||
1451
+ !process.env.FIREBASE_AUTH_DOMAIN ||
1452
+ !process.env.FIREBASE_PROJECT_ID ||
1453
+ !process.env.FIREBASE_MESSAGING_SENDER_ID ||
1454
+ !process.env.FIREBASE_APP_ID ||
1455
+ !process.env.FIREBASE_CF_URL_VERIFY_CONTRIBUTION)
1456
+ showError(CONFIG_ERRORS.CONFIG_FIREBASE_ERROR, true);
1457
+ if (!process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB ||
1458
+ !process.env.CONFIG_CEREMONY_BUCKET_POSTFIX ||
1459
+ !process.env.CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS)
1460
+ showError(CONFIG_ERRORS.CONFIG_OTHER_ERROR, true);
1461
+ // Initialize and return Firebase services instances (App, Firestore, Functions)
1462
+ return initializeFirebaseCoreServices(String(process.env.FIREBASE_API_KEY), String(process.env.FIREBASE_AUTH_DOMAIN), String(process.env.FIREBASE_PROJECT_ID), String(process.env.FIREBASE_MESSAGING_SENDER_ID), String(process.env.FIREBASE_APP_ID));
1463
+ };
1464
+ /**
1465
+ * Execute the sign in to Firebase using OAuth credentials.
1466
+ * @dev wrapper method to handle custom errors.
1467
+ * @param firebaseApp <FirebaseApp> - the configured instance of the Firebase App in use.
1468
+ * @param credentials <OAuthCredential> - the OAuth credential generated from token exchange.
1469
+ * @returns <Promise<void>>
1470
+ */
1471
+ const signInToFirebase = async (firebaseApp, credentials) => {
1472
+ try {
1473
+ // Sign in with credentials to Firebase.
1474
+ await signInToFirebaseWithCredentials(firebaseApp, credentials);
1475
+ }
1476
+ catch (error) {
1477
+ // Error handling by parsing error message.
1478
+ if (error.toString().includes("Firebase: Unsuccessful check authorization response from Github")) {
1479
+ showError(CORE_SERVICES_ERRORS.FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS, false);
1480
+ // Clean expired access token from local storage.
1481
+ deleteLocalAccessToken();
1482
+ // Inform user.
1483
+ console.log(`${theme.symbols.info} We have successfully removed your local token to make you able to repeat the authorization process once again. Please, run the auth command again whenever you are ready and complete the association with the CLI application.`);
1484
+ // Gracefully exit.
1485
+ process.exit(0);
1486
+ }
1487
+ if (error.toString().includes("Firebase: Error (auth/user-disabled)"))
1488
+ showError(CORE_SERVICES_ERRORS.FIREBASE_USER_DISABLED, true);
1489
+ if (error
1490
+ .toString()
1491
+ .includes("Firebase: Remote site 5XX from github.com for VERIFY_CREDENTIAL (auth/invalid-credential)"))
1492
+ showError(CORE_SERVICES_ERRORS.FIREBASE_FAILED_CREDENTIALS_VERIFICATION, true);
1493
+ if (error.toString().includes("Firebase: Error (auth/network-request-failed)"))
1494
+ showError(CORE_SERVICES_ERRORS.FIREBASE_NETWORK_ERROR, true);
1495
+ if (error.toString().includes("HttpError: The authorization request was denied"))
1496
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_ACCOUNT_ASSOCIATION_REJECTED, true);
1497
+ if (error
1498
+ .toString()
1499
+ .includes("HttpError: request to https://github.com/login/device/code failed, reason: connect ETIMEDOUT"))
1500
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_SERVER_TIMEDOUT, true);
1501
+ }
1502
+ };
1503
+ /**
1504
+ * Ensure that the callee is an authenticated user.
1505
+ * @notice The token will be passed as parameter.
1506
+ * @dev This method can be used within GitHub actions or other CI/CD pipelines.
1507
+ * @param firebaseApp <FirebaseApp> - the configured instance of the Firebase App in use.
1508
+ * @param token <string> - the token to be used for authentication.
1509
+ * @returns <Promise<AuthUser>> - a custom object containing info about the authenticated user, the token and github handle.
1510
+ */
1511
+ const authWithToken = async (firebaseApp, token) => {
1512
+ // Get credentials.
1513
+ const credentials = exchangeGithubTokenForCredentials(token);
1514
+ // Sign in to Firebase using credentials.
1515
+ await signInToFirebase(firebaseApp, credentials);
1516
+ // Get current authenticated user.
1517
+ const user = getCurrentFirebaseAuthUser(firebaseApp);
1518
+ // Get Github unique identifier (handle-id).
1519
+ const providerUserId = await getGithubProviderUserId(String(token));
1520
+ // Greet the user.
1521
+ console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1522
+ return {
1523
+ user,
1524
+ token,
1525
+ providerUserId
1526
+ };
1527
+ };
1528
+ /**
1529
+ * Ensure that the callee is an authenticated user.
1530
+ * @dev This method MUST be executed before each command to avoid authentication errors when interacting with the command.
1531
+ * @returns <Promise<AuthUser>> - a custom object containing info about the authenticated user, the token and github handle.
1532
+ */
1533
+ const checkAuth = async (firebaseApp) => {
1534
+ // Check for local token.
1535
+ const isLocalTokenStored = checkLocalAccessToken();
1536
+ if (!isLocalTokenStored)
1537
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_NOT_AUTHENTICATED, true);
1538
+ // Retrieve local access token.
1539
+ const token = String(getLocalAccessToken());
1540
+ // Get credentials.
1541
+ const credentials = exchangeGithubTokenForCredentials(token);
1542
+ // Sign in to Firebase using credentials.
1543
+ await signInToFirebase(firebaseApp, credentials);
1544
+ // Get current authenticated user.
1545
+ const user = getCurrentFirebaseAuthUser(firebaseApp);
1546
+ // Get Github unique identifier (handle-id).
1547
+ const providerUserId = await getGithubProviderUserId(String(token));
1548
+ // Greet the user.
1549
+ console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1550
+ return {
1551
+ user,
1552
+ token,
1553
+ providerUserId
1554
+ };
1555
+ };
1556
+
1557
+ /**
1558
+ * Handle whatever is needed to obtain the input data for a circuit that the coordinator would like to add to the ceremony.
1559
+ * @param choosenCircuitFilename <string> - the name of the circuit to add.
1560
+ * @param matchingWasmFilename <string> - the name of the circuit wasm file.
1561
+ * @param ceremonyTimeoutMechanismType <CeremonyTimeoutType> - the type of ceremony timeout mechanism.
1562
+ * @param sameCircomCompiler <boolean> - true, if this circuit shares with the others the <CircomCompilerData>; otherwise false.
1563
+ * @param circuitSequencePosition <number> - the position of the circuit in the contribution queue.
1564
+ * @param sharedCircomCompilerData <string> - version and commit hash of the Circom compiler used to compile the ceremony circuits.
1565
+ * @returns <Promise<CircuitInputData>> - the input data of the circuit to add to the ceremony.
1566
+ */
1567
+ const getInputDataToAddCircuitToCeremony = async (choosenCircuitFilename, matchingWasmFilename, ceremonyTimeoutMechanismType, sameCircomCompiler, circuitSequencePosition, sharedCircomCompilerData) => {
1568
+ // Extract name and prefix.
1569
+ const circuitName = choosenCircuitFilename.substring(0, choosenCircuitFilename.indexOf("."));
1570
+ const circuitPrefix = extractPrefix(circuitName);
1571
+ // R1CS file path.
1572
+ const r1csCWDFilePath = getCWDFilePath(process.cwd(), choosenCircuitFilename);
1573
+ const spinner = customSpinner(`Looking for circuit metadata...`, "clock");
1574
+ spinner.start();
1575
+ // Read R1CS and store metadata locally.
1576
+ const metadata = getR1CSInfo(r1csCWDFilePath);
1577
+ await sleep(2000); // Sleep 2s to avoid unexpected termination (file descriptor close).
1578
+ spinner.succeed(`Circuit metadata read and saved correctly`);
1579
+ // Prompt for circuit input data.
1580
+ const circuitInputData = await promptCircuitInputData(metadata.constraints, ceremonyTimeoutMechanismType, sameCircomCompiler, !(metadata.constraints <= 1000000) // nb. we assume after our dry-runs that CF works fine for up to one million circuit constraints.
1581
+ );
1582
+ process.stdout.write("\n");
1583
+ // Return updated data.
1584
+ return {
1585
+ ...circuitInputData,
1586
+ metadata,
1587
+ compiler: {
1588
+ commitHash: !circuitInputData.compiler.commitHash && sameCircomCompiler
1589
+ ? sharedCircomCompilerData.commitHash
1590
+ : circuitInputData.compiler.commitHash,
1591
+ version: !circuitInputData.compiler.version && sameCircomCompiler
1592
+ ? sharedCircomCompilerData.version
1593
+ : circuitInputData.compiler.version
1594
+ },
1595
+ compilationArtifacts: {
1596
+ r1csFilename: choosenCircuitFilename,
1597
+ wasmFilename: matchingWasmFilename
1598
+ },
1599
+ name: circuitName,
1600
+ prefix: circuitPrefix,
1601
+ sequencePosition: circuitSequencePosition
1602
+ };
1603
+ };
1604
+ /**
1605
+ * Handle the addition of one or more circuits to the ceremony.
1606
+ * @param options <Array<string>> - list of possible circuits that can be added to the ceremony.
1607
+ * @param ceremonyTimeoutMechanismType <CeremonyTimeoutType> - the type of ceremony timeout mechanism.
1608
+ * @returns <Promise<Array<CircuitInputData>>> - the input data for each circuit that has been added to the ceremony.
1609
+ */
1610
+ const handleAdditionOfCircuitsToCeremony = async (r1csOptions, wasmOptions, ceremonyTimeoutMechanismType) => {
1611
+ // Prepare data.
1612
+ const inputDataForCircuits = []; // All circuits interactive data.
1613
+ let circuitSequencePosition = 1; // The circuit's position for contribution.
1614
+ let readyToSummarizeCeremony = false; // Boolean flag to check whether the coordinator has finished to add circuits to the ceremony.
1615
+ let wannaAddAnotherCircuit = true; // Loop flag.
1616
+ const sharedCircomCompilerData = { version: "", commitHash: "" };
1617
+ // Prompt if the circuits to be added were compiled with the same version of Circom.
1618
+ // nb. CIRCOM compiler version/commit-hash is a declaration useful for later verifiability and avoid bugs.
1619
+ const sameCircomCompiler = await promptSameCircomCompiler();
1620
+ if (sameCircomCompiler) {
1621
+ // Prompt for Circom compiler.
1622
+ const { version, commitHash } = await promptCircomCompiler();
1623
+ sharedCircomCompilerData.version = version;
1624
+ sharedCircomCompilerData.commitHash = commitHash;
1625
+ }
1626
+ while (wannaAddAnotherCircuit) {
1627
+ // Gather information about the ceremony circuits.
1628
+ console.log(theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${circuitSequencePosition}`)}\n`));
1629
+ // Select one circuit among cwd circuits identified by R1CS files.
1630
+ const choosenCircuitFilename = await promptCircuitSelector(r1csOptions);
1631
+ // Update list of possible options for next selection (if, any).
1632
+ r1csOptions = r1csOptions.filter((circuitFilename) => circuitFilename !== choosenCircuitFilename);
1633
+ // Select the wasm file accordingly to circuit R1CS filename.
1634
+ const matchingWasms = wasmOptions.filter((wasmFilename) => choosenCircuitFilename.split(`.r1cs`)[0] ===
1635
+ wasmFilename.split(`.${commonTerms.foldersAndPathsTerms.wasm}`)[0]);
1636
+ if (matchingWasms.length !== 1)
1637
+ showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true);
1638
+ // Get input data for choosen circuit.
1639
+ const circuitInputData = await getInputDataToAddCircuitToCeremony(choosenCircuitFilename, matchingWasms[0], ceremonyTimeoutMechanismType, sameCircomCompiler, circuitSequencePosition, sharedCircomCompilerData);
1640
+ // Store circuit data.
1641
+ inputDataForCircuits.push(circuitInputData);
1642
+ // Check if any circuit is left for potentially addition to ceremony.
1643
+ if (r1csOptions.length !== 0) {
1644
+ // Prompt for selection.
1645
+ const wannaAddNewCircuit = await promptCircuitAddition();
1646
+ if (wannaAddNewCircuit === false)
1647
+ readyToSummarizeCeremony = true; // Terminate circuit addition.
1648
+ else
1649
+ circuitSequencePosition += 1; // Continue with next one.
1650
+ }
1651
+ else
1652
+ readyToSummarizeCeremony = true; // No more circuit to add.
1653
+ // Summarize the ceremony.
1654
+ if (readyToSummarizeCeremony)
1655
+ wannaAddAnotherCircuit = false;
1656
+ }
1657
+ return inputDataForCircuits;
1658
+ };
1659
+ /**
1660
+ * Print ceremony and related circuits information.
1661
+ * @param ceremonyInputData <CeremonyInputData> - the input data of the ceremony.
1662
+ * @param circuits <Array<CircuitDocument>> - the circuit documents associated to the circuits of the ceremony.
1663
+ */
1664
+ const displayCeremonySummary = (ceremonyInputData, circuits) => {
1665
+ // Prepare ceremony summary.
1666
+ let summary = `${`${theme.text.bold(ceremonyInputData.title)}\n${theme.text.italic(ceremonyInputData.description)}`}
1667
+ \n${`Opening: ${theme.text.bold(theme.text.underlined(new Date(ceremonyInputData.startDate).toUTCString().replace("GMT", "UTC")))}\nEnding: ${theme.text.bold(theme.text.underlined(new Date(ceremonyInputData.endDate).toUTCString().replace("GMT", "UTC")))}`}
1668
+ \n${theme.text.bold(ceremonyInputData.timeoutMechanismType === "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */ ? `Dynamic` : `Fixed`)} Timeout / ${theme.text.bold(ceremonyInputData.penalty)}m Penalty`;
1669
+ for (const circuit of circuits) {
1670
+ // Append circuit summary.
1671
+ summary += `\n\n${theme.text.bold(`- CIRCUIT # ${theme.text.bold(theme.colors.magenta(`${circuit.sequencePosition}`))}`)}
1672
+ \n${`${theme.text.bold(circuit.name)}\n${theme.text.italic(circuit.description)}
1673
+ \nCurve: ${theme.text.bold(circuit.metadata?.curve)}\nCompiler: ${theme.text.bold(`${circuit.compiler.version}`)} (${theme.text.bold(circuit.compiler.commitHash.slice(0, 7))})\nVerification: ${theme.text.bold(`${circuit.verification.cfOrVm}`)} ${theme.text.bold(circuit.verification.cfOrVm === "VM" /* CircuitContributionVerificationMechanism.VM */
1674
+ ? `(${circuit.verification.vm.vmConfigurationType} / ${circuit.verification.vm.vmDiskType} volume)`
1675
+ : "")}\nSource: ${theme.text.bold(circuit.template.source.split(`/`).at(-1))}(${theme.text.bold(circuit.template.paramsConfiguration)})\n${ceremonyInputData.timeoutMechanismType === "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */
1676
+ ? `Threshold: ${theme.text.bold(circuit.dynamicThreshold)}%`
1677
+ : `Max Contribution Time: ${theme.text.bold(circuit.fixedTimeWindow)}m`}
1678
+ \n# Wires: ${theme.text.bold(circuit.metadata?.wires)}\n# Constraints: ${theme.text.bold(circuit.metadata?.constraints)}\n# Private Inputs: ${theme.text.bold(circuit.metadata?.privateInputs)}\n# Public Inputs: ${theme.text.bold(circuit.metadata?.publicInputs)}\n# Labels: ${theme.text.bold(circuit.metadata?.labels)}\n# Outputs: ${theme.text.bold(circuit.metadata?.outputs)}\n# PoT: ${theme.text.bold(circuit.metadata?.pot)}`}`;
1679
+ }
1680
+ // Display complete summary.
1681
+ console.log(boxen(summary, {
1682
+ title: theme.colors.magenta(`CEREMONY SUMMARY`),
1683
+ titleAlignment: "center",
1684
+ textAlignment: "left",
1685
+ margin: 1,
1686
+ padding: 1
1687
+ }));
1688
+ };
1689
+ /**
1690
+ * Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
1691
+ * @dev we are downloading the Powers of Tau file from Hermez Cryptography Phase 1 Trusted Setup.
1692
+ * @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
1693
+ * @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
1694
+ * @returns <Promise<void>>
1695
+ */
1696
+ const checkAndDownloadSmallestPowersOfTau = async (powers, ptauCompleteFilename) => {
1697
+ // Get already downloaded ptau files.
1698
+ const alreadyDownloadedPtauFiles = await getDirFilesSubPaths(localPaths.pot);
1699
+ // Get the required smallest ptau file.
1700
+ const smallestPtauFileForGivenPowers = alreadyDownloadedPtauFiles
1701
+ .filter((dirent) => extractPoTFromFilename(dirent.name) === Number(powers))
1702
+ .map((dirent) => dirent.name);
1703
+ // Check if already downloaded or not.
1704
+ if (smallestPtauFileForGivenPowers.length === 0) {
1705
+ const spinner = customSpinner(`Downloading the ${theme.text.bold(`#${powers}`)} smallest PoT file needed from the Hermez Cryptography Phase 1 Trusted Setup...`, `clock`);
1706
+ spinner.start();
1707
+ // Download smallest Powers of Tau file from remote server.
1708
+ const streamPipeline = promisify(pipeline);
1709
+ // Make the call.
1710
+ const response = await fetch$1(`${potFileDownloadMainUrl}${ptauCompleteFilename}`);
1711
+ // Handle errors.
1712
+ if (!response.ok && response.status !== 200)
1713
+ showError(COMMAND_ERRORS.COMMAND_SETUP_DOWNLOAD_PTAU, true);
1714
+ // Write the file locally
1715
+ else
1716
+ await streamPipeline(response.body, createWriteStream(getPotLocalFilePath(ptauCompleteFilename)));
1717
+ spinner.succeed(`Powers of tau ${theme.text.bold(`#${powers}`)} downloaded successfully`);
1718
+ }
1719
+ else
1720
+ console.log(`${theme.symbols.success} Smallest Powers of Tau ${theme.text.bold(`#${powers}`)} already downloaded`);
1721
+ };
1722
+ /**
1723
+ * Handle the needs in terms of Powers of Tau for the selected pre-computed zKey.
1724
+ * @notice in case there are no Powers of Tau file suitable for the pre-computed zKey (i.e., having a
1725
+ * number of powers greater than or equal to the powers needed by the zKey), the coordinator will be asked
1726
+ * to provide a number of powers manually, ranging from the smallest possible to the largest.
1727
+ * @param neededPowers <number> - the smallest amount of powers needed by the zKey.
1728
+ * @returns Promise<string, string> - the information about the choosen Powers of Tau file for the pre-computed zKey
1729
+ * along with related powers.
1730
+ */
1731
+ const handlePreComputedZkeyPowersOfTauSelection = async (neededPowers) => {
1732
+ let doubleDigitsPowers = ""; // The amount of stringified powers in a double-digits format (XY).
1733
+ let potCompleteFilename = ""; // The complete filename of the Powers of Tau file selected for the pre-computed zKey.
1734
+ let usePreDownloadedPoT = false; // Boolean flag to check if the coordinator is going to use a pre-downloaded PoT file or not.
1735
+ // Check for PoT file associated to selected pre-computed zKey.
1736
+ const spinner = customSpinner("Looking for Powers of Tau files...", "clock");
1737
+ spinner.start();
1738
+ // Get local `.ptau` files.
1739
+ const potFilePaths = await filterDirectoryFilesByExtension(process.cwd(), `.ptau`);
1740
+ // Filter based on suitable amount of powers.
1741
+ const potOptions = potFilePaths
1742
+ .filter((dirent) => extractPoTFromFilename(dirent.name) >= neededPowers)
1743
+ .map((dirent) => dirent.name);
1744
+ if (potOptions.length <= 0) {
1745
+ spinner.warn(`There is no already downloaded Powers of Tau file suitable for this zKey`);
1746
+ // Ask coordinator to input the amount of powers.
1747
+ const choosenPowers = await promptNeededPowersForCircuit(neededPowers);
1748
+ // Convert to double digits powers (e.g., 9 -> 09).
1749
+ doubleDigitsPowers = convertToDoubleDigits(choosenPowers);
1750
+ potCompleteFilename = `${potFilenameTemplate}${doubleDigitsPowers}.ptau`;
1751
+ }
1752
+ else {
1753
+ spinner.stop();
1754
+ // Prompt for Powers of Tau selection among already downloaded ones.
1755
+ potCompleteFilename = await promptPotSelector(potOptions);
1756
+ // Convert to double digits powers (e.g., 9 -> 09).
1757
+ doubleDigitsPowers = convertToDoubleDigits(extractPoTFromFilename(potCompleteFilename));
1758
+ usePreDownloadedPoT = true;
1759
+ }
1760
+ return {
1761
+ doubleDigitsPowers,
1762
+ potCompleteFilename,
1763
+ usePreDownloadedPoT
1764
+ };
1765
+ };
1766
+ /**
1767
+ * Generate a brand new zKey from scratch.
1768
+ * @param r1csLocalPathAndFileName <string> - the local complete path of the R1CS selected file.
1769
+ * @param potLocalPathAndFileName <string> - the local complete path of the PoT selected file.
1770
+ * @param zkeyLocalPathAndFileName <string> - the local complete path of the pre-computed zKey selected file.
1771
+ */
1772
+ const handleNewZkeyGeneration = async (r1csLocalPathAndFileName, potLocalPathAndFileName, zkeyLocalPathAndFileName) => {
1773
+ console.log(`${theme.symbols.info} The computation of your brand new zKey is starting soon.\n${theme.text.bold(`${theme.symbols.warning} Be careful, stopping the process will result in the loss of all progress achieved so far.`)}`);
1774
+ // Generate zKey.
1775
+ await zKey.newZKey(r1csLocalPathAndFileName, potLocalPathAndFileName, zkeyLocalPathAndFileName, console);
1776
+ console.log(`\n${theme.symbols.success} Generation of genesis zKey completed successfully`);
1777
+ };
1778
+ /**
1779
+ * Manage the creation of a ceremony file storage bucket.
1780
+ * @param firebaseFunctions <Functions> - the Firebase Cloud Functions instance connected to the current application.
1781
+ * @param ceremonyPrefix <string> - the prefix of the ceremony.
1782
+ * @returns <Promise<string>> - the ceremony bucket name.
1783
+ */
1784
+ const handleCeremonyBucketCreation = async (firebaseFunctions, ceremonyPrefix) => {
1785
+ // Compose bucket name using the ceremony prefix.
1786
+ const bucketName = getBucketName(ceremonyPrefix, process.env.CONFIG_CEREMONY_BUCKET_POSTFIX);
1787
+ const spinner = customSpinner(`Getting ready for ceremony files and data storage...`, `clock`);
1788
+ spinner.start();
1789
+ try {
1790
+ // Make the call to create the bucket.
1791
+ await createS3Bucket(firebaseFunctions, bucketName);
1792
+ }
1793
+ catch (error) {
1794
+ const errorBody = JSON.parse(JSON.stringify(error));
1795
+ showError(`[${errorBody.code}] ${error.message} ${!errorBody.details ? "" : `\n${errorBody.details}`}`, true);
1796
+ }
1797
+ spinner.succeed(`Ceremony bucket has been successfully created`);
1798
+ return bucketName;
1799
+ };
1800
+ /**
1801
+ * Upload a circuit artifact (r1cs, WASM, ptau) to the ceremony storage.
1802
+ * @dev this method uses a multi part upload to upload the file in chunks.
1803
+ * @param firebaseFunctions <Functions> - the Firebase Cloud Functions instance connected to the current application.
1804
+ * @param bucketName <string> - the ceremony bucket name.
1805
+ * @param storageFilePath <string> - the storage (bucket) path where the file should be uploaded.
1806
+ * @param localPathAndFileName <string> - the local file path where is located.
1807
+ * @param completeFilename <string> - the complete filename.
1808
+ */
1809
+ const handleCircuitArtifactUploadToStorage = async (firebaseFunctions, bucketName, storageFilePath, localPathAndFileName, completeFilename) => {
1810
+ const spinner = customSpinner(`Uploading ${theme.text.bold(completeFilename)} file to ceremony storage...`, `clock`);
1811
+ spinner.start();
1812
+ await multiPartUpload(firebaseFunctions, bucketName, storageFilePath, localPathAndFileName, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
1813
+ spinner.succeed(`Upload of (${theme.text.bold(completeFilename)}) file completed successfully`);
1814
+ };
1815
+ /**
1816
+ * Setup command.
1817
+ * @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
1818
+ * @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
1819
+ * for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
1820
+ * from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
1821
+ * @param cmd? <any> - the path to the ceremony setup file.
1822
+ */
1823
+ const setup = async (cmd) => {
1824
+ // Setup command state.
1825
+ const circuits = []; // Circuits.
1826
+ let ceremonyId = ""; // The unique identifier of the ceremony.
1827
+ const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
1828
+ // Check for authentication.
1829
+ const { user, providerUserId } = cmd.auth ? await authWithToken(firebaseApp, cmd.auth) : await checkAuth(firebaseApp);
1830
+ // Preserve command execution only for coordinators.
1831
+ if (!(await isCoordinator(user)))
1832
+ showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
1833
+ // Get current working directory.
1834
+ const cwd = process.cwd();
1835
+ console.log(`${theme.symbols.warning} To setup a zkSNARK Groth16 Phase 2 Trusted Setup ceremony you need to have the Rank-1 Constraint System (R1CS) file for each circuit in your working directory`);
1836
+ console.log(`\n${theme.symbols.info} Your current working directory is ${theme.text.bold(theme.text.underlined(process.cwd()))}\n`);
1837
+ // Prepare local directories.
1838
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.output);
1839
+ cleanDir(localPaths.setup);
1840
+ cleanDir(localPaths.pot);
1841
+ cleanDir(localPaths.zkeys);
1842
+ cleanDir(localPaths.wasm);
1843
+ // if there is the file option, then set up the non interactively
1844
+ if (cmd.template) {
1845
+ // 1. parse the file
1846
+ // tmp data - do not cleanup files as we need them
1847
+ const spinner = customSpinner(`Parsing ${theme.text.bold(cmd.template)} setup configuration file...`, `clock`);
1848
+ spinner.start();
1849
+ const setupCeremonyData = await parseCeremonyFile(cmd.template);
1850
+ spinner.succeed(`Parsing of ${theme.text.bold(cmd.template)} setup configuration file completed successfully`);
1851
+ // final setup data
1852
+ const ceremonySetupData = setupCeremonyData;
1853
+ // create a new bucket
1854
+ const bucketName = await handleCeremonyBucketCreation(firebaseFunctions, ceremonySetupData.ceremonyPrefix);
1855
+ console.log(`\n${theme.symbols.success} Ceremony bucket name: ${theme.text.bold(bucketName)}`);
1856
+ // create S3 clienbt
1857
+ const s3 = new S3Client({ region: 'us-east-1' });
1858
+ // loop through each circuit
1859
+ for await (const circuit of setupCeremonyData.circuits) {
1860
+ // Local paths.
1861
+ const index = ceremonySetupData.circuits.indexOf(circuit);
1862
+ const r1csLocalPathAndFileName = `./${circuit.name}.r1cs`;
1863
+ const wasmLocalPathAndFileName = `./${circuit.name}.wasm`;
1864
+ const potLocalPathAndFileName = getPotLocalFilePath(circuit.files.potFilename);
1865
+ const zkeyLocalPathAndFileName = getZkeyLocalFilePath(circuit.files.initialZkeyFilename);
1866
+ // 2. download the pot and wasm files
1867
+ const streamPipeline = promisify(pipeline);
1868
+ await checkAndDownloadSmallestPowersOfTau(convertToDoubleDigits(circuit.metadata?.pot), circuit.files.potFilename);
1869
+ // download the wasm to calculate the hash
1870
+ const spinner = customSpinner(`Downloading the ${theme.text.bold(`#${circuit.name}`)} WASM file from the project's bucket...`, `clock`);
1871
+ spinner.start();
1872
+ const command = new GetObjectCommand({ Bucket: ceremonySetupData.circuitArtifacts[index].artifacts.bucket, Key: ceremonySetupData.circuitArtifacts[index].artifacts.wasmStoragePath });
1873
+ const response = await s3.send(command);
1874
+ if (response.$metadata.httpStatusCode !== 200) {
1875
+ throw new Error("There was an error while trying to download the wasm file. Please check that the file has the correct permissions (public) set.");
1876
+ }
1877
+ if (response.Body instanceof Readable)
1878
+ await streamPipeline(response.Body, createWriteStream(wasmLocalPathAndFileName));
1879
+ spinner.stop();
1880
+ // 3. generate the zKey
1881
+ await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1882
+ // 4. calculate the hashes
1883
+ const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName);
1884
+ const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename));
1885
+ const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName);
1886
+ // 5. upload the artifacts
1887
+ // Upload zKey to Storage.
1888
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.initialZkeyStoragePath, zkeyLocalPathAndFileName, circuit.files.initialZkeyFilename);
1889
+ // Check if PoT file has been already uploaded to storage.
1890
+ const alreadyUploadedPot = await checkIfObjectExist(firebaseFunctions, bucketName, circuit.files.potStoragePath);
1891
+ // If it wasn't uploaded yet, upload it.
1892
+ if (!alreadyUploadedPot) {
1893
+ // Upload PoT to Storage.
1894
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.potStoragePath, potLocalPathAndFileName, circuit.files.potFilename);
1895
+ }
1896
+ // Upload r1cs to Storage.
1897
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.r1csStoragePath, r1csLocalPathAndFileName, circuit.files.r1csFilename);
1898
+ // Upload wasm to Storage.
1899
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.wasmStoragePath, r1csLocalPathAndFileName, circuit.files.wasmFilename);
1900
+ // 6 update the setup data object
1901
+ ceremonySetupData.circuits[index].files = {
1902
+ ...circuit.files,
1903
+ potBlake2bHash: potBlake2bHash,
1904
+ wasmBlake2bHash: wasmBlake2bHash,
1905
+ initialZkeyBlake2bHash: initialZkeyBlake2bHash
1906
+ };
1907
+ ceremonySetupData.circuits[index].zKeySizeInBytes = getFileStats(zkeyLocalPathAndFileName).size;
1908
+ }
1909
+ // 7. setup the ceremony
1910
+ const ceremonyId = await setupCeremony(firebaseFunctions, ceremonySetupData.ceremonyInputData, ceremonySetupData.ceremonyPrefix, ceremonySetupData.circuits);
1911
+ console.log(`Congratulations, the setup of ceremony ${theme.text.bold(ceremonySetupData.ceremonyInputData.title)} (${`UID: ${theme.text.bold(ceremonyId)}`}) has been successfully completed ${theme.emojis.tada}. You will be able to find all the files and info respectively in the ceremony bucket and database document.`);
1912
+ terminate(providerUserId);
1913
+ }
1914
+ // Look for R1CS files.
1915
+ const r1csFilePaths = await filterDirectoryFilesByExtension(cwd, `.r1cs`);
1916
+ // Look for WASM files.
1917
+ const wasmFilePaths = await filterDirectoryFilesByExtension(cwd, `.wasm`);
1918
+ // Look for pre-computed zKeys references (if any).
1919
+ const localPreComputedZkeysFilenames = await filterDirectoryFilesByExtension(cwd, `.zkey`);
1920
+ if (!r1csFilePaths.length)
1921
+ showError(COMMAND_ERRORS.COMMAND_SETUP_NO_R1CS, true);
1922
+ if (!wasmFilePaths.length)
1923
+ showError(COMMAND_ERRORS.COMMAND_SETUP_NO_WASM, true);
1924
+ if (wasmFilePaths.length !== r1csFilePaths.length)
1925
+ showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true);
1926
+ // Prompt the coordinator for gather ceremony input data.
1927
+ const ceremonyInputData = await promptCeremonyInputData(firestoreDatabase);
1928
+ const ceremonyPrefix = extractPrefix(ceremonyInputData.title);
1929
+ // Add circuits to ceremony.
1930
+ const circuitsInputData = await handleAdditionOfCircuitsToCeremony(r1csFilePaths.map((dirent) => dirent.name), wasmFilePaths.map((dirent) => dirent.name), ceremonyInputData.timeoutMechanismType);
1931
+ // Move input data to circuits.
1932
+ circuitsInputData.forEach((data) => circuits.push(data));
1933
+ // Display ceremony summary.
1934
+ displayCeremonySummary(ceremonyInputData, circuits);
1935
+ // Prepare data.
1936
+ let wannaGenerateNewZkey = true; // New zKey generation flag.
1937
+ let wannaUsePreDownloadedPoT = false; // Local PoT file usage flag.
1938
+ let bucketName = ""; // The name of the bucket.
1939
+ // Ask for confirmation.
1940
+ const { confirmation } = await askForConfirmation("Do you want to continue with the ceremony setup?", "Yes", "No");
1941
+ if (confirmation) {
1942
+ await simpleLoader(`Looking for any pre-computed zkey file...`, `clock`, 1000);
1943
+ // Simulate pre-computed zkeys search.
1944
+ let leftPreComputedZkeys = localPreComputedZkeysFilenames;
1945
+ /** Circuit-based setup */
1946
+ for (let i = 0; i < circuits.length; i += 1) {
1947
+ const circuit = circuits[i];
1948
+ console.log(theme.text.bold(`\n- Setup for Circuit # ${theme.colors.magenta(`${circuit.sequencePosition}`)}\n`));
1949
+ // Convert to double digits powers (e.g., 9 -> 09).
1950
+ let doubleDigitsPowers = convertToDoubleDigits(circuit.metadata?.pot);
1951
+ let smallestPowersOfTauCompleteFilenameForCircuit = `${potFilenameTemplate}${doubleDigitsPowers}.ptau`;
1952
+ // Rename R1Cs and zKey based on circuit name and prefix.
1953
+ const r1csCompleteFilename = `${circuit.name}.r1cs`;
1954
+ const wasmCompleteFilename = `${circuit.name}.wasm`;
1955
+ const firstZkeyCompleteFilename = `${circuit.prefix}_${genesisZkeyIndex}.zkey`;
1956
+ let preComputedZkeyCompleteFilename = ``;
1957
+ // Local paths.
1958
+ const r1csLocalPathAndFileName = getCWDFilePath(cwd, r1csCompleteFilename);
1959
+ const wasmLocalPathAndFileName = getCWDFilePath(cwd, wasmCompleteFilename);
1960
+ let potLocalPathAndFileName = getPotLocalFilePath(smallestPowersOfTauCompleteFilenameForCircuit);
1961
+ let zkeyLocalPathAndFileName = getZkeyLocalFilePath(firstZkeyCompleteFilename);
1962
+ // Storage paths.
1963
+ const r1csStorageFilePath = getR1csStorageFilePath(circuit.prefix, r1csCompleteFilename);
1964
+ const wasmStorageFilePath = getWasmStorageFilePath(circuit.prefix, wasmCompleteFilename);
1965
+ let potStorageFilePath = getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit);
1966
+ const zkeyStorageFilePath = getZkeyStorageFilePath(circuit.prefix, firstZkeyCompleteFilename);
1967
+ if (leftPreComputedZkeys.length <= 0)
1968
+ console.log(`${theme.symbols.warning} No pre-computed zKey was found. Therefore, a new zKey from scratch will be generated.`);
1969
+ else {
1970
+ // Prompt if coordinator wanna use a pre-computed zKey for the circuit.
1971
+ const wannaUsePreComputedZkey = await promptPreComputedZkey();
1972
+ if (wannaUsePreComputedZkey) {
1973
+ // Prompt for pre-computed zKey selection.
1974
+ const preComputedZkeyOptions = leftPreComputedZkeys.map((dirent) => dirent.name);
1975
+ preComputedZkeyCompleteFilename = await promptPreComputedZkeySelector(preComputedZkeyOptions);
1976
+ // Switch to pre-computed zkey path.
1977
+ zkeyLocalPathAndFileName = getCWDFilePath(cwd, preComputedZkeyCompleteFilename);
1978
+ // Handle the selection for the PoT file to associate w/ the selected pre-computed zKey.
1979
+ const { doubleDigitsPowers: selectedDoubleDigitsPowers, potCompleteFilename: selectedPotCompleteFilename, usePreDownloadedPoT } = await handlePreComputedZkeyPowersOfTauSelection(circuit.metadata?.pot);
1980
+ // Update state.
1981
+ doubleDigitsPowers = selectedDoubleDigitsPowers;
1982
+ smallestPowersOfTauCompleteFilenameForCircuit = selectedPotCompleteFilename;
1983
+ wannaUsePreDownloadedPoT = usePreDownloadedPoT;
1984
+ // Update paths.
1985
+ potLocalPathAndFileName = getPotLocalFilePath(smallestPowersOfTauCompleteFilenameForCircuit);
1986
+ potStorageFilePath = getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit);
1987
+ // Check (and download) the smallest Powers of Tau for circuit.
1988
+ if (!wannaUsePreDownloadedPoT)
1989
+ await checkAndDownloadSmallestPowersOfTau(doubleDigitsPowers, smallestPowersOfTauCompleteFilenameForCircuit);
1990
+ // Update flag for zKey generation accordingly.
1991
+ wannaGenerateNewZkey = false;
1992
+ // Update paths.
1993
+ renameSync(getCWDFilePath(cwd, preComputedZkeyCompleteFilename), firstZkeyCompleteFilename); // the pre-computed zKey become the new first (genesis) zKey.
1994
+ zkeyLocalPathAndFileName = getCWDFilePath(cwd, firstZkeyCompleteFilename);
1995
+ // Remove the pre-computed zKey from the list of possible pre-computed options.
1996
+ leftPreComputedZkeys = leftPreComputedZkeys.filter((dirent) => dirent.name !== preComputedZkeyCompleteFilename);
1997
+ }
1998
+ }
1999
+ // Check (and download) the smallest Powers of Tau for circuit.
2000
+ if (!wannaUsePreDownloadedPoT)
2001
+ await checkAndDownloadSmallestPowersOfTau(doubleDigitsPowers, smallestPowersOfTauCompleteFilenameForCircuit);
2002
+ if (wannaGenerateNewZkey)
2003
+ await handleNewZkeyGeneration(r1csLocalPathAndFileName, potLocalPathAndFileName, zkeyLocalPathAndFileName);
2004
+ // Create a bucket for ceremony if it has not yet been created.
2005
+ if (!bucketName)
2006
+ bucketName = await handleCeremonyBucketCreation(firebaseFunctions, ceremonyPrefix);
2007
+ // Upload zKey to Storage.
2008
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, zkeyStorageFilePath, zkeyLocalPathAndFileName, firstZkeyCompleteFilename);
2009
+ // Check if PoT file has been already uploaded to storage.
2010
+ const alreadyUploadedPot = await checkIfObjectExist(firebaseFunctions, bucketName, getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit));
2011
+ if (!alreadyUploadedPot) {
2012
+ // Upload PoT to Storage.
2013
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, potStorageFilePath, potLocalPathAndFileName, smallestPowersOfTauCompleteFilenameForCircuit);
2014
+ }
2015
+ else
2016
+ console.log(`${theme.symbols.success} The Powers of Tau (${theme.text.bold(smallestPowersOfTauCompleteFilenameForCircuit)}) file is already saved in the storage`);
2017
+ // Upload R1CS to Storage.
2018
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, r1csStorageFilePath, r1csLocalPathAndFileName, r1csCompleteFilename);
2019
+ // Upload WASM to Storage.
2020
+ await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, wasmStorageFilePath, wasmLocalPathAndFileName, wasmCompleteFilename);
2021
+ process.stdout.write(`\n`);
2022
+ const spinner = customSpinner(`Preparing the ceremony data (this may take a while)...`, `clock`);
2023
+ spinner.start();
2024
+ // Computing file hash (this may take a while).
2025
+ const r1csBlake2bHash = await blake512FromPath(r1csLocalPathAndFileName);
2026
+ const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName);
2027
+ const potBlake2bHash = await blake512FromPath(potLocalPathAndFileName);
2028
+ const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName);
2029
+ spinner.stop();
2030
+ // Prepare circuit data for writing to the DB.
2031
+ const circuitFiles = {
2032
+ r1csFilename: r1csCompleteFilename,
2033
+ wasmFilename: wasmCompleteFilename,
2034
+ potFilename: smallestPowersOfTauCompleteFilenameForCircuit,
2035
+ initialZkeyFilename: firstZkeyCompleteFilename,
2036
+ r1csStoragePath: r1csStorageFilePath,
2037
+ wasmStoragePath: wasmStorageFilePath,
2038
+ potStoragePath: potStorageFilePath,
2039
+ initialZkeyStoragePath: zkeyStorageFilePath,
2040
+ r1csBlake2bHash,
2041
+ wasmBlake2bHash,
2042
+ potBlake2bHash,
2043
+ initialZkeyBlake2bHash
2044
+ };
2045
+ // nb. these will be populated after the first contribution.
2046
+ const circuitTimings = {
2047
+ contributionComputation: 0,
2048
+ fullContribution: 0,
2049
+ verifyCloudFunction: 0
2050
+ };
2051
+ circuits[i] = {
2052
+ ...circuit,
2053
+ files: circuitFiles,
2054
+ avgTimings: circuitTimings,
2055
+ zKeySizeInBytes: getFileStats(zkeyLocalPathAndFileName).size
2056
+ };
2057
+ // Reset flags.
2058
+ wannaGenerateNewZkey = true;
2059
+ wannaUsePreDownloadedPoT = false;
2060
+ }
2061
+ const spinner = customSpinner(`Writing ceremony data...`, `clock`);
2062
+ spinner.start();
2063
+ try {
2064
+ // Call the Cloud Function for writing ceremony data on Firestore DB.
2065
+ ceremonyId = await setupCeremony(firebaseFunctions, ceremonyInputData, ceremonyPrefix, circuits);
2066
+ }
2067
+ catch (error) {
2068
+ const errorBody = JSON.parse(JSON.stringify(error));
2069
+ showError(`[${errorBody.code}] ${error.message} ${!errorBody.details ? "" : `\n${errorBody.details}`}`, true);
2070
+ }
2071
+ await sleep(5000); // Cloud function unexpected termination workaround.
2072
+ spinner.succeed(`Congratulations, the setup of ceremony ${theme.text.bold(ceremonyInputData.title)} (${`UID: ${theme.text.bold(ceremonyId)}`}) has been successfully completed ${theme.emojis.tada}. You will be able to find all the files and info respectively in the ceremony bucket and database document.`);
2073
+ }
2074
+ terminate(providerUserId);
2075
+ };
2076
+
2077
+ const packagePath$1 = `${dirname(fileURLToPath(import.meta.url))}`;
2078
+ dotenv.config({
2079
+ path: packagePath$1.includes(`src/lib`)
2080
+ ? `${dirname(fileURLToPath(import.meta.url))}/../../.env`
2081
+ : `${dirname(fileURLToPath(import.meta.url))}/.env`
2082
+ });
2083
+ /**
2084
+ * Custom countdown which throws an error when expires.
2085
+ * @param expirationInSeconds <number> - the expiration time in seconds.
2086
+ */
2087
+ const expirationCountdownForGithubOAuth = (expirationInSeconds) => {
2088
+ // Prepare data.
2089
+ let secondsCounter = expirationInSeconds <= 60 ? expirationInSeconds : 60;
2090
+ const interval = 1; // 1s.
2091
+ setInterval(() => {
2092
+ if (expirationInSeconds !== 0) {
2093
+ // Update time and seconds counter.
2094
+ expirationInSeconds -= interval;
2095
+ secondsCounter -= interval;
2096
+ if (secondsCounter % 60 === 0)
2097
+ secondsCounter = 0;
2098
+ // Notify user.
2099
+ process.stdout.write(`${theme.symbols.warning} Expires in ${theme.text.bold(theme.colors.magenta(`00:${Math.floor(expirationInSeconds / 60)}:${secondsCounter}`))}\r`);
2100
+ }
2101
+ else {
2102
+ process.stdout.write(`\n\n`); // workaround to \r.
2103
+ showError(GENERIC_ERRORS.GENERIC_COUNTDOWN_EXPIRATION, true);
2104
+ }
2105
+ }, interval * 1000); // ms.
2106
+ };
2107
+ /**
2108
+ * Callback to manage the data requested for Github OAuth2.0 device flow.
2109
+ * @param verification <Verification> - the data from Github OAuth2.0 device flow.
2110
+ */
2111
+ const onVerification = async (verification) => {
2112
+ // Copy code to clipboard.
2113
+ clipboard.writeSync(verification.user_code);
2114
+ clipboard.readSync();
2115
+ // Display data.
2116
+ console.log(`${theme.symbols.warning} Visit ${theme.text.bold(theme.text.underlined(verification.verification_uri))} on this device to generate a new token and authenticate`);
2117
+ console.log(`${theme.symbols.info} Your auth code: ${theme.text.bold(verification.user_code)} (${theme.emojis.clipboard} ${theme.symbols.success})\n`);
2118
+ const spinner = customSpinner(`Redirecting to Github...`, `clock`);
2119
+ spinner.start();
2120
+ await sleep(10000); // ~10s to make users able to read the CLI.
2121
+ // Automatically open the page (# Step 2).
2122
+ await open(verification.verification_uri);
2123
+ spinner.stop();
2124
+ // Countdown for time expiration.
2125
+ expirationCountdownForGithubOAuth(verification.expires_in);
2126
+ };
2127
+ /**
2128
+ * Return the Github OAuth 2.0 token using manual Device Flow authentication process.
2129
+ * @param clientId <string> - the client id for the CLI OAuth app.
2130
+ * @returns <string> the Github OAuth 2.0 token.
2131
+ */
2132
+ const executeGithubDeviceFlow = async (clientId) => {
2133
+ /**
2134
+ * Github OAuth 2.0 Device Flow.
2135
+ * # Step 1: Request device and user verification codes and gets auth verification uri.
2136
+ * # Step 2: The app prompts the user to enter a user verification code at https://github.com/login/device.
2137
+ * # Step 3: The app polls/asks for the user authentication status.
2138
+ */
2139
+ const clientType = "oauth-app";
2140
+ const tokenType = "oauth";
2141
+ // # Step 1.
2142
+ const auth = createOAuthDeviceAuth({
2143
+ clientType,
2144
+ clientId,
2145
+ scopes: ["gist"],
2146
+ onVerification
2147
+ });
2148
+ // # Step 3.
2149
+ const { token } = await auth({
2150
+ type: tokenType
2151
+ });
2152
+ return token;
2153
+ };
2154
+ /**
2155
+ * Auth command.
2156
+ * @notice The auth command allows a user to make the association of their Github account with the CLI by leveraging OAuth 2.0 as an authentication mechanism.
2157
+ * @dev Under the hood, the command handles a manual Device Flow following the guidelines in the Github documentation.
2158
+ */
2159
+ const auth = async () => {
2160
+ const { firebaseApp } = await bootstrapCommandExecutionAndServices();
2161
+ // Console more context for the user.
2162
+ console.log(`${theme.symbols.info} ${theme.text.bold(`You are about to authenticate on this CLI using your Github account (device flow - OAuth 2.0 mechanism).\n${theme.symbols.warning} Please, note that only read and write permission for ${theme.text.italic(`gists`)} will be required in order to publish your contribution transcript!`)}\n`);
2163
+ const spinner = customSpinner(`Checking authentication token...`, `clock`);
2164
+ spinner.start();
2165
+ await sleep(5000);
2166
+ // Manage OAuth Github token.
2167
+ const isLocalTokenStored = checkLocalAccessToken();
2168
+ if (!isLocalTokenStored) {
2169
+ spinner.fail(`No local authentication token found\n`);
2170
+ // Generate a new access token using Github Device Flow (OAuth 2.0).
2171
+ const newToken = await executeGithubDeviceFlow(String(process.env.AUTH_GITHUB_CLIENT_ID));
2172
+ // Store the new access token.
2173
+ setLocalAccessToken(newToken);
2174
+ }
2175
+ else
2176
+ spinner.succeed(`Local authentication token found\n`);
2177
+ // Get access token from local store.
2178
+ const token = getLocalAccessToken();
2179
+ // Exchange token for credential.
2180
+ const credentials = exchangeGithubTokenForCredentials(String(token));
2181
+ spinner.text = `Authenticating...`;
2182
+ spinner.start();
2183
+ // Sign-in to Firebase using credentials.
2184
+ await signInToFirebase(firebaseApp, credentials);
2185
+ // Get Github handle.
2186
+ const providerUserId = await getGithubProviderUserId(String(token));
2187
+ spinner.succeed(`You are authenticated as ${theme.text.bold(`@${getUserHandleFromProviderUserId(providerUserId)}`)} and now able to interact with zk-SNARK Phase2 Trusted Setup ceremonies`);
2188
+ // Console more context for the user.
2189
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2190
+ terminate(providerUserId);
2191
+ };
2192
+
2193
+ /**
2194
+ * Return the verification result for latest contribution.
2195
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2196
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
2197
+ * @param circuitId <string> - the unique identifier of the circuit.
2198
+ * @param participantId <string> - the unique identifier of the contributor.
2199
+ */
2200
+ const getLatestVerificationResult = async (firestoreDatabase, ceremonyId, circuitId, participantId) => {
2201
+ // Clean cursor.
2202
+ process.stdout.clearLine(0);
2203
+ process.stdout.cursorTo(0);
2204
+ const spinner = customSpinner(`Getting info about the verification of your contribution...`, `clock`);
2205
+ spinner.start();
2206
+ // Get circuit contribution from contributor.
2207
+ const circuitContributionsFromContributor = await getCircuitContributionsFromContributor(firestoreDatabase, ceremonyId, circuitId, participantId);
2208
+ const contribution = circuitContributionsFromContributor.at(0);
2209
+ spinner.stop();
2210
+ console.log(`${contribution?.data.valid ? theme.symbols.success : theme.symbols.error} Your contribution is ${contribution?.data.valid ? `correct` : `wrong`}`);
2211
+ };
2212
+ /**
2213
+ * Generate a ready-to-share tweet on public attestation.
2214
+ * @param ceremonyTitle <string> - the title of the ceremony.
2215
+ * @param gistUrl <string> - the Github public attestation gist url.
2216
+ */
2217
+ const handleTweetGeneration = async (ceremonyTitle, gistUrl) => {
2218
+ // Generate a ready to share custom url to tweet about ceremony participation.
2219
+ const tweetUrl = generateCustomUrlToTweetAboutParticipation(ceremonyTitle, gistUrl, false);
2220
+ console.log(`${theme.symbols.info} We encourage you to tweet to spread the word about your participation to the ceremony by clicking the link below\n\n${theme.text.underlined(tweetUrl)}`);
2221
+ // Automatically open a webpage with the tweet.
2222
+ await open(tweetUrl);
2223
+ };
2224
+ /**
2225
+ * Display if a set of contributions computed for a circuit is valid/invalid.
2226
+ * @param contributionsWithValidity <Array<ContributionValidity>> - list of contributor contributions together with contribution validity.
2227
+ */
2228
+ const displayContributionValidity = (contributionsWithValidity) => {
2229
+ // Circuit index position.
2230
+ let circuitSequencePosition = 1; // nb. incremental value is enough because the contributions are already sorted x circuit sequence position.
2231
+ for (const contributionWithValidity of contributionsWithValidity) {
2232
+ // Display.
2233
+ console.log(`${contributionWithValidity.valid ? theme.symbols.success : theme.symbols.error} ${theme.text.bold(`Circuit`)} ${theme.text.bold(theme.colors.magenta(circuitSequencePosition))}`);
2234
+ // Increment circuit position.
2235
+ circuitSequencePosition += 1;
2236
+ }
2237
+ };
2238
+ /**
2239
+ * Display and manage data necessary when participant has already made the contribution for all circuits of a ceremony.
2240
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2241
+ * @param circuits <Array<FirebaseDocumentInfo>> - the array of ceremony circuits documents.
2242
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
2243
+ * @param participantId <string> - the unique identifier of the contributor.
2244
+ */
2245
+ const handleContributionValidity = async (firestoreDatabase, circuits, ceremonyId, participantId) => {
2246
+ // Get contributors' contributions validity.
2247
+ const contributionsWithValidity = await getContributionsValidityForContributor(firestoreDatabase, circuits, ceremonyId, participantId, false);
2248
+ // Filter only valid contributions.
2249
+ const validContributions = contributionsWithValidity.filter((contributionWithValidity) => contributionWithValidity.valid);
2250
+ if (!validContributions.length)
2251
+ console.log(`\n${theme.symbols.error} You have provided ${theme.text.bold(theme.colors.magenta(circuits.length))} out of ${theme.text.bold(theme.colors.magenta(circuits.length))} invalid contributions ${theme.emojis.upsideDown}`);
2252
+ else {
2253
+ console.log(`\nYou have provided ${theme.colors.magenta(theme.text.bold(validContributions.length))} out of ${theme.colors.magenta(theme.text.bold(circuits.length))} valid contributions ${theme.emojis.tada}`);
2254
+ // Display (in)valid contributions per circuit.
2255
+ displayContributionValidity(contributionsWithValidity);
2256
+ }
2257
+ };
2258
+ /**
2259
+ * Display and manage data necessary when participant would like to contribute but there is still an on-going timeout.
2260
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2261
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
2262
+ * @param participantId <string> - the unique identifier of the contributor.
2263
+ * @param participantContributionProgress <number> - the progress in the contribution of the various circuits of the ceremony.
2264
+ * @param wasContributing <boolean> - flag to discriminate between participant currently contributing (true) or not (false).
2265
+ */
2266
+ const handleTimedoutMessageForContributor = async (firestoreDatabase, participantId, ceremonyId, participantContributionProgress, wasContributing) => {
2267
+ // Check if the participant was contributing when timeout happened.
2268
+ if (!wasContributing)
2269
+ console.log(theme.text.bold(`\n- Circuit # ${theme.colors.magenta(participantContributionProgress)}`));
2270
+ // Display timeout message.
2271
+ console.log(`\n${theme.symbols.error} ${wasContributing
2272
+ ? `Your contribution took longer than the estimated time and you were removed as current contributor. You should wait for a timeout to expire before you can rejoin for contribution.`
2273
+ : `The waiting time (timeout) to retry the contribution has not yet expired.`}\n\n${theme.symbols.warning} Note that the timeout could be triggered due to network latency, disk availability issues, un/intentional crashes, limited hardware capabilities.`);
2274
+ // nb. workaround to attend timeout to be written on the database.
2275
+ /// @todo use listeners instead (when possible).
2276
+ await simpleLoader(`Getting timeout expiration...`, `clock`, 5000);
2277
+ // Retrieve latest updated active timeouts for contributor.
2278
+ const activeTimeouts = await getCurrentActiveParticipantTimeout(firestoreDatabase, ceremonyId, participantId);
2279
+ if (activeTimeouts.length !== 1)
2280
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_UNIQUE_ACTIVE_TIMEOUTS, true);
2281
+ // Get active timeout.
2282
+ const activeTimeout = activeTimeouts.at(0);
2283
+ if (!activeTimeout.data)
2284
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_ACTIVE_TIMEOUT_DATA, true);
2285
+ // Extract data.
2286
+ const { endDate } = activeTimeout.data;
2287
+ const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(Number(endDate) - Timestamp.now().toMillis());
2288
+ console.log(`${theme.symbols.info} Your timeout will end in ${theme.text.bold(`${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}`)} (dd/hh/mm/ss)`);
2289
+ };
2290
+ /**
2291
+ * Check if the participant has enough disk space available before joining the waiting queue
2292
+ * for the computing the next circuit contribution.
2293
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
2294
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
2295
+ * @param circuitSequencePosition <number> - the position of the circuit in the sequence for contribution.
2296
+ * @param circuitZkeySizeInBytes <number> - the size in bytes of the circuit zKey.
2297
+ * @param isResumingAfterTimeout <boolean> - flag to discriminate between resuming after a timeout expiration (true) or progressing to next contribution (false).
2298
+ * @param providerUserId <string> - the external third-party provider user identifier.
2299
+ * @return <Promise<boolean>> - true when the contributor would like to generate the attestation and do not provide any further contribution to the ceremony; otherwise false.
2300
+ */
2301
+ const handleDiskSpaceRequirementForNextContribution = async (cloudFunctions, ceremonyId, circuitSequencePosition, circuitZkeySizeInBytes, isResumingAfterTimeout, providerUserId) => {
2302
+ let wannaContributeOrHaveEnoughMemory = false; // true when the contributor has enough memory or wants to contribute in any case; otherwise false.
2303
+ // Custom spinner.
2304
+ const spinner = customSpinner(`Checking disk space requirement for next contribution...`, `clock`);
2305
+ spinner.start();
2306
+ // Compute disk space requirement to support circuit contribution (zKey size * 2).
2307
+ const contributionDiskSpaceRequirement = convertBytesOrKbToGb(circuitZkeySizeInBytes * 2, true);
2308
+ // Get participant available disk space.
2309
+ const participantFreeDiskSpace = convertBytesOrKbToGb(estimateParticipantFreeGlobalDiskSpace(), false);
2310
+ // Check.
2311
+ if (participantFreeDiskSpace < contributionDiskSpaceRequirement) {
2312
+ spinner.fail(`You may not have enough memory to calculate the contribution for the Circuit ${theme.colors.magenta(`${circuitSequencePosition}`)}.\n\n${theme.symbols.info} The required amount of disk space is ${contributionDiskSpaceRequirement < 0.01
2313
+ ? theme.text.bold(`< 0.01`)
2314
+ : theme.text.bold(contributionDiskSpaceRequirement)} GB but you only have ${participantFreeDiskSpace > 0 ? theme.text.bold(participantFreeDiskSpace.toFixed(2)) : theme.text.bold(0)} GB available memory \nThe estimate ${theme.text.bold("may not be 100% correct")} since is based on the aggregate free memory on your disks but some may not be detected!\n`);
2315
+ const { confirmation } = await askForConfirmation(`Please, we kindly ask you to continue with the contribution if you have noticed the estimate is wrong and you have enough memory in your machine`, "Continue", "Exit");
2316
+ wannaContributeOrHaveEnoughMemory = !!confirmation;
2317
+ if (circuitSequencePosition > 1) {
2318
+ console.log(`${theme.symbols.info} Please note, you have time until ceremony ends to free up your memory and complete remaining contributions`);
2319
+ // Asks the contributor if their wants to terminate contributions for the ceremony.
2320
+ const { confirmation } = await askForConfirmation(`Please note, this action is irreversible! Do you want to end your contributions for the ceremony?`);
2321
+ return !!confirmation;
2322
+ }
2323
+ }
2324
+ else
2325
+ wannaContributeOrHaveEnoughMemory = true;
2326
+ if (wannaContributeOrHaveEnoughMemory) {
2327
+ spinner.succeed(`Memory requirement to contribute to ${theme.text.bold(`Circuit ${theme.colors.magenta(`${circuitSequencePosition}`)}`)} satisfied`);
2328
+ // Memory requirement for next contribution met.
2329
+ if (!isResumingAfterTimeout) {
2330
+ spinner.text = "Progressing to next circuit for contribution...";
2331
+ spinner.start();
2332
+ // Progress the participant to the next circuit making it ready for contribution.
2333
+ await progressToNextCircuitForContribution(cloudFunctions, ceremonyId);
2334
+ }
2335
+ else {
2336
+ spinner.text = "Resuming your contribution after timeout expiration...";
2337
+ spinner.start();
2338
+ // Resume contribution after timeout expiration (same circuit).
2339
+ await resumeContributionAfterTimeoutExpiration(cloudFunctions, ceremonyId);
2340
+ }
2341
+ spinner.info(`Joining the ${theme.text.bold(`Circuit ${theme.colors.magenta(`${circuitSequencePosition}`)}`)} waiting queue for contribution (this may take a while)`);
2342
+ return false;
2343
+ }
2344
+ terminate(providerUserId);
2345
+ return false;
2346
+ };
2347
+ /**
2348
+ * Generate the public attestation for the contributor.
2349
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2350
+ * @param circuits <Array<FirebaseDocumentInfo>> - the array of ceremony circuits documents.
2351
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
2352
+ * @param participantId <string> - the unique identifier of the contributor.
2353
+ * @param participantContributions <Array<Co> - the document data of the participant.
2354
+ * @param contributorIdentifier <string> - the identifier of the contributor (handle, name, uid).
2355
+ * @param ceremonyName <string> - the name of the ceremony.
2356
+ * @returns <Promise<string>> - the public attestation.
2357
+ */
2358
+ const generatePublicAttestation = async (firestoreDatabase, circuits, ceremonyId, participantId, participantContributions, contributorIdentifier, ceremonyName) => {
2359
+ // Display contribution validity.
2360
+ await handleContributionValidity(firestoreDatabase, circuits, ceremonyId, participantId);
2361
+ await sleep(3000);
2362
+ // Get only valid contribution hashes.
2363
+ return generateValidContributionsAttestation(firestoreDatabase, circuits, ceremonyId, participantId, participantContributions, contributorIdentifier, ceremonyName, false);
2364
+ };
2365
+ /**
2366
+ * Generate a public attestation for a contributor, publish the attestation as gist, and prepare a new ready-to-share tweet about ceremony participation.
2367
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2368
+ * @param circuits <Array<FirebaseDocumentInfo>> - the array of ceremony circuits documents.
2369
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
2370
+ * @param participantId <string> - the unique identifier of the contributor.
2371
+ * @param participantContributions <Array<Co> - the document data of the participant.
2372
+ * @param contributorIdentifier <string> - the identifier of the contributor (handle, name, uid).
2373
+ * @param ceremonyName <string> - the name of the ceremony.
2374
+ * @param ceremonyPrefix <string> - the prefix of the ceremony.
2375
+ * @param participantAccessToken <string> - the access token of the participant.
2376
+ */
2377
+ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId, participantId, participantContributions, contributorIdentifier, ceremonyName, ceremonyPrefix, participantAccessToken) => {
2378
+ await simpleLoader(`Generating your public attestation...`, `clock`, 3000);
2379
+ // Generate attestation with valid contributions.
2380
+ const publicAttestation = await generatePublicAttestation(firestoreDatabase, circuits, ceremonyId, participantId, participantContributions, contributorIdentifier, ceremonyName);
2381
+ // Write public attestation locally.
2382
+ writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2383
+ await sleep(1000); // workaround for file descriptor unexpected close.
2384
+ const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2385
+ console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2386
+ // Prepare a ready-to-share tweet.
2387
+ await handleTweetGeneration(ceremonyName, gistUrl);
2388
+ };
2389
+ /**
2390
+ * Listen to circuit document changes.
2391
+ * @notice the circuit is the one for which the participant wants to contribute.
2392
+ * @dev display custom messages in order to make the participant able to follow what's going while waiting in the queue.
2393
+ * Also, this listener use another listener for the current circuit contributor in order to inform the waiting participant about the current contributor's progress.
2394
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2395
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
2396
+ * @param participantId <string> - the unique identifier of the participant.
2397
+ * @param circuit <FirebaseDocumentInfo> - the Firestore document info about the circuit.
2398
+ */
2399
+ const listenToCeremonyCircuitDocumentChanges = (firestoreDatabase, ceremonyId, participantId, circuit) => {
2400
+ console.log(`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${circuit.data.sequencePosition}`)}`)} (Waiting Queue)`);
2401
+ let cachedLatestPosition = 0;
2402
+ const unsubscribeToCeremonyCircuitListener = onSnapshot(circuit.ref, async (changedCircuit) => {
2403
+ // Check data.
2404
+ if (!circuit.data || !changedCircuit.data())
2405
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_CIRCUIT_DATA, true);
2406
+ // Extract data.
2407
+ const { avgTimings, waitingQueue } = changedCircuit.data();
2408
+ const { fullContribution, verifyCloudFunction } = avgTimings;
2409
+ const { currentContributor } = waitingQueue;
2410
+ // Get circuit current contributor participant document.
2411
+ const circuitCurrentContributor = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(ceremonyId), currentContributor);
2412
+ // Check data.
2413
+ if (!circuitCurrentContributor.data())
2414
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_CURRENT_CONTRIBUTOR_DATA, true);
2415
+ // Get participant position in the waiting queue of the circuit.
2416
+ const latestParticipantPositionInQueue = waitingQueue.contributors.indexOf(participantId) + 1;
2417
+ // Compute time estimation based on latest participant position in the waiting queue.
2418
+ const newEstimatedWaitingTime = fullContribution <= 0 && verifyCloudFunction <= 0
2419
+ ? 0
2420
+ : (fullContribution + verifyCloudFunction) * (latestParticipantPositionInQueue - 1);
2421
+ // Extract time.
2422
+ const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(newEstimatedWaitingTime);
2423
+ // Check if the participant is now the new current contributor for the circuit.
2424
+ if (latestParticipantPositionInQueue === 1) {
2425
+ console.log(`\n${theme.symbols.info} Your contribution will begin shortly ${theme.emojis.tada}`);
2426
+ // Unsubscribe from updates.
2427
+ unsubscribeToCeremonyCircuitListener();
2428
+ // eslint-disable no-unused-vars
2429
+ }
2430
+ else if (latestParticipantPositionInQueue !== cachedLatestPosition) {
2431
+ // Display updated position and waiting time.
2432
+ console.log(`${theme.symbols.info} ${`You will have to wait for ${theme.text.bold(theme.colors.magenta(latestParticipantPositionInQueue - 1))} contributors`} (~${newEstimatedWaitingTime > 0
2433
+ ? `${theme.text.bold(`${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}`)}`
2434
+ : `no time`} (dd/hh/mm/ss))`);
2435
+ cachedLatestPosition = latestParticipantPositionInQueue;
2436
+ }
2437
+ });
2438
+ };
2439
+ /**
2440
+ * Listen to current authenticated participant document changes.
2441
+ * @dev this is the core business logic related to the execution of the contribute command.
2442
+ * Basically, the command follows the updates of circuit waiting queue, participant status and contribution steps,
2443
+ * while covering aspects regarding memory requirements, contribution completion or resumability, interaction w/ cloud functions, and so on.
2444
+ * @notice in order to compute a contribute for each circuit, this method follows several steps:
2445
+ * 1) Checking participant memory availability on root disk before joining for the first contribution (circuit having circuitPosition = 1).
2446
+ * 2) Check if the participant has not completed the contributions for every circuit or has just finished contributing.
2447
+ * 3) If (2) is true:
2448
+ * 3.A) Check if the participant switched to `WAITING` as contribution status.
2449
+ * 3.A.1) if true; display circuit waiting queue updates to the participant (listener to circuit document changes).
2450
+ * 3.A.2) otherwise; do nothing and continue with other checks.
2451
+ * 3.B) Check if the participant switched to `CONTRIBUTING` status. The participant must be the current contributor for the circuit w/ a resumable contribution step.
2452
+ * 3.B.1) if true; start or resume the contribution from last contribution step.
2453
+ * 3.B.2) otherwise; do nothing and continue with other checks.
2454
+ * 3.C) Check if the current contributor is resuming from the "VERIFYING" contribution step.
2455
+ * 3.C.1) if true; display previous completed steps and wait for verification results.
2456
+ * 3.C.2) otherwise; do nothing and continue with other checks.
2457
+ * 3.D) Check if the 'verifycontribution' cloud function has successfully completed the execution.
2458
+ * 3.D.1) if true; get and display contribution verification results.
2459
+ * 3.D.2) otherwise; do nothing and continue with other checks.
2460
+ * 3.E) Check if the participant experiences a timeout while contributing.
2461
+ * 3.E.1) if true; display timeout message and gracefully terminate.
2462
+ * 3.E.2) otherwise; do nothing and continue with other checks.
2463
+ * 3.F) Check if the participant has completed the contribution or is trying to resume the contribution after timeout expiration.
2464
+ * 3.F.1) if true; check the memory requirement for next/current (completed/resuming) contribution while
2465
+ * handling early interruption of contributions resulting in a final public attestation generation.
2466
+ * (this allows a user to stop their contributions to a certain circuit X if their cannot provide/do not own
2467
+ * an adequate amount of memory for satisfying the memory requirements of the next/current contribution).
2468
+ * 3.F.2) otherwise; do nothing and continue with other checks.
2469
+ * 3.G) Check if the participant has already contributed to every circuit when running the command.
2470
+ * 3.G.1) if true; generate public final attestation and gracefully exit.
2471
+ * 3.G.2) otherwise; do nothing
2472
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2473
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
2474
+ * @param participant <DocumentSnapshot<DocumentData>> - the Firestore document of the participant.
2475
+ * @param ceremony <FirebaseDocumentInfo> - the Firestore document info about the selected ceremony.
2476
+ * @param entropy <string> - the random value (aka toxic waste) entered by the participant for the contribution.
2477
+ * @param providerUserId <string> - the unique provider user identifier associated to the authenticated account.
2478
+ * @param accessToken <string> - the Github token generated through the Device Flow process.
2479
+ */
2480
+ const listenToParticipantDocumentChanges = async (firestoreDatabase, cloudFunctions, participant, ceremony, entropy, providerUserId, accessToken) => {
2481
+ // Listen to participant document changes.
2482
+ // nb. this listener encapsulates the core business logic of the contribute command.
2483
+ // the `changedParticipant` is the updated version (w/ newest changes) of the participant's document.
2484
+ const unsubscribe = onSnapshot(participant.ref, async (changedParticipant) => {
2485
+ // Check data.
2486
+ if (!participant.data() || !changedParticipant.data())
2487
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true);
2488
+ // Extract data.
2489
+ const { contributionProgress: prevContributionProgress, status: prevStatus, contributions: prevContributions, contributionStep: prevContributionStep, tempContributionData: prevTempContributionData } = participant.data();
2490
+ const { contributionProgress: changedContributionProgress, status: changedStatus, contributionStep: changedContributionStep, contributions: changedContributions, tempContributionData: changedTempContributionData, verificationStartedAt: changedVerificationStartedAt } = changedParticipant.data();
2491
+ // Get latest updates from ceremony circuits.
2492
+ const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id);
2493
+ // Step (1).
2494
+ // Handle disk space requirement check for first contribution.
2495
+ if (changedStatus === "WAITING" /* ParticipantStatus.WAITING */ &&
2496
+ !changedContributionStep &&
2497
+ !changedContributions.length &&
2498
+ !changedContributionProgress) {
2499
+ // Get circuit by sequence position among ceremony circuits.
2500
+ const circuit = getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2501
+ // Extract data.
2502
+ const { sequencePosition, zKeySizeInBytes } = circuit.data;
2503
+ // Check participant disk space availability for next contribution.
2504
+ await handleDiskSpaceRequirementForNextContribution(cloudFunctions, ceremony.id, sequencePosition, zKeySizeInBytes, false, providerUserId);
2505
+ }
2506
+ // Step (2).
2507
+ if (changedContributionProgress > 0 && changedContributionProgress <= circuits.length) {
2508
+ // Step (3).
2509
+ // Get circuit for which the participant wants to contribute.
2510
+ const circuit = circuits[changedContributionProgress - 1];
2511
+ // Check data.
2512
+ if (!circuit.data)
2513
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_CIRCUIT_DATA, true);
2514
+ // Extract circuit data.
2515
+ const { waitingQueue } = circuit.data;
2516
+ // Define pre-conditions for different scenarios.
2517
+ const isWaitingForContribution = changedStatus === "WAITING" /* ParticipantStatus.WAITING */;
2518
+ const isCurrentContributor = changedStatus === "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */ && waitingQueue.currentContributor === participant.id;
2519
+ const isResumingContribution = changedContributionStep === prevContributionStep &&
2520
+ changedContributionProgress === prevContributionProgress;
2521
+ const noStatusChanges = changedStatus === prevStatus;
2522
+ const progressToNextContribution = changedContributionStep === "COMPLETED" /* ParticipantContributionStep.COMPLETED */;
2523
+ const completedContribution = progressToNextContribution && changedStatus === "CONTRIBUTED" /* ParticipantStatus.CONTRIBUTED */;
2524
+ const timeoutTriggeredWhileContributing = changedStatus === "TIMEDOUT" /* ParticipantStatus.TIMEDOUT */ &&
2525
+ changedContributionStep !== "COMPLETED" /* ParticipantContributionStep.COMPLETED */;
2526
+ const timeoutExpired = changedStatus === "EXHUMED" /* ParticipantStatus.EXHUMED */;
2527
+ const alreadyContributedToEveryCeremonyCircuit = changedStatus === "DONE" /* ParticipantStatus.DONE */ &&
2528
+ changedContributionStep === "COMPLETED" /* ParticipantContributionStep.COMPLETED */ &&
2529
+ changedContributionProgress === circuits.length &&
2530
+ changedContributions.length === circuits.length;
2531
+ const noTemporaryContributionData = !prevTempContributionData && !changedTempContributionData;
2532
+ const samePermanentContributionData = (!prevContributions && !changedContributions) ||
2533
+ prevContributions.length === changedContributions.length;
2534
+ const downloadingStep = changedContributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */;
2535
+ const computingStep = changedContributionStep === "COMPUTING" /* ParticipantContributionStep.COMPUTING */;
2536
+ const uploadingStep = changedContributionStep === "UPLOADING" /* ParticipantContributionStep.UPLOADING */;
2537
+ const hasResumableStep = downloadingStep || computingStep || uploadingStep;
2538
+ const resumingContribution = prevContributionStep === changedContributionStep &&
2539
+ prevStatus === changedStatus &&
2540
+ prevContributionProgress === changedContributionProgress;
2541
+ const resumingContributionButAdvancedToAnotherStep = prevContributionStep !== changedContributionStep;
2542
+ const resumingAfterTimeoutExpiration = prevStatus === "EXHUMED" /* ParticipantStatus.EXHUMED */;
2543
+ const neverResumedContribution = !prevContributionStep;
2544
+ const resumingWithSameTemporaryData = !!prevTempContributionData &&
2545
+ !!changedTempContributionData &&
2546
+ JSON.stringify(Object.keys(prevTempContributionData).sort()) ===
2547
+ JSON.stringify(Object.keys(changedTempContributionData).sort()) &&
2548
+ JSON.stringify(Object.values(prevTempContributionData).sort()) ===
2549
+ JSON.stringify(Object.values(changedTempContributionData).sort());
2550
+ const startingOrResumingContribution =
2551
+ // Pre-condition W => contribute / resume when contribution step = DOWNLOADING.
2552
+ (isCurrentContributor &&
2553
+ downloadingStep &&
2554
+ (resumingContribution ||
2555
+ resumingContributionButAdvancedToAnotherStep ||
2556
+ resumingAfterTimeoutExpiration ||
2557
+ neverResumedContribution)) ||
2558
+ // Pre-condition X => contribute / resume when contribution step = COMPUTING.
2559
+ (computingStep && resumingContribution && samePermanentContributionData) ||
2560
+ // Pre-condition Y => contribute / resume when contribution step = UPLOADING without any pre-uploaded chunk.
2561
+ (uploadingStep && resumingContribution && noTemporaryContributionData) ||
2562
+ // Pre-condition Z => contribute / resume when contribution step = UPLOADING w/ some pre-uploaded chunk.
2563
+ (!noTemporaryContributionData && resumingWithSameTemporaryData);
2564
+ // Scenario (3.B).
2565
+ if (isCurrentContributor && hasResumableStep && startingOrResumingContribution) {
2566
+ // Communicate resume / start of the contribution to participant.
2567
+ await simpleLoader(`${changedContributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */ ? `Starting` : `Resuming`} your contribution...`, `clock`, 3000);
2568
+ // Start / Resume the contribution for the participant.
2569
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropy, providerUserId, false // not finalizing.
2570
+ );
2571
+ }
2572
+ // Scenario (3.A).
2573
+ else if (isWaitingForContribution)
2574
+ listenToCeremonyCircuitDocumentChanges(firestoreDatabase, ceremony.id, participant.id, circuit);
2575
+ // Scenario (3.C).
2576
+ // Pre-condition: current contributor + resuming from verification step.
2577
+ if (isCurrentContributor &&
2578
+ isResumingContribution &&
2579
+ changedContributionStep === "VERIFYING" /* ParticipantContributionStep.VERIFYING */) {
2580
+ const spinner = customSpinner(`Getting info about your current contribution...`, `clock`);
2581
+ spinner.start();
2582
+ // Get current and next index.
2583
+ const currentZkeyIndex = formatZkeyIndex(changedContributionProgress);
2584
+ const nextZkeyIndex = formatZkeyIndex(changedContributionProgress + 1);
2585
+ // Get average verification time (Cloud Function).
2586
+ const avgVerifyCloudFunctionTime = circuit.data.avgTimings.verifyCloudFunction;
2587
+ // Compute estimated time left for this contribution verification.
2588
+ const estimatedTimeLeftForVerification = Date.now() - changedVerificationStartedAt - avgVerifyCloudFunctionTime;
2589
+ // Format time.
2590
+ const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(estimatedTimeLeftForVerification);
2591
+ spinner.stop();
2592
+ console.log(`${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${circuit.data.sequencePosition}`)}`)} (Contribution Steps)`);
2593
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${currentZkeyIndex}`)} downloaded`);
2594
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} computed`);
2595
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} saved on storage`);
2596
+ /// @todo resuming a contribution verification could potentially lead to no verification at all #18.
2597
+ console.log(`${theme.symbols.info} Contribution verification in progress (~ ${theme.text.bold(`${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}`)})`);
2598
+ }
2599
+ // Scenario (3.D).
2600
+ // Pre-condition: contribution has been verified and,
2601
+ // contributor status: DONE if completed all contributions or CONTRIBUTED if just completed the last one (not all).
2602
+ if (progressToNextContribution &&
2603
+ noStatusChanges &&
2604
+ (changedStatus === "DONE" /* ParticipantStatus.DONE */ || changedStatus === "CONTRIBUTED" /* ParticipantStatus.CONTRIBUTED */))
2605
+ // Get latest contribution verification result.
2606
+ await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id);
2607
+ // Scenario (3.E).
2608
+ if (timeoutTriggeredWhileContributing) {
2609
+ await handleTimedoutMessageForContributor(firestoreDatabase, participant.id, ceremony.id, changedContributionProgress, true);
2610
+ terminate(providerUserId);
2611
+ }
2612
+ // Scenario (3.F).
2613
+ if (completedContribution || timeoutExpired) {
2614
+ // Show data about latest contribution verification
2615
+ if (completedContribution)
2616
+ // Get latest contribution verification result.
2617
+ await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id);
2618
+ // Get next circuit for contribution.
2619
+ const nextCircuit = getCircuitBySequencePosition(circuits, changedContributionProgress + 1);
2620
+ // Check disk space requirements for participant.
2621
+ const wannaGenerateAttestation = await handleDiskSpaceRequirementForNextContribution(cloudFunctions, ceremony.id, nextCircuit.data.sequencePosition, nextCircuit.data.zKeySizeInBytes, timeoutExpired, providerUserId);
2622
+ // Check if the participant would like to generate a new attestation.
2623
+ if (wannaGenerateAttestation) {
2624
+ // Handle public attestation generation and operations.
2625
+ await handlePublicAttestation(firestoreDatabase, circuits, ceremony.id, participant.id, changedContributions, providerUserId, ceremony.data.title, ceremony.data.prefix, accessToken);
2626
+ console.log(`\nThank you for participating and securing the ${ceremony.data.title} ceremony ${theme.emojis.pray}`);
2627
+ // Unsubscribe from listener.
2628
+ unsubscribe();
2629
+ // Gracefully exit.
2630
+ terminate(providerUserId);
2631
+ }
2632
+ }
2633
+ // Scenario (3.G).
2634
+ if (alreadyContributedToEveryCeremonyCircuit) {
2635
+ // Get latest contribution verification result.
2636
+ await getLatestVerificationResult(firestoreDatabase, ceremony.id, circuit.id, participant.id);
2637
+ // Handle public attestation generation and operations.
2638
+ await handlePublicAttestation(firestoreDatabase, circuits, ceremony.id, participant.id, changedContributions, providerUserId, ceremony.data.title, ceremony.data.prefix, accessToken);
2639
+ console.log(`\nThank you for participating and securing the ${ceremony.data.title} ceremony ${theme.emojis.pray}`);
2640
+ // Unsubscribe from listener.
2641
+ unsubscribe();
2642
+ // Gracefully exit.
2643
+ terminate(providerUserId);
2644
+ }
2645
+ }
2646
+ });
2647
+ };
2648
+ /**
2649
+ * Contribute command.
2650
+ * @notice The contribute command allows an authenticated user to become a participant (contributor) to the selected ceremony by providing the
2651
+ * entropy (toxic waste) for the contribution.
2652
+ * @dev For proper execution, the command requires the user to be authenticated with Github account (run auth command first) in order to
2653
+ * handle sybil-resistance and connect to Github APIs to publish the gist containing the public attestation.
2654
+ */
2655
+ const contribute = async (opt) => {
2656
+ const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2657
+ // Check for authentication.
2658
+ const { user, providerUserId, token } = await checkAuth(firebaseApp);
2659
+ // Get options.
2660
+ const ceremonyOpt = opt.ceremony;
2661
+ const entropyOpt = opt.entropy;
2662
+ // Prepare data.
2663
+ let selectedCeremony;
2664
+ // Retrieve the opened ceremonies.
2665
+ const ceremoniesOpenedForContributions = await getOpenedCeremonies(firestoreDatabase);
2666
+ // Gracefully exit if no ceremonies are opened for contribution.
2667
+ if (!ceremoniesOpenedForContributions.length)
2668
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_OPENED_CEREMONIES, true);
2669
+ console.log(`${theme.symbols.warning} ${theme.text.bold(`The contribution process is based on a parallel waiting queue mechanism allowing one contributor at a time per circuit with a maximum time upper-bound. Each contribution may require the bulk of your computing resources and memory based on the size of the circuit (ETAs could vary!). If you stop your contribution at any step, you have to restart the step from scratch (except for uploading).`)}\n`);
2670
+ if (ceremonyOpt) {
2671
+ // Check if the input ceremony title match with an opened ceremony.
2672
+ const selectedCeremonyDocument = ceremoniesOpenedForContributions.filter((openedCeremony) => openedCeremony.data.prefix === ceremonyOpt);
2673
+ if (selectedCeremonyDocument.length !== 1) {
2674
+ // Notify user about error.
2675
+ console.log(`${theme.symbols.error} ${COMMAND_ERRORS.COMMAND_CONTRIBUTE_WRONG_OPTION_CEREMONY}`);
2676
+ // Show potential ceremonies
2677
+ console.log(`${theme.symbols.info} Currently, you can contribute to the following ceremonies: `);
2678
+ for (const openedCeremony of ceremoniesOpenedForContributions)
2679
+ console.log(`- ${theme.text.bold(openedCeremony.data.prefix)}\n`);
2680
+ terminate(providerUserId);
2681
+ }
2682
+ else
2683
+ selectedCeremony = selectedCeremonyDocument.at(0);
2684
+ }
2685
+ else {
2686
+ // Prompt the user to select a ceremony from the opened ones.
2687
+ selectedCeremony = await promptForCeremonySelection(ceremoniesOpenedForContributions, false);
2688
+ }
2689
+ // Get selected ceremony circuit(s) documents.
2690
+ const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
2691
+ const spinner = customSpinner(`Verifying your participant status...`, `clock`);
2692
+ spinner.start();
2693
+ // Check that the user's document is created
2694
+ const userDoc = await getDocumentById(firestoreDatabase, commonTerms.collections.users.name, user.uid);
2695
+ const userData = userDoc.data();
2696
+ if (!userData) {
2697
+ spinner.fail(`Unfortunately we could not find a user document with your information. This likely means that you did not pass the GitHub reputation checks and therefore are not elegible to contribute to any ceremony. Please contact the coordinator if you believe this to be an error.`);
2698
+ process.exit(0);
2699
+ }
2700
+ // Check the user's current participant readiness for contribution status (eligible, already contributed, timed out).
2701
+ const canParticipantContributeToCeremony = await checkParticipantForCeremony(firebaseFunctions, selectedCeremony.id);
2702
+ await sleep(2000); // wait for CF execution.
2703
+ // Get updated participant data.
2704
+ const participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid);
2705
+ const participantData = participant.data();
2706
+ if (!participantData)
2707
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true);
2708
+ if (canParticipantContributeToCeremony) {
2709
+ spinner.succeed(`Great, you are qualified to contribute to the ceremony`);
2710
+ let entropy = ""; // toxic waste.
2711
+ // Prepare local directories.
2712
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.output);
2713
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.contribute);
2714
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.contributions);
2715
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.attestations);
2716
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.transcripts);
2717
+ // Extract participant data.
2718
+ const { contributionProgress, contributionStep } = participantData;
2719
+ // Check if the participant can input the entropy
2720
+ if (contributionProgress < circuits.length ||
2721
+ (contributionProgress === circuits.length && contributionStep < "UPLOADING" /* ParticipantContributionStep.UPLOADING */)) {
2722
+ if (entropyOpt)
2723
+ entropy = entropyOpt;
2724
+ /// @todo should we preserve entropy between different re-run of the command? (e.g., resume after timeout).
2725
+ // Prompt for entropy generation.
2726
+ else
2727
+ entropy = await promptForEntropy();
2728
+ }
2729
+ // Listener to following the core contribution workflow.
2730
+ await listenToParticipantDocumentChanges(firestoreDatabase, firebaseFunctions, participant, selectedCeremony, entropy, providerUserId, token);
2731
+ }
2732
+ else {
2733
+ // Extract participant data.
2734
+ const { status, contributionStep, contributionProgress } = participantData;
2735
+ // Check whether the participant has already contributed to all circuits.
2736
+ if ((!canParticipantContributeToCeremony && status === "DONE" /* ParticipantStatus.DONE */) ||
2737
+ status === "FINALIZED" /* ParticipantStatus.FINALIZED */) {
2738
+ spinner.info(`You have already made the contributions for the circuits in the ceremony`);
2739
+ // await handleContributionValidity(firestoreDatabase, circuits, selectedCeremony.id, participant.id)
2740
+ spinner.text = "Checking your public attestation gist...";
2741
+ spinner.start();
2742
+ // Check whether the user has published the Github Gist about the public attestation.
2743
+ const publishedPublicAttestationGist = await getPublicAttestationGist(token, `${selectedCeremony.data.prefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`);
2744
+ if (!publishedPublicAttestationGist) {
2745
+ spinner.stop();
2746
+ await handlePublicAttestation(firestoreDatabase, circuits, selectedCeremony.id, participant.id, participantData?.contributions, providerUserId, selectedCeremony.data.title, selectedCeremony.data.prefix, token);
2747
+ }
2748
+ else {
2749
+ // Extract url from raw.
2750
+ const gistUrl = publishedPublicAttestationGist.raw_url.substring(0, publishedPublicAttestationGist.raw_url.indexOf("/raw/"));
2751
+ spinner.stop();
2752
+ process.stdout.write(`\n`);
2753
+ console.log(`${theme.symbols.success} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2754
+ // Prepare a ready-to-share tweet.
2755
+ await handleTweetGeneration(selectedCeremony.data.title, gistUrl);
2756
+ }
2757
+ console.log(`\nThank you for participating and securing the ${selectedCeremony.data.title} ceremony ${theme.emojis.pray}`);
2758
+ }
2759
+ // Check if there's a timeout still in effect for the participant.
2760
+ if (status === "TIMEDOUT" /* ParticipantStatus.TIMEDOUT */ && contributionStep !== "COMPLETED" /* ParticipantContributionStep.COMPLETED */) {
2761
+ spinner.warn(`Oops, you are not allowed to continue your contribution due to current timeout`);
2762
+ await handleTimedoutMessageForContributor(firestoreDatabase, participant.id, selectedCeremony.id, contributionProgress, false);
2763
+ }
2764
+ // Exit gracefully.
2765
+ terminate(providerUserId);
2766
+ }
2767
+ };
2768
+
2769
+ /**
2770
+ * Clean cursor lines from current position back to root (default: zero).
2771
+ * @param currentCursorPos - the current position of the cursor.
2772
+ * @returns <number>
2773
+ */
2774
+ const cleanCursorPosBackToRoot = (currentCursorPos) => {
2775
+ while (currentCursorPos < 0) {
2776
+ // Get back and clean line by line.
2777
+ readline.cursorTo(process.stdout, 0);
2778
+ readline.clearLine(process.stdout, 0);
2779
+ readline.moveCursor(process.stdout, -1, -1);
2780
+ currentCursorPos += 1;
2781
+ }
2782
+ return currentCursorPos;
2783
+ };
2784
+ /**
2785
+ * Show the latest updates for the given circuit.
2786
+ * @param firestoreDatabase <Firestore> - the Firestore database to query from.
2787
+ * @param ceremony <FirebaseDocumentInfo> - the Firebase document containing info about the ceremony.
2788
+ * @param circuit <FirebaseDocumentInfo> - the Firebase document containing info about the circuit.
2789
+ * @returns Promise<number> return the current position of the cursor (i.e., number of lines displayed).
2790
+ */
2791
+ const displayLatestCircuitUpdates = async (firestoreDatabase, ceremony, circuit) => {
2792
+ let observation = theme.text.bold(`- Circuit # ${theme.colors.magenta(circuit.data.sequencePosition)}`); // Observation output.
2793
+ let cursorPos = -1; // Current cursor position (nb. decrease every time there's a new line!).
2794
+ const { waitingQueue } = circuit.data;
2795
+ // Get info from circuit.
2796
+ const { currentContributor } = waitingQueue;
2797
+ const { completedContributions } = waitingQueue;
2798
+ if (!currentContributor) {
2799
+ observation += `\n> Nobody's currently waiting to contribute ${theme.emojis.eyes}`;
2800
+ cursorPos -= 1;
2801
+ }
2802
+ else {
2803
+ // Search for currentContributor' contribution.
2804
+ const contributions = await getCircuitContributionsFromContributor(firestoreDatabase, ceremony.id, circuit.id, currentContributor);
2805
+ if (!contributions.length) {
2806
+ // The contributor is currently contributing.
2807
+ observation += `\n> Participant ${theme.text.bold(`#${completedContributions + 1}`)} (${theme.text.bold(currentContributor)}) is currently contributing ${theme.emojis.fire}`;
2808
+ cursorPos -= 1;
2809
+ }
2810
+ else {
2811
+ // The contributor has contributed.
2812
+ observation += `\n> Participant ${theme.text.bold(`#${completedContributions}`)} (${theme.text.bold(currentContributor)}) has completed the contribution ${theme.emojis.tada}`;
2813
+ cursorPos -= 1;
2814
+ // The contributor has finished the contribution.
2815
+ const contributionData = contributions.at(0)?.data;
2816
+ if (!contributionData)
2817
+ showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true);
2818
+ // Convert times to seconds.
2819
+ const { seconds: contributionTimeSeconds, minutes: contributionTimeMinutes, hours: contributionTimeHours } = getSecondsMinutesHoursFromMillis(contributionData?.contributionComputationTime);
2820
+ const { seconds: verificationTimeSeconds, minutes: verificationTimeMinutes, hours: verificationTimeHours } = getSecondsMinutesHoursFromMillis(contributionData?.verificationComputationTime);
2821
+ observation += `\n> The ${theme.text.bold("computation")} took ${theme.text.bold(`${convertToDoubleDigits(contributionTimeHours)}:${convertToDoubleDigits(contributionTimeMinutes)}:${convertToDoubleDigits(contributionTimeSeconds)}`)}`;
2822
+ observation += `\n> The ${theme.text.bold("verification")} took ${theme.text.bold(`${convertToDoubleDigits(verificationTimeHours)}:${convertToDoubleDigits(verificationTimeMinutes)}:${convertToDoubleDigits(verificationTimeSeconds)}`)}`;
2823
+ observation += `\n> Contribution ${contributionData?.valid
2824
+ ? `${theme.text.bold("VALID")} ${theme.symbols.success}`
2825
+ : `${theme.text.bold("INVALID")} ${theme.symbols.error}`}`;
2826
+ cursorPos -= 3;
2827
+ }
2828
+ }
2829
+ // Show observation for circuit.
2830
+ process.stdout.write(`${observation}\n\n`);
2831
+ cursorPos -= 1;
2832
+ return cursorPos;
2833
+ };
2834
+ /**
2835
+ * Observe command.
2836
+ */
2837
+ const observe = async () => {
2838
+ // @todo to be moved as command configuration parameter.
2839
+ const observationWaitingTimeInMillis = 3000;
2840
+ try {
2841
+ // Initialize services.
2842
+ const { firebaseApp, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2843
+ // Handle current authenticated user sign in.
2844
+ const { user } = await checkAuth(firebaseApp);
2845
+ // Preserve command execution only for coordinators].
2846
+ if (!(await isCoordinator(user)))
2847
+ showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
2848
+ // Get running cerimonies info (if any).
2849
+ const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase);
2850
+ // Ask to select a ceremony.
2851
+ const ceremony = await promptForCeremonySelection(runningCeremoniesDocs, false);
2852
+ console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`);
2853
+ let cursorPos = 0; // Keep track of current cursor position.
2854
+ const spinner = customSpinner(`Getting ready...`, "clock");
2855
+ spinner.start();
2856
+ // Get circuit updates every 3 seconds.
2857
+ setInterval(async () => {
2858
+ // Clean cursor position back to root.
2859
+ cursorPos = cleanCursorPosBackToRoot(cursorPos);
2860
+ spinner.stop();
2861
+ spinner.text = `Updating...`;
2862
+ spinner.start();
2863
+ // Get updates from circuits.
2864
+ const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id);
2865
+ await sleep(observationWaitingTimeInMillis / 10); // Just for a smoother UX/UI experience.
2866
+ spinner.stop();
2867
+ // Observe changes for each circuit
2868
+ for await (const circuit of circuits)
2869
+ cursorPos += await displayLatestCircuitUpdates(firestoreDatabase, ceremony, circuit);
2870
+ process.stdout.write(`Press CTRL+C to exit`);
2871
+ await sleep(1000); // Just for a smoother UX/UI experience.
2872
+ }, observationWaitingTimeInMillis);
2873
+ await sleep(observationWaitingTimeInMillis); // Wait until the first update.
2874
+ spinner.stop();
2875
+ }
2876
+ catch (err) {
2877
+ showError(`Something went wrong: ${err.toString()}`, true);
2878
+ }
2879
+ };
2880
+
2881
+ /**
2882
+ * Export and store on the ceremony bucket the verification key for the given final contribution.
2883
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
2884
+ * @param bucketName <string> - the name of the ceremony bucket.
2885
+ * @param finalZkeyLocalFilePath <string> - the local file path of the final zKey.
2886
+ * @param verificationKeyLocalFilePath <string> - the local file path of the verification key.
2887
+ * @param verificationKeyStorageFilePath <string> - the storage file path of the verification key.
2888
+ */
2889
+ const handleVerificationKey = async (cloudFunctions, bucketName, finalZkeyLocalFilePath, verificationKeyLocalFilePath, verificationKeyStorageFilePath) => {
2890
+ const spinner = customSpinner(`Exporting the verification key...`, "clock");
2891
+ spinner.start();
2892
+ // Export the verification key.
2893
+ const vKey = await exportVkey(finalZkeyLocalFilePath);
2894
+ spinner.text = "Writing verification key...";
2895
+ // Write the verification key locally.
2896
+ writeLocalJsonFile(verificationKeyLocalFilePath, vKey);
2897
+ await sleep(3000); // workaound for file descriptor.
2898
+ // Upload verification key to storage.
2899
+ await multiPartUpload(cloudFunctions, bucketName, verificationKeyStorageFilePath, verificationKeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2900
+ spinner.succeed(`Verification key correctly saved on storage`);
2901
+ };
2902
+ /**
2903
+ * Derive and store on the ceremony bucket the Solidity Verifier smart contract for the given final contribution.
2904
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
2905
+ * @param bucketName <string> - the name of the ceremony bucket.
2906
+ * @param finalZkeyLocalFilePath <string> - the local file path of the final zKey.
2907
+ * @param verifierContractLocalFilePath <string> - the local file path of the verifier smart contract.
2908
+ * @param verifierContractStorageFilePath <string> - the storage file path of the verifier smart contract.
2909
+ */
2910
+ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkeyLocalFilePath, verifierContractLocalFilePath, verifierContractStorageFilePath) => {
2911
+ const spinner = customSpinner(`Extracting verifier contract...`, `clock`);
2912
+ spinner.start();
2913
+ // Verifier path.
2914
+ const packagePath = `${dirname(fileURLToPath(import.meta.url))}`;
2915
+ const verifierPath = packagePath.includes(`src/commands`)
2916
+ ? `${dirname(fileURLToPath(import.meta.url))}/../../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`
2917
+ : `${dirname(fileURLToPath(import.meta.url))}/../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs`;
2918
+ // Export the Solidity verifier smart contract.
2919
+ const verifierCode = await exportVerifierContract(finalZkeyLocalFilePath, verifierPath);
2920
+ spinner.text = `Writing verifier smart contract...`;
2921
+ // Write the verification key locally.
2922
+ writeFile(verifierContractLocalFilePath, verifierCode);
2923
+ await sleep(3000); // workaound for file descriptor.
2924
+ // Upload verifier smart contract to storage.
2925
+ await multiPartUpload(cloudFunctions, bucketName, verifierContractStorageFilePath, verifierContractLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2926
+ spinner.succeed(`Verifier smart contract correctly saved on storage`);
2927
+ };
2928
+ /**
2929
+ * Handle the process of finalizing a ceremony circuit.
2930
+ * @dev this process results in the extraction of the final ceremony artifacts for the calculation and verification of proofs.
2931
+ * @notice this method must enforce the order among these steps:
2932
+ * 1) Compute the final contribution (zKey).
2933
+ * 2) Extract the verification key (vKey).
2934
+ * 3) Extract the Verifier smart contract (.sol).
2935
+ * 4) Upload the artifacts in the AWS S3 storage.
2936
+ * 5) Complete the final contribution data w/ artifacts references and hashes (cloud function).
2937
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
2938
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
2939
+ * @param ceremony <FirebaseDocumentInfo> - the Firestore document of the ceremony.
2940
+ * @param circuit <FirebaseDocumentInfo> - the Firestore document of the ceremony circuit.
2941
+ * @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (coordinator).
2942
+ * @param beacon <string> - the value used to compute the final contribution while finalizing the ceremony.
2943
+ * @param coordinatorIdentifier <string> - the identifier of the coordinator.
2944
+ */
2945
+ const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier) => {
2946
+ // Step (1).
2947
+ await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true);
2948
+ await sleep(2000); // workaound for descriptors.
2949
+ // Extract data.
2950
+ const { prefix: circuitPrefix } = circuit.data;
2951
+ const { prefix: ceremonyPrefix } = ceremony.data;
2952
+ // Prepare local paths.
2953
+ const finalZkeyLocalFilePath = getFinalZkeyLocalFilePath(`${circuitPrefix}_${finalContributionIndex}.zkey`);
2954
+ const verificationKeyLocalFilePath = getVerificationKeyLocalFilePath(`${circuitPrefix}_${verificationKeyAcronym}.json`);
2955
+ const verifierContractLocalFilePath = getVerifierContractLocalFilePath(`${circuitPrefix}_${verifierSmartContractAcronym}.sol`);
2956
+ // Prepare storage paths.
2957
+ const verificationKeyStorageFilePath = getVerificationKeyStorageFilePath(circuitPrefix, `${circuitPrefix}_${verificationKeyAcronym}.json`);
2958
+ const verifierContractStorageFilePath = getVerifierContractStorageFilePath(circuitPrefix, `${circuitPrefix}_${verifierSmartContractAcronym}.sol`);
2959
+ // Get ceremony bucket.
2960
+ const bucketName = getBucketName(ceremonyPrefix, String(process.env.CONFIG_CEREMONY_BUCKET_POSTFIX));
2961
+ // Step (2 & 4).
2962
+ await handleVerificationKey(cloudFunctions, bucketName, finalZkeyLocalFilePath, verificationKeyLocalFilePath, verificationKeyStorageFilePath);
2963
+ // Step (3 & 4).
2964
+ await handleVerifierSmartContract(cloudFunctions, bucketName, finalZkeyLocalFilePath, verifierContractLocalFilePath, verifierContractStorageFilePath);
2965
+ // Step (5).
2966
+ const spinner = customSpinner(`Wrapping up the finalization of the circuit...`, `clock`);
2967
+ spinner.start();
2968
+ // Finalize circuit contribution.
2969
+ await finalizeCircuit(cloudFunctions, ceremony.id, circuit.id, bucketName, beacon);
2970
+ await sleep(2000);
2971
+ spinner.succeed(`Circuit has been finalized correctly`);
2972
+ };
2973
+ /**
2974
+ * Finalize command.
2975
+ * @notice The finalize command allows a coordinator to finalize a Trusted Setup Phase 2 ceremony by providing the final beacon,
2976
+ * computing the final zKeys and extracting the Verifier Smart Contract + Verification Keys per each ceremony circuit.
2977
+ * anyone could use the final zKey to create a proof and everyone else could verify the correctness using the
2978
+ * related verification key (off-chain) or Verifier smart contract (on-chain).
2979
+ * @dev For proper execution, the command requires the coordinator to be authenticated with a GitHub account (run auth command first) in order to
2980
+ * handle sybil-resistance and connect to GitHub APIs to publish the gist containing the final public attestation.
2981
+ */
2982
+ const finalize = async () => {
2983
+ const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices();
2984
+ // Check for authentication.
2985
+ const { user, providerUserId, token: coordinatorAccessToken } = await checkAuth(firebaseApp);
2986
+ // Preserve command execution only for coordinators.
2987
+ if (!(await isCoordinator(user)))
2988
+ showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true);
2989
+ // Retrieve the closed ceremonies (ready for finalization).
2990
+ const ceremoniesClosedForFinalization = await getClosedCeremonies(firestoreDatabase);
2991
+ // Gracefully exit if no ceremonies are closed and ready for finalization.
2992
+ if (!ceremoniesClosedForFinalization.length)
2993
+ showError(COMMAND_ERRORS.COMMAND_FINALIZED_NO_CLOSED_CEREMONIES, true);
2994
+ console.log(`${theme.symbols.warning} The computation of the final contribution could take the bulk of your computational resources and memory based on the size of the circuit ${theme.emojis.fire}\n`);
2995
+ // Prompt for ceremony selection.
2996
+ const selectedCeremony = await promptForCeremonySelection(ceremoniesClosedForFinalization, true);
2997
+ // Get coordinator participant document.
2998
+ let participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid);
2999
+ const isCoordinatorReadyForCeremonyFinalization = await checkAndPrepareCoordinatorForFinalization(firebaseFunctions, selectedCeremony.id);
3000
+ if (!isCoordinatorReadyForCeremonyFinalization)
3001
+ showError(COMMAND_ERRORS.COMMAND_FINALIZED_NOT_READY_FOR_FINALIZATION, true);
3002
+ // Prompt for beacon.
3003
+ const beacon = await promptToTypeEntropyOrBeacon(false);
3004
+ // Compute hash
3005
+ const beaconHash = computeSHA256ToHex(beacon);
3006
+ // Display.
3007
+ console.log(`${theme.symbols.info} Beacon SHA256 hash ${theme.text.bold(beaconHash)}`);
3008
+ // Clean directories.
3009
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.output);
3010
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalize);
3011
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalZkeys);
3012
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalPot);
3013
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.finalAttestations);
3014
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.verificationKeys);
3015
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.verifierContracts);
3016
+ // Get ceremony circuits.
3017
+ const circuits = await getCeremonyCircuits(firestoreDatabase, selectedCeremony.id);
3018
+ // Handle finalization for each ceremony circuit.
3019
+ for await (const circuit of circuits)
3020
+ await handleCircuitFinalization(firebaseFunctions, firestoreDatabase, selectedCeremony, circuit, participant, beacon, providerUserId);
3021
+ process.stdout.write(`\n`);
3022
+ const spinner = customSpinner(`Wrapping up the finalization of the ceremony...`, "clock");
3023
+ spinner.start();
3024
+ // Finalize the ceremony.
3025
+ await finalizeCeremony(firebaseFunctions, selectedCeremony.id);
3026
+ spinner.succeed(`Great, you have completed the finalization of the ${theme.text.bold(selectedCeremony.data.title)} ceremony ${theme.emojis.tada}\n`);
3027
+ // Get updated coordinator participant document.
3028
+ participant = await getDocumentById(firestoreDatabase, getParticipantsCollectionPath(selectedCeremony.id), user.uid);
3029
+ // Extract updated data.
3030
+ const { contributions } = participant.data();
3031
+ const { prefix, title: ceremonyName } = selectedCeremony.data;
3032
+ // Generate attestation with final contributions.
3033
+ const publicAttestation = await generateValidContributionsAttestation(firestoreDatabase, circuits, selectedCeremony.id, participant.id, contributions, providerUserId, ceremonyName, true);
3034
+ // Write public attestation locally.
3035
+ writeFile(getAttestationLocalFilePath(`${prefix}_${finalContributionIndex}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
3036
+ await sleep(3000); // workaround for file descriptor unexpected close.
3037
+ const gistUrl = await publishGist(coordinatorAccessToken, publicAttestation, ceremonyName, prefix);
3038
+ console.log(`\n${theme.symbols.info} Your public final attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
3039
+ // Generate a ready to share custom url to tweet about ceremony participation.
3040
+ const tweetUrl = generateCustomUrlToTweetAboutParticipation(ceremonyName, gistUrl, true);
3041
+ console.log(`${theme.symbols.info} We encourage you to tweet about the ceremony finalization by clicking the link below\n\n${theme.text.underlined(tweetUrl)}`);
3042
+ // Automatically open a webpage with the tweet.
3043
+ await open(tweetUrl);
3044
+ terminate(providerUserId);
3045
+ };
3046
+
3047
+ /**
3048
+ * Clean command.
3049
+ */
3050
+ const clean = async () => {
3051
+ try {
3052
+ // Initialize services.
3053
+ await bootstrapCommandExecutionAndServices();
3054
+ const spinner = customSpinner(`Cleaning up...`, "clock");
3055
+ if (directoryExists(localPaths.output)) {
3056
+ console.log(theme.text.bold(`${theme.symbols.warning} Be careful, this action is irreversible!`));
3057
+ const { confirmation } = await askForConfirmation("Are you sure you want to continue with the clean up?", "Yes", "No");
3058
+ if (confirmation) {
3059
+ spinner.start();
3060
+ // Do the clean up.
3061
+ deleteDir(localPaths.output);
3062
+ // nb. simulate waiting time for 1s.
3063
+ await sleep(1000);
3064
+ spinner.succeed(`Cleanup was successfully completed ${theme.emojis.broom}`);
3065
+ }
3066
+ }
3067
+ else {
3068
+ console.log(`${theme.symbols.info} There is nothing to clean ${theme.emojis.eyes}`);
3069
+ }
3070
+ }
3071
+ catch (err) {
3072
+ showError(`Something went wrong: ${err.toString()}`, true);
3073
+ }
3074
+ };
3075
+
3076
+ /**
3077
+ * Logout command.
3078
+ */
3079
+ const logout = async () => {
3080
+ try {
3081
+ // Initialize services.
3082
+ const { firebaseApp } = await bootstrapCommandExecutionAndServices();
3083
+ // Check for authentication.
3084
+ const { providerUserId } = await checkAuth(firebaseApp);
3085
+ // Inform the user about deassociation in Github and re run auth
3086
+ console.log(`${theme.symbols.warning} The logout could sign you out from Firebase and will delete the access token saved locally on this machine. Therefore, you have to run ${theme.text.bold("phase2cli auth")} to authenticate again.\n${theme.symbols.info} Remember, we cannot revoke the authorization from your Github account from this CLI! You can do this manually as reported in the official Github documentation ${theme.emojis.pointDown}\n\n${theme.text.bold(theme.text.underlined(`https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/reviewing-your-authorized-applications-oauth`))}\n`);
3087
+ // Ask for confirmation.
3088
+ const { confirmation } = await askForConfirmation("Are you sure you want to log out from this machine?", "Yes", "No");
3089
+ if (confirmation) {
3090
+ const spinner = customSpinner(`Logging out...`, "clock");
3091
+ spinner.start();
3092
+ // Sign out.
3093
+ const auth = getAuth();
3094
+ await signOut(auth);
3095
+ // Delete local token.
3096
+ deleteLocalAccessToken();
3097
+ await sleep(3000); // ~3s.
3098
+ spinner.stop();
3099
+ console.log(`${theme.symbols.success} Logout successfully completed`);
3100
+ }
3101
+ else
3102
+ terminate(providerUserId);
3103
+ }
3104
+ catch (err) {
3105
+ showError(`Something went wrong: ${err.toString()}`, true);
3106
+ }
3107
+ };
3108
+
3109
+ /**
3110
+ * Validate ceremony setup command.
3111
+ */
3112
+ const validate = async (cmd) => {
3113
+ try {
3114
+ // parse the file and cleanup after
3115
+ const parsedFile = await parseCeremonyFile(cmd.template, true);
3116
+ // check whether we have a constraints option otherwise default to 1M
3117
+ const constraints = cmd.constraints || 1000000;
3118
+ for await (const circuit of parsedFile.circuits) {
3119
+ if (circuit.metadata.constraints > constraints) {
3120
+ console.log(false);
3121
+ process.exit(0);
3122
+ }
3123
+ }
3124
+ console.log(true);
3125
+ }
3126
+ catch (err) {
3127
+ showError(`${err.toString()}`, false);
3128
+ // we want to exit with a non-zero exit code
3129
+ process.exit(1);
3130
+ }
3131
+ };
3132
+
3133
+ /**
3134
+ * Validate ceremony setup command.
3135
+ */
3136
+ const listCeremonies = async () => {
3137
+ try {
3138
+ // bootstrap command execution and services
3139
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices();
3140
+ // get all ceremonies
3141
+ const ceremonies = await getAllCollectionDocs(firestoreDatabase, commonTerms.collections.ceremonies.name);
3142
+ // store all names
3143
+ const names = [];
3144
+ // loop through all ceremonies
3145
+ for (const ceremony of ceremonies)
3146
+ names.push(ceremony.data().prefix);
3147
+ // print them to the console
3148
+ console.log(names.join(", "));
3149
+ process.exit(0);
3150
+ }
3151
+ catch (err) {
3152
+ showError(`${err.toString()}`, false);
3153
+ // we want to exit with a non-zero exit code
3154
+ process.exit(1);
3155
+ }
3156
+ };
3157
+
3158
+ // Get pkg info (e.g., name, version).
3159
+ const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
3160
+ const { description, version, name } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
3161
+ const program = createCommand();
3162
+ // Entry point.
3163
+ program.name(name).description(description).version(version);
3164
+ // User commands.
3165
+ program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth);
3166
+ program
3167
+ .command("contribute")
3168
+ .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
3169
+ .option("-c, --ceremony <string>", "the prefix of the ceremony you want to contribute for", "")
3170
+ .option("-e, --entropy <string>", "the entropy (aka toxic waste) of your contribution", "")
3171
+ .action(contribute);
3172
+ program
3173
+ .command("clean")
3174
+ .description("clean up output generated by commands from the current working directory")
3175
+ .action(clean);
3176
+ program
3177
+ .command("list")
3178
+ .description("List all ceremonies prefixes")
3179
+ .action(listCeremonies);
3180
+ program
3181
+ .command("logout")
3182
+ .description("sign out from Firebae Auth service and delete Github OAuth 2.0 token from local storage")
3183
+ .action(logout);
3184
+ program
3185
+ .command("validate")
3186
+ .description("Validate that a Ceremony Setup file is correct")
3187
+ .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
3188
+ .option("-c, --constraints <number>", "The number of constraints to check against")
3189
+ .action(validate);
3190
+ // Only coordinator commands.
3191
+ const ceremony = program.command("coordinate").description("commands for coordinating a ceremony");
3192
+ ceremony
3193
+ .command("setup")
3194
+ .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
3195
+ .option('-t, --template <path>', 'The path to the ceremony setup template', '')
3196
+ .option('-a, --auth <string>', 'The Github OAuth 2.0 token', '')
3197
+ .action(setup);
3198
+ ceremony
3199
+ .command("observe")
3200
+ .description("observe in real-time the waiting queue of each ceremony circuit")
3201
+ .action(observe);
3202
+ ceremony
3203
+ .command("finalize")
3204
+ .description("finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract")
3205
+ .action(finalize);
3206
+ program.parseAsync(process.argv);