@devtion/backend 0.0.0-09f6b45

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