@devtion/devcli
Version:
All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies
817 lines (705 loc) • 33.3 kB
text/typescript
import fetch from "@adobe/node-fetch-retry"
import { request } from "@octokit/request"
import {
commonTerms,
convertBytesOrKbToGb,
createCustomLoggerForFile,
convertToDoubleDigits,
finalContributionIndex,
FirebaseDocumentInfo,
formatZkeyIndex,
generateGetObjectPreSignedUrl,
getBucketName,
getDocumentById,
getParticipantsCollectionPath,
getZkeyStorageFilePath,
multiPartUpload,
numExpIterations,
ParticipantContributionStep,
permanentlyStoreCurrentContributionTimeAndHash,
progressToNextContributionStep,
verifyContribution,
contribHashRegex
} from "@devtion/actions"
import { Presets, SingleBar } from "cli-progress"
import dotenv from "dotenv"
import { GithubAuthProvider, OAuthCredential } from "firebase/auth"
import { DocumentData, Firestore } from "firebase/firestore"
import { Functions } from "firebase/functions"
import { createWriteStream } from "fs"
import { getDiskInfoSync } from "node-disk-info"
import ora, { Ora } from "ora"
import { zKey } from "snarkjs"
import { Timer } from "timer-node"
import { Logger } from "winston"
import { fileURLToPath } from "url"
import { dirname } from "path"
import { GithubGistFile, ProgressBarType, Timing } from "../types/index.js"
import { COMMAND_ERRORS, CORE_SERVICES_ERRORS, showError, THIRD_PARTY_SERVICES_ERRORS } from "./errors.js"
import { readFile } from "./files.js"
import {
getContributionLocalFilePath,
getFinalTranscriptLocalFilePath,
getFinalZkeyLocalFilePath,
getTranscriptLocalFilePath
} from "./localConfigs.js"
import theme from "./theme.js"
const packagePath = `${dirname(fileURLToPath(import.meta.url))}`
dotenv.config({
path: packagePath.includes(`src/lib`)
? `${dirname(fileURLToPath(import.meta.url))}/../../.env`
: `${dirname(fileURLToPath(import.meta.url))}/.env`
})
/**
* Exchange the Github token for OAuth credential.
* @param githubToken <string> - the Github token generated through the Device Flow process.
* @returns <OAuthCredential>
*/
export const exchangeGithubTokenForCredentials = (githubToken: string): OAuthCredential =>
GithubAuthProvider.credential(githubToken)
/**
* Get the information associated to the account from which the token has been generated to
* create a custom unique identifier for the user.
* @notice the unique identifier has the following form 'handle-identifier'.
* @param githubToken <string> - the Github token.
* @returns <Promise<any>> - the Github (provider) unique identifier associated to the user.
*/
export const getGithubProviderUserId = async (githubToken: string): Promise<any> => {
// Ask for user account public information through Github API.
const response = await request("GET https://api.github.com/user", {
headers: {
authorization: `token ${githubToken}`
}
})
if (response && response.status === 200) return `${response.data.login}-${response.data.id}`
showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
}
/**
* Get the gists associated to the authenticated user account.
* @param githubToken <string> - the Github token.
* @param params <Object<number,number>> - the necessary parameters for the request.
* @returns <Promise<any>> - the Github gists associated with the authenticated user account.
*/
export const getGithubAuthenticatedUserGists = async (
githubToken: string,
params: { perPage: number; page: number }
): Promise<any> => {
// Ask for user account public information through Github API.
const response = await request("GET https://api.github.com/gists{?per_page,page}", {
headers: {
authorization: `token ${githubToken}`
},
per_page: params.perPage, // max items per page = 100.
page: params.page
})
if (response && response.status === 200) return response.data
showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
}
/**
* Check whether or not the user has published the gist.
* @dev gather all the user's gists and check if there is a match with the expected public attestation.
* @param githubToken <string> - the Github token.
* @param publicAttestationFilename <string> - the public attestation filename.
* @returns <Promise<GithubGistFile | undefined>> - return the public attestation gist if and only if has been published.
*/
export const getPublicAttestationGist = async (
githubToken: string,
publicAttestationFilename: string
): Promise<GithubGistFile | undefined> => {
const itemsPerPage = 50 // number of gists to fetch x page.
let gists: Array<any> = [] // The list of user gists.
let publishedGist: GithubGistFile | undefined // the published public attestation gist.
let page = 1 // Page of gists = starts from 1.
// Get first batch (page) of gists
let pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page })
// State update.
gists = gists.concat(pageGists)
// Keep going until hitting a blank page.
while (pageGists.length > 0) {
// Fetch next page.
page += 1
pageGists = await getGithubAuthenticatedUserGists(githubToken, { perPage: itemsPerPage, page })
// State update.
gists = gists.concat(pageGists)
}
// Look for public attestation.
for (const gist of gists) {
const numberOfFiles = Object.keys(gist.files).length
const publicAttestationCandidateFile = Object.values(gist.files)[0] as GithubGistFile
/// @todo improve check by using expected public attestation content (e.g., hash).
if (numberOfFiles === 1 && publicAttestationCandidateFile.filename === publicAttestationFilename)
publishedGist = publicAttestationCandidateFile
}
return publishedGist
}
/**
* Return the Github handle from the provider user id.
* @notice the provider user identifier must have the following structure 'handle-id'.
* @param providerUserId <string> - the unique provider user identifier.
* @returns <string> - the third-party provider handle of the user.
*/
export const getUserHandleFromProviderUserId = (providerUserId: string): string => {
if (providerUserId.indexOf("-") === -1) {
return providerUserId
}
return providerUserId.substring(0, providerUserId.lastIndexOf("-"))
}
/**
* Return a custom spinner.
* @param text <string> - the text that should be displayed as spinner status.
* @param spinnerLogo <any> - the logo.
* @returns <Ora> - a new Ora custom spinner.
*/
export const customSpinner = (text: string, spinnerLogo: any): Ora =>
ora({
text,
spinner: spinnerLogo
})
/**
* Custom sleeper.
* @dev to be used in combination with loggers and for workarounds where listeners cannot help.
* @param ms <number> - sleep amount in milliseconds
* @returns <Promise<any>>
*/
export const sleep = (ms: number): Promise<any> => new Promise((resolve) => setTimeout(resolve, ms))
/**
* Simple loader for task simulation.
* @param loadingText <string> - spinner text while loading.
* @param spinnerLogo <any> - spinner logo.
* @param durationInMs <number> - spinner loading duration in ms.
* @returns <Promise<void>>.
*/
export const simpleLoader = async (loadingText: string, spinnerLogo: any, durationInMs: number): Promise<void> => {
// Custom spinner (used as loader).
const loader = customSpinner(loadingText, spinnerLogo)
loader.start()
// nb. simulate execution for requested duration.
await sleep(durationInMs)
loader.stop()
}
/**
* Check and return the free aggregated disk space (in KB) for participant machine.
* @dev this method use the node-disk-info method to retrieve the information about
* disk availability for all visible disks.
* nb. no other type of data or operation is performed by this methods.
* @returns <number> - the free aggregated disk space in kB for the participant machine.
*/
export const estimateParticipantFreeGlobalDiskSpace = (): number => {
// Get info about disks.
const disks = getDiskInfoSync()
// Get an estimation of available memory.
let availableDiskSpace = 0
for (const disk of disks) availableDiskSpace += disk.available
// Return the disk space available in KB.
return availableDiskSpace
}
/**
* Get seconds, minutes, hours and days from milliseconds.
* @param millis <number> - the amount of milliseconds.
* @returns <Timing> - a custom object containing the amount of seconds, minutes, hours and days in the provided millis.
*/
export const getSecondsMinutesHoursFromMillis = (millis: number): Timing => {
let delta = millis / 1000
const days = Math.floor(delta / 86400)
delta -= days * 86400
const hours = Math.floor(delta / 3600) % 24
delta -= hours * 3600
const minutes = Math.floor(delta / 60) % 60
delta -= minutes * 60
const seconds = Math.floor(delta) % 60
return {
seconds: seconds >= 60 ? 59 : seconds,
minutes: minutes >= 60 ? 59 : minutes,
hours: hours >= 24 ? 23 : hours,
days
}
}
/**
* Convert milliseconds to seconds.
* @param millis <number>
* @returns <number>
*/
export const convertMillisToSeconds = (millis: number): number => Number((millis / 1000).toFixed(2))
/**
* Gracefully terminate the command execution
* @params ghUsername <string> - the Github username of the user.
*/
export const terminate = async (ghUsername: string) => {
console.log(`\nSee you, ${theme.text.bold(`@${getUserHandleFromProviderUserId(ghUsername)}`)} ${theme.emojis.wave}`)
process.exit(0)
}
/**
* Publish public attestation using Github Gist.
* @dev the contributor must have agreed to provide 'gist' access during the execution of the 'auth' command.
* @param accessToken <string> - the contributor access token.
* @param publicAttestation <string> - the public attestation.
* @param ceremonyTitle <string> - the ceremony title.
* @param ceremonyPrefix <string> - the ceremony prefix.
* @returns <Promise<string>> - the url where the gist has been published.
*/
export const publishGist = async (
token: string,
content: string,
ceremonyTitle: string,
ceremonyPrefix: string
): Promise<string> => {
// Make request.
const response = await request("POST /gists", {
description: `Attestation for ${ceremonyTitle} MPC Phase 2 Trusted Setup ceremony`,
public: true,
files: {
[`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`]: {
content
}
},
headers: {
authorization: `token ${token}`
}
})
if (response.status !== 201 || !response.data.html_url)
showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GIST_PUBLICATION_FAILED, true)
return response.data.html_url!
}
/**
* Generate a custom url that when clicked allows you to compose a tweet ready to be shared.
* @param ceremonyName <string> - the name of the ceremony.
* @param gistUrl <string> - the url of the gist where the public attestation has been shared.
* @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
* @returns <string> - the ready to share tweet url.
*/
export const generateCustomUrlToTweetAboutParticipation = (
ceremonyName: string,
gistUrl: string,
isFinalizing: boolean
) => {
ceremonyName = ceremonyName.replace(/ /g, "%20")
return isFinalizing
? `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremonyName}${
ceremonyName.toLowerCase().includes("trusted") ||
ceremonyName.toLowerCase().includes("setup") ||
ceremonyName.toLowerCase().includes("phase2") ||
ceremonyName.toLowerCase().includes("ceremony")
? "!"
: "%20Phase%202%20Trusted%20Setup%20ceremony!"
}%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE`
: `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyName}${
ceremonyName.toLowerCase().includes("trusted") ||
ceremonyName.toLowerCase().includes("setup") ||
ceremonyName.toLowerCase().includes("phase2") ||
ceremonyName.toLowerCase().includes("ceremony")
? "!"
: "%20Phase%202%20Trusted%20Setup%20ceremony!"
}%20You%20can%20view%20the%20steps%20to%20contribute%20here:%20https://ceremony.pse.dev%20You%20can%20view%20my%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP`
}
/**
* Return a custom progress bar.
* @param type <ProgressBarType> - the type of the progress bar.
* @param [message] <string> - additional information to be displayed when downloading/uploading.
* @returns <SingleBar> - a new custom (single) progress bar.
*/
const customProgressBar = (type: ProgressBarType, message?: string): SingleBar => {
// Formats.
const uploadFormat = `${theme.emojis.arrowUp} Uploading ${message} [${theme.colors.magenta(
"{bar}"
)}] {percentage}% | {value}/{total} Chunks`
const downloadFormat = `${theme.emojis.arrowDown} Downloading ${message} [${theme.colors.magenta(
"{bar}"
)}] {percentage}% | {value}/{total} GB`
// Define a progress bar showing percentage of completion and chunks downloaded/uploaded.
return new SingleBar(
{
format: type === ProgressBarType.DOWNLOAD ? downloadFormat : uploadFormat,
hideCursor: true,
clearOnComplete: true
},
Presets.legacy
)
}
/**
* Download an artifact from the ceremony bucket.
* @dev this method request a pre-signed url to make a GET request to download the artifact.
* @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
* @param bucketName <string> - the name of the ceremony artifacts bucket (AWS S3).
* @param storagePath <string> - the storage path that locates the artifact to be downloaded in the bucket.
* @param localPath <string> - the local path where the artifact will be downloaded.
*/
export const downloadCeremonyArtifact = async (
cloudFunctions: Functions,
bucketName: string,
storagePath: string,
localPath: string
): Promise<void> => {
const spinner = customSpinner(`Preparing for downloading the contribution...`, `clock`)
spinner.start()
// 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)
showError(CORE_SERVICES_ERRORS.AWS_CEREMONY_BUCKET_CANNOT_DOWNLOAD_GET_PRESIGNED_URL, true)
// Extract and prepare data.
const content: any = response.body
const contentLength = Number(response.headers.get("content-length"))
const contentLengthInGB = convertBytesOrKbToGb(contentLength, true)
// Prepare stream.
const writeStream = createWriteStream(localPath)
spinner.stop()
// Prepare custom progress bar.
const progressBar = customProgressBar(ProgressBarType.DOWNLOAD, `last contribution`)
const progressBarStep = contentLengthInGB / 100
let chunkLengthWritingProgress = 0
let completedProgress = progressBarStep
// Bootstrap the progress bar.
progressBar.start(contentLengthInGB < 0.01 ? 0.01 : parseFloat(contentLengthInGB.toFixed(2)).valueOf(), 0)
// Write chunk by chunk.
for await (const chunk of content) {
// Write chunk.
writeStream.write(chunk)
// Update current progress.
chunkLengthWritingProgress += convertBytesOrKbToGb(chunk.length, true)
// Display the current progress.
while (chunkLengthWritingProgress >= completedProgress) {
// Store new completed progress step by step.
completedProgress += progressBarStep
// Display accordingly in the progress bar.
progressBar.update(contentLengthInGB < 0.01 ? 0.01 : parseFloat(completedProgress.toFixed(2)).valueOf())
}
}
await sleep(2000) // workaround to show bar for small artifacts.
progressBar.stop()
}
/**
*
* @param lastZkeyLocalFilePath <string> - the local path of the last contribution.
* @param nextZkeyLocalFilePath <string> - the local path where the next contribution is going to be stored.
* @param entropyOrBeacon <string> - the entropy or beacon (only when finalizing) for the contribution.
* @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
* @param averageComputingTime <number> - the current average contribution computation time.
* @param transcriptLogger <Logger> - the custom file logger to generate the contribution transcript.
* @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
* @returns <Promise<number>> - the amount of time spent contributing.
*/
export const handleContributionComputation = async (
lastZkeyLocalFilePath: string,
nextZkeyLocalFilePath: string,
entropyOrBeacon: string,
contributorOrCoordinatorIdentifier: string,
averageComputingTime: number,
transcriptLogger: Logger,
isFinalizing: boolean
): Promise<number> => {
// Prepare timer (statistics only).
const computingTimer = new Timer({ label: ParticipantContributionStep.COMPUTING })
computingTimer.start()
// Time format.
const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis(averageComputingTime)
const spinner = customSpinner(
`${isFinalizing ? `Applying beacon...` : `Computing contribution...`} ${
averageComputingTime > 0
? `${theme.text.bold(
`(ETA ${theme.text.bold(
`${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits(
minutes
)}:${convertToDoubleDigits(seconds)}`
)}).\n${
theme.symbols.warning
} This may take longer or less time on your machine! Everything's fine, just be patient and do not stop the computation to avoid starting over again`
)}`
: ``
}`,
`clock`
)
spinner.start()
// Discriminate between contribution finalization or computation.
if (isFinalizing)
await zKey.beacon(
lastZkeyLocalFilePath,
nextZkeyLocalFilePath,
contributorOrCoordinatorIdentifier,
entropyOrBeacon,
numExpIterations,
transcriptLogger
)
else
await zKey.contribute(
lastZkeyLocalFilePath,
nextZkeyLocalFilePath,
contributorOrCoordinatorIdentifier,
entropyOrBeacon,
transcriptLogger
)
computingTimer.stop()
await sleep(3000) // workaround for file descriptor.
spinner.stop()
return computingTimer.ms()
}
/**
* Return the most up-to-date data about the participant document for the given ceremony.
* @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
* @param ceremonyId <string> - the unique identifier of the ceremony.
* @param participantId <string> - the unique identifier of the participant.
* @returns <Promise<DocumentData>> - the most up-to-date participant data.
*/
export const getLatestUpdatesFromParticipant = async (
firestoreDatabase: Firestore,
ceremonyId: string,
participantId: string
): Promise<DocumentData> => {
// Fetch participant data.
const participant = await getDocumentById(
firestoreDatabase,
getParticipantsCollectionPath(ceremonyId),
participantId
)
if (!participant.data()) showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA, true)
return participant.data()!
}
/**
* Start or resume a contribution from the last participant contribution step.
* @notice this method goes through each contribution stage following this order:
* 1) Downloads the last contribution from previous contributor.
* 2) Computes the new contribution.
* 3) Uploads the new contribution.
* 4) Requests the verification of the new contribution to the coordinator's backend and waits for the result.
* @param cloudFunctions <Functions> - the instance of the Firebase cloud functions for the application.
* @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
* @param ceremony <FirebaseDocumentInfo> - the Firestore document of the ceremony.
* @param circuit <FirebaseDocumentInfo> - the Firestore document of the ceremony circuit.
* @param participant <FirebaseDocumentInfo> - the Firestore document of the participant (contributor or coordinator).
* @param participantContributionStep <ParticipantContributionStep> - the contribution step of the participant (from where to start/resume contribution).
* @param entropyOrBeaconHash <string> - the entropy or beacon hash (only when finalizing) for the contribution.
* @param contributorOrCoordinatorIdentifier <string> - the identifier of the contributor or coordinator (only when finalizing).
* @param isFinalizing <boolean> - flag to discriminate between ceremony finalization (true) and contribution (false).
* @param circuitsLength <number> - the total number of circuits in the ceremony.
*/
export const handleStartOrResumeContribution = async (
cloudFunctions: Functions,
firestoreDatabase: Firestore,
ceremony: FirebaseDocumentInfo,
circuit: FirebaseDocumentInfo,
participant: FirebaseDocumentInfo,
entropyOrBeaconHash: any,
contributorOrCoordinatorIdentifier: string,
isFinalizing: boolean,
circuitsLength: number
): Promise<void> => {
// Extract data.
const { prefix: ceremonyPrefix } = ceremony.data
const { waitingQueue, avgTimings, prefix: circuitPrefix, sequencePosition } = circuit.data
const { completedContributions } = waitingQueue // = current progress.
console.log(
`${theme.text.bold(
`\n- Circuit # ${theme.colors.magenta(`${sequencePosition}/${circuitsLength}`)}`
)} (Contribution Steps)`
)
// Get most up-to-date data from the participant document.
let participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
const spinner = customSpinner(
`${
participantData.contributionStep === ParticipantContributionStep.DOWNLOADING
? `Preparing to begin the contribution. Please note that the contribution can take a long time depending on the size of the circuits and your internet connection.`
: `Preparing to resume contribution. Please note that the contribution can take a long time depending on the size of the circuits and your internet connection.`
}`,
`clock`
)
spinner.start()
// Compute zkey indexes.
const lastZkeyIndex = formatZkeyIndex(completedContributions)
const nextZkeyIndex = formatZkeyIndex(completedContributions + 1)
// Prepare zKey filenames.
const lastZkeyCompleteFilename = `${circuitPrefix}_${lastZkeyIndex}.zkey`
const nextZkeyCompleteFilename = isFinalizing
? `${circuitPrefix}_${finalContributionIndex}.zkey`
: `${circuitPrefix}_${nextZkeyIndex}.zkey`
// Prepare zKey storage paths.
const lastZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, lastZkeyCompleteFilename)
const nextZkeyStorageFilePath = getZkeyStorageFilePath(circuitPrefix, nextZkeyCompleteFilename)
// Prepare zKey local paths.
const lastZkeyLocalFilePath = isFinalizing
? getFinalZkeyLocalFilePath(lastZkeyCompleteFilename)
: getContributionLocalFilePath(lastZkeyCompleteFilename)
const nextZkeyLocalFilePath = isFinalizing
? getFinalZkeyLocalFilePath(nextZkeyCompleteFilename)
: getContributionLocalFilePath(nextZkeyCompleteFilename)
// Generate a custom file logger for contribution transcript.
const transcriptCompleteFilename = isFinalizing
? `${circuit.data.prefix}_${contributorOrCoordinatorIdentifier}_${finalContributionIndex}.log`
: `${circuit.data.prefix}_${nextZkeyIndex}.log`
const transcriptLocalFilePath = isFinalizing
? getFinalTranscriptLocalFilePath(transcriptCompleteFilename)
: getTranscriptLocalFilePath(transcriptCompleteFilename)
const transcriptLogger = createCustomLoggerForFile(transcriptLocalFilePath)
// Populate transcript file w/ header.
transcriptLogger.info(
`${isFinalizing ? `Final` : `Contribution`} transcript for ${circuitPrefix} phase 2 contribution.\n${
isFinalizing
? `Coordinator: ${contributorOrCoordinatorIdentifier}`
: `Contributor # ${Number(nextZkeyIndex)}`
} (${contributorOrCoordinatorIdentifier})\n`
)
// Get ceremony bucket name.
const bucketName = getBucketName(ceremonyPrefix, String(process.env.CONFIG_CEREMONY_BUCKET_POSTFIX))
await sleep(3000) // ~3s.
spinner.stop()
// Contribution step = DOWNLOADING.
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.DOWNLOADING) {
// Download the latest contribution from bucket.
await downloadCeremonyArtifact(cloudFunctions, bucketName, lastZkeyStorageFilePath, lastZkeyLocalFilePath)
console.log(
`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} correctly downloaded`
)
await sleep(3000)
// Advance to next contribution step (COMPUTING) if not finalizing.
if (!isFinalizing) {
spinner.text = `Preparing for contribution computation...`
spinner.start()
await progressToNextContributionStep(cloudFunctions, ceremony.id)
await sleep(1000)
// Refresh most up-to-date data from the participant document.
participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
spinner.stop()
}
} else
console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${lastZkeyIndex}`)} already downloaded`)
// Contribution step = COMPUTING.
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.COMPUTING) {
// Handle the next contribution computation.
const computingTime = await handleContributionComputation(
lastZkeyLocalFilePath,
nextZkeyLocalFilePath,
entropyOrBeaconHash,
contributorOrCoordinatorIdentifier,
avgTimings.contributionComputation,
transcriptLogger,
isFinalizing
)
// Permanently store on db the contribution hash and computing time.
spinner.text = `Writing contribution metadata...`
spinner.start()
// Read local transcript file info to get the contribution hash.
const transcriptContents = readFile(transcriptLocalFilePath)
const matchContributionHash = transcriptContents.match(contribHashRegex)
if (!matchContributionHash)
showError(COMMAND_ERRORS.COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH, true)
// Format contribution hash.
const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "")!
await sleep(500)
// Make request to cloud functions to permanently store the information.
await permanentlyStoreCurrentContributionTimeAndHash(
cloudFunctions,
ceremony.id,
computingTime,
contributionHash
)
// Format computing time.
const {
seconds: computationSeconds,
minutes: computationMinutes,
hours: computationHours
} = getSecondsMinutesHoursFromMillis(computingTime)
spinner.succeed(
`${
isFinalizing ? "Contribution" : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`
} computation took ${theme.text.bold(
`${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits(
computationMinutes
)}:${convertToDoubleDigits(computationSeconds)}`
)}`
)
// ensure the previous step is completed
await sleep(5000)
// Advance to next contribution step (UPLOADING) if not finalizing.
if (!isFinalizing) {
spinner.text = `Preparing for uploading the contribution...`
spinner.start()
await progressToNextContributionStep(cloudFunctions, ceremony.id)
await sleep(1000)
// Refresh most up-to-date data from the participant document.
participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
spinner.stop()
}
} else console.log(`${theme.symbols.success} Contribution ${theme.text.bold(`#${nextZkeyIndex}`)} already computed`)
// Contribution step = UPLOADING.
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.UPLOADING) {
spinner.text = `Uploading ${isFinalizing ? "final" : "your"} contribution ${
!isFinalizing ? theme.text.bold(`#${nextZkeyIndex}`) : ""
} to storage.\n${
theme.symbols.warning
} This step may take a while based on circuit size and your internet speed. Everything's fine, just be patient.`
spinner.start()
const progressBar = customProgressBar(ProgressBarType.UPLOAD, `your contribution`)
if (!isFinalizing) {
await multiPartUpload(
cloudFunctions,
bucketName,
nextZkeyStorageFilePath,
nextZkeyLocalFilePath,
Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB),
ceremony.id,
participantData.tempContributionData,
progressBar
)
progressBar.stop()
} else
await multiPartUpload(
cloudFunctions,
bucketName,
nextZkeyStorageFilePath,
nextZkeyLocalFilePath,
Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB)
)
// small sleep to ensure the previous step is completed
await sleep(5000)
spinner.succeed(
`${
isFinalizing ? `Contribution` : `Contribution ${theme.text.bold(`#${nextZkeyIndex}`)}`
} correctly saved to storage`
)
// Advance to next contribution step (VERIFYING) if not finalizing.
if (!isFinalizing) {
spinner.text = `Preparing for requesting contribution verification...`
spinner.start()
await progressToNextContributionStep(cloudFunctions, ceremony.id)
await sleep(1000)
// Refresh most up-to-date data from the participant document.
participantData = await getLatestUpdatesFromParticipant(firestoreDatabase, ceremony.id, participant.id)
spinner.stop()
}
}
// Contribution step = VERIFYING.
if (isFinalizing || participantData.contributionStep === ParticipantContributionStep.VERIFYING) {
// Format verification time.
const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(avgTimings.verifyCloudFunction)
process.stdout.write(
`${theme.symbols.info} Your contribution is under verification ${
avgTimings.verifyCloudFunction > 0
? `(~ ${theme.text.bold(
`${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(
seconds
)}`
)})\n${
theme.symbols.warning
} This step can take up to one hour based on circuit size. Everything's fine, just be patient.`
: ``
}`
)
try {
// Execute contribution verification.
await verifyContribution(
cloudFunctions,
ceremony.id,
circuit,
bucketName,
contributorOrCoordinatorIdentifier,
String(process.env.FIREBASE_CF_URL_VERIFY_CONTRIBUTION)
)
} catch (error: any) {
process.stdout.write(
`\n${theme.symbols.error} ${theme.text.bold(
"Unfortunately there was an error with the contribution verification. Please restart phase2cli and try again. If the problem persists, please contact the ceremony coordinator."
)}\n`
)
}
}
}