refactor: optimized coordinator claim verification, ceremony data prompt and setup command bootstrap

This commit is contained in:
Jeeiii
2023-01-25 19:08:02 +01:00
committed by Giacomo
parent 18404cea54
commit 3bda9520c5
12 changed files with 291 additions and 269 deletions

View File

@@ -44,6 +44,19 @@ export const getDirFilesSubPaths = async (dirPath: string): Promise<Array<Dirent
return subPaths.filter((dirent: Dirent) => dirent.isFile())
}
/**
* Filter all files in a directory by returning only those that match the given extension.
* @param dir <string> - the directory.
* @param extension <string> - the file extension.
* @returns <Promise<Array<Dirent>>> - return the filenames of the file that match the given extension, if any
*/
export const filterDirectoryFilesByExtension = async (dir: string, extension: string): Promise<Array<Dirent>> => {
// Get the sub paths for each file stored in the given directory.
const cwdFiles = await getDirFilesSubPaths(dir)
// Filter by extension.
return cwdFiles.filter((file: Dirent) => file.name.includes(extension))
}
/**
* Delete a directory specified at a given path.
* @param dirPath <string> - the directory path.

View File

@@ -81,3 +81,14 @@ export const getCurrentFirebaseAuthUser = (firebaseApp: FirebaseApp): User => {
return user
}
/**
* Check if the user can claim to be a coordinator.
* @param user <User> - the user to be checked.
* @returns Promise<boolean> - true if the user is a coordinator, false otherwise.
*/
export const isCoordinator = async (user: User) => {
const userTokenAndClaims = await user.getIdTokenResult()
return !!userTokenAndClaims.claims.coordinator
}

View File

