UNPKG

@devtion/actions

Version:
996 lines (992 loc) 140 kB
/** * @module @p0tion/actions * @version 1.2.8 * @file A set of actions and helpers for CLI commands * @copyright Ethereum Foundation 2022 * @license MIT * @see [Github]{@link https://github.com/privacy-scaling-explorations/p0tion} */ import mime from 'mime-types'; import fs, { createWriteStream } from 'fs'; import fetch from '@adobe/node-fetch-retry'; import https from 'https'; import { httpsCallable, httpsCallableFromURL, getFunctions } from 'firebase/functions'; import { onSnapshot, query, collection, getDocs, doc, getDoc, where, Timestamp, getFirestore } from 'firebase/firestore'; import { zKey, groth16 } from 'snarkjs'; import crypto from 'crypto'; import blake from 'blakejs'; import winston from 'winston'; import { pipeline } from 'stream'; import { promisify } from 'util'; import { initializeApp } from 'firebase/app'; import { signInWithCredential, initializeAuth, getAuth } from 'firebase/auth'; import { ContractFactory } from 'ethers'; import solc from 'solc'; import { EC2Client, RunInstancesCommand, DescribeInstanceStatusCommand, StartInstancesCommand, StopInstancesCommand, TerminateInstancesCommand } from '@aws-sdk/client-ec2'; import { SSMClient, SendCommandCommand, GetCommandInvocationCommand } from '@aws-sdk/client-ssm'; import dotenv from 'dotenv'; // Main part for the PPoT Phase 1 Trusted Setup URLs to download PoT files. const potFileDownloadMainUrl = `https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/`; // Main part for the PPoT Phase 1 Trusted Setup PoT files to be downloaded. const potFilenameTemplate = `ppot_0080_`; // The genesis zKey index. const genesisZkeyIndex = `00000`; // The number of exponential iterations to be executed by SnarkJS when finalizing the ceremony. const numExpIterations = 10; // The Solidity version of the Verifier Smart Contract generated with SnarkJS when finalizing the ceremony. const solidityVersion = "0.8.0"; // The index of the final zKey. const finalContributionIndex = "final"; // The acronym for verification key. const verificationKeyAcronym = "vkey"; // The acronym for Verifier smart contract. const verifierSmartContractAcronym = "verifier"; // The tag for ec2 instances. const ec2InstanceTag = "p0tionec2instance"; // The name of the VM startup script file. const vmBootstrapScriptFilename = "bootstrap.sh"; // Match hash output by snarkjs in transcript log const contribHashRegex = /Contribution.+Hash.+\s+.+\s+.+\s+.+\s+.+\s*/; /** * Define the supported VM configuration types. * @dev the VM configurations can be retrieved at https://aws.amazon.com/ec2/instance-types/ * The on-demand prices for the configurations can be retrieved at https://aws.amazon.com/ec2/pricing/on-demand/. * @notice the price has to be intended as on-demand hourly billing usage for Linux OS * VMs located in the us-east-1 region expressed in USD. */ const vmConfigurationTypes = { t3_large: { type: "t3.large", ram: 8, vcpu: 2, pricePerHour: 0.08352 }, t3_2xlarge: { type: "t3.2xlarge", ram: 32, vcpu: 8, pricePerHour: 0.3328 }, c5_9xlarge: { type: "c5.9xlarge", ram: 72, vcpu: 36, pricePerHour: 1.53 }, c5_18xlarge: { type: "c5.18xlarge", ram: 144, vcpu: 72, pricePerHour: 3.06 }, c5a_8xlarge: { type: "c5a.8xlarge", ram: 64, vcpu: 32, pricePerHour: 1.232 }, c6id_32xlarge: { type: "c6id.32xlarge", ram: 256, vcpu: 128, pricePerHour: 6.4512 }, m6a_32xlarge: { type: "m6a.32xlarge", ram: 512, vcpu: 128, pricePerHour: 5.5296 } }; /** * Define the PPoT Trusted Setup ceremony output powers of tau files size (in GB). * @dev the powers of tau files can be retrieved at https://github.com/weijiekoh/perpetualpowersoftau */ const powersOfTauFiles = [ { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_01.ptau", size: 0.000084 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_02.ptau", size: 0.000086 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_03.ptau", size: 0.000091 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_04.ptau", size: 0.0001 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_05.ptau", size: 0.000117 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_06.ptau", size: 0.000153 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_07.ptau", size: 0.000225 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_08.ptau", size: 0.0004 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_09.ptau", size: 0.000658 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_10.ptau", size: 0.0013 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_11.ptau", size: 0.0023 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_12.ptau", size: 0.0046 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_13.ptau", size: 0.0091 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_14.ptau", size: 0.0181 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_15.ptau", size: 0.0361 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_16.ptau", size: 0.0721 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_17.ptau", size: 0.144 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_18.ptau", size: 0.288 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_19.ptau", size: 0.576 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_20.ptau", size: 1.1 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_21.ptau", size: 2.3 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_22.ptau", size: 4.5 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_23.ptau", size: 9.0 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_24.ptau", size: 18.0 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_25.ptau", size: 36.0 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_26.ptau", size: 72.0 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_27.ptau", size: 144.0 }, { ref: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_final.ptau", size: 288.0 } ]; /** * Commonly used terms. * @dev useful for creating paths, references to collections and queries, object properties, folder names, and so on. */ const commonTerms = { collections: { users: { name: "users", fields: { creationTime: "creationTime", displayName: "displayName", email: "email", emailVerified: "emailVerified", lastSignInTime: "lastSignInTime", lastUpdated: "lastUpdated", name: "name", photoURL: "photoURL" } }, participants: { name: "participants", fields: { contributionProgress: "contributionProgress", contributionStartedAt: "contributionStartedAt", contributionStep: "contributionStep", contributions: "contributions", lastUpdated: "lastUpdated", status: "status", verificationStartedAt: "verificationStartedAt" } }, avatars: { name: "avatars", fields: { avatarUrl: "avatarUrl" } }, ceremonies: { name: "ceremonies", fields: { coordinatorId: "coordinatorId", description: "description", endDate: "endDate", lastUpdated: "lastUpdated", penalty: "penalty", prefix: "prefix", startDate: "startDate", state: "state", timeoutType: "timeoutType", title: "title", type: "type" } }, circuits: { name: "circuits", fields: { avgTimings: "avgTimings", compiler: "compiler", description: "description", files: "files", lastUpdated: "lastUpdated", metadata: "metadata", name: "name", prefix: "prefix", sequencePosition: "sequencePosition", template: "template", timeoutMaxContributionWaitingTime: "timeoutMaxContributionWaitingTime", waitingQueue: "waitingQueue", zKeySizeInBytes: "zKeySizeInBytes", verification: "verification" } }, contributions: { name: "contributions", fields: { contributionComputationTime: "contributionComputationTime", files: "files", lastUpdated: "lastUpdated", participantId: "participantId", valid: "valid", verificationComputationTime: "verificationComputationTime", zkeyIndex: "zKeyIndex" } }, timeouts: { name: "timeouts", fields: { type: "type", startDate: "startDate", endDate: "endDate" } } }, foldersAndPathsTerms: { output: `output`, setup: `setup`, contribute: `contribute`, finalize: `finalize`, pot: `pot`, zkeys: `zkeys`, wasm: `wasm`, vkeys: `vkeys`, metadata: `metadata`, transcripts: `transcripts`, attestation: `attestation`, verifiers: `verifiers` }, cloudFunctionsNames: { setupCeremony: "setupCeremony", checkParticipantForCeremony: "checkParticipantForCeremony", progressToNextCircuitForContribution: "progressToNextCircuitForContribution", resumeContributionAfterTimeoutExpiration: "resumeContributionAfterTimeoutExpiration", createBucket: "createBucket", generateGetObjectPreSignedUrl: "generateGetObjectPreSignedUrl", progressToNextContributionStep: "progressToNextContributionStep", permanentlyStoreCurrentContributionTimeAndHash: "permanentlyStoreCurrentContributionTimeAndHash", startMultiPartUpload: "startMultiPartUpload", temporaryStoreCurrentContributionMultiPartUploadId: "temporaryStoreCurrentContributionMultiPartUploadId", temporaryStoreCurrentContributionUploadedChunkData: "temporaryStoreCurrentContributionUploadedChunkData", generatePreSignedUrlsParts: "generatePreSignedUrlsParts", completeMultiPartUpload: "completeMultiPartUpload", checkIfObjectExist: "checkIfObjectExist", verifyContribution: "verifycontribution", checkAndPrepareCoordinatorForFinalization: "checkAndPrepareCoordinatorForFinalization", finalizeCircuit: "finalizeCircuit", finalizeCeremony: "finalizeCeremony", downloadCircuitArtifacts: "downloadCircuitArtifacts", transferObject: "transferObject", bandadaValidateProof: "bandadaValidateProof", checkNonceOfSIWEAddress: "checkNonceOfSIWEAddress" } }; /** * Setup a new ceremony by calling the related cloud function. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyInputData <CeremonyInputData> - the input data of the ceremony. * @param ceremonyPrefix <string> - the prefix of the ceremony. * @param circuits <Circuit[]> - the circuits data. * @returns Promise<string> - the unique identifier of the created ceremony. */ const setupCeremony = async (functions, ceremonyInputData, ceremonyPrefix, circuits) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.setupCeremony); const { data: ceremonyId } = await cf({ ceremonyInputData, ceremonyPrefix, circuits }); return String(ceremonyId); }; /** * Check the user's current participant status for the ceremony * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @returns <boolean> - true when participant is able to contribute; otherwise false. */ const checkParticipantForCeremony = async (functions, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.checkParticipantForCeremony); const { data } = await cf({ ceremonyId }); return data; }; /** * Progress the participant to the next circuit preparing for the next contribution. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. */ const progressToNextCircuitForContribution = async (functions, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.progressToNextCircuitForContribution); await cf({ ceremonyId }); }; /** * Resume the contributor circuit contribution from scratch after the timeout expiration. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. */ const resumeContributionAfterTimeoutExpiration = async (functions, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.resumeContributionAfterTimeoutExpiration); await cf({ ceremonyId }); }; /** * Make a request to create a new AWS S3 bucket for a ceremony. * @param functions <Functions> - the Firebase cloud functions object instance. * @param bucketName <string> - the name of the ceremony bucket. */ const createS3Bucket = async (functions, bucketName) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.createBucket); await cf({ bucketName }); }; /** * Return a pre-signed url for a given object contained inside the provided AWS S3 bucket in order to perform a GET request. * @param functions <Functions> - the Firebase cloud functions object instance. * @param bucketName <string> - the name of the ceremony bucket. * @param objectKey <string> - the storage path that locates the artifact to be downloaded in the bucket. * @returns <Promise<string>> - the pre-signed url w/ GET request permissions for specified object key. */ const generateGetObjectPreSignedUrl = async (functions, bucketName, objectKey) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.generateGetObjectPreSignedUrl); const { data: getPreSignedUrl } = await cf({ bucketName, objectKey }); return String(getPreSignedUrl); }; /** * Progress the participant to the next circuit preparing for the next contribution. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. */ const progressToNextContributionStep = async (functions, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.progressToNextContributionStep); await cf({ ceremonyId }); }; /** * Write the information about current contribution hash and computation time for the current contributor. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param contributionComputationTime <number> - the time when it was computed * @param contributingHash <string> - the hash of the contribution */ const permanentlyStoreCurrentContributionTimeAndHash = async (functions, ceremonyId, contributionComputationTime, contributionHash) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.permanentlyStoreCurrentContributionTimeAndHash); await cf({ ceremonyId, contributionComputationTime, contributionHash }); }; /** * Start a new multi-part upload for a specific object in the given AWS S3 bucket. * @param functions <Functions> - the Firebase cloud functions object instance. * @param bucketName <string> - the name of the ceremony bucket. * @param objectKey <string> - the storage path that locates the artifact to be downloaded in the bucket. * @param ceremonyId <string> - the unique identifier of the ceremony. * @returns Promise<string> - the multi-part upload id. */ const openMultiPartUpload = async (functions, bucketName, objectKey, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.startMultiPartUpload); const { data: uploadId } = await cf({ bucketName, objectKey, ceremonyId }); return String(uploadId); }; /** * Write temporary information about the unique identifier about the opened multi-part upload to eventually resume the contribution. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param uploadId <string> - the unique identifier of the multi-part upload. */ const temporaryStoreCurrentContributionMultiPartUploadId = async (functions, ceremonyId, uploadId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.temporaryStoreCurrentContributionMultiPartUploadId); await cf({ ceremonyId, uploadId }); }; /** * Write temporary information about the etags and part numbers for each uploaded chunk in order to make the upload resumable from last chunk. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param chunk <ETagWithPartNumber> - the information about the already uploaded chunk. */ const temporaryStoreCurrentContributionUploadedChunkData = async (functions, ceremonyId, chunk) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.temporaryStoreCurrentContributionUploadedChunkData); await cf({ ceremonyId, chunk }); }; /** * Generate a new pre-signed url for each chunk related to a started multi-part upload. * @param functions <Functions> - the Firebase cloud functions object instance. * @param bucketName <string> - the name of the ceremony bucket. * @param objectKey <string> - the storage path that locates the artifact to be downloaded in the bucket. * @param uploadId <string> - the unique identifier of the multi-part upload. * @param numberOfChunks <number> - the number of pre-signed urls to be generated. * @param ceremonyId <string> - the unique identifier of the ceremony. * @returns Promise<Array<string>> - the set of pre-signed urls (one for each chunk). */ const generatePreSignedUrlsParts = async (functions, bucketName, objectKey, uploadId, numberOfParts, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.generatePreSignedUrlsParts); const { data: chunksUrls } = await cf({ bucketName, objectKey, uploadId, numberOfParts, ceremonyId }); return chunksUrls; }; /** * Complete a multi-part upload for a specific object in the given AWS S3 bucket. * @param functions <Functions> - the Firebase cloud functions object instance. * @param bucketName <string> - the name of the ceremony bucket. * @param objectKey <string> - the storage path that locates the artifact to be downloaded in the bucket. * @param uploadId <string> - the unique identifier of the multi-part upload. * @param parts Array<ETagWithPartNumber> - the completed . * @param ceremonyId <string> - the unique identifier of the ceremony. * @returns Promise<string> - the location of the uploaded ceremony artifact. */ const completeMultiPartUpload = async (functions, bucketName, objectKey, uploadId, parts, ceremonyId) => { // Call completeMultiPartUpload() Cloud Function. const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.completeMultiPartUpload); const { data: location } = await cf({ bucketName, objectKey, uploadId, parts, ceremonyId }); return String(location); }; /** * Check if a specified object exist in a given AWS S3 bucket. * @param functions <Functions> - the Firebase cloud functions object instance. * @param bucketName <string> - the name of the ceremony bucket. * @param objectKey <string> - the storage path that locates the artifact to be downloaded in the bucket. * @returns <Promise<string>> - true if and only if the object exists, otherwise false. */ const checkIfObjectExist = async (functions, bucketName, objectKey) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.checkIfObjectExist); const { data: doesObjectExist } = await cf({ bucketName, objectKey }); return doesObjectExist; }; /** * Request to verify the newest contribution for the circuit. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param circuit <FirebaseDocumentInfo> - the document info about the circuit. * @param bucketName <string> - the name of the ceremony bucket. * @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing). * @param verifyContributionCloudFunctionEndpoint <string> - the endpoint (direct url) necessary to call the V2 Cloud Function. * @returns <Promise<void>> - */ const verifyContribution = async (functions, ceremonyId, circuit, // any just to avoid breaking the tests. bucketName, contributorOrCoordinatorIdentifier, verifyContributionCloudFunctionEndpoint) => { const cf = httpsCallableFromURL(functions, verifyContributionCloudFunctionEndpoint, { timeout: 3600000 // max timeout 60 minutes. }); /** * @dev Force a race condition to fix #57. * TL;DR if the cloud function does not return despite having finished its execution, we use * a listener on the circuit, we check and retrieve the info about the correct execution and * return it manually. In other cases, it will be the function that returns either a timeout in case it * remains in execution for too long. */ await Promise.race([ cf({ ceremonyId, circuitId: circuit.id, contributorOrCoordinatorIdentifier, bucketName }), new Promise((resolve) => { setTimeout(() => { const unsubscribeToCeremonyCircuitListener = onSnapshot(circuit.ref, async (changedCircuit) => { // Check data. if (!circuit.data || !changedCircuit.data()) throw Error(`Unable to retrieve circuit data from the ceremony.`); // Extract data. const { avgTimings: changedAvgTimings, waitingQueue: changedWaitingQueue } = changedCircuit.data(); const { contributionComputation: changedContributionComputation, fullContribution: changedFullContribution, verifyCloudFunction: changedVerifyCloudFunction } = changedAvgTimings; const { failedContributions: changedFailedContributions, completedContributions: changedCompletedContributions } = changedWaitingQueue; const { avgTimings: prevAvgTimings, waitingQueue: prevWaitingQueue } = changedCircuit.data(); const { contributionComputation: prevContributionComputation, fullContribution: prevFullContribution, verifyCloudFunction: prevVerifyCloudFunction } = prevAvgTimings; const { failedContributions: prevFailedContributions, completedContributions: prevCompletedContributions } = prevWaitingQueue; // Pre-conditions. const invalidContribution = prevFailedContributions === changedFailedContributions - 1; const validContribution = prevCompletedContributions === changedCompletedContributions - 1; const avgTimeUpdates = prevContributionComputation !== changedContributionComputation && prevFullContribution !== changedFullContribution && prevVerifyCloudFunction !== changedVerifyCloudFunction; if ((invalidContribution || validContribution) && avgTimeUpdates) { resolve({}); } }); // Unsubscribe from listener. unsubscribeToCeremonyCircuitListener(); }, 3600000 - 1000); // 59:59 throws 1s before max time for CF execution. }) ]); }; /** * Prepare the coordinator for the finalization of the ceremony. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @returns <Promise<boolean>> - true when the coordinator is ready for finalization; otherwise false. */ const checkAndPrepareCoordinatorForFinalization = async (functions, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.checkAndPrepareCoordinatorForFinalization); const { data: isCoordinatorReadyForCeremonyFinalization } = await cf({ ceremonyId }); return isCoordinatorReadyForCeremonyFinalization; }; /** * Finalize the ceremony circuit. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param circuitId <string> - the unique identifier of the circuit. * @param bucketName <string> - the name of the ceremony bucket. * @param beacon <string> - the value used to compute the final contribution while finalizing the ceremony. */ const finalizeCircuit = async (functions, ceremonyId, circuitId, bucketName, beacon) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.finalizeCircuit); await cf({ ceremonyId, circuitId, bucketName, beacon }); }; /** * Conclude the finalization of the ceremony. * @param functions <Functions> - the Firebase cloud functions object instance. * @param ceremonyId <string> - the unique identifier of the ceremony. */ const finalizeCeremony = async (functions, ceremonyId) => { const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.finalizeCeremony); await cf({ ceremonyId }); }; /** * Return the bucket name based on ceremony prefix. * @param ceremonyPrefix <string> - the ceremony prefix. * @param ceremonyPostfix <string> - the ceremony postfix. * @returns <string> */ const getBucketName = (ceremonyPrefix, ceremonyPostfix) => `${ceremonyPrefix}${ceremonyPostfix}`; /** * Get chunks and signed urls related to an object that must be uploaded using a multi-part upload. * @param cloudFunctions <Functions> - the Firebase Cloud Functions service instance. * @param bucketName <string> - the name of the ceremony artifacts bucket (AWS S3). * @param objectKey <string> - the unique key to identify the object inside the given AWS S3 bucket. * @param localFilePath <string> - the local path where the artifact will be downloaded. * @param uploadId <string> - the unique identifier of the multi-part upload. * @param configStreamChunkSize <number> - size of each chunk into which the artifact is going to be splitted (nb. will be converted in MB). * @param [ceremonyId] <string> - the unique identifier of the ceremony. * @returns Promise<Array<ChunkWithUrl>> - the chunks with related pre-signed url. */ const getChunksAndPreSignedUrls = async (cloudFunctions, bucketName, objectKey, localFilePath, uploadId, configStreamChunkSize, ceremonyId) => { // Prepare a new stream to read the file. const stream = fs.createReadStream(localFilePath, { highWaterMark: configStreamChunkSize * 1024 * 1024 // convert to MB. }); // Split in chunks. const chunks = []; for await (const chunk of stream) chunks.push(chunk); // Check if the file is not empty. if (!chunks.length) throw new Error("Unable to split an empty file into chunks."); // Request pre-signed url generation for each chunk. const preSignedUrls = await generatePreSignedUrlsParts(cloudFunctions, bucketName, objectKey, uploadId, chunks.length, ceremonyId); // Map pre-signed urls with corresponding chunks. return chunks.map((val1, index) => ({ partNumber: index + 1, chunk: val1, preSignedUrl: preSignedUrls[index] })); }; /** * Forward the request to upload each single chunk of the related ceremony artifact. * @param chunksWithUrls <Array<ChunkWithUrl>> - the array containing each chunk mapped with the corresponding pre-signed urls. * @param contentType <string | false> - the content type of the ceremony artifact. * @param cloudFunctions <Functions> - the Firebase Cloud Functions service instance. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param alreadyUploadedChunks Array<ETagWithPartNumber> - the temporary information about the already uploaded chunks. * @param logger <GenericBar> - an optional logger to show progress. * @returns <Promise<Array<ETagWithPartNumber>>> - the completed (uploaded) chunks information. */ const uploadParts = async (chunksWithUrls, contentType, cloudFunctions, ceremonyId, alreadyUploadedChunks, logger) => { // Keep track of uploaded chunks. const uploadedChunks = alreadyUploadedChunks || []; // if we were passed a logger, start it if (logger) logger.start(chunksWithUrls.length, 0); // Loop through remaining chunks. for (let i = alreadyUploadedChunks ? alreadyUploadedChunks.length : 0; i < chunksWithUrls.length; i += 1) { // Consume the pre-signed url to upload the chunk. // @ts-ignore const response = await fetch(chunksWithUrls[i].preSignedUrl, { retryOptions: { retryInitialDelay: 500, // 500 ms. socketTimeout: 60000, // 60 seconds. retryMaxDuration: 300000 // 5 minutes. }, method: "PUT", body: chunksWithUrls[i].chunk, headers: { "Content-Type": contentType.toString(), "Content-Length": chunksWithUrls[i].chunk.length.toString() }, agent: new https.Agent({ keepAlive: true }) }); // Verify the response. if (response.status !== 200 || !response.ok) throw new Error(`Unable to upload chunk number ${i}. Please, terminate the current session and retry to resume from the latest uploaded chunk.`); // Extract uploaded chunk data. const chunk = { ETag: response.headers.get("etag") || undefined, PartNumber: chunksWithUrls[i].partNumber }; uploadedChunks.push(chunk); // Temporary store uploaded chunk data to enable later resumable contribution. // nb. this must be done only when contributing (not finalizing). if (!!ceremonyId && !!cloudFunctions) await temporaryStoreCurrentContributionUploadedChunkData(cloudFunctions, ceremonyId, chunk); // increment the count on the logger if (logger) logger.increment(); } return uploadedChunks; }; /** * Upload a ceremony artifact to the corresponding bucket. * @notice this method implements the multi-part upload using pre-signed urls, optimal for large files. * Steps: * 0) Check if current contributor could resume a multi-part upload. * 0.A) If yes, continue from last uploaded chunk using the already opened multi-part upload. * 0.B) Otherwise, start creating a new multi-part upload. * 1) Generate a pre-signed url for each (remaining) chunk of the ceremony artifact. * 2) Consume the pre-signed urls to upload chunks. * 3) Complete the multi-part upload. * @param cloudFunctions <Functions> - the Firebase Cloud Functions service instance. * @param bucketName <string> - the name of the ceremony artifacts bucket (AWS S3). * @param objectKey <string> - the unique key to identify the object inside the given AWS S3 bucket. * @param localPath <string> - the local path where the artifact will be downloaded. * @param configStreamChunkSize <number> - size of each chunk into which the artifact is going to be splitted (nb. will be converted in MB). * @param [ceremonyId] <string> - the unique identifier of the ceremony (used as a double-edge sword - as identifier and as a check if current contributor is the coordinator finalizing the ceremony). * @param [temporaryDataToResumeMultiPartUpload] <TemporaryParticipantContributionData> - the temporary information necessary to resume an already started multi-part upload. * @param logger <GenericBar> - an optional logger to show progress. */ const multiPartUpload = async (cloudFunctions, bucketName, objectKey, localFilePath, configStreamChunkSize, ceremonyId, temporaryDataToResumeMultiPartUpload, logger) => { // The unique identifier of the multi-part upload. let multiPartUploadId = ""; // The list of already uploaded chunks. let alreadyUploadedChunks = []; // Step (0). if (temporaryDataToResumeMultiPartUpload && !!temporaryDataToResumeMultiPartUpload.uploadId) { // Step (0.A). multiPartUploadId = temporaryDataToResumeMultiPartUpload.uploadId; alreadyUploadedChunks = temporaryDataToResumeMultiPartUpload.chunks; } else { // Step (0.B). // Open a new multi-part upload for the ceremony artifact. multiPartUploadId = await openMultiPartUpload(cloudFunctions, bucketName, objectKey, ceremonyId); // Store multi-part upload identifier on document collection. if (ceremonyId) // Store Multi-Part Upload ID after generation. await temporaryStoreCurrentContributionMultiPartUploadId(cloudFunctions, ceremonyId, multiPartUploadId); } // Step (1). const chunksWithUrlsZkey = await getChunksAndPreSignedUrls(cloudFunctions, bucketName, objectKey, localFilePath, multiPartUploadId, configStreamChunkSize, ceremonyId); // Step (2). const partNumbersAndETagsZkey = await uploadParts(chunksWithUrlsZkey, mime.lookup(localFilePath), // content-type. cloudFunctions, ceremonyId, alreadyUploadedChunks, logger); // Step (3). await completeMultiPartUpload(cloudFunctions, bucketName, objectKey, multiPartUploadId, partNumbersAndETagsZkey, ceremonyId); }; /** * Download an artifact from S3 (only for authorized users) * @param cloudFunctions <Functions> Firebase cloud functions instance. * @param bucketName <string> Name of the bucket where the artifact is stored. * @param storagePath <string> Path to the artifact in the bucket. * @param localPath <string> Path to the local file where the artifact will be saved. */ const downloadCeremonyArtifact = async (cloudFunctions, bucketName, storagePath, localPath) => { // Request pre-signed url to make GET download request. const getPreSignedUrl = await generateGetObjectPreSignedUrl(cloudFunctions, bucketName, storagePath); // Make fetch to get info about the artifact. // @ts-ignore const response = await fetch(getPreSignedUrl); if (response.status !== 200 && !response.ok) throw new Error(`There was an erorr while downloading the object ${storagePath} from the bucket ${bucketName}. Please check the function inputs and try again.`); const content = response.body; // Prepare stream. const writeStream = createWriteStream(localPath); // Write chunk by chunk. for await (const chunk of content) { // Write chunk. writeStream.write(chunk); } }; /** * Get R1CS file path tied to a particular circuit of a ceremony in the storage. * @notice each R1CS file in the storage must be stored in the following path: `circuits/<circuitPrefix>/<completeR1csFilename>`. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param circuitPrefix <string> - the prefix of the circuit. * @param completeR1csFilename <string> - the complete R1CS filename (name + ext). * @returns <string> - the storage path of the R1CS file. */ const getR1csStorageFilePath = (circuitPrefix, completeR1csFilename) => `${commonTerms.collections.circuits.name}/${circuitPrefix}/${completeR1csFilename}`; /** * Get WASM file path tied to a particular circuit of a ceremony in the storage. * @notice each WASM file in the storage must be stored in the following path: `circuits/<circuitPrefix>/<completeWasmFilename>`. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param circuitPrefix <string> - the prefix of the circuit. * @param completeWasmFilename <string> - the complete WASM filename (name + ext). * @returns <string> - the storage path of the WASM file. */ const getWasmStorageFilePath = (circuitPrefix, completeWasmFilename) => `${commonTerms.collections.circuits.name}/${circuitPrefix}/${completeWasmFilename}`; /** * Get PoT file path in the storage. * @notice each PoT file in the storage must be stored in the following path: `pot/<completePotFilename>`. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param completePotFilename <string> - the complete PoT filename (name + ext). * @returns <string> - the storage path of the PoT file. */ const getPotStorageFilePath = (completePotFilename) => `${commonTerms.foldersAndPathsTerms.pot}/${completePotFilename}`; /** * Get zKey file path tied to a particular circuit of a ceremony in the storage. * @notice each zKey file in the storage must be stored in the following path: `circuits/<circuitPrefix>/contributions/<completeZkeyFilename>`. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param circuitPrefix <string> - the prefix of the circuit. * @param completeZkeyFilename <string> - the complete zKey filename (name + ext). * @returns <string> - the storage path of the zKey file. */ const getZkeyStorageFilePath = (circuitPrefix, completeZkeyFilename) => `${commonTerms.collections.circuits.name}/${circuitPrefix}/${commonTerms.collections.contributions.name}/${completeZkeyFilename}`; /** * Get verification key file path tied to a particular circuit of a ceremony in the storage. * @notice each verification key file in the storage must be stored in the following path: `circuits/<circuitPrefix>/<completeVerificationKeyFilename>`. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param circuitPrefix <string> - the prefix of the circuit. * @param completeVerificationKeyFilename <string> - the complete verification key filename (name + ext). * @returns <string> - the storage path of the verification key file. */ const getVerificationKeyStorageFilePath = (circuitPrefix, completeVerificationKeyFilename) => `${commonTerms.collections.circuits.name}/${circuitPrefix}/${completeVerificationKeyFilename}`; /** * Get verifier contract file path tied to a particular circuit of a ceremony in the storage. * @notice each verifier contract file in the storage must be stored in the following path: `circuits/<circuitPrefix>/<completeVerificationKeyFilename>`. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param circuitPrefix <string> - the prefix of the circuit. * @param completeVerifierContractFilename <string> - the complete verifier contract filename (name + ext). * @returns <string> - the storage path of the verifier contract file. */ const getVerifierContractStorageFilePath = (circuitPrefix, completeVerifierContractFilename) => `${commonTerms.collections.circuits.name}/${circuitPrefix}/${completeVerifierContractFilename}`; /** * Get transcript file path tied to a particular circuit of a ceremony in the storage. * @notice each R1CS file in the storage must be stored in the following path: `circuits/<circuitPrefix>/<completeTranscriptFilename>`. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param circuitPrefix <string> - the prefix of the circuit. * @param completeTranscriptFilename <string> - the complete transcript filename (name + ext). * @returns <string> - the storage path of the transcript file. */ const getTranscriptStorageFilePath = (circuitPrefix, completeTranscriptFilename) => `${commonTerms.collections.circuits.name}/${circuitPrefix}/${commonTerms.foldersAndPathsTerms.transcripts}/${completeTranscriptFilename}`; /** * Get participants collection path for database reference. * @notice all participants related documents are store under `ceremonies/<ceremonyId>/participants` collection path. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param ceremonyId <string> - the unique identifier of the ceremony. * @returns <string> - the participants collection path. */ const getParticipantsCollectionPath = (ceremonyId) => `${commonTerms.collections.ceremonies.name}/${ceremonyId}/${commonTerms.collections.participants.name}`; /** * Get circuits collection path for database reference. * @notice all circuits related documents are store under `ceremonies/<ceremonyId>/circuits` collection path. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param ceremonyId <string> - the unique identifier of the ceremony. * @returns <string> - the participants collection path. */ const getCircuitsCollectionPath = (ceremonyId) => `${commonTerms.collections.ceremonies.name}/${ceremonyId}/${commonTerms.collections.circuits.name}`; /** * Get contributions collection path for database reference. * @notice all contributions related documents are store under `ceremonies/<ceremonyId>/circuits/<circuitId>/contributions` collection path. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param circuitId <string> - the unique identifier of the circuit. * @returns <string> - the contributions collection path. */ const getContributionsCollectionPath = (ceremonyId, circuitId) => `${getCircuitsCollectionPath(ceremonyId)}/${circuitId}/${commonTerms.collections.contributions.name}`; /** * Get timeouts collection path for database reference. * @notice all timeouts related documents are store under `ceremonies/<ceremonyId>/participants/<participantId>/timeouts` collection path. * nb. This is a rule that must be satisfied. This is NOT an optional convention. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param participantId <string> - the unique identifier of the participant. * @returns <string> - the timeouts collection path. */ const getTimeoutsCollectionPath = (ceremonyId, participantId) => `${getParticipantsCollectionPath(ceremonyId)}/${participantId}/${commonTerms.collections.timeouts.name}`; /** * Helper for query a collection based on certain constraints. * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application. * @param collection <string> - the name of the collection. * @param queryConstraints <Array<QueryConstraint>> - a sequence of where conditions. * @returns <Promise<QuerySnapshot<DocumentData>>> - return the matching documents (if any). */ const queryCollection = async (firestoreDatabase, collection$1, queryConstraints) => { // Make a query. const q = query(collection(firestoreDatabase, collection$1), ...queryConstraints); // Get docs. const snap = await getDocs(q); return snap; }; /** * Helper for obtaining uid and data for query document snapshots. * @param queryDocSnap <Array<QueryDocumentSnapshot>> - the array of query document snapshot to be converted. * @returns Array<FirebaseDocumentInfo> */ const fromQueryToFirebaseDocumentInfo = (queryDocSnap) => queryDocSnap.map((document) => ({ id: document.id, ref: document.ref, data: document.data() })); /** * Fetch for all documents in a collection. * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application. * @param collection <string> - the name of the collection. * @returns <Promise<Array<QueryDocumentSnapshot<DocumentData>>>> - return all documents (if any). */ const getAllCollectionDocs = async (firestoreDatabase, collection$1) => (await getDocs(collection(firestoreDatabase, collection$1))).docs; /** * Get a specific document from database. * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application. * @param collection <string> - the name of the collection. * @param documentId <string> - the unique identifier of the document in the collection. * @returns <Promise<DocumentSnapshot<DocumentData>>> - return the document from Firestore. */ const getDocumentById = async (firestoreDatabase, collection, documentId) => { const docRef = doc(firestoreDatabase, collection, documentId); return getDoc(docRef); }; /** * Query for opened ceremonies. * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application. * @returns <Promise<Array<FirebaseDocumentInfo>>> */ const getOpenedCeremonies = async (firestoreDatabase) => { const runningStateCeremoniesQuerySnap = await queryCollection(firestoreDatabase, commonTerms.collections.ceremonies.name, [ where(commonTerms.collections.ceremonies.fields.state, "==", "OPENED" /* CeremonyState.OPENED */), where(commonTerms.collections.ceremonies.fields.endDate, ">=", Date.now()) ]); return fromQueryToFirebaseDocumentInfo(runningStateCeremoniesQuerySnap.docs); }; /** * Query for ceremony circuits. * @notice the order by sequence position is fundamental to maintain parallelism among contributions for different circuits. * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application. * @param ceremonyId <string> - the ceremony unique identifier. * @returns Promise<Array<FirebaseDocumentInfo>> - the ceremony' circuits documents ordered by sequence position. */ const getCeremonyCircuits = async (firestoreDatabase, ceremonyId) => fromQueryToFirebaseDocumentInfo(await getAllCollectionDocs(firestoreDatabase, getCircuitsCollectionPath(ceremonyId))).sort((a, b) => a.data.sequencePosition - b.data.sequencePosition); /** * Query for a specific ceremony' circuit contribution from a given contributor (if any). * @notice if the caller is a coordinator, there could be more than one contribution (= the one from finalization applies to this criteria). * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application. * @param ceremonyId <string> - the unique identifier of the ceremony. * @param circuitId <string> - the unique identifier of the circuit. * @param participantId <string> - the unique identifier of the participant. * @returns <Promise<Array<FirebaseDocumentInfo>>> - the document info about the circuit contributions from contributor. */ const getCircuitContributionsFromContributor = async (firestoreDatabase, ceremonyId, circuitId, participantId) => { const participantContributionsQuerySnap = await queryCollection(firestoreDatabase, getContributionsCollectionPath(ceremonyId, circuitId), [where(commonTerms.collections.contributions.fields.participantId, "==", participantId)]); return fromQueryToFirebaseDocumentInfo(participantContributionsQuerySnap.docs); }; /** * Query for the active timeout from given participant for a given ceremony (if any). * @param ceremonyId <string> - the identifier of the ceremony. * @param participantId <string> - the identifier of the participant. * @returns <Promise<Array<FirebaseDocumentInfo>>> - the document info about the current active participant timeout. */ const getCurrentActiveParticipantTimeout = async (firestoreDatabase, ceremonyId, participantId) => { const participantTimeoutQuerySnap = await queryCollection(firestoreDatabase, getTimeoutsCollectionPath(ceremonyId, participantId), [where