@devtion/devcli 0.0.0-7e983e3

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/.env +41 -0
  4. package/dist/index.js +3206 -0
  5. package/dist/types/commands/auth.d.ts +25 -0
  6. package/dist/types/commands/clean.d.ts +6 -0
  7. package/dist/types/commands/contribute.d.ts +139 -0
  8. package/dist/types/commands/finalize.d.ts +51 -0
  9. package/dist/types/commands/index.d.ts +9 -0
  10. package/dist/types/commands/listCeremonies.d.ts +5 -0
  11. package/dist/types/commands/logout.d.ts +6 -0
  12. package/dist/types/commands/observe.d.ts +22 -0
  13. package/dist/types/commands/setup.d.ts +86 -0
  14. package/dist/types/commands/validate.d.ts +8 -0
  15. package/dist/types/index.d.ts +2 -0
  16. package/dist/types/lib/errors.d.ts +60 -0
  17. package/dist/types/lib/files.d.ts +64 -0
  18. package/dist/types/lib/localConfigs.d.ts +110 -0
  19. package/dist/types/lib/prompts.d.ts +104 -0
  20. package/dist/types/lib/services.d.ts +31 -0
  21. package/dist/types/lib/theme.d.ts +42 -0
  22. package/dist/types/lib/utils.d.ts +158 -0
  23. package/dist/types/types/index.d.ts +65 -0
  24. package/package.json +100 -0
  25. package/src/commands/auth.ts +194 -0
  26. package/src/commands/clean.ts +49 -0
  27. package/src/commands/contribute.ts +1090 -0
  28. package/src/commands/finalize.ts +382 -0
  29. package/src/commands/index.ts +9 -0
  30. package/src/commands/listCeremonies.ts +32 -0
  31. package/src/commands/logout.ts +67 -0
  32. package/src/commands/observe.ts +193 -0
  33. package/src/commands/setup.ts +901 -0
  34. package/src/commands/validate.ts +29 -0
  35. package/src/index.ts +66 -0
  36. package/src/lib/errors.ts +77 -0
  37. package/src/lib/files.ts +102 -0
  38. package/src/lib/localConfigs.ts +186 -0
  39. package/src/lib/prompts.ts +748 -0
  40. package/src/lib/services.ts +191 -0
  41. package/src/lib/theme.ts +45 -0
  42. package/src/lib/utils.ts +778 -0
  43. package/src/types/conf.d.ts +16 -0
  44. package/src/types/index.ts +70 -0
