@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
@@ -0,0 +1,778 @@
1
+ import fetch from "@adobe/node-fetch-retry"
2
+ import { request } from "@octokit/request"
3
+ import {
4
+ commonTerms,
5
+ convertBytesOrKbToGb,
6
+ createCustomLoggerForFile,
7
+ convertToDoubleDigits,
8
+ finalContributionIndex,
9
+ FirebaseDocumentInfo,
10
+ formatZkeyIndex,
11
+ generateGetObjectPreSignedUrl,
12
+ getBucketName,
13
+ getDocumentById,
14
+ getParticipantsCollectionPath,
15
+ getZkeyStorageFilePath,
16
+ multiPartUpload,
17
+ numExpIterations,
18
+ ParticipantContributionStep,
19
+ permanentlyStoreCurrentContributionTimeAndHash,
20
+ progressToNextContributionStep,
21
+ verifyContribution
22
+ } from "@p0tion/actions"
23
+ import { Presets, SingleBar } from "cli-progress"
24
+ import dotenv from "dotenv"
25
+ import { GithubAuthProvider, OAuthCredential } from "firebase/auth"
26
+ import { DocumentData, Firestore } from "firebase/firestore"
27
+ import { Functions } from "firebase/functions"
28
+ import { createWriteStream } from "fs"
29
+ import { getDiskInfoSync } from "node-disk-info"
30
+ import ora, { Ora } from "ora"
31
+ import { zKey } from "snarkjs"
32
+ import { Timer } from "timer-node"
33
+ import { Logger } from "winston"
34
+ import { fileURLToPath } from "url"
35
+ import { dirname } from "path"
36
+ import { GithubGistFile, ProgressBarType, Timing } from "../types/index.js"
37
+ import { COMMAND_ERRORS, CORE_SERVICES_ERRORS, showError, THIRD_PARTY_SERVICES_ERRORS } from "./errors.js"
38
+ import { readFile } from "./files.js"
39
+ import {
40
+ getContributionLocalFilePath,
41
+ getFinalTranscriptLocalFilePath,
42
+ getFinalZkeyLocalFilePath,
43
+ getTranscriptLocalFilePath
44
+ } from "./localConfigs.js"
45
+ import theme from "./theme.js"
46
+
47
+ const packagePath = `${dirname(fileURLToPath(import.meta.url))}`
48
+ dotenv.config({
49
+ path: packagePath.includes(`src/lib`)
50
+ ? `${dirname(fileURLToPath(import.meta.url))}/../../.env`
51
+ : `${dirname(fileURLToPath(import.meta.url))}/.env`
52
+ })
53
+
54
+ /**
55
+ * Exchange the Github token for OAuth credential.
56
+ * @param githubToken <string> - the Github token generated through the Device Flow process.
57
+ * @returns <OAuthCredential>
58
+ */
59
+ export const exchangeGithubTokenForCredentials = (githubToken: string): OAuthCredential =>
60
+ GithubAuthProvider.credential(githubToken)
61
+
62
+ /**
63
+ * Get the information associated to the account from which the token has been generated to
64
+ * create a custom unique identifier for the user.
65
+ * @notice the unique identifier has the following form 'handle-identifier'.
66
+ * @param githubToken <string> - the Github token.
67
+ * @returns <Promise<any>> - the Github (provider) unique identifier associated to the user.
68
+ */
69
+ export const getGithubProviderUserId = async (githubToken: string): Promise<any> => {
70
+ // Ask for user account public information through Github API.
71
+ const response = await request("GET https://api.github.com/user", {
72
+ headers: {
73
+ authorization: `token ${githubToken}`
74
+ }
75
+ })
76
+
77
+ if (response && response.status === 200) return `${response.data.login}-${response.data.id}`
78
+
79
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
80
+ }
81
+
82
+ /**
83
+ * Get the gists associated to the authenticated user account.
84
+ * @param githubToken <string> - the Github token.
85
+ * @param params <Object<number,number>> - the necessary parameters for the request.
86
+ * @returns <Promise<any>> - the Github gists associated with the authenticated user account.
87
+ */
88
+ export const getGithubAuthenticatedUserGists = async (
89
+ githubToken: string,
90
+ params: { perPage: number; page: number }
91
+ ): Promise<any> => {
92
+ // Ask for user account public information through Github API.
93
+ const response = await request("GET https://api.github.com/gists{?per_page,page}", {
94
+ headers: {
95
+ authorization: `token ${githubToken}`
96
+ },
97
+ per_page: params.perPage, // max items per page = 100.
98
+ page: params.page
99
+ })
100
+
101
+ if (response && response.status === 200) return response.data
102
+
103
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
104
+ }
105
+
106
+ /**
107
+ * Check whether or not the user has published the gist.
108
+ * @dev gather all the user's gists and check if there is a match with the expected public attestation.
109
+ * @param githubToken <string> - the Github token.
110
+ * @param publicAttestationFilename <string> - the public attestation filename.
111
+ * @returns <Promise<GithubGistFile | undefined>> - return the public attestation gist if and only if has been published.
112
+ */
113
+ export const getPublicAttestationGist = async (
114
+ githubToken: string,
115
+ publicAttestationFilename: string
116
+ ): Promise<GithubGistFile | undefined> => {
117
+ const itemsPerPage = 50 // number of gists to fetch x page.
118
+ let gists: Array<any> = [] // The list of user gists.
119
+ let publishedGist: GithubGistFile | undefined // the published public attestation gist.
120
+ let page = 1 // Page of gists = starts from 1.
121
+
122
+ // Get first batch (page) of gists
123
+ let pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page })
124
+
125
+ // State update.
126
+ gists = gists.concat(pageGists)
127
+
128
+ // Keep going until hitting a blank page.
129
+ while (pageGists.length > 0) {
130
+ // Fetch next page.
131
+ page += 1
132
+ pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page })
133
+
134
+ // State update.
135
+ gists = gists.concat(pageGists)
136
+ }
137
+
138
+ // Look for public attestation.
139
+ for (const gist of gists) {
140
+ const numberOfFiles = Object.keys(gist.files).length
141
+ const publicAttestationCandidateFile = Object.values(gist.files)[0] as GithubGistFile
142
+
143
+ /// @todo improve check by using expected public attestation content (e.g., hash).
144
+ if (numberOfFiles === 1 && publicAttestationCandidateFile.filename === publicAttestationFilename)
145
+ publishedGist = publicAttestationCandidateFile
146
+ }
147
+
148
+ return publishedGist
149
+ }
150
+
151
+ /**
152
+ * Return the Github handle from the provider user id.
153
+ * @notice the provider user identifier must have the following structure 'handle-id'.
154
+ * @param providerUserId <string> - the unique provider user identifier.
155
+ * @returns <string> - the third-party provider handle of the user.
156
+ */
157
+ export const getUserHandleFromProviderUserId = (providerUserId: string): string => {
158
+ if (providerUserId.indexOf("-") === -1) showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
159
+
160
+ return providerUserId.split("-")[0]
161
+ }
162
+
163
+ /**
164
+ * Return a custom spinner.
165
+ * @param text <string> - the text that should be displayed as spinner status.
166
+ * @param spinnerLogo <any> - the logo.
167
+ * @returns <Ora> - a new Ora custom spinner.
168
+ */
169
+ export const customSpinner = (text: string, spinnerLogo: any): Ora =>
170
+ ora({
171
+ text,
172
+ spinner: spinnerLogo
173
+ })
174
+
175
+ /**
176
+ * Custom sleeper.
177
+ * @dev to be used in combination with loggers and for workarounds where listeners cannot help.
178
+ * @param ms <number> - sleep amount in milliseconds
179
+ * @returns <Promise<any>>
180
+ */
181
+ export const sleep = (ms: number): Promise<any> => new Promise((resolve) => setTimeout(resolve, ms))
182
+
183
+ /**
184
+ * Simple loader for task simulation.
185
+ * @param loadingText <string> - spinner text while loading.
186
+ * @param spinnerLogo <any> - spinner logo.
187
+ * @param durationInMs <number> - spinner loading duration in ms.
188
+ * @returns <Promise<void>>.
189
+ */
190
+ export const simpleLoader = async (loadingText: string, spinnerLogo: any, durationInMs: number): Promise<void> => {
191
+ // Custom spinner (used as loader).
192
+ const loader = customSpinner(loadingText, spinnerLogo)
193
+
194
+ loader.start()
195
+
196
+ // nb. simulate execution for requested duration.
197
+ await sleep(durationInMs)
198
+
199
+ loader.stop()
200
+ }
201
+
202
+ /**
203
+ * Check and return the free aggregated disk space (in KB) for participant machine.
204
+ * @dev this method use the node-disk-info method to retrieve the information about
205
+ * disk availability for all visible disks.
206
+ * nb. no other type of data or operation is performed by this methods.
207
+ * @returns <number> - the free aggregated disk space in kB for the participant machine.
208
+ */
209
+ export const estimateParticipantFreeGlobalDiskSpace = (): number => {
210
+ // Get info about disks.
211
+ const disks = getDiskInfoSync()
212
+
213
+ // Get an estimation of available memory.
214
+ let availableDiskSpace = 0
215
+
216
+ for (const disk of disks) availableDiskSpace += disk.available
217
+
218
+ // Return the disk space available in KB.
219
+ return availableDiskSpace
220
+ }
221
+
222
+ /**
223
+ * Get seconds, minutes, hours and days from milliseconds.
224
+ * @param millis <number> - the amount of milliseconds.
225
+ * @returns <Timing> - a custom object containing the amount of seconds, minutes, hours and days in the provided millis.
226
+ */
227
+ export const getSecondsMinutesHoursFromMillis = (millis: number): Timing => {
228
+ let delta = millis / 1000
229
+
230
+ const days = Math.floor(delta / 86400)
231
+ delta -= days * 86400
232
+
233
+ const hours = Math.floor(delta / 3600) % 24
234
+ delta -= hours * 3600
235
+
236
+ const minutes = Math.floor(delta / 60) % 60
237
+ delta -= minutes * 60
238
+
239
+ const seconds = Math.floor(delta) % 60
240
+
241
+ return {
242
+ seconds: seconds >= 60 ? 59 : seconds,
243
+ minutes: minutes >= 60 ? 59 : minutes,
244
+ hours: hours >= 24 ? 23 : hours,
245
+ days
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Convert milliseconds to seconds.
251
+ * @param millis <number>
252
+ * @returns <number>
253
+ */
254
+ export const convertMillisToSeconds = (millis: number): number => Number((millis / 1000).toFixed(2))
255
+
256
+ /**
257
+ * Gracefully terminate the command execution
258
+ * @params ghUsername <string> - the Github username of the user.
259
+ */
260
+ export const terminate = async (ghUsername: string) => {
261
+ console.log(`\nSee you, ${theme.text.bold(`@${getUserHandleFromProviderUserId(ghUsername)}`)} ${theme.emojis.wave}`)
262
+
263
+ process.exit(0)
264
+ }
265
+
266
+ /**
267
+ * Publish public attestation using Github Gist.
268
+ * @dev the contributor must have agreed to provide 'gist' access during the execution of the 'auth' command.
269
+ * @param accessToken <string> - the contributor access token.
270
+ * @param publicAttestation <string> - the public attestation.
271
+ * @param ceremonyTitle <string> - the ceremony title.
272
+ * @param ceremonyPrefix <string> - the ceremony prefix.
273
+ * @returns <Promise<string>> - the url where the gist has been published.
274
+ */
275
+ export const publishGist = async (
276
+ token: string,
277
+ content: string,
278
+ ceremonyTitle: string,
279
+ ceremonyPrefix: string
280
+ ): Promise<string> => {
281
+ // Make request.
282
+ const response = await request("POST /gists", {
283
+ description: `Attestation for ${ceremonyTitle} MPC Phase 2 Trusted Setup ceremony`,
284
+ public: true,
285
+ files: {
286
+ [`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`]: {
287
+ content
288
+ }
289
+ },
290
+ headers: {
291
+ authorization: `token ${token}`
292
+ }
293
+ })
294
+
295
+ if (response.status !== 201 || !response.data.html_url)
296
+ showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GIST_PUBLICATION_FAILED, true)
297
+
298
+ return response.data.html_url!
299
+ }
300
+
301
+ /**
302
+ * Generate a custom url that when clicked allows you to compose a tweet ready to be shared.
303
+ * @param ceremonyName <string> - the name of the ceremony.
304
+ * @param gistUrl <string> - the url of the gist where the public attestation has been shared.
305
+ * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
306
+ * @returns <string> - the ready to share tweet url.
307
+ */
308
+ export const generateCustomUrlToTweetAboutParticipation = (
309
+ ceremonyName: string,
310
+ gistUrl: string,
311
+ isFinalizing: boolean
312
+ ) =>
313
+ isFinalizing
314
+ ? `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`
315
+ : `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`
316
+
317
+ /**
318
+ * Return a custom progress bar.
319
+ * @param type <ProgressBarType> - the type of the progress bar.
320
+ * @param [message] <string> - additional information to be displayed when downloading/uploading.
321
+ * @returns <SingleBar> - a new custom (single) progress bar.
322
+ */
323
+ const customProgressBar = (type: ProgressBarType, message?: string): SingleBar => {
324
+ // Formats.
325
+ const uploadFormat = `${theme.emojis.arrowUp} Uploading ${message} [${theme.colors.magenta(
326
+ "{bar}"
327
+ )}] {percentage}% | {value}/{total} Chunks`
328
+
329
+ const downloadFormat = `${theme.emojis.arrowDown} Downloading ${message} [${theme.colors.magenta(
330
+ "{bar}"
331
+ )}] {percentage}% | {value}/{total} GB`
332
+
333
+ // Define a progress bar showing percentage of completion and chunks downloaded/uploaded.
334
+ return new SingleBar(
335
+ {
336
+ format: type === ProgressBarType.DOWNLOAD ? downloadFormat : uploadFormat,
337
+ hideCursor: true,
338
+ clearOnComplete: true
339
+ },
340
+ Presets.legacy
341
+ )
342
+ }
343
+
344
+ /**
345
+ * Download an artifact from the ceremony bucket.
346
+ * @dev this method request a pre-signed url to make a GET request to download the artifact.
347
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
348
+ * @param bucketName <string> - the name of the ceremony artifacts bucket (AWS S3).
349
+ * @param storagePath <string> - the storage path that locates the artifact to be downloaded in the bucket.
350
+ * @param localPath <string> - the local path where the artifact will be downloaded.
351
+ */
352
+ export const downloadCeremonyArtifact = async (
353
+ cloudFunctions: Functions,
354
+ bucketName: string,
355
+ storagePath: string,
356
+ localPath: string
357
+ ): Promise<void> => {
358
+ const spinner = customSpinner(`Preparing for downloading the contribution...`, `clock`)
359
+ spinner.start()
360
+
361
+ // Request pre-signed url to make GET download request.
362
+ const getPreSignedUrl = await generateGetObjectPreSignedUrl(cloudFunctions, bucketName, storagePath)
363
+
364
+ // Make fetch to get info about the artifact.
365
+ // @ts-ignore
366
+ const response = await fetch(getPreSignedUrl)
367
+
368
+ if (response.status !== 200 && !response.ok)
369
+ showError(CORE_SERVICES_ERRORS.AWS_CEREMONY_BUCKET_CANNOT_DOWNLOAD_GET_PRESIGNED_URL, true)
370
+
371
+ // Extract and prepare data.
372
+ const content: any = response.body
373
+ const contentLength = Number(response.headers.get("content-length"))
374
+ const contentLengthInGB = convertBytesOrKbToGb(contentLength, true)
375
+
376
+ // Prepare stream.
377
+ const writeStream = createWriteStream(localPath)
378
+ spinner.stop()
379
+
380
+ // Prepare custom progress bar.
381
+ const progressBar = customProgressBar(ProgressBarType.DOWNLOAD, `last contribution`)
382
+ const progressBarStep = contentLengthInGB / 100
383
+ let chunkLengthWritingProgress = 0
384
+ let completedProgress = progressBarStep
385
+
386
+ // Bootstrap the progress bar.
387
+ progressBar.start(contentLengthInGB < 0.01 ? 0.01 : parseFloat(contentLengthInGB.toFixed(2)).valueOf(), 0)
388
+
389
+ // Write chunk by chunk.
390
+ for await (const chunk of content) {
391
+ // Write chunk.
392
+ writeStream.write(chunk)
393
+ // Update current progress.
394
+ chunkLengthWritingProgress += convertBytesOrKbToGb(chunk.length, true)
395
+
396
+ // Display the current progress.
397
+ while (chunkLengthWritingProgress >= completedProgress) {
398
+ // Store new completed progress step by step.
399
+ completedProgress += progressBarStep
400
+
401
+ // Display accordingly in the progress bar.
402
+ progressBar.update(contentLengthInGB < 0.01 ? 0.01 : parseFloat(completedProgress.toFixed(2)).valueOf())
403
+ }
404
+ }
405
+
406
+ await sleep(2000) // workaround to show bar for small artifacts.
407
+
408
+ progressBar.stop()
409
+ }
410
+
411
+ /**
412
+ *
413
+ * @param lastZkeyLocalFilePath <string> - the local path of the last contribution.
414
+ * @param nextZkeyLocalFilePath <string> - the local path where the next contribution is going to be stored.
415
+ * @param entropyOrBeacon <string> - the entropy or beacon (only when finalizing) for the contribution.
416
+ * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
417
+ * @param averageComputingTime <number> - the current average contribution computation time.
418
+ * @param transcriptLogger <Logger> - the custom file logger to generate the contribution transcript.
419
+ * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
420
+ * @returns <Promise<number>> - the amount of time spent contributing.
421
+ */
422
+ export const handleContributionComputation = async (
423
+ lastZkeyLocalFilePath: string,
424
+ nextZkeyLocalFilePath: string,
425
+ entropyOrBeacon: string,
426
+ contributorOrCoordinatorIdentifier: string,
427
+ averageComputingTime: number,
428
+ transcriptLogger: Logger,
429
+ isFinalizing: boolean
430
+ ): Promise<number> => {
431
+ // Prepare timer (statistics only).
432
+ const computingTimer = new Timer({ label: ParticipantContributionStep.COMPUTING })
433
+ computingTimer.start()
434
+
435
+ // Time format.
436
+ const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(averageComputingTime)
437
+
438
+ const spinner = customSpinner(
439
+ `${isFinalizing ? `Applying beacon...` : `Computing contribution...`} ${
440
+ averageComputingTime > 0
441
+ ? `${theme.text.bold(
442
+ `(ETA ${theme.text.bold(
443
+ `${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(
444
+ minutes
445
+ )}:${convertToDoubleDigits(seconds)}`
446
+ )}).\n${
447
+ theme.symbols.warning
448
+ } 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`
449
+ )}`
450
+ : ``
451
+ }`,
452
+ `clock`
453
+ )
454
+ spinner.start()
455
+
456
+ // Discriminate between contribution finalization or computation.
457
+ if (isFinalizing)
458
+ await zKey.beacon(
459
+ lastZkeyLocalFilePath,
460
+ nextZkeyLocalFilePath,
461
+ contributorOrCoordinatorIdentifier,
462
+ entropyOrBeacon,
463
+ numExpIterations,
464
+ transcriptLogger
465
+ )
466
+ else
467
+ await zKey.contribute(
468
+ lastZkeyLocalFilePath,
469
+ nextZkeyLocalFilePath,
470
+ contributorOrCoordinatorIdentifier,
471
+ entropyOrBeacon,
472
+ transcriptLogger
473
+ )
474
+
475
+ computingTimer.stop()
476
+
477
+ await sleep(3000) // workaround for file descriptor.
478
+
479
+ spinner.stop()
480
+
481
+ return computingTimer.ms()
482
+ }
483
+
484
+ /**
485
+ * Return the most up-to-date data about the participant document for the given ceremony.
486
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
487
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
488
+ * @param participantId <string> - the unique identifier of the participant.
489
+ * @returns <Promise<DocumentData>> - the most up-to-date participant data.
490
+ */
491
+ export const getLatestUpdatesFromParticipant = async (
492
+ firestoreDatabase: Firestore,
493
+ ceremonyId: string,
494
+ participantId: string
495
+ ): Promise<DocumentData> => {
496
+ // Fetch participant data.
497
+ const participant = await getDocumentById(
498
+ firestoreDatabase,
499
+ getParticipantsCollectionPath(ceremonyId),
500
+ participantId
501
+ )
502
+
503
+ if (!participant.data()) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true)
504
+
505
+ return participant.data()!
506
+ }
507
+
508
+ /**
509
+ * Start or resume a contribution from the last participant contribution step.
510
+ * @notice this method goes through each contribution stage following this order:
511
+ * 1) Downloads the last contribution from previous contributor.
512
+ * 2) Computes the new contribution.
513
+ * 3) Uploads the new contribution.
514
+ * 4) Requests the verification of the new contribution to the coordinator's backend and waits for the result.
515
+ * @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
516
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
517
+ * @param ceremony <FirebaseDocumentInfo> - the Firestore document of the ceremony.
518
+ * @param circuit <FirebaseDocumentInfo> - the Firestore document of the ceremony circuit.
519
+ * @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (contributor or coordinator).
520
+ * @param participantContributionStep <ParticipantContributionStep> - the contribution step of the participant (from where to start/resume contribution).
521
+ * @param entropyOrBeaconHash <string> - the entropy or beacon hash (only when finalizing) for the contribution.
522
+ * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
523
+ * @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
524
+ */
525
+ export const handleStartOrResumeContribution = async (
526
+ cloudFunctions: Functions,
527
+ firestoreDatabase: Firestore,
528
+ ceremony: FirebaseDocumentInfo,
529
+ circuit: FirebaseDocumentInfo,
530
+ participant: FirebaseDocumentInfo,
531
+ entropyOrBeaconHash: any,
532
+ contributorOrCoordinatorIdentifier: string,
533
+ isFinalizing: boolean
534
+ ): Promise<void> => {
535
+ // Extract data.
536
+ const { prefix: ceremonyPrefix } = ceremony.data
537
+ const { waitingQueue, avgTimings, prefix: circuitPrefix, sequencePosition } = circuit.data
538
+ const { completedContributions } = waitingQueue // = current progress.
539
+
540
+ console.log(
541
+ `${theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}`)}`)} (Contribution Steps)`
542
+ )
543
+
544
+ // Get most up-to-date data from the participant document.
545
+ let participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
546
+
547
+ const spinner = customSpinner(
548
+ `${
549
+ participantData.contributionStep === ParticipantContributionStep.DOWNLOADING
550
+ ? `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.`
551
+ : `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.`
552
+ }`,
553
+ `clock`
554
+ )
555
+ spinner.start()
556
+
557
+ // Compute zkey indexes.
558
+ const lastZkeyIndex = formatZkeyIndex(completedContributions)
559
+ const nextZkeyIndex = formatZkeyIndex(completedContributions + 1)
560
+
561
+ // Prepare zKey filenames.
562
+ const lastZkeyCompleteFilename = `${circuitPrefix}_${lastZkeyIndex}.zkey`
563
+ const nextZkeyCompleteFilename = isFinalizing
564
+ ? `${circuitPrefix}_${finalContributionIndex}.zkey`
565
+ : `${circuitPrefix}_${nextZkeyIndex}.zkey`
566
+ // Prepare zKey storage paths.
567
+ const lastZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, lastZkeyCompleteFilename)
568
+ const nextZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, nextZkeyCompleteFilename)
569
+ // Prepare zKey local paths.
570
+ const lastZkeyLocalFilePath = isFinalizing
571
+ ? getFinalZkeyLocalFilePath(lastZkeyCompleteFilename)
572
+ : getContributionLocalFilePath(lastZkeyCompleteFilename)
573
+ const nextZkeyLocalFilePath = isFinalizing
574
+ ? getFinalZkeyLocalFilePath(nextZkeyCompleteFilename)
575
+ : getContributionLocalFilePath(nextZkeyCompleteFilename)
576
+
577
+ // Generate a custom file logger for contribution transcript.
578
+ const transcriptCompleteFilename = isFinalizing
579
+ ? `${circuit.data.prefix}_${contributorOrCoordinatorIdentifier}_${finalContributionIndex}.log`
580
+ : `${circuit.data.prefix}_${nextZkeyIndex}.log`
581
+ const transcriptLocalFilePath = isFinalizing
582
+ ? getFinalTranscriptLocalFilePath(transcriptCompleteFilename)
583
+ : getTranscriptLocalFilePath(transcriptCompleteFilename)
584
+ const transcriptLogger = createCustomLoggerForFile(transcriptLocalFilePath)
585
+
586
+ // Populate transcript file w/ header.
587
+ transcriptLogger.info(
588
+ `${isFinalizing ? `Final` : `Contribution`} transcript for ${circuitPrefix} phase 2 contribution.\n${
589
+ isFinalizing
590
+ ? `Coordinator: ${contributorOrCoordinatorIdentifier}`
591
+ : `Contributor # ${Number(nextZkeyIndex)}`
592
+ } (${contributorOrCoordinatorIdentifier})\n`
593
+ )
594
+
595
+ // Get ceremony bucket name.
596
+ const bucketName = getBucketName(ceremonyPrefix, String(process.env.CONFIG_CEREMONY_BUCKET_POSTFIX))
597
+
598
+ await sleep(3000) // ~3s.
599
+ spinner.stop()
600
+
601
+ // Contribution step = DOWNLOADING.
602
+ if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.DOWNLOADING) {
603
+ // Download the latest contribution from bucket.
604
+ await downloadCeremonyArtifact(cloudFunctions, bucketName, lastZkeyStorageFilePath, lastZkeyLocalFilePath)
605
+
606
+ console.log(
607
+ `${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} correctly downloaded`
608
+ )
609
+
610
+ // Advance to next contribution step (COMPUTING) if not finalizing.
611
+ if (!isFinalizing) {
612
+ spinner.text = `Preparing for contribution computation...`
613
+ spinner.start()
614
+
615
+ await progressToNextContributionStep(cloudFunctions, ceremony.id)
616
+
617
+ await sleep(1000)
618
+ // Refresh most up-to-date data from the participant document.
619
+ participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
620
+
621
+ spinner.stop()
622
+ }
623
+ } else
624
+ console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} already downloaded`)
625
+
626
+ // Contribution step = COMPUTING.
627
+ if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.COMPUTING) {
628
+ // Handle the next contribution computation.
629
+ const computingTime = await handleContributionComputation(
630
+ lastZkeyLocalFilePath,
631
+ nextZkeyLocalFilePath,
632
+ entropyOrBeaconHash,
633
+ contributorOrCoordinatorIdentifier,
634
+ avgTimings.contributionComputation,
635
+ transcriptLogger,
636
+ isFinalizing
637
+ )
638
+
639
+ // Permanently store on db the contribution hash and computing time.
640
+ spinner.text = `Writing contribution metadata...`
641
+ spinner.start()
642
+
643
+ // Read local transcript file info to get the contribution hash.
644
+ const transcriptContents = readFile(transcriptLocalFilePath)
645
+ const matchContributionHash = transcriptContents.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/)
646
+
647
+ if (!matchContributionHash)
648
+ showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true)
649
+
650
+ // Format contribution hash.
651
+ const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "")!
652
+
653
+ // Make request to cloud functions to permanently store the information.
654
+ await permanentlyStoreCurrentContributionTimeAndHash(
655
+ cloudFunctions,
656
+ ceremony.id,
657
+ computingTime,
658
+ contributionHash
659
+ )
660
+
661
+ // Format computing time.
662
+ const {
663
+ seconds: computationSeconds,
664
+ minutes: computationMinutes,
665
+ hours: computationHours
666
+ } = getSecondsMinutesHoursFromMillis(computingTime)
667
+
668
+ spinner.succeed(
669
+ `${
670
+ isFinalizing ? "Contribution" : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`
671
+ } computation took ${theme.text.bold(
672
+ `${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits(
673
+ computationMinutes
674
+ )}:${convertToDoubleDigits(computationSeconds)}`
675
+ )}`
676
+ )
677
+
678
+ // Advance to next contribution step (UPLOADING) if not finalizing.
679
+ if (!isFinalizing) {
680
+ spinner.text = `Preparing for uploading the contribution...`
681
+ spinner.start()
682
+
683
+ await progressToNextContributionStep(cloudFunctions, ceremony.id)
684
+ await sleep(1000)
685
+
686
+ // Refresh most up-to-date data from the participant document.
687
+ participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
688
+
689
+ spinner.stop()
690
+ }
691
+ } else console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} already computed`)
692
+
693
+ // Contribution step = UPLOADING.
694
+ if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.UPLOADING) {
695
+ spinner.text = `Uploading ${isFinalizing ? "final" : "your"} contribution ${
696
+ !isFinalizing ? theme.text.bold(`#${nextZkeyIndex}`) : ""
697
+ } to storage.\n${
698
+ theme.symbols.warning
699
+ } This step may take a while based on circuit size and your contribution speed. Everything's fine, just be patient.`
700
+ spinner.start()
701
+
702
+ if (!isFinalizing)
703
+ await multiPartUpload(
704
+ cloudFunctions,
705
+ bucketName,
706
+ nextZkeyStorageFilePath,
707
+ nextZkeyLocalFilePath,
708
+ Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB),
709
+ ceremony.id,
710
+ participantData.tempContributionData
711
+ )
712
+ else
713
+ await multiPartUpload(
714
+ cloudFunctions,
715
+ bucketName,
716
+ nextZkeyStorageFilePath,
717
+ nextZkeyLocalFilePath,
718
+ Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB)
719
+ )
720
+
721
+ spinner.succeed(
722
+ `${
723
+ isFinalizing ? `Contribution` : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`
724
+ } correctly saved to storage`
725
+ )
726
+
727
+ // Advance to next contribution step (VERIFYING) if not finalizing.
728
+ if (!isFinalizing) {
729
+ spinner.text = `Preparing for requesting contribution verification...`
730
+ spinner.start()
731
+
732
+ await progressToNextContributionStep(cloudFunctions, ceremony.id)
733
+ await sleep(1000)
734
+
735
+ // Refresh most up-to-date data from the participant document.
736
+ participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
737
+ spinner.stop()
738
+ }
739
+ }
740
+
741
+ // Contribution step = VERIFYING.
742
+ if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.VERIFYING) {
743
+ // Format verification time.
744
+ const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(avgTimings.verifyCloudFunction)
745
+
746
+ process.stdout.write(
747
+ `${theme.symbols.info} Your contribution is under verification ${
748
+ avgTimings.verifyCloudFunction > 0
749
+ ? `(~ ${theme.text.bold(
750
+ `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(
751
+ seconds
752
+ )}`
753
+ )})\n${
754
+ theme.symbols.warning
755
+ } This step can take up to one hour based on circuit size. Everything's fine, just be patient.`
756
+ : ``
757
+ }`
758
+ )
759
+
760
+ try {
761
+ // Execute contribution verification.
762
+ await verifyContribution(
763
+ cloudFunctions,
764
+ ceremony.id,
765
+ circuit,
766
+ bucketName,
767
+ contributorOrCoordinatorIdentifier,
768
+ String(process.env.FIREBASE_CF_URL_VERIFY_CONTRIBUTION)
769
+ )
770
+ } catch (error: any) {
771
+ process.stdout.write(
772
+ `\n${theme.symbols.error} ${theme.text.bold(
773
+ "Unfortunately there was an error with the contribution verification. Please restart phase2cli and try again. If the problem persists, please contact the ceremony coordinator."
774
+ )}\n`
775
+ )
776
+ }
777
+ }
778
+ }