@devtion/actions
Version:
A set of actions and helpers for CLI commands
996 lines (992 loc) • 140 kB
JavaScript
/**
* @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