@@ -0,0 +1,901 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { zKey } from "snarkjs"
4
+ import boxen from "boxen"
5
+ import { createWriteStream, Dirent, renameSync } from "fs"
6
+ import { pipeline } from "node:stream"
7
+ import { promisify } from "node:util"
8
+ import fetch from "node-fetch"
9
+ import { Functions } from "firebase/functions"
10
+ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
11
+ import {
12
+ CeremonyTimeoutType,
13
+ CircomCompilerData,
14
+ CircuitInputData,
15
+ extractPrefix,
16
+ getR1CSInfo,
17
+ commonTerms,
18
+ convertToDoubleDigits,
19
+ CeremonyInputData,
20
+ CircuitDocument,
21
+ extractPoTFromFilename,
22
+ potFileDownloadMainUrl,
23
+ potFilenameTemplate,
24
+ getBucketName,
25
+ createS3Bucket,
26
+ multiPartUpload,
27
+ isCoordinator,
28
+ genesisZkeyIndex,
29
+ getR1csStorageFilePath,
30
+ getWasmStorageFilePath,
31
+ getPotStorageFilePath,
32
+ getZkeyStorageFilePath,
33
+ checkIfObjectExist,
34
+ blake512FromPath,
35
+ CircuitArtifacts,
36
+ CircuitTimings,
37
+ setupCeremony,
38
+ parseCeremonyFile,
39
+ CircuitContributionVerificationMechanism
40
+ } from "@p0tion/actions"
41
+ import { customSpinner, simpleLoader, sleep, terminate } from "../lib/utils.js"
42
+ import {
43
+ promptCeremonyInputData,
44
+ promptCircomCompiler,
45
+ promptCircuitInputData,
46
+ askForConfirmation,
47
+ promptCircuitSelector,
48
+ promptSameCircomCompiler,
49
+ promptCircuitAddition,
50
+ promptPreComputedZkey,
51
+ promptPreComputedZkeySelector,
52
+ promptNeededPowersForCircuit,
53
+ promptPotSelector
54
+ } from "../lib/prompts.js"
55
+ import { COMMAND_ERRORS, showError } from "../lib/errors.js"
56
+ import { authWithToken, bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
57
+ import { getCWDFilePath, getPotLocalFilePath, getZkeyLocalFilePath, localPaths } from "../lib/localConfigs.js"
58
+ import theme from "../lib/theme.js"
59
+ import {
60
+ filterDirectoryFilesByExtension,
61
+ cleanDir,
62
+ getDirFilesSubPaths,
63
+ getFileStats,
64
+ checkAndMakeNewDirectoryIfNonexistent
65
+ } from "../lib/files.js"
66
+ import { Readable } from "stream"
67
+
68
+ /**
69
+ * Handle whatever is needed to obtain the input data for a circuit that the coordinator would like to add to the ceremony.
70
+ * @param choosenCircuitFilename <string> - the name of the circuit to add.
71
+ * @param matchingWasmFilename <string> - the name of the circuit wasm file.
72
+ * @param ceremonyTimeoutMechanismType <CeremonyTimeoutType> - the type of ceremony timeout mechanism.
73
+ * @param sameCircomCompiler <boolean> - true, if this circuit shares with the others the <CircomCompilerData>; otherwise false.
74
+ * @param circuitSequencePosition <number> - the position of the circuit in the contribution queue.
75
+ * @param sharedCircomCompilerData <string> - version and commit hash of the Circom compiler used to compile the ceremony circuits.
76
+ * @returns <Promise<CircuitInputData>> - the input data of the circuit to add to the ceremony.
77
+ */
78
+ export const getInputDataToAddCircuitToCeremony = async (
79
+ choosenCircuitFilename: string,
80
+ matchingWasmFilename: string,
81
+ ceremonyTimeoutMechanismType: CeremonyTimeoutType,
82
+ sameCircomCompiler: boolean,
83
+ circuitSequencePosition: number,
84
+ sharedCircomCompilerData: CircomCompilerData
85
+ ): Promise<CircuitInputData> => {
86
+ // Extract name and prefix.
87
+ const circuitName = choosenCircuitFilename.substring(0, choosenCircuitFilename.indexOf("."))
88
+ const circuitPrefix = extractPrefix(circuitName)
89
+
90
+ // R1CS file path.
91
+ const r1csCWDFilePath = getCWDFilePath(process.cwd(), choosenCircuitFilename)
92
+
93
+ const spinner = customSpinner(`Looking for circuit metadata...`, "clock")
94
+ spinner.start()
95
+
96
+ // Read R1CS and store metadata locally.
97
+ const metadata = getR1CSInfo(r1csCWDFilePath)
98
+
99
+ await sleep(2000) // Sleep 2s to avoid unexpected termination (file descriptor close).
100
+
101
+ spinner.succeed(`Circuit metadata read and saved correctly`)
102
+
103
+ // Prompt for circuit input data.
104
+ const circuitInputData = await promptCircuitInputData(
105
+ metadata.constraints,
106
+ ceremonyTimeoutMechanismType,
107
+ sameCircomCompiler,
108
+ !(metadata.constraints <= 1000000) // nb. we assume after our dry-runs that CF works fine for up to one million circuit constraints.
109
+ )
110
+
111
+ process.stdout.write("\n")
112
+
113
+ // Return updated data.
114
+ return {
115
+ ...circuitInputData,
116
+ metadata,
117
+ compiler: {
118
+ commitHash:
119
+ !circuitInputData.compiler.commitHash && sameCircomCompiler
120
+ ? sharedCircomCompilerData.commitHash
121
+ : circuitInputData.compiler.commitHash,
122
+ version:
123
+ !circuitInputData.compiler.version && sameCircomCompiler
124
+ ? sharedCircomCompilerData.version
125
+ : circuitInputData.compiler.version
126
+ },
127
+ compilationArtifacts: {
128
+ r1csFilename: choosenCircuitFilename,
129
+ wasmFilename: matchingWasmFilename
130
+ },
131
+ name: circuitName,
132
+ prefix: circuitPrefix,
133
+ sequencePosition: circuitSequencePosition
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Handle the addition of one or more circuits to the ceremony.
139
+ * @param options <Array<string>> - list of possible circuits that can be added to the ceremony.
140
+ * @param ceremonyTimeoutMechanismType <CeremonyTimeoutType> - the type of ceremony timeout mechanism.
141
+ * @returns <Promise<Array<CircuitInputData>>> - the input data for each circuit that has been added to the ceremony.
142
+ */
143
+ export const handleAdditionOfCircuitsToCeremony = async (
144
+ r1csOptions: Array<string>,
145
+ wasmOptions: Array<string>,
146
+ ceremonyTimeoutMechanismType: CeremonyTimeoutType
147
+ ): Promise<Array<CircuitInputData>> => {
148
+ // Prepare data.
149
+ const inputDataForCircuits: Array<CircuitInputData> = [] // All circuits interactive data.
150
+ let circuitSequencePosition = 1 // The circuit's position for contribution.
151
+ let readyToSummarizeCeremony = false // Boolean flag to check whether the coordinator has finished to add circuits to the ceremony.
152
+ let wannaAddAnotherCircuit = true // Loop flag.
153
+ const sharedCircomCompilerData: CircomCompilerData = { version: "", commitHash: "" }
154
+
155
+ // Prompt if the circuits to be added were compiled with the same version of Circom.
156
+ // nb. CIRCOM compiler version/commit-hash is a declaration useful for later verifiability and avoid bugs.
157
+ const sameCircomCompiler = await promptSameCircomCompiler()
158
+
159
+ if (sameCircomCompiler) {
160
+ // Prompt for Circom compiler.
161
+ const { version, commitHash } = await promptCircomCompiler()
162
+
163
+ sharedCircomCompilerData.version = version
164
+ sharedCircomCompilerData.commitHash = commitHash
165
+ }
166
+
167
+ while (wannaAddAnotherCircuit) {
168
+ // Gather information about the ceremony circuits.
169
+ console.log(theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${circuitSequencePosition}`)}\n`))
170
+
171
+ // Select one circuit among cwd circuits identified by R1CS files.
172
+ const choosenCircuitFilename = await promptCircuitSelector(r1csOptions)
173
+
174
+ // Update list of possible options for next selection (if, any).
175
+ r1csOptions = r1csOptions.filter((circuitFilename: string) => circuitFilename !== choosenCircuitFilename)
176
+
177
+ // Select the wasm file accordingly to circuit R1CS filename.
178
+ const matchingWasms = wasmOptions.filter(
179
+ (wasmFilename: string) =>
180
+ choosenCircuitFilename.split(`.r1cs`)[0] ===
181
+ wasmFilename.split(`.${commonTerms.foldersAndPathsTerms.wasm}`)[0]
182
+ )
183
+
184
+ if (matchingWasms.length !== 1) showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true)
185
+
186
+ // Get input data for choosen circuit.
187
+ const circuitInputData = await getInputDataToAddCircuitToCeremony(
188
+ choosenCircuitFilename,
189
+ matchingWasms[0],
190
+ ceremonyTimeoutMechanismType,
191
+ sameCircomCompiler,
192
+ circuitSequencePosition,
193
+ sharedCircomCompilerData
194
+ )
195
+
196
+ // Store circuit data.
197
+ inputDataForCircuits.push(circuitInputData)
198
+
199
+ // Check if any circuit is left for potentially addition to ceremony.
200
+ if (r1csOptions.length !== 0) {
201
+ // Prompt for selection.
202
+ const wannaAddNewCircuit = await promptCircuitAddition()
203
+
204
+ if (wannaAddNewCircuit === false) readyToSummarizeCeremony = true // Terminate circuit addition.
205
+ else circuitSequencePosition += 1 // Continue with next one.
206
+ } else readyToSummarizeCeremony = true // No more circuit to add.
207
+
208
+ // Summarize the ceremony.
209
+ if (readyToSummarizeCeremony) wannaAddAnotherCircuit = false
210
+ }
211
+
212
+ return inputDataForCircuits
213
+ }
214
+
215
+ /**
216
+ * Print ceremony and related circuits information.
217
+ * @param ceremonyInputData <CeremonyInputData> - the input data of the ceremony.
218
+ * @param circuits <Array<CircuitDocument>> - the circuit documents associated to the circuits of the ceremony.
219
+ */
220
+ export const displayCeremonySummary = (ceremonyInputData: CeremonyInputData, circuits: Array<CircuitDocument>) => {
221
+ // Prepare ceremony summary.
222
+ let summary = `${`${theme.text.bold(ceremonyInputData.title)}\n${theme.text.italic(ceremonyInputData.description)}`}
223
+ \n${`Opening: ${theme.text.bold(
224
+ theme.text.underlined(new Date(ceremonyInputData.startDate).toUTCString().replace("GMT", "UTC"))
225
+ )}\nEnding: ${theme.text.bold(
226
+ theme.text.underlined(new Date(ceremonyInputData.endDate).toUTCString().replace("GMT", "UTC"))
227
+ )}`}
228
+ \n${theme.text.bold(
229
+ ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC ? `Dynamic` : `Fixed`
230
+ )} Timeout / ${theme.text.bold(ceremonyInputData.penalty)}m Penalty`
231
+
232
+ for (const circuit of circuits) {
233
+ // Append circuit summary.
234
+ summary += `\n\n${theme.text.bold(
235
+ `- CIRCUIT # ${theme.text.bold(theme.colors.magenta(`${circuit.sequencePosition}`))}`
236
+ )}
237
+ \n${`${theme.text.bold(circuit.name)}\n${theme.text.italic(circuit.description)}
238
+ \nCurve: ${theme.text.bold(circuit.metadata?.curve)}\nCompiler: ${theme.text.bold(
239
+ `${circuit.compiler.version}`
240
+ )} (${theme.text.bold(circuit.compiler.commitHash.slice(0, 7))})\nVerification: ${theme.text.bold(
241
+ `${circuit.verification.cfOrVm}`
242
+ )} ${theme.text.bold(
243
+ circuit.verification.cfOrVm === CircuitContributionVerificationMechanism.VM
244
+ ? `(${circuit.verification.vm.vmConfigurationType} / ${circuit.verification.vm.vmDiskType} volume)`
245
+ : ""
246
+ )}\nSource: ${theme.text.bold(circuit.template.source.split(`/`).at(-1))}(${theme.text.bold(
247
+ circuit.template.paramsConfiguration
248
+ )})\n${
249
+ ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC
250
+ ? `Threshold: ${theme.text.bold(circuit.dynamicThreshold)}%`
251
+ : `Max Contribution Time: ${theme.text.bold(circuit.fixedTimeWindow)}m`
252
+ }
253
+ \n# Wires: ${theme.text.bold(circuit.metadata?.wires)}\n# Constraints: ${theme.text.bold(
254
+ circuit.metadata?.constraints
255
+ )}\n# Private Inputs: ${theme.text.bold(circuit.metadata?.privateInputs)}\n# Public Inputs: ${theme.text.bold(
256
+ circuit.metadata?.publicInputs
257
+ )}\n# Labels: ${theme.text.bold(circuit.metadata?.labels)}\n# Outputs: ${theme.text.bold(
258
+ circuit.metadata?.outputs
259
+ )}\n# PoT: ${theme.text.bold(circuit.metadata?.pot)}`}`
260
+ }
261
+
262
+ // Display complete summary.
263
+ console.log(
264
+ boxen(summary, {
265
+ title: theme.colors.magenta(`CEREMONY SUMMARY`),
266
+ titleAlignment: "center",
267
+ textAlignment: "left",
268
+ margin: 1,
269
+ padding: 1
270
+ })
271
+ )
272
+ }
273
+
274
+ /**
275
+ * Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
276
+ * @dev we are downloading the Powers of Tau file from Hermez Cryptography Phase 1 Trusted Setup.
277
+ * @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
278
+ * @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
279
+ * @returns <Promise<void>>
280
+ */
281
+ export const checkAndDownloadSmallestPowersOfTau = async (
282
+ powers: string,
283
+ ptauCompleteFilename: string
284
+ ): Promise<void> => {
285
+ // Get already downloaded ptau files.
286
+ const alreadyDownloadedPtauFiles = await getDirFilesSubPaths(localPaths.pot)
287
+
288
+ // Get the required smallest ptau file.
289
+ const smallestPtauFileForGivenPowers: Array<string> = alreadyDownloadedPtauFiles
290
+ .filter((dirent: Dirent) => extractPoTFromFilename(dirent.name) === Number(powers))
291
+ .map((dirent: Dirent) => dirent.name)
292
+
293
+ // Check if already downloaded or not.
294
+ if (smallestPtauFileForGivenPowers.length === 0) {
295
+ const spinner = customSpinner(
296
+ `Downloading the ${theme.text.bold(
297
+ `#${powers}`
298
+ )} smallest PoT file needed from the Hermez Cryptography Phase 1 Trusted Setup...`,
299
+ `clock`
300
+ )
301
+ spinner.start()
302
+
303
+ // Download smallest Powers of Tau file from remote server.
304
+ const streamPipeline = promisify(pipeline)
305
+
306
+ // Make the call.
307
+ const response = await fetch(`${potFileDownloadMainUrl}${ptauCompleteFilename}`)
308
+
309
+ // Handle errors.
310
+ if (!response.ok && response.status !== 200) showError(COMMAND_ERRORS.COMMAND_SETUP_DOWNLOAD_PTAU, true)
311
+ // Write the file locally
312
+ else await streamPipeline(response.body!, createWriteStream(getPotLocalFilePath(ptauCompleteFilename)))
313
+
314
+ spinner.succeed(`Powers of tau ${theme.text.bold(`#${powers}`)} downloaded successfully`)
315
+ } else
316
+ console.log(
317
+ `${theme.symbols.success} Smallest Powers of Tau ${theme.text.bold(`#${powers}`)} already downloaded`
318
+ )
319
+ }
320
+
321
+ /**
322
+ * Handle the needs in terms of Powers of Tau for the selected pre-computed zKey.
323
+ * @notice in case there are no Powers of Tau file suitable for the pre-computed zKey (i.e., having a
324
+ * number of powers greater than or equal to the powers needed by the zKey), the coordinator will be asked
325
+ * to provide a number of powers manually, ranging from the smallest possible to the largest.
326
+ * @param neededPowers <number> - the smallest amount of powers needed by the zKey.
327
+ * @returns Promise<string, string> - the information about the choosen Powers of Tau file for the pre-computed zKey
328
+ * along with related powers.
329
+ */
330
+ export const handlePreComputedZkeyPowersOfTauSelection = async (
331
+ neededPowers: number
332
+ ): Promise<{
333
+ doubleDigitsPowers: string
334
+ potCompleteFilename: string
335
+ usePreDownloadedPoT: boolean
336
+ }> => {
337
+ let doubleDigitsPowers: string = "" // The amount of stringified powers in a double-digits format (XY).
338
+ let potCompleteFilename: string = "" // The complete filename of the Powers of Tau file selected for the pre-computed zKey.
339
+ let usePreDownloadedPoT = false // Boolean flag to check if the coordinator is going to use a pre-downloaded PoT file or not.
340
+
341
+ // Check for PoT file associated to selected pre-computed zKey.
342
+ const spinner = customSpinner("Looking for Powers of Tau files...", "clock")
343
+ spinner.start()
344
+
345
+ // Get local `.ptau` files.
346
+ const potFilePaths = await filterDirectoryFilesByExtension(process.cwd(), `.ptau`)
347
+
348
+ // Filter based on suitable amount of powers.
349
+ const potOptions: Array<string> = potFilePaths
350
+ .filter((dirent: Dirent) => extractPoTFromFilename(dirent.name) >= neededPowers)
351
+ .map((dirent: Dirent) => dirent.name)
352
+
353
+ if (potOptions.length <= 0) {
354
+ spinner.warn(`There is no already downloaded Powers of Tau file suitable for this zKey`)
355
+
356
+ // Ask coordinator to input the amount of powers.
357
+ const choosenPowers = await promptNeededPowersForCircuit(neededPowers)
358
+
359
+ // Convert to double digits powers (e.g., 9 -> 09).
360
+ doubleDigitsPowers = convertToDoubleDigits(choosenPowers)
361
+ potCompleteFilename = `${potFilenameTemplate}${doubleDigitsPowers}.ptau`
362
+ } else {
363
+ spinner.stop()
364
+
365
+ // Prompt for Powers of Tau selection among already downloaded ones.
366
+ potCompleteFilename = await promptPotSelector(potOptions)
367
+
368
+ // Convert to double digits powers (e.g., 9 -> 09).
369
+ doubleDigitsPowers = convertToDoubleDigits(extractPoTFromFilename(potCompleteFilename))
370
+
371
+ usePreDownloadedPoT = true
372
+ }
373
+
374
+ return {
375
+ doubleDigitsPowers,
376
+ potCompleteFilename,
377
+ usePreDownloadedPoT
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Generate a brand new zKey from scratch.
383
+ * @param r1csLocalPathAndFileName <string> - the local complete path of the R1CS selected file.
384
+ * @param potLocalPathAndFileName <string> - the local complete path of the PoT selected file.
385
+ * @param zkeyLocalPathAndFileName <string> - the local complete path of the pre-computed zKey selected file.
386
+ */
387
+ export const handleNewZkeyGeneration = async (
388
+ r1csLocalPathAndFileName: string,
389
+ potLocalPathAndFileName: string,
390
+ zkeyLocalPathAndFileName: string
391
+ ) => {
392
+ console.log(
393
+ `${theme.symbols.info} The computation of your brand new zKey is starting soon.\n${theme.text.bold(
394
+ `${theme.symbols.warning} Be careful, stopping the process will result in the loss of all progress achieved so far.`
395
+ )}`
396
+ )
397
+
398
+ // Generate zKey.
399
+ await zKey.newZKey(r1csLocalPathAndFileName, potLocalPathAndFileName, zkeyLocalPathAndFileName, console)
400
+
401
+ console.log(`\n${theme.symbols.success} Generation of genesis zKey completed successfully`)
402
+ }
403
+
404
+ /**
405
+ * Manage the creation of a ceremony file storage bucket.
406
+ * @param firebaseFunctions <Functions> - the Firebase Cloud Functions instance connected to the current application.
407
+ * @param ceremonyPrefix <string> - the prefix of the ceremony.
408
+ * @returns <Promise<string>> - the ceremony bucket name.
409
+ */
410
+ export const handleCeremonyBucketCreation = async (
411
+ firebaseFunctions: Functions,
412
+ ceremonyPrefix: string
413
+ ): Promise<string> => {
414
+ // Compose bucket name using the ceremony prefix.
415
+ const bucketName = getBucketName(ceremonyPrefix, process.env.CONFIG_CEREMONY_BUCKET_POSTFIX!)
416
+
417
+ const spinner = customSpinner(`Getting ready for ceremony files and data storage...`, `clock`)
418
+ spinner.start()
419
+
420
+ try {
421
+ // Make the call to create the bucket.
422
+ await createS3Bucket(firebaseFunctions, bucketName)
423
+ } catch (error: any) {
424
+ const errorBody = JSON.parse(JSON.stringify(error))
425
+ showError(`[${errorBody.code}] ${error.message} ${!errorBody.details ? "" : `\n${errorBody.details}`}`, true)
426
+ }
427
+
428
+ spinner.succeed(`Ceremony bucket has been successfully created`)
429
+
430
+ return bucketName
431
+ }
432
+
433
+ /**
434
+ * Upload a circuit artifact (r1cs, WASM, ptau) to the ceremony storage.
435
+ * @dev this method uses a multi part upload to upload the file in chunks.
436
+ * @param firebaseFunctions <Functions> - the Firebase Cloud Functions instance connected to the current application.
437
+ * @param bucketName <string> - the ceremony bucket name.
438
+ * @param storageFilePath <string> - the storage (bucket) path where the file should be uploaded.
439
+ * @param localPathAndFileName <string> - the local file path where is located.
440
+ * @param completeFilename <string> - the complete filename.
441
+ */
442
+ export const handleCircuitArtifactUploadToStorage = async (
443
+ firebaseFunctions: Functions,
444
+ bucketName: string,
445
+ storageFilePath: string,
446
+ localPathAndFileName: string,
447
+ completeFilename: string
448
+ ) => {
449
+ const spinner = customSpinner(`Uploading ${theme.text.bold(completeFilename)} file to ceremony storage...`, `clock`)
450
+ spinner.start()
451
+
452
+ await multiPartUpload(
453
+ firebaseFunctions,
454
+ bucketName,
455
+ storageFilePath,
456
+ localPathAndFileName,
457
+ Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB)
458
+ )
459
+
460
+ spinner.succeed(`Upload of (${theme.text.bold(completeFilename)}) file completed successfully`)
461
+ }
462
+
463
+ /**
464
+ * Setup command.
465
+ * @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
466
+ * @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
467
+ * for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
468
+ * from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
469
+ * @param cmd? <any> - the path to the ceremony setup file.
470
+ */
471
+ const setup = async (cmd: { template?: string, auth?: string}) => {
472
+ // Setup command state.
473
+ const circuits: Array<CircuitDocument> = [] // Circuits.
474
+ let ceremonyId: string = "" // The unique identifier of the ceremony.
475
+
476
+ const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices()
477
+
478
+ // Check for authentication.
479
+ const { user, providerUserId } = cmd.auth ? await authWithToken(firebaseApp, cmd.auth) : await checkAuth(firebaseApp)
480
+
481
+ // Preserve command execution only for coordinators.
482
+ if (!(await isCoordinator(user))) showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true)
483
+
484
+ // Get current working directory.
485
+ const cwd = process.cwd()
486
+
487
+ console.log(
488
+ `${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`
489
+ )
490
+ console.log(
491
+ `\n${theme.symbols.info} Your current working directory is ${theme.text.bold(
492
+ theme.text.underlined(process.cwd())
493
+ )}\n`
494
+ )
495
+
496
+ // Prepare local directories.
497
+ checkAndMakeNewDirectoryIfNonexistent(localPaths.output)
498
+ cleanDir(localPaths.setup)
499
+ cleanDir(localPaths.pot)
500
+ cleanDir(localPaths.zkeys)
501
+ cleanDir(localPaths.wasm)
502
+
503
+ // if there is the file option, then set up the non interactively
504
+ if (cmd.template) {
505
+ // 1. parse the file
506
+ // tmp data - do not cleanup files as we need them
507
+ const spinner = customSpinner(`Parsing ${theme.text.bold(cmd.template!)} setup configuration file...`, `clock`)
508
+ spinner.start()
509
+ const setupCeremonyData = await parseCeremonyFile(cmd.template!)
510
+ spinner.succeed(`Parsing of ${theme.text.bold(cmd.template!)} setup configuration file completed successfully`)
511
+
512
+ // final setup data
513
+ const ceremonySetupData = setupCeremonyData
514
+
515
+ // create a new bucket
516
+ const bucketName = await handleCeremonyBucketCreation(firebaseFunctions, ceremonySetupData.ceremonyPrefix)
517
+ console.log(`\n${theme.symbols.success} Ceremony bucket name: ${theme.text.bold(bucketName)}`)
518
+
519
+ // create S3 clienbt
520
+ const s3 = new S3Client({region: 'us-east-1'})
521
+
522
+ // loop through each circuit
523
+ for await (const circuit of setupCeremonyData.circuits) {
524
+ // Local paths.
525
+ const index = ceremonySetupData.circuits.indexOf(circuit)
526
+ const r1csLocalPathAndFileName = `./${circuit.name}.r1cs`
527
+ const wasmLocalPathAndFileName = `./${circuit.name}.wasm`
528
+ const potLocalPathAndFileName = getPotLocalFilePath(circuit.files.potFilename)
529
+ const zkeyLocalPathAndFileName = getZkeyLocalFilePath(circuit.files.initialZkeyFilename)
530
+
531
+ // 2. download the pot and wasm files
532
+ const streamPipeline = promisify(pipeline)
533
+ await checkAndDownloadSmallestPowersOfTau(convertToDoubleDigits(circuit.metadata?.pot!), circuit.files.potFilename)
534
+
535
+ // download the wasm to calculate the hash
536
+ const spinner = customSpinner(
537
+ `Downloading the ${theme.text.bold(
538
+ `#${circuit.name}`
539
+ )} WASM file from the project's bucket...`,
540
+ `clock`
541
+ )
542
+ spinner.start()
543
+ const command = new GetObjectCommand({ Bucket: ceremonySetupData.circuitArtifacts[index].artifacts.bucket, Key: ceremonySetupData.circuitArtifacts[index].artifacts.wasmStoragePath })
544
+
545
+ const response = await s3.send(command)
546
+
547
+ if (response.$metadata.httpStatusCode !== 200) {
548
+ 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.")
549
+ }
550
+
551
+ if (response.Body instanceof Readable)
552
+ await streamPipeline(response.Body, createWriteStream(wasmLocalPathAndFileName))
553
+
554
+ spinner.stop()
555
+ // 3. generate the zKey
556
+ await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined)
557
+
558
+ // 4. calculate the hashes
559
+ const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName)
560
+ const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename))
561
+ const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName)
562
+
563
+ // 5. upload the artifacts
564
+
565
+ // Upload zKey to Storage.
566
+ await handleCircuitArtifactUploadToStorage(
567
+ firebaseFunctions,
568
+ bucketName,
569
+ circuit.files.initialZkeyStoragePath,
570
+ zkeyLocalPathAndFileName,
571
+ circuit.files.initialZkeyFilename
572
+ )
573
+
574
+ // Check if PoT file has been already uploaded to storage.
575
+ const alreadyUploadedPot = await checkIfObjectExist(
576
+ firebaseFunctions,
577
+ bucketName,
578
+ circuit.files.potStoragePath
579
+ )
580
+
581
+ // If it wasn't uploaded yet, upload it.
582
+ if (!alreadyUploadedPot) {
583
+ // Upload PoT to Storage.
584
+ await handleCircuitArtifactUploadToStorage(
585
+ firebaseFunctions,
586
+ bucketName,
587
+ circuit.files.potStoragePath,
588
+ potLocalPathAndFileName,
589
+ circuit.files.potFilename
590
+ )
591
+ }
592
+
593
+ // Upload r1cs to Storage.
594
+ await handleCircuitArtifactUploadToStorage(
595
+ firebaseFunctions,
596
+ bucketName,
597
+ circuit.files.r1csStoragePath,
598
+ r1csLocalPathAndFileName,
599
+ circuit.files.r1csFilename
600
+ )
601
+
602
+ // Upload wasm to Storage.
603
+ await handleCircuitArtifactUploadToStorage(
604
+ firebaseFunctions,
605
+ bucketName,
606
+ circuit.files.wasmStoragePath,
607
+ r1csLocalPathAndFileName,
608
+ circuit.files.wasmFilename
609
+ )
610
+
611
+ // 6 update the setup data object
612
+ ceremonySetupData.circuits[index].files = {
613
+ ...circuit.files,
614
+ potBlake2bHash: potBlake2bHash,
615
+ wasmBlake2bHash: wasmBlake2bHash,
616
+ initialZkeyBlake2bHash: initialZkeyBlake2bHash
617
+ }
618
+
619
+ ceremonySetupData.circuits[index].zKeySizeInBytes = getFileStats(zkeyLocalPathAndFileName).size
620
+ }
621
+
622
+
623
+ // 7. setup the ceremony
624
+ const ceremonyId = await setupCeremony(firebaseFunctions, ceremonySetupData.ceremonyInputData, ceremonySetupData.ceremonyPrefix, ceremonySetupData.circuits)
625
+ console.log( `Congratulations, the setup of ceremony ${theme.text.bold(
626
+ ceremonySetupData.ceremonyInputData.title
627
+ )} (${`UID: ${theme.text.bold(ceremonyId)}`}) has been successfully completed ${
628
+ theme.emojis.tada
629
+ }. You will be able to find all the files and info respectively in the ceremony bucket and database document.`)
630
+
631
+ terminate(providerUserId)
632
+ }
633
+
634
+ // Look for R1CS files.
635
+ const r1csFilePaths = await filterDirectoryFilesByExtension(cwd, `.r1cs`)
636
+ // Look for WASM files.
637
+ const wasmFilePaths = await filterDirectoryFilesByExtension(cwd, `.wasm`)
638
+ // Look for pre-computed zKeys references (if any).
639
+ const localPreComputedZkeysFilenames = await filterDirectoryFilesByExtension(cwd, `.zkey`)
640
+
641
+ if (!r1csFilePaths.length) showError(COMMAND_ERRORS.COMMAND_SETUP_NO_R1CS, true)
642
+ if (!wasmFilePaths.length) showError(COMMAND_ERRORS.COMMAND_SETUP_NO_WASM, true)
643
+ if (wasmFilePaths.length !== r1csFilePaths.length) showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true)
644
+
645
+ // Prompt the coordinator for gather ceremony input data.
646
+ const ceremonyInputData = await promptCeremonyInputData(firestoreDatabase)
647
+ const ceremonyPrefix = extractPrefix(ceremonyInputData.title)
648
+
649
+ // Add circuits to ceremony.
650
+ const circuitsInputData: Array<CircuitInputData> = await handleAdditionOfCircuitsToCeremony(
651
+ r1csFilePaths.map((dirent: Dirent) => dirent.name),
652
+ wasmFilePaths.map((dirent: Dirent) => dirent.name),
653
+ ceremonyInputData.timeoutMechanismType
654
+ )
655
+
656
+ // Move input data to circuits.
657
+ circuitsInputData.forEach((data: CircuitInputData) => circuits.push(data))
658
+
659
+ // Display ceremony summary.
660
+ displayCeremonySummary(ceremonyInputData, circuits)
661
+
662
+ // Prepare data.
663
+ let wannaGenerateNewZkey = true // New zKey generation flag.
664
+ let wannaUsePreDownloadedPoT = false // Local PoT file usage flag.
665
+ let bucketName: string = "" // The name of the bucket.
666
+
667
+ // Ask for confirmation.
668
+ const { confirmation } = await askForConfirmation("Do you want to continue with the ceremony setup?", "Yes", "No")
669
+
670
+ if (confirmation) {
671
+ await simpleLoader(`Looking for any pre-computed zkey file...`, `clock`, 1000)
672
+
673
+ // Simulate pre-computed zkeys search.
674
+ let leftPreComputedZkeys = localPreComputedZkeysFilenames
675
+
676
+ /** Circuit-based setup */
677
+ for (let i = 0; i < circuits.length; i += 1) {
678
+ const circuit = circuits[i]
679
+
680
+ console.log(
681
+ theme.text.bold(`\n- Setup for Circuit # ${theme.colors.magenta(`${circuit.sequencePosition}`)}\n`)
682
+ )
683
+
684
+ // Convert to double digits powers (e.g., 9 -> 09).
685
+ let doubleDigitsPowers = convertToDoubleDigits(circuit.metadata?.pot!)
686
+ let smallestPowersOfTauCompleteFilenameForCircuit = `${potFilenameTemplate}${doubleDigitsPowers}.ptau`
687
+
688
+ // Rename R1Cs and zKey based on circuit name and prefix.
689
+ const r1csCompleteFilename = `${circuit.name}.r1cs`
690
+ const wasmCompleteFilename = `${circuit.name}.wasm`
691
+ const firstZkeyCompleteFilename = `${circuit.prefix}_${genesisZkeyIndex}.zkey`
692
+ let preComputedZkeyCompleteFilename = ``
693
+
694
+ // Local paths.
695
+ const r1csLocalPathAndFileName = getCWDFilePath(cwd, r1csCompleteFilename)
696
+ const wasmLocalPathAndFileName = getCWDFilePath(cwd, wasmCompleteFilename)
697
+ let potLocalPathAndFileName = getPotLocalFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
698
+ let zkeyLocalPathAndFileName = getZkeyLocalFilePath(firstZkeyCompleteFilename)
699
+
700
+ // Storage paths.
701
+ const r1csStorageFilePath = getR1csStorageFilePath(circuit.prefix!, r1csCompleteFilename)
702
+ const wasmStorageFilePath = getWasmStorageFilePath(circuit.prefix!, wasmCompleteFilename)
703
+ let potStorageFilePath = getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
704
+ const zkeyStorageFilePath = getZkeyStorageFilePath(circuit.prefix!, firstZkeyCompleteFilename)
705
+
706
+ if (leftPreComputedZkeys.length <= 0)
707
+ console.log(
708
+ `${theme.symbols.warning} No pre-computed zKey was found. Therefore, a new zKey from scratch will be generated.`
709
+ )
710
+ else {
711
+ // Prompt if coordinator wanna use a pre-computed zKey for the circuit.
712
+ const wannaUsePreComputedZkey = await promptPreComputedZkey()
713
+
714
+ if (wannaUsePreComputedZkey) {
715
+ // Prompt for pre-computed zKey selection.
716
+ const preComputedZkeyOptions = leftPreComputedZkeys.map((dirent: Dirent) => dirent.name)
717
+ preComputedZkeyCompleteFilename = await promptPreComputedZkeySelector(preComputedZkeyOptions)
718
+
719
+ // Switch to pre-computed zkey path.
720
+ zkeyLocalPathAndFileName = getCWDFilePath(cwd, preComputedZkeyCompleteFilename)
721
+
722
+ // Handle the selection for the PoT file to associate w/ the selected pre-computed zKey.
723
+ const {
724
+ doubleDigitsPowers: selectedDoubleDigitsPowers,
725
+ potCompleteFilename: selectedPotCompleteFilename,
726
+ usePreDownloadedPoT
727
+ } = await handlePreComputedZkeyPowersOfTauSelection(circuit.metadata?.pot!)
728
+
729
+ // Update state.
730
+ doubleDigitsPowers = selectedDoubleDigitsPowers
731
+ smallestPowersOfTauCompleteFilenameForCircuit = selectedPotCompleteFilename
732
+ wannaUsePreDownloadedPoT = usePreDownloadedPoT
733
+
734
+ // Update paths.
735
+ potLocalPathAndFileName = getPotLocalFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
736
+ potStorageFilePath = getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
737
+
738
+ // Check (and download) the smallest Powers of Tau for circuit.
739
+ if (!wannaUsePreDownloadedPoT)
740
+ await checkAndDownloadSmallestPowersOfTau(
741
+ doubleDigitsPowers,
742
+ smallestPowersOfTauCompleteFilenameForCircuit
743
+ )
744
+
745
+ // Update flag for zKey generation accordingly.
746
+ wannaGenerateNewZkey = false
747
+
748
+ // Update paths.
749
+ renameSync(getCWDFilePath(cwd, preComputedZkeyCompleteFilename), firstZkeyCompleteFilename) // the pre-computed zKey become the new first (genesis) zKey.
750
+ zkeyLocalPathAndFileName = getCWDFilePath(cwd, firstZkeyCompleteFilename)
751
+
752
+ // Remove the pre-computed zKey from the list of possible pre-computed options.
753
+ leftPreComputedZkeys = leftPreComputedZkeys.filter(
754
+ (dirent: Dirent) => dirent.name !== preComputedZkeyCompleteFilename
755
+ )
756
+ }
757
+ }
758
+
759
+ // Check (and download) the smallest Powers of Tau for circuit.
760
+ if (!wannaUsePreDownloadedPoT)
761
+ await checkAndDownloadSmallestPowersOfTau(
762
+ doubleDigitsPowers,
763
+ smallestPowersOfTauCompleteFilenameForCircuit
764
+ )
765
+
766
+ if (wannaGenerateNewZkey)
767
+ await handleNewZkeyGeneration(
768
+ r1csLocalPathAndFileName,
769
+ potLocalPathAndFileName,
770
+ zkeyLocalPathAndFileName
771
+ )
772
+
773
+ // Create a bucket for ceremony if it has not yet been created.
774
+ if (!bucketName) bucketName = await handleCeremonyBucketCreation(firebaseFunctions, ceremonyPrefix)
775
+
776
+ // Upload zKey to Storage.
777
+ await handleCircuitArtifactUploadToStorage(
778
+ firebaseFunctions,
779
+ bucketName,
780
+ zkeyStorageFilePath,
781
+ zkeyLocalPathAndFileName,
782
+ firstZkeyCompleteFilename
783
+ )
784
+
785
+ // Check if PoT file has been already uploaded to storage.
786
+ const alreadyUploadedPot = await checkIfObjectExist(
787
+ firebaseFunctions,
788
+ bucketName,
789
+ getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
790
+ )
791
+
792
+ if (!alreadyUploadedPot) {
793
+ // Upload PoT to Storage.
794
+ await handleCircuitArtifactUploadToStorage(
795
+ firebaseFunctions,
796
+ bucketName,
797
+ potStorageFilePath,
798
+ potLocalPathAndFileName,
799
+ smallestPowersOfTauCompleteFilenameForCircuit
800
+ )
801
+ } else
802
+ console.log(
803
+ `${theme.symbols.success} The Powers of Tau (${theme.text.bold(
804
+ smallestPowersOfTauCompleteFilenameForCircuit
805
+ )}) file is already saved in the storage`
806
+ )
807
+
808
+ // Upload R1CS to Storage.
809
+ await handleCircuitArtifactUploadToStorage(
810
+ firebaseFunctions,
811
+ bucketName,
812
+ r1csStorageFilePath,
813
+ r1csLocalPathAndFileName,
814
+ r1csCompleteFilename
815
+ )
816
+
817
+ // Upload WASM to Storage.
818
+ await handleCircuitArtifactUploadToStorage(
819
+ firebaseFunctions,
820
+ bucketName,
821
+ wasmStorageFilePath,
822
+ wasmLocalPathAndFileName,
823
+ wasmCompleteFilename
824
+ )
825
+
826
+ process.stdout.write(`\n`)
827
+
828
+ const spinner = customSpinner(`Preparing the ceremony data (this may take a while)...`, `clock`)
829
+ spinner.start()
830
+
831
+ // Computing file hash (this may take a while).
832
+ const r1csBlake2bHash = await blake512FromPath(r1csLocalPathAndFileName)
833
+ const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName)
834
+ const potBlake2bHash = await blake512FromPath(potLocalPathAndFileName)
835
+ const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName)
836
+
837
+ spinner.stop()
838
+
839
+ // Prepare circuit data for writing to the DB.
840
+ const circuitFiles: CircuitArtifacts = {
841
+ r1csFilename: r1csCompleteFilename,
842
+ wasmFilename: wasmCompleteFilename,
843
+ potFilename: smallestPowersOfTauCompleteFilenameForCircuit,
844
+ initialZkeyFilename: firstZkeyCompleteFilename,
845
+ r1csStoragePath: r1csStorageFilePath,
846
+ wasmStoragePath: wasmStorageFilePath,
847
+ potStoragePath: potStorageFilePath,
848
+ initialZkeyStoragePath: zkeyStorageFilePath,
849
+ r1csBlake2bHash,
850
+ wasmBlake2bHash,
851
+ potBlake2bHash,
852
+ initialZkeyBlake2bHash
853
+ }
854
+
855
+ // nb. these will be populated after the first contribution.
856
+ const circuitTimings: CircuitTimings = {
857
+ contributionComputation: 0,
858
+ fullContribution: 0,
859
+ verifyCloudFunction: 0
860
+ }
861
+
862
+ circuits[i] = {
863
+ ...circuit,
864
+ files: circuitFiles,
865
+ avgTimings: circuitTimings,
866
+ zKeySizeInBytes: getFileStats(zkeyLocalPathAndFileName).size
867
+ }
868
+
869
+ // Reset flags.
870
+ wannaGenerateNewZkey = true
871
+ wannaUsePreDownloadedPoT = false
872
+ }
873
+
874
+ const spinner = customSpinner(`Writing ceremony data...`, `clock`)
875
+ spinner.start()
876
+
877
+ try {
878
+ // Call the Cloud Function for writing ceremony data on Firestore DB.
879
+ ceremonyId = await setupCeremony(firebaseFunctions, ceremonyInputData, ceremonyPrefix, circuits)
880
+ } catch (error: any) {
881
+ const errorBody = JSON.parse(JSON.stringify(error))
882
+ showError(
883
+ `[${errorBody.code}] ${error.message} ${!errorBody.details ? "" : `\n${errorBody.details}`}`,
884
+ true
885
+ )
886
+ }
887
+
888
+ await sleep(5000) // Cloud function unexpected termination workaround.
889
+
890
+ spinner.succeed(
891
+ `Congratulations, the setup of ceremony ${theme.text.bold(
892
+ ceremonyInputData.title
893
+ )} (${`UID: ${theme.text.bold(ceremonyId)}`}) has been successfully completed ${
894
+ theme.emojis.tada
895
+ }. You will be able to find all the files and info respectively in the ceremony bucket and database document.`
896
+ )
897
+ }
898
+ terminate(providerUserId)
899
+ }
900
+
901
+ export default setup