@chainwayxyz/phase2cli
Version:
All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies
914 lines (784 loc) • 39.9 kB
text/typescript
import { zKey } from "snarkjs"
import boxen from "boxen"
import { createWriteStream, Dirent, renameSync, existsSync } from "fs"
import { pipeline } from "node:stream"
import { promisify } from "node:util"
import fetch from "node-fetch"
import { Functions } from "firebase/functions"
import {
CeremonyTimeoutType,
CircomCompilerData,
CircuitInputData,
extractPrefix,
getR1CSInfo,
commonTerms,
convertToDoubleDigits,
CeremonyInputData,
CircuitDocument,
extractPoTFromFilename,
potFileDownloadMainUrl,
potFilenameTemplate,
getBucketName,
createS3Bucket,
multiPartUpload,
isCoordinator,
genesisZkeyIndex,
getR1csStorageFilePath,
getWasmStorageFilePath,
getPotStorageFilePath,
getZkeyStorageFilePath,
checkIfObjectExist,
blake512FromPath,
CircuitArtifacts,
CircuitTimings,
setupCeremony,
parseCeremonyFile,
CircuitContributionVerificationMechanism
} from "@p0tion/actions"
import { customSpinner, simpleLoader, sleep, terminate } from "../lib/utils.js"
import {
promptCeremonyInputData,
promptCircomCompiler,
promptCircuitInputData,
askForConfirmation,
promptCircuitSelector,
promptSameCircomCompiler,
promptCircuitAddition,
promptPreComputedZkey,
promptPreComputedZkeySelector,
promptNeededPowersForCircuit,
promptPotSelector
} from "../lib/prompts.js"
import { COMMAND_ERRORS, showError } from "../lib/errors.js"
import { authWithToken, bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
import { getCWDFilePath, getPotLocalFilePath, getZkeyLocalFilePath, localPaths } from "../lib/localConfigs.js"
import theme from "../lib/theme.js"
import {
filterDirectoryFilesByExtension,
cleanDir,
getDirFilesSubPaths,
getFileStats,
checkAndMakeNewDirectoryIfNonexistent
} from "../lib/files.js"
/**
* Handle whatever is needed to obtain the input data for a circuit that the coordinator would like to add to the ceremony.
* @param choosenCircuitFilename <string> - the name of the circuit to add.
* @param matchingWasmFilename <string> - the name of the circuit wasm file.
* @param ceremonyTimeoutMechanismType <CeremonyTimeoutType> - the type of ceremony timeout mechanism.
* @param sameCircomCompiler <boolean> - true, if this circuit shares with the others the <CircomCompilerData>; otherwise false.
* @param circuitSequencePosition <number> - the position of the circuit in the contribution queue.
* @param sharedCircomCompilerData <string> - version and commit hash of the Circom compiler used to compile the ceremony circuits.
* @returns <Promise<CircuitInputData>> - the input data of the circuit to add to the ceremony.
*/
export const getInputDataToAddCircuitToCeremony = async (
choosenCircuitFilename: string,
matchingWasmFilename: string,
ceremonyTimeoutMechanismType: CeremonyTimeoutType,
sameCircomCompiler: boolean,
circuitSequencePosition: number,
sharedCircomCompilerData: CircomCompilerData
): Promise<CircuitInputData> => {
// Extract name and prefix.
const circuitName = choosenCircuitFilename.substring(0, choosenCircuitFilename.indexOf("."))
const circuitPrefix = extractPrefix(circuitName)
// R1CS file path.
const r1csCWDFilePath = getCWDFilePath(process.cwd(), choosenCircuitFilename)
const spinner = customSpinner(`Looking for circuit metadata...`, "clock")
spinner.start()
// Read R1CS and store metadata locally.
const metadata = getR1CSInfo(r1csCWDFilePath)
await sleep(2000) // Sleep 2s to avoid unexpected termination (file descriptor close).
spinner.succeed(`Circuit metadata read and saved correctly`)
// Prompt for circuit input data.
const circuitInputData = await promptCircuitInputData(
metadata.constraints,
ceremonyTimeoutMechanismType,
sameCircomCompiler,
!(metadata.constraints <= 1000000) // nb. we assume after our dry-runs that CF works fine for up to one million circuit constraints.
)
process.stdout.write("\n")
// Return updated data.
return {
...circuitInputData,
metadata,
compiler: {
commitHash:
!circuitInputData.compiler.commitHash && sameCircomCompiler
? sharedCircomCompilerData.commitHash
: circuitInputData.compiler.commitHash,
version:
!circuitInputData.compiler.version && sameCircomCompiler
? sharedCircomCompilerData.version
: circuitInputData.compiler.version
},
compilationArtifacts: {
r1csFilename: choosenCircuitFilename,
wasmFilename: matchingWasmFilename
},
name: circuitName,
prefix: circuitPrefix,
sequencePosition: circuitSequencePosition
}
}
/**
* Handle the addition of one or more circuits to the ceremony.
* @param options <Array<string>> - list of possible circuits that can be added to the ceremony.
* @param ceremonyTimeoutMechanismType <CeremonyTimeoutType> - the type of ceremony timeout mechanism.
* @returns <Promise<Array<CircuitInputData>>> - the input data for each circuit that has been added to the ceremony.
*/
export const handleAdditionOfCircuitsToCeremony = async (
r1csOptions: Array<string>,
wasmOptions: Array<string>,
ceremonyTimeoutMechanismType: CeremonyTimeoutType
): Promise<Array<CircuitInputData>> => {
// Prepare data.
const inputDataForCircuits: Array<CircuitInputData> = [] // All circuits interactive data.
let circuitSequencePosition = 1 // The circuit's position for contribution.
let readyToSummarizeCeremony = false // Boolean flag to check whether the coordinator has finished to add circuits to the ceremony.
let wannaAddAnotherCircuit = true // Loop flag.
const sharedCircomCompilerData: CircomCompilerData = { version: "", commitHash: "" }
// Prompt if the circuits to be added were compiled with the same version of Circom.
// nb. CIRCOM compiler version/commit-hash is a declaration useful for later verifiability and avoid bugs.
const sameCircomCompiler = await promptSameCircomCompiler()
if (sameCircomCompiler) {
// Prompt for Circom compiler.
const { version, commitHash } = await promptCircomCompiler()
sharedCircomCompilerData.version = version
sharedCircomCompilerData.commitHash = commitHash
}
while (wannaAddAnotherCircuit) {
// Gather information about the ceremony circuits.
console.log(theme.text.bold(`\n- Circuit # ${theme.colors.magenta(`${circuitSequencePosition}`)}\n`))
// Select one circuit among cwd circuits identified by R1CS files.
const choosenCircuitFilename = await promptCircuitSelector(r1csOptions)
// Update list of possible options for next selection (if, any).
r1csOptions = r1csOptions.filter((circuitFilename: string) => circuitFilename !== choosenCircuitFilename)
// Select the wasm file accordingly to circuit R1CS filename.
const matchingWasms = wasmOptions.filter(
(wasmFilename: string) =>
choosenCircuitFilename.split(`.r1cs`)[0] ===
wasmFilename.split(`.${commonTerms.foldersAndPathsTerms.wasm}`)[0]
)
if (matchingWasms.length !== 1) showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true)
// Get input data for chosen circuit.
const circuitInputData = await getInputDataToAddCircuitToCeremony(
choosenCircuitFilename,
matchingWasms[0],
ceremonyTimeoutMechanismType,
sameCircomCompiler,
circuitSequencePosition,
sharedCircomCompilerData
)
// Store circuit data.
inputDataForCircuits.push(circuitInputData)
// Check if any circuit is left for potentially addition to ceremony.
if (r1csOptions.length !== 0) {
// Prompt for selection.
const wannaAddNewCircuit = await promptCircuitAddition()
if (wannaAddNewCircuit === false) readyToSummarizeCeremony = true // Terminate circuit addition.
else circuitSequencePosition += 1 // Continue with next one.
} else readyToSummarizeCeremony = true // No more circuit to add.
// Summarize the ceremony.
if (readyToSummarizeCeremony) wannaAddAnotherCircuit = false
}
return inputDataForCircuits
}
/**
* Print ceremony and related circuits information.
* @param ceremonyInputData <CeremonyInputData> - the input data of the ceremony.
* @param circuits <Array<CircuitDocument>> - the circuit documents associated to the circuits of the ceremony.
*/
export const displayCeremonySummary = (ceremonyInputData: CeremonyInputData, circuits: Array<CircuitDocument>) => {
// Prepare ceremony summary.
let summary = `${`${theme.text.bold(ceremonyInputData.title)}\n${theme.text.italic(ceremonyInputData.description)}`}
\n${`Opening: ${theme.text.bold(
theme.text.underlined(new Date(ceremonyInputData.startDate).toUTCString().replace("GMT", "UTC"))
)}\nEnding: ${theme.text.bold(
theme.text.underlined(new Date(ceremonyInputData.endDate).toUTCString().replace("GMT", "UTC"))
)}`}
\n${theme.text.bold(
ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC ? `Dynamic` : `Fixed`
)} Timeout / ${theme.text.bold(ceremonyInputData.penalty)}m Penalty`
for (const circuit of circuits) {
// Append circuit summary.
summary += `\n\n${theme.text.bold(
`- CIRCUIT # ${theme.text.bold(theme.colors.magenta(`${circuit.sequencePosition}`))}`
)}
\n${`${theme.text.bold(circuit.name)}\n${theme.text.italic(circuit.description)}
\nCurve: ${theme.text.bold(circuit.metadata?.curve)}\nCompiler: ${theme.text.bold(
`${circuit.compiler.version}`
)} (${theme.text.bold(circuit.compiler.commitHash.slice(0, 7))})\nVerification: ${theme.text.bold(
`${circuit.verification.cfOrVm}`
)} ${theme.text.bold(
circuit.verification.cfOrVm === CircuitContributionVerificationMechanism.VM
? `(${circuit.verification.vm.vmConfigurationType} / ${circuit.verification.vm.vmDiskType} volume)`
: ""
)}\nSource: ${theme.text.bold(circuit.template.source.split(`/`).at(-1))}(${theme.text.bold(
circuit.template.paramsConfiguration
)})\n${
ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC
? `Threshold: ${theme.text.bold(circuit.dynamicThreshold)}%`
: `Max Contribution Time: ${theme.text.bold(circuit.fixedTimeWindow)}m`
}
\n# Wires: ${theme.text.bold(circuit.metadata?.wires)}\n# Constraints: ${theme.text.bold(
circuit.metadata?.constraints
)}\n# Private Inputs: ${theme.text.bold(circuit.metadata?.privateInputs)}\n# Public Inputs: ${theme.text.bold(
circuit.metadata?.publicInputs
)}\n# Labels: ${theme.text.bold(circuit.metadata?.labels)}\n# Outputs: ${theme.text.bold(
circuit.metadata?.outputs
)}\n# PoT: ${theme.text.bold(circuit.metadata?.pot)}`}`
}
// Display complete summary.
console.log(
boxen(summary, {
title: theme.colors.magenta(`CEREMONY SUMMARY`),
titleAlignment: "center",
textAlignment: "left",
margin: 1,
padding: 1
})
)
}
/**
* Check if the smallest Powers of Tau has already been downloaded/stored in the correspondent local path
* @dev we are downloading the Powers of Tau file from Perpetual Powers of Tau Phase 1 Trusted Setup.
* @param powers <string> - the smallest amount of powers needed for the given circuit (should be in a 'XY' stringified form).
* @param ptauCompleteFilename <string> - the complete file name of the powers of tau file to be downloaded.
* @returns <Promise<void>>
*/
export const checkAndDownloadSmallestPowersOfTau = async (
powers: string,
ptauCompleteFilename: string
): Promise<void> => {
// Get already downloaded ptau files.
const alreadyDownloadedPtauFiles = await getDirFilesSubPaths(localPaths.pot)
// Get the required smallest ptau file.
const smallestPtauFileForGivenPowers: Array<string> = alreadyDownloadedPtauFiles
.filter((dirent: Dirent) => extractPoTFromFilename(dirent.name) === Number(powers))
.map((dirent: Dirent) => dirent.name)
// Check if already downloaded or not.
if (smallestPtauFileForGivenPowers.length === 0) {
const spinner = customSpinner(
`Downloading the ${theme.text.bold(
`#${powers}`
)} smallest PoT file needed from the Perpetual Powers of Tau Phase 1 Trusted Setup...`,
`clock`
)
spinner.start()
// Download smallest Powers of Tau file from remote server.
const streamPipeline = promisify(pipeline)
// Make the call.
const response = await fetch(`${potFileDownloadMainUrl}${ptauCompleteFilename}`)
// Handle errors.
if (!response.ok && response.status !== 200) showError(COMMAND_ERRORS.COMMAND_SETUP_DOWNLOAD_PTAU, true)
// Write the file locally
else await streamPipeline(response.body!, createWriteStream(getPotLocalFilePath(ptauCompleteFilename)))
spinner.succeed(`Powers of tau ${theme.text.bold(`#${powers}`)} downloaded successfully`)
} else
console.log(
`${theme.symbols.success} Smallest Powers of Tau ${theme.text.bold(`#${powers}`)} already downloaded`
)
}
/**
* Handle the needs in terms of Powers of Tau for the selected pre-computed zKey.
* @notice in case there are no Powers of Tau file suitable for the pre-computed zKey (i.e., having a
* number of powers greater than or equal to the powers needed by the zKey), the coordinator will be asked
* to provide a number of powers manually, ranging from the smallest possible to the largest.
* @param neededPowers <number> - the smallest amount of powers needed by the zKey.
* @returns Promise<string, string> - the information about the chosen Powers of Tau file for the pre-computed zKey
* along with related powers.
*/
export const handlePreComputedZkeyPowersOfTauSelection = async (
neededPowers: number
): Promise<{
doubleDigitsPowers: string
potCompleteFilename: string
usePreDownloadedPoT: boolean
}> => {
let doubleDigitsPowers: string = "" // The amount of stringified powers in a double-digits format (XY).
let potCompleteFilename: string = "" // The complete filename of the Powers of Tau file selected for the pre-computed zKey.
let usePreDownloadedPoT = false // Boolean flag to check if the coordinator is going to use a pre-downloaded PoT file or not.
// Check for PoT file associated to selected pre-computed zKey.
const spinner = customSpinner("Looking for Powers of Tau files...", "clock")
spinner.start()
// Get local `.ptau` files.
const potFilePaths = await filterDirectoryFilesByExtension(process.cwd(), `.ptau`)
// Filter based on suitable amount of powers.
const potOptions: Array<string> = potFilePaths
.filter((dirent: Dirent) => extractPoTFromFilename(dirent.name) >= neededPowers)
.map((dirent: Dirent) => dirent.name)
if (potOptions.length <= 0) {
spinner.warn(`There is no already downloaded Powers of Tau file suitable for this zKey`)
// Ask coordinator to input the amount of powers.
const choosenPowers = await promptNeededPowersForCircuit(neededPowers)
// Convert to double digits powers (e.g., 9 -> 09).
doubleDigitsPowers = convertToDoubleDigits(choosenPowers)
potCompleteFilename = `${potFilenameTemplate}${doubleDigitsPowers}.ptau`
} else {
spinner.stop()
// Prompt for Powers of Tau selection among already downloaded ones.
potCompleteFilename = await promptPotSelector(potOptions)
// Convert to double digits powers (e.g., 9 -> 09).
doubleDigitsPowers = convertToDoubleDigits(extractPoTFromFilename(potCompleteFilename))
usePreDownloadedPoT = true
}
return {
doubleDigitsPowers,
potCompleteFilename,
usePreDownloadedPoT
}
}
/**
* Generate a brand new zKey from scratch.
* @param r1csLocalPathAndFileName <string> - the local complete path of the R1CS selected file.
* @param potLocalPathAndFileName <string> - the local complete path of the PoT selected file.
* @param zkeyLocalPathAndFileName <string> - the local complete path of the pre-computed zKey selected file.
*/
export const handleNewZkeyGeneration = async (
r1csLocalPathAndFileName: string,
potLocalPathAndFileName: string,
zkeyLocalPathAndFileName: string
) => {
console.log(
`${theme.symbols.info} The computation of your brand new zKey is starting soon.\n${theme.text.bold(
`${theme.symbols.warning} Be careful, stopping the process will result in the loss of all progress achieved so far.`
)}`
)
// Generate zKey.
await zKey.newZKey(r1csLocalPathAndFileName, potLocalPathAndFileName, zkeyLocalPathAndFileName, console)
console.log(`\n${theme.symbols.success} Generation of genesis zKey completed successfully`)
}
/**
* Manage the creation of a ceremony file storage bucket.
* @param firebaseFunctions <Functions> - the Firebase Cloud Functions instance connected to the current application.
* @param ceremonyPrefix <string> - the prefix of the ceremony.
* @returns <Promise<string>> - the ceremony bucket name.
*/
export const handleCeremonyBucketCreation = async (
firebaseFunctions: Functions,
ceremonyPrefix: string
): Promise<string> => {
// Compose bucket name using the ceremony prefix.
const bucketName = getBucketName(ceremonyPrefix, process.env.CONFIG_CEREMONY_BUCKET_POSTFIX!)
const spinner = customSpinner(`Getting ready for ceremony files and data storage...`, `clock`)
spinner.start()
try {
// Make the call to create the bucket.
spinner.info(`Creating bucket ${bucketName}`)
await createS3Bucket(firebaseFunctions, bucketName)
} catch (error: any) {
const errorBody = JSON.parse(JSON.stringify(error))
showError(`[${errorBody.code}] ${error.message} ${!errorBody.details ? "" : `\n${errorBody.details}`}`, true)
}
spinner.succeed(`Ceremony bucket has been successfully created`)
return bucketName
}
/**
* Upload a circuit artifact (r1cs, WASM, ptau) to the ceremony storage.
* @dev this method uses a multi part upload to upload the file in chunks.
* @param firebaseFunctions <Functions> - the Firebase Cloud Functions instance connected to the current application.
* @param bucketName <string> - the ceremony bucket name.
* @param storageFilePath <string> - the storage (bucket) path where the file should be uploaded.
* @param localPathAndFileName <string> - the local file path where is located.
* @param completeFilename <string> - the complete filename.
*/
export const handleCircuitArtifactUploadToStorage = async (
firebaseFunctions: Functions,
bucketName: string,
storageFilePath: string,
localPathAndFileName: string,
completeFilename: string
) => {
const spinner = customSpinner(`Uploading ${theme.text.bold(completeFilename)} file to ceremony storage...`, `clock`)
spinner.start()
await multiPartUpload(
firebaseFunctions,
bucketName,
storageFilePath,
localPathAndFileName,
Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB)
)
spinner.succeed(`Upload of (${theme.text.bold(completeFilename)}) file completed successfully`)
}
/**
* 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 PPoT ceremony Phase 1 Setup Ceremony.
* @param cmd? <any> - the path to the ceremony setup file.
*/
const setup = async (cmd: { template?: string; auth?: string }) => {
// Setup command state.
const circuits: Array<CircuitDocument> = [] // Circuits.
let ceremonyId: string = "" // The unique identifier of the ceremony.
const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExecutionAndServices()
// Check for authentication.
const { user, providerUserId } = cmd.auth
? await authWithToken(firebaseApp, cmd.auth)
: 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(
`${theme.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${theme.symbols.info} Your current working directory is ${theme.text.bold(
theme.text.underlined(process.cwd())
)}\n`
)
// Prepare local directories.
checkAndMakeNewDirectoryIfNonexistent(localPaths.output)
cleanDir(localPaths.setup)
cleanDir(localPaths.pot)
cleanDir(localPaths.zkeys)
cleanDir(localPaths.wasm)
// if there is the file option, then set up the non interactively
if (cmd.template) {
// 1. parse the file
// tmp data - do not cleanup files as we need them
const spinner = customSpinner(`Parsing ${theme.text.bold(cmd.template!)} setup configuration file...`, `clock`)
spinner.start()
const setupCeremonyData = await parseCeremonyFile(cmd.template!)
spinner.succeed(`Parsing of ${theme.text.bold(cmd.template!)} setup configuration file completed successfully`)
// final setup data
const ceremonySetupData = setupCeremonyData
// create a new bucket
const bucketName = await handleCeremonyBucketCreation(firebaseFunctions, ceremonySetupData.ceremonyPrefix)
console.log(`\n${theme.symbols.success} Ceremony bucket name: ${theme.text.bold(bucketName)}`)
// loop through each circuit
for await (const circuit of setupCeremonyData.circuits) {
// Local paths.
const index = ceremonySetupData.circuits.indexOf(circuit)
const r1csLocalPathAndFileName = `./${circuit.name}.r1cs`
const wasmLocalPathAndFileName = `./${circuit.name}.wasm`
const potLocalPathAndFileName = getPotLocalFilePath(circuit.files.potFilename)
const zkeyLocalPathAndFileName = getZkeyLocalFilePath(circuit.files.initialZkeyFilename)
// 2. download the pot and wasm files
await checkAndDownloadSmallestPowersOfTau(
convertToDoubleDigits(circuit.metadata?.pot!),
circuit.files.potFilename
)
// 3. generate the zKey
const spinner = customSpinner(
`Generating genesis zKey for circuit ${theme.text.bold(circuit.name)}...`,
`clock`
)
spinner.start()
if (existsSync(zkeyLocalPathAndFileName)) {
spinner.succeed(
`The genesis zKey for circuit ${theme.text.bold(circuit.name)} is already present on disk`
)
} else {
await zKey.newZKey(
r1csLocalPathAndFileName,
getPotLocalFilePath(circuit.files.potFilename),
zkeyLocalPathAndFileName,
undefined
)
spinner.succeed(
`Generation of the genesis zKey for circuit ${theme.text.bold(circuit.name)} completed successfully`
)
}
const hashSpinner = customSpinner(
`Calculating hashes for circuit ${theme.text.bold(circuit.name)}...`,
`clock`
)
hashSpinner.start()
// 4. calculate the hashes
const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName)
const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename))
const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName)
hashSpinner.succeed(`Hashes for circuit ${theme.text.bold(circuit.name)} calculated successfully`)
// 5. upload the artifacts
// Upload zKey to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
circuit.files.initialZkeyStoragePath,
zkeyLocalPathAndFileName,
circuit.files.initialZkeyFilename
)
// Check if PoT file has been already uploaded to storage.
const alreadyUploadedPot = await checkIfObjectExist(
firebaseFunctions,
bucketName,
circuit.files.potStoragePath
)
// If it wasn't uploaded yet, upload it.
if (!alreadyUploadedPot) {
// Upload PoT to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
circuit.files.potStoragePath,
potLocalPathAndFileName,
circuit.files.potFilename
)
}
// Upload r1cs to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
circuit.files.r1csStoragePath,
r1csLocalPathAndFileName,
circuit.files.r1csFilename
)
// Upload wasm to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
circuit.files.wasmStoragePath,
r1csLocalPathAndFileName,
circuit.files.wasmFilename
)
// 6 update the setup data object
ceremonySetupData.circuits[index].files = {
...circuit.files,
potBlake2bHash,
wasmBlake2bHash,
initialZkeyBlake2bHash
}
ceremonySetupData.circuits[index].zKeySizeInBytes = getFileStats(zkeyLocalPathAndFileName).size
}
// 7. setup the ceremony
const ceremonyId = await setupCeremony(
firebaseFunctions,
ceremonySetupData.ceremonyInputData,
ceremonySetupData.ceremonyPrefix,
ceremonySetupData.circuits
)
console.log(
`Congratulations, the setup of ceremony ${theme.text.bold(
ceremonySetupData.ceremonyInputData.title
)} (${`UID: ${theme.text.bold(ceremonyId)}`}) has been successfully completed ${
theme.emojis.tada
}. You will be able to find all the files and info respectively in the ceremony bucket and database document.`
)
terminate(providerUserId)
}
// Look for R1CS files.
const r1csFilePaths = await filterDirectoryFilesByExtension(cwd, `.r1cs`)
// Look for WASM files.
const wasmFilePaths = await filterDirectoryFilesByExtension(cwd, `.wasm`)
// Look for pre-computed zKeys references (if any).
const localPreComputedZkeysFilenames = await filterDirectoryFilesByExtension(cwd, `.zkey`)
if (!r1csFilePaths.length) showError(COMMAND_ERRORS.COMMAND_SETUP_NO_R1CS, true)
if (!wasmFilePaths.length) showError(COMMAND_ERRORS.COMMAND_SETUP_NO_WASM, true)
if (wasmFilePaths.length !== r1csFilePaths.length) showError(COMMAND_ERRORS.COMMAND_SETUP_MISMATCH_R1CS_WASM, true)
// Prompt the coordinator for gather ceremony input data.
const ceremonyInputData = await promptCeremonyInputData(firestoreDatabase)
const ceremonyPrefix = extractPrefix(ceremonyInputData.title)
// Add circuits to ceremony.
const circuitsInputData: Array<CircuitInputData> = await handleAdditionOfCircuitsToCeremony(
r1csFilePaths.map((dirent: Dirent) => dirent.name),
wasmFilePaths.map((dirent: Dirent) => dirent.name),
ceremonyInputData.timeoutMechanismType
)
// Move input data to circuits.
circuitsInputData.forEach((data: CircuitInputData) => circuits.push(data))
// Display ceremony summary.
displayCeremonySummary(ceremonyInputData, circuits)
// Prepare data.
let wannaGenerateNewZkey = true // New zKey generation flag.
let wannaUsePreDownloadedPoT = false // Local PoT file usage flag.
let bucketName: string = "" // The name of the bucket.
// Ask for confirmation.
const { confirmation } = await askForConfirmation("Do you want to continue with the ceremony setup?", "Yes", "No")
if (confirmation) {
await simpleLoader(`Looking for any pre-computed zkey file...`, `clock`, 1000)
// Simulate pre-computed zkeys search.
let leftPreComputedZkeys = localPreComputedZkeysFilenames
/** Circuit-based setup */
for (let i = 0; i < circuits.length; i += 1) {
const circuit = circuits[i]
console.log(
theme.text.bold(`\n- Setup for Circuit # ${theme.colors.magenta(`${circuit.sequencePosition}`)}\n`)
)
// Convert to double digits powers (e.g., 9 -> 09).
let doubleDigitsPowers = convertToDoubleDigits(circuit.metadata?.pot!)
let smallestPowersOfTauCompleteFilenameForCircuit = `${potFilenameTemplate}${doubleDigitsPowers}.ptau`
// Rename R1Cs and zKey based on circuit name and prefix.
const r1csCompleteFilename = `${circuit.name}.r1cs`
const wasmCompleteFilename = `${circuit.name}.wasm`
const firstZkeyCompleteFilename = `${circuit.prefix}_${genesisZkeyIndex}.zkey`
let preComputedZkeyCompleteFilename = ``
// Local paths.
const r1csLocalPathAndFileName = getCWDFilePath(cwd, r1csCompleteFilename)
const wasmLocalPathAndFileName = getCWDFilePath(cwd, wasmCompleteFilename)
let potLocalPathAndFileName = getPotLocalFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
let zkeyLocalPathAndFileName = getZkeyLocalFilePath(firstZkeyCompleteFilename)
// Storage paths.
const r1csStorageFilePath = getR1csStorageFilePath(circuit.prefix!, r1csCompleteFilename)
const wasmStorageFilePath = getWasmStorageFilePath(circuit.prefix!, wasmCompleteFilename)
let potStorageFilePath = getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
const zkeyStorageFilePath = getZkeyStorageFilePath(circuit.prefix!, firstZkeyCompleteFilename)
if (leftPreComputedZkeys.length <= 0)
console.log(
`${theme.symbols.warning} No pre-computed zKey was found. Therefore, a new zKey from scratch will be generated.`
)
else {
// Prompt if coordinator wanna use a pre-computed zKey for the circuit.
const wannaUsePreComputedZkey = await promptPreComputedZkey()
if (wannaUsePreComputedZkey) {
// Prompt for pre-computed zKey selection.
const preComputedZkeyOptions = leftPreComputedZkeys.map((dirent: Dirent) => dirent.name)
preComputedZkeyCompleteFilename = await promptPreComputedZkeySelector(preComputedZkeyOptions)
// Switch to pre-computed zkey path.
zkeyLocalPathAndFileName = getCWDFilePath(cwd, preComputedZkeyCompleteFilename)
// Handle the selection for the PoT file to associate w/ the selected pre-computed zKey.
const {
doubleDigitsPowers: selectedDoubleDigitsPowers,
potCompleteFilename: selectedPotCompleteFilename,
usePreDownloadedPoT
} = await handlePreComputedZkeyPowersOfTauSelection(circuit.metadata?.pot!)
// Update state.
doubleDigitsPowers = selectedDoubleDigitsPowers
smallestPowersOfTauCompleteFilenameForCircuit = selectedPotCompleteFilename
wannaUsePreDownloadedPoT = usePreDownloadedPoT
// Update paths.
potLocalPathAndFileName = getPotLocalFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
potStorageFilePath = getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
// Check (and download) the smallest Powers of Tau for circuit.
if (!wannaUsePreDownloadedPoT)
await checkAndDownloadSmallestPowersOfTau(
doubleDigitsPowers,
smallestPowersOfTauCompleteFilenameForCircuit
)
// Update flag for zKey generation accordingly.
wannaGenerateNewZkey = false
// Update paths.
renameSync(getCWDFilePath(cwd, preComputedZkeyCompleteFilename), firstZkeyCompleteFilename) // the pre-computed zKey become the new first (genesis) zKey.
zkeyLocalPathAndFileName = getCWDFilePath(cwd, firstZkeyCompleteFilename)
// Remove the pre-computed zKey from the list of possible pre-computed options.
leftPreComputedZkeys = leftPreComputedZkeys.filter(
(dirent: Dirent) => dirent.name !== preComputedZkeyCompleteFilename
)
}
}
// Check (and download) the smallest Powers of Tau for circuit.
if (!wannaUsePreDownloadedPoT)
await checkAndDownloadSmallestPowersOfTau(
doubleDigitsPowers,
smallestPowersOfTauCompleteFilenameForCircuit
)
if (wannaGenerateNewZkey)
await handleNewZkeyGeneration(
r1csLocalPathAndFileName,
potLocalPathAndFileName,
zkeyLocalPathAndFileName
)
// Create a bucket for ceremony if it has not yet been created.
if (!bucketName) bucketName = await handleCeremonyBucketCreation(firebaseFunctions, ceremonyPrefix)
// Upload zKey to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
zkeyStorageFilePath,
zkeyLocalPathAndFileName,
firstZkeyCompleteFilename
)
// Check if PoT file has been already uploaded to storage.
const alreadyUploadedPot = await checkIfObjectExist(
firebaseFunctions,
bucketName,
getPotStorageFilePath(smallestPowersOfTauCompleteFilenameForCircuit)
)
if (!alreadyUploadedPot) {
// Upload PoT to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
potStorageFilePath,
potLocalPathAndFileName,
smallestPowersOfTauCompleteFilenameForCircuit
)
} else
console.log(
`${theme.symbols.success} The Powers of Tau (${theme.text.bold(
smallestPowersOfTauCompleteFilenameForCircuit
)}) file is already saved in the storage`
)
// Upload R1CS to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
r1csStorageFilePath,
r1csLocalPathAndFileName,
r1csCompleteFilename
)
// Upload WASM to Storage.
await handleCircuitArtifactUploadToStorage(
firebaseFunctions,
bucketName,
wasmStorageFilePath,
wasmLocalPathAndFileName,
wasmCompleteFilename
)
process.stdout.write(`\n`)
const spinner = customSpinner(`Preparing the ceremony data (this may take a while)...`, `clock`)
spinner.start()
// Computing file hash (this may take a while).
const r1csBlake2bHash = await blake512FromPath(r1csLocalPathAndFileName)
const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName)
const potBlake2bHash = await blake512FromPath(potLocalPathAndFileName)
const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName)
spinner.stop()
// Prepare circuit data for writing to the DB.
const circuitFiles: CircuitArtifacts = {
r1csFilename: r1csCompleteFilename,
wasmFilename: wasmCompleteFilename,
potFilename: smallestPowersOfTauCompleteFilenameForCircuit,
initialZkeyFilename: firstZkeyCompleteFilename,
r1csStoragePath: r1csStorageFilePath,
wasmStoragePath: wasmStorageFilePath,
potStoragePath: potStorageFilePath,
initialZkeyStoragePath: zkeyStorageFilePath,
r1csBlake2bHash,
wasmBlake2bHash,
potBlake2bHash,
initialZkeyBlake2bHash
}
// nb. these will be populated after the first contribution.
const circuitTimings: CircuitTimings = {
contributionComputation: 0,
fullContribution: 0,
verifyCloudFunction: 0
}
circuits[i] = {
...circuit,
files: circuitFiles,
avgTimings: circuitTimings,
zKeySizeInBytes: getFileStats(zkeyLocalPathAndFileName).size
}
// Reset flags.
wannaGenerateNewZkey = true
wannaUsePreDownloadedPoT = false
}
const spinner = customSpinner(`Writing ceremony data...`, `clock`)
spinner.start()
try {
// Call the Cloud Function for writing ceremony data on Firestore DB.
ceremonyId = await setupCeremony(firebaseFunctions, ceremonyInputData, ceremonyPrefix, circuits)
} catch (error: any) {
const errorBody = JSON.parse(JSON.stringify(error))
showError(
`[${errorBody.code}] ${error.message} ${!errorBody.details ? "" : `\n${errorBody.details}`}`,
true
)
}
await sleep(5000) // Cloud function unexpected termination workaround.
spinner.succeed(
`Congratulations, the setup of ceremony ${theme.text.bold(
ceremonyInputData.title
)} (${`UID: ${theme.text.bold(ceremonyId)}`}) has been successfully completed ${
theme.emojis.tada
}. You will be able to find all the files and info respectively in the ceremony bucket and database document.`
)
}
terminate(providerUserId)
}
export default setup