@devtion/backend 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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/dist/src/functions/index.js +2644 -0
  4. package/dist/src/functions/index.mjs +2596 -0
  5. package/dist/types/functions/ceremony.d.ts +33 -0
  6. package/dist/types/functions/ceremony.d.ts.map +1 -0
  7. package/dist/types/functions/circuit.d.ts +63 -0
  8. package/dist/types/functions/circuit.d.ts.map +1 -0
  9. package/dist/types/functions/index.d.ts +7 -0
  10. package/dist/types/functions/index.d.ts.map +1 -0
  11. package/dist/types/functions/participant.d.ts +58 -0
  12. package/dist/types/functions/participant.d.ts.map +1 -0
  13. package/dist/types/functions/storage.d.ts +37 -0
  14. package/dist/types/functions/storage.d.ts.map +1 -0
  15. package/dist/types/functions/timeout.d.ts +26 -0
  16. package/dist/types/functions/timeout.d.ts.map +1 -0
  17. package/dist/types/functions/user.d.ts +15 -0
  18. package/dist/types/functions/user.d.ts.map +1 -0
  19. package/dist/types/lib/errors.d.ts +75 -0
  20. package/dist/types/lib/errors.d.ts.map +1 -0
  21. package/dist/types/lib/services.d.ts +9 -0
  22. package/dist/types/lib/services.d.ts.map +1 -0
  23. package/dist/types/lib/utils.d.ts +141 -0
  24. package/dist/types/lib/utils.d.ts.map +1 -0
  25. package/dist/types/types/enums.d.ts +13 -0
  26. package/dist/types/types/enums.d.ts.map +1 -0
  27. package/dist/types/types/index.d.ts +130 -0
  28. package/dist/types/types/index.d.ts.map +1 -0
  29. package/package.json +89 -0
  30. package/src/functions/ceremony.ts +333 -0
  31. package/src/functions/circuit.ts +1092 -0
  32. package/src/functions/index.ts +36 -0
  33. package/src/functions/participant.ts +526 -0
  34. package/src/functions/storage.ts +548 -0
  35. package/src/functions/timeout.ts +294 -0
  36. package/src/functions/user.ts +142 -0
  37. package/src/lib/errors.ts +237 -0
  38. package/src/lib/services.ts +28 -0
  39. package/src/lib/utils.ts +472 -0
  40. package/src/types/enums.ts +12 -0
  41. package/src/types/index.ts +140 -0
  42. package/test/index.test.ts +62 -0