@@ -42,6 +42,7 @@ export {
readFile,
getFileStats,
getDirFilesSubPaths,
filterDirectoryFilesByExtension,
deleteDir,
cleanDir,
checkAndMakeNewDirectoryIfNonexistent,
@@ -52,5 +53,6 @@ export {
export {
initializeFirebaseCoreServices,
signInToFirebaseWithCredentials,
getCurrentFirebaseAuthUser
getCurrentFirebaseAuthUser,
isCoordinator
} from "./helpers/firebase"

View File

@@ -1,17 +1,10 @@
#!/usr/bin/env node
import dotenv from "dotenv"
import { signInToFirebaseWithCredentials } from "@zkmpc/actions"
import { symbols, theme } from "../lib/constants"
import { exchangeGithubTokenForCredentials, getGithubUserHandle, terminate } from "../lib/utils"
import { bootstrapCommandExecutionAndServices } from "../lib/commands"
import { FIREBASE_ERRORS, GITHUB_ERRORS, showError } from "../lib/errors"
import { executeGithubDeviceFlow } from "../lib/authorization"
import {
checkLocalAccessToken,
deleteLocalAccessToken,
getLocalAccessToken,
setLocalAccessToken
} from "../lib/localStorage"
import { executeGithubDeviceFlow, signInToFirebase } from "../lib/authorization"
import { checkLocalAccessToken, getLocalAccessToken, setLocalAccessToken } from "../lib/localStorage"
dotenv.config()
@@ -40,51 +33,8 @@ const auth = async () => {
// Exchange token for credential.
const credentials = exchangeGithubTokenForCredentials(String(token))
try {
// Sign in with credentials to Firebase.
await signInToFirebaseWithCredentials(firebaseApp, credentials)
} catch (error: any) {
// Error handling by parsing error message.
if (error.toString().includes("Firebase: Unsuccessful check authorization response from Github")) {
showError(FIREBASE_ERRORS.FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS, false)
// Clean expired access token from local storage.
deleteLocalAccessToken()
// Inform user.
console.log(
`${symbols.info} We have successfully removed your local token to make you able to repeat the authorization process once again. Please, run the auth command again whenever you are ready and complete the association with the CLI application.`
)
// Gracefully exit.
process.exit(0)
}
if (error.toString().includes("Firebase: Error (auth/user-disabled)"))
showError(FIREBASE_ERRORS.FIREBASE_USER_DISABLED, true)
if (
error
.toString()
.includes("Firebase: Remote site 5XX from github.com for VERIFY_CREDENTIAL (auth/invalid-credential)")
)
showError(FIREBASE_ERRORS.FIREBASE_FAILED_CREDENTIALS_VERIFICATION, true)
if (error.toString().includes("Firebase: Error (auth/network-request-failed)"))
showError(FIREBASE_ERRORS.FIREBASE_NETWORK_ERROR, true)
if (error.toString().includes("HttpError: The authorization request was denied"))
showError(GITHUB_ERRORS.GITHUB_ACCOUNT_ASSOCIATION_REJECTED, true)
if (
error
.toString()
.includes(
"HttpError: request to https://github.com/login/device/code failed, reason: connect ETIMEDOUT"
)
)
showError(GITHUB_ERRORS.GITHUB_SERVER_TIMEDOUT, true)
}
// Sign-in to Firebase using credentials.
await signInToFirebase(firebaseApp, credentials)
// Get Github handle.
const githubUserHandle = await getGithubUserHandle(String(token))

View File

@@ -16,14 +16,15 @@ import {
getDocumentById,
checkAndPrepareCoordinatorForFinalization,
finalizeLastContribution,
finalizeCeremony
finalizeCeremony,
isCoordinator
} from "@zkmpc/actions"
import { collections, emojis, paths, solidityVersion, symbols, theme } from "../lib/constants"
import { GENERIC_ERRORS, showError } from "../lib/errors"
import { COMMAND_ERRORS, GENERIC_ERRORS, showError } from "../lib/errors"
import { askForCeremonySelection, getEntropyOrBeacon } from "../lib/prompts"
import { customSpinner, getLocalFilePath, makeContribution, publishGist, sleep, terminate } from "../lib/utils"
import { bootstrapCommandExecutionAndServices } from "../lib/commands"
import { checkAuth, onlyCoordinator } from "../lib/authorization"
import { checkAuth } from "../lib/authorization"
/**
* Finalize command.
@@ -40,8 +41,8 @@ const finalize = async () => {
// Handle current authenticated user sign in.
const { user, token, handle } = await checkAuth(firebaseApp)
// Check custom claims for coordinator role.
await onlyCoordinator(user)
// Preserve command execution only for coordinators].
if (!(await isCoordinator(user))) showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true)
// Get closed cerimonies info (if any).
const closedCeremoniesDocs = await getClosedCeremonies(firestoreDatabase)

View File

@@ -2,15 +2,20 @@
import readline from "readline"
import logSymbols from "log-symbols"
import { getOpenedCeremonies, getCeremonyCircuits, getCurrentContributorContribution } from "@zkmpc/actions"
import {
getOpenedCeremonies,
getCeremonyCircuits,
getCurrentContributorContribution,
isCoordinator
} from "@zkmpc/actions"
import { Firestore } from "firebase/firestore"
import { FirebaseDocumentInfo } from "../../types/index"
import { convertToDoubleDigits, customSpinner, getSecondsMinutesHoursFromMillis, sleep } from "../lib/utils"
import { askForCeremonySelection } from "../lib/prompts"
import { GENERIC_ERRORS, showError } from "../lib/errors"
import { COMMAND_ERRORS, GENERIC_ERRORS, showError } from "../lib/errors"
import { theme, emojis, symbols, observationWaitingTimeInMillis } from "../lib/constants"
import { bootstrapCommandExecutionAndServices } from "../lib/commands"
import { checkAuth, onlyCoordinator } from "../lib/authorization"
import { checkAuth } from "../lib/authorization"
/**
* Clean cursor lines from current position back to root (default: zero).
@@ -133,8 +138,8 @@ const observe = async () => {
// Handle current authenticated user sign in.
const { user } = await checkAuth(firebaseApp)
// Check custom claims for coordinator role.
await onlyCoordinator(user)
// Preserve command execution only for coordinators].
if (!(await isCoordinator(user))) showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true)
// Get running cerimonies info (if any).
const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase)

View File

@@ -20,7 +20,9 @@ import {
downloadFileFromUrl,
getDirFilesSubPaths,
getFileStats,
readFile
readFile,
isCoordinator,
filterDirectoryFilesByExtension
} from "@zkmpc/actions"
import {
theme,
@@ -34,7 +36,7 @@ import {
} from "../lib/constants"
import { convertToDoubleDigits, customSpinner, simpleLoader, sleep, terminate } from "../lib/utils"
import {
askCeremonyInputData,
promptCeremonyInputData,
askCircomCompilerVersionAndCommitHash,
askCircuitInputData,
askForCircuitSelectionFromLocalDir,
@@ -44,23 +46,9 @@ import {
askPowersOftau
} from "../lib/prompts"
import { CeremonyTimeoutType, Circuit, CircuitFiles, CircuitInputData, CircuitTimings } from "../../types/index"
import { GENERIC_ERRORS, showError } from "../lib/errors"
import { COMMAND_ERRORS, GENERIC_ERRORS, showError } from "../lib/errors"
import { bootstrapCommandExecutionAndServices } from "../lib/commands"
import { checkAuth, onlyCoordinator } from "../lib/authorization"
/**
* Return the files from the current working directory which have the extension specified as input.
* @param cwd <string> - the current working directory.
* @param ext <string> - the file extension.
* @returns <Promise<Array<Dirent>>>
*/
const getSpecifiedFilesFromCwd = async (cwd: string, ext: string): Promise<Array<Dirent>> => {
// Check if the current directory contains the .r1cs files.
const cwdFiles = await getDirFilesSubPaths(cwd)
const cwdExtFiles = cwdFiles.filter((file: Dirent) => file.name.includes(ext))
return cwdExtFiles
}
import { checkAuth } from "../lib/authorization"
/**
* Handle one or more circuit addition for the specified ceremony.
@@ -183,55 +171,59 @@ const checkIfPotAlreadyDownloaded = async (neededPowers: number): Promise<boolea
}
/**
* Setup a new Groth16 zkSNARK Phase 2 Trusted Setup ceremony.
* Setup command.
* @notice The setup command allows the coordinator of the ceremony to prepare the next ceremony by interacting with the CLI.
* @dev For proper execution, the command must be run in a folder containing the R1CS files related to the circuits
* for which the coordinator wants to create the ceremony. The command will download the necessary Tau powers
* from Hermez's ceremony Phase 1 Reliable Setup Ceremony.
*/
const setup = async () => {
const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices()
// Check for authentication.
const { user, handle } = await checkAuth(firebaseApp)
// Preserve command execution only for coordinators].
if (!(await isCoordinator(user))) showError(COMMAND_ERRORS.COMMAND_NOT_COORDINATOR, true)
// Get current working directory.
const cwd = process.cwd()
console.log(
`${symbols.warning} To setup a zkSNARK Groth16 Phase 2 Trusted Setup ceremony you need to have the Rank-1 Constraint System (R1CS) file for each circuit in your working directory`
)
console.log(`\n${symbols.info} Your current working directory is ${theme.bold(theme.underlined(process.cwd()))}\n`)
// Circuit data state.
let circuitsInputData: Array<CircuitInputData> = []
const circuits: Array<Circuit> = []
/** CORE */
// Look for R1CS files.
const r1csFilePaths = await filterDirectoryFilesByExtension(cwd, `.r1cs`)
if (!r1csFilePaths.length) showError(COMMAND_ERRORS.COMMAND_SETUP_NO_R1CS, true)
// Prepare local directories.
if (!directoryExists(paths.outputPath)) cleanDir(paths.outputPath)
cleanDir(paths.setupPath)
cleanDir(paths.potPath)
cleanDir(paths.metadataPath)
cleanDir(paths.zkeysPath)
// Prompt the coordinator for gather ceremony input data.
const ceremonyInputData = await promptCeremonyInputData(firestoreDatabase)
const ceremonyPrefix = extractPrefix(ceremonyInputData.title)
process.stdout.write(`\n`)
try {
// Get current working directory.
const cwd = process.cwd()
const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices()
// Handle current authenticated user sign in.
const { user, handle } = await checkAuth(firebaseApp)
// Check custom claims for coordinator role.
await onlyCoordinator(user)
console.log(
`${symbols.warning} To setup a zkSNARK Groth16 Phase 2 Trusted Setup ceremony you need to have the Rank-1 Constraint System (R1CS) file for each circuit in your working directory`
)
console.log(`${symbols.info} Current working directory: ${theme.bold(theme.underlined(cwd))}\n`)
// Check if the current directory contains the .r1cs files.
const cwdR1csFiles = await getSpecifiedFilesFromCwd(cwd, `.r1cs`)
if (!cwdR1csFiles.length) showError(`Your working directory must contain the R1CS files for each circuit`, true)
// Ask for ceremony input data.
const ceremonyInputData = await askCeremonyInputData(firestoreDatabase)
const ceremonyPrefix = extractPrefix(ceremonyInputData.title)
// Check for circom compiler version and commit hash.
// Ask for CIRCOM compiler version/commit-hash declaration for later verifiability.
const { confirmation: isCircomVersionEqualAmongCircuits } = await askForConfirmation(
"Was the same version of the circom compiler used for each circuit that will be designated for the ceremony?",
"Yes",
"No"
)
// Check for output directory.
if (!directoryExists(paths.outputPath)) cleanDir(paths.outputPath)
// Clean directories.
cleanDir(paths.setupPath)
cleanDir(paths.potPath)
cleanDir(paths.metadataPath)
cleanDir(paths.zkeysPath)
if (isCircomVersionEqualAmongCircuits) {
// Ask for circom compiler data.
const { version, commitHash } = await askCircomCompilerVersionAndCommitHash()
@@ -239,7 +231,7 @@ const setup = async () => {
// Ask to add circuits.
circuitsInputData = await handleCircuitsAddition(
cwd,
cwdR1csFiles,
r1csFilePaths,
ceremonyInputData.timeoutMechanismType,
isCircomVersionEqualAmongCircuits
)
@@ -252,7 +244,7 @@ const setup = async () => {
} else
circuitsInputData = await handleCircuitsAddition(
cwd,
cwdR1csFiles,
r1csFilePaths,
ceremonyInputData.timeoutMechanismType,
isCircomVersionEqualAmongCircuits
)
@@ -360,7 +352,7 @@ const setup = async () => {
spinner.text = "Checking for pre-computed zkeys..."
spinner.start()
const cwdZkeysFiles = await getSpecifiedFilesFromCwd(cwd, `.zkey`)
const cwdZkeysFiles = await filterDirectoryFilesByExtension(cwd, `.zkey`)
await sleep(1000)
@@ -426,7 +418,7 @@ const setup = async () => {
spinner.text = "Checking for Powers of Tau..."
spinner.start()
const cwdPtausFiles = await getSpecifiedFilesFromCwd(cwd, `.ptau`)
const cwdPtausFiles = await filterDirectoryFilesByExtension(cwd, `.ptau`)
await sleep(1000)
if (!cwdPtausFiles.length) {

View File

@@ -2,15 +2,70 @@ import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device"
import { Verification } from "@octokit/auth-oauth-device/dist-types/types"
import { getCurrentFirebaseAuthUser, signInToFirebaseWithCredentials } from "@zkmpc/actions"
import { FirebaseApp } from "firebase/app"
import { User, IdTokenResult } from "firebase/auth"
import { OAuthCredential } from "firebase/auth"
import open from "open"
import clipboard from "clipboardy"
import { AuthUser } from "../../types"
import { theme, emojis, symbols } from "./constants"
import { showError, GENERIC_ERRORS, GITHUB_ERRORS } from "./errors"
import { checkLocalAccessToken, getLocalAccessToken } from "./localStorage"
import { showError, GENERIC_ERRORS, GITHUB_ERRORS, FIREBASE_ERRORS } from "./errors"
import { checkLocalAccessToken, deleteLocalAccessToken, getLocalAccessToken } from "./localStorage"
import { exchangeGithubTokenForCredentials, getGithubUserHandle } from "./utils"
/**
* Execute the sign in to Firebase using OAuth credentials.
* @dev wrapper method to handle custom errors.
* @param firebaseApp <FirebaseApp> - the configured instance of the Firebase App in use.
* @param credentials <OAuthCredential> - the OAuth credential generated from token exchange.
* @returns <Promise<void>>
*/
export const signInToFirebase = async (firebaseApp: FirebaseApp, credentials: OAuthCredential): Promise<void> => {
try {
// Sign in with credentials to Firebase.
await signInToFirebaseWithCredentials(firebaseApp, credentials)
} catch (error: any) {
// Error handling by parsing error message.
if (error.toString().includes("Firebase: Unsuccessful check authorization response from Github")) {
showError(FIREBASE_ERRORS.FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS, false)
// Clean expired access token from local storage.
deleteLocalAccessToken()
// Inform user.
console.log(
`${symbols.info} We have successfully removed your local token to make you able to repeat the authorization process once again. Please, run the auth command again whenever you are ready and complete the association with the CLI application.`
)
// Gracefully exit.
process.exit(0)
}
if (error.toString().includes("Firebase: Error (auth/user-disabled)"))
showError(FIREBASE_ERRORS.FIREBASE_USER_DISABLED, true)
if (
error
.toString()
.includes("Firebase: Remote site 5XX from github.com for VERIFY_CREDENTIAL (auth/invalid-credential)")
)
showError(FIREBASE_ERRORS.FIREBASE_FAILED_CREDENTIALS_VERIFICATION, true)
if (error.toString().includes("Firebase: Error (auth/network-request-failed)"))
showError(FIREBASE_ERRORS.FIREBASE_NETWORK_ERROR, true)
if (error.toString().includes("HttpError: The authorization request was denied"))
showError(GITHUB_ERRORS.GITHUB_ACCOUNT_ASSOCIATION_REJECTED, true)
if (
error
.toString()
.includes(
"HttpError: request to https://github.com/login/device/code failed, reason: connect ETIMEDOUT"
)
)
showError(GITHUB_ERRORS.GITHUB_SERVER_TIMEDOUT, true)
}
}
/**
* Custom countdown which throws an error when expires.
* @param expirationInSeconds <number> - the expiration time in seconds.
@@ -41,18 +96,6 @@ const expirationCountdownForGithubOAuth = (expirationInSeconds: number) => {
}, interval * 1000) // ms.
}
/**
* Return the JWT token and helpers (claims) related to the current authenticated user.
* @param user <User> - the current authenticated user.
* @returns <Promise<IdTokenResult>>
*/
const getTokenAndClaims = async (user: User): Promise<IdTokenResult> => {
// Force refresh to update custom claims.
await user.getIdToken(true)
return user.getIdTokenResult()
}
/**
* Callback to manage the data requested for Github OAuth2.0 device flow.
* @param verification <Verification> - the data from Github OAuth2.0 device flow.
@@ -114,23 +157,15 @@ export const executeGithubDeviceFlow = async (clientId: string): Promise<string>
}
/**
* Throw an error if the user does not have a coordinator role.
* @param user <User> - the current authenticated user.
*/
export const onlyCoordinator = async (user: User) => {
const userTokenAndClaims = await getTokenAndClaims(user)
if (!userTokenAndClaims.claims.coordinator) showError(GENERIC_ERRORS.GENERIC_NOT_COORDINATOR, true)
}
/**
* Ensure that the callee user is authenticated.
* Ensure that the callee is an authenticated user.
* @dev This method MUST be executed before each command to avoid authentication errors when interacting with the command.
* @returns <Promise<AuthUser>> - a custom object containing info about the authenticated user, the token and github handle.
*/
export const checkAuth = async (firebaseApp: FirebaseApp): Promise<AuthUser> => {
// Check for local token.
if (!checkLocalAccessToken()) showError(GITHUB_ERRORS.GITHUB_NOT_AUTHENTICATED, true)
const isLocalTokenStored = checkLocalAccessToken()
if (!isLocalTokenStored) showError(GITHUB_ERRORS.GITHUB_NOT_AUTHENTICATED, true)
// Retrieve local access token.
const token = String(getLocalAccessToken())
@@ -139,7 +174,7 @@ export const checkAuth = async (firebaseApp: FirebaseApp): Promise<AuthUser> =>
const credentials = exchangeGithubTokenForCredentials(token)
// Sign in to Firebase using credentials.
await signInToFirebaseWithCredentials(firebaseApp, credentials)
await signInToFirebase(firebaseApp, credentials)
// Get current authenticated user.
const user = await getCurrentFirebaseAuthUser(firebaseApp)

View File

@@ -18,16 +18,22 @@ export const GITHUB_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_HANDLE_FAILED: `Something went wrong while retrieving your Github handle. Please, try once again later`,
GITHUB_NOT_AUTHENTICATED: `You are not authenticated. Please, authorize this device by running the auth command first.`,
GITHUB_NOT_AUTHENTICATED: `You are unable to execute the command since you have not authorized this device with your Github account. Please, execute the auth command (\`phase2cli auth\`) and then re-run this command.`,
GITHUB_GIST_PUBLICATION_FAILED: `Something went wrong while publishing a Gist from your Github account`
}
/** Command */
export 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 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 files.`
}
/** Generic */
export const GENERIC_ERRORS = {
GENERIC_NOT_CONFIGURED_PROPERLY: `Check that all CONFIG environment variables are configured properly`,
GENERIC_ERROR_RETRIEVING_DATA: `Something went wrong when retrieving the data from the database`,
GENERIC_FILE_ERROR: `File not found`,
GENERIC_NOT_COORDINATOR: `You are not a coordinator for the ceremony`,
GENERIC_COUNTDOWN_EXPIRED: `The amount of time for completing the operation has expired`,
GENERIC_R1CS_MISSING_INFO: `The necessary information was not found in the given R1CS file`,
GENERIC_COUNTDOWN_EXPIRATION: `Your time to carry out the action has expired`,

View File

@@ -10,14 +10,15 @@ import {
FirebaseDocumentInfo
} from "../../types/index"
import { symbols, theme } from "./constants"
import { GENERIC_ERRORS, showError } from "./errors"
import { customSpinner, getCreatedCeremoniesPrefixes } from "./utils"
import { COMMAND_ERRORS, GENERIC_ERRORS, showError } from "./errors"
import { customSpinner } from "./utils"
import { getAllCeremoniesDocuments } from "./queries"
/**
* Show a binary question with custom options for confirmation purposes.
* Ask a binary (yes/no or true/false) customizable question.
* @param question <string> - the question to be answered.
* @param active <string> - the active option (= yes).
* @param inactive <string> - the inactive option (= no).
* @param active <string> - the active option (default yes).
* @param inactive <string> - the inactive option (default no).
* @returns <Promise<Answers<string>>>
*/
export const askForConfirmation = async (question: string, active = "yes", inactive = "no"): Promise<Answers<string>> =>
@@ -30,6 +31,121 @@ export const askForConfirmation = async (question: string, active = "yes", inact
inactive
})
/**
* Prompt a series of questios to gather input data for the ceremony setup.
* @param firestore <Firestore> - the instance of the Firestore database.
* @returns <Promise<CeremonyInputData>> - the necessary information for the ceremony provided by the coordinator.
*/
export const promptCeremonyInputData = async (firestore: Firestore): Promise<CeremonyInputData> => {
// Get ceremonies prefixes already in use.
const ceremoniesDocs = await getAllCeremoniesDocuments(firestore)
const prefixesAlreadyInUse =
ceremoniesDocs.length > 0 ? ceremoniesDocs.map((ceremony: FirebaseDocumentInfo) => ceremony.data.prefix) : []
// Define questions.
const questions: Array<PromptObject> = [
{
type: "text",
name: "title",
message: theme.bold(`Ceremony name`),
validate: (title: string) => {
if (title.length <= 0)
return theme.red(`${symbols.error} Please, enter a non-empty string as the name of the ceremony`)
// Check if the current name matches one of the already used prefixes.
if (prefixesAlreadyInUse.includes(extractPrefix(title)))
return theme.red(`${symbols.error} The name is already in use for another ceremony`)
return true
}
},
{
type: "text",
name: "description",
message: theme.bold(`Short description`),
validate: (title: string) =>
title.length > 0 ||
theme.red(`${symbols.error} Please, enter a non-empty string as the name of the ceremony`)
},
{
type: "date",
name: "startDate",
message: theme.bold(`When should the ceremony open for contributions?`),
validate: (d: any) =>
new Date(d).valueOf() > Date.now()
? true
: theme.red(`${symbols.error} Please, enter a date subsequent to current date`)
}
]
// Prompt questions.
const { title, description, startDate } = await prompts(questions)
if (!title || !description || !startDate) showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true)
// Prompt for questions that depend on the answers to the previous ones.
const { endDate } = await prompts({
type: "date",
name: "endDate",
message: theme.bold(`When should the ceremony stop accepting contributions?`),
validate: (d) =>
new Date(d).valueOf() > new Date(startDate).valueOf()
? true
: theme.red(`${symbols.error} Please, enter a date subsequent to starting date`)
})
if (!endDate) showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true)
process.stdout.write("\n")
// Prompt for timeout mechanism type selection.
const { timeoutMechanismType } = await prompts({
type: "select",
name: "timeoutMechanismType",
message: theme.bold(
"Select the methodology for deciding to unblock the queue due to contributor disconnection, extreme slow computation, or malicious behavior"
),
choices: [
{
title: "Dynamic (self-update approach based on latest contribution time)",
value: CeremonyTimeoutType.DYNAMIC
},
{
title: "Fixed (approach based on a fixed amount of time)",
value: CeremonyTimeoutType.FIXED
}
],
initial: 0
})
if (timeoutMechanismType !== CeremonyTimeoutType.DYNAMIC && timeoutMechanismType !== CeremonyTimeoutType.FIXED)
showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true)
// Prompt for penalty.
const { penalty } = await prompts({
type: "number",
name: "penalty",
message: theme.bold(
`How long should a user have to attend before they can join the waiting queue again after a detected blocking situation? Please, express the value in minutes`
),
validate: (pnlt: number) => {
if (pnlt < 1) return theme.red(`${symbols.error} Please, enter a penalty at least one minute long`)
return true
}
})
if (!penalty) showError(COMMAND_ERRORS.COMMAND_ABORT_PROMPT, true)
return {
title,
description,
startDate,
endDate,
timeoutMechanismType,
penalty
}
}
/**
* Prompt for entropy or beacon.
* @param askEntropy <boolean> - true when requesting entropy; otherwise false.
@@ -83,96 +199,6 @@ export const getEntropyOrBeacon = async (askEntropy: boolean): Promise<string> =
return entropyOrBeacon
}
/**
* Show a series of questions about the ceremony.
* @param firestore <Firestore> - the instance of the Firestore database.
* @returns <Promise<CeremonyInputData>> - the necessary information for the ceremony entered by the coordinator.
*/
export const askCeremonyInputData = async (firestore: Firestore): Promise<CeremonyInputData> => {
// Get ceremonies prefixes to check for duplicates.
const ceremoniesPrefixes = await getCreatedCeremoniesPrefixes(firestore)
const noEndDateCeremonyQuestions: Array<PromptObject> = [
{
type: "text",
name: "title",
message: theme.bold(`Give a title to your ceremony`),
validate: (title: string) => {
if (title.length <= 0)
return theme.red(`${symbols.error} You must provide a valid title for your ceremony!`)
if (ceremoniesPrefixes.includes(extractPrefix(title)))
return theme.red(`${symbols.error} The title is already in use for another ceremony!`)
return true
}
},
{
type: "text",
name: "description",
message: theme.bold(`Add a description`),
validate: (title: string) =>
title.length > 0 || theme.red(`${symbols.error} You must provide a valid description!`)
},
{
type: "date",
name: "startDate",
message: theme.bold(`When should the ceremony open?`),
validate: (d: any) =>
new Date(d).valueOf() > Date.now()
? true
: theme.red(`${symbols.error} You cannot start a ceremony in the past!`)
}
]
const { title, description, startDate } = await prompts(noEndDateCeremonyQuestions)
if (!title || !description || !startDate) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true)
const { endDate } = await prompts({
type: "date",
name: "endDate",
message: theme.bold(`And when close?`),
validate: (d) =>
new Date(d).valueOf() > new Date(startDate).valueOf()
? true
: theme.red(`${symbols.error} You cannot close a ceremony before the opening!`)
})
if (!endDate) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true)
// Choose timeout mechanism.
const { confirmation: timeoutMechanismType } = await askForConfirmation(
`Choose which timeout mechanism you would like to use to penalize blocking contributors`,
`Dynamic`,
`Fixed`
)
const { penalty } = await prompts({
type: "number",
name: "penalty",
message: theme.bold(
`Specify the amount of time a blocking contributor needs to wait when timedout (in minutes):`
),
validate: (pnlt: number) => {
if (pnlt < 0) return theme.red(`${symbols.error} You must provide a penalty greater than zero`)
return true
}
})
if (penalty < 0) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true)
return {
title,
description,
startDate,
endDate,
timeoutMechanismType: timeoutMechanismType ? CeremonyTimeoutType.DYNAMIC : CeremonyTimeoutType.FIXED,
penalty
}
}
/**
* Show a series of questions about the circom compiler.
* @returns <Promise<CircomCompilerData>> - the necessary information for the circom compiler entered by the coordinator.

View File

@@ -4,11 +4,11 @@ import { collections, contributionsCollectionFields } from "./constants"
import { FirebaseDocumentInfo } from "../../types/index"
/**
* Retrieve all ceremonies.
* Retrieve all ceremonies documents from Firestore database.
* @param firestore <Firestore> - the instance of the Firestore database.
* @returns Promise<Array<FirebaseDocumentInfo>>
*/
export const getAllCeremonies = async (firestore: Firestore): Promise<Array<FirebaseDocumentInfo>> =>
export const getAllCeremoniesDocuments = async (firestore: Firestore): Promise<Array<FirebaseDocumentInfo>> =>
fromQueryToFirebaseDocumentInfo(await getAllCollectionDocs(firestore, collections.ceremonies)).sort(
(a: FirebaseDocumentInfo, b: FirebaseDocumentInfo) => a.data.sequencePosition - b.data.sequencePosition
)

View File

@@ -38,7 +38,6 @@ import {
import { collections, emojis, numIterationsExp, paths, symbols, theme } from "./constants"
import { GENERIC_ERRORS, GITHUB_ERRORS, showError } from "./errors"
import { downloadLocalFileFromBucket } from "./storage"
import { getAllCeremonies } from "./queries"
dotenv.config()
@@ -219,24 +218,6 @@ export const simpleLoader = async (
else loader.stop()
}
/**
* Return the ceremonies prefixes for every ceremony.
* @param firestore <Firestore> - the instance of the Firestore database.
* @returns Promise<Array<string>>
*/
export const getCreatedCeremoniesPrefixes = async (firestore: Firestore): Promise<Array<string>> => {
// Get all ceremonies documents.
const ceremonies = await getAllCeremonies(firestore)
let ceremoniesPrefixes = []
// Return prefixes (if any ceremony).
if (ceremonies.length > 0)
ceremoniesPrefixes = ceremonies.map((ceremony: FirebaseDocumentInfo) => ceremony.data.prefix)
return ceremoniesPrefixes
}
/**
* Convert milliseconds to seconds.
* @param millis <number>