UNPKG

@stagtion/stagcli

Version:

All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies

869 lines (862 loc) 213 kB
#!/usr/bin/env node /** * @module @p0tion/phase2cli * @version 1.2.6 * @file All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies * @copyright Ethereum Foundation 2022 * @license MIT * @see [Github]{@link https://github.com/privacy-scaling-explorations/p0tion} */ import { createCommand } from 'commander'; import fs, { readFileSync, createWriteStream, existsSync, renameSync } from 'fs'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; import { zKey, groth16 } from 'snarkjs'; import boxen from 'boxen'; import { pipeline } from 'node:stream'; import { promisify } from 'node:util'; import fetch$1 from 'node-fetch'; import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, contribHashRegex, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract, getAllCeremonies } from '@stagtion/actions'; import fetch from '@adobe/node-fetch-retry'; import { request } from '@octokit/request'; import { SingleBar, Presets } from 'cli-progress'; import dotenv from 'dotenv'; import { GithubAuthProvider, signInWithCustomToken, getAuth, signOut } from 'firebase/auth'; import { getDiskInfoSync } from 'node-disk-info'; import ora from 'ora'; import { Timer } from 'timer-node'; import chalk from 'chalk'; import logSymbols from 'log-symbols'; import emoji from 'node-emoji'; import Conf from 'conf'; import prompts from 'prompts'; import clear from 'clear'; import figlet from 'figlet'; import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device'; import clipboard from 'clipboardy'; import open from 'open'; import { Identity } from '@semaphore-protocol/identity'; import { httpsCallable } from 'firebase/functions'; import { ApiSdk } from '@bandada/api-sdk'; import { Timestamp, onSnapshot, doc, collection, getDocs } from 'firebase/firestore'; import readline from 'readline'; /** * Different custom progress bar types. * @enum {string} */ var ProgressBarType; (function (ProgressBarType) { ProgressBarType["DOWNLOAD"] = "DOWNLOAD"; ProgressBarType["UPLOAD"] = "UPLOAD"; })(ProgressBarType || (ProgressBarType = {})); /** * Custom theme object. */ var theme = { colors: { yellow: chalk.yellow, magenta: chalk.magenta, red: chalk.red, green: chalk.green }, text: { underlined: chalk.underline, bold: chalk.bold, italic: chalk.italic }, symbols: { success: logSymbols.success, warning: logSymbols.warning, error: logSymbols.error, info: logSymbols.info }, emojis: { tada: emoji.get("tada"), key: emoji.get("key"), broom: emoji.get("broom"), pointDown: emoji.get("point_down"), eyes: emoji.get("eyes"), wave: emoji.get("wave"), clipboard: emoji.get("clipboard"), fire: emoji.get("fire"), clock: emoji.get("hourglass"), dizzy: emoji.get("dizzy_face"), rocket: emoji.get("rocket"), oldKey: emoji.get("old_key"), pray: emoji.get("pray"), moon: emoji.get("moon"), upsideDown: emoji.get("upside_down_face"), arrowUp: emoji.get("arrow_up"), arrowDown: emoji.get("arrow_down") } }; /** Services */ const CORE_SERVICES_ERRORS = { FIREBASE_DEFAULT_APP_DOUBLE_CONFIG: `Wrong double default configuration for Firebase application`, FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `The Github authorization has failed due to lack of association between your account and the CLI`, FIREBASE_USER_DISABLED: `The Github account has been suspended by the ceremony coordinator(s), blocking the possibility of contribution. Please, contact them to understand the motivation behind it.`, FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials due to network errors. Please, try once again later.`, FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network errors. Please, try once again later and make sure your Internet connection is stable.`, FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`, FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization`, AWS_CEREMONY_BUCKET_CREATION: `Unable to create a new bucket for the ceremony. Something went wrong during the creation. Please, repeat the process by providing a new ceremony name of the ceremony.`, AWS_CEREMONY_BUCKET_CANNOT_DOWNLOAD_GET_PRESIGNED_URL: `Unable to download the file from the ceremony bucket. This problem could be related to failure when generating the pre-signed url. Please, we kindly ask you to terminate the current session and repeat the process.` }; /** Github */ const THIRD_PARTY_SERVICES_ERRORS = { GITHUB_ACCOUNT_ASSOCIATION_REJECTED: `You have decided not to associate the CLI application with your Github account. This declination will not allow you to make a contribution to any ceremony. In case you made a mistake, you can always repeat the process and accept the association of your Github account with the CLI.`, GITHUB_SERVER_TIMEDOUT: `Github's servers are experiencing downtime. Please, try once again later and make sure your Internet connection is stable.`, GITHUB_GET_GITHUB_ACCOUNT_INFO: `Something went wrong while retrieving your Github account public information (handle and identifier). Please, try once again later`, GITHUB_NOT_AUTHENTICATED: `You are unable to execute the command since you have not authorized this device with your Github account.\n${theme.symbols.info} Please, run the ${theme.text.bold("phase2cli auth")} command and make sure that your account meets the authentication criteria.`, GITHUB_GIST_PUBLICATION_FAILED: `Unable to publish the public attestation as gist making the request using your authenticated Github account. Please, verify that you have allowed the 'gist' access permission during the authentication step.` }; /** Command */ const COMMAND_ERRORS = { COMMAND_NOT_COORDINATOR: `Unable to execute the command. In order to perform coordinator functionality you must authenticate with an account having adeguate permissions.`, COMMAND_ABORT_PROMPT: `The data submission process was suddenly interrupted. Your previous data has not been saved. We are sorry, you will have to repeat the process again from the beginning.`, COMMAND_ABORT_SELECTION: `The data selection process was suddenly interrupted. Your previous data has not been saved. We are sorry, you will have to repeat the process again from the beginning.`, COMMAND_SETUP_NO_R1CS: `Unable to retrieve R1CS files from current working directory. Please, run this command from a working directory where the R1CS files are located to continue with the setup process. We kindly ask you to run the command from an empty directory containing only the R1CS and WASM files.`, COMMAND_SETUP_NO_WASM: `Unable to retrieve WASM files from current working directory. Please, run this command from a working directory where the WASM files are located to continue with the setup process. We kindly ask you to run the command from an empty directory containing only the WASM and R1CS files.`, COMMAND_SETUP_MISMATCH_R1CS_WASM: `The folder contains more R1CS files than WASM files (or vice versa). Please, run this command from a working directory where each R1CS is paired with its corresponding file WASM.`, COMMAND_SETUP_DOWNLOAD_PTAU: `Unable to download Powers of Tau file from PPoT Phase 1 Trusted Setup. Possible causes may involve an error while making the request (be sure to have a stable internet connection). Please, we kindly ask you to terminate the current session and repeat the process.`, COMMAND_SETUP_ABORT: `You chose to abort the setup process.`, COMMAND_CONTRIBUTE_NO_OPENED_CEREMONIES: `Unfortunately, there is no ceremony for which you can make a contribution at this time. Please, try again later.`, COMMAND_CONTRIBUTE_NO_PARTICIPANT_DATA: `Unable to retrieve your data as ceremony participant. Please, terminate the current session and try again later. If the error persists, please contact the ceremony coordinator.`, COMMAND_CONTRIBUTE_WRONG_OPTION_CEREMONY: `The ceremony name you provided does not exist or belongs to a ceremony not yet open. Please, double-check your option and retry.`, COMMAND_CONTRIBUTE_NO_CURRENT_CONTRIBUTOR_DATA: `Unable to retrieve current circuit contributor information. Please, terminate the current session and try again later. If the error persists, please contact the ceremony coordinator.`, COMMAND_CONTRIBUTE_NO_CURRENT_CONTRIBUTOR_CONTRIBUTION: `Unable to retrieve circuit last contribution information. This could happen due to a timeout or some errors while writing the information on the database.`, COMMAND_CONTRIBUTE_WRONG_CURRENT_CONTRIBUTOR_CONTRIBUTION_STEP: `Something went wrong when progressing the contribution step of the current circuit contributor. If the error persists, please contact the ceremony coordinator.`, COMMAND_CONTRIBUTE_NO_CIRCUIT_DATA: `Unable to retrieve circuit data from the ceremony. Please, terminate the current session and try again later. If the error persists, please contact the ceremony coordinator.`, COMMAND_CONTRIBUTE_NO_ACTIVE_TIMEOUT_DATA: `Unable to retrieve your active timeout data. This problem could be related to failure to write timeout data to the database. If the error persists, please contact the ceremony coordinator.`, COMMAND_CONTRIBUTE_NO_UNIQUE_ACTIVE_TIMEOUTS: `The number of active timeouts is different from one. This problem could be related to failure to update timeout document in the database. If the error persists, please contact the ceremony coordinator.`, COMMAND_CONTRIBUTE_FINALIZE_NO_TRANSCRIPT_CONTRIBUTION_HASH_MATCH: `Unable to retrieve contribution hash from transcript. Possible causes may involve an error while using the logger or unexpected file descriptor termination. Please, terminate the current session and repeat the process.`, COMMAND_FINALIZED_NO_CLOSED_CEREMONIES: `Unfortunately, there is no ceremony closed and ready for finalization. Please, try again later.`, COMMAND_FINALIZED_NOT_READY_FOR_FINALIZATION: `You are not ready for ceremony finalization. This could happen because the ceremony does not appear closed or you do not have completed every circuit contributions. If the error persists, please contact the operator to check the server logs.` }; /** Config */ const CONFIG_ERRORS = { CONFIG_GITHUB_ERROR: `Configuration error. The Github client id environment variable has not been configured correctly.`, CONFIG_FIREBASE_ERROR: `Configuration error. The Firebase environment variable has not been configured correctly`, CONFIG_OTHER_ERROR: `Configuration error. One or more config environment variable has not been configured correctly` }; /** Generic */ const GENERIC_ERRORS = { GENERIC_ERROR_RETRIEVING_DATA: `Something went wrong when retrieving the data from the database`, GENERIC_COUNTDOWN_EXPIRATION: `Your time to carry out the action has expired` }; /** * Print an error string and gracefully terminate the process. * @param err <string> - the error string to be shown. * @param doExit <boolean> - when true the function terminate the process; otherwise not. */ const showError = (err, doExit) => { // Print the error. console.error(`${theme.symbols.error} ${err}`); // Terminate the process. if (doExit) process.exit(1); }; /** * Check a directory path. * @param directoryPath <string> - the local path of the directory. * @returns <boolean> true if the directory at given path exists, otherwise false. */ const directoryExists = (directoryPath) => fs.existsSync(directoryPath); /** * Write a new file locally. * @param localFilePath <string> - the local path of the file. * @param data <Buffer> - the content to be written inside the file. */ const writeFile = (localFilePath, data) => fs.writeFileSync(localFilePath, data); /** * Read a new file from local folder. * @param localFilePath <string> - the local path of the file. */ const readFile = (localFilePath) => fs.readFileSync(localFilePath, "utf-8"); /** * Get back the statistics of the provided file. * @param localFilePath <string> - the local path of the file. * @returns <Stats> - the metadata of the file. */ const getFileStats = (localFilePath) => fs.statSync(localFilePath); /** * Return the sub-paths for each file stored in the given directory. * @param directoryLocalPath <string> - the local path of the directory. * @returns <Promise<Array<Dirent>>> - the list of sub-paths of the files contained inside the directory. */ const getDirFilesSubPaths = async (directoryLocalPath) => { // Get Dirent sub paths for folders and files. const subPaths = await fs.promises.readdir(directoryLocalPath, { withFileTypes: true }); // Return Dirent sub paths for files only. return subPaths.filter((dirent) => dirent.isFile()); }; /** * Filter all files in a directory by returning only those that match the given extension. * @param directoryLocalPath <string> - the local path of the directory. * @param fileExtension <string> - the file extension. * @returns <Promise<Array<Dirent>>> - return the filenames of the file that match the given extension, if any */ const filterDirectoryFilesByExtension = async (directoryLocalPath, fileExtension) => { // Get the sub paths for each file stored in the given directory. const cwdFiles = await getDirFilesSubPaths(directoryLocalPath); // Filter by extension. return cwdFiles.filter((file) => file.name.includes(fileExtension)); }; /** * Delete a directory specified at a given path. * @param directoryLocalPath <string> - the local path of the directory. */ const deleteDir = (directoryLocalPath) => { fs.rmSync(directoryLocalPath, { recursive: true, force: true }); }; /** * Clean a directory specified at a given path. * @param directoryLocalPath <string> - the local path of the directory. */ const cleanDir = (directoryLocalPath) => { deleteDir(directoryLocalPath); fs.mkdirSync(directoryLocalPath); }; /** * Create a new directory in a specified path if not exist in that path. * @param directoryLocalPath <string> - the local path of the directory. */ const checkAndMakeNewDirectoryIfNonexistent = (directoryLocalPath) => { if (!directoryExists(directoryLocalPath)) fs.mkdirSync(directoryLocalPath); }; /** * Write data a local JSON file at a given path. * @param localFilePath <string> - the local path of the file. * @param data <JSON> - the JSON content to be written inside the file. */ const writeLocalJsonFile = (filePath, data) => { fs.writeFileSync(filePath, JSON.stringify(data), "utf-8"); }; /** * Return the local current project directory name. * @returns <string> - the local project (e.g., dist/) directory name. */ const getLocalDirname = () => { const filename = fileURLToPath(import.meta.url); return path.dirname(filename); }; // Get npm package name. const packagePath$4 = `${dirname(fileURLToPath(import.meta.url))}/..`; const { name: name$1 } = JSON.parse(readFileSync(packagePath$4.includes(`src/lib/`) ? `${packagePath$4}/../package.json` : `${packagePath$4}/package.json`, "utf8")); /** * Local Storage. * @dev The CLI implementation use the Conf package to create a local storage * in the user device (`.config/@p0tion/phase2cli-nodejs/config.json` path) to store the access token. */ const config = new Conf({ projectName: name$1, schema: { accessToken: { type: "string", default: "" }, bandadaIdentity: { type: "string", default: "" }, authMethod: { type: "string", default: "" } } }); /** * Local Paths. * @dev definition of the paths to the local folders containing the CLI-generated artifacts. */ const outputLocalFolderPath = `./${commonTerms.foldersAndPathsTerms.output}`; const setupLocalFolderPath = `${outputLocalFolderPath}/${commonTerms.foldersAndPathsTerms.setup}`; const contributeLocalFolderPath = `${outputLocalFolderPath}/${commonTerms.foldersAndPathsTerms.contribute}`; const finalizeLocalFolderPath = `${outputLocalFolderPath}/${commonTerms.foldersAndPathsTerms.finalize}`; const potLocalFolderPath = `${setupLocalFolderPath}/${commonTerms.foldersAndPathsTerms.pot}`; const zkeysLocalFolderPath = `${setupLocalFolderPath}/${commonTerms.foldersAndPathsTerms.zkeys}`; const wasmLocalFolderPath = `${setupLocalFolderPath}/${commonTerms.foldersAndPathsTerms.wasm}`; const contributionsLocalFolderPath = `${contributeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.zkeys}`; const contributionTranscriptsLocalFolderPath = `${contributeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.transcripts}`; const attestationLocalFolderPath = `${contributeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.attestation}`; const finalZkeysLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.zkeys}`; const finalPotLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.pot}`; const finalTranscriptsLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.transcripts}`; const finalAttestationsLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.attestation}`; const verificationKeysLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.vkeys}`; const verifierContractsLocalFolderPath = `${finalizeLocalFolderPath}/${commonTerms.foldersAndPathsTerms.verifiers}`; const localPaths = { output: outputLocalFolderPath, setup: setupLocalFolderPath, contribute: contributeLocalFolderPath, finalize: finalizeLocalFolderPath, pot: potLocalFolderPath, zkeys: zkeysLocalFolderPath, wasm: wasmLocalFolderPath, contributions: contributionsLocalFolderPath, transcripts: contributionTranscriptsLocalFolderPath, attestations: attestationLocalFolderPath, finalZkeys: finalZkeysLocalFolderPath, finalPot: finalPotLocalFolderPath, finalTranscripts: finalTranscriptsLocalFolderPath, finalAttestations: finalAttestationsLocalFolderPath, verificationKeys: verificationKeysLocalFolderPath, verifierContracts: verifierContractsLocalFolderPath }; /** * Return the access token, if present. * @returns <string | undefined> - the access token if present, otherwise undefined. */ const getLocalAccessToken = () => config.get("accessToken"); /** * Check if the access token exists in the local storage. * @returns <boolean> */ const checkLocalAccessToken = () => config.has("accessToken") && !!config.get("accessToken"); /** * Set the access token. * @param token <string> - the access token to be stored. */ const setLocalAccessToken = (token) => config.set("accessToken", token); /** * Delete the stored access token. */ const deleteLocalAccessToken = () => config.delete("accessToken"); /** * Return the Bandada identity, if present. * @returns <string | undefined> - the Bandada identity if present, otherwise undefined. */ const getLocalBandadaIdentity = () => config.get("bandadaIdentity"); /** * Check if the Bandada identity exists in the local storage. * @returns <boolean> */ const checkLocalBandadaIdentity = () => config.has("bandadaIdentity") && !!config.get("bandadaIdentity"); /** * Set the Bandada identity. * @param identity <string> - the Bandada identity to be stored. */ const setLocalBandadaIdentity = (identity) => config.set("bandadaIdentity", identity); /** * Delete the stored Bandada identity. */ const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity"); /** * Return the authentication method, if present. * @returns <string | undefined> - the authentication method if present, otherwise undefined. */ const getLocalAuthMethod = () => config.get("authMethod"); /** * Set the authentication method. * @param method <string> - the authentication method to be stored. */ const setLocalAuthMethod = (method) => config.set("authMethod", method); /** * Delete the stored authentication method. */ const deleteLocalAuthMethod = () => config.delete("authMethod"); /** * Get the complete local file path. * @param cwd <string> - the current working directory path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete local path to the file. */ const getCWDFilePath = (cwd, completeFilename) => `${cwd}/${completeFilename}`; /** * Get the complete PoT file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete PoT path to the file. */ const getPotLocalFilePath = (completeFilename) => `${potLocalFolderPath}/${completeFilename}`; /** * Get the complete zKey file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete zKey path to the file. */ const getZkeyLocalFilePath = (completeFilename) => `${zkeysLocalFolderPath}/${completeFilename}`; /** * Get the complete contribution file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete contribution path to the file. */ const getContributionLocalFilePath = (completeFilename) => `${contributionsLocalFolderPath}/${completeFilename}`; /** * Get the contribution attestation file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the the contribution attestation path to the file. */ const getAttestationLocalFilePath = (completeFilename) => `${attestationLocalFolderPath}/${completeFilename}`; /** * Get the transcript file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the the transcript path to the file. */ const getTranscriptLocalFilePath = (completeFilename) => `${contributionTranscriptsLocalFolderPath}/${completeFilename}`; /** * Get the complete final zKey file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete final zKey path to the file. */ const getFinalZkeyLocalFilePath = (completeFilename) => `${finalZkeysLocalFolderPath}/${completeFilename}`; /** * Get the complete verification key file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete final verification key path to the file. */ const getVerificationKeyLocalFilePath = (completeFilename) => `${verificationKeysLocalFolderPath}/${completeFilename}`; /** * Get the complete verifier contract file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete final verifier contract path to the file. */ const getVerifierContractLocalFilePath = (completeFilename) => `${verifierContractsLocalFolderPath}/${completeFilename}`; /** * Get the complete final attestation file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the complete final final attestation path to the file. */ const getFinalAttestationLocalFilePath = (completeFilename) => `${finalAttestationsLocalFolderPath}/${completeFilename}`; /** * Get the final transcript file path. * @param completeFilename <string> - the complete filename of the file (name.ext). * @returns <string> - the the final transcript path to the file. */ const getFinalTranscriptLocalFilePath = (completeFilename) => `${finalTranscriptsLocalFolderPath}/${completeFilename}`; const packagePath$3 = `${dirname(fileURLToPath(import.meta.url))}`; dotenv.config({ path: packagePath$3.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> */ const exchangeGithubTokenForCredentials = (githubToken) => 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. */ const getGithubProviderUserId = async (githubToken) => { // 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. */ const getGithubAuthenticatedUserGists = async (githubToken, params) => { // 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. */ const getPublicAttestationGist = async (githubToken, publicAttestationFilename) => { const itemsPerPage = 50; // number of gists to fetch x page. let gists = []; // The list of user gists. let publishedGist; // 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]; /// @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. */ const getUserHandleFromProviderUserId = (providerUserId) => { 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. */ const customSpinner = (text, spinnerLogo) => 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>> */ const sleep = (ms) => 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>>. */ const simpleLoader = async (loadingText, spinnerLogo, durationInMs) => { // 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. */ const estimateParticipantFreeGlobalDiskSpace = () => { // 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. */ const getSecondsMinutesHoursFromMillis = (millis) => { 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 }; }; /** * Gracefully terminate the command execution * @params ghUsername <string> - the Github username of the user. */ const terminate = async (ghUsername) => { 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. */ const publishGist = async (token, content, ceremonyTitle, ceremonyPrefix) => { // 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. */ const generateCustomUrlToTweetAboutParticipation = (ceremonyName, gistUrl, isFinalizing) => { 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, message) => { // 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. */ const downloadCeremonyArtifact = async (cloudFunctions, bucketName, storagePath, localPath) => { 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 = 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. */ const handleContributionComputation = async (lastZkeyLocalFilePath, nextZkeyLocalFilePath, entropyOrBeacon, contributorOrCoordinatorIdentifier, averageComputingTime, transcriptLogger, isFinalizing) => { // Prepare timer (statistics only). const computingTimer = new Timer({ label: "COMPUTING" /* 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. */ const getLatestUpdatesFromParticipant = async (firestoreDatabase, ceremonyId, participantId) => { // 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. */ const handleStartOrResumeContribution = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, entropyOrBeaconHash, contributorOrCoordinatorIdentifier, isFinalizing, circuitsLength) => { // 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 === "DOWNLOADING" /* 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 === "DOWNLOADING" /* 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 === "COMPUTING" /* 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(`${convertToDoubleD