@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,2588 @@
1
+ /**
2
+ * @module @p0tion/backend
3
+ * @version 1.1.0
4
+ * @file MPC Phase 2 backend for Firebase services management
5
+ * @copyright Ethereum Foundation 2022
6
+ * @license MIT
7
+ * @see [Github]{@link https://github.com/privacy-scaling-explorations/p0tion}
8
+ */
9
+ import admin from 'firebase-admin';
10
+ import * as functions from 'firebase-functions';
11
+ import dotenv from 'dotenv';
12
+ import { getCircuitsCollectionPath, getTimeoutsCollectionPath, commonTerms, finalContributionIndex, getContributionsCollectionPath, githubReputation, getBucketName, vmBootstrapCommand, vmDependenciesAndCacheArtifactsCommand, vmBootstrapScriptFilename, computeDiskSizeForVM, createEC2Instance, getParticipantsCollectionPath, terminateEC2Instance, formatZkeyIndex, getTranscriptStorageFilePath, getZkeyStorageFilePath, startEC2Instance, vmContributionVerificationCommand, runCommandUsingSSM, getPotStorageFilePath, genesisZkeyIndex, createCustomLoggerForFile, blake512FromPath, getVerificationKeyStorageFilePath, getVerifierContractStorageFilePath, computeSHA256ToHex, checkIfRunning, retrieveCommandOutput, stopEC2Instance, verificationKeyAcronym, verifierSmartContractAcronym, retrieveCommandStatus } from '@p0tion/actions';
13
+ import { encode } from 'html-entities';
14
+ import { Timestamp, FieldValue } from 'firebase-admin/firestore';
15
+ import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketCorsCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand } from '@aws-sdk/client-s3';
16
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
17
+ import { createWriteStream } from 'node:fs';
18
+ import { pipeline } from 'node:stream';
19
+ import { promisify } from 'node:util';
20
+ import fs, { readFileSync } from 'fs';
21
+ import mime from 'mime-types';
22
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
23
+ import fetch from '@adobe/node-fetch-retry';
24
+ import path from 'path';
25
+ import os from 'os';
26
+ import { SSMClient, CommandInvocationStatus } from '@aws-sdk/client-ssm';
27
+ import { EC2Client } from '@aws-sdk/client-ec2';
28
+ import * as functionsV1 from 'firebase-functions/v1';
29
+ import * as functionsV2 from 'firebase-functions/v2';
30
+ import { Timer } from 'timer-node';
31
+ import { zKey } from 'snarkjs';
32
+
33
+ /**
34
+ * Log levels.
35
+ * @notice useful to discriminate the log level for message printing.
36
+ * @enum {string}
37
+ */
38
+ var LogLevel;
39
+ (function (LogLevel) {
40
+ LogLevel["INFO"] = "INFO";
41
+ LogLevel["DEBUG"] = "DEBUG";
42
+ LogLevel["WARN"] = "WARN";
43
+ LogLevel["ERROR"] = "ERROR";
44
+ LogLevel["LOG"] = "LOG";
45
+ })(LogLevel || (LogLevel = {}));
46
+
47
+ /**
48
+ * Create a new custom HTTPs error for cloud functions.
49
+ * @notice the set of Firebase Functions status codes. The codes are the same at the
50
+ * ones exposed by {@link https://github.com/grpc/grpc/blob/master/doc/statuscodes.md | gRPC}.
51
+ * @param errorCode <FunctionsErrorCode> - the set of possible error codes.
52
+ * @param message <string> - the error messge.
53
+ * @param [details] <string> - the details of the error (optional).
54
+ * @returns <HttpsError>
55
+ */
56
+ const makeError = (errorCode, message, details) => new functions.https.HttpsError(errorCode, message, details);
57
+ /**
58
+ * Log a custom message on console using a specific level.
59
+ * @param message <string> - the message to be shown.
60
+ * @param logLevel <LogLevel> - the level of the log to be used to show the message (e.g., debug, error).
61
+ */
62
+ const printLog = (message, logLevel) => {
63
+ switch (logLevel) {
64
+ case LogLevel.INFO:
65
+ functions.logger.info(`[${logLevel}] ${message}`);
66
+ break;
67
+ case LogLevel.DEBUG:
68
+ functions.logger.debug(`[${logLevel}] ${message}`);
69
+ break;
70
+ case LogLevel.WARN:
71
+ functions.logger.warn(`[${logLevel}] ${message}`);
72
+ break;
73
+ case LogLevel.ERROR:
74
+ functions.logger.error(`[${logLevel}] ${message}`);
75
+ break;
76
+ case LogLevel.LOG:
77
+ functions.logger.log(`[${logLevel}] ${message}`);
78
+ break;
79
+ default:
80
+ console.log(`[${logLevel}] ${message}`);
81
+ break;
82
+ }
83
+ };
84
+ /**
85
+ * Log and throw an HTTPs error.
86
+ * @param error <HttpsError> - the error to be logged and thrown.
87
+ */
88
+ const logAndThrowError = (error) => {
89
+ printLog(`${error.code}: ${error.message} ${!error.details ? "" : `\ndetails: ${error.details}`}`, LogLevel.ERROR);
90
+ throw error;
91
+ };
92
+ /**
93
+ * A set of Cloud Function specific errors.
94
+ * @notice these are errors that happen only on specific cloud functions.
95
+ */
96
+ const SPECIFIC_ERRORS = {
97
+ SE_AUTH_NO_CURRENT_AUTH_USER: makeError("failed-precondition", "Unable to retrieve the authenticated user.", "Authenticated user information could not be retrieved. No document will be created in the relevant collection."),
98
+ SE_AUTH_SET_CUSTOM_USER_CLAIMS_FAIL: makeError("invalid-argument", "Unable to set custom claims for authenticated user."),
99
+ SE_AUTH_USER_NOT_REPUTABLE: makeError("permission-denied", "The authenticated user is not reputable.", "The authenticated user is not reputable. No document will be created in the relevant collection."),
100
+ SE_STORAGE_INVALID_BUCKET_NAME: makeError("already-exists", "Unable to create the AWS S3 bucket for the ceremony since the provided name is already in use. Please, provide a different bucket name for the ceremony.", "More info about the error could be found at the following link https://docs.aws.amazon.com/simspaceweaver/latest/userguide/troubleshooting_bucket-name-too-long.html"),
101
+ SE_STORAGE_TOO_MANY_BUCKETS: makeError("resource-exhausted", "Unable to create the AWS S3 bucket for the ceremony since the are too many buckets already in use. Please, delete 2 or more existing Amazon S3 buckets that you don't need or increase your limits.", "More info about the error could be found at the following link https://docs.aws.amazon.com/simspaceweaver/latest/userguide/troubeshooting_too-many-buckets.html"),
102
+ SE_STORAGE_MISSING_PERMISSIONS: makeError("permission-denied", "You do not have privileges to perform this operation.", "Authenticated user does not have proper permissions on AWS S3."),
103
+ SE_STORAGE_BUCKET_NOT_CONNECTED_TO_CEREMONY: makeError("not-found", "Unable to generate a pre-signed url for the given object in the provided bucket.", "The bucket is not associated with any valid ceremony document on the Firestore database."),
104
+ SE_STORAGE_WRONG_OBJECT_KEY: makeError("failed-precondition", "Unable to interact with a multi-part upload (start, create pre-signed urls or complete).", "The object key provided does not match the expected one."),
105
+ SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD: makeError("failed-precondition", "Unable to interact with a multi-part upload (start, create pre-signed urls or complete).", "Authenticated user is not a current contributor which is currently in the uploading step."),
106
+ SE_STORAGE_DOWNLOAD_FAILED: makeError("failed-precondition", "Unable to download the AWS S3 object from the provided ceremony bucket.", "This could happen if the file reference stored in the database or bucket turns out to be wrong or if the pre-signed url was not generated correctly."),
107
+ SE_STORAGE_UPLOAD_FAILED: makeError("failed-precondition", "Unable to upload the file to the AWS S3 ceremony bucket.", "This could happen if the local file or bucket do not exist or if the pre-signed url was not generated correctly."),
108
+ SE_STORAGE_DELETE_FAILED: makeError("failed-precondition", "Unable to delete the AWS S3 object from the provided ceremony bucket.", "This could happen if the local file or the bucket do not exist."),
109
+ SE_CONTRIBUTE_NO_CEREMONY_CIRCUITS: makeError("not-found", "There is no circuit associated with the ceremony.", "No documents in the circuits subcollection were found for the selected ceremony."),
110
+ SE_CONTRIBUTE_NO_OPENED_CEREMONIES: makeError("not-found", "There are no ceremonies open to contributions."),
111
+ SE_CONTRIBUTE_CANNOT_PROGRESS_TO_NEXT_CIRCUIT: makeError("failed-precondition", "Unable to progress to next circuit for contribution", "In order to progress for the contribution the participant must have just been registered for the ceremony or have just finished a contribution."),
112
+ SE_PARTICIPANT_CEREMONY_NOT_OPENED: makeError("failed-precondition", "Unable to progress to next contribution step.", "The ceremony does not appear to be opened"),
113
+ SE_PARTICIPANT_NOT_CONTRIBUTING: makeError("failed-precondition", "Unable to progress to next contribution step.", "This may happen due wrong contribution step from participant."),
114
+ SE_PARTICIPANT_CANNOT_STORE_PERMANENT_DATA: makeError("failed-precondition", "Unable to store contribution hash and computing time.", "This may happen due wrong contribution step from participant or missing coordinator permission (only when finalizing)."),
115
+ SE_PARTICIPANT_CANNOT_STORE_TEMPORARY_DATA: makeError("failed-precondition", "Unable to store temporary data to resume a multi-part upload.", "This may happen due wrong contribution step from participant."),
116
+ SE_VERIFICATION_NO_PARTICIPANT_CONTRIBUTION_DATA: makeError("not-found", `Unable to retrieve current contribution data from participant document.`),
117
+ SE_CEREMONY_CANNOT_FINALIZE_CEREMONY: makeError("failed-precondition", `Unable to finalize the ceremony.`, `Please, verify to have successfully completed the finalization of each circuit in the ceremony.`),
118
+ SE_FINALIZE_NO_CEREMONY_CONTRIBUTIONS: makeError("not-found", "There are no contributions associated with the ceremony circuit.", "No documents in the contributions subcollection were found for the selected ceremony circuit."),
119
+ SE_FINALIZE_NO_FINAL_CONTRIBUTION: makeError("not-found", "There is no final contribution associated with the ceremony circuit."),
120
+ SE_VM_NOT_RUNNING: makeError("failed-precondition", "The EC2 VM is not running yet"),
121
+ SE_VM_FAILED_COMMAND_EXECUTION: makeError("failed-precondition", "VM command execution failed", "Please, contact the coordinator if this error persists."),
122
+ SE_VM_TIMEDOUT_COMMAND_EXECUTION: makeError("deadline-exceeded", "VM command execution took too long and has been timed-out", "Please, contact the coordinator if this error persists."),
123
+ SE_VM_CANCELLED_COMMAND_EXECUTION: makeError("cancelled", "VM command execution has been cancelled", "Please, contact the coordinator if this error persists."),
124
+ SE_VM_DELAYED_COMMAND_EXECUTION: makeError("unavailable", "VM command execution has been delayed since there were no available instance at the moment", "Please, contact the coordinator if this error persists."),
125
+ SE_VM_UNKNOWN_COMMAND_STATUS: makeError("unavailable", "VM command execution has failed due to an unknown status code", "Please, contact the coordinator if this error persists.")
126
+ };
127
+ /**
128
+ * A set of common errors.
129
+ * @notice these are errors that happen on multiple cloud functions (e.g., auth, missing data).
130
+ */
131
+ const COMMON_ERRORS = {
132
+ CM_NOT_COORDINATOR_ROLE: makeError("permission-denied", "You do not have privileges to perform this operation.", "Authenticated user does not have the coordinator role (missing custom claims)."),
133
+ CM_MISSING_OR_WRONG_INPUT_DATA: makeError("invalid-argument", "Unable to perform the operation due to incomplete or incorrect data."),
134
+ CM_WRONG_CONFIGURATION: makeError("failed-precondition", "Missing or incorrect configuration.", "This may happen due wrong environment configuration for the backend services."),
135
+ CM_NOT_AUTHENTICATED: makeError("failed-precondition", "You are not authorized to perform this operation.", "You could not perform the requested operation because you are not authenticated on the Firebase Application."),
136
+ CM_INEXISTENT_DOCUMENT: makeError("not-found", "Unable to find a document with the given identifier for the provided collection path."),
137
+ CM_INEXISTENT_DOCUMENT_DATA: makeError("not-found", "The provided document with the given identifier has no data associated with it.", "This problem may occur if the document has not yet been written in the database."),
138
+ CM_INVALID_CEREMONY_FOR_PARTICIPANT: makeError("not-found", "The participant does not seem to be related to a ceremony."),
139
+ CM_NO_CIRCUIT_FOR_GIVEN_SEQUENCE_POSITION: makeError("not-found", "Unable to find the circuit having the provided sequence position for the given ceremony"),
140
+ CM_INVALID_REQUEST: makeError("unknown", "Failed request."),
141
+ CM_INVALID_COMMAND_EXECUTION: makeError("unknown", "There was an error while executing the command on the VM", "Please, contact the coordinator if the error persists.")
142
+ };
143
+
144
+ /**
145
+ * Return a configured and connected instance of the AWS S3 client.
146
+ * @dev this method check and utilize the environment variables to configure the connection
147
+ * w/ the S3 client.
148
+ * @returns <Promise<S3Client>> - the instance of the connected S3 Client instance.
149
+ */
150
+ const getS3Client = async () => {
151
+ if (!process.env.AWS_ACCESS_KEY_ID ||
152
+ !process.env.AWS_SECRET_ACCESS_KEY ||
153
+ !process.env.AWS_REGION ||
154
+ !process.env.AWS_PRESIGNED_URL_EXPIRATION ||
155
+ !process.env.AWS_CEREMONY_BUCKET_POSTFIX)
156
+ logAndThrowError(COMMON_ERRORS.CM_WRONG_CONFIGURATION);
157
+ // Return the connected S3 Client instance.
158
+ return new S3Client({
159
+ credentials: {
160
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
161
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
162
+ },
163
+ region: process.env.AWS_REGION
164
+ });
165
+ };
166
+
167
+ dotenv.config();
168
+ /**
169
+ * Get a specific document from database.
170
+ * @dev this method differs from the one in the `actions` package because we need to use
171
+ * the admin SDK here; therefore the Firestore instances are not interchangeable between admin
172
+ * and user instance.
173
+ * @param collection <string> - the name of the collection.
174
+ * @param documentId <string> - the unique identifier of the document in the collection.
175
+ * @returns <Promise<DocumentSnapshot<DocumentData>>> - the requested document w/ relative data.
176
+ */
177
+ const getDocumentById = async (collection, documentId) => {
178
+ // Prepare Firestore db instance.
179
+ const firestore = admin.firestore();
180
+ // Get document.
181
+ const doc = await firestore.collection(collection).doc(documentId).get();
182
+ // Return only if doc exists; otherwise throw error.
183
+ return doc.exists ? doc : logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT);
184
+ };
185
+ /**
186
+ * Get the current server timestamp.
187
+ * @dev the value is in milliseconds.
188
+ * @returns <number> - the timestamp of the server (ms).
189
+ */
190
+ const getCurrentServerTimestampInMillis = () => Timestamp.now().toMillis();
191
+ /**
192
+ * Interrupt the current execution for a specified amount of time.
193
+ * @param ms <number> - the amount of time expressed in milliseconds.
194
+ */
195
+ const sleep = async (ms) => setTimeout$1(ms);
196
+ /**
197
+ * Query for ceremony circuits.
198
+ * @notice the order by sequence position is fundamental to maintain parallelism among contributions for different circuits.
199
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
200
+ * @returns Promise<Array<FirebaseDocumentInfo>> - the ceremony' circuits documents ordered by sequence position.
201
+ */
202
+ const getCeremonyCircuits = async (ceremonyId) => {
203
+ // Prepare Firestore db instance.
204
+ const firestore = admin.firestore();
205
+ // Execute query.
206
+ const querySnap = await firestore.collection(getCircuitsCollectionPath(ceremonyId)).get();
207
+ if (!querySnap.docs)
208
+ logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_NO_CEREMONY_CIRCUITS);
209
+ return querySnap.docs.sort((a, b) => a.data().sequencePosition - b.data().sequencePosition);
210
+ };
211
+ /**
212
+ * Query for ceremony circuit contributions.
213
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
214
+ * @param circuitId <string> - the unique identifier of the circuitId.
215
+ * @returns Promise<Array<FirebaseDocumentInfo>> - the contributions of the ceremony circuit.
216
+ */
217
+ const getCeremonyCircuitContributions = async (ceremonyId, circuitId) => {
218
+ // Prepare Firestore db instance.
219
+ const firestore = admin.firestore();
220
+ // Execute query.
221
+ const querySnap = await firestore.collection(getContributionsCollectionPath(ceremonyId, circuitId)).get();
222
+ if (!querySnap.docs)
223
+ logAndThrowError(SPECIFIC_ERRORS.SE_FINALIZE_NO_CEREMONY_CONTRIBUTIONS);
224
+ return querySnap.docs;
225
+ };
226
+ /**
227
+ * Query not expired timeouts.
228
+ * @notice a timeout is considered valid (aka not expired) if and only if the timeout end date
229
+ * value is less than current timestamp.
230
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
231
+ * @param participantId <string> - the unique identifier of the participant.
232
+ * @returns <Promise<QuerySnapshot<DocumentData>>>
233
+ */
234
+ const queryNotExpiredTimeouts = async (ceremonyId, participantId) => {
235
+ // Prepare Firestore db.
236
+ const firestoreDb = admin.firestore();
237
+ // Execute and return query result.
238
+ return firestoreDb
239
+ .collection(getTimeoutsCollectionPath(ceremonyId, participantId))
240
+ .where(commonTerms.collections.timeouts.fields.endDate, ">=", getCurrentServerTimestampInMillis())
241
+ .get();
242
+ };
243
+ /**
244
+ * Query for opened ceremonies.
245
+ * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
246
+ * @returns <Promise<Array<FirebaseDocumentInfo>>>
247
+ */
248
+ const queryOpenedCeremonies = async () => {
249
+ const querySnap = await admin
250
+ .firestore()
251
+ .collection(commonTerms.collections.ceremonies.name)
252
+ .where(commonTerms.collections.ceremonies.fields.state, "==", "OPENED" /* CeremonyState.OPENED */)
253
+ .where(commonTerms.collections.ceremonies.fields.endDate, ">=", getCurrentServerTimestampInMillis())
254
+ .get();
255
+ if (!querySnap.docs)
256
+ logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_NO_OPENED_CEREMONIES);
257
+ return querySnap.docs;
258
+ };
259
+ /**
260
+ * Get ceremony circuit document by sequence position.
261
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
262
+ * @param sequencePosition <number> - the sequence position of the circuit.
263
+ * @returns Promise<QueryDocumentSnapshot<DocumentData>>
264
+ */
265
+ const getCircuitDocumentByPosition = async (ceremonyId, sequencePosition) => {
266
+ // Query for all ceremony circuits.
267
+ const circuits = await getCeremonyCircuits(ceremonyId);
268
+ // Apply a filter using the sequence position.
269
+ const matchedCircuits = circuits.filter((circuit) => circuit.data().sequencePosition === sequencePosition);
270
+ if (matchedCircuits.length !== 1)
271
+ logAndThrowError(COMMON_ERRORS.CM_NO_CIRCUIT_FOR_GIVEN_SEQUENCE_POSITION);
272
+ return matchedCircuits.at(0);
273
+ };
274
+ /**
275
+ * Create a temporary file path in the virtual memory of the cloud function.
276
+ * @dev useful when downloading files from AWS S3 buckets for processing within cloud functions.
277
+ * @param completeFilename <string> - the complete file name (name + ext).
278
+ * @returns <string> - the path to the local temporary location.
279
+ */
280
+ const createTemporaryLocalPath = (completeFilename) => path.join(os.tmpdir(), completeFilename);
281
+ /**
282
+ * Download an artifact from the AWS S3 bucket.
283
+ * @dev this method uses streams.
284
+ * @param bucketName <string> - the name of the bucket.
285
+ * @param objectKey <string> - the unique key to identify the object inside the given AWS S3 bucket.
286
+ * @param localFilePath <string> - the local path where the file will be stored.
287
+ */
288
+ const downloadArtifactFromS3Bucket = async (bucketName, objectKey, localFilePath) => {
289
+ // Prepare AWS S3 client instance.
290
+ const client = await getS3Client();
291
+ // Prepare command.
292
+ const command = new GetObjectCommand({ Bucket: bucketName, Key: objectKey });
293
+ // Generate a pre-signed url for downloading the file.
294
+ const url = await getSignedUrl(client, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION) });
295
+ // Execute download request.
296
+ // @ts-ignore
297
+ const response = await fetch(url, {
298
+ method: "GET",
299
+ headers: {
300
+ "Access-Control-Allow-Origin": "*"
301
+ }
302
+ });
303
+ if (response.status !== 200 || !response.ok)
304
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_DOWNLOAD_FAILED);
305
+ // Write the file locally using streams.
306
+ const writeStream = createWriteStream(localFilePath);
307
+ const streamPipeline = promisify(pipeline);
308
+ await streamPipeline(response.body, writeStream);
309
+ writeStream.on("finish", () => {
310
+ writeStream.end();
311
+ });
312
+ };
313
+ /**
314
+ * Upload a new artifact to the AWS S3 bucket.
315
+ * @dev this method uses streams.
316
+ * @param bucketName <string> - the name of the bucket.
317
+ * @param objectKey <string> - the unique key to identify the object inside the given AWS S3 bucket.
318
+ * @param localFilePath <string> - the local path where the file to be uploaded is stored.
319
+ */
320
+ const uploadFileToBucket = async (bucketName, objectKey, localFilePath, isPublic = false) => {
321
+ // Prepare AWS S3 client instance.
322
+ const client = await getS3Client();
323
+ // Extract content type.
324
+ const contentType = mime.lookup(localFilePath) || "";
325
+ // Prepare command.
326
+ const command = new PutObjectCommand({
327
+ Bucket: bucketName,
328
+ Key: objectKey,
329
+ ContentType: contentType,
330
+ ACL: isPublic ? "public-read" : "private"
331
+ });
332
+ // Generate a pre-signed url for uploading the file.
333
+ const url = await getSignedUrl(client, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION) });
334
+ // Execute upload request.
335
+ // @ts-ignore
336
+ const response = await fetch(url, {
337
+ method: "PUT",
338
+ body: readFileSync(localFilePath),
339
+ headers: { "Content-Type": contentType }
340
+ });
341
+ if (response.status !== 200 || !response.ok)
342
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_UPLOAD_FAILED);
343
+ };
344
+ const uploadFileToBucketNoFile = async (bucketName, objectKey, data, isPublic = false) => {
345
+ // Prepare AWS S3 client instance.
346
+ const client = await getS3Client();
347
+ // Prepare command.
348
+ const command = new PutObjectCommand({
349
+ Bucket: bucketName,
350
+ Key: objectKey,
351
+ ContentType: "text/plain",
352
+ ACL: isPublic ? "public-read" : "private"
353
+ });
354
+ // Generate a pre-signed url for uploading the file.
355
+ const url = await getSignedUrl(client, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION) });
356
+ // Execute upload request.
357
+ // @ts-ignore
358
+ const response = await fetch(url, {
359
+ method: "PUT",
360
+ body: data,
361
+ headers: { "Content-Type": "text/plain" }
362
+ });
363
+ if (response.status !== 200 || !response.ok)
364
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_UPLOAD_FAILED);
365
+ };
366
+ /**
367
+ * Upload an artifact from the AWS S3 bucket.
368
+ * @param bucketName <string> - the name of the bucket.
369
+ * @param objectKey <string> - the unique key to identify the object inside the given AWS S3 bucket.
370
+ */
371
+ const deleteObject = async (bucketName, objectKey) => {
372
+ // Prepare AWS S3 client instance.
373
+ const client = await getS3Client();
374
+ // Prepare command.
375
+ const command = new DeleteObjectCommand({ Bucket: bucketName, Key: objectKey });
376
+ // Execute command.
377
+ const data = await client.send(command);
378
+ if (data.$metadata.httpStatusCode !== 204)
379
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_DELETE_FAILED);
380
+ };
381
+ /**
382
+ * Query ceremonies by state and (start/end) date value.
383
+ * @param state <string> - the state of the ceremony.
384
+ * @param needToCheckStartDate <boolean> - flag to discriminate when to check startDate (true) or endDate (false).
385
+ * @param check <WhereFilerOp> - the type of filter (query check - e.g., '<' or '>').
386
+ * @returns <Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>>> - the queried ceremonies after filtering operation.
387
+ */
388
+ const queryCeremoniesByStateAndDate = async (state, needToCheckStartDate, check) => admin
389
+ .firestore()
390
+ .collection(commonTerms.collections.ceremonies.name)
391
+ .where(commonTerms.collections.ceremonies.fields.state, "==", state)
392
+ .where(needToCheckStartDate
393
+ ? commonTerms.collections.ceremonies.fields.startDate
394
+ : commonTerms.collections.ceremonies.fields.endDate, check, getCurrentServerTimestampInMillis())
395
+ .get();
396
+ /**
397
+ * Return the document associated with the final contribution for a ceremony circuit.
398
+ * @dev this method is useful during ceremony finalization.
399
+ * @param ceremonyId <string> -
400
+ * @param circuitId <string> -
401
+ * @returns Promise<QueryDocumentSnapshot<DocumentData>> - the final contribution for the ceremony circuit.
402
+ */
403
+ const getFinalContribution = async (ceremonyId, circuitId) => {
404
+ // Get contributions for the circuit.
405
+ const contributions = await getCeremonyCircuitContributions(ceremonyId, circuitId);
406
+ // Match the final one.
407
+ const matchContribution = contributions.filter((contribution) => contribution.data().zkeyIndex === finalContributionIndex);
408
+ if (!matchContribution)
409
+ logAndThrowError(SPECIFIC_ERRORS.SE_FINALIZE_NO_FINAL_CONTRIBUTION);
410
+ // Get the final contribution.
411
+ // nb. there must be only one final contributions x circuit.
412
+ const finalContribution = matchContribution.at(0);
413
+ return finalContribution;
414
+ };
415
+ /**
416
+ * Helper function to HTML encode circuit data.
417
+ * @param circuitDocument <CircuitDocument> - the circuit document to be encoded.
418
+ * @returns <CircuitDocument> - the circuit document encoded.
419
+ */
420
+ const htmlEncodeCircuitData = (circuitDocument) => ({
421
+ ...circuitDocument,
422
+ description: encode(circuitDocument.description),
423
+ name: encode(circuitDocument.name),
424
+ prefix: encode(circuitDocument.prefix)
425
+ });
426
+ /**
427
+ * Fetch the variables related to GitHub anti-sybil checks
428
+ * @returns <any> - the GitHub variables.
429
+ */
430
+ const getGitHubVariables = () => {
431
+ if (!process.env.GITHUB_MINIMUM_FOLLOWERS ||
432
+ !process.env.GITHUB_MINIMUM_FOLLOWING ||
433
+ !process.env.GITHUB_MINIMUM_PUBLIC_REPOS ||
434
+ !process.env.GITHUB_MINIMUM_AGE)
435
+ logAndThrowError(COMMON_ERRORS.CM_WRONG_CONFIGURATION);
436
+ return {
437
+ minimumFollowers: Number(process.env.GITHUB_MINIMUM_FOLLOWERS),
438
+ minimumFollowing: Number(process.env.GITHUB_MINIMUM_FOLLOWING),
439
+ minimumPublicRepos: Number(process.env.GITHUB_MINIMUM_PUBLIC_REPOS),
440
+ minimumAge: Number(process.env.GITHUB_MINIMUM_AGE)
441
+ };
442
+ };
443
+ /**
444
+ * Fetch the variables related to EC2 verification
445
+ * @returns <any> - the AWS EC2 variables.
446
+ */
447
+ const getAWSVariables = () => {
448
+ if (!process.env.AWS_ACCESS_KEY_ID ||
449
+ !process.env.AWS_SECRET_ACCESS_KEY ||
450
+ !process.env.AWS_INSTANCE_PROFILE_ARN ||
451
+ !process.env.AWS_AMI_ID ||
452
+ !process.env.AWS_SNS_TOPIC_ARN)
453
+ logAndThrowError(COMMON_ERRORS.CM_WRONG_CONFIGURATION);
454
+ return {
455
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
456
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
457
+ region: process.env.AWS_REGION || "eu-central-1",
458
+ instanceProfileArn: process.env.AWS_INSTANCE_PROFILE_ARN,
459
+ amiId: process.env.AWS_AMI_ID,
460
+ snsTopic: process.env.AWS_SNS_TOPIC_ARN
461
+ };
462
+ };
463
+ /**
464
+ * Create an EC2 client object
465
+ * @returns <Promise<EC2Client>> an EC2 client
466
+ */
467
+ const createEC2Client = async () => {
468
+ const { accessKeyId, secretAccessKey, region } = getAWSVariables();
469
+ const ec2 = new EC2Client({
470
+ credentials: {
471
+ accessKeyId,
472
+ secretAccessKey
473
+ },
474
+ region
475
+ });
476
+ return ec2;
477
+ };
478
+ /**
479
+ * Create an SSM client object
480
+ * @returns <Promise<SSMClient>> an SSM client
481
+ */
482
+ const createSSMClient = async () => {
483
+ const { accessKeyId, secretAccessKey, region } = getAWSVariables();
484
+ const ssm = new SSMClient({
485
+ credentials: {
486
+ accessKeyId,
487
+ secretAccessKey
488
+ },
489
+ region
490
+ });
491
+ return ssm;
492
+ };
493
+
494
+ dotenv.config();
495
+ /**
496
+ * Record the authenticated user information inside the Firestore DB upon authentication.
497
+ * @dev the data is recorded in a new document in the `users` collection.
498
+ * @notice this method is automatically triggered upon user authentication in the Firebase app
499
+ * which uses the Firebase Authentication service.
500
+ */
501
+ const registerAuthUser = functions
502
+ .region("europe-west1")
503
+ .runWith({
504
+ memory: "512MB"
505
+ })
506
+ .auth.user()
507
+ .onCreate(async (user) => {
508
+ // Get DB.
509
+ const firestore = admin.firestore();
510
+ // Get user information.
511
+ if (!user.uid)
512
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
513
+ // The user object has basic properties such as display name, email, etc.
514
+ const { displayName } = user;
515
+ const { email } = user;
516
+ const { photoURL } = user;
517
+ const { emailVerified } = user;
518
+ // Metadata.
519
+ const { creationTime } = user.metadata;
520
+ const { lastSignInTime } = user.metadata;
521
+ // The user's ID, unique to the Firebase project. Do NOT use
522
+ // this value to authenticate with your backend server, if
523
+ // you have one. Use User.getToken() instead.
524
+ const { uid } = user;
525
+ // Reference to a document using uid.
526
+ const userRef = firestore.collection(commonTerms.collections.users.name).doc(uid);
527
+ // html encode the display name (or put the ID if the name is not displayed)
528
+ const encodedDisplayName = user.displayName === "Null" || user.displayName === null ? user.uid : encode(displayName);
529
+ // store the avatar URL of a contributor
530
+ let avatarUrl = "";
531
+ // we only do reputation check if the user is not a coordinator
532
+ if (!(email?.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
533
+ email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN)) {
534
+ const auth = admin.auth();
535
+ // if provider == github.com let's use our functions to check the user's reputation
536
+ if (user.providerData[0].providerId === "github.com") {
537
+ const vars = getGitHubVariables();
538
+ // this return true or false
539
+ try {
540
+ const { reputable, avatarUrl: avatarURL } = await githubReputation(user.providerData[0].uid, vars.minimumFollowing, vars.minimumFollowers, vars.minimumPublicRepos, vars.minimumAge);
541
+ if (!reputable) {
542
+ // Delete user
543
+ await auth.deleteUser(user.uid);
544
+ // Throw error
545
+ logAndThrowError(makeError("permission-denied", "The user is not allowed to sign up because their Github reputation is not high enough.", `The user ${user.displayName === "Null" || user.displayName === null
546
+ ? user.uid
547
+ : user.displayName} is not allowed to sign up because their Github reputation is not high enough. Please contact the administrator if you think this is a mistake.`));
548
+ }
549
+ // store locally
550
+ avatarUrl = avatarURL;
551
+ printLog(`Github reputation check passed for user ${user.displayName === "Null" || user.displayName === null ? user.uid : user.displayName}`, LogLevel.DEBUG);
552
+ }
553
+ catch (error) {
554
+ // Delete user
555
+ await auth.deleteUser(user.uid);
556
+ logAndThrowError(makeError("permission-denied", "There was an error while checking the user's Github reputation.", `${error}`));
557
+ }
558
+ }
559
+ }
560
+ // Set document (nb. we refer to providerData[0] because we use Github OAuth provider only).
561
+ // In future releases we might want to loop through the providerData array as we support
562
+ // more providers.
563
+ await userRef.set({
564
+ name: encodedDisplayName,
565
+ encodedDisplayName,
566
+ // Metadata.
567
+ creationTime,
568
+ lastSignInTime,
569
+ // Optional.
570
+ email: email || "",
571
+ emailVerified: emailVerified || false,
572
+ photoURL: photoURL || "",
573
+ lastUpdated: getCurrentServerTimestampInMillis()
574
+ });
575
+ // we want to create a new collection for the users to store the avatars
576
+ const avatarRef = firestore.collection(commonTerms.collections.avatars.name).doc(uid);
577
+ await avatarRef.set({
578
+ avatarUrl: avatarUrl || ""
579
+ });
580
+ printLog(`Authenticated user document with identifier ${uid} has been correctly stored`, LogLevel.DEBUG);
581
+ printLog(`Authenticated user avatar with identifier ${uid} has been correctly stored`, LogLevel.DEBUG);
582
+ });
583
+ /**
584
+ * Set custom claims for role-based access control on the newly created user.
585
+ * @notice this method is automatically triggered upon user authentication in the Firebase app
586
+ * which uses the Firebase Authentication service.
587
+ */
588
+ const processSignUpWithCustomClaims = functions
589
+ .region("europe-west1")
590
+ .runWith({
591
+ memory: "512MB"
592
+ })
593
+ .auth.user()
594
+ .onCreate(async (user) => {
595
+ // Get user information.
596
+ if (!user.uid)
597
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
598
+ // Prepare state.
599
+ let customClaims;
600
+ // Check if user meets role criteria to be a coordinator.
601
+ if (user.email &&
602
+ (user.email.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) ||
603
+ user.email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN)) {
604
+ customClaims = { coordinator: true };
605
+ printLog(`Authenticated user ${user.uid} has been identified as coordinator`, LogLevel.DEBUG);
606
+ }
607
+ else {
608
+ customClaims = { participant: true };
609
+ printLog(`Authenticated user ${user.uid} has been identified as participant`, LogLevel.DEBUG);
610
+ }
611
+ try {
612
+ // Set custom user claims on this newly created user.
613
+ await admin.auth().setCustomUserClaims(user.uid, customClaims);
614
+ }
615
+ catch (error) {
616
+ const specificError = SPECIFIC_ERRORS.SE_AUTH_SET_CUSTOM_USER_CLAIMS_FAIL;
617
+ const additionalDetails = error.toString();
618
+ logAndThrowError(makeError(specificError.code, specificError.message, additionalDetails));
619
+ }
620
+ });
621
+
622
+ dotenv.config();
623
+ /**
624
+ * Make a scheduled ceremony open.
625
+ * @dev this function automatically runs every 30 minutes.
626
+ * @todo this methodology for transitioning a ceremony from `scheduled` to `opened` state will be replaced with one
627
+ * that resolves the issues presented in the issue #192 (https://github.com/quadratic-funding/mpc-phase2-suite/issues/192).
628
+ */
629
+ const startCeremony = functions
630
+ .region("europe-west1")
631
+ .runWith({
632
+ memory: "512MB"
633
+ })
634
+ .pubsub.schedule(`every 30 minutes`)
635
+ .onRun(async () => {
636
+ // Get ready to be opened ceremonies.
637
+ const scheduledCeremoniesQuerySnap = await queryCeremoniesByStateAndDate("SCHEDULED" /* CeremonyState.SCHEDULED */, true, "<=");
638
+ if (!scheduledCeremoniesQuerySnap.empty)
639
+ scheduledCeremoniesQuerySnap.forEach(async (ceremonyDoc) => {
640
+ // Make state transition to start ceremony.
641
+ await ceremonyDoc.ref.set({ state: "OPENED" /* CeremonyState.OPENED */ }, { merge: true });
642
+ printLog(`Ceremony ${ceremonyDoc.id} is now open`, LogLevel.DEBUG);
643
+ });
644
+ });
645
+ /**
646
+ * Make a scheduled ceremony close.
647
+ * @dev this function automatically runs every 30 minutes.
648
+ * @todo this methodology for transitioning a ceremony from `opened` to `closed` state will be replaced with one
649
+ * that resolves the issues presented in the issue #192 (https://github.com/quadratic-funding/mpc-phase2-suite/issues/192).
650
+ */
651
+ const stopCeremony = functions
652
+ .region("europe-west1")
653
+ .runWith({
654
+ memory: "512MB"
655
+ })
656
+ .pubsub.schedule(`every 30 minutes`)
657
+ .onRun(async () => {
658
+ // Get opened ceremonies.
659
+ const runningCeremoniesQuerySnap = await queryCeremoniesByStateAndDate("OPENED" /* CeremonyState.OPENED */, false, "<=");
660
+ if (!runningCeremoniesQuerySnap.empty) {
661
+ runningCeremoniesQuerySnap.forEach(async (ceremonyDoc) => {
662
+ // Make state transition to close ceremony.
663
+ await ceremonyDoc.ref.set({ state: "CLOSED" /* CeremonyState.CLOSED */ }, { merge: true });
664
+ printLog(`Ceremony ${ceremonyDoc.id} is now closed`, LogLevel.DEBUG);
665
+ });
666
+ }
667
+ });
668
+ /**
669
+ * Register all ceremony setup-related documents on the Firestore database.
670
+ * @dev this function will create a new document in the `ceremonies` collection and as needed `circuit`
671
+ * documents in the sub-collection.
672
+ */
673
+ const setupCeremony = functions
674
+ .region("europe-west1")
675
+ .runWith({
676
+ memory: "512MB"
677
+ })
678
+ .https.onCall(async (data, context) => {
679
+ // Check if the user has the coordinator claim.
680
+ if (!context.auth || !context.auth.token.coordinator)
681
+ logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE);
682
+ // Validate the provided data.
683
+ if (!data.ceremonyInputData || !data.ceremonyPrefix || !data.circuits.length)
684
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
685
+ // Prepare Firestore DB.
686
+ const firestore = admin.firestore();
687
+ const batch = firestore.batch();
688
+ // Prepare data.
689
+ const { ceremonyInputData, ceremonyPrefix, circuits } = data;
690
+ const userId = context.auth?.uid;
691
+ // Create a new ceremony document.
692
+ const ceremonyDoc = await firestore.collection(`${commonTerms.collections.ceremonies.name}`).doc().get();
693
+ // Prepare tx to write ceremony data.
694
+ batch.create(ceremonyDoc.ref, {
695
+ title: encode(ceremonyInputData.title),
696
+ description: encode(ceremonyInputData.description),
697
+ startDate: new Date(ceremonyInputData.startDate).valueOf(),
698
+ endDate: new Date(ceremonyInputData.endDate).valueOf(),
699
+ prefix: ceremonyPrefix,
700
+ state: "SCHEDULED" /* CeremonyState.SCHEDULED */,
701
+ type: "PHASE2" /* CeremonyType.PHASE2 */,
702
+ penalty: ceremonyInputData.penalty,
703
+ timeoutType: ceremonyInputData.timeoutMechanismType,
704
+ coordinatorId: userId,
705
+ lastUpdated: getCurrentServerTimestampInMillis()
706
+ });
707
+ // Get the bucket name so we can upload the startup script
708
+ const bucketName = getBucketName(ceremonyPrefix, String(process.env.AWS_CEREMONY_BUCKET_POSTFIX));
709
+ // Create a new circuit document (circuits ceremony document sub-collection).
710
+ for (let circuit of circuits) {
711
+ // The VM unique identifier (if any).
712
+ let vmInstanceId = "";
713
+ // Get a new circuit document.
714
+ const circuitDoc = await firestore.collection(getCircuitsCollectionPath(ceremonyDoc.ref.id)).doc().get();
715
+ // Check if using the VM approach for contribution verification.
716
+ if (circuit.verification.cfOrVm === "VM" /* CircuitContributionVerificationMechanism.VM */) {
717
+ // VM command to be run at the startup.
718
+ const startupCommand = vmBootstrapCommand(`${bucketName}/circuits/${circuit.name}`);
719
+ // Get EC2 client.
720
+ const ec2Client = await createEC2Client();
721
+ // Get AWS variables.
722
+ const { snsTopic, region } = getAWSVariables();
723
+ // Prepare dependencies and cache artifacts command.
724
+ const vmCommands = vmDependenciesAndCacheArtifactsCommand(`${bucketName}/${circuit.files?.initialZkeyStoragePath}`, `${bucketName}/${circuit.files?.potStoragePath}`, snsTopic, region);
725
+ printLog(`Check VM dependencies and cache artifacts commands ${vmCommands.join("\n")}`, LogLevel.DEBUG);
726
+ // Upload the post-startup commands script file.
727
+ printLog(`Uploading VM post-startup commands script file ${vmBootstrapScriptFilename}`, LogLevel.DEBUG);
728
+ await uploadFileToBucketNoFile(bucketName, `circuits/${circuit.name}/${vmBootstrapScriptFilename}`, vmCommands.join("\n"));
729
+ // Compute the VM disk space requirement (in GB).
730
+ const vmDiskSize = computeDiskSizeForVM(circuit.zKeySizeInBytes, circuit.metadata?.pot);
731
+ printLog(`Check VM startup commands ${startupCommand.join("\n")}`, LogLevel.DEBUG);
732
+ // Configure and instantiate a new VM based on the coordinator input.
733
+ const instance = await createEC2Instance(ec2Client, startupCommand, circuit.verification.vm?.vmConfigurationType, vmDiskSize, circuit.verification.vm?.vmDiskType);
734
+ // Get the VM instance identifier.
735
+ vmInstanceId = instance.instanceId;
736
+ // Update the circuit document info accordingly.
737
+ circuit = {
738
+ ...circuit,
739
+ verification: {
740
+ cfOrVm: circuit.verification.cfOrVm,
741
+ vm: {
742
+ vmConfigurationType: circuit.verification.vm?.vmConfigurationType,
743
+ vmDiskSize,
744
+ vmInstanceId
745
+ }
746
+ }
747
+ };
748
+ }
749
+ // Encode circuit data.
750
+ const encodedCircuit = htmlEncodeCircuitData(circuit);
751
+ // Prepare tx to write circuit data.
752
+ batch.create(circuitDoc.ref, {
753
+ ...encodedCircuit,
754
+ lastUpdated: getCurrentServerTimestampInMillis()
755
+ });
756
+ }
757
+ // Send txs in a batch (to avoid race conditions).
758
+ await batch.commit();
759
+ printLog(`Setup completed for ceremony ${ceremonyDoc.id}`, LogLevel.DEBUG);
760
+ return ceremonyDoc.id;
761
+ });
762
+ /**
763
+ * Prepare all the necessary information needed for initializing the waiting queue of a circuit.
764
+ * @dev this function will add a new field `waitingQueue` in the newly created circuit document.
765
+ */
766
+ const initEmptyWaitingQueueForCircuit = functions
767
+ .region("europe-west1")
768
+ .runWith({
769
+ memory: "512MB"
770
+ })
771
+ .firestore.document(`/${commonTerms.collections.ceremonies.name}/{ceremony}/${commonTerms.collections.circuits.name}/{circuit}`)
772
+ .onCreate(async (doc) => {
773
+ // Prepare Firestore DB.
774
+ const firestore = admin.firestore();
775
+ // Get circuit document identifier and data.
776
+ const circuitId = doc.id;
777
+ // Get parent ceremony collection path.
778
+ const parentCollectionPath = doc.ref.parent.path; // == /ceremonies/{ceremony}/circuits/.
779
+ // Define an empty waiting queue.
780
+ const emptyWaitingQueue = {
781
+ contributors: [],
782
+ currentContributor: "",
783
+ completedContributions: 0,
784
+ failedContributions: 0
785
+ };
786
+ // Update the circuit document.
787
+ await firestore.collection(parentCollectionPath).doc(circuitId).set({
788
+ waitingQueue: emptyWaitingQueue,
789
+ lastUpdated: getCurrentServerTimestampInMillis()
790
+ }, { merge: true });
791
+ printLog(`An empty waiting queue has been successfully initialized for circuit ${circuitId} which belongs to ceremony ${doc.id}`, LogLevel.DEBUG);
792
+ });
793
+ /**
794
+ * Conclude the finalization of the ceremony.
795
+ * @dev checks that the ceremony is closed (= CLOSED), the coordinator is finalizing and has already
796
+ * provided the final contribution for each ceremony circuit.
797
+ */
798
+ const finalizeCeremony = functions
799
+ .region("europe-west1")
800
+ .runWith({
801
+ memory: "512MB"
802
+ })
803
+ .https.onCall(async (data, context) => {
804
+ if (!context.auth || !context.auth.token.coordinator)
805
+ logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE);
806
+ if (!data.ceremonyId)
807
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
808
+ // Prepare Firestore DB.
809
+ const firestore = admin.firestore();
810
+ const batch = firestore.batch();
811
+ // Extract data.
812
+ const { ceremonyId } = data;
813
+ const userId = context.auth?.uid;
814
+ // Look for the ceremony document.
815
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
816
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId);
817
+ if (!ceremonyDoc.data() || !participantDoc.data())
818
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
819
+ // Get ceremony circuits.
820
+ const circuits = await getCeremonyCircuits(ceremonyId);
821
+ // Get final contribution for each circuit.
822
+ // nb. the `getFinalContributionDocument` checks the existance of the final contribution document (if not present, throws).
823
+ // Therefore, we just need to call the method without taking any data to verify the pre-condition of having already computed
824
+ // the final contributions for each ceremony circuit.
825
+ for await (const circuit of circuits)
826
+ await getFinalContribution(ceremonyId, circuit.id);
827
+ // Extract data.
828
+ const { state } = ceremonyDoc.data();
829
+ const { status } = participantDoc.data();
830
+ // Pre-conditions: verify the ceremony is closed and coordinator is finalizing.
831
+ if (state === "CLOSED" /* CeremonyState.CLOSED */ && status === "FINALIZING" /* ParticipantStatus.FINALIZING */) {
832
+ // Prepare txs for updates.
833
+ batch.update(ceremonyDoc.ref, { state: "FINALIZED" /* CeremonyState.FINALIZED */ });
834
+ batch.update(participantDoc.ref, {
835
+ status: "FINALIZED" /* ParticipantStatus.FINALIZED */
836
+ });
837
+ // Check for VM termination (if any).
838
+ for (const circuit of circuits) {
839
+ const circuitData = circuit.data();
840
+ const { verification } = circuitData;
841
+ if (verification.cfOrVm === "VM" /* CircuitContributionVerificationMechanism.VM */) {
842
+ // Prepare EC2 client.
843
+ const ec2Client = await createEC2Client();
844
+ const { vm } = verification;
845
+ await terminateEC2Instance(ec2Client, vm.vmInstanceId);
846
+ }
847
+ }
848
+ // Send txs.
849
+ await batch.commit();
850
+ printLog(`Ceremony ${ceremonyDoc.id} correctly finalized - Coordinator ${participantDoc.id}`, LogLevel.INFO);
851
+ }
852
+ else
853
+ logAndThrowError(SPECIFIC_ERRORS.SE_CEREMONY_CANNOT_FINALIZE_CEREMONY);
854
+ });
855
+
856
+ dotenv.config();
857
+ /**
858
+ * Check the user's current participant status for the ceremony.
859
+ * @notice this cloud function has several tasks:
860
+ * 1) Check if the authenticated user is a participant
861
+ * 1.A) If not, register it has new participant for the ceremony.
862
+ * 1.B) Otherwise:
863
+ * 2.A) Check if already contributed to all circuits or,
864
+ * 3.A) If already contributed, return false
865
+ * 2.B) Check if it has a timeout in progress
866
+ * 3.B) If timeout expired, allows the participant to resume the contribution and remove stale/outdated
867
+ * temporary data.
868
+ * 3.C) Otherwise, return false.
869
+ * 2.C) Check if there are temporary stale contribution data if the contributor has interrupted the contribution
870
+ * while completing the `COMPUTING` step and, if any, delete them.
871
+ * 1.D) If no timeout / participant already exist, just return true.
872
+ * @dev true when the participant can participate (1.A, 3.B, 1.D); otherwise false.
873
+ */
874
+ const checkParticipantForCeremony = functions
875
+ .region("europe-west1")
876
+ .runWith({
877
+ memory: "512MB"
878
+ })
879
+ .https.onCall(async (data, context) => {
880
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
881
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
882
+ if (!data.ceremonyId)
883
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
884
+ // Prepare Firestore DB.
885
+ const firestore = admin.firestore();
886
+ // Get data.
887
+ const { ceremonyId } = data;
888
+ const userId = context.auth?.uid;
889
+ // Look for the ceremony document.
890
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
891
+ // Extract data.
892
+ const ceremonyData = ceremonyDoc.data();
893
+ const { state } = ceremonyData;
894
+ if (!ceremonyData)
895
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
896
+ // Check pre-condition (ceremony state opened).
897
+ if (state !== "OPENED" /* CeremonyState.OPENED */)
898
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CEREMONY_NOT_OPENED);
899
+ // Check (1).
900
+ // nb. do not use `getDocumentById()` here as we need the falsy condition.
901
+ const participantDoc = await firestore.collection(getParticipantsCollectionPath(ceremonyId)).doc(userId).get();
902
+ if (!participantDoc.exists) {
903
+ // Action (1.A).
904
+ const participantData = {
905
+ userId: participantDoc.id,
906
+ status: "WAITING" /* ParticipantStatus.WAITING */,
907
+ contributionProgress: 0,
908
+ contributionStartedAt: 0,
909
+ contributions: [],
910
+ lastUpdated: getCurrentServerTimestampInMillis()
911
+ };
912
+ // Register user as participant.
913
+ await participantDoc.ref.set(participantData);
914
+ printLog(`The user ${userId} has been registered as participant for ceremony ${ceremonyDoc.id}`, LogLevel.DEBUG);
915
+ return true;
916
+ }
917
+ // Check (1.B).
918
+ // Extract data.
919
+ const participantData = participantDoc.data();
920
+ const { contributionProgress, contributionStep, contributions, status, tempContributionData } = participantData;
921
+ if (!participantData)
922
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
923
+ // Get ceremony' circuits.
924
+ const circuits = await getCeremonyCircuits(ceremonyDoc.id);
925
+ // Check (2.A).
926
+ if (contributionProgress === circuits.length && status === "DONE" /* ParticipantStatus.DONE */) {
927
+ // Action (3.A).
928
+ printLog(`Contributor ${participantDoc.id} has already contributed to all circuits`, LogLevel.DEBUG);
929
+ return false;
930
+ }
931
+ // Pre-conditions.
932
+ const staleContributionData = contributionProgress >= 1 && contributions.length === contributionProgress;
933
+ const wasComputing = !!contributionStep && contributionStep === "COMPUTING" /* ParticipantContributionStep.COMPUTING */;
934
+ // Check (2.B).
935
+ if (status === "TIMEDOUT" /* ParticipantStatus.TIMEDOUT */) {
936
+ // Query for not expired timeouts.
937
+ const notExpiredTimeouts = await queryNotExpiredTimeouts(ceremonyDoc.id, participantDoc.id);
938
+ if (notExpiredTimeouts.empty) {
939
+ // nb. stale contribution data is always the latest contribution.
940
+ if (staleContributionData)
941
+ contributions.pop();
942
+ // Action (3.B).
943
+ participantDoc.ref.update({
944
+ status: "EXHUMED" /* ParticipantStatus.EXHUMED */,
945
+ contributions,
946
+ tempContributionData: tempContributionData || FieldValue.delete(),
947
+ contributionStep: "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */,
948
+ contributionStartedAt: 0,
949
+ verificationStartedAt: FieldValue.delete(),
950
+ lastUpdated: getCurrentServerTimestampInMillis()
951
+ });
952
+ printLog(`Timeout expired for participant ${participantDoc.id}`, LogLevel.DEBUG);
953
+ return true;
954
+ }
955
+ // Action (3.C).
956
+ printLog(`Timeout still in effect for the participant ${participantDoc.id}`, LogLevel.DEBUG);
957
+ return false;
958
+ }
959
+ // Check (2.C).
960
+ if (staleContributionData && wasComputing) {
961
+ // nb. stale contribution data is always the latest contribution.
962
+ contributions.pop();
963
+ participantDoc.ref.update({
964
+ contributions,
965
+ lastUpdated: getCurrentServerTimestampInMillis()
966
+ });
967
+ printLog(`Removed stale contribution data for ${participantDoc.id}`, LogLevel.DEBUG);
968
+ }
969
+ // Action (1.D).
970
+ return true;
971
+ });
972
+ /**
973
+ * Progress the participant to the next circuit preparing for the next contribution.
974
+ * @dev The participant can progress if and only if:
975
+ * 1) the participant has just been registered and is waiting to be queued for the first contribution (contributionProgress = 0 && status = WAITING).
976
+ * 2) the participant has just finished the contribution for a circuit (contributionProgress != 0 && status = CONTRIBUTED && contributionStep = COMPLETED).
977
+ */
978
+ const progressToNextCircuitForContribution = functions
979
+ .region("europe-west1")
980
+ .runWith({
981
+ memory: "512MB"
982
+ })
983
+ .https.onCall(async (data, context) => {
984
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
985
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
986
+ if (!data.ceremonyId)
987
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
988
+ // Get data.
989
+ const { ceremonyId } = data;
990
+ const userId = context.auth?.uid;
991
+ // Look for the ceremony document.
992
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
993
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId);
994
+ // Prepare documents data.
995
+ const participantData = participantDoc.data();
996
+ if (!ceremonyDoc.data() || !participantData)
997
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
998
+ // Extract data.
999
+ const { contributionProgress, contributionStep, status } = participantData;
1000
+ // Define pre-conditions.
1001
+ const waitingToBeQueuedForFirstContribution = status === "WAITING" /* ParticipantStatus.WAITING */ && contributionProgress === 0;
1002
+ const completedContribution = status === "CONTRIBUTED" /* ParticipantStatus.CONTRIBUTED */ &&
1003
+ contributionStep === "COMPLETED" /* ParticipantContributionStep.COMPLETED */ &&
1004
+ contributionProgress !== 0;
1005
+ // Check pre-conditions (1) or (2).
1006
+ if (completedContribution || waitingToBeQueuedForFirstContribution)
1007
+ await participantDoc.ref.update({
1008
+ contributionProgress: contributionProgress + 1,
1009
+ status: "READY" /* ParticipantStatus.READY */,
1010
+ lastUpdated: getCurrentServerTimestampInMillis()
1011
+ });
1012
+ else
1013
+ logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_CANNOT_PROGRESS_TO_NEXT_CIRCUIT);
1014
+ printLog(`Participant/Contributor ${userId} progress to the circuit in position ${contributionProgress + 1}`, LogLevel.DEBUG);
1015
+ });
1016
+ /**
1017
+ * Progress the participant to the next contribution step while contributing to a circuit.
1018
+ * @dev this cloud function must enforce the order among the contribution steps:
1019
+ * 1) Downloading the last contribution.
1020
+ * 2) Computing the next contribution.
1021
+ * 3) Uploading the next contribution.
1022
+ * 4) Requesting the verification to the cloud function `verifycontribution`.
1023
+ * 5) Completed contribution computation and verification.
1024
+ */
1025
+ const progressToNextContributionStep = functions
1026
+ .region("europe-west1")
1027
+ .runWith({
1028
+ memory: "512MB"
1029
+ })
1030
+ .https.onCall(async (data, context) => {
1031
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
1032
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
1033
+ if (!data.ceremonyId)
1034
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
1035
+ // Get data.
1036
+ const { ceremonyId } = data;
1037
+ const userId = context.auth?.uid;
1038
+ // Look for the ceremony document.
1039
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1040
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId);
1041
+ if (!ceremonyDoc.data() || !participantDoc.data())
1042
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1043
+ // Extract data.
1044
+ const { state } = ceremonyDoc.data();
1045
+ const { status, contributionStep } = participantDoc.data();
1046
+ // Pre-condition: ceremony must be opened.
1047
+ if (state !== "OPENED" /* CeremonyState.OPENED */)
1048
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CEREMONY_NOT_OPENED);
1049
+ // Pre-condition: participant has contributing status.
1050
+ if (status !== "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */)
1051
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_NOT_CONTRIBUTING);
1052
+ // Prepare the next contribution step.
1053
+ let nextContributionStep = contributionStep;
1054
+ if (contributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */)
1055
+ nextContributionStep = "COMPUTING" /* ParticipantContributionStep.COMPUTING */;
1056
+ else if (contributionStep === "COMPUTING" /* ParticipantContributionStep.COMPUTING */)
1057
+ nextContributionStep = "UPLOADING" /* ParticipantContributionStep.UPLOADING */;
1058
+ else if (contributionStep === "UPLOADING" /* ParticipantContributionStep.UPLOADING */)
1059
+ nextContributionStep = "VERIFYING" /* ParticipantContributionStep.VERIFYING */;
1060
+ else if (contributionStep === "VERIFYING" /* ParticipantContributionStep.VERIFYING */)
1061
+ nextContributionStep = "COMPLETED" /* ParticipantContributionStep.COMPLETED */;
1062
+ // Send tx.
1063
+ await participantDoc.ref.update({
1064
+ contributionStep: nextContributionStep,
1065
+ verificationStartedAt: nextContributionStep === "VERIFYING" /* ParticipantContributionStep.VERIFYING */
1066
+ ? getCurrentServerTimestampInMillis()
1067
+ : 0,
1068
+ lastUpdated: getCurrentServerTimestampInMillis()
1069
+ });
1070
+ printLog(`Participant ${participantDoc.id} advanced to ${nextContributionStep} contribution step`, LogLevel.DEBUG);
1071
+ });
1072
+ /**
1073
+ * Write the information about current contribution hash and computation time for the current contributor.
1074
+ * @dev enable the current contributor to resume a contribution from where it had left off.
1075
+ */
1076
+ const permanentlyStoreCurrentContributionTimeAndHash = functions
1077
+ .region("europe-west1")
1078
+ .runWith({
1079
+ memory: "512MB"
1080
+ })
1081
+ .https.onCall(async (data, context) => {
1082
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
1083
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
1084
+ if (!data.ceremonyId || !data.contributionHash || data.contributionComputationTime <= 0)
1085
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
1086
+ // Get data.
1087
+ const { ceremonyId } = data;
1088
+ const userId = context.auth?.uid;
1089
+ const isCoordinator = context?.auth?.token.coordinator;
1090
+ // Look for the ceremony document.
1091
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1092
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId);
1093
+ if (!ceremonyDoc.data() || !participantDoc.data())
1094
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1095
+ // Extract data.
1096
+ const { status, contributionStep, contributions: currentContributions } = participantDoc.data();
1097
+ // Pre-condition: computing contribution step or finalizing (only for coordinator when finalizing ceremony).
1098
+ if (contributionStep === "COMPUTING" /* ParticipantContributionStep.COMPUTING */ ||
1099
+ (isCoordinator && status === "FINALIZING" /* ParticipantStatus.FINALIZING */))
1100
+ // Send tx.
1101
+ await participantDoc.ref.set({
1102
+ contributions: [
1103
+ ...currentContributions,
1104
+ {
1105
+ hash: data.contributionHash,
1106
+ computationTime: data.contributionComputationTime
1107
+ }
1108
+ ]
1109
+ }, { merge: true });
1110
+ else
1111
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CANNOT_STORE_PERMANENT_DATA);
1112
+ printLog(`Participant ${participantDoc.id} has successfully stored the contribution hash ${data.contributionHash} and computation time ${data.contributionComputationTime}`, LogLevel.DEBUG);
1113
+ });
1114
+ /**
1115
+ * Write temporary information about the unique identifier about the opened multi-part upload to eventually resume the contribution.
1116
+ * @dev enable the current contributor to resume a multi-part upload from where it had left off.
1117
+ */
1118
+ const temporaryStoreCurrentContributionMultiPartUploadId = functions
1119
+ .region("europe-west1")
1120
+ .runWith({
1121
+ memory: "512MB"
1122
+ })
1123
+ .https.onCall(async (data, context) => {
1124
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
1125
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
1126
+ if (!data.ceremonyId || !data.uploadId)
1127
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
1128
+ // Get data.
1129
+ const { ceremonyId, uploadId } = data;
1130
+ const userId = context.auth?.uid;
1131
+ // Look for the ceremony document.
1132
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1133
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId);
1134
+ if (!ceremonyDoc.data() || !participantDoc.data())
1135
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1136
+ // Extract data.
1137
+ const { contributionStep, tempContributionData: currentTempContributionData } = participantDoc.data();
1138
+ // Pre-condition: check if the current contributor has uploading contribution step.
1139
+ if (contributionStep !== "UPLOADING" /* ParticipantContributionStep.UPLOADING */)
1140
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CANNOT_STORE_TEMPORARY_DATA);
1141
+ // Send tx.
1142
+ await participantDoc.ref.set({
1143
+ tempContributionData: {
1144
+ ...currentTempContributionData,
1145
+ uploadId,
1146
+ chunks: []
1147
+ },
1148
+ lastUpdated: getCurrentServerTimestampInMillis()
1149
+ }, { merge: true });
1150
+ printLog(`Participant ${participantDoc.id} has successfully stored the temporary data for ${uploadId} multi-part upload`, LogLevel.DEBUG);
1151
+ });
1152
+ /**
1153
+ * Write temporary information about the etags and part numbers for each uploaded chunk in order to make the upload resumable from last chunk.
1154
+ * @dev enable the current contributor to resume a multi-part upload from where it had left off.
1155
+ */
1156
+ const temporaryStoreCurrentContributionUploadedChunkData = functions
1157
+ .region("europe-west1")
1158
+ .runWith({
1159
+ memory: "512MB"
1160
+ })
1161
+ .https.onCall(async (data, context) => {
1162
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
1163
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
1164
+ if (!data.ceremonyId || !data.chunk)
1165
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
1166
+ // Get data.
1167
+ const { ceremonyId, chunk } = data;
1168
+ const userId = context.auth?.uid;
1169
+ // Look for the ceremony document.
1170
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1171
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyDoc.id), userId);
1172
+ if (!ceremonyDoc.data() || !participantDoc.data())
1173
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1174
+ // Extract data.
1175
+ const { contributionStep, tempContributionData: currentTempContributionData } = participantDoc.data();
1176
+ // Pre-condition: check if the current contributor has uploading contribution step.
1177
+ if (contributionStep !== "UPLOADING" /* ParticipantContributionStep.UPLOADING */)
1178
+ logAndThrowError(SPECIFIC_ERRORS.SE_PARTICIPANT_CANNOT_STORE_TEMPORARY_DATA);
1179
+ // Get already uploaded chunks.
1180
+ const chunks = currentTempContributionData.chunks ? currentTempContributionData.chunks : [];
1181
+ // Push last chunk.
1182
+ chunks.push(chunk);
1183
+ // Update.
1184
+ await participantDoc.ref.set({
1185
+ tempContributionData: {
1186
+ ...currentTempContributionData,
1187
+ chunks
1188
+ },
1189
+ lastUpdated: getCurrentServerTimestampInMillis()
1190
+ }, { merge: true });
1191
+ printLog(`Participant ${participantDoc.id} has successfully stored the temporary uploaded chunk data: ETag ${chunk.ETag} and PartNumber ${chunk.PartNumber}`, LogLevel.DEBUG);
1192
+ });
1193
+ /**
1194
+ * Prepare the coordinator for the finalization of the ceremony.
1195
+ * @dev checks that the ceremony is closed (= CLOSED) and that the coordinator has already +
1196
+ * contributed to every selected ceremony circuits (= DONE).
1197
+ */
1198
+ const checkAndPrepareCoordinatorForFinalization = functions
1199
+ .region("europe-west1")
1200
+ .runWith({
1201
+ memory: "512MB"
1202
+ })
1203
+ .https.onCall(async (data, context) => {
1204
+ if (!context.auth || !context.auth.token.coordinator)
1205
+ logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE);
1206
+ if (!data.ceremonyId)
1207
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
1208
+ // Get data.
1209
+ const { ceremonyId } = data;
1210
+ const userId = context.auth?.uid;
1211
+ // Look for the ceremony document.
1212
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1213
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId);
1214
+ if (!ceremonyDoc.data() || !participantDoc.data())
1215
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1216
+ // Get ceremony circuits.
1217
+ const circuits = await getCeremonyCircuits(ceremonyId);
1218
+ // Extract data.
1219
+ const { state } = ceremonyDoc.data();
1220
+ const { contributionProgress, status } = participantDoc.data();
1221
+ // Check pre-conditions.
1222
+ if (state === "CLOSED" /* CeremonyState.CLOSED */ &&
1223
+ status === "DONE" /* ParticipantStatus.DONE */ &&
1224
+ contributionProgress === circuits.length) {
1225
+ // Make coordinator ready for finalization.
1226
+ await participantDoc.ref.set({
1227
+ status: "FINALIZING" /* ParticipantStatus.FINALIZING */,
1228
+ lastUpdated: getCurrentServerTimestampInMillis()
1229
+ }, { merge: true });
1230
+ printLog(`The coordinator ${participantDoc.id} is now ready to finalize the ceremony ${ceremonyId}.`, LogLevel.DEBUG);
1231
+ return true;
1232
+ }
1233
+ printLog(`The coordinator ${participantDoc.id} is not ready to finalize the ceremony ${ceremonyId}.`, LogLevel.DEBUG);
1234
+ return false;
1235
+ });
1236
+
1237
+ dotenv.config();
1238
+ /**
1239
+ * Execute the coordination of the participant for the given circuit.
1240
+ * @dev possible coordination scenarios:
1241
+ * A) The participant becomes the current contributor of circuit X (single participant).
1242
+ * B) The participant is placed in the contribution waiting queue because someone else is currently contributing to circuit X (single participant)
1243
+ * C) The participant is removed as current contributor from Circuit X and gets coordinated for Circuit X + 1 (multi-participant).
1244
+ * C.1) The first participant in the waiting queue for Circuit X (if any), becomes the new contributor for circuit X.
1245
+ * @param participant <QueryDocumentSnapshot> - the Firestore document of the participant.
1246
+ * @param circuit <QueryDocumentSnapshot> - the Firestore document of the circuit.
1247
+ * @param isSingleParticipantCoordination <boolean> - true if the coordination involves only a single participant; otherwise false (= involves multiple participant).
1248
+ * @param [ceremonyId] <string> - the unique identifier of the ceremony (needed only for multi-participant coordination).
1249
+ */
1250
+ const coordinate = async (participant, circuit, isSingleParticipantCoordination, ceremonyId) => {
1251
+ // Prepare db and transactions batch.
1252
+ const firestore = admin.firestore();
1253
+ const batch = firestore.batch();
1254
+ // Extract data.
1255
+ const { status, contributionStep } = participant.data();
1256
+ const { waitingQueue } = circuit.data();
1257
+ const { contributors, currentContributor } = waitingQueue;
1258
+ // Prepare state updates for waiting queue.
1259
+ const newContributors = contributors;
1260
+ let newCurrentContributorId = "";
1261
+ // Prepare state updates for participant.
1262
+ let newParticipantStatus = "";
1263
+ let newContributionStep = "";
1264
+ // Prepare pre-conditions.
1265
+ const noCurrentContributor = !currentContributor;
1266
+ const noContributorsInWaitingQueue = !contributors.length;
1267
+ const emptyWaitingQueue = noCurrentContributor && noContributorsInWaitingQueue;
1268
+ const participantIsNotCurrentContributor = currentContributor !== participant.id;
1269
+ const participantIsCurrentContributor = currentContributor === participant.id;
1270
+ const participantIsReady = status === "READY" /* ParticipantStatus.READY */;
1271
+ const participantResumingAfterTimeoutExpiration = participantIsCurrentContributor && participantIsReady;
1272
+ const participantCompletedOneOrAllContributions = (status === "CONTRIBUTED" /* ParticipantStatus.CONTRIBUTED */ || status === "DONE" /* ParticipantStatus.DONE */) &&
1273
+ contributionStep === "COMPLETED" /* ParticipantContributionStep.COMPLETED */;
1274
+ // Check for scenarios.
1275
+ if (isSingleParticipantCoordination) {
1276
+ // Scenario (A).
1277
+ if (emptyWaitingQueue) {
1278
+ printLog(`Coordinate - executing scenario A - emptyWaitingQueue`, LogLevel.DEBUG);
1279
+ // Update.
1280
+ newCurrentContributorId = participant.id;
1281
+ newParticipantStatus = "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */;
1282
+ newContributionStep = "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */;
1283
+ newContributors.push(newCurrentContributorId);
1284
+ }
1285
+ // Scenario (A).
1286
+ else if (participantResumingAfterTimeoutExpiration) {
1287
+ printLog(`Coordinate - executing scenario A - single - participantResumingAfterTimeoutExpiration`, LogLevel.DEBUG);
1288
+ newParticipantStatus = "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */;
1289
+ newContributionStep = "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */;
1290
+ newCurrentContributorId = participant.id;
1291
+ }
1292
+ // Scenario (B).
1293
+ else if (participantIsNotCurrentContributor) {
1294
+ printLog(`Coordinate - executing scenario B - single - participantIsNotCurrentContributor`, LogLevel.DEBUG);
1295
+ newCurrentContributorId = currentContributor;
1296
+ newParticipantStatus = "WAITING" /* ParticipantStatus.WAITING */;
1297
+ newContributors.push(participant.id);
1298
+ }
1299
+ // Prepare tx - Scenario (A) only.
1300
+ if (newContributionStep)
1301
+ batch.update(participant.ref, {
1302
+ contributionStep: newContributionStep,
1303
+ lastUpdated: getCurrentServerTimestampInMillis()
1304
+ });
1305
+ // Prepare tx - Scenario (A) or (B).
1306
+ batch.update(participant.ref, {
1307
+ status: newParticipantStatus,
1308
+ contributionStartedAt: newParticipantStatus === "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */ ? getCurrentServerTimestampInMillis() : 0,
1309
+ lastUpdated: getCurrentServerTimestampInMillis()
1310
+ });
1311
+ }
1312
+ else if (participantIsCurrentContributor && participantCompletedOneOrAllContributions && !!ceremonyId) {
1313
+ printLog(`Coordinate - executing scenario C - multi - participantIsCurrentContributor && participantCompletedOneOrAllContributions`, LogLevel.DEBUG);
1314
+ newParticipantStatus = "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */;
1315
+ newContributionStep = "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */;
1316
+ // Remove from waiting queue of circuit X.
1317
+ newContributors.shift();
1318
+ // Step (C.1).
1319
+ if (newContributors.length > 0) {
1320
+ // Get new contributor for circuit X.
1321
+ newCurrentContributorId = newContributors.at(0);
1322
+ // Pass the baton to the new contributor.
1323
+ const newCurrentContributorDocument = await getDocumentById(getParticipantsCollectionPath(ceremonyId), newCurrentContributorId);
1324
+ // Prepare update tx.
1325
+ batch.update(newCurrentContributorDocument.ref, {
1326
+ status: newParticipantStatus,
1327
+ contributionStep: newContributionStep,
1328
+ contributionStartedAt: getCurrentServerTimestampInMillis(),
1329
+ lastUpdated: getCurrentServerTimestampInMillis()
1330
+ });
1331
+ printLog(`Participant ${newCurrentContributorId} is the new current contributor for circuit ${circuit.id}`, LogLevel.DEBUG);
1332
+ }
1333
+ }
1334
+ // Prepare tx - must be done for all Scenarios.
1335
+ batch.update(circuit.ref, {
1336
+ waitingQueue: {
1337
+ ...waitingQueue,
1338
+ contributors: newContributors,
1339
+ currentContributor: newCurrentContributorId
1340
+ },
1341
+ lastUpdated: getCurrentServerTimestampInMillis()
1342
+ });
1343
+ // Send txs.
1344
+ await batch.commit();
1345
+ printLog(`Coordinate successfully completed`, LogLevel.DEBUG);
1346
+ };
1347
+ /**
1348
+ * Wait until the command has completed its execution inside the VM.
1349
+ * @dev this method implements a custom interval to check 5 times after 1 minute if the command execution
1350
+ * has been completed or not by calling the `retrieveCommandStatus` method.
1351
+ * @param {SSMClient} ssm the SSM client.
1352
+ * @param {string} vmInstanceId the unique identifier of the VM instance.
1353
+ * @param {string} commandId the unique identifier of the VM command.
1354
+ * @returns <Promise<void>> true when the command execution succeed; otherwise false.
1355
+ */
1356
+ const waitForVMCommandExecution = (ssm, vmInstanceId, commandId) => new Promise((resolve, reject) => {
1357
+ const poll = async () => {
1358
+ try {
1359
+ // Get command status.
1360
+ const cmdStatus = await retrieveCommandStatus(ssm, vmInstanceId, commandId);
1361
+ printLog(`Checking command ${commandId} status => ${cmdStatus}`, LogLevel.DEBUG);
1362
+ let error;
1363
+ switch (cmdStatus) {
1364
+ case CommandInvocationStatus.CANCELLING:
1365
+ case CommandInvocationStatus.CANCELLED: {
1366
+ error = SPECIFIC_ERRORS.SE_VM_CANCELLED_COMMAND_EXECUTION;
1367
+ break;
1368
+ }
1369
+ case CommandInvocationStatus.DELAYED: {
1370
+ error = SPECIFIC_ERRORS.SE_VM_DELAYED_COMMAND_EXECUTION;
1371
+ break;
1372
+ }
1373
+ case CommandInvocationStatus.FAILED: {
1374
+ error = SPECIFIC_ERRORS.SE_VM_FAILED_COMMAND_EXECUTION;
1375
+ break;
1376
+ }
1377
+ case CommandInvocationStatus.TIMED_OUT: {
1378
+ error = SPECIFIC_ERRORS.SE_VM_TIMEDOUT_COMMAND_EXECUTION;
1379
+ break;
1380
+ }
1381
+ case CommandInvocationStatus.IN_PROGRESS:
1382
+ case CommandInvocationStatus.PENDING: {
1383
+ // wait a minute and poll again
1384
+ setTimeout(poll, 60000);
1385
+ return;
1386
+ }
1387
+ case CommandInvocationStatus.SUCCESS: {
1388
+ printLog(`Command ${commandId} successfully completed`, LogLevel.DEBUG);
1389
+ // Resolve the promise.
1390
+ resolve();
1391
+ return;
1392
+ }
1393
+ default: {
1394
+ logAndThrowError(SPECIFIC_ERRORS.SE_VM_UNKNOWN_COMMAND_STATUS);
1395
+ }
1396
+ }
1397
+ if (error) {
1398
+ logAndThrowError(error);
1399
+ }
1400
+ }
1401
+ catch (error) {
1402
+ printLog(`Invalid command ${commandId} execution`, LogLevel.DEBUG);
1403
+ const ec2 = await createEC2Client();
1404
+ // if it errors out, let's just log it as a warning so the coordinator is aware
1405
+ try {
1406
+ await stopEC2Instance(ec2, vmInstanceId);
1407
+ }
1408
+ catch (error) {
1409
+ printLog(`Error while stopping VM instance ${vmInstanceId} - Error ${error}`, LogLevel.WARN);
1410
+ }
1411
+ if (!error.toString().includes(commandId))
1412
+ logAndThrowError(COMMON_ERRORS.CM_INVALID_COMMAND_EXECUTION);
1413
+ // Reject the promise.
1414
+ reject();
1415
+ }
1416
+ };
1417
+ setTimeout(poll, 60000);
1418
+ });
1419
+ /**
1420
+ * This method is used to coordinate the waiting queues of ceremony circuits.
1421
+ * @dev this cloud function is triggered whenever an update of a document related to a participant of a ceremony occurs.
1422
+ * The function verifies that such update is preparatory towards a waiting queue update for one or more circuits in the ceremony.
1423
+ * If that's the case, this cloud functions proceeds with the "coordination" of the waiting queues, leading to three different scenarios:
1424
+ * A) The participant becomes the current contributor of circuit X (single participant).
1425
+ * B) The participant is placed in the contribution waiting queue because someone else is currently contributing to circuit X (single participant)
1426
+ * C) The participant is removed as current contributor from Circuit X and gets coordinated for Circuit X + 1 (multi-participant).
1427
+ * C.1) The first participant in the waiting queue for Circuit X (if any), becomes the new contributor for circuit X.
1428
+ * Before triggering the above scenarios, the cloud functions verifies that suitable pre-conditions are met.
1429
+ * @notice The cloud function performs the subsequent steps:
1430
+ * 0) Prepares the participant's previous and current data (after/before document change).
1431
+ * 1) Retrieve the ceremony from the participant's document path.
1432
+ * 2) Verifies that the participant has changed to a state for which it is ready for contribution.
1433
+ * 2.A) If ready, verifies whether the participant is ready to:
1434
+ * - 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).
1435
+ * 2.B) Otherwise, check whether the participant has:
1436
+ * - Just completed a contribution or all contributions for each circuit. If yes, coordinate (multi-participant scenario).
1437
+ */
1438
+ const coordinateCeremonyParticipant = functionsV1
1439
+ .region("europe-west1")
1440
+ .runWith({
1441
+ memory: "512MB"
1442
+ })
1443
+ .firestore.document(`${commonTerms.collections.ceremonies.name}/{ceremonyId}/${commonTerms.collections.participants.name}/{participantId}`)
1444
+ .onUpdate(async (participantChanges) => {
1445
+ // Step (0).
1446
+ const exParticipant = participantChanges.before;
1447
+ const changedParticipant = participantChanges.after;
1448
+ if (!exParticipant.data() || !changedParticipant.data())
1449
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1450
+ // Step (1).
1451
+ const ceremonyId = exParticipant.ref.parent.parent.path.replace(`${commonTerms.collections.ceremonies.name}/`, "");
1452
+ if (!ceremonyId)
1453
+ logAndThrowError(COMMON_ERRORS.CM_INVALID_CEREMONY_FOR_PARTICIPANT);
1454
+ // Extract data.
1455
+ const { contributionProgress: prevContributionProgress, status: prevStatus, contributionStep: prevContributionStep } = exParticipant.data();
1456
+ const { contributionProgress: changedContributionProgress, status: changedStatus, contributionStep: changedContributionStep } = changedParticipant.data();
1457
+ printLog(`Coordinate participant ${exParticipant.id} for ceremony ${ceremonyId}`, LogLevel.DEBUG);
1458
+ printLog(`Participant status: ${prevStatus} => ${changedStatus} - Participant contribution step: ${prevContributionStep} => ${changedContributionStep}`, LogLevel.DEBUG);
1459
+ // Define pre-conditions.
1460
+ const participantReadyToContribute = changedStatus === "READY" /* ParticipantStatus.READY */;
1461
+ const participantReadyForFirstContribution = participantReadyToContribute && prevContributionProgress === 0;
1462
+ const participantResumingContributionAfterTimeout = participantReadyToContribute && prevContributionProgress === changedContributionProgress;
1463
+ const participantReadyForNextContribution = participantReadyToContribute &&
1464
+ prevContributionProgress === changedContributionProgress - 1 &&
1465
+ prevContributionProgress !== 0;
1466
+ const participantCompletedEveryCircuitContribution = changedStatus === "DONE" /* ParticipantStatus.DONE */ && prevStatus !== "DONE" /* ParticipantStatus.DONE */;
1467
+ const participantCompletedContribution = prevContributionProgress === changedContributionProgress &&
1468
+ prevStatus === "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */ &&
1469
+ prevContributionStep === "VERIFYING" /* ParticipantContributionStep.VERIFYING */ &&
1470
+ changedStatus === "CONTRIBUTED" /* ParticipantStatus.CONTRIBUTED */ &&
1471
+ changedContributionStep === "COMPLETED" /* ParticipantContributionStep.COMPLETED */;
1472
+ // Step (2).
1473
+ if (participantReadyForFirstContribution ||
1474
+ participantResumingContributionAfterTimeout ||
1475
+ participantReadyForNextContribution) {
1476
+ // Step (2.A).
1477
+ printLog(`Participant is ready for first contribution (${participantReadyForFirstContribution}) or for the next contribution (${participantReadyForNextContribution}) or is resuming after a timeout expiration (${participantResumingContributionAfterTimeout})`, LogLevel.DEBUG);
1478
+ // Get the circuit.
1479
+ const circuit = await getCircuitDocumentByPosition(ceremonyId, changedContributionProgress);
1480
+ // Coordinate.
1481
+ await coordinate(changedParticipant, circuit, true);
1482
+ printLog(`Coordination for circuit ${circuit.id} completed`, LogLevel.DEBUG);
1483
+ }
1484
+ else if (participantCompletedContribution || participantCompletedEveryCircuitContribution) {
1485
+ // Step (2.B).
1486
+ printLog(`Participant completed a contribution (${participantCompletedContribution}) or every contribution for each circuit (${participantCompletedEveryCircuitContribution})`, LogLevel.DEBUG);
1487
+ // Get the circuit.
1488
+ const circuit = await getCircuitDocumentByPosition(ceremonyId, prevContributionProgress);
1489
+ // Coordinate.
1490
+ await coordinate(changedParticipant, circuit, false, ceremonyId);
1491
+ printLog(`Coordination for circuit ${circuit.id} completed`, LogLevel.DEBUG);
1492
+ }
1493
+ });
1494
+ /**
1495
+ * Recursive function to check whether an EC2 is in a running state
1496
+ * @notice required step to run commands
1497
+ * @param ec2 <EC2Client> - the EC2Client object
1498
+ * @param vmInstanceId <string> - the instance Id
1499
+ * @param attempts <number> - how many times to retry before failing
1500
+ * @returns <Promise<boolean>> - whether the VM was started
1501
+ */
1502
+ const checkIfVMRunning = async (ec2, vmInstanceId, attempts = 5) => {
1503
+ // if we tried 5 times, then throw an error
1504
+ if (attempts <= 0)
1505
+ logAndThrowError(SPECIFIC_ERRORS.SE_VM_NOT_RUNNING);
1506
+ await sleep(60000); // Wait for 1 min
1507
+ const isVMRunning = await checkIfRunning(ec2, vmInstanceId);
1508
+ if (!isVMRunning) {
1509
+ printLog(`VM not running, ${attempts - 1} attempts remaining. Retrying in 1 minute...`, LogLevel.DEBUG);
1510
+ return checkIfVMRunning(ec2, vmInstanceId, attempts - 1);
1511
+ }
1512
+ return true;
1513
+ };
1514
+ /**
1515
+ * Verify the contribution of a participant computed while contributing to a specific circuit of a ceremony.
1516
+ * @dev a huge amount of resources (memory, CPU, and execution time) is required for the contribution verification task.
1517
+ * For this reason, we are using a V2 Cloud Function (more memory, more CPU, and longer timeout).
1518
+ * Through the current configuration (16GiB memory and 4 vCPUs) we are able to support verification of contributions for 3.8M constraints circuit size.
1519
+ @notice The cloud function performs the subsequent steps:
1520
+ * 0) Prepare documents and extract necessary data.
1521
+ * 1) Check if the participant is the current contributor to the circuit or is the ceremony coordinator
1522
+ * 1.A) If either condition is true:
1523
+ * 1.A.1) Prepare verification transcript logger, storage, and temporary paths.
1524
+ * 1.A.2) Download necessary AWS S3 ceremony bucket artifacts.
1525
+ * 1.A.3) Execute contribution verification.
1526
+ * 1.A.3.0) Check if is using VM or CF approach for verification.
1527
+ * 1.A.3.1) Start the instance and wait until the instance is up.
1528
+ * 1.A.3.2) Prepare and run contribution verification command.
1529
+ * 1.A.3.3) Wait until command complete.
1530
+ * 1.A.4) Check contribution validity:
1531
+ * 1.A.4.A) If valid:
1532
+ * 1.A.4.A.1) Upload verification transcript to AWS S3 storage.
1533
+ * 1.A.4.A.2) Creates a new valid contribution document on Firestore.
1534
+ * 1.A.4.B) If not valid:
1535
+ * 1.A.4.B.1) Creates a new invalid contribution document on Firestore.
1536
+ * 1.A.4.C) Check if not finalizing:
1537
+ * 1.A.4.C.1) If true, update circuit waiting for queue and average timings accordingly to contribution verification results;
1538
+ * 2) Send all updates atomically to the Firestore database.
1539
+ */
1540
+ const verifycontribution = functionsV2.https.onCall({ memory: "16GiB", timeoutSeconds: 3600, region: "europe-west1" }, async (request) => {
1541
+ if (!request.auth || (!request.auth.token.participant && !request.auth.token.coordinator))
1542
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
1543
+ if (!request.data.ceremonyId ||
1544
+ !request.data.circuitId ||
1545
+ !request.data.contributorOrCoordinatorIdentifier ||
1546
+ !request.data.bucketName)
1547
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
1548
+ if (!process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME ||
1549
+ !process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION ||
1550
+ !process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH)
1551
+ logAndThrowError(COMMON_ERRORS.CM_WRONG_CONFIGURATION);
1552
+ // Step (0).
1553
+ // Prepare and start timer.
1554
+ const verifyContributionTimer = new Timer({ label: commonTerms.cloudFunctionsNames.verifyContribution });
1555
+ verifyContributionTimer.start();
1556
+ // Get DB.
1557
+ const firestore = admin.firestore();
1558
+ // Prepare batch of txs.
1559
+ const batch = firestore.batch();
1560
+ // Extract data.
1561
+ const { ceremonyId, circuitId, contributorOrCoordinatorIdentifier, bucketName } = request.data;
1562
+ const userId = request.auth?.uid;
1563
+ // Look for the ceremony, circuit and participant document.
1564
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1565
+ const circuitDoc = await getDocumentById(getCircuitsCollectionPath(ceremonyId), circuitId);
1566
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId);
1567
+ if (!ceremonyDoc.data() || !circuitDoc.data() || !participantDoc.data())
1568
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1569
+ // Extract documents data.
1570
+ const { state } = ceremonyDoc.data();
1571
+ const { status, contributions, verificationStartedAt, contributionStartedAt } = participantDoc.data();
1572
+ const { waitingQueue, prefix, avgTimings, verification, files } = circuitDoc.data();
1573
+ const { completedContributions, failedContributions } = waitingQueue;
1574
+ const { contributionComputation: avgContributionComputationTime, fullContribution: avgFullContributionTime, verifyCloudFunction: avgVerifyCloudFunctionTime } = avgTimings;
1575
+ const { cfOrVm, vm } = verification;
1576
+ // we might not have it if the circuit is not using VM.
1577
+ let vmInstanceId = "";
1578
+ if (vm)
1579
+ vmInstanceId = vm.vmInstanceId;
1580
+ // Define pre-conditions.
1581
+ const isFinalizing = state === "CLOSED" /* CeremonyState.CLOSED */ && request.auth && request.auth.token.coordinator; // true only when the coordinator verifies the final contributions.
1582
+ const isContributing = status === "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */;
1583
+ const isUsingVM = cfOrVm === "VM" /* CircuitContributionVerificationMechanism.VM */ && !!vmInstanceId;
1584
+ // Prepare state.
1585
+ let isContributionValid = false;
1586
+ let verifyCloudFunctionExecutionTime = 0; // time spent while executing the verify contribution cloud function.
1587
+ let verifyCloudFunctionTime = 0; // time spent while executing the core business logic of this cloud function.
1588
+ let fullContributionTime = 0; // time spent while doing non-verification contributions tasks (download, compute, upload).
1589
+ let contributionComputationTime = 0; // time spent while computing the contribution.
1590
+ let lastZkeyBlake2bHash = ""; // the Blake2B hash of the last zKey.
1591
+ let verificationTranscriptTemporaryLocalPath = ""; // the local temporary path for the verification transcript.
1592
+ let transcriptBlake2bHash = ""; // the Blake2B hash of the verification transcript.
1593
+ let commandId = ""; // the unique identifier of the VM command.
1594
+ // Derive necessary data.
1595
+ const lastZkeyIndex = formatZkeyIndex(completedContributions + 1);
1596
+ const verificationTranscriptCompleteFilename = `${prefix}_${isFinalizing
1597
+ ? `${contributorOrCoordinatorIdentifier}_${finalContributionIndex}_verification_transcript.log`
1598
+ : `${lastZkeyIndex}_${contributorOrCoordinatorIdentifier}_verification_transcript.log`}`;
1599
+ const lastZkeyFilename = `${prefix}_${isFinalizing ? finalContributionIndex : lastZkeyIndex}.zkey`;
1600
+ // Prepare state for VM verification (if needed).
1601
+ const ec2 = await createEC2Client();
1602
+ const ssm = await createSSMClient();
1603
+ // Step (1.A.1).
1604
+ // Get storage paths.
1605
+ const verificationTranscriptStoragePathAndFilename = getTranscriptStorageFilePath(prefix, verificationTranscriptCompleteFilename);
1606
+ // the zKey storage path is required to be sent to the VM api
1607
+ const lastZkeyStoragePath = getZkeyStorageFilePath(prefix, `${prefix}_${isFinalizing ? finalContributionIndex : lastZkeyIndex}.zkey`);
1608
+ const verificationTaskTimer = new Timer({ label: `${ceremonyId}-${circuitId}-${participantDoc.id}` });
1609
+ const completeVerification = async () => {
1610
+ // Stop verification task timer.
1611
+ printLog("Completing verification", LogLevel.DEBUG);
1612
+ verificationTaskTimer.stop();
1613
+ verifyCloudFunctionExecutionTime = verificationTaskTimer.ms();
1614
+ if (isUsingVM) {
1615
+ // Create temporary path.
1616
+ verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}.log`);
1617
+ await sleep(1000); // wait 1s for file creation.
1618
+ // Download from bucket.
1619
+ // nb. the transcript MUST be uploaded from the VM by verification commands.
1620
+ await downloadArtifactFromS3Bucket(bucketName, verificationTranscriptStoragePathAndFilename, verificationTranscriptTemporaryLocalPath);
1621
+ // Read the verification trascript and validate data by checking for core info ("ZKey Ok!").
1622
+ const content = fs.readFileSync(verificationTranscriptTemporaryLocalPath, "utf-8");
1623
+ if (content.includes("ZKey Ok!"))
1624
+ isContributionValid = true;
1625
+ // If the contribution is valid, then format and store the trascript.
1626
+ if (isContributionValid) {
1627
+ // eslint-disable-next-line no-control-regex
1628
+ const updated = content.replace(/\x1b[[0-9;]*m/g, "");
1629
+ fs.writeFileSync(verificationTranscriptTemporaryLocalPath, updated);
1630
+ }
1631
+ }
1632
+ printLog(`The contribution has been verified - Result ${isContributionValid}`, LogLevel.DEBUG);
1633
+ // Create a new contribution document.
1634
+ const contributionDoc = await firestore
1635
+ .collection(getContributionsCollectionPath(ceremonyId, circuitId))
1636
+ .doc()
1637
+ .get();
1638
+ // Step (1.A.4).
1639
+ if (isContributionValid) {
1640
+ // Sleep ~3 seconds to wait for verification transcription.
1641
+ await sleep(3000);
1642
+ // Step (1.A.4.A.1).
1643
+ if (isUsingVM) {
1644
+ // Retrieve the contribution hash from the command output.
1645
+ lastZkeyBlake2bHash = await retrieveCommandOutput(ssm, vmInstanceId, commandId);
1646
+ const hashRegex = /[a-fA-F0-9]{64}/;
1647
+ const match = lastZkeyBlake2bHash.match(hashRegex);
1648
+ lastZkeyBlake2bHash = match.at(0);
1649
+ // re upload the formatted verification transcript
1650
+ await uploadFileToBucket(bucketName, verificationTranscriptStoragePathAndFilename, verificationTranscriptTemporaryLocalPath, true);
1651
+ }
1652
+ else {
1653
+ // Upload verification transcript.
1654
+ /// nb. do not use multi-part upload here due to small file size.
1655
+ await uploadFileToBucket(bucketName, verificationTranscriptStoragePathAndFilename, verificationTranscriptTemporaryLocalPath, true);
1656
+ }
1657
+ // Compute verification transcript hash.
1658
+ transcriptBlake2bHash = await blake512FromPath(verificationTranscriptTemporaryLocalPath);
1659
+ // Free resources by unlinking transcript temporary file.
1660
+ fs.unlinkSync(verificationTranscriptTemporaryLocalPath);
1661
+ // Filter participant contributions to find the data related to the one verified.
1662
+ const participantContributions = contributions.filter((contribution) => !!contribution.hash && !!contribution.computationTime && !contribution.doc);
1663
+ /// @dev (there must be only one contribution with an empty 'doc' field).
1664
+ if (participantContributions.length !== 1)
1665
+ logAndThrowError(SPECIFIC_ERRORS.SE_VERIFICATION_NO_PARTICIPANT_CONTRIBUTION_DATA);
1666
+ // Get contribution computation time.
1667
+ contributionComputationTime = contributions.at(0).computationTime;
1668
+ // Step (1.A.4.A.2).
1669
+ batch.create(contributionDoc.ref, {
1670
+ participantId: participantDoc.id,
1671
+ contributionComputationTime,
1672
+ verificationComputationTime: verifyCloudFunctionExecutionTime,
1673
+ zkeyIndex: isFinalizing ? finalContributionIndex : lastZkeyIndex,
1674
+ files: {
1675
+ transcriptFilename: verificationTranscriptCompleteFilename,
1676
+ lastZkeyFilename,
1677
+ transcriptStoragePath: verificationTranscriptStoragePathAndFilename,
1678
+ lastZkeyStoragePath,
1679
+ transcriptBlake2bHash,
1680
+ lastZkeyBlake2bHash
1681
+ },
1682
+ verificationSoftware: {
1683
+ name: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME),
1684
+ version: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION),
1685
+ commitHash: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH)
1686
+ },
1687
+ valid: isContributionValid,
1688
+ lastUpdated: getCurrentServerTimestampInMillis()
1689
+ });
1690
+ verifyContributionTimer.stop();
1691
+ verifyCloudFunctionTime = verifyContributionTimer.ms();
1692
+ }
1693
+ else {
1694
+ // Step (1.A.4.B).
1695
+ // Free-up storage by deleting invalid contribution.
1696
+ await deleteObject(bucketName, lastZkeyStoragePath);
1697
+ // Step (1.A.4.B.1).
1698
+ batch.create(contributionDoc.ref, {
1699
+ participantId: participantDoc.id,
1700
+ verificationComputationTime: verifyCloudFunctionExecutionTime,
1701
+ zkeyIndex: isFinalizing ? finalContributionIndex : lastZkeyIndex,
1702
+ verificationSoftware: {
1703
+ name: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_NAME),
1704
+ version: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_VERSION),
1705
+ commitHash: String(process.env.CUSTOM_CONTRIBUTION_VERIFICATION_SOFTWARE_COMMIT_HASH)
1706
+ },
1707
+ valid: isContributionValid,
1708
+ lastUpdated: getCurrentServerTimestampInMillis()
1709
+ });
1710
+ }
1711
+ // Stop VM instance
1712
+ if (isUsingVM) {
1713
+ // using try and catch as the VM stopping function can throw
1714
+ // however we want to continue without stopping as the
1715
+ // verification was valid, and inform the coordinator
1716
+ try {
1717
+ await stopEC2Instance(ec2, vmInstanceId);
1718
+ }
1719
+ catch (error) {
1720
+ printLog(`Error while stopping VM instance ${vmInstanceId} - Error ${error}`, LogLevel.WARN);
1721
+ }
1722
+ }
1723
+ // Step (1.A.4.C)
1724
+ if (!isFinalizing) {
1725
+ // Step (1.A.4.C.1)
1726
+ // Compute new average contribution/verification time.
1727
+ fullContributionTime = Number(verificationStartedAt) - Number(contributionStartedAt);
1728
+ const newAvgContributionComputationTime = avgContributionComputationTime > 0
1729
+ ? (avgContributionComputationTime + contributionComputationTime) / 2
1730
+ : contributionComputationTime;
1731
+ const newAvgFullContributionTime = avgFullContributionTime > 0
1732
+ ? (avgFullContributionTime + fullContributionTime) / 2
1733
+ : fullContributionTime;
1734
+ const newAvgVerifyCloudFunctionTime = avgVerifyCloudFunctionTime > 0
1735
+ ? (avgVerifyCloudFunctionTime + verifyCloudFunctionTime) / 2
1736
+ : verifyCloudFunctionTime;
1737
+ // Prepare tx to update circuit average contribution/verification time.
1738
+ const updatedCircuitDoc = await getDocumentById(getCircuitsCollectionPath(ceremonyId), circuitId);
1739
+ const { waitingQueue: updatedWaitingQueue } = updatedCircuitDoc.data();
1740
+ /// @dev this must happen only for valid contributions.
1741
+ batch.update(circuitDoc.ref, {
1742
+ avgTimings: {
1743
+ contributionComputation: isContributionValid
1744
+ ? newAvgContributionComputationTime
1745
+ : avgContributionComputationTime,
1746
+ fullContribution: isContributionValid ? newAvgFullContributionTime : avgFullContributionTime,
1747
+ verifyCloudFunction: isContributionValid
1748
+ ? newAvgVerifyCloudFunctionTime
1749
+ : avgVerifyCloudFunctionTime
1750
+ },
1751
+ waitingQueue: {
1752
+ ...updatedWaitingQueue,
1753
+ completedContributions: isContributionValid
1754
+ ? completedContributions + 1
1755
+ : completedContributions,
1756
+ failedContributions: isContributionValid ? failedContributions : failedContributions + 1
1757
+ },
1758
+ lastUpdated: getCurrentServerTimestampInMillis()
1759
+ });
1760
+ }
1761
+ // Step (2).
1762
+ await batch.commit();
1763
+ printLog(`The contribution #${isFinalizing ? finalContributionIndex : lastZkeyIndex} of circuit ${circuitId} (ceremony ${ceremonyId}) has been verified as ${isContributionValid ? "valid" : "invalid"} for the participant ${participantDoc.id}`, LogLevel.DEBUG);
1764
+ };
1765
+ // Step (1).
1766
+ if (isContributing || isFinalizing) {
1767
+ // Prepare timer.
1768
+ verificationTaskTimer.start();
1769
+ // Step (1.A.3.0).
1770
+ if (isUsingVM) {
1771
+ printLog(`Starting the VM mechanism`, LogLevel.DEBUG);
1772
+ // Prepare for VM execution.
1773
+ let isVMRunning = false; // true when the VM is up, otherwise false.
1774
+ // Step (1.A.3.1).
1775
+ await startEC2Instance(ec2, vmInstanceId);
1776
+ await sleep(60000); // nb. wait for VM startup (1 mins + retry).
1777
+ // Check if the startup is running.
1778
+ isVMRunning = await checkIfVMRunning(ec2, vmInstanceId);
1779
+ printLog(`VM running: ${isVMRunning}`, LogLevel.DEBUG);
1780
+ // Step (1.A.3.2).
1781
+ // Prepare.
1782
+ const verificationCommand = vmContributionVerificationCommand(bucketName, lastZkeyStoragePath, verificationTranscriptStoragePathAndFilename);
1783
+ // Run.
1784
+ commandId = await runCommandUsingSSM(ssm, vmInstanceId, verificationCommand);
1785
+ printLog(`Starting the execution of command ${commandId}`, LogLevel.DEBUG);
1786
+ // Step (1.A.3.3).
1787
+ return waitForVMCommandExecution(ssm, vmInstanceId, commandId)
1788
+ .then(async () => {
1789
+ // Command execution successfully completed.
1790
+ printLog(`Command ${commandId} execution has been successfully completed`, LogLevel.DEBUG);
1791
+ await completeVerification();
1792
+ })
1793
+ .catch((error) => {
1794
+ // Command execution aborted.
1795
+ printLog(`Command ${commandId} execution has been aborted - Error ${error}`, LogLevel.DEBUG);
1796
+ logAndThrowError(COMMON_ERRORS.CM_INVALID_COMMAND_EXECUTION);
1797
+ });
1798
+ }
1799
+ // CF approach.
1800
+ printLog(`CF mechanism`, LogLevel.DEBUG);
1801
+ const potStoragePath = getPotStorageFilePath(files.potFilename);
1802
+ const firstZkeyStoragePath = getZkeyStorageFilePath(prefix, `${prefix}_${genesisZkeyIndex}.zkey`);
1803
+ // Prepare temporary file paths.
1804
+ // (nb. these are needed to download the necessary artifacts for verification from AWS S3).
1805
+ verificationTranscriptTemporaryLocalPath = createTemporaryLocalPath(verificationTranscriptCompleteFilename);
1806
+ const potTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}.pot`);
1807
+ const firstZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_genesis.zkey`);
1808
+ const lastZkeyTempFilePath = createTemporaryLocalPath(`${circuitId}_${participantDoc.id}_last.zkey`);
1809
+ // Create and populate transcript.
1810
+ const transcriptLogger = createCustomLoggerForFile(verificationTranscriptTemporaryLocalPath);
1811
+ transcriptLogger.info(`${isFinalizing ? `Final verification` : `Verification`} transcript for ${prefix} circuit Phase 2 contribution.\n${isFinalizing ? `Coordinator ` : `Contributor # ${Number(lastZkeyIndex)}`} (${contributorOrCoordinatorIdentifier})\n`);
1812
+ // Step (1.A.2).
1813
+ await downloadArtifactFromS3Bucket(bucketName, potStoragePath, potTempFilePath);
1814
+ await downloadArtifactFromS3Bucket(bucketName, firstZkeyStoragePath, firstZkeyTempFilePath);
1815
+ await downloadArtifactFromS3Bucket(bucketName, lastZkeyStoragePath, lastZkeyTempFilePath);
1816
+ // Step (1.A.4).
1817
+ isContributionValid = await zKey.verifyFromInit(firstZkeyTempFilePath, potTempFilePath, lastZkeyTempFilePath, transcriptLogger);
1818
+ // Compute contribution hash.
1819
+ lastZkeyBlake2bHash = await blake512FromPath(lastZkeyTempFilePath);
1820
+ // Free resources by unlinking temporary folders.
1821
+ // Do not free-up verification transcript path here.
1822
+ try {
1823
+ fs.unlinkSync(potTempFilePath);
1824
+ fs.unlinkSync(firstZkeyTempFilePath);
1825
+ fs.unlinkSync(lastZkeyTempFilePath);
1826
+ }
1827
+ catch (error) {
1828
+ printLog(`Error while unlinking temporary files - Error ${error}`, LogLevel.WARN);
1829
+ }
1830
+ await completeVerification();
1831
+ }
1832
+ });
1833
+ /**
1834
+ * Update the related participant's document after verification of its last contribution.
1835
+ * @dev this cloud functions is responsible for preparing the participant for the contribution toward the next circuit.
1836
+ * this does not happen if the participant is actually the coordinator who is finalizing the ceremony.
1837
+ */
1838
+ const refreshParticipantAfterContributionVerification = functionsV1
1839
+ .region("europe-west1")
1840
+ .runWith({
1841
+ memory: "512MB"
1842
+ })
1843
+ .firestore.document(`/${commonTerms.collections.ceremonies.name}/{ceremony}/${commonTerms.collections.circuits.name}/{circuit}/${commonTerms.collections.contributions.name}/{contributions}`)
1844
+ .onCreate(async (createdContribution) => {
1845
+ // Prepare db.
1846
+ const firestore = admin.firestore();
1847
+ // Prepare batch of txs.
1848
+ const batch = firestore.batch();
1849
+ // Derive data from document.
1850
+ // == /ceremonies/{ceremony}/circuits/.
1851
+ const ceremonyId = createdContribution.ref.parent.parent?.parent?.parent?.path.replace(`${commonTerms.collections.ceremonies.name}/`, "");
1852
+ // == /ceremonies/{ceremony}/participants.
1853
+ const ceremonyParticipantsCollectionPath = `${createdContribution.ref.parent.parent?.parent?.parent?.path}/${commonTerms.collections.participants.name}`;
1854
+ if (!createdContribution.data())
1855
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1856
+ // Extract data.
1857
+ const { participantId } = createdContribution.data();
1858
+ // Get documents from derived paths.
1859
+ const circuits = await getCeremonyCircuits(ceremonyId);
1860
+ const participantDoc = await getDocumentById(ceremonyParticipantsCollectionPath, participantId);
1861
+ if (!participantDoc.data())
1862
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1863
+ // Extract data.
1864
+ const { contributions, status, contributionProgress } = participantDoc.data();
1865
+ // Define pre-conditions.
1866
+ const isFinalizing = status === "FINALIZING" /* ParticipantStatus.FINALIZING */;
1867
+ // Link the newest created contribution document w/ participant contributions info.
1868
+ // nb. there must be only one contribution with an empty doc.
1869
+ contributions.forEach((participantContribution) => {
1870
+ // Define pre-conditions.
1871
+ const isContributionWithoutDocRef = !!participantContribution.hash &&
1872
+ !!participantContribution.computationTime &&
1873
+ !participantContribution.doc;
1874
+ if (isContributionWithoutDocRef)
1875
+ participantContribution.doc = createdContribution.id;
1876
+ });
1877
+ // Check if the participant is not the coordinator trying to finalize the ceremony.
1878
+ if (!isFinalizing)
1879
+ batch.update(participantDoc.ref, {
1880
+ // - DONE = provided a contribution for every circuit
1881
+ // - CONTRIBUTED = some contribution still missing.
1882
+ status: contributionProgress + 1 > circuits.length ? "DONE" /* ParticipantStatus.DONE */ : "CONTRIBUTED" /* ParticipantStatus.CONTRIBUTED */,
1883
+ contributionStep: "COMPLETED" /* ParticipantContributionStep.COMPLETED */,
1884
+ tempContributionData: FieldValue.delete()
1885
+ });
1886
+ // nb. valid both for participant or coordinator (finalizing).
1887
+ batch.update(participantDoc.ref, {
1888
+ contributions,
1889
+ lastUpdated: getCurrentServerTimestampInMillis()
1890
+ });
1891
+ await batch.commit();
1892
+ printLog(`Participant ${participantId} refreshed after contribution ${createdContribution.id} - The participant was finalizing the ceremony ${isFinalizing}`, LogLevel.DEBUG);
1893
+ });
1894
+ /**
1895
+ * Finalize the ceremony circuit.
1896
+ * @dev this cloud function stores the hashes and storage references of the Verifier smart contract
1897
+ * and verification key extracted from the circuit final contribution (as part of the ceremony finalization process).
1898
+ */
1899
+ const finalizeCircuit = functionsV1
1900
+ .region("europe-west1")
1901
+ .runWith({
1902
+ memory: "512MB"
1903
+ })
1904
+ .https.onCall(async (data, context) => {
1905
+ if (!context.auth || !context.auth.token.coordinator)
1906
+ logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE);
1907
+ if (!data.ceremonyId || !data.circuitId || !data.bucketName || !data.beacon)
1908
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
1909
+ // Get data.
1910
+ const { ceremonyId, circuitId, bucketName, beacon } = data;
1911
+ const userId = context.auth?.uid;
1912
+ // Look for documents.
1913
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1914
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId);
1915
+ const circuitDoc = await getDocumentById(getCircuitsCollectionPath(ceremonyId), circuitId);
1916
+ const contributionDoc = await getFinalContribution(ceremonyId, circuitId);
1917
+ if (!ceremonyDoc.data() || !circuitDoc.data() || !participantDoc.data() || !contributionDoc.data())
1918
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1919
+ // Extract data.
1920
+ const { prefix: circuitPrefix } = circuitDoc.data();
1921
+ const { files } = contributionDoc.data();
1922
+ // Prepare filenames and storage paths.
1923
+ const verificationKeyFilename = `${circuitPrefix}_${verificationKeyAcronym}.json`;
1924
+ const verifierContractFilename = `${circuitPrefix}_${verifierSmartContractAcronym}.sol`;
1925
+ const verificationKeyStorageFilePath = getVerificationKeyStorageFilePath(circuitPrefix, verificationKeyFilename);
1926
+ const verifierContractStorageFilePath = getVerifierContractStorageFilePath(circuitPrefix, verifierContractFilename);
1927
+ // Prepare temporary paths.
1928
+ const verificationKeyTemporaryFilePath = createTemporaryLocalPath(verificationKeyFilename);
1929
+ const verifierContractTemporaryFilePath = createTemporaryLocalPath(verifierContractFilename);
1930
+ // Download artifact from ceremony bucket.
1931
+ await downloadArtifactFromS3Bucket(bucketName, verificationKeyStorageFilePath, verificationKeyTemporaryFilePath);
1932
+ await downloadArtifactFromS3Bucket(bucketName, verifierContractStorageFilePath, verifierContractTemporaryFilePath);
1933
+ // Compute hash before unlink.
1934
+ const verificationKeyBlake2bHash = await blake512FromPath(verificationKeyTemporaryFilePath);
1935
+ const verifierContractBlake2bHash = await blake512FromPath(verifierContractTemporaryFilePath);
1936
+ // Free resources by unlinking temporary folders.
1937
+ fs.unlinkSync(verificationKeyTemporaryFilePath);
1938
+ fs.unlinkSync(verifierContractTemporaryFilePath);
1939
+ // Add references and hashes of the final contribution artifacts.
1940
+ await contributionDoc.ref.update({
1941
+ files: {
1942
+ ...files,
1943
+ verificationKeyBlake2bHash,
1944
+ verificationKeyFilename,
1945
+ verificationKeyStoragePath: verificationKeyStorageFilePath,
1946
+ verifierContractBlake2bHash,
1947
+ verifierContractFilename,
1948
+ verifierContractStoragePath: verifierContractStorageFilePath
1949
+ },
1950
+ beacon: {
1951
+ value: beacon,
1952
+ hash: computeSHA256ToHex(beacon)
1953
+ }
1954
+ });
1955
+ printLog(`Circuit ${circuitId} finalization completed - Ceremony ${ceremonyDoc.id} - Coordinator ${participantDoc.id}`, LogLevel.DEBUG);
1956
+ });
1957
+
1958
+ dotenv.config();
1959
+ /**
1960
+ * Check if the pre-condition for interacting w/ a multi-part upload for an identified current contributor is valid.
1961
+ * @notice the precondition is be a current contributor (contributing status) in the uploading contribution step.
1962
+ * @param contributorId <string> - the unique identifier of the contributor.
1963
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
1964
+ */
1965
+ const checkPreConditionForCurrentContributorToInteractWithMultiPartUpload = async (contributorId, ceremonyId) => {
1966
+ // Get ceremony and participant documents.
1967
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
1968
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), contributorId);
1969
+ // Get data from docs.
1970
+ const ceremonyData = ceremonyDoc.data();
1971
+ const participantData = participantDoc.data();
1972
+ if (!ceremonyData || !participantData)
1973
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1974
+ // Check pre-condition to start multi-part upload for a current contributor.
1975
+ const { status, contributionStep } = participantData;
1976
+ if (status !== "CONTRIBUTING" /* ParticipantStatus.CONTRIBUTING */ && contributionStep !== "UPLOADING" /* ParticipantContributionStep.UPLOADING */)
1977
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD);
1978
+ };
1979
+ /**
1980
+ * Helper function to check whether a contributor is uploading a file related to its contribution.
1981
+ * @param contributorId <string> - the unique identifier of the contributor.
1982
+ * @param ceremonyId <string> - the unique identifier of the ceremony.
1983
+ * @param objectKey <string> - the object key of the file being uploaded.
1984
+ */
1985
+ const checkUploadingFileValidity = async (contributorId, ceremonyId, objectKey) => {
1986
+ // Get the circuits for the ceremony
1987
+ const circuits = await getCeremonyCircuits(ceremonyId);
1988
+ // Get the participant document
1989
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), contributorId);
1990
+ const participantData = participantDoc.data();
1991
+ if (!participantData)
1992
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
1993
+ // The index of the circuit will be the contribution progress - 1
1994
+ const index = participantData?.contributionProgress;
1995
+ // If the index is zero the user is not the current contributor
1996
+ if (index === 0)
1997
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD);
1998
+ // We can safely use index - 1
1999
+ const circuit = circuits.at(index - 1);
2000
+ // If the circuit is undefined, throw an error
2001
+ if (!circuit)
2002
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD);
2003
+ // Extract the data we need
2004
+ const { prefix, waitingQueue } = circuit.data();
2005
+ const { completedContributions, currentContributor } = waitingQueue;
2006
+ // If we are not a contributor to this circuit then we cannot upload files
2007
+ if (currentContributor === contributorId) {
2008
+ // Get the index of the zKey
2009
+ const contributorZKeyIndex = formatZkeyIndex(completedContributions + 1);
2010
+ // The uploaded file must be the expected one
2011
+ const zkeyNameContributor = `${prefix}_${contributorZKeyIndex}.zkey`;
2012
+ const contributorZKeyStoragePath = getZkeyStorageFilePath(prefix, zkeyNameContributor);
2013
+ // If the object key does not have the expected storage path, throw an error
2014
+ if (objectKey !== contributorZKeyStoragePath) {
2015
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_WRONG_OBJECT_KEY);
2016
+ }
2017
+ }
2018
+ else
2019
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_CANNOT_INTERACT_WITH_MULTI_PART_UPLOAD);
2020
+ };
2021
+ /**
2022
+ * Helper function that confirms whether a bucket is used for a ceremony.
2023
+ * @dev this helps to prevent unauthorized access to coordinator's buckets.
2024
+ * @param bucketName
2025
+ */
2026
+ const checkIfBucketIsDedicatedToCeremony = async (bucketName) => {
2027
+ // Get Firestore DB.
2028
+ const firestoreDatabase = admin.firestore();
2029
+ // Extract ceremony prefix from bucket name.
2030
+ const ceremonyPrefix = bucketName.replace(String(process.env.AWS_CEREMONY_BUCKET_POSTFIX), "");
2031
+ // Query the collection.
2032
+ const ceremonyCollection = await firestoreDatabase
2033
+ .collection(commonTerms.collections.ceremonies.name)
2034
+ .where(commonTerms.collections.ceremonies.fields.prefix, "==", ceremonyPrefix)
2035
+ .get();
2036
+ if (ceremonyCollection.empty)
2037
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_BUCKET_NOT_CONNECTED_TO_CEREMONY);
2038
+ };
2039
+ /**
2040
+ * Create a new AWS S3 bucket for a particular ceremony.
2041
+ * @notice the S3 bucket is used to store all the ceremony artifacts and contributions.
2042
+ */
2043
+ const createBucket = functions
2044
+ .region("europe-west1")
2045
+ .runWith({
2046
+ memory: "512MB"
2047
+ })
2048
+ .https.onCall(async (data, context) => {
2049
+ // Check if the user has the coordinator claim.
2050
+ if (!context.auth || !context.auth.token.coordinator)
2051
+ logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE);
2052
+ if (!data.bucketName)
2053
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
2054
+ // Connect to S3 client.
2055
+ const S3 = await getS3Client();
2056
+ try {
2057
+ // Try to get information about the bucket.
2058
+ await S3.send(new HeadBucketCommand({ Bucket: data.bucketName }));
2059
+ // If the command succeeded, the bucket exists, throw an error.
2060
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_INVALID_BUCKET_NAME);
2061
+ }
2062
+ catch (error) {
2063
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2064
+ if (error.name === "NotFound") {
2065
+ // Prepare S3 command.
2066
+ const command = new CreateBucketCommand({
2067
+ Bucket: data.bucketName,
2068
+ // CreateBucketConfiguration: {
2069
+ // LocationConstraint: String(process.env.AWS_REGION)
2070
+ // },
2071
+ ObjectOwnership: "BucketOwnerPreferred"
2072
+ });
2073
+ try {
2074
+ // Execute S3 command.
2075
+ const response = await S3.send(command);
2076
+ // Check response.
2077
+ if (response.$metadata.httpStatusCode === 200 && !!response.Location)
2078
+ printLog(`The AWS S3 bucket ${data.bucketName} has been created successfully`, LogLevel.LOG);
2079
+ const publicBlockCommand = new PutPublicAccessBlockCommand({
2080
+ Bucket: data.bucketName,
2081
+ PublicAccessBlockConfiguration: {
2082
+ BlockPublicAcls: false,
2083
+ BlockPublicPolicy: false
2084
+ }
2085
+ });
2086
+ // Allow objects to be public
2087
+ const publicBlockResponse = await S3.send(publicBlockCommand);
2088
+ // Check response.
2089
+ if (publicBlockResponse.$metadata.httpStatusCode === 204)
2090
+ printLog(`The AWS S3 bucket ${data.bucketName} has been set with the PublicAccessBlock disabled.`, LogLevel.LOG);
2091
+ // Set CORS
2092
+ const corsCommand = new PutBucketCorsCommand({
2093
+ Bucket: data.bucketName,
2094
+ CORSConfiguration: {
2095
+ CORSRules: [
2096
+ {
2097
+ AllowedMethods: ["GET", "PUT"],
2098
+ AllowedOrigins: ["*"],
2099
+ ExposeHeaders: ["ETag", "Content-Length"],
2100
+ AllowedHeaders: ["*"]
2101
+ }
2102
+ ]
2103
+ }
2104
+ });
2105
+ const corsResponse = await S3.send(corsCommand);
2106
+ // Check response.
2107
+ if (corsResponse.$metadata.httpStatusCode === 200)
2108
+ printLog(`The AWS S3 bucket ${data.bucketName} has been set with the CORS configuration.`, LogLevel.LOG);
2109
+ }
2110
+ catch (error) {
2111
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2112
+ /** * {@link https://docs.aws.amazon.com/simspaceweaver/latest/userguide/troubeshooting_too-many-buckets.html | TooManyBuckets} */
2113
+ if (error.$metadata.httpStatusCode === 400 && error.Code === `TooManyBuckets`)
2114
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_TOO_MANY_BUCKETS);
2115
+ // @todo handle more errors here.
2116
+ const commonError = COMMON_ERRORS.CM_INVALID_REQUEST;
2117
+ const additionalDetails = error.toString();
2118
+ logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails));
2119
+ }
2120
+ }
2121
+ else {
2122
+ // If there was a different error, re-throw it.
2123
+ const commonError = COMMON_ERRORS.CM_INVALID_REQUEST;
2124
+ const additionalDetails = error.toString();
2125
+ logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails));
2126
+ }
2127
+ }
2128
+ });
2129
+ /**
2130
+ * Check if a specified object exist in a given AWS S3 bucket.
2131
+ * @returns <Promise<boolean>> - true if the object exist in the given bucket; otherwise false.
2132
+ */
2133
+ const checkIfObjectExist = functions
2134
+ .region("europe-west1")
2135
+ .runWith({
2136
+ memory: "512MB"
2137
+ })
2138
+ .https.onCall(async (data, context) => {
2139
+ // Check if the user has the coordinator claim.
2140
+ if (!context.auth || !context.auth.token.coordinator)
2141
+ logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE);
2142
+ if (!data.bucketName || !data.objectKey)
2143
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
2144
+ // Connect to S3 client.
2145
+ const S3 = await getS3Client();
2146
+ // Prepare S3 command.
2147
+ const command = new HeadObjectCommand({ Bucket: data.bucketName, Key: data.objectKey });
2148
+ try {
2149
+ // Execute S3 command.
2150
+ const response = await S3.send(command);
2151
+ // Check response.
2152
+ if (response.$metadata.httpStatusCode === 200 && !!response.ETag) {
2153
+ printLog(`The object associated w/ ${data.objectKey} key has been found in the ${data.bucketName} bucket`, LogLevel.LOG);
2154
+ return true;
2155
+ }
2156
+ }
2157
+ catch (error) {
2158
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2159
+ if (error.$metadata.httpStatusCode === 403)
2160
+ logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_MISSING_PERMISSIONS);
2161
+ // @todo handle more specific errors here.
2162
+ // nb. do not handle common errors! This method must return false if not found!
2163
+ // const commonError = COMMON_ERRORS.CM_INVALID_REQUEST
2164
+ // const additionalDetails = error.toString()
2165
+ // logAndThrowError(makeError(
2166
+ // commonError.code,
2167
+ // commonError.message,
2168
+ // additionalDetails
2169
+ // ))
2170
+ }
2171
+ return false;
2172
+ });
2173
+ /**
2174
+ * Return a pre-signed url for a given object contained inside the provided AWS S3 bucket in order to perform a GET request.
2175
+ * @notice the pre-signed url has a predefined expiration expressed in seconds inside the environment
2176
+ * configuration of the `backend` package. The value should match the configuration of `phase2cli` package
2177
+ * environment to avoid inconsistency between client request and CF.
2178
+ */
2179
+ const generateGetObjectPreSignedUrl = functions
2180
+ .region("europe-west1")
2181
+ .runWith({
2182
+ memory: "512MB"
2183
+ })
2184
+ .https.onCall(async (data, context) => {
2185
+ if (!context.auth)
2186
+ logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED);
2187
+ if (!data.bucketName || !data.objectKey)
2188
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
2189
+ // Prepare input data.
2190
+ const { objectKey, bucketName } = data;
2191
+ // Check whether the bucket for which we are generating the pre-signed url is dedicated to a ceremony.
2192
+ await checkIfBucketIsDedicatedToCeremony(bucketName);
2193
+ // Connect to S3 client.
2194
+ const S3 = await getS3Client();
2195
+ // Prepare S3 command.
2196
+ const command = new GetObjectCommand({ Bucket: bucketName, Key: objectKey });
2197
+ try {
2198
+ // Execute S3 command.
2199
+ const url = await getSignedUrl(S3, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION) });
2200
+ if (url) {
2201
+ printLog(`The generated pre-signed url is ${url}`, LogLevel.DEBUG);
2202
+ return url;
2203
+ }
2204
+ }
2205
+ catch (error) {
2206
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2207
+ // @todo handle more errors here.
2208
+ // if (error.$metadata.httpStatusCode !== 200) {
2209
+ const commonError = COMMON_ERRORS.CM_INVALID_REQUEST;
2210
+ const additionalDetails = error.toString();
2211
+ logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails));
2212
+ // }
2213
+ }
2214
+ });
2215
+ /**
2216
+ * Start a new multi-part upload for a specific object in the given AWS S3 bucket.
2217
+ * @notice this operation can be performed by either an authenticated participant or a coordinator.
2218
+ */
2219
+ const startMultiPartUpload = functions
2220
+ .region("europe-west1")
2221
+ .runWith({
2222
+ memory: "512MB"
2223
+ })
2224
+ .https.onCall(async (data, context) => {
2225
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
2226
+ logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED);
2227
+ if (!data.bucketName || !data.objectKey || (context.auth?.token.participant && !data.ceremonyId))
2228
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
2229
+ // Prepare data.
2230
+ const { bucketName, objectKey, ceremonyId } = data;
2231
+ const userId = context.auth?.uid;
2232
+ // Check if the user is a current contributor.
2233
+ if (context.auth?.token.participant && !!ceremonyId) {
2234
+ // Check pre-condition.
2235
+ await checkPreConditionForCurrentContributorToInteractWithMultiPartUpload(userId, ceremonyId);
2236
+ // Check whether the bucket where the object for which we are generating the pre-signed url is dedicated to a ceremony.
2237
+ await checkIfBucketIsDedicatedToCeremony(bucketName);
2238
+ // Check the validity of the uploaded file.
2239
+ await checkUploadingFileValidity(userId, ceremonyId, objectKey);
2240
+ }
2241
+ // Connect to S3 client.
2242
+ const S3 = await getS3Client();
2243
+ // Prepare S3 command.
2244
+ const command = new CreateMultipartUploadCommand({
2245
+ Bucket: bucketName,
2246
+ Key: objectKey,
2247
+ ACL: context.auth?.token.participant ? "private" : "public-read"
2248
+ });
2249
+ try {
2250
+ // Execute S3 command.
2251
+ const response = await S3.send(command);
2252
+ if (response.$metadata.httpStatusCode === 200 && !!response.UploadId) {
2253
+ printLog(`The multi-part upload identifier is ${response.UploadId}. Requested by ${userId}`, LogLevel.DEBUG);
2254
+ return response.UploadId;
2255
+ }
2256
+ }
2257
+ catch (error) {
2258
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2259
+ // @todo handle more errors here.
2260
+ if (error.$metadata.httpStatusCode !== 200) {
2261
+ const commonError = COMMON_ERRORS.CM_INVALID_REQUEST;
2262
+ const additionalDetails = error.toString();
2263
+ logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails));
2264
+ }
2265
+ }
2266
+ });
2267
+ /**
2268
+ * Generate a new pre-signed url for each chunk related to a started multi-part upload.
2269
+ * @notice this operation can be performed by either an authenticated participant or a coordinator.
2270
+ * the pre-signed url has a predefined expiration expressed in seconds inside the environment
2271
+ * configuration of the `backend` package. The value should match the configuration of `phase2cli` package
2272
+ * environment to avoid inconsistency between client request and CF.
2273
+ */
2274
+ const generatePreSignedUrlsParts = functions
2275
+ .region("europe-west1")
2276
+ .runWith({
2277
+ memory: "512MB",
2278
+ timeoutSeconds: 300
2279
+ })
2280
+ .https.onCall(async (data, context) => {
2281
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
2282
+ logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED);
2283
+ if (!data.bucketName ||
2284
+ !data.objectKey ||
2285
+ !data.uploadId ||
2286
+ data.numberOfParts <= 0 ||
2287
+ (context.auth?.token.participant && !data.ceremonyId))
2288
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
2289
+ // Prepare data.
2290
+ const { bucketName, objectKey, uploadId, numberOfParts, ceremonyId } = data;
2291
+ const userId = context.auth?.uid;
2292
+ // Check if the user is a current contributor.
2293
+ if (context.auth?.token.participant && !!ceremonyId) {
2294
+ // Check pre-condition.
2295
+ await checkPreConditionForCurrentContributorToInteractWithMultiPartUpload(userId, ceremonyId);
2296
+ }
2297
+ // Connect to S3 client.
2298
+ const S3 = await getS3Client();
2299
+ // Prepare state.
2300
+ const parts = [];
2301
+ for (let i = 0; i < numberOfParts; i += 1) {
2302
+ // Prepare S3 command for each chunk.
2303
+ const command = new UploadPartCommand({
2304
+ Bucket: bucketName,
2305
+ Key: objectKey,
2306
+ PartNumber: i + 1,
2307
+ UploadId: uploadId
2308
+ });
2309
+ try {
2310
+ // Get the pre-signed url for the specific chunk.
2311
+ const url = await getSignedUrl(S3, command, {
2312
+ expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION)
2313
+ });
2314
+ if (url) {
2315
+ // Save.
2316
+ parts.push(url);
2317
+ }
2318
+ }
2319
+ catch (error) {
2320
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2321
+ // @todo handle more errors here.
2322
+ // if (error.$metadata.httpStatusCode !== 200) {
2323
+ const commonError = COMMON_ERRORS.CM_INVALID_REQUEST;
2324
+ const additionalDetails = error.toString();
2325
+ logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails));
2326
+ // }
2327
+ }
2328
+ }
2329
+ return parts;
2330
+ });
2331
+ /**
2332
+ * Complete a multi-part upload for a specific object in the given AWS S3 bucket.
2333
+ * @notice this operation can be performed by either an authenticated participant or a coordinator.
2334
+ */
2335
+ const completeMultiPartUpload = functions
2336
+ .region("europe-west1")
2337
+ .runWith({
2338
+ memory: "512MB"
2339
+ })
2340
+ .https.onCall(async (data, context) => {
2341
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
2342
+ logAndThrowError(COMMON_ERRORS.CM_NOT_AUTHENTICATED);
2343
+ if (!data.bucketName ||
2344
+ !data.objectKey ||
2345
+ !data.uploadId ||
2346
+ !data.parts ||
2347
+ (context.auth?.token.participant && !data.ceremonyId))
2348
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
2349
+ // Prepare data.
2350
+ const { bucketName, objectKey, uploadId, parts, ceremonyId } = data;
2351
+ const userId = context.auth?.uid;
2352
+ // Check if the user is a current contributor.
2353
+ if (context.auth?.token.participant && !!ceremonyId) {
2354
+ // Check pre-condition.
2355
+ await checkPreConditionForCurrentContributorToInteractWithMultiPartUpload(userId, ceremonyId);
2356
+ // Check if the bucket is dedicated to a ceremony.
2357
+ await checkIfBucketIsDedicatedToCeremony(bucketName);
2358
+ }
2359
+ // Connect to S3.
2360
+ const S3 = await getS3Client();
2361
+ // Prepare S3 command.
2362
+ const command = new CompleteMultipartUploadCommand({
2363
+ Bucket: bucketName,
2364
+ Key: objectKey,
2365
+ UploadId: uploadId,
2366
+ MultipartUpload: { Parts: parts }
2367
+ });
2368
+ try {
2369
+ // Execute S3 command.
2370
+ const response = await S3.send(command);
2371
+ if (response.$metadata.httpStatusCode === 200 && !!response.Location) {
2372
+ printLog(`Multi-part upload ${data.uploadId} completed. Object location: ${response.Location}`, LogLevel.DEBUG);
2373
+ return response.Location;
2374
+ }
2375
+ }
2376
+ catch (error) {
2377
+ // eslint-disable-next-line @typescript-eslint/no-shadow
2378
+ // @todo handle more errors here.
2379
+ if (error.$metadata.httpStatusCode !== 200) {
2380
+ const commonError = COMMON_ERRORS.CM_INVALID_REQUEST;
2381
+ const additionalDetails = error.toString();
2382
+ logAndThrowError(makeError(commonError.code, commonError.message, additionalDetails));
2383
+ }
2384
+ }
2385
+ });
2386
+
2387
+ dotenv.config();
2388
+ /**
2389
+ * Check and remove the current contributor if it doesn't complete the contribution on the specified amount of time.
2390
+ * @dev since this cloud function is executed every minute, delay problems may occur. See issue #192 (https://github.com/quadratic-funding/mpc-phase2-suite/issues/192).
2391
+ * @notice the reasons why a contributor may be considered blocking are many.
2392
+ * for example due to network latency, disk availability issues, un/intentional crashes, limited hardware capabilities.
2393
+ * the timeout mechanism (fixed/dynamic) could also influence this decision.
2394
+ * this cloud function should check each circuit and:
2395
+ * A) avoid timeout if there's no current contributor for the circuit.
2396
+ * B) avoid timeout if the current contributor is the first for the circuit
2397
+ * and timeout mechanism type is dynamic (suggestion: coordinator should be the first contributor).
2398
+ * C) check if the current contributor is a potential blocking contributor for the circuit.
2399
+ * D) discriminate between blocking contributor (= when downloading, computing, uploading contribution steps)
2400
+ * or verification (= verifying contribution step) timeout types.
2401
+ * E) execute timeout.
2402
+ * E.1) prepare next contributor (if any).
2403
+ * E.2) update circuit contributors waiting queue removing the current contributor.
2404
+ * E.3) assign timeout to blocking contributor (participant doc update + timeout doc).
2405
+ */
2406
+ const checkAndRemoveBlockingContributor = functions
2407
+ .region("europe-west1")
2408
+ .runWith({
2409
+ memory: "512MB"
2410
+ })
2411
+ .pubsub.schedule("every 1 minutes")
2412
+ .onRun(async () => {
2413
+ // Prepare Firestore DB.
2414
+ const firestore = admin.firestore();
2415
+ // Get current server timestamp in milliseconds.
2416
+ const currentServerTimestamp = getCurrentServerTimestampInMillis();
2417
+ // Get opened ceremonies.
2418
+ const ceremonies = await queryOpenedCeremonies();
2419
+ // For each ceremony.
2420
+ for (const ceremony of ceremonies) {
2421
+ if (!ceremony.data())
2422
+ // Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
2423
+ printLog(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA.message, LogLevel.WARN);
2424
+ else {
2425
+ // Get ceremony circuits.
2426
+ const circuits = await getCeremonyCircuits(ceremony.id);
2427
+ // Extract ceremony data.
2428
+ const { timeoutType: timeoutMechanismType, penalty } = ceremony.data();
2429
+ for (const circuit of circuits) {
2430
+ if (!circuit.data())
2431
+ // Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
2432
+ printLog(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA.message, LogLevel.WARN);
2433
+ else {
2434
+ // Extract circuit data.
2435
+ const { waitingQueue, avgTimings, dynamicThreshold, fixedTimeWindow } = circuit.data();
2436
+ const { contributors, currentContributor, failedContributions, completedContributions } = waitingQueue;
2437
+ const { fullContribution: avgFullContribution, contributionComputation: avgContributionComputation, verifyCloudFunction: avgVerifyCloudFunction } = avgTimings;
2438
+ // Case (A).
2439
+ if (!currentContributor)
2440
+ // Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
2441
+ printLog(`No current contributor for circuit ${circuit.id} - ceremony ${ceremony.id}`, LogLevel.WARN);
2442
+ else if (avgFullContribution === 0 &&
2443
+ avgContributionComputation === 0 &&
2444
+ avgVerifyCloudFunction === 0 &&
2445
+ completedContributions === 0 &&
2446
+ timeoutMechanismType === "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */)
2447
+ printLog(`No timeout will be executed for the first contributor to the circuit ${circuit.id} - ceremony ${ceremony.id}`, LogLevel.WARN);
2448
+ else {
2449
+ // Get current contributor document.
2450
+ const participant = await getDocumentById(getParticipantsCollectionPath(ceremony.id), currentContributor);
2451
+ if (!participant.data())
2452
+ // Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
2453
+ printLog(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA.message, LogLevel.WARN);
2454
+ else {
2455
+ // Extract participant data.
2456
+ const { contributionStartedAt, verificationStartedAt, contributionStep } = participant.data();
2457
+ // Case (C).
2458
+ // Compute dynamic timeout threshold.
2459
+ const timeoutDynamicThreshold = timeoutMechanismType === "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */
2460
+ ? (avgFullContribution / 100) * Number(dynamicThreshold)
2461
+ : 0;
2462
+ // Compute the timeout expiration date (in ms).
2463
+ const timeoutExpirationDateInMsForBlockingContributor = timeoutMechanismType === "DYNAMIC" /* CeremonyTimeoutType.DYNAMIC */
2464
+ ? Number(contributionStartedAt) +
2465
+ Number(avgFullContribution) +
2466
+ Number(timeoutDynamicThreshold)
2467
+ : Number(contributionStartedAt) + Number(fixedTimeWindow) * 60000; // * 60000 = convert minutes to millis.
2468
+ // Case (D).
2469
+ const timeoutExpirationDateInMsForVerificationCloudFunction = contributionStep === "VERIFYING" /* ParticipantContributionStep.VERIFYING */ &&
2470
+ !!verificationStartedAt
2471
+ ? Number(verificationStartedAt) + 3540000 // 3540000 = 59 minutes in ms.
2472
+ : 0;
2473
+ // Assign the timeout type.
2474
+ let timeoutType = "";
2475
+ if (timeoutExpirationDateInMsForBlockingContributor < currentServerTimestamp &&
2476
+ (contributionStep === "DOWNLOADING" /* ParticipantContributionStep.DOWNLOADING */ ||
2477
+ contributionStep === "COMPUTING" /* ParticipantContributionStep.COMPUTING */ ||
2478
+ contributionStep === "UPLOADING" /* ParticipantContributionStep.UPLOADING */))
2479
+ timeoutType = "BLOCKING_CONTRIBUTION" /* TimeoutType.BLOCKING_CONTRIBUTION */;
2480
+ if (timeoutExpirationDateInMsForVerificationCloudFunction > 0 &&
2481
+ timeoutExpirationDateInMsForVerificationCloudFunction < currentServerTimestamp &&
2482
+ contributionStep === "VERIFYING" /* ParticipantContributionStep.VERIFYING */)
2483
+ timeoutType = "BLOCKING_CLOUD_FUNCTION" /* TimeoutType.BLOCKING_CLOUD_FUNCTION */;
2484
+ printLog(`${timeoutType} detected for circuit ${circuit.id} - ceremony ${ceremony.id}`, LogLevel.DEBUG);
2485
+ if (!timeoutType)
2486
+ // Do not use `logAndThrowError` method to avoid the function to exit before checking every ceremony.
2487
+ printLog(`No timeout for circuit ${circuit.id} - ceremony ${ceremony.id}`, LogLevel.WARN);
2488
+ else {
2489
+ // Case (E).
2490
+ let nextCurrentContributorId = "";
2491
+ // Prepare Firestore batch of txs.
2492
+ const batch = firestore.batch();
2493
+ // Remove current contributor from waiting queue.
2494
+ contributors.shift();
2495
+ // Check if someone else is ready to start the contribution.
2496
+ if (contributors.length > 0) {
2497
+ // Step (E.1).
2498
+ // Take the next participant to be current contributor.
2499
+ nextCurrentContributorId = contributors.at(0);
2500
+ // Get the document of the next current contributor.
2501
+ const nextCurrentContributor = await getDocumentById(getParticipantsCollectionPath(ceremony.id), nextCurrentContributorId);
2502
+ // Prepare next current contributor.
2503
+ batch.update(nextCurrentContributor.ref, {
2504
+ status: "READY" /* ParticipantStatus.READY */,
2505
+ lastUpdated: getCurrentServerTimestampInMillis()
2506
+ });
2507
+ }
2508
+ // Step (E.2).
2509
+ // Update accordingly the waiting queue.
2510
+ batch.update(circuit.ref, {
2511
+ waitingQueue: {
2512
+ ...waitingQueue,
2513
+ contributors,
2514
+ currentContributor: nextCurrentContributorId,
2515
+ failedContributions: failedContributions + 1
2516
+ },
2517
+ lastUpdated: getCurrentServerTimestampInMillis()
2518
+ });
2519
+ // Step (E.3).
2520
+ batch.update(participant.ref, {
2521
+ status: "TIMEDOUT" /* ParticipantStatus.TIMEDOUT */,
2522
+ lastUpdated: getCurrentServerTimestampInMillis()
2523
+ });
2524
+ // Compute the timeout duration (penalty) in milliseconds.
2525
+ const timeoutPenaltyInMs = Number(penalty) * 60000; // 60000 = amount of ms x minute.
2526
+ // Prepare an empty doc for timeout (w/ auto-gen uid).
2527
+ const timeout = await firestore
2528
+ .collection(getTimeoutsCollectionPath(ceremony.id, participant.id))
2529
+ .doc()
2530
+ .get();
2531
+ // Prepare tx to store info about the timeout.
2532
+ batch.create(timeout.ref, {
2533
+ type: timeoutType,
2534
+ startDate: currentServerTimestamp,
2535
+ endDate: currentServerTimestamp + timeoutPenaltyInMs
2536
+ });
2537
+ // Send atomic update for Firestore.
2538
+ await batch.commit();
2539
+ printLog(`The contributor ${participant.id} has been identified as potential blocking contributor. A timeout of type ${timeoutType} has been triggered w/ a penalty of ${timeoutPenaltyInMs} ms`, LogLevel.DEBUG);
2540
+ }
2541
+ }
2542
+ }
2543
+ }
2544
+ }
2545
+ }
2546
+ }
2547
+ });
2548
+ /**
2549
+ * Resume the contributor circuit contribution from scratch after the timeout expiration.
2550
+ * @dev The participant can resume the contribution if and only if the last timeout in progress was verified as expired (status == EXHUMED).
2551
+ */
2552
+ const resumeContributionAfterTimeoutExpiration = functions
2553
+ .region("europe-west1")
2554
+ .runWith({
2555
+ memory: "512MB"
2556
+ })
2557
+ .https.onCall(async (data, context) => {
2558
+ if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator))
2559
+ logAndThrowError(SPECIFIC_ERRORS.SE_AUTH_NO_CURRENT_AUTH_USER);
2560
+ if (!data.ceremonyId)
2561
+ logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA);
2562
+ // Get data.
2563
+ const { ceremonyId } = data;
2564
+ const userId = context.auth?.uid;
2565
+ // Look for the ceremony document.
2566
+ const ceremonyDoc = await getDocumentById(commonTerms.collections.ceremonies.name, ceremonyId);
2567
+ const participantDoc = await getDocumentById(getParticipantsCollectionPath(ceremonyId), userId);
2568
+ // Prepare documents data.
2569
+ const participantData = participantDoc.data();
2570
+ if (!ceremonyDoc.data() || !participantData)
2571
+ logAndThrowError(COMMON_ERRORS.CM_INEXISTENT_DOCUMENT_DATA);
2572
+ // Extract data.
2573
+ const { contributionProgress, status } = participantData;
2574
+ // Check pre-condition for resumable contribution after timeout expiration.
2575
+ if (status === "EXHUMED" /* ParticipantStatus.EXHUMED */)
2576
+ await participantDoc.ref.update({
2577
+ status: "READY" /* ParticipantStatus.READY */,
2578
+ lastUpdated: getCurrentServerTimestampInMillis(),
2579
+ tempContributionData: {}
2580
+ });
2581
+ else
2582
+ logAndThrowError(SPECIFIC_ERRORS.SE_CONTRIBUTE_CANNOT_PROGRESS_TO_NEXT_CIRCUIT);
2583
+ printLog(`Contributor ${userId} can retry the contribution for the circuit in position ${contributionProgress + 1} after timeout expiration`, LogLevel.DEBUG);
2584
+ });
2585
+
2586
+ admin.initializeApp();
2587
+
2588
+ export { checkAndPrepareCoordinatorForFinalization, checkAndRemoveBlockingContributor, checkIfObjectExist, checkParticipantForCeremony, completeMultiPartUpload, coordinateCeremonyParticipant, createBucket, finalizeCeremony, finalizeCircuit, generateGetObjectPreSignedUrl, generatePreSignedUrlsParts, initEmptyWaitingQueueForCircuit, permanentlyStoreCurrentContributionTimeAndHash, processSignUpWithCustomClaims, progressToNextCircuitForContribution, progressToNextContributionStep, refreshParticipantAfterContributionVerification, registerAuthUser, resumeContributionAfterTimeoutExpiration, setupCeremony, startCeremony, startMultiPartUpload, stopCeremony, temporaryStoreCurrentContributionMultiPartUploadId, temporaryStoreCurrentContributionUploadedChunkData, verifycontribution };