@@ -0,0 +1,1092 @@
1
+ import * as functionsV1 from "firebase-functions/v1"
2
+ import * as functionsV2 from "firebase-functions/v2"
3
+ import admin from "firebase-admin"
4
+ import dotenv from "dotenv"
5
+ import { QueryDocumentSnapshot } from "firebase-functions/v1/firestore"
6
+ import { Change } from "firebase-functions"
7
+ import fs from "fs"
8
+ import { Timer } from "timer-node"
9
+ import { FieldValue } from "firebase-admin/firestore"
10
+ import {
11
+ commonTerms,
12
+ getParticipantsCollectionPath,
13
+ getCircuitsCollectionPath,
14
+ getZkeyStorageFilePath,
15
+ getContributionsCollectionPath,
16
+ formatZkeyIndex,
17
+ getTranscriptStorageFilePath,
18
+ getVerificationKeyStorageFilePath,
19
+ getVerifierContractStorageFilePath,
20
+ finalContributionIndex,
21
+ verificationKeyAcronym,
22
+ verifierSmartContractAcronym,
23
+ computeSHA256ToHex,
24
+ ParticipantStatus,
25
+ ParticipantContributionStep,
26
+ CeremonyState,
27
+ Contribution,
28
+ blake512FromPath,
29
+ startEC2Instance,
30
+ retrieveCommandOutput,
31
+ runCommandUsingSSM,
32
+ CircuitContributionVerificationMechanism,
33
+ checkIfRunning,
34
+ vmContributionVerificationCommand,
35
+ getPotStorageFilePath,
36
+ genesisZkeyIndex,
37
+ createCustomLoggerForFile,
38
+ retrieveCommandStatus,
39
+ stopEC2Instance
40
+ } from "@p0tion/actions"
41
+ import { zKey } from "snarkjs"
42
+ import { CommandInvocationStatus, SSMClient } from "@aws-sdk/client-ssm"
43
+ import { FinalizeCircuitData, VerifyContributionData } from "../types/index"
44
+ import { LogLevel } from "../types/enums"
45
+ import { COMMON_ERRORS, logAndThrowError, makeError, printLog, SPECIFIC_ERRORS } from "../lib/errors"
46
+ import {
47
+ createEC2Client,
48
+ createSSMClient,
49
+ createTemporaryLocalPath,
50
+ deleteObject,
51
+ downloadArtifactFromS3Bucket,
52
+ getCeremonyCircuits,
53
+ getCircuitDocumentByPosition,
54
+ getCurrentServerTimestampInMillis,
55
+ getDocumentById,
56
+ getFinalContribution,
57
+ sleep,
58
+ uploadFileToBucket
59
+ } from "../lib/utils"
60
+ import { EC2Client } from "@aws-sdk/client-ec2"
61
+
62
+ dotenv.config()
63
+
64
+ /**
65
+ * Execute the coordination of the participant for the given circuit.
66
+ * @dev possible coordination scenarios:
67
+ * A) The participant becomes the current contributor of circuit X (single participant).
68
+ * B) The participant is placed in the contribution waiting queue because someone else is currently contributing to circuit X (single participant)
69
+ * C) The participant is removed as current contributor from Circuit X and gets coordinated for Circuit X + 1 (multi-participant).
70
+ * C.1) The first participant in the waiting queue for Circuit X (if any), becomes the new contributor for circuit X.
71
+ * @param participant <QueryDocumentSnapshot> - the Firestore document of the participant.
72
+ * @param circuit <QueryDocumentSnapshot> - the Firestore document of the circuit.
73
+ * @param isSingleParticipantCoordination <boolean> - true if the coordination involves only a single participant; otherwise false (= involves multiple participant).
74
+ * @param [ceremonyId] <string> - the unique identifier of the ceremony (needed only for multi-participant coordination).
75
+ */
76
+ const coordinate = async (
77
+ participant: QueryDocumentSnapshot,
78
+ circuit: QueryDocumentSnapshot,
79
+ isSingleParticipantCoordination: boolean,
80
+ ceremonyId?: string
81
+ ) => {
82
+ // Prepare db and transactions batch.
83
+ const firestore = admin.firestore()
84
+ const batch = firestore.batch()
85
+
86
+ // Extract data.
87
+ const { status, contributionStep } = participant.data()
88
+ const { waitingQueue } = circuit.data()
89
+ const { contributors, currentContributor } = waitingQueue
90
+
91
+ // Prepare state updates for waiting queue.
92
+ const newContributors: Array<string> = contributors
93
+ let newCurrentContributorId: string = ""
94
+
95
+ // Prepare state updates for participant.
96
+ let newParticipantStatus: string = ""
97
+ let newContributionStep: string = ""
98
+
99
+ // Prepare pre-conditions.
100
+ const noCurrentContributor = !currentContributor
101
+ const noContributorsInWaitingQueue = !contributors.length
102
+ const emptyWaitingQueue = noCurrentContributor && noContributorsInWaitingQueue
103
+
104
+ const participantIsNotCurrentContributor = currentContributor !== participant.id
105
+ const participantIsCurrentContributor = currentContributor === participant.id
106
+ const participantIsReady = status === ParticipantStatus.READY
107
+ const participantResumingAfterTimeoutExpiration = participantIsCurrentContributor && participantIsReady
108
+
109
+ const participantCompletedOneOrAllContributions =
110
+ (status === ParticipantStatus.CONTRIBUTED || status === ParticipantStatus.DONE) &&
111
+ contributionStep === ParticipantContributionStep.COMPLETED
112
+
113
+ // Check for scenarios.
114
+ if (isSingleParticipantCoordination) {
115
+ // Scenario (A).
116
+ if (emptyWaitingQueue) {
117
+ printLog(`Coordinate - executing scenario A - emptyWaitingQueue`, LogLevel.DEBUG)
118
+
119
+ // Update.
120
+ newCurrentContributorId = participant.id
121
+ newParticipantStatus = ParticipantStatus.CONTRIBUTING
122
+ newContributionStep = ParticipantContributionStep.DOWNLOADING
123
+ newContributors.push(newCurrentContributorId)
124
+ }
125
+ // Scenario (A).
126
+ else if (participantResumingAfterTimeoutExpiration) {
127
+ printLog(
128
+ `Coordinate - executing scenario A - single - participantResumingAfterTimeoutExpiration`,
129
+ LogLevel.DEBUG
130
+ )
131
+
132
+ newParticipantStatus = ParticipantStatus.CONTRIBUTING
133
+ newContributionStep = ParticipantContributionStep.DOWNLOADING
134
+ }
135
+ // Scenario (B).
136
+ else if (participantIsNotCurrentContributor) {
137
+ printLog(`Coordinate - executing scenario B - single - participantIsNotCurrentContributor`, LogLevel.DEBUG)
138
+
139
+ newCurrentContributorId = currentContributor
140
+ newParticipantStatus = ParticipantStatus.WAITING
141
+ newContributors.push(participant.id)
142
+ }
143
+
144
+ // Prepare tx - Scenario (A) only.
145
+ if (newContributionStep)
146
+ batch.update(participant.ref, {
147
+ contributionStep: newContributionStep,
148
+ lastUpdated: getCurrentServerTimestampInMillis()
149
+ })
150
+
151
+ // Prepare tx - Scenario (A) or (B).
152
+ batch.update(participant.ref, {
153
+ status: newParticipantStatus,
154
+ contributionStartedAt:
155
+ newParticipantStatus === ParticipantStatus.CONTRIBUTING ? getCurrentServerTimestampInMillis() : 0,
156
+ lastUpdated: getCurrentServerTimestampInMillis()
157
+ })
158
+ } else if (participantIsCurrentContributor && participantCompletedOneOrAllContributions && !!ceremonyId) {
159
+ printLog(
160
+ `Coordinate - executing scenario C - multi - participantIsCurrentContributor && participantCompletedOneOrAllContributions`,
161
+ LogLevel.DEBUG
162
+ )
163
+
164
+ newParticipantStatus = ParticipantStatus.CONTRIBUTING
165
+ newContributionStep = ParticipantContributionStep.DOWNLOADING
166
+
167
+ // Remove from waiting queue of circuit X.
168
+ newContributors.shift()
169
+
170
+ // Step (C.1).
171
+ if (newContributors.length > 0) {
172
+ // Get new contributor for circuit X.
173
+ newCurrentContributorId = newContributors.at(0)!
174
+
175
+ // Pass the baton to the new contributor.
176
+ const newCurrentContributorDocument = await getDocumentById(
177
+ getParticipantsCollectionPath(ceremonyId),
178
+ newCurrentContributorId
179
+ )
180
+
181
+ // Prepare update tx.
182
+ batch.update(newCurrentContributorDocument.ref, {
183
+ status: newParticipantStatus,
184
+ contributionStep: newContributionStep,
185
+ contributionStartedAt: getCurrentServerTimestampInMillis(),
186
+ lastUpdated: getCurrentServerTimestampInMillis()
187
+ })
188
+
189
+ printLog(
190
+ `Participant ${newCurrentContributorId} is the new current contributor for circuit ${circuit.id}`,
191
+ LogLevel.DEBUG
192
+ )
193
+ }
194
+ }
195
+
196
+ // Prepare tx - must be done for all Scenarios.
197
+ batch.update(circuit.ref, {
198
+ waitingQueue: {
199
+ ...waitingQueue,
200
+ contributors: newContributors,
201
+ currentContributor: newCurrentContributorId
202
+ },
203
+ lastUpdated: getCurrentServerTimestampInMillis()
204
+ })
205
+
206
+ // Send txs.
207
+ await batch.commit()
208
+
209
+ printLog(`Coordinate successfully completed`, LogLevel.DEBUG)
210
+ }
211
+
212
+ /**
213
+ * Wait until the command has completed its execution inside the VM.
214
+ * @dev this method implements a custom interval to check 5 times after 1 minute if the command execution
215
+ * has been completed or not by calling the `retrieveCommandStatus` method.
216
+ * @param {any} resolve the promise.
217
+ * @param {any} reject the promise.
218
+ * @param {SSMClient} ssm the SSM client.
219
+ * @param {string} vmInstanceId the unique identifier of the VM instance.
220
+ * @param {string} commandId the unique identifier of the VM command.
221
+ * @returns <Promise<void>> true when the command execution succeed; otherwise false.
222
+ */
223
+ const waitForVMCommandExecution = (
224
+ resolve: any,
225
+ reject: any,
226
+ ssm: SSMClient,
227
+ vmInstanceId: string,
228
+ commandId: string
229
+ ) => {
230
+ const interval = setInterval(async () => {
231
+ try {
232
+ // Get command status.
233
+ const cmdStatus = await retrieveCommandStatus(ssm, vmInstanceId, commandId)
234
+ printLog(`Checking command ${commandId} status => ${cmdStatus}`, LogLevel.DEBUG)
235
+
236
+ if (cmdStatus === CommandInvocationStatus.SUCCESS) {
237
+ printLog(`Command ${commandId} successfully completed`, LogLevel.DEBUG)
238
+
239
+ // Resolve the promise.
240
+ resolve()
241
+ } else if (cmdStatus === CommandInvocationStatus.FAILED) {
242
+ logAndThrowError(SPECIFIC_ERRORS.SE_VM_FAILED_COMMAND_EXECUTION)
243
+ reject()
244
+ } else if (cmdStatus === CommandInvocationStatus.TIMED_OUT) {
245
+ logAndThrowError(SPECIFIC_ERRORS.SE_VM_TIMEDOUT_COMMAND_EXECUTION)
246
+ reject()
247
+ } else if (cmdStatus === CommandInvocationStatus.CANCELLED) {
248
+ logAndThrowError(SPECIFIC_ERRORS.SE_VM_CANCELLED_COMMAND_EXECUTION)
249
+ reject()
250
+ } else if (cmdStatus === CommandInvocationStatus.DELAYED) {
251
+ logAndThrowError(SPECIFIC_ERRORS.SE_VM_DELAYED_COMMAND_EXECUTION)
252
+ reject()
253
+ }
254
+ } catch (error: any) {
255
+ printLog(`Invalid command ${commandId} execution`, LogLevel.DEBUG)
256
+
257
+ if (!error.toString().includes(commandId)) logAndThrowError(COMMON_ERRORS.CM_INVALID_COMMAND_EXECUTION)
258
+
259
+ // Reject the promise.
260
+ reject()
261
+ } finally {
262
+ // Clear the interval.
263
+ clearInterval(interval)
264
+ }
265
+ }, 60000) // 1 minute.
266
+ }
267
+
268
+ /**
269
+ * Wait until the artifacts have been downloaded.
270
+ * @param {any} resolve the promise.
271
+ * @param {any} reject the promise.
272
+ * @param {string} potTempFilePath the tmp path to the locally downloaded pot file.
273
+ * @param {string} firstZkeyTempFilePath the tmp path to the locally downloaded first zkey file.
274
+ * @param {string} lastZkeyTempFilePath the tmp path to the locally downloaded last zkey file.
275
+ */
276
+ const waitForFileDownload = (
277
+ resolve: any,
278
+ reject: any,
279
+ potTempFilePath: string,
280
+ firstZkeyTempFilePath: string,
281
+ lastZkeyTempFilePath: string,
282
+ circuitId: string,
283
+ participantId: string
284
+ ) => {
285
+ const maxWaitTime = 5 * 60 * 1000 // 5 minutes
286
+ // every second check if the file download was completed
287
+ const interval = setInterval(async () => {
288
+ printLog(`Verifying that the artifacts were downloaded for circuit ${circuitId} and participant ${participantId}`, LogLevel.DEBUG)
289
+ try {
290
+ // check if files have been downloaded
291
+ if (!fs.existsSync(potTempFilePath)) {
292
+ printLog(`Pot file not found at ${potTempFilePath}`, LogLevel.DEBUG)
293
+ }
294
+ if (!fs.existsSync(firstZkeyTempFilePath)) {
295
+ printLog(`First zkey file not found at ${firstZkeyTempFilePath}`, LogLevel.DEBUG)
296
+ }
297
+ if (!fs.existsSync(lastZkeyTempFilePath)) {
298
+ printLog(`Last zkey file not found at ${lastZkeyTempFilePath}`, LogLevel.DEBUG)
299
+ }
300
+
301
+ // if all files were downloaded
302
+ if (fs.existsSync(potTempFilePath) && fs.existsSync(firstZkeyTempFilePath) && fs.existsSync(lastZkeyTempFilePath)) {
303
+ printLog(`All required files are present on disk.`, LogLevel.INFO)
304
+ // resolve the promise
305
+ resolve()
306
+ }
307
+ } catch (error: any) {
308
+ // if we have an error then we print it as a warning and reject
309
+ printLog(`Error while downloading files: ${error}`, LogLevel.WARN)
310
+ reject()
311
+ } finally {
312
+ printLog(`Clearing the interval for file download. Circuit ${circuitId} and participant ${participantId}`, LogLevel.DEBUG)
313
+ clearInterval(interval)
314
+ }
315
+ }, 5000)
316
+
317
+ // we want to clean in 5 minutes in case
318
+ setTimeout(() => {
319
+ clearInterval(interval)
320
+ reject(new Error('Timeout exceeded while waiting for files to be downloaded.'))
321
+ }, maxWaitTime)
322
+ }
323
+
324
+ /**
325
+ * This method is used to coordinate the waiting queues of ceremony circuits.
326
+ * @dev this cloud function is triggered whenever an update of a document related to a participant of a ceremony occurs.
327
+ * The function verifies that such update is preparatory towards a waiting queue update for one or more circuits in the ceremony.
328
+ * If that's the case, this cloud functions proceeds with the "coordination" of the waiting queues, leading to three different scenarios:
329
+ * A) The participant becomes the current contributor of circuit X (single participant).
330
+ * B) The participant is placed in the contribution waiting queue because someone else is currently contributing to circuit X (single participant)
331
+ * C) The participant is removed as current contributor from Circuit X and gets coordinated for Circuit X + 1 (multi-participant).
332
+ * C.1) The first participant in the waiting queue for Circuit X (if any), becomes the new contributor for circuit X.
333
+ * Before triggering the above scenarios, the cloud functions verifies that suitable pre-conditions are met.
334
+ * @notice The cloud function performs the subsequent steps:
335
+ * 0) Prepares the participant's previous and current data (after/before document change).
336
+ * 1) Retrieve the ceremony from the participant's document path.
337
+ * 2) Verifies that the participant has changed to a state for which it is ready for contribution.
338
+ * 2.A) If ready, verifies whether the participant is ready to:
339
+ * - Contribute for the first time or for the next circuit (other than the first) or contribute after a timeout has expired. If yes, coordinate (single participant scenario).
340
+ * 2.B) Otherwise, check whether the participant has:
341
+ * - Just completed a contribution or all contributions for each circuit. If yes, coordinate (multi-participant scenario).
342
+ */
343
+ export const coordinateCeremonyParticipant = functionsV1
344
+ .region('europe-west1')
345
+ .runWith({
346
+ memory: "512MB"
347
+ })
348
+ .firestore.document(
349
+ `${commonTerms.collections.ceremonies.name}/{ceremonyId}/${commonTerms.collections.participants.name}/{participantId}`
350
+ )
351
+ .onUpdate(async (participantChanges: Change<QueryDocumentSnapshot>) => {
352
+ // Step (0).
353
+ const exParticipant = participantChanges.before
354
+ const changedParticipant = participantChanges.after
355
+
356
+ if (!exParticipant.data() || !changedParticipant.data())
357
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
358
+
359
+ // Step (1).
360
+ const ceremonyId = exParticipant.ref.parent.parent!.path.replace(
361
+ `${commonTerms.collections.ceremonies.name}/`,
362
+ ""
363
+ )
364
+
365
+ if (!ceremonyId) logAndThrowError(COMMON_ERRORS.CM_INVALID_CEREMONY_FOR_PARTICIPANT)
366
+
367
+ // Extract data.
368
+ const {
369
+ contributionProgress: prevContributionProgress,
370
+ status: prevStatus,
371
+ contributionStep: prevContributionStep
372
+ } = exParticipant.data()!
373
+
374
+ const {
375
+ contributionProgress: changedContributionProgress,
376
+ status: changedStatus,
377
+ contributionStep: changedContributionStep
378
+ } = changedParticipant.data()!
379
+
380
+ printLog(`Coordinate participant ${exParticipant.id} for ceremony ${ceremonyId}`, LogLevel.DEBUG)
381
+ printLog(
382
+ `Participant status: ${prevStatus} => ${changedStatus} - Participant contribution step: ${prevContributionStep} => ${changedContributionStep}`,
383
+ LogLevel.DEBUG
384
+ )
385
+
386
+ // Define pre-conditions.
387
+ const participantReadyToContribute = changedStatus === ParticipantStatus.READY
388
+
389
+ const participantReadyForFirstContribution = participantReadyToContribute && prevContributionProgress === 0
390
+
391
+ const participantResumingContributionAfterTimeout =
392
+ participantReadyToContribute && prevContributionProgress === changedContributionProgress
393
+
394
+ const participantReadyForNextContribution =
395
+ participantReadyToContribute &&
396
+ prevContributionProgress === changedContributionProgress - 1 &&
397
+ prevContributionProgress !== 0
398
+
399
+ const participantCompletedEveryCircuitContribution =
400
+ changedStatus === ParticipantStatus.DONE && prevStatus !== ParticipantStatus.DONE
401
+
402
+ const participantCompletedContribution =
403
+ prevContributionProgress === changedContributionProgress &&
404
+ prevStatus === ParticipantStatus.CONTRIBUTING &&
405
+ prevContributionStep === ParticipantContributionStep.VERIFYING &&
406
+ changedStatus === ParticipantStatus.CONTRIBUTED &&
407
+ changedContributionStep === ParticipantContributionStep.COMPLETED
408
+
409
+ // Step (2).
410
+ if (
411
+ participantReadyForFirstContribution ||
412
+ participantResumingContributionAfterTimeout ||
413
+ participantReadyForNextContribution
414
+ ) {
415
+ // Step (2.A).
416
+ printLog(
417
+ `Participant is ready for first contribution (${participantReadyForFirstContribution}) or for the next contribution (${participantReadyForNextContribution}) or is resuming after a timeout expiration (${participantResumingContributionAfterTimeout})`,
418
+ LogLevel.DEBUG
419
+ )
420
+
421
+ // Get the circuit.
422
+ const circuit = await getCircuitDocumentByPosition(ceremonyId, changedContributionProgress)
423
+
424
+ // Coordinate.
425
+ await coordinate(changedParticipant, circuit, true)
426
+
427
+ printLog(`Coordination for circuit ${circuit.id} completed`, LogLevel.DEBUG)
428
+ } else if (participantCompletedContribution || participantCompletedEveryCircuitContribution) {
429
+ // Step (2.B).
430
+ printLog(
431
+ `Participant completed a contribution (${participantCompletedContribution}) or every contribution for each circuit (${participantCompletedEveryCircuitContribution})`,
432
+ LogLevel.DEBUG
433
+ )
434
+
435
+ // Get the circuit.
436
+ const circuit = await getCircuitDocumentByPosition(ceremonyId, prevContributionProgress)
437
+
438
+ // Coordinate.
439
+ await coordinate(changedParticipant, circuit, false, ceremonyId)
440
+
441
+ printLog(`Coordination for circuit ${circuit.id} completed`, LogLevel.DEBUG)
442
+ }
443
+ })
444
+
445
+
446
+ /**
447
+ * Recursive function to check whether an EC2 is in a running state
448
+ * @notice required step to run commands
449
+ * @param ec2 <EC2Client> - the EC2Client object
450
+ * @param vmInstanceId <string> - the instance Id
451
+ * @param attempts <number> - how many times to retry before failing
452
+ * @returns <Promise<boolean>> - whether the VM was started
453
+ */
454
+ const checkIfVMRunning = async (
455
+ ec2: EC2Client,
456
+ vmInstanceId: string,
457
+ attempts = 5
458
+ ): Promise<boolean> => {
459
+ // if we tried 5 times, then throw an error
460
+ if (attempts <= 0) logAndThrowError(SPECIFIC_ERRORS.SE_VM_NOT_RUNNING)
461
+
462
+ await sleep(60000); // Wait for 1 min
463
+ const isVMRunning = await checkIfRunning(ec2, vmInstanceId)
464
+
465
+ if (!isVMRunning) {
466
+ printLog(`VM not running, ${attempts - 1} attempts remaining. Retrying in 1 minute...`, LogLevel.DEBUG)
467
+ return await checkIfVMRunning(ec2, vmInstanceId, attempts - 1)
468
+ } else {
469
+ return true
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Verify the contribution of a participant computed while contributing to a specific circuit of a ceremony.
475
+ * @dev a huge amount of resources (memory, CPU, and execution time) is required for the contribution verification task.
476
+ * For this reason, we are using a V2 Cloud Function (more memory, more CPU, and longer timeout).
477
+ * Through the current configuration (16GiB memory and 4 vCPUs) we are able to support verification of contributions for 3.8M constraints circuit size.
478
+ @notice The cloud function performs the subsequent steps:
479
+ * 0) Prepare documents and extract necessary data.
480
+ * 1) Check if the participant is the current contributor to the circuit or is the ceremony coordinator
481
+ * 1.A) If either condition is true:
482
+ * 1.A.1) Prepare verification transcript logger, storage, and temporary paths.
483
+ * 1.A.2) Download necessary AWS S3 ceremony bucket artifacts.
484
+ * 1.A.3) Execute contribution verification.
485
+ * 1.A.3.0) Check if is using VM or CF approach for verification.
486
+ * 1.A.3.1) Start the instance and wait until the instance is up.
487
+ * 1.A.3.2) Prepare and run contribution verification command.
488
+ * 1.A.3.3) Wait until command complete.
489
+ * 1.A.4) Check contribution validity:
490
+ * 1.A.4.A) If valid:
491
+ * 1.A.4.A.1) Upload verification transcript to AWS S3 storage.
492
+ * 1.A.4.A.2) Creates a new valid contribution document on Firestore.
493
+ * 1.A.4.B) If not valid:
494
+ * 1.A.4.B.1) Creates a new invalid contribution document on Firestore.
495
+ * 1.A.4.C) Check if not finalizing:
496
+ * 1.A.4.C.1) If true, update circuit waiting for queue and average timings accordingly to contribution verification results;
497
+ * 2) Send all updates atomically to the Firestore database.
498
+ */
499
+ export const verifycontribution = functionsV2.https.onCall(
500
+ { memory: "16GiB", timeoutSeconds: 3600, region: 'europe-west1' },
501
+ async (request: functionsV2.https.CallableRequest<VerifyContributionData>): Promise<any> => {
502
+ if (!request.auth || (!request.auth.token.participant && !request.auth.token.coordinator))
503
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER)
504
+
505
+ if (
506
+ !request.data.ceremonyId ||
507
+ !request.data.circuitId ||
508
+ !request.data.contributorOrCoordinatorIdentifier ||
509
+ !request.data.bucketName
510
+ )
511
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
512
+
513
+ if (
514
+ !process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME ||
515
+ !process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION ||
516
+ !process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH
517
+ )
518
+ logAndThrowError(COMMON_ERRORS.CM_WRONG_CONFIGURATION)
519
+
520
+ // Step (0).
521
+
522
+ // Prepare and start timer.
523
+ const verifyContributionTimer = new Timer({ label: commonTerms.cloudFunctionsNames.verifyContribution })
524
+ verifyContributionTimer.start()
525
+
526
+ // Get DB.
527
+ const firestore = admin.firestore()
528
+ // Prepare batch of txs.
529
+ const batch = firestore.batch()
530
+
531
+ // Extract data.
532
+ const { ceremonyId, circuitId, contributorOrCoordinatorIdentifier, bucketName } = request.data
533
+ const userId = request.auth?.uid
534
+
535
+ // Look for the ceremony, circuit and participant document.
536
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
537
+ const circuitDoc = await getDocumentById(getCircuitsCollectionPath(ceremonyId), circuitId)
538
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
539
+
540
+ if (!ceremonyDoc.data() || !circuitDoc.data() || !participantDoc.data())
541
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
542
+
543
+ // Extract documents data.
544
+ const { state } = ceremonyDoc.data()!
545
+ const { status, contributions, verificationStartedAt, contributionStartedAt } = participantDoc.data()!
546
+ const { waitingQueue, prefix, avgTimings, verification, files } = circuitDoc.data()!
547
+ const { completedContributions, failedContributions } = waitingQueue
548
+ const {
549
+ contributionComputation: avgContributionComputationTime,
550
+ fullContribution: avgFullContributionTime,
551
+ verifyCloudFunction: avgVerifyCloudFunctionTime
552
+ } = avgTimings
553
+ const { cfOrVm, vm } = verification
554
+ // we might not have it if the circuit is not using VM.
555
+ let vmInstanceId: string = ""
556
+ if (vm) vmInstanceId = vm.vmInstanceId
557
+
558
+ // Define pre-conditions.
559
+ const isFinalizing = state === CeremonyState.CLOSED && request.auth && request.auth.token.coordinator // true only when the coordinator verifies the final contributions.
560
+ const isContributing = status === ParticipantStatus.CONTRIBUTING
561
+ const isUsingVM = cfOrVm === CircuitContributionVerificationMechanism.VM && !!vmInstanceId
562
+
563
+ // Prepare state.
564
+ let isContributionValid = false
565
+ let verifyCloudFunctionExecutionTime = 0 // time spent while executing the verify contribution cloud function.
566
+ let verifyCloudFunctionTime = 0 // time spent while executing the core business logic of this cloud function.
567
+ let fullContributionTime = 0 // time spent while doing non-verification contributions tasks (download, compute, upload).
568
+ let contributionComputationTime = 0 // time spent while computing the contribution.
569
+ let lastZkeyBlake2bHash: string = "" // the Blake2B hash of the last zKey.
570
+ let verificationTranscriptTemporaryLocalPath: string = "" // the local temporary path for the verification transcript.
571
+ let transcriptBlake2bHash: string = "" // the Blake2B hash of the verification transcript.
572
+ let commandId: string = "" // the unique identifier of the VM command.
573
+
574
+ // Derive necessary data.
575
+ const lastZkeyIndex = formatZkeyIndex(completedContributions + 1)
576
+ const verificationTranscriptCompleteFilename = `${prefix}_${
577
+ isFinalizing
578
+ ? `${contributorOrCoordinatorIdentifier}_${finalContributionIndex}_verification_transcript.log`
579
+ : `${lastZkeyIndex}_${contributorOrCoordinatorIdentifier}_verification_transcript.log`
580
+ }`
581
+
582
+ const lastZkeyFilename = `${prefix}_${isFinalizing ? finalContributionIndex : lastZkeyIndex}.zkey`
583
+
584
+ // Prepare state for VM verification (if needed).
585
+ const ec2 = await createEC2Client()
586
+ const ssm = await createSSMClient()
587
+
588
+ // Step (1.A.1).
589
+ // Get storage paths.
590
+ const verificationTranscriptStoragePathAndFilename = getTranscriptStorageFilePath(
591
+ prefix,
592
+ verificationTranscriptCompleteFilename
593
+ )
594
+ // the zKey storage path is required to be sent to the VM api
595
+ const lastZkeyStoragePath = getZkeyStorageFilePath(
596
+ prefix,
597
+ `${prefix}_${isFinalizing ? finalContributionIndex : lastZkeyIndex}.zkey`
598
+ )
599
+
600
+ const verificationTaskTimer = new Timer({ label: `${ceremonyId}-${circuitId}-${participantDoc.id}` })
601
+
602
+ const completeVerification = async () => {
603
+ // Stop verification task timer.
604
+ printLog("Completing verification", LogLevel.DEBUG)
605
+ verificationTaskTimer.stop()
606
+ verifyCloudFunctionExecutionTime = verificationTaskTimer.ms()
607
+
608
+ if (isUsingVM) {
609
+ // Create temporary path.
610
+ verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(
611
+ `${circuitId}_${participantDoc.id}.log`
612
+ )
613
+
614
+ await sleep(1000) // wait 1s for file creation.
615
+
616
+ // Download from bucket.
617
+ // nb. the transcript MUST be uploaded from the VM by verification commands.
618
+ await downloadArtifactFromS3Bucket(
619
+ bucketName,
620
+ verificationTranscriptStoragePathAndFilename,
621
+ verificationTranscriptTemporaryLocalPath
622
+ )
623
+
624
+ // Read the verification trascript and validate data by checking for core info ("ZKey Ok!").
625
+ const content = fs.readFileSync(verificationTranscriptTemporaryLocalPath, "utf-8")
626
+
627
+ if (content.includes("ZKey Ok!")) isContributionValid = true
628
+
629
+ // If the contribution is valid, then format and store the trascript.
630
+ if (isContributionValid) {
631
+ // eslint-disable-next-line no-control-regex
632
+ const updated = content.replace(/\x1b[[0-9;]*m/g, "")
633
+
634
+ fs.writeFileSync(verificationTranscriptTemporaryLocalPath, updated)
635
+ }
636
+ }
637
+
638
+ printLog(`The contribution has been verified - Result ${isContributionValid}`, LogLevel.DEBUG)
639
+
640
+ // Create a new contribution document.
641
+ const contributionDoc = await firestore
642
+ .collection(getContributionsCollectionPath(ceremonyId, circuitId))
643
+ .doc()
644
+ .get()
645
+
646
+ // Step (1.A.4).
647
+ if (isContributionValid) {
648
+ // Sleep ~3 seconds to wait for verification transcription.
649
+ await sleep(3000)
650
+
651
+ // Step (1.A.4.A.1).
652
+ if (isUsingVM) {
653
+ // Retrieve the contribution hash from the command output.
654
+ lastZkeyBlake2bHash = await retrieveCommandOutput(ssm, vmInstanceId, commandId)
655
+
656
+ const hashRegex = /[a-fA-F0-9]{64}/
657
+ const match = lastZkeyBlake2bHash.match(hashRegex)!
658
+
659
+ lastZkeyBlake2bHash = match.at(0)!
660
+
661
+ // re upload the formatted verification transcript
662
+ await uploadFileToBucket(
663
+ bucketName,
664
+ verificationTranscriptStoragePathAndFilename,
665
+ verificationTranscriptTemporaryLocalPath,
666
+ true
667
+ )
668
+
669
+ // Stop VM instance.
670
+ await stopEC2Instance(ec2, vmInstanceId)
671
+ } else {
672
+ // Upload verification transcript.
673
+ /// nb. do not use multi-part upload here due to small file size.
674
+ await uploadFileToBucket(
675
+ bucketName,
676
+ verificationTranscriptStoragePathAndFilename,
677
+ verificationTranscriptTemporaryLocalPath,
678
+ true
679
+ )
680
+ }
681
+
682
+ // Compute verification transcript hash.
683
+ transcriptBlake2bHash = await blake512FromPath(verificationTranscriptTemporaryLocalPath)
684
+
685
+ // Free resources by unlinking transcript temporary file.
686
+ fs.unlinkSync(verificationTranscriptTemporaryLocalPath)
687
+
688
+ // Filter participant contributions to find the data related to the one verified.
689
+ const participantContributions = contributions.filter(
690
+ (contribution: Contribution) =>
691
+ !!contribution.hash && !!contribution.computationTime && !contribution.doc
692
+ )
693
+
694
+ /// @dev (there must be only one contribution with an empty 'doc' field).
695
+ if (participantContributions.length !== 1)
696
+ logAndThrowError(SPECIFIC_ERRORS.SE_VERIFICATION_NO_PARTICIPANT_CONTRIBUTION_DATA)
697
+
698
+ // Get contribution computation time.
699
+ contributionComputationTime = contributions.at(0).computationTime
700
+
701
+ // Step (1.A.4.A.2).
702
+ batch.create(contributionDoc.ref, {
703
+ participantId: participantDoc.id,
704
+ contributionComputationTime,
705
+ verificationComputationTime: verifyCloudFunctionExecutionTime,
706
+ zkeyIndex: isFinalizing ? finalContributionIndex : lastZkeyIndex,
707
+ files: {
708
+ transcriptFilename: verificationTranscriptCompleteFilename,
709
+ lastZkeyFilename,
710
+ transcriptStoragePath: verificationTranscriptStoragePathAndFilename,
711
+ lastZkeyStoragePath,
712
+ transcriptBlake2bHash,
713
+ lastZkeyBlake2bHash
714
+ },
715
+ verificationSoftware: {
716
+ name: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME),
717
+ version: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION),
718
+ commitHash: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH)
719
+ },
720
+ valid: isContributionValid,
721
+ lastUpdated: getCurrentServerTimestampInMillis()
722
+ })
723
+
724
+ verifyContributionTimer.stop()
725
+ verifyCloudFunctionTime = verifyContributionTimer.ms()
726
+ } else {
727
+ // Step (1.A.4.B).
728
+
729
+ // Free-up storage by deleting invalid contribution.
730
+ await deleteObject(bucketName, lastZkeyStoragePath)
731
+
732
+ // Step (1.A.4.B.1).
733
+ batch.create(contributionDoc.ref, {
734
+ participantId: participantDoc.id,
735
+ verificationComputationTime: verifyCloudFunctionExecutionTime,
736
+ zkeyIndex: isFinalizing ? finalContributionIndex : lastZkeyIndex,
737
+ verificationSoftware: {
738
+ name: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME),
739
+ version: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION),
740
+ commitHash: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH)
741
+ },
742
+ valid: isContributionValid,
743
+ lastUpdated: getCurrentServerTimestampInMillis()
744
+ })
745
+ }
746
+
747
+ // Step (1.A.4.C)
748
+ if (!isFinalizing) {
749
+ // Step (1.A.4.C.1)
750
+ // Compute new average contribution/verification time.
751
+ fullContributionTime = Number(verificationStartedAt) - Number(contributionStartedAt)
752
+
753
+ const newAvgContributionComputationTime =
754
+ avgContributionComputationTime > 0
755
+ ? (avgContributionComputationTime + contributionComputationTime) / 2
756
+ : contributionComputationTime
757
+ const newAvgFullContributionTime =
758
+ avgFullContributionTime > 0
759
+ ? (avgFullContributionTime + fullContributionTime) / 2
760
+ : fullContributionTime
761
+ const newAvgVerifyCloudFunctionTime =
762
+ avgVerifyCloudFunctionTime > 0
763
+ ? (avgVerifyCloudFunctionTime + verifyCloudFunctionTime) / 2
764
+ : verifyCloudFunctionTime
765
+
766
+ // Prepare tx to update circuit average contribution/verification time.
767
+ /// @dev this must happen only for valid contributions.
768
+ batch.update(circuitDoc.ref, {
769
+ avgTimings: {
770
+ contributionComputation: isContributionValid
771
+ ? newAvgContributionComputationTime
772
+ : avgContributionComputationTime,
773
+ fullContribution: isContributionValid ? newAvgFullContributionTime : avgFullContributionTime,
774
+ verifyCloudFunction: isContributionValid
775
+ ? newAvgVerifyCloudFunctionTime
776
+ : avgVerifyCloudFunctionTime
777
+ },
778
+ waitingQueue: {
779
+ ...waitingQueue,
780
+ completedContributions: isContributionValid
781
+ ? completedContributions + 1
782
+ : completedContributions,
783
+ failedContributions: isContributionValid ? failedContributions : failedContributions + 1
784
+ },
785
+ lastUpdated: getCurrentServerTimestampInMillis()
786
+ })
787
+ }
788
+
789
+ // Step (2).
790
+ await batch.commit()
791
+
792
+ printLog(
793
+ `The contribution #${
794
+ isFinalizing ? finalContributionIndex : lastZkeyIndex
795
+ } of circuit ${circuitId} (ceremony ${ceremonyId}) has been verified as ${
796
+ isContributionValid ? "valid" : "invalid"
797
+ } for the participant ${participantDoc.id}`,
798
+ LogLevel.DEBUG
799
+ )
800
+ }
801
+
802
+ // Step (1).
803
+ if (isContributing || isFinalizing) {
804
+ // Prepare timer.
805
+ verificationTaskTimer.start()
806
+
807
+ // Step (1.A.3.0).
808
+ if (isUsingVM) {
809
+ printLog(`Starting the VM mechanism`, LogLevel.DEBUG)
810
+
811
+ // Prepare for VM execution.
812
+ let isVMRunning = false // true when the VM is up, otherwise false.
813
+
814
+ // Step (1.A.3.1).
815
+ await startEC2Instance(ec2, vmInstanceId)
816
+
817
+ await sleep(60000) // nb. wait for VM startup (1 mins + retry).
818
+
819
+ // Check if the startup is running.
820
+ isVMRunning = await checkIfVMRunning(ec2, vmInstanceId)
821
+
822
+ printLog(`VM running: ${isVMRunning}`, LogLevel.DEBUG)
823
+
824
+ // Step (1.A.3.2).
825
+ // Prepare.
826
+ const verificationCommand = vmContributionVerificationCommand(
827
+ bucketName,
828
+ lastZkeyStoragePath,
829
+ verificationTranscriptStoragePathAndFilename
830
+ )
831
+
832
+ // Run.
833
+ commandId = await runCommandUsingSSM(ssm, vmInstanceId, verificationCommand)
834
+
835
+ printLog(`Starting the execution of command ${commandId}`, LogLevel.DEBUG)
836
+
837
+ // Step (1.A.3.3).
838
+ return new Promise<void>((resolve, reject) =>
839
+ waitForVMCommandExecution(resolve, reject, ssm, vmInstanceId, commandId)
840
+ )
841
+ .then(async () => {
842
+ // Command execution successfully completed.
843
+ printLog(`Command ${commandId} execution has been successfully completed`, LogLevel.DEBUG)
844
+ await completeVerification()
845
+ })
846
+ .catch((error: any) => {
847
+ // Command execution aborted.
848
+ printLog(`Command ${commandId} execution has been aborted - Error ${error}`, LogLevel.DEBUG)
849
+
850
+ logAndThrowError(COMMON_ERRORS.CM_INVALID_COMMAND_EXECUTION)
851
+ })
852
+ } else {
853
+ // CF approach.
854
+ printLog(`CF mechanism`, LogLevel.DEBUG)
855
+
856
+ const potStoragePath = getPotStorageFilePath(files.potFilename)
857
+ const firstZkeyStoragePath = getZkeyStorageFilePath(prefix, `${prefix}_${genesisZkeyIndex}.zkey`)
858
+ // Prepare temporary file paths.
859
+ // (nb. these are needed to download the necessary artifacts for verification from AWS S3).
860
+ verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(
861
+ verificationTranscriptCompleteFilename
862
+ )
863
+ const potTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}.pot`)
864
+ const firstZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_genesis.zkey`)
865
+ const lastZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_last.zkey`)
866
+
867
+ // Create and populate transcript.
868
+ const transcriptLogger = createCustomLoggerForFile(verificationTranscriptTemporaryLocalPath)
869
+ transcriptLogger.info(
870
+ `${
871
+ isFinalizing ? `Final verification` : `Verification`
872
+ } transcript for ${prefix} circuit Phase 2 contribution.\n${
873
+ isFinalizing ? `Coordinator ` : `Contributor # ${Number(lastZkeyIndex)}`
874
+ } (${contributorOrCoordinatorIdentifier})\n`
875
+ )
876
+
877
+ // Step (1.A.2).
878
+ await downloadArtifactFromS3Bucket(bucketName, potStoragePath, potTempFilePath)
879
+ await downloadArtifactFromS3Bucket(bucketName, firstZkeyStoragePath, firstZkeyTempFilePath)
880
+ await downloadArtifactFromS3Bucket(bucketName, lastZkeyStoragePath, lastZkeyTempFilePath)
881
+
882
+ await sleep(6000)
883
+
884
+ // wait until the files are actually downloaded
885
+ return new Promise<void>((resolve, reject) =>
886
+ waitForFileDownload(resolve, reject, potTempFilePath, firstZkeyTempFilePath, lastZkeyTempFilePath, circuitId, participantDoc.id)
887
+ )
888
+ .then(async () => {
889
+ printLog(`Downloads from AWS S3 bucket completed - ceremony ${ceremonyId} circuit ${circuitId}`, LogLevel.DEBUG)
890
+
891
+ // Step (1.A.4).
892
+ isContributionValid = await zKey.verifyFromInit(
893
+ firstZkeyTempFilePath,
894
+ potTempFilePath,
895
+ lastZkeyTempFilePath,
896
+ transcriptLogger
897
+ )
898
+
899
+ // Compute contribution hash.
900
+ lastZkeyBlake2bHash = await blake512FromPath(lastZkeyTempFilePath)
901
+
902
+ // Free resources by unlinking temporary folders.
903
+ // Do not free-up verification transcript path here.
904
+ try {
905
+ fs.unlinkSync(potTempFilePath)
906
+ fs.unlinkSync(firstZkeyTempFilePath)
907
+ fs.unlinkSync(lastZkeyTempFilePath)
908
+ } catch (error: any) {
909
+ printLog(`Error while unlinking temporary files - Error ${error}`, LogLevel.WARN)
910
+ }
911
+
912
+ await completeVerification()
913
+ })
914
+ .catch((error: any) => {
915
+ // Throw the new error
916
+ const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
917
+ const additionalDetails = error.toString()
918
+
919
+ logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails))
920
+ })
921
+ }
922
+ }
923
+ }
924
+ )
925
+
926
+ /**
927
+ * Update the related participant's document after verification of its last contribution.
928
+ * @dev this cloud functions is responsible for preparing the participant for the contribution toward the next circuit.
929
+ * this does not happen if the participant is actually the coordinator who is finalizing the ceremony.
930
+ */
931
+ export const refreshParticipantAfterContributionVerification = functionsV1
932
+ .region('europe-west1')
933
+ .runWith({
934
+ memory: "512MB"
935
+ })
936
+ .firestore.document(
937
+ `/${commonTerms.collections.ceremonies.name}/{ceremony}/${commonTerms.collections.circuits.name}/{circuit}/${commonTerms.collections.contributions.name}/{contributions}`
938
+ )
939
+ .onCreate(async (createdContribution: QueryDocumentSnapshot) => {
940
+ // Prepare db.
941
+ const firestore = admin.firestore()
942
+ // Prepare batch of txs.
943
+ const batch = firestore.batch()
944
+
945
+ // Derive data from document.
946
+ // == /ceremonies/{ceremony}/circuits/.
947
+ const ceremonyId = createdContribution.ref.parent.parent?.parent?.parent?.path.replace(
948
+ `${commonTerms.collections.ceremonies.name}/`,
949
+ ""
950
+ )!
951
+ // == /ceremonies/{ceremony}/participants.
952
+ const ceremonyParticipantsCollectionPath =
953
+ `${createdContribution.ref.parent.parent?.parent?.parent?.path}/${commonTerms.collections.participants.name}`!
954
+
955
+ if (!createdContribution.data()) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
956
+
957
+ // Extract data.
958
+ const { participantId } = createdContribution.data()!
959
+
960
+ // Get documents from derived paths.
961
+ const circuits = await getCeremonyCircuits(ceremonyId)
962
+ const participantDoc = await getDocumentById(ceremonyParticipantsCollectionPath, participantId)
963
+
964
+ if (!participantDoc.data()) logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
965
+
966
+ // Extract data.
967
+ const { contributions, status, contributionProgress } = participantDoc.data()!
968
+
969
+ // Define pre-conditions.
970
+ const isFinalizing = status === ParticipantStatus.FINALIZING
971
+
972
+ // Link the newest created contribution document w/ participant contributions info.
973
+ // nb. there must be only one contribution with an empty doc.
974
+ contributions.forEach((participantContribution: Contribution) => {
975
+ // Define pre-conditions.
976
+ const isContributionWithoutDocRef =
977
+ !!participantContribution.hash &&
978
+ !!participantContribution.computationTime &&
979
+ !participantContribution.doc
980
+
981
+ if (isContributionWithoutDocRef) participantContribution.doc = createdContribution.id
982
+ })
983
+
984
+ // Check if the participant is not the coordinator trying to finalize the ceremony.
985
+ if (!isFinalizing)
986
+ batch.update(participantDoc.ref, {
987
+ // - DONE = provided a contribution for every circuit
988
+ // - CONTRIBUTED = some contribution still missing.
989
+ status:
990
+ contributionProgress + 1 > circuits.length ? ParticipantStatus.DONE : ParticipantStatus.CONTRIBUTED,
991
+ contributionStep: ParticipantContributionStep.COMPLETED,
992
+ tempContributionData: FieldValue.delete()
993
+ })
994
+
995
+ // nb. valid both for participant or coordinator (finalizing).
996
+ batch.update(participantDoc.ref, {
997
+ contributions,
998
+ lastUpdated: getCurrentServerTimestampInMillis()
999
+ })
1000
+
1001
+ await batch.commit()
1002
+
1003
+ printLog(
1004
+ `Participant ${participantId} refreshed after contribution ${createdContribution.id} - The participant was finalizing the ceremony ${isFinalizing}`,
1005
+ LogLevel.DEBUG
1006
+ )
1007
+ })
1008
+
1009
+ /**
1010
+ * Finalize the ceremony circuit.
1011
+ * @dev this cloud function stores the hashes and storage references of the Verifier smart contract
1012
+ * and verification key extracted from the circuit final contribution (as part of the ceremony finalization process).
1013
+ */
1014
+ export const finalizeCircuit = functionsV1
1015
+ .region('europe-west1')
1016
+ .runWith({
1017
+ memory: "512MB"
1018
+ })
1019
+ .https.onCall(async (data: FinalizeCircuitData, context: functionsV1.https.CallableContext) => {
1020
+ if (!context.auth || !context.auth.token.coordinator) logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE)
1021
+
1022
+ if (!data.ceremonyId || !data.circuitId || !data.bucketName || !data.beacon)
1023
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
1024
+
1025
+ // Get data.
1026
+ const { ceremonyId, circuitId, bucketName, beacon } = data
1027
+ const userId = context.auth?.uid
1028
+
1029
+ // Look for documents.
1030
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId)
1031
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId!)
1032
+ const circuitDoc = await getDocumentById(getCircuitsCollectionPath(ceremonyId), circuitId)
1033
+ const contributionDoc = await getFinalContribution(ceremonyId, circuitId)
1034
+
1035
+ if (!ceremonyDoc.data() || !circuitDoc.data() || !participantDoc.data() || !contributionDoc.data())
1036
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA)
1037
+
1038
+ // Extract data.
1039
+ const { prefix: circuitPrefix } = circuitDoc.data()!
1040
+ const { files } = contributionDoc.data()!
1041
+
1042
+ // Prepare filenames and storage paths.
1043
+ const verificationKeyFilename = `${circuitPrefix}_${verificationKeyAcronym}.json`
1044
+ const verifierContractFilename = `${circuitPrefix}_${verifierSmartContractAcronym}.sol`
1045
+ const verificationKeyStorageFilePath = getVerificationKeyStorageFilePath(circuitPrefix, verificationKeyFilename)
1046
+ const verifierContractStorageFilePath = getVerifierContractStorageFilePath(
1047
+ circuitPrefix,
1048
+ verifierContractFilename
1049
+ )
1050
+
1051
+ // Prepare temporary paths.
1052
+ const verificationKeyTemporaryFilePath = createTemporaryLocalPath(verificationKeyFilename)
1053
+ const verifierContractTemporaryFilePath = createTemporaryLocalPath(verifierContractFilename)
1054
+
1055
+ // Download artifact from ceremony bucket.
1056
+ await downloadArtifactFromS3Bucket(bucketName, verificationKeyStorageFilePath, verificationKeyTemporaryFilePath)
1057
+ await downloadArtifactFromS3Bucket(
1058
+ bucketName,
1059
+ verifierContractStorageFilePath,
1060
+ verifierContractTemporaryFilePath
1061
+ )
1062
+
1063
+ // Compute hash before unlink.
1064
+ const verificationKeyBlake2bHash = await blake512FromPath(verificationKeyTemporaryFilePath)
1065
+ const verifierContractBlake2bHash = await blake512FromPath(verifierContractTemporaryFilePath)
1066
+
1067
+ // Free resources by unlinking temporary folders.
1068
+ fs.unlinkSync(verificationKeyTemporaryFilePath)
1069
+ fs.unlinkSync(verifierContractTemporaryFilePath)
1070
+
1071
+ // Add references and hashes of the final contribution artifacts.
1072
+ await contributionDoc.ref.update({
1073
+ files: {
1074
+ ...files,
1075
+ verificationKeyBlake2bHash,
1076
+ verificationKeyFilename,
1077
+ verificationKeyStoragePath: verificationKeyStorageFilePath,
1078
+ verifierContractBlake2bHash,
1079
+ verifierContractFilename,
1080
+ verifierContractStoragePath: verifierContractStorageFilePath
1081
+ },
1082
+ beacon: {
1083
+ value: beacon,
1084
+ hash: computeSHA256ToHex(beacon)
1085
+ }
1086
+ })
1087
+
1088
+ printLog(
1089
+ `Circuit ${circuitId} finalization completed - Ceremony ${ceremonyDoc.id} - Coordinator ${participantDoc.id}`,
1090
+ LogLevel.DEBUG
1091
+ )
1092
+ })