diff --git a/.commitlintrc.json b/.commitlintrc.json index c30e5a97..f4fbb7dd 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,3 +1,3 @@ { - "extends": ["@commitlint/config-conventional"] + "extends": ["@commitlint/config-conventional"] } diff --git a/.editorconfig b/.editorconfig index a8c26d5c..c94bae0f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 120 -indent_size = 2 +indent_size = 4 [*.md] trim_trailing_whitespace = false diff --git a/.eslintrc.json b/.eslintrc.json index e3414633..0f12126e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,59 +1,26 @@ { - "root": true, - "env": { - "es6": true - }, - "extends": [ - "airbnb", - "airbnb/hooks", - "airbnb-typescript", - "plugin:jest/recommended", - "plugin:jest/style", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module", - "project": ["./tsconfig.json", "./cli/**/tsconfig.json"] - }, - "plugins": ["@typescript-eslint", "jest"], - "rules": { - "no-console": [ - "warn", - { - "allow": ["info", "warn", "error"] - } - ], - "no-promise-executor-return": "off", - "no-unsafe-optional-chaining": "warn", - "no-restricted-syntax": "off", - "consistent-return": "warn", - "no-underscore-dangle": "off", - "no-param-reassign": "off", - "no-await-in-loop": "off", - "import/no-cycle": "off", - "import/extensions": "off", - "no-bitwise": "off", - "jest/no-disabled-tests": "warn", - "jest/no-focused-tests": "error", - "jest/no-identical-title": "error", - "jest/prefer-to-have-length": "warn", - "jest/valid-expect": "warn", - "class-methods-use-this": "warn", - "import/no-extraneous-dependencies": "off", - "import/no-relative-packages": "off", - "@typescript-eslint/no-loop-func": "off", - "@typescript-eslint/no-shadow": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/lines-between-class-members": "off", - "@typescript-eslint/naming-convention": [ - "error", - { - "selector": "variable", - "format": ["camelCase", "PascalCase", "UPPER_CASE", "snake_case"], - "leadingUnderscore": "allow" - } - ] - } + "root": true, + "env": { + "es6": true + }, + "extends": ["airbnb-base", "airbnb-typescript/base", "plugin:jest/recommended", "plugin:jest/style", "prettier"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "project": ["./tsconfig.json", "./packages/*/tsconfig.json"] + }, + "plugins": ["@typescript-eslint", "jest"], + "rules": { + "no-underscore-dangle": "off", + "import/no-extraneous-dependencies": "off", + "no-bitwise": "off", + "no-await-in-loop": "off", + "no-restricted-syntax": "off", + "no-console": ["warn", { "allow": ["info", "warn", "error", "log"] }], + "@typescript-eslint/lines-between-class-members": "off", + "no-param-reassign": "off", + "jest/expect-expect": "off", + "no-promise-executor-return": "warn" + } } diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index d626be2e..46c8a9aa 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,22 +1,22 @@ name: Deploy - dev environment on: - push: - branches: - - dev + push: + branches: + - dev jobs: - deploy: - name: Deploy - runs-on: ubuntu-20.04 - steps: - - name: Checkout Repo - uses: actions/checkout@v3 + deploy: + name: Deploy + runs-on: ubuntu-20.04 + steps: + - name: Checkout Repo + uses: actions/checkout@v3 - - name: Install Dependencies - run: yarn install + - name: Install Dependencies + run: yarn install - - name: Deploy to Firebase - run: yarn firebase:deploy - working-directory: firebase - env: - FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + - name: Deploy to Firebase + run: yarn firebase:deploy + working-directory: firebase + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} diff --git a/.lintstagedrc.json b/.lintstagedrc.json index acbb84b7..9afe6370 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,3 +1,3 @@ { - "**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"] + "**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"] } diff --git a/.prettierrc.json b/.prettierrc.json index a6d13f8d..27140f2b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,5 @@ { - "semi": false, - "arrowParens": "always", - "trailingComma": "none" + "semi": false, + "arrowParens": "always", + "trailingComma": "none" } diff --git a/README.md b/README.md index 07353cfe..7a9d3d6b 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ The idea is to have a NodeJS CLI published as NPM package and use Firebase Cloud ### Actors -- **Coordinator**: an individual responsible for conducting and monitoring the ceremony. Basically, the coordinator have to prepare and conduct each step of the Phase 2 ceremony, from beginning to end. -- **Participant**: an individual who wants to contribute to the ceremony. The participant computes the contribution locally on their machine, generates an attestation, and makes it publicly available to everyone. +- **Coordinator**: an individual responsible for conducting and monitoring the ceremony. Basically, the coordinator have to prepare and conduct each step of the Phase 2 ceremony, from beginning to end. +- **Participant**: an individual who wants to contribute to the ceremony. The participant computes the contribution locally on their machine, generates an attestation, and makes it publicly available to everyone. ### Components -- **phase2cli**: all-in-one command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies. Both the participant and the coordinator can use it to interact with the ceremony, from its setup to generating a contribution. -- **firebase**: 3rd party Firebase CLI tool used to bootstrap the project to the cloud, locally emulate functions, db, storage and rules. +- **phase2cli**: all-in-one command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies. Both the participant and the coordinator can use it to interact with the ceremony, from its setup to generating a contribution. +- **firebase**: 3rd party Firebase CLI tool used to bootstrap the project to the cloud, locally emulate functions, db, storage and rules. ## Getting Started @@ -83,9 +83,9 @@ yarn prettier:fix What's missing -- Code of conduct -- Contributing -- Support -- License +- Code of conduct +- Contributing +- Support +- License **Please, follow the project boards to stay up-to-date!** diff --git a/apps/backend/README.md b/apps/backend/README.md deleted file mode 100644 index 1b1d426f..00000000 --- a/apps/backend/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# mpc-phase2-suite-firebase - -MPC Phase 2 backend for Firebase services management - -- Description (w/ Features). -- Install + Configs - - Firebase login - - Firebase init (select everything except Hosting and Remote Config) - - Console - - App - - Service Account Key generation -- Usage + Examples -- License -- Support + FAQ? - - FIREBASE_CONFIG & GCLOUD_PROJECT not set warn - because we are running on NodeJS and not on cloud. - - Emulator scheduled functions execution: emulator terminal + emulator shell terminal (here, call scheduled function, e.g., scheduledFunction()). - -Coordinator Guide .md for more infos about the Firebase configuration? diff --git a/apps/backend/firebase.json b/apps/backend/firebase.json deleted file mode 100644 index 609a3adf..00000000 --- a/apps/backend/firebase.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "firestore": { - "rules": "firestore.rules", - "indexes": "firestore.indexes.json" - }, - "functions": { - "predeploy": "yarn --prefix \"$RESOURCE_DIR\" build", - "source": "." - }, - "storage": { - "rules": "storage.rules" - }, - "emulators": { - "auth": { - "port": 9099 - }, - "functions": { - "port": 5001 - }, - "firestore": { - "port": 8080 - }, - "database": { - "port": 9000 - }, - "pubsub": { - "port": 8085 - }, - "storage": { - "port": 9199 - }, - "ui": { - "enabled": true - } - } -} diff --git a/apps/backend/firestore.indexes.json b/apps/backend/firestore.indexes.json deleted file mode 100644 index b9e83f41..00000000 --- a/apps/backend/firestore.indexes.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "indexes": [ - { - "collectionGroup": "ceremonies", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "state", - "order": "ASCENDING" - }, - { - "fieldPath": "endDate", - "order": "ASCENDING" - } - ] - }, - { - "collectionGroup": "ceremonies", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "state", - "order": "ASCENDING" - }, - { - "fieldPath": "startDate", - "order": "ASCENDING" - } - ] - } - ], - "fieldOverrides": [] -} diff --git a/apps/backend/package.json b/apps/backend/package.json deleted file mode 100644 index 83a6915c..00000000 --- a/apps/backend/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "@mpcts/backend", - "version": "0.0.1", - "description": "MPC Phase 2 backend for Firebase services management", - "repository": "https://github.com/quadratic-funding/mpc-phase2-suite/apps/backend", - "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", - "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", - "author": { - "name": "Giacomo (0xjei)" - }, - "license": "MIT", - "private": false, - "main": "dist/src/functions/index.js", - "types": "dist/types/src/index.d.ts", - "type": "module", - "engines": { - "node": "16" - }, - "files": [ - "dist/", - "src/", - "test/", - "types", - "README.md" - ], - "keywords": [ - "typescript", - "zero-knowledge", - "zk-snarks", - "phase-2", - "trusted-setup", - "ceremony", - "snarkjs", - "circom" - ], - "scripts": { - "build": "tsc", - "firebase:login": "firebase login", - "firebase:logout": "firebase logout", - "firebase:init": "firebase init", - "firebase:deploy": "yarn firestore:get-indexes && firebase deploy", - "firebase:deploy-functions": "firebase deploy --only functions", - "firebase:deploy-firestore": "yarn firestore:get-indexes && firebase deploy --only firestore", - "firebase:deploy-storage": "firebase deploy --only storage", - "firebase:log-functions": "firebase functions:log", - "firestore:get-indexes": "firebase firestore:indexes > firestore.indexes.json", - "emulator:serve": "yarn build && firebase emulators:start", - "emulator:serve-functions": "yarn build && firebase emulators:start --only functions", - "emulator:shell": "yarn build && firebase functions:shell" - }, - "devDependencies": { - "@firebase/rules-unit-testing": "^2.0.4", - "@types/uuid": "^8.3.4", - "firebase-functions-test": "^2.3.0", - "firebase-tools": "^11.8.0", - "typescript": "^4.7.4" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.178.0", - "@aws-sdk/middleware-endpoint": "^3.178.0", - "@aws-sdk/s3-request-presigner": "^3.178.0", - "blakejs": "^1.2.1", - "dotenv": "^16.0.1", - "firebase-admin": "^11.0.1", - "firebase-functions": "^3.22.0", - "snarkjs": "^0.5.0", - "timer-node": "^5.0.6", - "uuid": "^8.3.2", - "winston": "^3.8.1" - } -} diff --git a/apps/backend/src/functions/auth.ts b/apps/backend/src/functions/auth.ts deleted file mode 100644 index 21fbc585..00000000 --- a/apps/backend/src/functions/auth.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as functions from "firebase-functions" -import { UserRecord } from "firebase-functions/v1/auth" -import admin from "firebase-admin" -import dotenv from "dotenv" -import { GENERIC_ERRORS, logMsg } from "../lib/logs.js" -import { getCurrentServerTimestampInMillis } from "../lib/utils.js" -import { MsgType } from "../../types/index.js" - -dotenv.config() - -/** - * Auth-triggered function which writes a user document to Firestore. - */ -export const registerAuthUser = functions.auth.user().onCreate(async (user: UserRecord) => { - // Get DB. - const firestore = admin.firestore() - - // Get user information. - if (!user.uid) logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - // The user object has basic properties such as display name, email, etc. - const { displayName } = user - const { email } = user - const { photoURL } = user - const { emailVerified } = user - - // Metadata. - const { creationTime } = user.metadata - const { lastSignInTime } = user.metadata - - // The user's ID, unique to the Firebase project. Do NOT use - // this value to authenticate with your backend server, if - // you have one. Use User.getToken() instead. - const { uid } = user - - // Reference to a document using uid. - const userRef = firestore.collection("users").doc(uid) - - // Set document (nb. we refer to providerData[0] because we use Github OAuth provider only). - await userRef.set({ - name: displayName, - // Metadata. - creationTime, - lastSignInTime, - // Optional. - email: email || "", - emailVerified: emailVerified || false, - photoURL: photoURL || "", - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`User ${uid} correctly stored`, MsgType.INFO) -}) - -/** - * Set custom claims for role-based access control on the newly created user. - */ -export const processSignUpWithCustomClaims = functions.auth.user().onCreate(async (user: UserRecord) => { - // Get user information. - if (!user.uid) logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - let customClaims: any - // Check if user meets role criteria to be a coordinator. - if ( - user.email && - (user.email.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) || - user.email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN) - ) { - customClaims = { coordinator: true } - - logMsg(`User ${user.uid} identified as coordinator`, MsgType.INFO) - } else { - customClaims = { participant: true } - - logMsg(`User ${user.uid} identified as participant`, MsgType.INFO) - } - - try { - // Set custom user claims on this newly created user. - await admin.auth().setCustomUserClaims(user.uid, customClaims) - } catch (error: any) { - logMsg(`Something went wrong: ${error.toString()}`, MsgType.ERROR) - } -}) diff --git a/apps/backend/src/functions/ceremony.ts b/apps/backend/src/functions/ceremony.ts deleted file mode 100644 index 2cfe3885..00000000 --- a/apps/backend/src/functions/ceremony.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as functions from "firebase-functions" -import dotenv from "dotenv" -import { DocumentSnapshot } from "firebase-functions/v1/firestore" -import { CeremonyState, MsgType } from "../../types/index.js" -import { queryCeremoniesByStateAndDate } from "../lib/utils.js" -import { GENERIC_LOGS, logMsg } from "../lib/logs.js" - -dotenv.config() - -/** - * Automatically look and (if any) start scheduled ceremonies. - */ -export const startCeremony = functions.pubsub.schedule(`every 30 minutes`).onRun(async () => { - // Get ceremonies in `scheduled` state. - const scheduledCeremoniesQuerySnap = await queryCeremoniesByStateAndDate(CeremonyState.SCHEDULED, "startDate", "<=") - - if (scheduledCeremoniesQuerySnap.empty) logMsg(GENERIC_LOGS.GENLOG_NO_CEREMONIES_READY_TO_BE_OPENED, MsgType.INFO) - else { - scheduledCeremoniesQuerySnap.forEach(async (ceremonyDoc: DocumentSnapshot) => { - logMsg(`Ceremony ${ceremonyDoc.id} opened`, MsgType.INFO) - - // Update ceremony state to `running`. - await ceremonyDoc.ref.set({ state: CeremonyState.OPENED }, { merge: true }) - }) - } -}) - -/** - * Automatically look and (if any) stop running ceremonies. - */ -export const stopCeremony = functions.pubsub.schedule(`every 30 minutes`).onRun(async () => { - // Get ceremonies in `running` state. - const runningCeremoniesQuerySnap = await queryCeremoniesByStateAndDate(CeremonyState.OPENED, "endDate", "<=") - - if (runningCeremoniesQuerySnap.empty) logMsg(GENERIC_LOGS.GENLOG_NO_CEREMONIES_READY_TO_BE_CLOSED, MsgType.INFO) - else { - runningCeremoniesQuerySnap.forEach(async (ceremonyDoc: DocumentSnapshot) => { - logMsg(`Ceremony ${ceremonyDoc.id} closed`, MsgType.INFO) - - // Update ceremony state to `finished`. - await ceremonyDoc.ref.set({ state: CeremonyState.CLOSED }, { merge: true }) - }) - } -}) diff --git a/apps/backend/src/functions/contribute.ts b/apps/backend/src/functions/contribute.ts deleted file mode 100644 index 320612cf..00000000 --- a/apps/backend/src/functions/contribute.ts +++ /dev/null @@ -1,644 +0,0 @@ -import * as functions from "firebase-functions" -import admin from "firebase-admin" -import dotenv from "dotenv" -import { - CeremonyState, - CeremonyTimeoutType, - MsgType, - ParticipantContributionStep, - ParticipantStatus, - TimeoutType -} from "../../types/index.js" -import { GENERIC_ERRORS, GENERIC_LOGS, logMsg } from "../lib/logs.js" -import { collections, timeoutsCollectionFields } from "../lib/constants.js" -import { - getCeremonyCircuits, - getCurrentServerTimestampInMillis, - getParticipantById, - queryCeremoniesByStateAndDate, - queryValidTimeoutsByDate -} from "../lib/utils.js" - -dotenv.config() - -/** - * Check if a user can participate for the given ceremony (e.g., new contributor, after timeout expiration, etc.). - */ -export const checkParticipantForCeremony = functions.https.onCall( - async (data: any, context: functions.https.CallableContext) => { - console.log(context.auth) - console.log(context.auth?.token.participant) - console.log(context.auth?.token.coordinator) - - // Check if sender is authenticated. - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for the ceremony. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - - // Check existence. - if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - - // Get ceremony data. - const ceremonyData = ceremonyDoc.data() - - // Check if running. - if (!ceremonyData || ceremonyData.state !== CeremonyState.OPENED) - logMsg(GENERIC_ERRORS.GENERR_CEREMONY_NOT_OPENED, MsgType.ERROR) - - // Look for the user among ceremony participants. - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!participantDoc.exists) { - // Create a new Participant doc for the sender. - await participantDoc.ref.set({ - status: ParticipantStatus.WAITING, - contributionProgress: 0, - contributions: [], - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`User ${userId} has been registered as participant for ceremony ${ceremonyDoc.id}`, MsgType.INFO) - } else { - // Check if the participant has completed the contributions for all circuits. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - const circuits = await getCeremonyCircuits(`${collections.ceremonies}/${ceremonyDoc.id}/${collections.circuits}`) - - // Already contributed to all circuits or currently contributor without any timeout. - if ( - participantData?.contributionProgress === circuits.length && - participantData?.status === ParticipantStatus.DONE - ) { - logMsg( - `Participant ${participantDoc.id} has already contributed to all circuits or is the current contributor to that circuit (no timed out yet)`, - MsgType.DEBUG - ) - - return false - } - - if (participantData?.status === ParticipantStatus.TIMEDOUT) { - // Get `valid` timeouts (i.e., endDate is not expired). - const validTimeoutsQuerySnap = await queryValidTimeoutsByDate( - ceremonyDoc.id, - participantDoc.id, - timeoutsCollectionFields.endDate - ) - - if (validTimeoutsQuerySnap.empty) { - // TODO: need to remove unstable contributions (only one without doc link) and temp data, contributor must restart from step 1. - // The participant can retry the contribution. - await participantDoc.ref.set( - { - status: ParticipantStatus.EXHUMED, - contributionStep: ParticipantContributionStep.DOWNLOADING, - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - - logMsg(`Participant ${participantDoc.id} can retry the contribution from right now`, MsgType.DEBUG) - - return true - } - logMsg(`Participant ${participantDoc.id} cannot retry the contribution yet`, MsgType.DEBUG) - - return false - } - } - - return true - } -) - -/** - * Check and remove the current contributor who is taking more than a specified amount of time for completing the contribution. - */ -export const checkAndRemoveBlockingContributor = functions.pubsub.schedule("every 1 minutes").onRun(async () => { - if (!process.env.CF_RETRY_WAITING_TIME_IN_DAYS) logMsg(GENERIC_ERRORS.GENERR_WRONG_ENV_CONFIGURATION, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - const currentDate = getCurrentServerTimestampInMillis() - - // Get ceremonies in `opened` state. - const openedCeremoniesQuerySnap = await queryCeremoniesByStateAndDate(CeremonyState.OPENED, "endDate", ">=") - - if (openedCeremoniesQuerySnap.empty) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONIES_OPENED, MsgType.ERROR) - - // For each ceremony. - for (const ceremonyDoc of openedCeremoniesQuerySnap.docs) { - if (!ceremonyDoc.exists || !ceremonyDoc.data()) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - - // Get data. - const { timeoutType: ceremonyTimeoutType, penalty } = ceremonyDoc.data() - - // Get circuits. - const circuitsDocs = await getCeremonyCircuits( - `${collections.ceremonies}/${ceremonyDoc.id}/${collections.circuits}` - ) - - // For each circuit. - for (const circuitDoc of circuitsDocs) { - if (!circuitDoc.exists || !circuitDoc.data()) logMsg(GENERIC_ERRORS.GENERR_INVALID_CIRCUIT, MsgType.ERROR) - - const circuitData = circuitDoc.data() - - logMsg(`Circuit document ${circuitDoc.id} okay`, MsgType.DEBUG) - - // Get data. - const { waitingQueue, avgTimings } = circuitData - const { contributors, currentContributor, failedContributions, completedContributions } = waitingQueue - const { fullContribution: avgFullContribution } = avgTimings - - // Check for current contributor. - if (!currentContributor) logMsg(GENERIC_ERRORS.GENERR_NO_CURRENT_CONTRIBUTOR, MsgType.WARN) - - // Check if first contributor. - if ( - !currentContributor && - avgFullContribution === 0 && - completedContributions === 0 && - ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC - ) - logMsg(GENERIC_ERRORS.GENERR_NO_TIMEOUT_FIRST_COTRIBUTOR, MsgType.DEBUG) - - if ( - !!currentContributor && - ((avgFullContribution > 0 && completedContributions > 0) || ceremonyTimeoutType === CeremonyTimeoutType.FIXED) - ) { - // Get current contributor data (i.e., participant). - const participantDoc = await getParticipantById(ceremonyDoc.id, currentContributor) - - if (!participantDoc.exists || !participantDoc.data()) - logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.WARN) - else { - const participantData = participantDoc.data() - const contributionStartedAt = participantData?.contributionStartedAt - const verificationStartedAt = participantData?.verificationStartedAt - const currentContributionStep = participantData?.contributionStep - - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check for blocking contributions (frontend-side). - const timeoutToleranceThreshold = - ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC - ? (avgFullContribution / 100) * Number(circuitData.timeoutThreshold) - : 0 - - const timeoutExpirationDateInMillisForBlockingContributor = - ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC - ? Number(contributionStartedAt) + Number(avgFullContribution) + Number(timeoutToleranceThreshold) - : Number(contributionStartedAt) + Number(circuitData.timeoutMaxContributionWaitingTime) * 60000 - - logMsg(`Contribution start date ${contributionStartedAt}`, MsgType.DEBUG) - if (ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC) { - logMsg(`Average contribution per circuit time ${avgFullContribution} ms`, MsgType.DEBUG) - logMsg(`Timeout tolerance threshold set to ${timeoutToleranceThreshold}`, MsgType.DEBUG) - } - logMsg(`BC Timeout expirartion date ${timeoutExpirationDateInMillisForBlockingContributor} ms`, MsgType.DEBUG) - - // Check for blocking verifications (backend-side). - const timeoutExpirationDateInMillisForBlockingFunction = !verificationStartedAt - ? 0 - : Number(verificationStartedAt) + 3540000 // 3540000 = 59 minutes in ms. - - logMsg(`Verification start date ${verificationStartedAt}`, MsgType.DEBUG) - logMsg(`CF Timeout expirartion date ${timeoutExpirationDateInMillisForBlockingFunction} ms`, MsgType.DEBUG) - - // Get timeout type. - let timeoutType = 0 - - if ( - timeoutExpirationDateInMillisForBlockingContributor < currentDate && - currentContributionStep >= ParticipantContributionStep.DOWNLOADING && - currentContributionStep <= ParticipantContributionStep.UPLOADING - ) - timeoutType = TimeoutType.BLOCKING_CONTRIBUTION - - if ( - timeoutExpirationDateInMillisForBlockingFunction > 0 && - timeoutExpirationDateInMillisForBlockingFunction < currentDate && - currentContributionStep === ParticipantContributionStep.VERIFYING - ) - timeoutType = TimeoutType.BLOCKING_CLOUD_FUNCTION - - logMsg(`Ceremony Timeout type ${ceremonyTimeoutType}`, MsgType.DEBUG) - logMsg(`Timeout type ${timeoutType}`, MsgType.DEBUG) - - // Check if one timeout should be triggered. - if (timeoutType !== 0) { - // Timeout the participant. - const batch = firestore.batch() - - // 1. Update circuit' waiting queue. - contributors.shift(1) - - let newCurrentContributor = "" - - if (contributors.length > 0) { - // There's someone else ready to contribute. - newCurrentContributor = contributors.at(0) - - // Pass the baton to the next participant. - const newCurrentContributorDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyDoc.id}/${collections.participants}`) - .doc(newCurrentContributor) - .get() - - if (newCurrentContributorDoc.exists) { - batch.update(newCurrentContributorDoc.ref, { - status: ParticipantStatus.WAITING, - lastUpdated: getCurrentServerTimestampInMillis() - }) - } - } - - batch.update(circuitDoc.ref, { - waitingQueue: { - ...circuitData.waitingQueue, - contributors, - currentContributor: newCurrentContributor, - failedContributions: failedContributions + 1 - }, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Batch: update for circuit' waiting queue`, MsgType.DEBUG) - - // 2. Change blocking contributor status. - batch.update(participantDoc.ref, { - status: ParticipantStatus.TIMEDOUT, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Batch: change blocking contributor status to TIMEDOUT`, MsgType.DEBUG) - - // 3. Create a new collection of timeouts (to keep track of participants timeouts). - const retryWaitingTimeInMillis = - timeoutType === TimeoutType.BLOCKING_CONTRIBUTION - ? Number(penalty) * 60000 // 60000 = amount of ms x minute. - : Number(process.env.CF_RETRY_WAITING_TIME_IN_DAYS) * 86400000 // 86400000 = amount of ms x day. - - // Timeout collection. - const timeoutDoc = await firestore - .collection( - `${collections.ceremonies}/${ceremonyDoc.id}/${collections.participants}/${participantDoc.id}/${collections.timeouts}` - ) - .doc() - .get() - - batch.create(timeoutDoc.ref, { - type: timeoutType, - startDate: currentDate, - endDate: currentDate + retryWaitingTimeInMillis - }) - - logMsg(`Batch: add timeout document for blocking contributor`, MsgType.DEBUG) - - await batch.commit() - - logMsg(`Blocking contributor ${participantDoc.id} timedout. Cause ${timeoutType}`, MsgType.INFO) - } else logMsg(GENERIC_LOGS.GENLOG_NO_TIMEOUT, MsgType.INFO) - } - } - } - } -}) - -/** - * Progress to next contribution step for the current contributor of a specified circuit in a given ceremony. - */ -export const progressToNextContributionStep = functions.https.onCall( - async (data: any, context: functions.https.CallableContext) => { - // Check if sender is authenticated. - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for the ceremony. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - - // Check existence. - if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - - // Get ceremony data. - const ceremonyData = ceremonyDoc.data() - - // Check if running. - if (!ceremonyData || ceremonyData.state !== CeremonyState.OPENED) - logMsg(GENERIC_ERRORS.GENERR_CEREMONY_NOT_OPENED, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) - - // Look for the user among ceremony participants. - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - // Check existence. - if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) - - // Get participant data. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check if participant is able to advance to next contribution step. - if (participantData?.status !== ParticipantStatus.CONTRIBUTING) - logMsg(`Participant ${participantDoc.id} is not contributing`, MsgType.ERROR) - - // Make the advancement. - const progress = participantData?.contributionStep + 1 - - logMsg(`Current contribution step should be ${participantData?.contributionStep}`, MsgType.DEBUG) - logMsg(`Next contribution step should be ${progress}`, MsgType.DEBUG) - - // nb. DOWNLOADING (=1) must be set when coordinating the waiting queue while COMPLETED (=5) must be set in verifyContribution(). - if (progress <= ParticipantContributionStep.DOWNLOADING || progress >= ParticipantContributionStep.COMPLETED) - logMsg(`Wrong contribution step ${progress} for ${participantDoc.id}`, MsgType.ERROR) - - if (progress === ParticipantContributionStep.VERIFYING) - await participantDoc.ref.update({ - contributionStep: progress, - verificationStartedAt: getCurrentServerTimestampInMillis(), - lastUpdated: getCurrentServerTimestampInMillis() - }) - else - await participantDoc.ref.update({ - contributionStep: progress, - lastUpdated: getCurrentServerTimestampInMillis() - }) - } -) - -/** - * Temporary store the contribution computation time for the current contributor. - */ -export const temporaryStoreCurrentContributionComputationTime = functions.https.onCall( - async (data: any, context: functions.https.CallableContext) => { - // Check if sender is authenticated. - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId || data.contributionComputationTime <= 0) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - // Check existence. - if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) - - // Get data. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check if has reached the computing step while contributing. - if (participantData?.contributionStep !== ParticipantContributionStep.COMPUTING) - logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) - - // Update. - await participantDoc.ref.set( - { - ...participantData!, - tempContributionData: { - contributionComputationTime: data.contributionComputationTime - }, - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - } -) - -/** - * Permanently store the contribution computation hash for attestation generation for the current contributor. - */ -export const permanentlyStoreCurrentContributionTimeAndHash = functions.https.onCall( - async (data: any, context: functions.https.CallableContext) => { - // Check if sender is authenticated. - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId || data.contributionComputationTime <= 0 || !data.contributionHash) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - // Check existence. - if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) - - // Get data. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check if has reached the computing step while contributing or is finalizing. - if ( - participantData?.contributionStep === ParticipantContributionStep.COMPUTING || - (context?.auth?.token.coordinator && participantData?.status === ParticipantStatus.FINALIZING) - ) - // Update. - await participantDoc.ref.set( - { - ...participantData!, - contributions: [ - ...participantData!.contributions, - { - hash: data.contributionHash!, - computationTime: data.contributionComputationTime - } - ], - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - else logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) - } -) - -/** - * Temporary store the the Multi-Part Upload identifier for the current contributor. - */ -export const temporaryStoreCurrentContributionMultiPartUploadId = functions.https.onCall( - async (data: any, context: functions.https.CallableContext) => { - // Check if sender is authenticated. - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId || !data.uploadId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - // Check existence. - if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) - - // Get data. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check if has reached the uploading step while contributing. - if (participantData?.contributionStep !== ParticipantContributionStep.UPLOADING) - logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) - - // Update. - await participantDoc.ref.set( - { - ...participantData!, - tempContributionData: { - ...participantData?.tempContributionData, - uploadId: data.uploadId, - chunks: [] - }, - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - } -) - -/** - * Temporary store the ETag and PartNumber for each uploaded chunk in order to make the upload resumable from last chunk. - */ -export const temporaryStoreCurrentContributionUploadedChunkData = functions.https.onCall( - async (data: any, context: functions.https.CallableContext) => { - // Check if sender is authenticated. - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId || !data.eTag || data.partNumber <= 0) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - // Check existence. - if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) - - // Get data. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check if has reached the uploading step while contributing. - if (participantData?.contributionStep !== ParticipantContributionStep.UPLOADING) - logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) - - const chunks = participantData?.tempContributionData.chunks ? participantData?.tempContributionData.chunks : [] - - // Add last chunk. - chunks.push({ - ETag: data.eTag, - PartNumber: data.partNumber - }) - - // Update. - await participantDoc.ref.set( - { - ...participantData!, - tempContributionData: { - ...participantData?.tempContributionData, - chunks - }, - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - } -) diff --git a/apps/backend/src/functions/finalize.ts b/apps/backend/src/functions/finalize.ts deleted file mode 100644 index 0917fbf2..00000000 --- a/apps/backend/src/functions/finalize.ts +++ /dev/null @@ -1,240 +0,0 @@ -import * as functions from "firebase-functions" -import admin from "firebase-admin" -import path from "path" -import os from "os" -import fs from "fs" -import blake from "blakejs" -import { logMsg, GENERIC_ERRORS } from "../lib/logs.js" -import { collections } from "../lib/constants.js" -import { CeremonyState, MsgType, ParticipantStatus } from "../../types/index.js" -import { - getCeremonyCircuits, - getCurrentServerTimestampInMillis, - getFinalContributionDocument, - getS3Client, - tempDownloadFromBucket -} from "../lib/utils.js" - -/** - * Check and prepare the coordinator for the ceremony finalization. - */ -export const checkAndPrepareCoordinatorForFinalization = functions.https.onCall( - async (data: any, context: functions.https.CallableContext) => { - // Check if sender is authenticated. - if (!context.auth || !context.auth.token.coordinator) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for the ceremony. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - - // Check existence. - if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) - - // Get ceremony data. - const ceremonyData = ceremonyDoc.data() - - // Check if running. - if (!ceremonyData || ceremonyData.state !== CeremonyState.CLOSED) - logMsg(GENERIC_ERRORS.GENERR_CEREMONY_NOT_CLOSED, MsgType.ERROR) - - // Look for the coordinator among ceremony participant. - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - // Check if the coordinator has completed the contributions for all circuits. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - const circuits = await getCeremonyCircuits(`${collections.ceremonies}/${ceremonyDoc.id}/${collections.circuits}`) - - // Already contributed to all circuits. - if ( - participantData?.contributionProgress === circuits.length + 1 || - participantData?.status === ParticipantStatus.DONE - ) { - // Update participant status. - await participantDoc.ref.set( - { - status: ParticipantStatus.FINALIZING, - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - - logMsg(`Coordinator ${participantDoc.id} ready for finalization`, MsgType.DEBUG) - - return true - } - return false - } -) - -/** - * Add Verifier smart contract and verification key files metadata to the last final contribution for verifiability/integrity of the ceremony. - */ -export const finalizeLastContribution = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - if (!context.auth || !context.auth.token.coordinator) logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) - - if (!data.ceremonyId || !data.circuitId || !data.bucketName) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get Storage. - const S3 = await getS3Client() - - // Get data. - const { ceremonyId, circuitId, bucketName } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const circuitDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.circuits}`) - .doc(circuitId) - .get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - const contributionDoc = await getFinalContributionDocument( - `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuitId}/${collections.contributions}` - ) - - if (!ceremonyDoc.exists || !circuitDoc.exists || !participantDoc.exists || !contributionDoc.exists) - logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const circuitData = circuitDoc.data() - const participantData = participantDoc.data() - const contributionData = contributionDoc.data() - - if (!ceremonyData || !circuitData || !participantData || !contributionData) - logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Circuit document ${circuitDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - logMsg(`Contribution document ${contributionDoc.id} okay`, MsgType.DEBUG) - - // Filenames. - const verificationKeyFilename = `${circuitData?.prefix}_vkey.json` - const verifierContractFilename = `${circuitData?.prefix}_verifier.sol` - - // Get storage paths. - const verificationKeyStoragePath = `${collections.circuits}/${circuitData?.prefix}/${verificationKeyFilename}` - const verifierContractStoragePath = `${collections.circuits}/${circuitData?.prefix}/${verifierContractFilename}` - - // Temporary store files from bucket. - const verificationKeyTmpFilePath = path.join(os.tmpdir(), verificationKeyFilename) - const verifierContractTmpFilePath = path.join(os.tmpdir(), verifierContractFilename) - - await tempDownloadFromBucket(S3, bucketName, verificationKeyStoragePath, verificationKeyTmpFilePath) - await tempDownloadFromBucket(S3, bucketName, verifierContractStoragePath, verifierContractTmpFilePath) - - // Compute blake2b hash before unlink. - const verificationKeyBuffer = fs.readFileSync(verificationKeyTmpFilePath) - const verifierContractBuffer = fs.readFileSync(verifierContractTmpFilePath) - - logMsg(`Downloads from storage completed`, MsgType.INFO) - - const verificationKeyBlake2bHash = blake.blake2bHex(verificationKeyBuffer) - const verifierContractBlake2bHash = blake.blake2bHex(verifierContractBuffer) - - // Unlink folders. - fs.unlinkSync(verificationKeyTmpFilePath) - fs.unlinkSync(verifierContractTmpFilePath) - - // Update DB. - const batch = firestore.batch() - - batch.update(contributionDoc.ref, { - files: { - ...contributionData?.files, - verificationKeyBlake2bHash, - verificationKeyFilename, - verificationKeyStoragePath, - verifierContractBlake2bHash, - verifierContractFilename, - verifierContractStoragePath - }, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - await batch.commit() - - logMsg( - `Circuit ${circuitId} correctly finalized - Ceremony ${ceremonyDoc.id} - Coordinator ${participantDoc.id}`, - MsgType.INFO - ) - } -) - -/** - * Finalize a closed ceremony. - */ -export const finalizeCeremony = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - if (!context.auth || !context.auth.token.coordinator) logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) - - if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - // Update DB. - const batch = firestore.batch() - - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!ceremonyDoc.exists || !participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const participantData = participantDoc.data() - - if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check if the ceremony has state equal to closed. - if (ceremonyData?.state === CeremonyState.CLOSED && participantData?.status === ParticipantStatus.FINALIZING) { - // Finalize the ceremony. - batch.update(ceremonyDoc.ref, { state: CeremonyState.FINALIZED }) - - // Update coordinator status. - batch.update(participantDoc.ref, { - status: ParticipantStatus.FINALIZED - }) - - await batch.commit() - - logMsg(`Ceremony ${ceremonyDoc.id} correctly finalized - Coordinator ${participantDoc.id}`, MsgType.INFO) - } - } -) diff --git a/apps/backend/src/functions/index.ts b/apps/backend/src/functions/index.ts deleted file mode 100644 index 1c2f2a0c..00000000 --- a/apps/backend/src/functions/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import admin from "firebase-admin" -import { registerAuthUser, processSignUpWithCustomClaims } from "./auth.js" -import { startCeremony, stopCeremony } from "./ceremony.js" -import { setupCeremony, initEmptyWaitingQueueForCircuit } from "./setup.js" -import { - checkParticipantForCeremony, - checkAndRemoveBlockingContributor, - progressToNextContributionStep, - temporaryStoreCurrentContributionComputationTime, - permanentlyStoreCurrentContributionTimeAndHash, - temporaryStoreCurrentContributionMultiPartUploadId, - temporaryStoreCurrentContributionUploadedChunkData -} from "./contribute.js" -import { - coordinateContributors, - verifycontribution, - refreshParticipantAfterContributionVerification, - makeProgressToNextContribution, - resumeContributionAfterTimeoutExpiration -} from "./waitingQueue.js" -import { checkAndPrepareCoordinatorForFinalization, finalizeLastContribution, finalizeCeremony } from "./finalize.js" -import { - createBucket, - checkIfObjectExist, - generateGetObjectPreSignedUrl, - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload -} from "./storage.js" - -admin.initializeApp() - -export { - registerAuthUser, - processSignUpWithCustomClaims, - startCeremony, - stopCeremony, - checkAndPrepareCoordinatorForFinalization, - finalizeLastContribution, - finalizeCeremony, - setupCeremony, - initEmptyWaitingQueueForCircuit, - checkParticipantForCeremony, - checkAndRemoveBlockingContributor, - progressToNextContributionStep, - temporaryStoreCurrentContributionComputationTime, - permanentlyStoreCurrentContributionTimeAndHash, - temporaryStoreCurrentContributionMultiPartUploadId, - temporaryStoreCurrentContributionUploadedChunkData, - coordinateContributors, - verifycontribution, - refreshParticipantAfterContributionVerification, - makeProgressToNextContribution, - resumeContributionAfterTimeoutExpiration, - createBucket, - checkIfObjectExist, - generateGetObjectPreSignedUrl, - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload -} diff --git a/apps/backend/src/functions/setup.ts b/apps/backend/src/functions/setup.ts deleted file mode 100644 index ad21cdcd..00000000 --- a/apps/backend/src/functions/setup.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as functions from "firebase-functions" -import admin from "firebase-admin" -import dotenv from "dotenv" -import { QueryDocumentSnapshot } from "firebase-functions/v1/firestore" -import { CeremonyState, CeremonyType, MsgType } from "../../types/index.js" -import { GENERIC_ERRORS, logMsg } from "../lib/logs.js" -import { getCurrentServerTimestampInMillis } from "../lib/utils.js" -import { collections } from "../lib/constants.js" - -dotenv.config() - -/** - * Bootstrap/Setup every necessary document for running a ceremony. - */ -export const setupCeremony = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - if (!context.auth || !context.auth.token.coordinator) logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) - - if (!data.ceremonyInputData || !data.ceremonyPrefix || !data.circuits) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Database. - const firestore = admin.firestore() - const batch = firestore.batch() - - // Get data. - const { ceremonyInputData, ceremonyPrefix, circuits } = data - const userId = context.auth?.uid - - // Ceremonies. - const ceremonyDoc = await firestore.collection(`${collections.ceremonies}/`).doc().get() - - batch.create(ceremonyDoc.ref, { - title: ceremonyInputData.title, - description: ceremonyInputData.description, - startDate: new Date(ceremonyInputData.startDate).valueOf(), - endDate: new Date(ceremonyInputData.endDate).valueOf(), - prefix: ceremonyPrefix, - state: CeremonyState.SCHEDULED, - type: CeremonyType.PHASE2, - penalty: ceremonyInputData.penalty, - timeoutType: ceremonyInputData.timeoutMechanismType, - coordinatorId: userId, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - // Circuits. - if (!circuits.length) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUIT_PROVIDED, MsgType.ERROR) - - for (const circuit of circuits) { - const circuitDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyDoc.ref.id}/${collections.circuits}`) - .doc() - .get() - - batch.create(circuitDoc.ref, { - ...circuit, - lastUpdated: getCurrentServerTimestampInMillis() - }) - } - - await batch.commit() - - logMsg(`Ceremony ${ceremonyDoc.id} setup successfully completed - Coordinator ${userId}`, MsgType.INFO) - } -) - -/** - * Initialize an empty Waiting Queue field for the newly created circuit document. - */ -export const initEmptyWaitingQueueForCircuit = functions.firestore - .document(`/${collections.ceremonies}/{ceremony}/${collections.circuits}/{circuit}`) - .onCreate(async (doc: QueryDocumentSnapshot) => { - // Get DB. - const firestore = admin.firestore() - - // Get doc info. - const circuitId = doc.id - const circuitData = doc.data() - const parentCollectionPath = doc.ref.parent.path // == /ceremonies/{ceremony}/circuits/. - - // Empty waiting queue. - const waitingQueue = { - contributors: [], - currentContributor: "", - completedContributions: 0, // == nextZkeyIndex. - failedContributions: 0 - } - - // Update the circuit document. - await firestore - .collection(parentCollectionPath) - .doc(circuitId) - .set( - { - ...circuitData, - waitingQueue, - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - - logMsg(`Empty waiting queue successfully initialized for circuit ${circuitId} - Ceremony ${doc.id}`, MsgType.INFO) - }) diff --git a/apps/backend/src/functions/storage.ts b/apps/backend/src/functions/storage.ts deleted file mode 100644 index aa3ada62..00000000 --- a/apps/backend/src/functions/storage.ts +++ /dev/null @@ -1,325 +0,0 @@ -import * as functions from "firebase-functions" -import admin from "firebase-admin" -import { - GetObjectCommand, - CreateMultipartUploadCommand, - UploadPartCommand, - CompleteMultipartUploadCommand, - HeadObjectCommand, - CreateBucketCommand -} from "@aws-sdk/client-s3" -import { getSignedUrl } from "@aws-sdk/s3-request-presigner" -import dotenv from "dotenv" -import { MsgType, ParticipantContributionStep, ParticipantStatus } from "../../types/index.js" -import { logMsg, GENERIC_ERRORS } from "../lib/logs.js" -import { getS3Client } from "../lib/utils.js" -import { collections } from "../lib/constants.js" - -dotenv.config() - -/** - * Create a new AWS S3 bucket for a particular ceremony. - */ -export const createBucket = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - // Checks. - if (!context.auth || !context.auth.token.coordinator) logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) - - if (!data.bucketName) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Connect w/ S3. - const S3 = await getS3Client() - - // Prepare command. - const command = new CreateBucketCommand({ - Bucket: data.bucketName, - CreateBucketConfiguration: { - LocationConstraint: process.env.AWS_REGION! - } - }) - - try { - // Send command. - const response = await S3.send(command) - - // Check response. - if (response.$metadata.httpStatusCode === 200 && !!response.Location) { - logMsg(`Bucket successfully created`, MsgType.LOG) - - return true - } - } catch (error: any) { - if (error.$metadata.httpStatusCode === 400 && error.Code === "InvalidBucketName") { - logMsg(`Bucket not created: ${error.Code}`, MsgType.LOG) - - return false - } - - logMsg(`Generic error when creating a new S3 bucket: ${error}`, MsgType.ERROR) - } - } -) - -/** - * Check if a specified object exist in a given AWS S3 bucket. - */ -export const checkIfObjectExist = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - // Checks. - if (!context.auth || !context.auth.token.coordinator) logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) - - if (!data.bucketName || !data.objectKey) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Connect w/ S3. - const S3 = await getS3Client() - - // Prepare command. - const command = new HeadObjectCommand({ Bucket: data.bucketName, Key: data.objectKey }) - - try { - // Send command. - const response = await S3.send(command) - - // Check response. - if (response.$metadata.httpStatusCode === 200 && !!response.ETag) { - logMsg(`Object: ${data.objectKey} exists!`, MsgType.LOG) - - return true - } - } catch (error: any) { - if (error.$metadata.httpStatusCode === 404 && !error.ETag) { - logMsg(`Object: ${data.objectKey} does not exist!`, MsgType.LOG) - - return false - } - - logMsg(`Generic error when checking for object on S3 bucket: ${error}`, MsgType.ERROR) - } - } -) - -/** - * Generate a new AWS S3 pre signed url to upload/download an object (GET). - */ -export const generateGetObjectPreSignedUrl = functions.https.onCall(async (data: any): Promise<any> => { - if (!data.bucketName || !data.objectKey) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Connect w/ S3. - const S3 = await getS3Client() - - // Prepare the command. - const command = new GetObjectCommand({ Bucket: data.bucketName, Key: data.objectKey }) - - // Get the PreSignedUrl. - const url = await getSignedUrl(S3, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) }) - - logMsg(`Single Pre-Signed URL ${url}`, MsgType.LOG) - - return url -}) - -/** - * Initiate a multi part upload for a specific object in AWS S3 bucket. - */ -export const startMultiPartUpload = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.bucketName || !data.objectKey || (context.auth?.token.participant && !data.ceremonyId)) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { bucketName, objectKey, ceremonyId } = data - const userId = context.auth?.uid - - if (context.auth?.token.participant && !!ceremonyId) { - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!ceremonyDoc.exists || !participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const participantData = participantDoc.data() - - if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check participant status and contribution step. - const { status, contributionStep } = participantData! - - if (status !== ParticipantStatus.CONTRIBUTING && contributionStep !== ParticipantContributionStep.UPLOADING) - logMsg(`Participant ${participantDoc.id} is not able to start a multi part upload right now`, MsgType.ERROR) - } - - // Connect w/ S3. - const S3 = await getS3Client() - - // Prepare command. - const command = new CreateMultipartUploadCommand({ Bucket: bucketName, Key: objectKey }) - - // Send command. - const responseInitiate = await S3.send(command) - const uploadId = responseInitiate.UploadId - - logMsg(`Upload ID: ${uploadId}`, MsgType.LOG) - - return uploadId - } -) - -/** - * Generate a PreSignedUrl for each part of the given multi part upload. - */ -export const generatePreSignedUrlsParts = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if ( - !data.bucketName || - !data.objectKey || - !data.uploadId || - data.numberOfParts <= 0 || - (context.auth?.token.participant && !data.ceremonyId) - ) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { bucketName, objectKey, uploadId, numberOfParts, ceremonyId } = data - const userId = context.auth?.uid - - if (context.auth?.token.participant && !!ceremonyId) { - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!ceremonyDoc.exists || !participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const participantData = participantDoc.data() - - if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check participant status and contribution step. - const { status, contributionStep } = participantData! - - if (status !== ParticipantStatus.CONTRIBUTING && contributionStep !== ParticipantContributionStep.UPLOADING) - logMsg(`Participant ${participantDoc.id} is not able to start a multi part upload right now`, MsgType.ERROR) - } - - // Connect w/ S3. - const S3 = await getS3Client() - - const parts = [] - - for (let i = 0; i < numberOfParts; i += 1) { - // Prepare command for each part. - const command = new UploadPartCommand({ - Bucket: bucketName, - Key: objectKey, - PartNumber: i + 1, - UploadId: uploadId - }) - - // Get the PreSignedUrl for uploading the specific part. - const signedUrl = await getSignedUrl(S3, command, { - expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) - }) - - parts.push(signedUrl) - } - - return parts - } -) - -/** - * Ultimate the multi part upload for a specific object in AWS S3 bucket. - */ -export const completeMultiPartUpload = functions.https.onCall( - async (data: any, context: functions.https.CallableContext): Promise<any> => { - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if ( - !data.bucketName || - !data.objectKey || - !data.uploadId || - !data.parts || - (context.auth?.token.participant && !data.ceremonyId) - ) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { bucketName, objectKey, uploadId, parts, ceremonyId } = data - const userId = context.auth?.uid - - if (context.auth?.token.participant && !!ceremonyId) { - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!ceremonyDoc.exists || !participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const participantData = participantDoc.data() - - if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - // Check participant status and contribution step. - const { status, contributionStep } = participantData! - - if (status !== ParticipantStatus.CONTRIBUTING && contributionStep !== ParticipantContributionStep.UPLOADING) - logMsg(`Participant ${participantDoc.id} is not able to start a multi part upload right now`, MsgType.ERROR) - } - - // Connect w/ S3. - const S3 = await getS3Client() - - // Prepare command. - const command = new CompleteMultipartUploadCommand({ - Bucket: bucketName, - Key: objectKey, - UploadId: uploadId, - MultipartUpload: { Parts: parts } - }) - - // Send command. - const responseComplete = await S3.send(command) - - logMsg(`Upload for ${data.uploadId} completed! Object location ${responseComplete.Location}`, MsgType.LOG) - - return responseComplete.Location - } -) diff --git a/apps/backend/src/functions/waitingQueue.ts b/apps/backend/src/functions/waitingQueue.ts deleted file mode 100644 index bb09f4f1..00000000 --- a/apps/backend/src/functions/waitingQueue.ts +++ /dev/null @@ -1,736 +0,0 @@ -import * as functionsV1 from "firebase-functions/v1" -import * as functionsV2 from "firebase-functions/v2" -import admin from "firebase-admin" -import dotenv from "dotenv" -import { QueryDocumentSnapshot } from "firebase-functions/v1/firestore" -import { Change } from "firebase-functions" -import { zKey } from "snarkjs" -import path from "path" -import os from "os" -import fs from "fs" -import { Timer } from "timer-node" -import blake from "blakejs" -import winston from "winston" -import { FieldValue } from "firebase-admin/firestore" -import { CeremonyState, MsgType, ParticipantContributionStep, ParticipantStatus } from "../../types/index.js" -import { - deleteObject, - formatZkeyIndex, - getCircuitDocumentByPosition, - getCurrentServerTimestampInMillis, - getS3Client, - sleep, - tempDownloadFromBucket, - uploadFileToBucket -} from "../lib/utils.js" -import { collections, names } from "../lib/constants.js" -import { GENERIC_ERRORS, logMsg } from "../lib/logs.js" - -dotenv.config() - -/** - * Automate the coordination for participants contributions. - * @param circuit <QueryDocumentSnapshot> - the circuit document. - * @param participant <QueryDocumentSnapshot> - the participant document. - * @param ceremonyId <string> - the ceremony identifier. - */ -const coordinate = async (circuit: QueryDocumentSnapshot, participant: QueryDocumentSnapshot, ceremonyId?: string) => { - // Get DB. - const firestore = admin.firestore() - // Update DB. - const batch = firestore.batch() - - // Get info. - const participantId = participant.id - const circuitData = circuit.data() - const participantData = participant.data() - - logMsg(`Circuit document ${circuit.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantId} okay`, MsgType.DEBUG) - - const { waitingQueue } = circuitData - const { contributors } = waitingQueue - let { currentContributor } = waitingQueue - let newParticipantStatus = 0 - let newContributionStep = 0 - - // Case 1: Participant is ready to contribute and there's nobody in the queue. - if (!contributors.length && !currentContributor) { - logMsg(`Coordination use-case 1: Participant is ready to contribute and there's nobody in the queue`, MsgType.INFO) - - currentContributor = participantId - newParticipantStatus = ParticipantStatus.CONTRIBUTING - newContributionStep = ParticipantContributionStep.DOWNLOADING - } - - // Case 2: Participant is ready to contribute but there's another participant currently contributing. - if (currentContributor !== participantId) { - logMsg( - `Coordination use-case 2: Participant is ready to contribute but there's another participant currently contributing`, - MsgType.INFO - ) - - newParticipantStatus = ParticipantStatus.WAITING - } - - // Case 3: the participant has finished the contribution so this case is used to update the i circuit queue. - if ( - currentContributor === participantId && - (participantData.status === ParticipantStatus.CONTRIBUTED || participantData.status === ParticipantStatus.DONE) && - participantData.contributionStep === ParticipantContributionStep.COMPLETED - ) { - logMsg( - `Coordination use-case 3: Participant has finished the contribution so this case is used to update the i circuit queue`, - MsgType.INFO - ) - - contributors.shift(1) - - if (contributors.length > 0) { - // There's someone else ready to contribute. - currentContributor = contributors.at(0) - - // Pass the baton to the next participant. - const newCurrentContributorDoc = await firestore - .collection(`${ceremonyId}/${collections.participants}`) - .doc(currentContributor) - .get() - - if (newCurrentContributorDoc.exists) { - batch.update(newCurrentContributorDoc.ref, { - status: ParticipantStatus.WAITING, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Batch update use-case 3: New current contributor`, MsgType.INFO) - } - } else currentContributor = "" - } - - // Updates for cases 1 and 2. - if (newParticipantStatus !== 0) { - contributors.push(participantId) - - batch.update(participant.ref, { - status: newParticipantStatus, - contributionStartedAt: - newParticipantStatus === ParticipantStatus.CONTRIBUTING ? getCurrentServerTimestampInMillis() : 0, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - // Case 1 only. - if (newContributionStep !== 0) - batch.update(participant.ref, { - contributionStep: newContributionStep, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Batch update use-case 1 or 2: participant updates`, MsgType.INFO) - } - - // Update waiting queue. - batch.update(circuit.ref, { - waitingQueue: { - ...waitingQueue, - contributors, - currentContributor - }, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Batch update all use-cases: update circuit waiting queue`, MsgType.INFO) - - await batch.commit() -} - -/** - * Coordinate waiting queue contributors. - */ -export const coordinateContributors = functionsV1.firestore - .document(`${collections.ceremonies}/{ceremonyId}/${collections.participants}/{participantId}`) - .onUpdate(async (change: Change<QueryDocumentSnapshot>) => { - // Before changes. - const participantBefore = change.before - const dataBefore = participantBefore.data() - const { - contributionProgress: beforeContributionProgress, - status: beforeStatus, - contributionStep: beforeContributionStep - } = dataBefore - - // After changes. - const participantAfter = change.after - const dataAfter = participantAfter.data() - const { - contributionProgress: afterContributionProgress, - status: afterStatus, - contributionStep: afterContributionStep - } = dataAfter - - // Get the ceremony identifier (this does not change from before/after). - const ceremonyId = participantBefore.ref.parent.parent!.path - - if (!ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) - - logMsg(`Coordinating participants for ceremony ${ceremonyId}`, MsgType.INFO) - - logMsg(`Participant document ${participantBefore.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantAfter.id} okay`, MsgType.DEBUG) - logMsg( - `Participant ${participantBefore.id} the status from ${beforeStatus} to ${afterStatus} and the contribution progress from ${beforeContributionProgress} to ${afterContributionProgress}`, - MsgType.INFO - ) - - // nb. existance checked above. - const circuitsPath = `${participantBefore.ref.parent.parent!.path}/${collections.circuits}` - - // When a participant changes is status to ready, is "ready" to become a contributor. - if (afterStatus === ParticipantStatus.READY) { - // When beforeContributionProgress === 0 is a new participant, when beforeContributionProgress === afterContributionProgress the participant is retrying. - if (beforeContributionProgress === 0 || beforeContributionProgress === afterContributionProgress) { - logMsg( - `Participant has status READY and before contribution progress ${beforeContributionProgress} is different from after contribution progress ${afterContributionProgress}`, - MsgType.INFO - ) - - // i -> k where i == 0 - // (participant newly created). We work only on circuit k. - const circuit = await getCircuitDocumentByPosition(circuitsPath, afterContributionProgress) - - logMsg(`Circuit document ${circuit.id} okay`, MsgType.DEBUG) - - // The circuit info (i.e., the queue) is useful only to check turns for contribution. - // The participant info is useful to really pass the baton (starting the contribution). - // So, the info on the circuit says "it's your turn" while the info on the participant says "okay, i'm ready/waiting etc.". - // The contribution progress number completes everything because indicates which circuit is involved. - await coordinate(circuit, participantAfter) - logMsg(`Circuit ${circuit.id} has been updated (waiting queue)`, MsgType.INFO) - } - - if (afterContributionProgress === beforeContributionProgress + 1 && beforeContributionProgress !== 0) { - logMsg( - `Participant has status READY and before contribution progress ${beforeContributionProgress} is different from before contribution progress ${afterContributionProgress}`, - MsgType.INFO - ) - - // i -> k where k === i + 1 - // (participant has already contributed to i and the contribution has been verified, - // participant now is ready to be put in line for contributing on k circuit). - - const afterCircuit = await getCircuitDocumentByPosition(circuitsPath, afterContributionProgress) - - // logMsg(`Circuit document ${beforeCircuit.id} okay`, MsgType.DEBUG) - logMsg(`Circuit document ${afterCircuit.id} okay`, MsgType.DEBUG) - - // Coordinate after circuit (update waiting queue). - await coordinate(afterCircuit, participantAfter) - logMsg(`After circuit ${afterCircuit.id} has been updated (waiting queue)`, MsgType.INFO) - } - } - - // The contributor has finished the contribution and the waiting queue for the circuit needs to be updated. - if ( - (afterStatus === ParticipantStatus.DONE && beforeStatus !== ParticipantStatus.DONE) || - (beforeContributionProgress === afterContributionProgress && - afterStatus === ParticipantStatus.CONTRIBUTED && - beforeStatus === ParticipantStatus.CONTRIBUTING && - beforeContributionStep === ParticipantContributionStep.VERIFYING && - afterContributionStep === ParticipantContributionStep.COMPLETED) - ) { - logMsg(`Participant has status DONE or has finished the contribution`, MsgType.INFO) - - // Update the last circuits waiting queue. - const beforeCircuit = await getCircuitDocumentByPosition(circuitsPath, beforeContributionProgress) - - logMsg(`Circuit document ${beforeCircuit.id} okay`, MsgType.DEBUG) - - // Coordinate before circuit (update waiting queue + pass the baton to the next). - await coordinate(beforeCircuit, participantAfter, ceremonyId) - logMsg( - `Before circuit ${beforeCircuit.id} has been updated (waiting queue + pass the baton to next)`, - MsgType.INFO - ) - } - }) - -/** - * Automate the contribution verification. - */ -export const verifycontribution = functionsV2.https.onCall( - { memory: "32GiB", cpu: 8, timeoutSeconds: 3600, retry: true, maxInstances: 1000 }, - async (request: functionsV2.https.CallableRequest<any>): Promise<any> => { - const verifyCloudFunctionTimer = new Timer({ label: "verifyCloudFunction" }) - verifyCloudFunctionTimer.start() - - if (!request.auth || (!request.auth.token.participant && !request.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!request.data.ceremonyId || !request.data.circuitId || !request.data.ghUsername || !request.data.bucketName) - logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get Storage. - const S3 = await getS3Client() - - // Get data. - const { ceremonyId, circuitId, ghUsername, bucketName } = request.data - const userId = request.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const circuitDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.circuits}`) - .doc(circuitId) - .get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!ceremonyDoc.exists || !circuitDoc.exists || !participantDoc.exists) - logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const circuitData = circuitDoc.data() - const participantData = participantDoc.data() - - if (!ceremonyData || !circuitData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Circuit document ${circuitDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - let valid = false - let verificationComputationTime = 0 - const fullContributionTime = 0 - - // Check if is the verification for ceremony finalization. - const finalize = ceremonyData?.state === CeremonyState.CLOSED && request.auth && request.auth.token.coordinator - - if (participantData?.status === ParticipantStatus.CONTRIBUTING || finalize) { - // Compute last zkey index. - const lastZkeyIndex = formatZkeyIndex(circuitData!.waitingQueue.completedContributions + 1) - - // Reconstruct transcript path. - const transcriptFilename = `${circuitData?.prefix}_${ - finalize - ? `${ghUsername}_final_verification_transcript.log` - : `${lastZkeyIndex}_${ghUsername}_verification_transcript.log` - }` - const transcriptStoragePath = `${collections.circuits}/${circuitData?.prefix}/${names.transcripts}/${transcriptFilename}` - const transcriptTempFilePath = path.join(os.tmpdir(), transcriptFilename) - - // Custom logger for verification transcript. - const transcriptLogger = winston.createLogger({ - level: "info", - format: winston.format.printf((log) => log.message), - transports: [ - // Write all logs with importance level of `info` to `transcript.json`. - new winston.transports.File({ - filename: transcriptTempFilePath, - level: "info" - }) - ] - }) - - transcriptLogger.info( - `${finalize ? `Final verification` : `Verification`} transcript for ${ - circuitData?.prefix - } circuit Phase 2 contribution.\n${ - finalize ? `Coordinator ` : `Contributor # ${Number(lastZkeyIndex)}` - } (${ghUsername})\n` - ) - - // Get storage paths. - const potStoragePath = `${names.pot}/${circuitData?.files.potFilename}` - const firstZkeyStoragePath = `${collections.circuits}/${circuitData?.prefix}/${collections.contributions}/${circuitData?.prefix}_00000.zkey` - const lastZkeyStoragePath = `${collections.circuits}/${circuitData?.prefix}/${collections.contributions}/${ - circuitData?.prefix - }_${finalize ? `final` : lastZkeyIndex}.zkey` - - // Temporary store files from bucket. - const { potFilename } = circuitData!.files - const firstZkeyFilename = `${circuitData?.prefix}_00000.zkey` - const lastZkeyFilename = `${circuitData?.prefix}_${finalize ? `final` : lastZkeyIndex}.zkey` - - const potTempFilePath = path.join(os.tmpdir(), potFilename) - const firstZkeyTempFilePath = path.join(os.tmpdir(), firstZkeyFilename) - const lastZkeyTempFilePath = path.join(os.tmpdir(), lastZkeyFilename) - - // Download from AWS S3 bucket. - await tempDownloadFromBucket(S3, bucketName, potStoragePath, potTempFilePath) - logMsg(`${potStoragePath} downloaded`, MsgType.DEBUG) - - await tempDownloadFromBucket(S3, bucketName, firstZkeyStoragePath, firstZkeyTempFilePath) - logMsg(`${firstZkeyStoragePath} downloaded`, MsgType.DEBUG) - - await tempDownloadFromBucket(S3, bucketName, lastZkeyStoragePath, lastZkeyTempFilePath) - logMsg(`${lastZkeyStoragePath} downloaded`, MsgType.DEBUG) - - logMsg(`Downloads from storage completed`, MsgType.INFO) - - // Verify contribution. - const verificationComputationTimer = new Timer({ label: "verificationComputation" }) - verificationComputationTimer.start() - - valid = await zKey.verifyFromInit(firstZkeyTempFilePath, potTempFilePath, lastZkeyTempFilePath, transcriptLogger) - - verificationComputationTimer.stop() - - verificationComputationTime = verificationComputationTimer.ms() - - // Compute blake2b hash before unlink. - const lastZkeyBuffer = fs.readFileSync(lastZkeyTempFilePath) - const lastZkeyBlake2bHash = blake.blake2bHex(lastZkeyBuffer) - - // Unlink folders. - fs.unlinkSync(potTempFilePath) - fs.unlinkSync(firstZkeyTempFilePath) - fs.unlinkSync(lastZkeyTempFilePath) - - logMsg(`Contribution is ${valid ? `valid` : `invalid`}`, MsgType.INFO) - logMsg(`Verification computation time ${verificationComputationTime} ms`, MsgType.INFO) - - // Update DB. - const batch = firestore.batch() - - // Contribution. - const contributionDoc = await firestore - .collection( - `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuitId}/${collections.contributions}` - ) - .doc() - .get() - - if (valid) { - // Sleep ~5 seconds to wait for verification transcription. - await sleep(5000) - - // Upload transcript (small file - multipart upload not required). - await uploadFileToBucket(S3, bucketName, transcriptStoragePath, transcriptTempFilePath) - - // Compute blake2b hash. - const transcriptBuffer = fs.readFileSync(transcriptTempFilePath) - const transcriptBlake2bHash = blake.blake2bHex(transcriptBuffer) - - fs.unlinkSync(transcriptTempFilePath) - - // Get contribution computation time. - const contributions = participantData?.contributions.filter( - (contribution: { hash: string; doc: string; computationTime: number }) => - !!contribution.hash && !!contribution.computationTime && !contribution.doc - ) - - if (contributions.length !== 1) - logMsg(`There should be only one contribution without a doc link`, MsgType.ERROR) - - const contributionComputationTime = contributions[0].computationTime - - // Update only when coordinator is finalizing the ceremony. - batch.create(contributionDoc.ref, { - participantId: participantDoc.id, - contributionComputationTime, - verificationComputationTime, - zkeyIndex: finalize ? `final` : lastZkeyIndex, - files: { - transcriptFilename, - lastZkeyFilename, - transcriptStoragePath, - lastZkeyStoragePath, - transcriptBlake2bHash, - lastZkeyBlake2bHash - }, - valid, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Batch: create contribution document`, MsgType.DEBUG) - - verifyCloudFunctionTimer.stop() - const verifyCloudFunctionTime = verifyCloudFunctionTimer.ms() - - if (!finalize) { - // Circuit. - const { completedContributions, failedContributions } = circuitData!.waitingQueue - const { - contributionComputation: avgContributionComputation, - fullContribution: avgFullContribution, - verifyCloudFunction: avgVerifyCloudFunction - } = circuitData!.avgTimings - - logMsg(`Current average full contribution (down + comp + up) time ${avgFullContribution} ms`, MsgType.INFO) - logMsg(`Current verify cloud function time ${avgVerifyCloudFunction} ms`, MsgType.INFO) - - // Calculate full contribution time. - const fullContributionTime = participantData?.verificationStartedAt - participantData?.contributionStartedAt - - // Update avg timings. - const newAvgContributionComputationTime = - avgContributionComputation > 0 - ? (avgContributionComputation + contributionComputationTime) / 2 - : contributionComputationTime - const newAvgFullContributionTime = - avgFullContribution > 0 ? (avgFullContribution + fullContributionTime) / 2 : fullContributionTime - const newAvgVerifyCloudFunctionTime = - avgVerifyCloudFunction > 0 - ? (avgVerifyCloudFunction + verifyCloudFunctionTime) / 2 - : verifyCloudFunctionTime - - logMsg(`New average contribution computation time ${newAvgContributionComputationTime} ms`, MsgType.INFO) - logMsg(`New average full contribution (down + comp + up) time ${newAvgFullContributionTime} ms`, MsgType.INFO) - logMsg(`New verify cloud function time ${newAvgVerifyCloudFunctionTime} ms`, MsgType.INFO) - - batch.update(circuitDoc.ref, { - avgTimings: { - contributionComputation: valid ? newAvgContributionComputationTime : contributionComputationTime, - fullContribution: valid ? newAvgFullContributionTime : fullContributionTime, - verifyCloudFunction: valid ? newAvgVerifyCloudFunctionTime : verifyCloudFunctionTime - }, - waitingQueue: { - ...circuitData?.waitingQueue, - completedContributions: valid ? completedContributions + 1 : completedContributions, - failedContributions: valid ? failedContributions : failedContributions + 1 - }, - lastUpdated: getCurrentServerTimestampInMillis() - }) - } - - logMsg(`Batch: update timings and waiting queue for circuit`, MsgType.DEBUG) - - await batch.commit() - } else { - // Delete invalid contribution from storage. - await deleteObject(S3, bucketName, lastZkeyStoragePath) - - // Unlink transcript temp file. - fs.unlinkSync(transcriptTempFilePath) - - // Create a new contribution doc without files. - batch.create(contributionDoc.ref, { - participantId: participantDoc.id, - verificationComputationTime, - zkeyIndex: finalize ? `final` : lastZkeyIndex, - valid, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Batch: create invalid contribution document`, MsgType.DEBUG) - - if (!finalize) { - const { failedContributions } = circuitData!.waitingQueue - - // Update the failed contributions. - batch.update(circuitDoc.ref, { - waitingQueue: { - ...circuitData?.waitingQueue, - failedContributions: failedContributions + 1 - }, - lastUpdated: getCurrentServerTimestampInMillis() - }) - } - logMsg(`Batch: update invalid contributions counter`, MsgType.DEBUG) - - await batch.commit() - } - } - - logMsg( - `Participant ${userId} has verified the contribution #${participantData?.contributionProgress}`, - MsgType.INFO - ) - logMsg(`Returned values: valid ${valid} - verificationComputationTime ${verificationComputationTime}`, MsgType.INFO) - - return { - valid, - fullContributionTime, - verifyCloudFunctionTime: verifyCloudFunctionTimer.ms() - } - } -) - -/** - * Update the participant document after a contribution. - */ -export const refreshParticipantAfterContributionVerification = functionsV1.firestore - .document( - `/${collections.ceremonies}/{ceremony}/${collections.circuits}/{circuit}/${collections.contributions}/{contributions}` - ) - .onCreate(async (doc: QueryDocumentSnapshot) => { - // Get DB. - const firestore = admin.firestore() - - // Get doc info. - const contributionId = doc.id - const contributionData = doc.data() - const ceremonyCircuitsCollectionPath = doc.ref.parent.parent?.parent?.path // == /ceremonies/{ceremony}/circuits/. - const ceremonyParticipantsCollectionPath = `${doc.ref.parent.parent?.parent?.parent?.path}/${collections.participants}` // == /ceremonies/{ceremony}/participants. - - if (!ceremonyCircuitsCollectionPath || !ceremonyParticipantsCollectionPath) - logMsg(GENERIC_ERRORS.GENERR_WRONG_PATHS, MsgType.ERROR) - - // Looks for documents. - const circuits = await firestore.collection(ceremonyCircuitsCollectionPath!).listDocuments() - const participantDoc = await firestore - .collection(ceremonyParticipantsCollectionPath) - .doc(contributionData.participantId) - .get() - - if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data. - const participantData = participantDoc.data() - - if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - const participantContributions = participantData?.contributions - - // Update the only one contribution with missing doc (i.e., the last one). - participantContributions.forEach( - (participantContribution: { hash: string; doc: string; computationTime: number }) => { - if ( - !!participantContribution.hash && - !!participantContribution.computationTime && - !participantContribution.doc - ) { - participantContribution.doc = contributionId - } - } - ) - - // Don't update the participant status and progress when finalizing. - if (participantData!.status !== ParticipantStatus.FINALIZING) { - const newStatus = - participantData!.contributionProgress + 1 > circuits.length - ? ParticipantStatus.DONE - : ParticipantStatus.CONTRIBUTED - - await firestore.collection(ceremonyParticipantsCollectionPath).doc(contributionData.participantId).set( - { - status: newStatus, - contributionStep: ParticipantContributionStep.COMPLETED, - contributions: participantContributions, - tempContributionData: FieldValue.delete(), - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - - logMsg(`Participant ${contributionData.participantId} updated after contribution`, MsgType.DEBUG) - } else { - await firestore.collection(ceremonyParticipantsCollectionPath).doc(contributionData.participantId).set( - { - contributions: participantContributions, - lastUpdated: getCurrentServerTimestampInMillis() - }, - { merge: true } - ) - - logMsg(`Coordinator ${contributionData.participantId} updated after final contribution`, MsgType.DEBUG) - } - }) - -/** - * Make the progress to next contribution after successfully verified the contribution. - */ -export const makeProgressToNextContribution = functionsV1.https.onCall( - async (data: any, context: functionsV1.https.CallableContext): Promise<any> => { - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!ceremonyDoc.exists || !participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const participantData = participantDoc.data() - - if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - const { contributionProgress, contributionStep, status } = participantData! - - // Check for contribution completion here. - if (contributionStep !== ParticipantContributionStep.COMPLETED && status !== ParticipantStatus.WAITING) - logMsg(`Cannot progress!`, MsgType.ERROR) - - await participantDoc.ref.update({ - contributionProgress: contributionProgress + 1, - status: ParticipantStatus.READY, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Participant ${userId} progressed to ${contributionProgress + 1}`, MsgType.DEBUG) - } -) - -/** - * Resume a contribution after a timeout expiration. - */ -export const resumeContributionAfterTimeoutExpiration = functionsV1.https.onCall( - async (data: any, context: functionsV1.https.CallableContext): Promise<any> => { - if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) - logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) - - if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) - - // Get DB. - const firestore = admin.firestore() - - // Get data. - const { ceremonyId } = data - const userId = context.auth?.uid - - // Look for documents. - const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(userId!) - .get() - - if (!ceremonyDoc.exists || !participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) - - // Get data from docs. - const ceremonyData = ceremonyDoc.data() - const participantData = participantDoc.data() - - if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) - - logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) - logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) - - const { contributionProgress, status } = participantData! - - // Check if can resume. - if (status !== ParticipantStatus.EXHUMED) - logMsg(`Cannot resume the contribution after a timeout expiration`, MsgType.ERROR) - - await participantDoc.ref.update({ - status: ParticipantStatus.READY, - lastUpdated: getCurrentServerTimestampInMillis() - }) - - logMsg(`Participant ${userId} has resumed the contribution for circuit ${contributionProgress}`, MsgType.DEBUG) - } -) diff --git a/apps/backend/src/lib/constants.ts b/apps/backend/src/lib/constants.ts deleted file mode 100644 index cac1337b..00000000 --- a/apps/backend/src/lib/constants.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** Firebase */ -export const collections = { - users: "users", - participants: "participants", - ceremonies: "ceremonies", - circuits: "circuits", - contributions: "contributions", - timeouts: "timeouts" -} - -export const names = { - output: `output`, - setup: `setup`, - contribute: `contribute`, - pot: `pot`, - zkeys: `zkeys`, - metadata: `metadata`, - transcripts: `transcripts`, - attestation: `attestation` -} - -export const ceremoniesCollectionFields = { - coordinatorId: "coordinatorId", - description: "description", - endDate: "endDate", - lastUpdated: "lastUpdated", - prefix: "prefix", - startDate: "startDate", - state: "state", - title: "title", - type: "type" -} - -export const contributionsCollectionFields = { - contributionTime: "contributionTime", - files: "files", - lastUpdated: "lastUpdated", - participantId: "participantId", - valid: "valid", - verificationTime: "verificationTime", - zkeyIndex: "zKeyIndex" -} - -export const timeoutsCollectionFields = { - endDate: "endDate", - startDate: "startDate" -} diff --git a/apps/backend/src/lib/logs.ts b/apps/backend/src/lib/logs.ts deleted file mode 100644 index 92b69789..00000000 --- a/apps/backend/src/lib/logs.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as functions from "firebase-functions" -import { MsgType } from "../../types/index.js" - -export const GENERIC_ERRORS = { - GENERR_MISSING_INPUT: `You have not provided all the necessary data`, - GENERR_NO_AUTH_USER_FOUND: `The given id does not belong to an authenticated user`, - GENERR_NO_COORDINATOR: `The given id does not belong to a coordinator`, - GENERR_NO_CEREMONY_PROVIDED: `No ceremony has been provided`, - GENERR_NO_CIRCUIT_PROVIDED: `No circuit has been provided`, - GENERR_NO_CEREMONIES_OPENED: `No ceremonies are opened to contributions`, - GENERR_INVALID_CEREMONY: `The given ceremony is invalid`, - GENERR_INVALID_CIRCUIT: `The given circuit is invalid`, - GENERR_INVALID_PARTICIPANT: `The given participant is invalid`, - GENERR_CEREMONY_NOT_OPENED: `The given ceremony is not opened to contributions`, - GENERR_CEREMONY_NOT_CLOSED: `The given ceremony is not closed for finalization`, - GENERR_INVALID_PARTICIPANT_STATUS: `The participant has an invalid status`, - GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP: `The participant has an invalid contribution step`, - GENERR_INVALID_CONTRIBUTION_PROGRESS: `The contribution progress is invalid`, - GENERR_INVALID_DOCUMENTS: `One or more provided identifier does not belong to a document`, - GENERR_NO_DATA: `Data not found`, - GENERR_NO_CIRCUIT: `Circuits not found`, - GENERR_NO_PARTICIPANT: `Participant not found`, - GENERR_NO_CONTRIBUTION: `Contributions not found`, - GENERR_NO_CURRENT_CONTRIBUTOR: `There is no current contributor for the circuit`, - GENERR_NO_TIMEOUT_FIRST_COTRIBUTOR: `Cannot compute a dynamic timeout for the first contributor`, - GENERR_NO_CIRCUITS: `Circuits not found for the ceremony`, - GENERR_NO_CONTRIBUTIONS: `Contributions not found for the circuit`, - GENERR_NO_RETRY: `The retry waiting time has not passed away yet`, - GENERR_WRONG_PATHS: `Wrong storage or database paths`, - GENERR_WRONG_FIELD: `Wrong document field`, - GENERR_WRONG_ENV_CONFIGURATION: `Your environment variables are not configured properly` -} - -export const GENERIC_LOGS = { - GENLOG_NO_CEREMONIES_READY_TO_BE_OPENED: `There are no cerimonies ready to be opened to contributions`, - GENLOG_NO_CEREMONIES_READY_TO_BE_CLOSED: `There are no cerimonies ready to be closed`, - GENLOG_NO_CURRENT_CONTRIBUTOR: `There is no current contributor for the circuit`, - GENLOG_NO_TIMEOUT: `The timeout must not be triggered yet` -} - -/** - * Print a message customizing the default logger. - * @param msg <string> - the msg to be shown. - * @param msgType <MsgType> - the type of the message (e.g., debug, error). - */ -export const logMsg = (msg: string, msgType: MsgType) => { - switch (msgType) { - case MsgType.INFO: - functions.logger.info(`[INFO] ${msg}`) - break - case MsgType.DEBUG: - functions.logger.debug(`[DEBUG] ${msg}`) - break - case MsgType.WARN: - functions.logger.warn(`[WARN] ${msg}`) - break - case MsgType.ERROR: { - functions.logger.error(`[ERROR] ${msg}`) - process.exit(0) - } - // eslint-disable-next-line - case MsgType.LOG: - functions.logger.log(`[LOG] ${msg}`) - break - default: - console.log(`[LOG] ${msg}`) - break - } -} diff --git a/apps/backend/src/lib/utils.ts b/apps/backend/src/lib/utils.ts deleted file mode 100644 index 9d04950e..00000000 --- a/apps/backend/src/lib/utils.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { DocumentData, DocumentSnapshot, Timestamp, WhereFilterOp } from "firebase-admin/firestore" -import admin from "firebase-admin" -import * as functions from "firebase-functions" -import dotenv from "dotenv" -import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3" -import { getSignedUrl } from "@aws-sdk/s3-request-presigner" -import { createWriteStream } from "node:fs" -import { pipeline } from "node:stream" -import { promisify } from "node:util" -import { readFileSync } from "fs" -import fetch from "node-fetch" -import mime from "mime-types" -import { GENERIC_ERRORS, logMsg } from "./logs.js" -import { ceremoniesCollectionFields, collections, timeoutsCollectionFields } from "./constants.js" -import { CeremonyState, MsgType } from "../../types/index.js" - -dotenv.config() - -/** - * Return the current server timestamp in milliseconds. - * @returns <number> - */ -export const getCurrentServerTimestampInMillis = (): number => Timestamp.now().toMillis() - -/** - * Query ceremonies by state and (start/end) date value. - * @param state <CeremonyState> - the value of the state to be queried. - * @param dateField <string> - the start or end date field. - * @param check <WhereFilerOp> - the query filter (where check). - * @returns <Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>>> - */ -export const queryCeremoniesByStateAndDate = async ( - state: CeremonyState, - dateField: string, - check: WhereFilterOp -): Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>> => { - // Get DB. - const firestoreDb = admin.firestore() - - if (dateField !== ceremoniesCollectionFields.startDate && dateField !== ceremoniesCollectionFields.endDate) - logMsg(GENERIC_ERRORS.GENERR_WRONG_FIELD, MsgType.ERROR) - - return firestoreDb - .collection(collections.ceremonies) - .where(ceremoniesCollectionFields.state, "==", state) - .where(dateField, check, getCurrentServerTimestampInMillis()) - .get() -} - -/** - * Query timeouts by (start/end) date value. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param participantId <string> - the unique identifier of the participant. - * @param dateField <string> - the name of the date field. - * @returns <Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>>> - */ -export const queryValidTimeoutsByDate = async ( - ceremonyId: string, - participantId: string, - dateField: string -): Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>> => { - // Get DB. - const firestoreDb = admin.firestore() - - if (dateField !== timeoutsCollectionFields.startDate && dateField !== timeoutsCollectionFields.endDate) - logMsg(GENERIC_ERRORS.GENERR_WRONG_FIELD, MsgType.ERROR) - - return firestoreDb - .collection( - `${collections.ceremonies}/${ceremonyId}/${collections.participants}/${participantId}/${collections.timeouts}` - ) - .where(dateField, ">=", getCurrentServerTimestampInMillis()) - .get() -} - -/** - * Return the document belonging to a participant with a specified id (if exist). - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param participantId <string> - the unique identifier of the participant. - * @returns <Promise<DocumentSnapshot<DocumentData>>> - */ -export const getParticipantById = async ( - ceremonyId: string, - participantId: string -): Promise<DocumentSnapshot<DocumentData>> => { - // Get DB. - const firestore = admin.firestore() - - const participantDoc = await firestore - .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) - .doc(participantId) - .get() - - if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_NO_PARTICIPANT, MsgType.ERROR) - - return participantDoc -} - -/** - * Return all circuits for a given ceremony (if any). - * @param circuitsPath <string> - the collection path from ceremonies to circuits. - * @returns Promise<Array<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>>> - */ -export const getCeremonyCircuits = async ( - circuitsPath: string -): Promise<Array<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>>> => { - // Get DB. - const firestore = admin.firestore() - - // Query for all docs. - const circuitsQuerySnap = await firestore.collection(circuitsPath).get() - const circuitDocs = circuitsQuerySnap.docs - - if (!circuitDocs) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUITS, MsgType.ERROR) - - return circuitDocs -} - -/** - * Format the next zkey index. - * @param progress <number> - the progression in zkey index (= contributions). - * @returns <string> - */ -export const formatZkeyIndex = (progress: number): string => { - if (!process.env.FIRST_ZKEY_INDEX) logMsg(GENERIC_ERRORS.GENERR_WRONG_ENV_CONFIGURATION, MsgType.ERROR) - - const initialZkeyIndex = process.env.FIRST_ZKEY_INDEX! - - let index = progress.toString() - - while (index.length < initialZkeyIndex.length) { - index = `0${index}` - } - - return index -} - -/** - * Get the document for the circuit of the ceremony with a given sequence position. - * @param circuitsPath <string> - the collection path from ceremonies to circuits. - * @param position <number> - the sequence position of the circuit. - * @returns Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> - */ -export const getCircuitDocumentByPosition = async ( - circuitsPath: string, - position: number -): Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> => { - // Query for all circuit docs. - const circuitDocs = await getCeremonyCircuits(circuitsPath) - - // Filter by position. - const filteredCircuits = circuitDocs.filter( - (circuit: admin.firestore.DocumentData) => circuit.data().sequencePosition === position - ) - - if (!filteredCircuits) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUIT, MsgType.ERROR) - - // Get the circuit (nb. there will be only one circuit w/ that position). - const circuit = filteredCircuits.at(0) - - if (!circuit) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUIT, MsgType.ERROR) - - functions.logger.info(`Circuit w/ UID ${circuit?.id} at position ${position}`) - - return circuit! -} - -/** - * Get the final contribution document for a specific circuit. - * @param contributionsPath <string> - the collection path from circuit to contributions. - * @returns Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> - */ -export const getFinalContributionDocument = async ( - contributionsPath: string -): Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> => { - // Get DB. - const firestore = admin.firestore() - - // Query for all contribution docs for circuit. - const contributionsQuerySnap = await firestore.collection(contributionsPath).get() - const contributionsDocs = contributionsQuerySnap.docs - - if (!contributionsDocs) logMsg(GENERIC_ERRORS.GENERR_NO_CONTRIBUTIONS, MsgType.ERROR) - - // Filter by index. - const filteredContributions = contributionsDocs.filter( - (contribution: admin.firestore.DocumentData) => contribution.data().zkeyIndex === "final" - ) - - if (!filteredContributions) logMsg(GENERIC_ERRORS.GENERR_NO_CONTRIBUTION, MsgType.ERROR) - - // Get the contribution (nb. there will be only one final contribution). - const finalContribution = filteredContributions.at(0) - - if (!finalContribution) logMsg(GENERIC_ERRORS.GENERR_NO_CONTRIBUTION, MsgType.ERROR) - - return finalContribution! -} - -/** - * Return a new instance of the AWS S3 Client. - * @returns <Promise<S3Client> - */ -export const getS3Client = async (): Promise<S3Client> => { - if ( - !process.env.AWS_ACCESS_KEY_ID || - !process.env.AWS_SECRET_ACCESS_KEY || - !process.env.AWS_REGION || - !process.env.AWS_PRESIGNED_URL_EXPIRATION - ) - logMsg(GENERIC_ERRORS.GENERR_WRONG_ENV_CONFIGURATION, MsgType.ERROR) - - // Connect w/ S3. - return new S3Client({ - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID!, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY! - }, - region: process.env.AWS_REGION! - }) -} - -/** - * Downloads and temporarily write a file from S3 bucket. - * @param client <S3Client> - the AWS S3 client. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the location of the object in the AWS S3 bucket. - * @param tempFilePath <string> - the local path where the file will be written. - */ -export const tempDownloadFromBucket = async ( - client: S3Client, - bucketName: string, - objectKey: string, - tempFilePath: string -) => { - // Prepare get object command. - const command = new GetObjectCommand({ Bucket: bucketName, Key: objectKey }) - - // Get pre-signed url. - const url = await getSignedUrl(client, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) }) - - // Download the file. - const response: any = await fetch(url, { - method: "GET", - headers: { - "Access-Control-Allow-Origin": "*" - } - }) - - if (!response.ok) - logMsg(`Something went wrong when downloading the file from the bucket: ${response.statusText}`, MsgType.ERROR) - - // Temporarily write the file. - const streamPipeline = promisify(pipeline) - await streamPipeline(response.body!, createWriteStream(tempFilePath)) -} - -/** - * Sleeps the function execution for given millis. - * @dev to be used in combination with loggers when writing data into files. - * @param ms <number> - sleep amount in milliseconds - * @returns <Promise<unknown>> - */ -export const sleep = (ms: number): Promise<unknown> => new Promise((resolve) => setTimeout(resolve, ms)) - -/** - * Upload a file from S3 bucket. - * @param client <S3Client> - the AWS S3 client. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the location of the object in the AWS S3 bucket. - * @param tempFilePath <string> - the local path where the file will be written. - */ -export const uploadFileToBucket = async ( - client: S3Client, - bucketName: string, - objectKey: string, - tempFilePath: string -) => { - // Get file content type. - const contentType = mime.lookup(tempFilePath) || "" - - // Prepare command. - const command = new PutObjectCommand({ Bucket: bucketName, Key: objectKey, ContentType: contentType }) - - // Get pre-signed url. - const url = await getSignedUrl(client, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) }) - - // Make upload request (PUT). - const uploadTranscriptResponse = await fetch(url, { - method: "PUT", - body: readFileSync(tempFilePath), - headers: { "Content-Type": contentType } - }) - - // Check response. - if (!uploadTranscriptResponse.ok) - logMsg(`Something went wrong when uploading the transcript: ${uploadTranscriptResponse.statusText}`, MsgType.ERROR) - - logMsg(`File uploaded successfully`, MsgType.DEBUG) -} - -/** - * Delete a file from S3 bucket. - * @param client <S3Client> - the AWS S3 client. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the location of the object in the AWS S3 bucket. - */ -export const deleteObject = async (client: S3Client, bucketName: string, objectKey: string) => { - try { - // Prepare command. - const command = new DeleteObjectCommand({ Bucket: bucketName, Key: objectKey }) - - // Send command. - const data = await client.send(command) - - logMsg(`Object ${objectKey} successfully deleted: ${data.$metadata.httpStatusCode}`, MsgType.INFO) - } catch (error: any) { - logMsg(`Something went wrong while deleting the ${objectKey} object: ${error}`, MsgType.ERROR) - } -} diff --git a/apps/backend/test/index.test.ts b/apps/backend/test/index.test.ts deleted file mode 100644 index 19c8ebc5..00000000 --- a/apps/backend/test/index.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import chai from "chai" -import chaiAsPromised from "chai-as-promised" -import admin from "firebase-admin" -import firebaseFncTest from "firebase-functions-test" -// Import the exported function definitions from our functions/index.js file -import { registerAuthUser } from "../src/functions/index.js" - -// Config chai. -chai.use(chaiAsPromised) -const { expect } = chai - -// Initialize the firebase-functions-test SDK using environment variables. -// These variables are automatically set by firebase emulators:exec -// -// This configuration will be used to initialize the Firebase Admin SDK, so -// when we use the Admin SDK in the tests below we can be confident it will -// communicate with the emulators, not production. -const test = firebaseFncTest({ - databaseURL: process.env.FIREBASE_FIRESTORE_DATABASE_URL, - storageBucket: process.env.FIREBASE_STORAGE_BUCKET -}) - -describe("Unit tests", () => { - afterAll(() => { - test.cleanup() - }) - - it("tests an Auth function that interacts with Firestore", async () => { - const wrapped = test.wrap(registerAuthUser) - - // Make a fake user to pass to the function - const uid = `${new Date().getTime()}` - const displayName = "UserA" - const email = `user-${uid}@example.com` - const photoURL = `https://www...."` - - const user = test.auth.makeUserRecord({ - uid, - displayName, - email, - photoURL - }) - - // Call the function - await wrapped(user) - - // Check the data was written to the Firestore emulator - const snap = await admin.firestore().collection("users").doc(uid).get() - const data = snap.data() - - expect(data?.name).to.eql(displayName) - expect(data?.email).to.eql(email) - expect(data?.photoURL).to.eql(photoURL) - }) - - it("should reject an Auth function when called without an authenticated user", async () => { - const wrapped = test.wrap(registerAuthUser) - - // Call the function - await expect(wrapped).to.be.rejectedWith(Error) - }) -}) diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json deleted file mode 100644 index a257b9e4..00000000 --- a/apps/backend/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist/", - "declarationDir": "dist/types" - }, - "include": ["src/**/*", "test/**/*", "types/**/*"], - "exclude": ["node_modules", "tsconfig.json", "dist/**/*"] -} diff --git a/apps/backend/types/index.ts b/apps/backend/types/index.ts deleted file mode 100644 index 8f78b742..00000000 --- a/apps/backend/types/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -export enum CeremonyState { - SCHEDULED = 1, - OPENED = 2, - PAUSED = 3, - CLOSED = 4, - FINALIZED = 5 -} - -export enum ParticipantStatus { - CREATED = 1, - WAITING = 2, - READY = 3, - CONTRIBUTING = 4, - CONTRIBUTED = 5, - DONE = 6, - FINALIZING = 7, - FINALIZED = 8, - TIMEDOUT = 9, - EXHUMED = 10 -} - -export enum ParticipantContributionStep { - DOWNLOADING = 1, - COMPUTING = 2, - UPLOADING = 3, - VERIFYING = 4, - COMPLETED = 5 -} - -export enum CeremonyType { - PHASE1 = 1, - PHASE2 = 2 -} - -export enum MsgType { - INFO = 1, - DEBUG = 2, - WARN = 3, - ERROR = 4, - LOG = 5 -} - -export enum RequestType { - PUT = 1, - GET = 2 -} - -export enum TimeoutType { - BLOCKING_CONTRIBUTION = 1, - BLOCKING_CLOUD_FUNCTION = 2 -} - -export enum CeremonyTimeoutType { - DYNAMIC = 1, - FIXED = 2 -} diff --git a/apps/backend/types/snarkjs.d.ts b/apps/backend/types/snarkjs.d.ts deleted file mode 100644 index ab419e76..00000000 --- a/apps/backend/types/snarkjs.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** Declaration file generated by dts-gen */ - -declare module "snarkjs" { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - export = snarkjs - - declare const snarkjs: { - groth16: { - exportSolidityCallData: any - fullProve: any - prove: any - verify: any - } - plonk: { - exportSolidityCallData: any - fullProve: any - prove: any - setup: any - verify: any - } - powersOfTau: { - beacon: any - challengeContribute: any - contribute: any - convert: any - exportChallenge: any - exportJson: any - importResponse: any - newAccumulator: any - preparePhase2: any - truncate: any - verify: any - } - r1cs: { - exportJson: any - info: any - print: any - } - wtns: { - calculate: any - debug: any - exportJson: any - } - zKey: { - beacon: any - bellmanContribute: any - contribute: any - exportBellman: any - exportJson: any - exportSolidityVerifier: any - exportVerificationKey: any - importBellman: any - newZKey: any - verifyFromInit: any - verifyFromR1cs: any - } - } -} diff --git a/apps/phase2cli/env.json.default b/apps/phase2cli/env.json.default deleted file mode 100644 index 2ea6473d..00000000 --- a/apps/phase2cli/env.json.default +++ /dev/null @@ -1,28 +0,0 @@ -{ - "firebase": { - "FIREBASE_API_KEY": "your-firebase-api-key", - "FIREBASE_AUTH_DOMAIN": "your-firebase-auth-domain", - "FIREBASE_PROJECT_ID": "your-firebase-project-id", - "FIREBASE_STORAGE_BUCKET": "your-firebase-storage-bucket", - "FIREBASE_MESSAGING_SENDER_ID": "your-firebase-messaging-sender-id", - "FIREBASE_APP_ID": "your-firebase-app-id", - "FIREBASE_CF_URL_VERIFY_CONTRIBUTION: "your-verify-contribution-url" - }, - "github": { - "GITHUB_CLIENT_ID": "your-github-oauth-app-client-id" - }, - "localPaths": { - "LOCAL_PATH_DIR_CIRCUITS_R1CS": "./circuits/r1cs", - "LOCAL_PATH_DIR_CIRCUITS_METADATA": "./circuits/metadata", - "LOCAL_PATH_DIR_ZKEYS": "./zkeys", - "LOCAL_PATH_DIR_PTAU": "./circuits/ptau" - }, - "others": { - "FIRST_ZKEY_INDEX": "00000" - }, - "config": { - "CONFIG_CEREMONY_BUCKET_POSTFIX": "-phase2-ceremony-contributions", - "CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS": "900", - "CONFIG_STREAM_CHUNK_SIZE_IN_MB": "512" - } -} diff --git a/apps/phase2cli/package.json b/apps/phase2cli/package.json deleted file mode 100644 index 65dcb11e..00000000 --- a/apps/phase2cli/package.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "name": "@zkmpc/phase2cli", - "version": "0.0.2", - "description": "All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies", - "main": "dist/src/index.js", - "repository": "https://github.com/quadratic-funding/mpc-phase2-suite/cli", - "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", - "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", - "author": { - "name": "Giacomo (0xjei)" - }, - "license": "MIT", - "private": false, - "type": "module", - "types": "dist/types/index.d.ts", - "files": [ - "dist/", - "src/", - "README.md" - ], - "keywords": [ - "typescript", - "zero-knowledge", - "zk-snarks", - "phase-2", - "trusted-setup", - "ceremony", - "snarkjs", - "circom" - ], - "bin": { - "phase2cli": "dist/src/index.js" - }, - "scripts": { - "build": "tsc", - "start": "node dist/src/index.js", - "auth": "yarn start auth", - "contribute": "yarn start contribute", - "clean": "yarn start clean", - "logout": "yarn start logout", - "coordinate:setup": "yarn start coordinate setup", - "coordinate:observe": "yarn start coordinate observe", - "coordinate:finalize": "yarn start coordinate finalize" - }, - "devDependencies": { - "@types/clear": "^0.1.2", - "@types/cli-progress": "^3.11.0", - "@types/conf": "^3.0.0", - "@types/figlet": "^1.5.4", - "@types/mime-types": "^2.1.1", - "@types/node-emoji": "^1.8.1", - "@types/node-fetch": "^2.6.2", - "@types/ora": "^3.2.0", - "@types/prompts": "^2.0.14", - "@types/winston": "^2.4.4", - "typescript": "^4.7.4" - }, - "dependencies": { - "@adobe/node-fetch-retry": "^2.2.0", - "@octokit/auth-oauth-app": "^5.0.1", - "@octokit/auth-oauth-device": "^4.0.0", - "@octokit/request": "^6.2.0", - "@zkmpc/actions": "^0.0.0", - "blakejs": "^1.2.1", - "boxen": "^7.0.0", - "chalk": "^5.0.1", - "clear": "^0.1.0", - "cli-progress": "^3.11.2", - "clipboardy": "^3.0.0", - "commander": "^9.4.0", - "conf": "^10.2.0", - "dotenv": "^16.0.1", - "figlet": "^1.5.2", - "firebase": "^9.9.1", - "log-symbols": "^5.1.0", - "mime-types": "^2.1.35", - "node-disk-info": "^1.3.0", - "node-emoji": "^1.11.0", - "node-fetch": "^3.2.10", - "open": "^8.4.0", - "ora": "^6.1.2", - "prompts": "^2.4.2", - "snarkjs": "^0.5.0", - "timer-node": "^5.0.6", - "winston": "^3.8.1" - } -} diff --git a/apps/phase2cli/src/commands/auth.ts b/apps/phase2cli/src/commands/auth.ts deleted file mode 100644 index 1cc6a538..00000000 --- a/apps/phase2cli/src/commands/auth.ts +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node - -import { getNewOAuthTokenUsingGithubDeviceFlow, signInToFirebaseWithGithubToken } from "@zkmpc/actions" -import { symbols, theme } from "../lib/constants.js" -import { GITHUB_ERRORS, handleAuthErrors, showError } from "../lib/errors.js" -import { bootstrapCommandExec, getGithubUsername, terminate } from "../lib/utils.js" -import { readLocalJsonFile } from "../lib/files.js" -import { getStoredOAuthToken, hasStoredOAuthToken, setStoredOAuthToken } from "../lib/auth.js" - -// Get local configs. -const { github } = readLocalJsonFile("../../env.json") - -/** - * Look for the Github 2.0 OAuth token in the local storage if present; otherwise manage the request for a new token. - * @returns <Promise<string>> - */ -const handleGithubToken = async (): Promise<string> => { - let token: string - - if (hasStoredOAuthToken()) - // Get stored token. - token = String(getStoredOAuthToken()) - else { - if (!github.GITHUB_CLIENT_ID) showError(GITHUB_ERRORS.GITHUB_NOT_CONFIGURED_PROPERLY, true) - - // Request a new token. - token = await getNewOAuthTokenUsingGithubDeviceFlow(github.GITHUB_CLIENT_ID) - - // Store the new token. - setStoredOAuthToken(token) - } - - return token -} - -/** - * Auth command. - * @dev TODO: add docs. - */ -const auth = async () => { - try { - const { firebaseApp } = await bootstrapCommandExec() - - if (!github.GITHUB_CLIENT_ID) showError(GITHUB_ERRORS.GITHUB_NOT_CONFIGURED_PROPERLY, true) - - // Manage OAuth Github token. - const token = await handleGithubToken() - - // Sign in with credentials. - await signInToFirebaseWithGithubToken(firebaseApp, token) - - // Get Github username. - const ghUsername = await getGithubUsername(token) - - console.log(`${symbols.success} You are authenticated as ${theme.bold(`@${ghUsername}`)}`) - console.log( - `${ - symbols.info - } You can now contribute to zk-SNARK Phase2 Trusted Setup running ceremonies by running ${theme.bold( - theme.italic(`phase2cli contribute`) - )} command` - ) - - terminate(ghUsername) - } catch (err: any) { - handleAuthErrors(err) - } -} - -export default auth diff --git a/apps/phase2cli/src/commands/clean.ts b/apps/phase2cli/src/commands/clean.ts deleted file mode 100644 index 235c544c..00000000 --- a/apps/phase2cli/src/commands/clean.ts +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env node - -import { emojis, paths, symbols, theme } from "../lib/constants.js" -import { showError } from "../lib/errors.js" -import { deleteDir, directoryExists } from "../lib/files.js" -import { askForConfirmation } from "../lib/prompts.js" -import { bootstrapCommandExec, customSpinner, sleep } from "../lib/utils.js" - -/** - * Clean command. - */ -const clean = async () => { - try { - // Initialize services. - await bootstrapCommandExec() - - const spinner = customSpinner(`Cleaning up...`, "clock") - - if (directoryExists(paths.outputPath)) { - console.log(theme.bold(`${symbols.warning} Be careful, this action is irreversible!`)) - - const { confirmation } = await askForConfirmation( - "Are you sure you want to continue with the clean up?", - "Yes", - "No" - ) - - if (confirmation) { - spinner.start() - - // Do the clean up. - deleteDir(paths.outputPath) - - // nb. simulate waiting time for 1s. - await sleep(1000) - - spinner.succeed(`Cleanup was successfully completed ${emojis.broom}`) - } - } else { - console.log(`${symbols.info} There is nothing to clean ${emojis.eyes}`) - } - } catch (err: any) { - showError(`Something went wrong: ${err.toString()}`, true) - } -} - -export default clean diff --git a/apps/phase2cli/src/commands/contribute.ts b/apps/phase2cli/src/commands/contribute.ts deleted file mode 100644 index 01509c05..00000000 --- a/apps/phase2cli/src/commands/contribute.ts +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env node - -import { getOpenedCeremonies, getCeremonyCircuits } from "@zkmpc/actions" -import { httpsCallable } from "firebase/functions" -import { handleCurrentAuthUserSignIn } from "../lib/auth.js" -import { theme, emojis, collections, symbols, paths } from "../lib/constants.js" -import { askForCeremonySelection } from "../lib/prompts.js" -import { ParticipantContributionStep, ParticipantStatus } from "../../types/index.js" -import { - bootstrapCommandExec, - terminate, - handleTimedoutMessageForContributor, - getContributorContributionsVerificationResults, - customSpinner, - getEntropyOrBeacon, - simpleLoader -} from "../lib/utils.js" -import { getDocumentById } from "../lib/firebase.js" -import listenForContribution from "../lib/listeners.js" -import { FIREBASE_ERRORS, GENERIC_ERRORS, showError } from "../lib/errors.js" -import { checkAndMakeNewDirectoryIfNonexistent } from "../lib/files.js" - -/** - * Contribute command. - */ -const contribute = async () => { - try { - // Initialize services. - const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExec() - const checkParticipantForCeremony = httpsCallable(firebaseFunctions, "checkParticipantForCeremony") - - // Handle current authenticated user sign in. - const { user, token, username } = await handleCurrentAuthUserSignIn(firebaseApp) - - // Get running cerimonies info (if any). - const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase) - - if (runningCeremoniesDocs.length === 0) showError(FIREBASE_ERRORS.FIREBASE_CEREMONY_NOT_OPENED, true) - - console.log( - `${symbols.warning} ${theme.bold( - `The contribution process is based on a waiting queue mechanism (one contributor at a time) with an upper-bound time constraint per each contribution (does not restart if the process is halted for any reason).\n${symbols.info} Any contribution could take the bulk of your computational resources and memory based on the size of the circuit` - )} ${emojis.fire}\n` - ) - - // Ask to select a ceremony. - const ceremony = await askForCeremonySelection(runningCeremoniesDocs) - - // Get ceremony circuits. - const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) - const numberOfCircuits = circuits.length - - const spinner = customSpinner(`Checking eligibility...`, `clock`) - spinner.start() - - // Call Cloud Function for participant check and registration. - const { data: canParticipate } = await checkParticipantForCeremony({ ceremonyId: ceremony.id }) - - // Get participant document. - // To be moved (maybe helpers folder? w/ query?) - const participantDoc = await getDocumentById( - `${collections.ceremonies}/${ceremony.id}/${collections.participants}`, - user.uid - ) - - // Get updated data from snap. - const participantData = participantDoc.data() - - if (!participantData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Check if the user can take part of the waiting queue for contributing. - if (canParticipate) { - spinner.succeed(`You are eligible to contribute to the ceremony ${emojis.tada}\n`) - - // Check for output directory. - checkAndMakeNewDirectoryIfNonexistent(paths.outputPath) - checkAndMakeNewDirectoryIfNonexistent(paths.contributePath) - checkAndMakeNewDirectoryIfNonexistent(paths.contributionsPath) - checkAndMakeNewDirectoryIfNonexistent(paths.attestationPath) - checkAndMakeNewDirectoryIfNonexistent(paths.contributionTranscriptsPath) - - // Check if entropy is needed. - let entropy = "" - - if ( - (participantData?.contributionProgress === numberOfCircuits && - participantData?.contributionStep < ParticipantContributionStep.UPLOADING) || - participantData?.contributionProgress < numberOfCircuits - ) - entropy = await getEntropyOrBeacon(true) - - // Listen to circuits and participant document changes. - await listenForContribution( - participantDoc, - ceremony, - firestoreDatabase, - circuits, - firebaseFunctions, - token, - username, - entropy - ) - } else { - spinner.warn(`You are not eligible to contribute to the ceremony right now`) - - await handleTimedoutMessageForContributor(participantData!, participantDoc.id, ceremony.id, false, username) - } - - // Check if already contributed. - if ( - ((!canParticipate && participantData?.status === ParticipantStatus.DONE) || - participantData?.status === ParticipantStatus.FINALIZED) && - participantData?.contributions.length > 0 - ) { - spinner.fail(`You are not eligible to contribute to the ceremony\n`) - - await simpleLoader(`Checking for contributions...`, `clock`, 1500) - - // Return true and false based on contribution verification. - const contributionsValidity = await getContributorContributionsVerificationResults( - ceremony.id, - participantDoc.id, - circuits, - false - ) - const numberOfValidContributions = contributionsValidity.filter(Boolean).length - - if (numberOfValidContributions) { - console.log( - `Congrats, you have already contributed to ${theme.magenta( - theme.bold(numberOfValidContributions) - )} out of ${theme.magenta(theme.bold(numberOfCircuits))} circuits ${emojis.tada}` - ) - - // Show valid/invalid contributions per each circuit. - let idx = 0 - for (const contributionValidity of contributionsValidity) { - console.log( - `${contributionValidity ? symbols.success : symbols.error} ${theme.bold(`Circuit`)} ${theme.bold( - theme.magenta(idx + 1) - )}` - ) - idx += 1 - } - - console.log( - `\nWe wanna thank you for your participation in preserving the security for ${theme.bold( - ceremony.data.title - )} Trusted Setup ceremony ${emojis.pray}` - ) - } else - console.log( - `\nYou have not successfully contributed to any of the ${theme.bold( - theme.magenta(circuits.length) - )} circuits ${emojis.upsideDown}` - ) - - // Graceful exit. - terminate(username) - } - } catch (err: any) { - showError(`Something went wrong: ${err.toString()}`, true) - } -} - -export default contribute diff --git a/apps/phase2cli/src/commands/finalize.ts b/apps/phase2cli/src/commands/finalize.ts deleted file mode 100644 index 424072ef..00000000 --- a/apps/phase2cli/src/commands/finalize.ts +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env node -import crypto from "crypto" -import { zKey } from "snarkjs" -import open from "open" -import { getCeremonyCircuits } from "@zkmpc/actions" -import { httpsCallable } from "firebase/functions" -import { handleCurrentAuthUserSignIn, onlyCoordinator } from "../lib/auth.js" -import { collections, emojis, paths, solidityVersion, symbols, theme } from "../lib/constants.js" -import { GENERIC_ERRORS, showError } from "../lib/errors.js" -import { - checkAndMakeNewDirectoryIfNonexistent, - getLocalFilePath, - readFile, - writeFile, - writeLocalJsonFile -} from "../lib/files.js" -import { askForCeremonySelection } from "../lib/prompts.js" -import { getClosedCeremonies } from "../lib/queries.js" -import { - bootstrapCommandExec, - customSpinner, - getBucketName, - getContributorContributionsVerificationResults, - getEntropyOrBeacon, - getValidContributionAttestation, - makeContribution, - multiPartUpload, - publishGist, - sleep, - terminate -} from "../lib/utils.js" -import { getDocumentById } from "../lib/firebase.js" - -/** - * Finalize command. - */ -const finalize = async () => { - try { - // Initialize services. - const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExec() - - // Setup ceremony callable Cloud Function initialization. - const checkAndPrepareCoordinatorForFinalization = httpsCallable( - firebaseFunctions, - "checkAndPrepareCoordinatorForFinalization" - ) - const finalizeLastContribution = httpsCallable(firebaseFunctions, "finalizeLastContribution") - const finalizeCeremony = httpsCallable(firebaseFunctions, "finalizeCeremony") - - // Handle current authenticated user sign in. - const { user, token, username } = await handleCurrentAuthUserSignIn(firebaseApp) - - // Check custom claims for coordinator role. - await onlyCoordinator(user) - - // Get closed cerimonies info (if any). - const closedCeremoniesDocs = await getClosedCeremonies() - - console.log( - `${symbols.warning} The computation of the final contribution could take the bulk of your computational resources and memory based on the size of the circuit ${emojis.fire}\n` - ) - - // Ask to select a ceremony. - const ceremony = await askForCeremonySelection(closedCeremoniesDocs) - - // Get coordinator participant document. - const participantDoc = await getDocumentById( - `${collections.ceremonies}/${ceremony.id}/${collections.participants}`, - user.uid - ) - - const { data: canFinalize } = await checkAndPrepareCoordinatorForFinalization({ ceremonyId: ceremony.id }) - - if (!canFinalize) showError(`You are not able to finalize the ceremony`, true) - - // Clean directories. - checkAndMakeNewDirectoryIfNonexistent(paths.outputPath) - checkAndMakeNewDirectoryIfNonexistent(paths.finalizePath) - checkAndMakeNewDirectoryIfNonexistent(paths.finalZkeysPath) - checkAndMakeNewDirectoryIfNonexistent(paths.finalPotPath) - checkAndMakeNewDirectoryIfNonexistent(paths.finalAttestationsPath) - checkAndMakeNewDirectoryIfNonexistent(paths.verificationKeysPath) - checkAndMakeNewDirectoryIfNonexistent(paths.verifierContractsPath) - - // Handle random beacon request/generation. - const beacon = await getEntropyOrBeacon(false) - const beaconHashStr = crypto.createHash("sha256").update(beacon).digest("hex") - console.log(`${symbols.info} Your final beacon hash: ${theme.bold(beaconHashStr)}`) - - // Get ceremony circuits. - const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) - - // Attestation preamble. - const attestationPreamble = `Hey, I'm ${username} and I have finalized the ${ceremony.data.title} MPC Phase2 Trusted Setup ceremony.\nThe following are the finalization signatures:` - - // Finalize each circuit - for await (const circuit of circuits) { - await makeContribution(ceremony, circuit, beaconHashStr, username, true, firebaseFunctions) - - // 6. Export the verification key. - - // Paths config. - const finalZkeyLocalPath = `${paths.finalZkeysPath}/${circuit.data.prefix}_final.zkey` - const verificationKeyLocalPath = `${paths.verificationKeysPath}/${circuit.data.prefix}_vkey.json` - const verificationKeyStoragePath = `${collections.circuits}/${circuit.data.prefix}/${circuit.data.prefix}_vkey.json` - - const spinner = customSpinner(`Extracting verification key...`, "clock") - spinner.start() - - // Export vkey. - const verificationKeyJSONData = await zKey.exportVerificationKey(finalZkeyLocalPath) - - spinner.text = `Writing verification key locally...` - - // Write locally. - writeLocalJsonFile(verificationKeyLocalPath, verificationKeyJSONData) - - // nb. need to wait for closing the file descriptor. - await sleep(1500) - - // Upload vkey to storage. - const startMultiPartUpload = httpsCallable(firebaseFunctions, "startMultiPartUpload") - const generatePreSignedUrlsParts = httpsCallable(firebaseFunctions, "generatePreSignedUrlsParts") - const completeMultiPartUpload = httpsCallable(firebaseFunctions, "completeMultiPartUpload") - - const bucketName = getBucketName(ceremony.data.prefix) - - await multiPartUpload( - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload, - bucketName, - verificationKeyStoragePath, - verificationKeyLocalPath - ) - - spinner.succeed(`Verification key correctly stored`) - - // 7. Turn the verifier into a smart contract. - const verifierContractLocalPath = `${paths.verifierContractsPath}/${circuit.data.name}_verifier.sol` - const verifierContractStoragePath = `${collections.circuits}/${circuit.data.prefix}/${circuit.data.prefix}_verifier.sol` - - spinner.text = `Extracting verifier contract...` - spinner.start() - - // Export solidity verifier. - let verifierCode = await zKey.exportSolidityVerifier( - finalZkeyLocalPath, - { groth16: readFile(getLocalFilePath("../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs")) }, - console - ) - - // Update solidity version. - verifierCode = verifierCode.replace(/pragma solidity \^\d+\.\d+\.\d+/, `pragma solidity ^${solidityVersion}`) - - spinner.text = `Writing verifier contract locally...` - - // Write locally. - writeFile(verifierContractLocalPath, verifierCode) - - // nb. need to wait for closing the file descriptor. - await sleep(1500) - - // Upload vkey to storage. - await multiPartUpload( - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload, - bucketName, - verifierContractStoragePath, - verifierContractLocalPath - ) - spinner.succeed(`Verifier contract correctly stored`) - - spinner.text = `Finalizing circuit...` - spinner.start() - - // Finalize circuit contribution. - await finalizeLastContribution({ - ceremonyId: ceremony.id, - circuitId: circuit.id, - bucketName - }) - - spinner.succeed(`Circuit successfully finalized`) - } - - process.stdout.write(`\n`) - - const spinner = customSpinner(`Finalizing the ceremony...`, "clock") - spinner.start() - - // Setup ceremony on the server. - await finalizeCeremony({ - ceremonyId: ceremony.id - }) - - spinner.succeed( - `Congrats, you have correctly finalized the ${theme.bold(ceremony.data.title)} circuits ${emojis.tada}\n` - ) - - spinner.text = `Generating public finalization attestation...` - spinner.start() - - // Get updated participant data. - const participantData = participantDoc.data() - - if (!participantData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Return true and false based on contribution verification. - const contributionsValidity = await getContributorContributionsVerificationResults( - ceremony.id, - participantDoc.id, - circuits, - true - ) - - // Get only valid contribution hashes. - const attestation = await getValidContributionAttestation( - contributionsValidity, - circuits, - participantData!, - ceremony.id, - participantDoc.id, - attestationPreamble, - true - ) - - writeFile(`${paths.finalAttestationsPath}/${ceremony.data.prefix}_final_attestation.log`, Buffer.from(attestation)) - - // nb. wait for closing file descriptor. - await sleep(1000) - - spinner.text = `Uploading public finalization attestation as Github Gist...` - - const gistUrl = await publishGist(token, attestation, ceremony.data.prefix, ceremony.data.title) - - spinner.succeed( - `Public finalization attestation successfully published as Github Gist at this link ${theme.bold( - theme.underlined(gistUrl) - )}` - ) - - // Attestation link via Twitter. - const attestationTweet = `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremony.data.title}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE` - - console.log( - `\nYou can tweet about the ceremony finalization if you'd like (click on the link below ${ - emojis.pointDown - }) \n\n${theme.underlined(attestationTweet)}` - ) - - await open(attestationTweet) - - terminate(username) - } catch (err: any) { - showError(`Something went wrong: ${err.toString()}`, true) - } -} - -export default finalize diff --git a/apps/phase2cli/src/commands/index.ts b/apps/phase2cli/src/commands/index.ts deleted file mode 100644 index b60ad60c..00000000 --- a/apps/phase2cli/src/commands/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import setup from "./setup.js" -import auth from "./auth.js" -import contribute from "./contribute.js" -import observe from "./observe.js" -import finalize from "./finalize.js" -import clean from "./clean.js" -import logout from "./logout.js" - -export { setup, auth, contribute, observe, finalize, clean, logout } diff --git a/apps/phase2cli/src/commands/logout.ts b/apps/phase2cli/src/commands/logout.ts deleted file mode 100644 index f666fb22..00000000 --- a/apps/phase2cli/src/commands/logout.ts +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env node - -import { getAuth, signOut } from "firebase/auth" -import { deleteStoredOAuthToken, handleCurrentAuthUserSignIn } from "../lib/auth.js" -import { emojis, symbols, theme } from "../lib/constants.js" -import { showError } from "../lib/errors.js" -import { askForConfirmation } from "../lib/prompts.js" -import { bootstrapCommandExec, customSpinner } from "../lib/utils.js" - -/** - * Logout command. - */ -const logout = async () => { - try { - // Initialize services. - const { firebaseApp } = await bootstrapCommandExec() - - // Handle current authenticated user sign in. - await handleCurrentAuthUserSignIn(firebaseApp) - - // Inform the user about deassociation in Github and re run auth - console.log( - `${symbols.warning} We do not use any Github access token for authentication; thus we cannot revoke the authorization from your Github account for this CLI application` - ) - console.log( - `${symbols.info} You can do this manually as reported in the official Github documentation ${ - emojis.pointDown - }\n\n${theme.bold( - theme.underlined( - `https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/reviewing-your-authorized-applications-oauth` - ) - )}\n` - ) - - // Ask for confirmation. - const { confirmation } = await askForConfirmation("Are you sure you want to log out?", "Yes", "No") - - if (confirmation) { - const spinner = customSpinner(`Logging out...`, "clock") - spinner.start() - - // Sign out. - const auth = getAuth() - await signOut(auth) - - // Delete local token. - deleteStoredOAuthToken() - - spinner.stop() - console.log(`${symbols.success} Logout successfully completed ${emojis.wave}`) - } - } catch (err: any) { - showError(`Something went wrong: ${err.toString()}`, true) - } -} - -export default logout diff --git a/apps/phase2cli/src/commands/observe.ts b/apps/phase2cli/src/commands/observe.ts deleted file mode 100644 index 2551ecaa..00000000 --- a/apps/phase2cli/src/commands/observe.ts +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env node - -import readline from "readline" -import logSymbols from "log-symbols" -import { getOpenedCeremonies, getCeremonyCircuits } from "@zkmpc/actions" -import { FirebaseDocumentInfo } from "../../types/index.js" -import { onlyCoordinator, handleCurrentAuthUserSignIn } from "../lib/auth.js" -import { - bootstrapCommandExec, - convertToDoubleDigits, - customSpinner, - getSecondsMinutesHoursFromMillis, - sleep -} from "../lib/utils.js" -import { askForCeremonySelection } from "../lib/prompts.js" -import { getCurrentContributorContribution } from "../lib/queries.js" -import { GENERIC_ERRORS, showError } from "../lib/errors.js" -import { theme, emojis, symbols, observationWaitingTimeInMillis } from "../lib/constants.js" - -/** - * Clean cursor lines from current position back to root (default: zero). - * @param currentCursorPos - the current position of the cursor. - * @returns <number> - */ -const cleanCursorPosBackToRoot = (currentCursorPos: number) => { - while (currentCursorPos < 0) { - // Get back and clean line by line. - readline.cursorTo(process.stdout, 0) - readline.clearLine(process.stdout, 0) - readline.moveCursor(process.stdout, -1, -1) - - currentCursorPos += 1 - } - - return currentCursorPos -} - -/** - * Show the latest updates for the given circuit. - * @param ceremony <FirebaseDocumentInfo> - the Firebase document containing info about the ceremony. - * @param circuit <FirebaseDocumentInfo> - the Firebase document containing info about the circuit. - * @returns Promise<number> return the current position of the cursor (i.e., number of lines displayed). - */ -const displayLatestCircuitUpdates = async ( - ceremony: FirebaseDocumentInfo, - circuit: FirebaseDocumentInfo -): Promise<number> => { - let observation = theme.bold(`- Circuit # ${theme.magenta(circuit.data.sequencePosition)}`) // Observation output. - let cursorPos = -1 // Current cursor position (nb. decrease every time there's a new line!). - - const { waitingQueue } = circuit.data - - // Get info from circuit. - const { currentContributor } = waitingQueue - const { completedContributions } = waitingQueue - - if (!currentContributor) { - observation += `\n> Nobody's currently waiting to contribute ${emojis.eyes}` - cursorPos -= 1 - } else { - // Search for currentContributor' contribution. - const contributions = await getCurrentContributorContribution(ceremony.id, circuit.id, currentContributor) - - if (!contributions.length) { - // The contributor is currently contributing. - observation += `\n> Participant ${theme.bold(`#${completedContributions + 1}`)} (${theme.bold( - currentContributor - )}) is currently contributing ${emojis.fire}` - - cursorPos -= 1 - } else { - // The contributor has contributed. - observation += `\n> Participant ${theme.bold(`#${completedContributions}`)} (${theme.bold( - currentContributor - )}) has completed the contribution ${emojis.tada}` - - cursorPos -= 1 - - // The contributor has finished the contribution. - const contributionData = contributions.at(0)?.data - - if (!contributionData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Convert times to seconds. - const { - seconds: contributionTimeSeconds, - minutes: contributionTimeMinutes, - hours: contributionTimeHours - } = getSecondsMinutesHoursFromMillis(contributionData?.contributionTime) - const { - seconds: verificationTimeSeconds, - minutes: verificationTimeMinutes, - hours: verificationTimeHours - } = getSecondsMinutesHoursFromMillis(contributionData?.verificationTime) - - observation += `\n> The ${theme.bold("computation")} took ${theme.bold( - `${convertToDoubleDigits(contributionTimeHours)}:${convertToDoubleDigits( - contributionTimeMinutes - )}:${convertToDoubleDigits(contributionTimeSeconds)}` - )}` - observation += `\n> The ${theme.bold("verification")} took ${theme.bold( - `${convertToDoubleDigits(verificationTimeHours)}:${convertToDoubleDigits( - verificationTimeMinutes - )}:${convertToDoubleDigits(verificationTimeSeconds)}` - )}` - observation += `\n> Contribution ${ - contributionData?.valid - ? `${theme.bold("VALID")} ${symbols.success}` - : `${theme.bold("INVALID")} ${symbols.error}` - }` - - cursorPos -= 3 - } - } - - // Show observation for circuit. - process.stdout.write(`${observation}\n\n`) - cursorPos -= 1 - - return cursorPos -} - -/** - * Observe command. - */ -const observe = async () => { - try { - // Initialize services. - const { firebaseApp, firestoreDatabase } = await bootstrapCommandExec() - - // Handle current authenticated user sign in. - const { user } = await handleCurrentAuthUserSignIn(firebaseApp) - - // Check custom claims for coordinator role. - await onlyCoordinator(user) - - // Get running cerimonies info (if any). - const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase) - - // Ask to select a ceremony. - const ceremony = await askForCeremonySelection(runningCeremoniesDocs) - - console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`) - - let cursorPos = 0 // Keep track of current cursor position. - - const spinner = customSpinner(`Getting ready...`, "clock") - spinner.start() - - // Get circuit updates every 3 seconds. - setInterval(async () => { - // Clean cursor position back to root. - cursorPos = cleanCursorPosBackToRoot(cursorPos) - - const spinner = customSpinner(`Updating...`, "clock") - spinner.start() - - // Get updates from circuits. - const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) - - await sleep(observationWaitingTimeInMillis / 10) // Just for a smoother UX/UI experience. - - spinner.stop() - - // Observe changes for each circuit - for await (const circuit of circuits) cursorPos += await displayLatestCircuitUpdates(ceremony, circuit) - - process.stdout.write(`Press CTRL+C to exit`) - - await sleep(1000) // Just for a smoother UX/UI experience. - }, observationWaitingTimeInMillis) - - await sleep(observationWaitingTimeInMillis) // Wait until the first update. - - spinner.stop() - } catch (err: any) { - showError(`Something went wrong: ${err.toString()}`, true) - } -} - -export default observe diff --git a/apps/phase2cli/src/commands/setup.ts b/apps/phase2cli/src/commands/setup.ts deleted file mode 100644 index 4adbbbf3..00000000 --- a/apps/phase2cli/src/commands/setup.ts +++ /dev/null @@ -1,649 +0,0 @@ -#!/usr/bin/env node - -import { zKey, r1cs } from "snarkjs" -import winston from "winston" -import blake from "blakejs" -import boxen from "boxen" -import { httpsCallable } from "firebase/functions" -import { Dirent, renameSync } from "fs" -import { - theme, - symbols, - emojis, - potFilenameTemplate, - potDownloadUrlTemplate, - paths, - names, - collections -} from "../lib/constants.js" -import { handleCurrentAuthUserSignIn, onlyCoordinator } from "../lib/auth.js" -import { - bootstrapCommandExec, - convertToDoubleDigits, - customSpinner, - estimatePoT, - extractPoTFromFilename, - extractPrefix, - getBucketName, - getCircuitMetadataFromR1csFile, - multiPartUpload, - simpleLoader, - sleep, - terminate -} from "../lib/utils.js" -import { - askCeremonyInputData, - askCircomCompilerVersionAndCommitHash, - askCircuitInputData, - askForCircuitSelectionFromLocalDir, - askForConfirmation, - askForPtauSelectionFromLocalDir, - askForZkeySelectionFromLocalDir, - askPowersOftau -} from "../lib/prompts.js" -import { - cleanDir, - directoryExists, - downloadFileFromUrl, - getDirFilesSubPaths, - getFileStats, - readFile -} from "../lib/files.js" -import { CeremonyTimeoutType, Circuit, CircuitFiles, CircuitInputData, CircuitTimings } from "../../types/index.js" -import { GENERIC_ERRORS, showError } from "../lib/errors.js" -import { createS3Bucket, objectExist } from "../lib/storage.js" - -/** - * 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 -} - -/** - * Handle one or more circuit addition for the specified ceremony. - * @param cwd <string> - the current working directory. - * @param cwdR1csFiles <Array<Dirent>> - the list of R1CS files in the current working directory. - * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony. - * @param isCircomVersionEqualAmongCircuits <boolean> - true if the circom compiler version is equal among circuits; otherwise false. - * @returns <Promise<Array<CircuitInputData>>> - */ -const handleCircuitsAddition = async ( - cwd: string, - cwdR1csFiles: Array<Dirent>, - timeoutMechanismType: CeremonyTimeoutType, - isCircomVersionEqualAmongCircuits: boolean -): Promise<Array<CircuitInputData>> => { - const circuitsInputData: Array<CircuitInputData> = [] - - let wannaAddAnotherCircuit = true // Loop flag. - let circuitSequencePosition = 1 // Sequential circuit position for handling the contributions queue for the ceremony. - let leftCircuits: Array<Dirent> = cwdR1csFiles - - // Clear directory. - cleanDir(paths.metadataPath) - - while (wannaAddAnotherCircuit) { - console.log(theme.bold(`\n- Circuit # ${theme.magenta(`${circuitSequencePosition}`)}\n`)) - - // Interactively select a circuit. - const circuitNameWithExt = await askForCircuitSelectionFromLocalDir(leftCircuits) - - // Remove the selected circuit from the list. - leftCircuits = leftCircuits.filter((dirent: Dirent) => dirent.name !== circuitNameWithExt) - - // Ask for circuit input data. - const circuitInputData = await askCircuitInputData(timeoutMechanismType, isCircomVersionEqualAmongCircuits) - - // Remove .r1cs file extension. - const circuitName = circuitNameWithExt.substring(0, circuitNameWithExt.indexOf(".")) - const circuitPrefix = extractPrefix(circuitName) - - // R1CS circuit file path. - const r1csMetadataFilePath = `${paths.metadataPath}/${circuitPrefix}_${names.metadata}.log` - const r1csFilePath = `${cwd}/${circuitName}.r1cs` - - // Custom logger for R1CS metadata save. - const logger = winston.createLogger({ - level: "info", - transports: new winston.transports.File({ - filename: r1csMetadataFilePath, - format: winston.format.printf((log) => log.message), - level: "info" - }) - }) - - const spinner = customSpinner(`Looking for metadata...`, "clock") - spinner.start() - - // Read .r1cs file and log/store info. - await r1cs.info(r1csFilePath, logger) - - // Sleep to avoid logger unexpected termination. - await sleep(1000) - - // Store data. - circuitsInputData.push({ - ...circuitInputData, - name: circuitName, - prefix: circuitPrefix, - sequencePosition: circuitSequencePosition - }) - - spinner.succeed( - `Metadata stored in your working directory ${theme.bold(theme.underlined(r1csMetadataFilePath.substring(1)))}\n` - ) - - let readyToAssembly = false - - // In case of negative confirmation or no more circuits left. - if (leftCircuits.length !== 0) { - // Ask for another circuit. - const { confirmation } = await askForConfirmation("Want to add another circuit for the ceremony?", "Okay", "No") - - if (confirmation === undefined) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - if (confirmation === false) readyToAssembly = true - else circuitSequencePosition += 1 - } else readyToAssembly = true - - // Assembly the ceremony. - if (readyToAssembly) wannaAddAnotherCircuit = false - } - - return circuitsInputData -} - -/** - * Check if the smallest pot has been already downloaded. - * @param neededPowers <number> - the representation of the constraints of the circuit in terms of powers. - * @returns <Promise<boolean>> - */ -const checkIfPotAlreadyDownloaded = async (neededPowers: number): Promise<boolean> => { - // Get files from dir. - const potFiles = await getDirFilesSubPaths(paths.potPath) - - let alreadyDownloaded = false - - for (const potFile of potFiles) { - const powers = extractPoTFromFilename(potFile.name) - - if (powers === neededPowers) alreadyDownloaded = true - } - - return alreadyDownloaded -} - -/** - * Setup a new Groth16 zkSNARK Phase 2 Trusted Setup ceremony. - */ -const setup = async () => { - // Circuit data state. - let circuitsInputData: Array<CircuitInputData> = [] - const circuits: Array<Circuit> = [] - - /** CORE */ - try { - // Get current working directory. - const cwd = process.cwd() - - const { firebaseApp, firebaseFunctions } = await bootstrapCommandExec() - - // Setup ceremony callable Cloud Function initialization. - const setupCeremony = httpsCallable(firebaseFunctions, "setupCeremony") - const createBucket = httpsCallable(firebaseFunctions, "createBucket") - const startMultiPartUpload = httpsCallable(firebaseFunctions, "startMultiPartUpload") - const generatePreSignedUrlsParts = httpsCallable(firebaseFunctions, "generatePreSignedUrlsParts") - const completeMultiPartUpload = httpsCallable(firebaseFunctions, "completeMultiPartUpload") - const checkIfObjectExist = httpsCallable(firebaseFunctions, "checkIfObjectExist") - - // Handle current authenticated user sign in. - const { user, username } = await handleCurrentAuthUserSignIn(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() - const ceremonyPrefix = extractPrefix(ceremonyInputData.title) - - // Check for circom compiler version and commit hash. - 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() - - // Ask to add circuits. - circuitsInputData = await handleCircuitsAddition( - cwd, - cwdR1csFiles, - ceremonyInputData.timeoutMechanismType, - isCircomVersionEqualAmongCircuits - ) - - // Add the data to the circuit input data. - circuitsInputData = circuitsInputData.map((circuitInputData: CircuitInputData) => ({ - ...circuitInputData, - compiler: { version, commitHash } - })) - } else - circuitsInputData = await handleCircuitsAddition( - cwd, - cwdR1csFiles, - ceremonyInputData.timeoutMechanismType, - isCircomVersionEqualAmongCircuits - ) - - await simpleLoader(`Assembling your ceremony...`, `clock`, 2000) - - // Ceremony summary. - let summary = `${`${theme.bold(ceremonyInputData.title)}\n${theme.italic(ceremonyInputData.description)}`} - \n${`Opening: ${theme.bold( - theme.underlined(ceremonyInputData.startDate.toUTCString().replace("GMT", "UTC")) - )}\nEnding: ${theme.bold(theme.underlined(ceremonyInputData.endDate.toUTCString().replace("GMT", "UTC")))}`} - \n${theme.bold( - ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC ? `Dynamic` : `Fixed` - )} Timeout / ${theme.bold(ceremonyInputData.penalty)}m Penalty` - - for (let i = 0; i < circuitsInputData.length; i += 1) { - const circuitInputData = circuitsInputData[i] - - // Read file. - const r1csMetadataFilePath = `${paths.metadataPath}/${circuitInputData.prefix}_metadata.log` - const circuitMetadata = readFile(r1csMetadataFilePath) - - // Extract info from file. - const curve = getCircuitMetadataFromR1csFile(circuitMetadata, /Curve: .+\n/s) - const wires = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Wires: .+\n/s)) - const constraints = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Constraints: .+\n/s)) - const privateInputs = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Private Inputs: .+\n/s)) - const publicOutputs = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Public Inputs: .+\n/s)) - const labels = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Labels: .+\n/s)) - const outputs = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Outputs: .+\n/s)) - const pot = estimatePoT(constraints, outputs) - - // Store info. - circuits.push({ - ...circuitInputData, - metadata: { - curve, - wires, - constraints, - privateInputs, - publicOutputs, - labels, - outputs, - pot - } - }) - - // Show circuit summary. - summary += `\n\n${theme.bold(`- CIRCUIT # ${theme.bold(theme.magenta(`${circuitInputData.sequencePosition}`))}`)} - \n${`${theme.bold(circuitInputData.name)}\n${theme.italic(circuitInputData.description)} - \nCurve: ${theme.bold(curve)}\nCompiler: v${theme.bold(`${circuitInputData.compiler.version}`)} (${theme.bold( - circuitInputData.compiler.commitHash?.slice(0, 7) - )})\nSource: ${theme.bold(circuitInputData.template.source.split(`/`).at(-1))}(${theme.bold( - circuitInputData.template.paramsConfiguration - )})\n${ - ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC - ? `Threshold: ${theme.bold(circuitInputData.timeoutThreshold)}%` - : `Max Contribution Time: ${theme.bold(circuitInputData.timeoutMaxContributionWaitingTime)}m` - } - \n# Wires: ${theme.bold(wires)}\n# Constraints: ${theme.bold(constraints)}\n# Private Inputs: ${theme.bold( - privateInputs - )}\n# Public Inputs: ${theme.bold(publicOutputs)}\n# Labels: ${theme.bold(labels)}\n# Outputs: ${theme.bold( - outputs - )}\n# PoT: ${theme.bold(pot)}`}` - } - - // Show ceremony summary. - console.log( - boxen(summary, { - title: theme.magenta(`CEREMONY SUMMARY`), - titleAlignment: "center", - textAlignment: "left", - margin: 1, - padding: 1 - }) - ) - - // Ask for confirmation. - const { confirmation } = await askForConfirmation("Please, confirm to create the ceremony", "Okay", "Exit") - - if (confirmation) { - // Create the bucket. - const bucketName = getBucketName(ceremonyPrefix) - - const spinner = customSpinner(`Creating the storage bucket...`, `clock`) - spinner.start() - - await createS3Bucket(createBucket, bucketName) - await sleep(1000) - - spinner.succeed(`Storage bucket ${bucketName} successfully created`) - - // Get local zkeys (if any). - spinner.text = "Checking for pre-computed zkeys..." - spinner.start() - - const cwdZkeysFiles = await getSpecifiedFilesFromCwd(cwd, `.zkey`) - - await sleep(1000) - - spinner.stop() - - let leftPreComputedZkeys: Array<Dirent> = cwdZkeysFiles - - // Circuit setup. - for (let i = 0; i < circuits.length; i += 1) { - // Flag for generation of zkey from scratch. - let wannaGenerateZkey = true - // Flag for PoT download. - let wannaUsePreDownloadedPoT = false - - // Get the current circuit - const circuit = circuits[i] - - // Convert to double digits powers (e.g., 9 -> 09). - let stringifyNeededPowers = convertToDoubleDigits(circuit.metadata.pot) - let smallestPotForCircuit = `${potFilenameTemplate}${stringifyNeededPowers}.ptau` - - // Circuit r1cs and zkey file names. - const r1csFileName = `${circuit.name}.r1cs` - const firstZkeyFileName = `${circuit.prefix}_00000.zkey` - let preComputedZkeyNameWithExt = `` - - const r1csLocalPathAndFileName = `${cwd}/${r1csFileName}` - let potLocalPathAndFileName = `${paths.potPath}/${smallestPotForCircuit}` - let zkeyLocalPathAndFileName = `${paths.zkeysPath}/${firstZkeyFileName}` - - const potStoragePath = `${names.pot}` - const r1csStoragePath = `${collections.circuits}/${circuit.prefix}` - const zkeyStoragePath = `${collections.circuits}/${circuit.prefix}/${collections.contributions}` - - const r1csStorageFilePath = `${r1csStoragePath}/${r1csFileName}` - let potStorageFilePath = `${potStoragePath}/${smallestPotForCircuit}` - const zkeyStorageFilePath = `${zkeyStoragePath}/${firstZkeyFileName}` - - console.log(theme.bold(`\n- Setup for Circuit # ${theme.magenta(`${circuit.sequencePosition}`)}\n`)) - - if (!leftPreComputedZkeys.length) console.log(`${symbols.warning} There are no pre-computed zKeys`) - else { - const { confirmation } = await askForConfirmation( - `Do you wanna select a pre-computed zkey for the ${circuit.name} circuit?`, - `Yes`, - `No` - ) - - if (confirmation) { - // Ask for zKey selection. - preComputedZkeyNameWithExt = await askForZkeySelectionFromLocalDir(leftPreComputedZkeys) - - // Switch to pre-computed zkey path. - zkeyLocalPathAndFileName = `${cwd}/${preComputedZkeyNameWithExt}` - - // Switch the flag. - wannaGenerateZkey = false - } - } - - // If the coordinator wants to use a pre-computed zkey, needs to provide the related ptau. - if (!wannaGenerateZkey) { - spinner.text = "Checking for Powers of Tau..." - spinner.start() - - const cwdPtausFiles = await getSpecifiedFilesFromCwd(cwd, `.ptau`) - await sleep(1000) - - if (!cwdPtausFiles.length) { - spinner.warn(`No Powers of Tau (.ptau) files found`) - - // Download the PoT. - const { powers } = await askPowersOftau(circuit.metadata.pot) - - // Convert to double digits powers (e.g., 9 -> 09). - stringifyNeededPowers = convertToDoubleDigits(Number(powers)) - smallestPotForCircuit = `${potFilenameTemplate}${stringifyNeededPowers}.ptau` - - // Override. - potLocalPathAndFileName = `${paths.potPath}/${smallestPotForCircuit}` - potStorageFilePath = `${potStoragePath}/${smallestPotForCircuit}` - } else { - spinner.stop() - - // Ask for ptau selection. - smallestPotForCircuit = await askForPtauSelectionFromLocalDir(cwdPtausFiles, circuit.metadata.pot) - - // Update. - stringifyNeededPowers = convertToDoubleDigits(extractPoTFromFilename(smallestPotForCircuit)) - - // Switch to new ptau path. - potLocalPathAndFileName = `${cwd}/${smallestPotForCircuit}` - potStorageFilePath = `${potStoragePath}/${smallestPotForCircuit}` - - wannaUsePreDownloadedPoT = true - } - } - - // Check if the smallest pot has been already downloaded. - const alreadyDownloaded = - (await checkIfPotAlreadyDownloaded(Number(smallestPotForCircuit))) || wannaUsePreDownloadedPoT - - if (!alreadyDownloaded) { - // Get smallest suitable pot for circuit. - const spinner = customSpinner( - `Downloading ${theme.bold(`#${stringifyNeededPowers}`)} Powers of Tau from PPoT...`, - "clock" - ) - spinner.start() - - // Download PoT file. - const potDownloadUrl = `${potDownloadUrlTemplate}${smallestPotForCircuit}` - const destFilePath = `${paths.potPath}/${smallestPotForCircuit}` - - await downloadFileFromUrl(destFilePath, potDownloadUrl) - - spinner.succeed(`Powers of Tau ${theme.bold(`#${stringifyNeededPowers}`)} correctly downloaded`) - } else - console.log(`${symbols.success} Powers of Tau ${theme.bold(`#${stringifyNeededPowers}`)} already downloaded`) - - // Check if the smallest pot has been already uploaded. - const alreadyUploadedPot = await objectExist( - checkIfObjectExist, - bucketName, - `${ceremonyPrefix}/${names.pot}/${smallestPotForCircuit}` - ) - - // Validity check for the pre-computed zKey (avoids to upload an invalid combination of r1cs, ptau and zkey files). - if (!wannaGenerateZkey) { - // Check validity. - await simpleLoader(`Checking pre-computed zkey validity...`, `clock`, 1500) - - const valid = await zKey.verifyFromR1cs( - r1csLocalPathAndFileName, - potLocalPathAndFileName, - zkeyLocalPathAndFileName, - console - ) - - // nb. workaround for file descriptor closing. - await sleep(3000) - - if (valid) { - spinner.succeed(`Your pre-computed zKey is valid`) - - // Remove the selected zkey from the list. - leftPreComputedZkeys = leftPreComputedZkeys.filter( - (dirent: Dirent) => dirent.name !== preComputedZkeyNameWithExt - ) - - // Rename to first zkey filename. - renameSync(`${cwd}/${preComputedZkeyNameWithExt}`, `${circuit.prefix}_00000.zkey`) - } else { - spinner.fail(`Something went wrong during the verification of your pre-computed zKey`) - - // Ask to generate a new one from scratch. - const { confirmation } = await askForConfirmation( - `Do you wanna generate a new zkey for the ${circuit.name} circuit? (nb. A negative answer will ABORT the entire setup process)`, - `Yes`, - `No` - ) - - if (!confirmation) showError(`You have choosen to abort the entire setup process`, true) - else wannaGenerateZkey = true - } - } - - // Generate a brand new zKey. - if (wannaGenerateZkey) { - console.log( - `${symbols.warning} ${theme.bold( - `The computation of your zKey is starting soon (nb. do not interrupt the process because this will ABORT the entire setup process)` - )}\n` - ) - - // Compute first .zkey file (without any contribution). - await zKey.newZKey(r1csLocalPathAndFileName, potLocalPathAndFileName, zkeyLocalPathAndFileName, console) - - console.log(`\n${symbols.success} First zkey ${theme.bold(firstZkeyFileName)} successfully computed`) - } - - // Upload zkey. - await multiPartUpload( - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload, - bucketName, - zkeyStorageFilePath, - zkeyLocalPathAndFileName - ) - - console.log(`${symbols.success} First zkey ${theme.bold(firstZkeyFileName)} successfully saved on storage`) - - // PoT. - if (!alreadyUploadedPot) { - // Upload. - await multiPartUpload( - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload, - bucketName, - potStorageFilePath, - potLocalPathAndFileName - ) - - console.log( - `${symbols.success} Powers of Tau ${theme.bold(smallestPotForCircuit)} successfully saved on storage` - ) - } else { - console.log(`${symbols.success} Powers of Tau ${theme.bold(smallestPotForCircuit)} already stored`) - } - - // Upload R1CS. - await multiPartUpload( - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload, - bucketName, - r1csStorageFilePath, - r1csLocalPathAndFileName - ) - - console.log(`${symbols.success} R1CS ${theme.bold(r1csFileName)} successfully saved on storage`) - - // Circuit-related files info. - const circuitFiles: CircuitFiles = { - files: { - r1csFilename: r1csFileName, - potFilename: smallestPotForCircuit, - initialZkeyFilename: firstZkeyFileName, - r1csStoragePath: r1csStorageFilePath, - potStoragePath: potStorageFilePath, - initialZkeyStoragePath: zkeyStorageFilePath, - r1csBlake2bHash: blake.blake2bHex(r1csStorageFilePath), - potBlake2bHash: blake.blake2bHex(potStorageFilePath), - initialZkeyBlake2bHash: blake.blake2bHex(zkeyStorageFilePath) - } - } - - // nb. these will be validated after the first contribution. - const circuitTimings: CircuitTimings = { - avgTimings: { - contributionComputation: 0, - fullContribution: 0, - verifyCloudFunction: 0 - } - } - - circuits[i] = { - ...circuit, - ...circuitFiles, - ...circuitTimings, - zKeySizeInBytes: getFileStats(zkeyLocalPathAndFileName).size - } - - // Reset flags. - wannaGenerateZkey = true - wannaUsePreDownloadedPoT = false - } - - process.stdout.write(`\n`) - - /** POPULATE DB */ - spinner.text = `Storing ceremony data...` - spinner.start() - - // Setup ceremony on the server. - await setupCeremony({ - ceremonyInputData, - ceremonyPrefix, - circuits - }) - - // nb. workaround for CF termination. - await sleep(1000) - - spinner.succeed( - `Congrats, you have successfully completed your ${theme.bold(ceremonyInputData.title)} ceremony setup ${ - emojis.tada - }` - ) - } - - terminate(username) - } catch (err: any) { - showError(`Something went wrong: ${err.toString()}`, true) - } -} - -export default setup diff --git a/apps/phase2cli/src/index.ts b/apps/phase2cli/src/index.ts deleted file mode 100755 index 5514f8b7..00000000 --- a/apps/phase2cli/src/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env node - -import { createCommand } from "commander" -import { setup, auth, contribute, observe, finalize, clean, logout } from "./commands/index.js" -import { readLocalJsonFile } from "./lib/files.js" - -// Get pkg info (e.g., name, version). -const pkg = readLocalJsonFile("../../package.json") - -const program = createCommand() - -// Entry point. -program.name(pkg.name).description(pkg.description).version(pkg.version) - -// User commands. -program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth) -program - .command("contribute") - .description("compute contributions for a Phase2 Trusted Setup ceremony circuits") - .action(contribute) -program - .command("clean") - .description("clean up output generated by commands from the current working directory") - .action(clean) -program - .command("logout") - .description("sign out from Firebae Auth service and delete Github OAuth 2.0 token from local storage") - .action(logout) - -// Only coordinator commands. -const ceremony = program.command("coordinate").description("commands for coordinating a ceremony") - -ceremony - .command("setup") - .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits") - .action(setup) - -ceremony - .command("observe") - .description("observe in real-time the waiting queue of each ceremony circuit") - .action(observe) - -ceremony - .command("finalize") - .description( - "finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract" - ) - .action(finalize) - -program.parseAsync() diff --git a/apps/phase2cli/src/lib/constants.ts b/apps/phase2cli/src/lib/constants.ts deleted file mode 100644 index 39c839ae..00000000 --- a/apps/phase2cli/src/lib/constants.ts +++ /dev/null @@ -1,139 +0,0 @@ -import chalk from "chalk" -import logSymbols from "log-symbols" -import emoji from "node-emoji" - -/** Theme */ -export const theme = { - yellow: chalk.yellow, - magenta: chalk.magenta, - red: chalk.red, - green: chalk.green, - underlined: chalk.underline, - bold: chalk.bold, - italic: chalk.italic -} - -export const symbols = { - success: logSymbols.success, - warning: logSymbols.warning, - error: logSymbols.error, - info: logSymbols.info -} - -export const 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") -} - -/** ZK related */ -export const potDownloadUrlTemplate = `https://hermez.s3-eu-west-1.amazonaws.com/` -export const potFilenameTemplate = `powersOfTau28_hez_final_` -export const firstZkeyIndex = `00000` -export const numIterationsExp = 10 -export const solidityVersion = "0.8.0" - -/** Commands related */ -export const observationWaitingTimeInMillis = 3000 // 3 seconds. - -/** Shared */ -export const names = { - output: `output`, - setup: `setup`, - contribute: `contribute`, - finalize: `finalize`, - pot: `pot`, - zkeys: `zkeys`, - vkeys: `vkeys`, - metadata: `metadata`, - transcripts: `transcripts`, - attestation: `attestation`, - verifiers: `verifiers` -} - -const outputPath = `./${names.output}` -const setupPath = `${outputPath}/${names.setup}` -const contributePath = `${outputPath}/${names.contribute}` -const finalizePath = `${outputPath}/${names.finalize}` -const potPath = `${setupPath}/${names.pot}` -const zkeysPath = `${setupPath}/${names.zkeys}` -const metadataPath = `${setupPath}/${names.metadata}` -const contributionsPath = `${contributePath}/${names.zkeys}` -const contributionTranscriptsPath = `${contributePath}/${names.transcripts}` -const attestationPath = `${contributePath}/${names.attestation}` -const finalZkeysPath = `${finalizePath}/${names.zkeys}` -const finalPotPath = `${finalizePath}/${names.pot}` -const finalTranscriptsPath = `${finalizePath}/${names.transcripts}` -const finalAttestationsPath = `${finalizePath}/${names.attestation}` -const verificationKeysPath = `${finalizePath}/${names.vkeys}` -const verifierContractsPath = `${finalizePath}/${names.verifiers}` - -export const paths = { - outputPath, - setupPath, - contributePath, - finalizePath, - potPath, - zkeysPath, - metadataPath, - contributionsPath, - contributionTranscriptsPath, - attestationPath, - finalZkeysPath, - finalPotPath, - finalTranscriptsPath, - finalAttestationsPath, - verificationKeysPath, - verifierContractsPath -} - -/** Firebase */ -export const collections = { - users: "users", - participants: "participants", - ceremonies: "ceremonies", - circuits: "circuits", - contributions: "contributions", - timeouts: "timeouts" -} - -export const ceremoniesCollectionFields = { - coordinatorId: "coordinatorId", - description: "description", - endDate: "endDate", - lastUpdated: "lastUpdated", - prefix: "prefix", - startDate: "startDate", - state: "state", - title: "title", - type: "type" -} - -export const contributionsCollectionFields = { - contributionTime: "contributionTime", - files: "files", - lastUpdated: "lastUpdated", - participantId: "participantId", - valid: "valid", - verificationTime: "verificationTime", - zkeyIndex: "zKeyIndex" -} - -export const timeoutsCollectionFields = { - startDate: "startDate", - endDate: "endDate" -} diff --git a/apps/phase2cli/src/lib/errors.ts b/apps/phase2cli/src/lib/errors.ts deleted file mode 100644 index d84b8488..00000000 --- a/apps/phase2cli/src/lib/errors.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { deleteStoredOAuthToken } from "./auth.js" -import { emojis, symbols } from "./constants.js" - -/** Firebase */ -export const FIREBASE_ERRORS = { - FIREBASE_NOT_CONFIGURED_PROPERLY: `Check that all FIREBASE environment variables are configured properly`, - FIREBASE_DEFAULT_APP_DOUBLE_CONFIG: `Wrong double default configuration for Firebase application`, - FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `Unsuccessful check authorization response from Github. This usually happens when a token expires or the CLI do not have permissions associated with your Github account`, - FIREBASE_USER_DISABLED: `Your Github account has been disabled and can no longer be used to contribute. Get in touch with the coordinator to find out more`, - FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials. This usually happens due to network errors`, - FIREBASE_NETWORK_ERROR: `Unable to reach Firebase. This usually happens due to network errors`, - FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`, - FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization` -} - -/** Github */ -export const GITHUB_ERRORS = { - GITHUB_NOT_CONFIGURED_PROPERLY: `Github \`CLIENT_ID\` environment variable is not configured properly`, - GITHUB_ACCOUNT_ASSOCIATION_REJECTED: `You refused to associate your Github account with the CLI`, - GITHUB_SERVER_TIMEDOUT: `Github server has timed out. This usually happens due to network error or Github server downtime`, - GITHUB_GET_USERNAME_FAILED: `Something went wrong while retrieving your Github username`, - GITHUB_NOT_AUTHENTICATED: `You are not authenticated. Please, run \`phase2cli auth\` command first`, - GITHUB_GIST_PUBLICATION_FAILED: `Something went wrong while publishing a Gist from your Github account` -} - -/** 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`, - GENERIC_CEREMONY_SELECTION: `You have aborted the ceremony selection process`, - GENERIC_CIRCUIT_SELECTION: `You have aborted the circuit selection process`, - GENERIC_DATA_INPUT: `You have aborted the process without providing any of the requested data`, - GENERIC_CONTRIBUTION_HASH_INVALID: `You have aborted the process and do not have provided the requested data` -} - -/** - * 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. - */ -export const showError = (err: string, doExit: boolean) => { - // Print the error. - console.error(`${symbols.error} ${err}`) - - // Terminate the process. - if (doExit) process.exit(0) -} - -/** - * Error handling for auth command. - * @param err <any> - any error that may happen while running the auth command. - */ -export const handleAuthErrors = (err: any) => { - const error = err.toString() - - /** Firebase */ - - if (error.includes("Firebase: Unsuccessful check authorization response from Github")) { - showError(FIREBASE_ERRORS.FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS, false) - - // Clean expired token from local storage. - deleteStoredOAuthToken() - - console.log(`${symbols.success} Removed expired token from your local storage ${emojis.broom}`) - console.log( - `${symbols.info} Please, run \`phase2cli auth\` again to generate a new token and associate your Github account` - ) - - process.exit(0) - } - - if (error.includes("Firebase: Firebase App named '[DEFAULT]' already exists with different options or config")) - showError(FIREBASE_ERRORS.FIREBASE_DEFAULT_APP_DOUBLE_CONFIG, true) - - if (error.includes("Firebase: Error (auth/user-disabled)")) showError(FIREBASE_ERRORS.FIREBASE_USER_DISABLED, true) - - if (error.includes("Firebase: Error (auth/network-request-failed)")) - showError(FIREBASE_ERRORS.FIREBASE_NETWORK_ERROR, true) - - if (error.includes("Firebase: Remote site 5XX from github.com for VERIFY_CREDENTIAL (auth/invalid-credential)")) - showError(FIREBASE_ERRORS.FIREBASE_FAILED_CREDENTIALS_VERIFICATION, true) - - /** Github */ - - if (error.includes("HttpError: The authorization request was denied")) - showError(GITHUB_ERRORS.GITHUB_ACCOUNT_ASSOCIATION_REJECTED, true) - - if (error.includes("HttpError: request to https://github.com/login/device/code failed, reason: connect ETIMEDOUT")) - showError(GITHUB_ERRORS.GITHUB_SERVER_TIMEDOUT, true) - - /** Generic */ - - showError(`Something went wrong: ${error}`, true) -} diff --git a/apps/phase2cli/src/lib/listeners.ts b/apps/phase2cli/src/lib/listeners.ts deleted file mode 100644 index af093665..00000000 --- a/apps/phase2cli/src/lib/listeners.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { DocumentData, DocumentSnapshot, Firestore, onSnapshot } from "firebase/firestore" -import { Functions, httpsCallable } from "firebase/functions" -import { getCeremonyCircuits } from "@zkmpc/actions" -import { FirebaseDocumentInfo, ParticipantContributionStep, ParticipantStatus } from "../../types/index.js" -import { collections, emojis, symbols, theme } from "./constants.js" -import { getCurrentContributorContribution } from "./queries.js" -import { - convertToDoubleDigits, - customSpinner, - formatZkeyIndex, - generatePublicAttestation, - getContributorContributionsVerificationResults, - getNextCircuitForContribution, - getSecondsMinutesHoursFromMillis, - handleDiskSpaceRequirementForNextContribution, - handleTimedoutMessageForContributor, - makeContribution, - simpleCountdown, - terminate -} from "./utils.js" -import { GENERIC_ERRORS, showError } from "./errors.js" -import { getDocumentById } from "./firebase.js" - -/** - * Return the index of a given participant in a circuit waiting queue. - * @param contributors <Array<string>> - the list of the contributors in queue for a circuit. - * @param participantId <string> - the unique identifier of the participant. - * @returns <number> - */ -const getParticipantPositionInQueue = (contributors: Array<string>, participantId: string): number => - contributors.indexOf(participantId) + 1 - -/** - * Listen to circuit document changes and reacts in realtime. - * @param participantId <string> - the unique identifier of the contributor. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param circuit <FirebaseDocumentInfo> - the document information about the current circuit. - */ -const listenToCircuitChanges = (participantId: string, ceremonyId: string, circuit: FirebaseDocumentInfo) => { - const unsubscriberForCircuitDocument = onSnapshot(circuit.ref, async (circuitDocSnap: DocumentSnapshot) => { - // Get updated data from snap. - const newCircuitData = circuitDocSnap.data() - - if (!newCircuitData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Get data. - const { avgTimings, waitingQueue } = newCircuitData! - const { fullContribution, verifyCloudFunction } = avgTimings - const { currentContributor, completedContributions } = waitingQueue - - // Retrieve current contributor data. - const currentContributorDoc = await getDocumentById( - `${collections.ceremonies}/${ceremonyId}/${collections.participants}`, - currentContributor - ) - - // Get updated data from snap. - const currentContributorData = currentContributorDoc.data() - - if (!currentContributorData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Get updated position for contributor in the queue. - const newParticipantPositionInQueue = getParticipantPositionInQueue(waitingQueue.contributors, participantId) - - let newEstimatedWaitingTime = 0 - - // Show new time estimation. - if (fullContribution > 0 && verifyCloudFunction > 0) - newEstimatedWaitingTime = (fullContribution + verifyCloudFunction) * (newParticipantPositionInQueue - 1) - - const { - seconds: estSeconds, - minutes: estMinutes, - hours: estHours - } = getSecondsMinutesHoursFromMillis(newEstimatedWaitingTime) - - // Check if is the current contributor. - if (newParticipantPositionInQueue === 1) { - console.log( - `\n${symbols.success} Your turn has come ${emojis.tada}\n${symbols.info} Your contribution will begin soon` - ) - unsubscriberForCircuitDocument() - } else { - // Position and time. - console.log( - `\n${symbols.info} ${ - newParticipantPositionInQueue === 2 - ? `You are the next contributor` - : `Your position in the waiting queue is ${theme.bold(theme.magenta(newParticipantPositionInQueue - 1))}` - } (${ - newEstimatedWaitingTime > 0 - ? `${theme.bold( - `${convertToDoubleDigits(estHours)}:${convertToDoubleDigits(estMinutes)}:${convertToDoubleDigits( - estSeconds - )}` - )} left before your turn)` - : `no time estimation)` - }` - ) - - // Participant data. - console.log(` - Contributor # ${theme.bold(theme.magenta(completedContributions + 1))}`) - - // Data for displaying info about steps. - const currentZkeyIndex = formatZkeyIndex(completedContributions) - const nextZkeyIndex = formatZkeyIndex(completedContributions + 1) - - let interval: NodeJS.Timer - - const unsubscriberForCurrentContributorDocument = onSnapshot( - currentContributorDoc.ref, - async (currentContributorDocSnap: DocumentSnapshot) => { - // Get updated data from snap. - const newCurrentContributorData = currentContributorDocSnap.data() - - if (!newCurrentContributorData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Get current contributor data. - const { contributionStep, contributionStartedAt } = newCurrentContributorData! - - // Average time. - const timeSpentWhileContributing = Date.now() - contributionStartedAt - const remainingTime = fullContribution - timeSpentWhileContributing - - // Clear previous step interval (if exist). - if (interval) clearInterval(interval) - - switch (contributionStep) { - case ParticipantContributionStep.DOWNLOADING: { - const message = ` ${symbols.info} Downloading contribution ${theme.bold(`#${currentZkeyIndex}`)}` - interval = simpleCountdown(remainingTime, message) - - break - } - case ParticipantContributionStep.COMPUTING: { - process.stdout.write( - ` ${symbols.success} Contribution ${theme.bold(`#${currentZkeyIndex}`)} correctly downloaded\n` - ) - - const message = ` ${symbols.info} Computing contribution ${theme.bold(`#${nextZkeyIndex}`)}` - interval = simpleCountdown(remainingTime, message) - - break - } - case ParticipantContributionStep.UPLOADING: { - process.stdout.write( - ` ${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} successfully computed\n` - ) - - const message = ` ${symbols.info} Uploading contribution ${theme.bold(`#${nextZkeyIndex}`)}` - interval = simpleCountdown(remainingTime, message) - - break - } - case ParticipantContributionStep.VERIFYING: { - process.stdout.write( - ` ${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} successfully uploaded\n` - ) - - const message = ` ${symbols.info} Contribution verification ${theme.bold(`#${nextZkeyIndex}`)}` - interval = simpleCountdown(remainingTime, message) - - break - } - case ParticipantContributionStep.COMPLETED: { - process.stdout.write( - ` ${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} has been correctly verified\n` - ) - - const currentContributorContributions = await getCurrentContributorContribution( - ceremonyId, - circuit.id, - currentContributorDocSnap.id - ) - - if (currentContributorContributions.length !== 1) - process.stdout.write(` ${symbols.error} We could not recover the contribution data`) - else { - const contribution = currentContributorContributions.at(0) - - const { valid } = contribution?.data! - - console.log( - ` ${valid ? symbols.success : symbols.error} Contribution ${theme.bold(`#${nextZkeyIndex}`)} is ${ - valid ? `VALID` : `INVALID` - }` - ) - } - - unsubscriberForCurrentContributorDocument() - break - } - default: { - showError(`Wrong contribution step`, true) - break - } - } - } - ) - } - }) -} - -// Listen to changes on the user-related participant document. -export default async ( - participantDoc: DocumentSnapshot<DocumentData>, - ceremony: FirebaseDocumentInfo, - firestoreDatabase: Firestore, - circuits: Array<FirebaseDocumentInfo>, - firebaseFunctions: Functions, - ghToken: string, - ghUsername: string, - entropy: string -) => { - // Get number of circuits for the selected ceremony. - const numberOfCircuits = circuits.length - - // Listen to participant document changes. - const unsubscriberForParticipantDocument = onSnapshot( - participantDoc.ref, - async (participantDocSnap: DocumentSnapshot) => { - // Get updated data from snap. - const newParticipantData = participantDocSnap.data() - const oldParticipantData = participantDoc.data() - - if (!newParticipantData || !oldParticipantData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Extract updated participant document data. - const { contributionProgress, status, contributionStep, contributions, tempContributionData } = - newParticipantData! - const { - contributionStep: oldContributionStep, - tempContributionData: oldTempContributionData, - contributionProgress: oldContributionProgress, - contributions: oldContributions, - status: oldStatus - } = oldParticipantData! - const participantId = participantDoc.id - - // 0. Whem joining for the first time the waiting queue. - if ( - status === ParticipantStatus.WAITING && - !contributionStep && - !contributions.length && - contributionProgress === 0 - ) { - // Get next circuit. - const nextCircuit = getNextCircuitForContribution(circuits, contributionProgress + 1) - - // Check disk space requirements for participant. - const makeProgressToNextContribution = httpsCallable(firebaseFunctions, "makeProgressToNextContribution") - await handleDiskSpaceRequirementForNextContribution(makeProgressToNextContribution, nextCircuit, ceremony.id) - } - - // A. Do not have completed the contributions for each circuit; move to the next one. - if (contributionProgress > 0 && contributionProgress <= circuits.length) { - // Get updated circuits data. - const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) - const circuit = circuits[contributionProgress - 1] - const { waitingQueue } = circuit.data - - // Check if the contribution step is valid for starting/resuming the contribution. - const isStepValidForStartingOrResumingContribution = - (contributionStep === ParticipantContributionStep.DOWNLOADING && - status === ParticipantStatus.CONTRIBUTING && - (!oldContributionStep || - oldContributionStep !== contributionStep || - (oldContributionStep === contributionStep && - status === oldStatus && - oldContributionProgress === contributionProgress) || - oldStatus === ParticipantStatus.EXHUMED)) || - (contributionStep === ParticipantContributionStep.COMPUTING && - oldContributionStep === contributionStep && - oldContributions.length === contributions.length) || - (contributionStep === ParticipantContributionStep.UPLOADING && - !oldTempContributionData && - !tempContributionData && - contributionStep === oldContributionStep) || - (!!oldTempContributionData && - !!tempContributionData && - JSON.stringify(Object.keys(oldTempContributionData).sort()) === - JSON.stringify(Object.keys(tempContributionData).sort()) && - JSON.stringify(Object.values(oldTempContributionData).sort()) === - JSON.stringify(Object.values(tempContributionData).sort())) - - // A.1 If the participant is in `waiting` status, he/she must receive updates from the circuit's waiting queue. - if (status === ParticipantStatus.WAITING && oldStatus !== ParticipantStatus.TIMEDOUT) { - console.log( - `${theme.bold(`\n- Circuit # ${theme.magenta(`${circuit.data.sequencePosition}`)}`)} (Waiting Queue)` - ) - - listenToCircuitChanges(participantId, ceremony.id, circuit) - } - // A.2 If the participant is in `contributing` status and is the current contributor, he/she must compute the contribution. - if ( - status === ParticipantStatus.CONTRIBUTING && - contributionStep !== ParticipantContributionStep.VERIFYING && - waitingQueue.currentContributor === participantId && - isStepValidForStartingOrResumingContribution - ) { - console.log( - `\n${symbols.success} Your contribution will ${ - contributionStep === ParticipantContributionStep.DOWNLOADING ? `start` : `resume` - } soon ${emojis.clock}` - ) - - // Compute the contribution. - await makeContribution(ceremony, circuit, entropy, ghUsername, false, firebaseFunctions, newParticipantData!) - } - - // A.3 Current contributor has already started the verification step. - if ( - status === ParticipantStatus.CONTRIBUTING && - waitingQueue.currentContributor === participantId && - contributionStep === oldContributionStep && - contributionStep === ParticipantContributionStep.VERIFYING && - contributionProgress === oldContributionProgress - ) { - const spinner = customSpinner(`Resuming your contribution...`, `clock`) - spinner.start() - - // Get current and next index. - const currentZkeyIndex = formatZkeyIndex(contributionProgress) - const nextZkeyIndex = formatZkeyIndex(contributionProgress + 1) - - // Calculate remaining est. time for verification. - const avgVerifyCloudFunctionTime = circuit.data.avgTimings.verifyCloudFunction - const verificationStartedAt = newParticipantData?.verificationStartedAt - const estRemainingTimeInMillis = avgVerifyCloudFunctionTime - (Date.now() - verificationStartedAt) - const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(estRemainingTimeInMillis) - - spinner.succeed(`Your contribution will resume soon ${emojis.clock}`) - - console.log( - `${theme.bold(`\n- Circuit # ${theme.magenta(`${circuit.data.sequencePosition}`)}`)} (Contribution Steps)` - ) - console.log(`${symbols.success} Contribution ${theme.bold(`#${currentZkeyIndex}`)} already downloaded`) - console.log(`${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} already computed`) - console.log(`${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} already saved on storage`) - console.log( - `${symbols.info} Contribution verification already started (est. time ${theme.bold( - `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}` - )})` - ) - } - - // A.4 Server has terminated the already started verification step above. - if ( - ((status === ParticipantStatus.DONE && oldStatus === ParticipantStatus.DONE) || - (status === ParticipantStatus.CONTRIBUTED && oldStatus === ParticipantStatus.CONTRIBUTED)) && - oldContributionProgress === contributionProgress - 1 && - contributionStep === ParticipantContributionStep.COMPLETED - ) { - console.log(`\n${symbols.success} Contribute verification has been completed`) - - // Return true and false based on contribution verification. - const contributionsValidity = await getContributorContributionsVerificationResults( - ceremony.id, - participantDoc.id, - circuits, - false - ) - - // Check last contribution validity. - const isContributionValid = contributionsValidity[oldContributionProgress - 1] - - console.log( - `${isContributionValid ? symbols.success : symbols.error} Your contribution ${ - isContributionValid ? `is ${theme.bold("VALID")}` : `is ${theme.bold("INVALID")}` - }` - ) - } - - // A.5 Current contributor timedout. - if (status === ParticipantStatus.TIMEDOUT && contributionStep !== ParticipantContributionStep.COMPLETED) - await handleTimedoutMessageForContributor( - newParticipantData!, - participantDoc.id, - ceremony.id, - true, - ghUsername - ) - - // A.6 Contributor has finished the contribution and we need to check the memory before progressing. - if (status === ParticipantStatus.CONTRIBUTED && contributionStep === ParticipantContributionStep.COMPLETED) { - // Get next circuit for contribution. - const nextCircuit = getNextCircuitForContribution(circuits, contributionProgress + 1) - - // Check disk space requirements for participant. - const makeProgressToNextContribution = httpsCallable(firebaseFunctions, "makeProgressToNextContribution") - const wannaGenerateAttestation = await handleDiskSpaceRequirementForNextContribution( - makeProgressToNextContribution, - nextCircuit, - ceremony.id - ) - - if (wannaGenerateAttestation) { - // Generate attestation with valid contributions. - await generatePublicAttestation(ceremony, participantId, newParticipantData!, circuits, ghUsername, ghToken) - - unsubscriberForParticipantDocument() - terminate(ghUsername) - } - } - - // A.7 If the participant is in `EXHUMED` status can be only after a timeout expiration. - if (status === ParticipantStatus.EXHUMED) { - // Check disk space requirements for participant before resuming the contribution. - const resumeContributionAfterTimeoutExpiration = httpsCallable( - firebaseFunctions, - "resumeContributionAfterTimeoutExpiration" - ) - await handleDiskSpaceRequirementForNextContribution( - resumeContributionAfterTimeoutExpiration, - circuit, - ceremony.id - ) - } - - // B. Already contributed to each circuit. - if ( - status === ParticipantStatus.DONE && - contributionStep === ParticipantContributionStep.COMPLETED && - contributionProgress === numberOfCircuits && - contributions.length === numberOfCircuits - ) { - await generatePublicAttestation(ceremony, participantId, newParticipantData!, circuits, ghUsername, ghToken) - - unsubscriberForParticipantDocument() - terminate(ghUsername) - } - } - } - ) -} diff --git a/apps/phase2cli/src/lib/prompts.ts b/apps/phase2cli/src/lib/prompts.ts deleted file mode 100644 index 639fed70..00000000 --- a/apps/phase2cli/src/lib/prompts.ts +++ /dev/null @@ -1,539 +0,0 @@ -import { Dirent } from "fs" -import prompts, { Answers, Choice, PromptObject } from "prompts" -import { - CeremonyInputData, - CeremonyTimeoutType, - CircomCompilerData, - CircuitInputData, - FirebaseDocumentInfo -} from "../../types/index.js" -import { symbols, theme } from "./constants.js" -import { GENERIC_ERRORS, showError } from "./errors.js" -import { extractPoTFromFilename, extractPrefix, getCreatedCeremoniesPrefixes } from "./utils.js" - -/** - * Show a binary question with custom options for confirmation purposes. - * @param question <string> - the question to be answered. - * @param active <string> - the active option (= yes). - * @param inactive <string> - the inactive option (= no). - * @returns <Promise<Answers<string>>> - */ -export const askForConfirmation = async (question: string, active = "yes", inactive = "no"): Promise<Answers<string>> => - prompts({ - type: "toggle", - name: "confirmation", - message: theme.bold(question), - initial: false, - active, - inactive - }) - -/** - * Show a series of questions about the ceremony. - * @returns <Promise<CeremonyInputData>> - the necessary information for the ceremony entered by the coordinator. - */ -export const askCeremonyInputData = async (): Promise<CeremonyInputData> => { - // Get ceremonies prefixes to check for duplicates. - const ceremoniesPrefixes = await getCreatedCeremoniesPrefixes() - - 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: (penalty: number) => { - if (penalty < 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. - */ -export const askCircomCompilerVersionAndCommitHash = async (): Promise<CircomCompilerData> => { - const questions: Array<PromptObject> = [ - { - type: "text", - name: "version", - message: theme.bold(`Give the circom compiler version`), - validate: (version: string) => { - if (version.length <= 0) return theme.red(`${symbols.error} You must provide a valid version (e.g., 2.0.1)`) - - if (!version.match(/^[0-9].[0-9.]*$/)) - return theme.red(`${symbols.error} You must provide a valid version (e.g., 2.0.1)`) - - return true - } - }, - { - type: "text", - name: "commitHash", - message: theme.bold(`Give the commit hash of the circom compiler version`), - validate: (commitHash: string) => - commitHash.length === 40 || - theme.red( - `${symbols.error} You must provide a valid commit hash (e.g., b7ad01b11f9b4195e38ecc772291251260ab2c67)` - ) - } - ] - - const { version, commitHash } = await prompts(questions) - - if (!version || !commitHash) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - return { - version, - commitHash - } -} - -/** - * Show a series of questions about the circuits. - * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony. - * @param isCircomVersionDifferentAmongCircuits <boolean> - true if the circom compiler version is equal among circuits; otherwise false. - * @returns Promise<Array<Circuit>> - the necessary information for the circuits entered by the coordinator. - */ -export const askCircuitInputData = async ( - timeoutMechanismType: CeremonyTimeoutType, - isCircomVersionEqualAmongCircuits: boolean -): Promise<CircuitInputData> => { - const circuitQuestions: Array<PromptObject> = [ - { - name: "description", - type: "text", - message: theme.bold(`Add a description`), - validate: (value) => (value.length ? true : theme.red(`${symbols.error} You must provide a valid description`)) - }, - { - name: "templateSource", - type: "text", - message: theme.bold(`Give the external reference to the source template (.circom file)`), - validate: (value) => - value.length > 0 && value.match(/(https?:\/\/[^\s]+\.circom$)/g) - ? true - : theme.red(`${symbols.error} You must provide a valid link to the .circom source template`) - }, - { - name: "templateCommitHash", - type: "text", - message: theme.bold(`Give the commit hash of the source template`), - validate: (commitHash: string) => - commitHash.length === 40 || - theme.red( - `${symbols.error} You must provide a valid commit hash (e.g., b7ad01b11f9b4195e38ecc772291251260ab2c67)` - ) - } - ] - - // Prompt for circuit data. - const { description, templateSource, templateCommitHash } = await prompts(circuitQuestions) - - if (!description || !templateSource || !templateCommitHash) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - // Ask for dynamic or fixed data. - let paramsConfiguration: Array<string> = [] - let timeoutThreshold = 0 - let timeoutMaxContributionWaitingTime = 0 - let circomVersion = "" - let circomCommitHash = "" - - // Ask for params config values (if any). - const { confirmation: needConfiguration } = await askForConfirmation( - `Did the source template need configuration?`, - `Yes`, - `No` - ) - - if (needConfiguration) { - const { templateParamsValues } = await prompts({ - name: "templateParamsValues", - type: "text", - message: theme.bold(`Please, provide a comma-separated list of the parameters values used for configuration`), - validate: (value: string) => - value.split(",").length === 1 || - (value.split(`,`).length > 1 && value.includes(",")) || - theme.red( - `${symbols.error} You must provide a valid comma-separated list of parameters values (e.g., 10,2,1,2)` - ) - }) - - if (!templateParamsValues) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - paramsConfiguration = templateParamsValues.split(",") - } - - // Ask for circom info (if different from other circuits). - if (!isCircomVersionEqualAmongCircuits) { - const { version, commitHash } = await askCircomCompilerVersionAndCommitHash() - - if (!version || !commitHash) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - circomVersion = version - circomCommitHash = commitHash - } - - // Ask for dynamic timeout mechanism data. - if (timeoutMechanismType === CeremonyTimeoutType.DYNAMIC) { - const { threshold } = await prompts({ - type: "number", - name: "threshold", - message: theme.bold(`Provide an additional threshold up to the total average contribution time (in percentage):`), - validate: (threshold: number) => { - if (threshold < 0 || threshold > 100) - return theme.red(`${symbols.error} You must provide a threshold between 0 and 100`) - - return true - } - }) - - if (threshold < 0 || threshold > 100) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - timeoutThreshold = threshold - } - - // Ask for fixed timeout mechanism data. - if (timeoutMechanismType === CeremonyTimeoutType.FIXED) { - const { maxContributionWaitingTime } = await prompts({ - type: "number", - name: `maxContributionWaitingTime`, - message: theme.bold(`Specify the max amount of time tolerable while contributing (in minutes):`), - validate: (threshold: number) => { - if (threshold <= 0) - return theme.red(`${symbols.error} You must provide a maximum contribution waiting time greater than zero`) - - return true - } - }) - - if (maxContributionWaitingTime <= 0) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - timeoutMaxContributionWaitingTime = maxContributionWaitingTime - } - - if ( - (timeoutMechanismType === CeremonyTimeoutType.DYNAMIC && timeoutThreshold < 0) || - (timeoutMechanismType === CeremonyTimeoutType.FIXED && timeoutMaxContributionWaitingTime < 0) || - (needConfiguration && paramsConfiguration.length === 0) || - (isCircomVersionEqualAmongCircuits && !!circomVersion && !!circomCommitHash) - ) - showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - return timeoutMechanismType === CeremonyTimeoutType.DYNAMIC - ? { - description, - timeoutThreshold, - compiler: { - version: circomVersion, - commitHash: circomCommitHash - }, - template: { - source: templateSource, - commitHash: templateCommitHash, - paramsConfiguration - } - } - : { - description, - timeoutMaxContributionWaitingTime, - compiler: { - version: circomVersion, - commitHash: circomCommitHash - }, - template: { - source: templateSource, - commitHash: templateCommitHash, - paramsConfiguration - } - } -} - -/** - * Request the powers of the Powers of Tau for a specified circuit. - * @param suggestedPowers <number> - the minimal number of powers necessary for circuit zKey generation. - * @returns Promise<Array<Circuit>> - the necessary information for the circuits entered by the coordinator. - */ -export const askPowersOftau = async (suggestedPowers: number): Promise<any> => { - const question: PromptObject = { - name: "powers", - type: "number", - message: theme.bold( - `Please, provide the amounts of powers you have used to generate the pre-computed zkey (>= ${suggestedPowers}):` - ), - validate: (value) => - value >= suggestedPowers - ? true - : theme.red(`${symbols.error} You must provide a value greater than or equal to ${suggestedPowers}`) - } - - // Prompt for circuit data. - const { powers } = await prompts(question) - - if (powers < suggestedPowers) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - return { - powers - } -} - -/** - * Prompt the list of circuits from a specific directory. - * @param circuitsDirents <Array<Dirent>> - * @returns Promise<string> - */ -export const askForCircuitSelectionFromLocalDir = async (circuitsDirents: Array<Dirent>): Promise<string> => { - const choices: Array<Choice> = [] - - // Make a 'Choice' for each circuit. - for (const circuitDirent of circuitsDirents) { - choices.push({ - title: circuitDirent.name, - value: circuitDirent.name - }) - } - - // Ask for selection. - const { circuit } = await prompts({ - type: "select", - name: "circuit", - message: theme.bold("Select a circuit"), - choices, - initial: 0 - }) - - if (!circuit) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) - - return circuit -} - -/** - * Prompt the list of pre-computed zkeys files from a specific directory. - * @param zkeysDirents <Array<Dirent>> - * @returns Promise<string> - */ -export const askForZkeySelectionFromLocalDir = async (zkeysDirents: Array<Dirent>): Promise<string> => { - const choices: Array<Choice> = [] - - // Make a 'Choice' for each zkey. - for (const zkeyDirent of zkeysDirents) { - choices.push({ - title: zkeyDirent.name, - value: zkeyDirent.name - }) - } - - // Ask for selection. - const { zkey } = await prompts({ - type: "select", - name: "zkey", - message: theme.bold("Select a pre-computed zkey"), - choices, - initial: 0 - }) - - if (!zkey) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) - - return zkey -} - -/** - * Prompt the list of ptau files from a specific directory. - * @param ptausDirents <Array<Dirent>> - * @param suggestedPowers <number> - the minimal number of powers necessary for circuit zKey generation. - * @returns Promise<string> - */ -export const askForPtauSelectionFromLocalDir = async ( - ptausDirents: Array<Dirent>, - suggestedPowers: number -): Promise<string> => { - const choices: Array<Choice> = [] - - // Make a 'Choice' for each ptau. - for (const ptauDirent of ptausDirents) { - const powers = extractPoTFromFilename(ptauDirent.name) - - if (powers >= suggestedPowers) - choices.push({ - title: ptauDirent.name, - value: ptauDirent.name - }) - } - - // Ask for selection. - const { ptau } = await prompts({ - type: "select", - name: "ptau", - message: theme.bold("Select the Powers of Tau file used to generate the zKey"), - choices, - initial: 0, - validate: (value) => - extractPoTFromFilename(value) >= suggestedPowers - ? true - : theme.red( - `${symbols.error} You must select a Powers of Tau file having an equal to or greater than ${suggestedPowers} amount of powers` - ) - }) - - if (!ptau) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) - - return ptau -} - -/** - * Prompt the list of opened ceremonies for selection. - * @param openedCeremoniesDocs <Array<FirebaseDocumentInfo>> - The uid and data of opened cerimonies documents. - * @returns Promise<FirebaseDocumentInfo> - */ -export const askForCeremonySelection = async ( - openedCeremoniesDocs: Array<FirebaseDocumentInfo> -): Promise<FirebaseDocumentInfo> => { - const choices: Array<Choice> = [] - - // Make a 'Choice' for each opened ceremony. - for (const ceremonyDoc of openedCeremoniesDocs) { - const now = Date.now() - const daysLeft = Math.ceil(Math.abs(now - ceremonyDoc.data.endDate) / (1000 * 60 * 60 * 24)) - - choices.push({ - title: ceremonyDoc.data.title, - description: `${ceremonyDoc.data.description} (${theme.magenta(daysLeft)} ${ - now - ceremonyDoc.data.endDate < 0 ? `days left` : `days gone since closing` - })`, - value: ceremonyDoc - }) - } - - // Ask for selection. - const { ceremony } = await prompts({ - type: "select", - name: "ceremony", - message: theme.bold("Select a ceremony"), - choices, - initial: 0 - }) - - if (!ceremony) showError(GENERIC_ERRORS.GENERIC_CEREMONY_SELECTION, true) - - return ceremony -} - -/** - * Prompt the list of circuits for a specific ceremony for selection. - * @param circuitsDocs <Array<FirebaseDocumentInfo>> - The uid and data of ceremony circuits. - * @returns Promise<FirebaseDocumentInfo> - */ -export const askForCircuitSelectionFromFirebase = async ( - circuitsDocs: Array<FirebaseDocumentInfo> -): Promise<FirebaseDocumentInfo> => { - const choices: Array<Choice> = [] - - // Make a 'Choice' for each circuit. - for (const circuitDoc of circuitsDocs) { - choices.push({ - title: `${circuitDoc.data.name}`, - description: `(#${theme.magenta(circuitDoc.data.sequencePosition)}) ${circuitDoc.data.description}`, - value: circuitDoc - }) - } - - // Ask for selection. - const { circuit } = await prompts({ - type: "select", - name: "circuit", - message: theme.bold("Select a circuit"), - choices, - initial: 0 - }) - - if (!circuit) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) - - return circuit -} - -/** - * Prompt for entropy or beacon. - * @param askEntropy <boolean> - true when requesting entropy; otherwise false. - * @returns <Promise<string>> - */ -export const askForEntropyOrBeacon = async (askEntropy: boolean): Promise<string> => { - const { entropyOrBeacon } = await prompts({ - type: "text", - name: "entropyOrBeacon", - style: `${askEntropy ? `password` : `text`}`, - message: theme.bold(`Provide ${askEntropy ? `some entropy` : `the final beacon`}`), - validate: (title: string) => - title.length > 0 || - theme.red(`${symbols.error} You must provide a valid value for the ${askEntropy ? `entropy` : `beacon`}!`) - }) - - if (!entropyOrBeacon) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - return entropyOrBeacon -} diff --git a/apps/phase2cli/src/lib/queries.ts b/apps/phase2cli/src/lib/queries.ts deleted file mode 100644 index 230ce305..00000000 --- a/apps/phase2cli/src/lib/queries.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { where } from "firebase/firestore" -import { FirebaseDocumentInfo, CeremonyState } from "../../types/index.js" -import { queryCollection, getAllCollectionDocs } from "./firebase.js" -import { - ceremoniesCollectionFields, - collections, - contributionsCollectionFields, - timeoutsCollectionFields -} from "./constants.js" -import { fromQueryToFirebaseDocumentInfo, getServerTimestampInMillis } from "./utils.js" -import { FIREBASE_ERRORS, showError } from "./errors.js" - -/** - * Query for closed ceremonies documents and return their data (if any). - * @returns <Promise<Array<FirebaseDocumentInfo>>> - */ -export const getClosedCeremonies = async (): Promise<Array<FirebaseDocumentInfo>> => { - let closedStateCeremoniesQuerySnap: any - - try { - closedStateCeremoniesQuerySnap = await queryCollection(collections.ceremonies, [ - where(ceremoniesCollectionFields.state, "==", CeremonyState.CLOSED), - where(ceremoniesCollectionFields.endDate, "<=", Date.now()) - ]) - - if (closedStateCeremoniesQuerySnap.empty && closedStateCeremoniesQuerySnap.size === 0) - showError(FIREBASE_ERRORS.FIREBASE_CEREMONY_NOT_CLOSED, true) - } catch (err: any) { - showError(err.toString(), true) - } - - return fromQueryToFirebaseDocumentInfo(closedStateCeremoniesQuerySnap.docs) -} - -/** - * Retrieve all ceremonies. - * @returns Promise<Array<FirebaseDocumentInfo>> - */ -export const getAllCeremonies = async (): Promise<Array<FirebaseDocumentInfo>> => - fromQueryToFirebaseDocumentInfo(await getAllCollectionDocs(`${collections.ceremonies}`)).sort( - (a: FirebaseDocumentInfo, b: FirebaseDocumentInfo) => a.data.sequencePosition - b.data.sequencePosition - ) - -/** - * Query for contribution from given participant for a given circuit (if any). - * @param ceremonyId <string> - the identifier of the ceremony. - * @param circuitId <string> - the identifier of the circuit. - * @param participantId <string> - the identifier of the participant. - * @returns <Promise<Array<FirebaseDocumentInfo>>> - */ -export const getCurrentContributorContribution = async ( - ceremonyId: string, - circuitId: string, - participantId: string -): Promise<Array<FirebaseDocumentInfo>> => { - const participantContributionQuerySnap = await queryCollection( - `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuitId}/${collections.contributions}`, - [where(contributionsCollectionFields.participantId, "==", participantId)] - ) - - return fromQueryToFirebaseDocumentInfo(participantContributionQuerySnap.docs) -} - -/** - * Query for circuits with a contribution from given participant. - * @param ceremonyId <string> - the identifier of the ceremony. - * @param circuits <Array<FirebaseDocumentInfo>> - the circuits of the ceremony - * @param participantId <string> - the identifier of the participant. - * @returns <Promise<Array<FirebaseDocumentInfo>>> - */ -export const getCircuitsWithParticipantContribution = async ( - ceremonyId: string, - circuits: Array<FirebaseDocumentInfo>, - participantId: string -): Promise<Array<string>> => { - const circuitsWithContributionIds: Array<string> = [] // nb. store circuit identifier. - - for (const circuit of circuits) { - const participantContributionQuerySnap = await queryCollection( - `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuit.id}/${collections.contributions}`, - [where(contributionsCollectionFields.participantId, "==", participantId)] - ) - - if (participantContributionQuerySnap.size === 1) circuitsWithContributionIds.push(circuit.id) - } - - return circuitsWithContributionIds -} - -/** - * Query for the active timeout from given participant for a given ceremony (if any). - * @param ceremonyId <string> - the identifier of the ceremony. - * @param participantId <string> - the identifier of the participant. - * @returns Promise<Array<FirebaseDocumentInfo>> - */ -export const getCurrentActiveParticipantTimeout = async ( - ceremonyId: string, - participantId: string -): Promise<Array<FirebaseDocumentInfo>> => { - const participantTimeoutQuerySnap = await queryCollection( - `${collections.ceremonies}/${ceremonyId}/${collections.participants}/${participantId}/${collections.timeouts}`, - [where(timeoutsCollectionFields.endDate, ">=", getServerTimestampInMillis())] - ) - - return fromQueryToFirebaseDocumentInfo(participantTimeoutQuerySnap.docs) -} diff --git a/apps/phase2cli/src/lib/storage.ts b/apps/phase2cli/src/lib/storage.ts deleted file mode 100644 index 93bfc211..00000000 --- a/apps/phase2cli/src/lib/storage.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { HttpsCallable } from "firebase/functions" -import fs from "fs" -import fetch from "@adobe/node-fetch-retry" -import { createWriteStream } from "node:fs" -import https from "https" -import { ChunkWithUrl, ETagWithPartNumber, ProgressBarType } from "../../types/index.js" -import { GENERIC_ERRORS, showError } from "./errors.js" -import { readLocalJsonFile } from "./files.js" -import { convertToGB, customProgressBar, sleep } from "./utils.js" - -// Get local configs. -const { config } = readLocalJsonFile("../../env.json") - -export const createS3Bucket = async (cf: HttpsCallable<unknown, unknown>, bucketName: string): Promise<boolean> => { - // Call createBucket() Cloud Function. - const response: any = await cf({ - bucketName - }) - - // Return true if exists, otherwise false. - return response.data -} - -/** - * Check if an object exists in a given AWS S3 bucket. - * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the identifier of the object. - * @returns Promise<string> - true if the object exists, otherwise false. - */ -export const objectExist = async ( - cf: HttpsCallable<unknown, unknown>, - bucketName: string, - objectKey: string -): Promise<boolean> => { - // Call checkIfObjectExist() Cloud Function. - const response: any = await cf({ - bucketName, - objectKey - }) - - // Return true if exists, otherwise false. - return response.data -} - -/** - * Initiate the multi part upload in AWS S3 Bucket for a large object. - * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the identifier of the object. - * @param ceremonyId <string> - the identifier of the ceremony. - * @returns Promise<string> - the Upload ID reference. - */ -export const openMultiPartUpload = async ( - cf: HttpsCallable<unknown, unknown>, - bucketName: string, - objectKey: string, - ceremonyId?: string -): Promise<string> => { - // Call startMultiPartUpload() Cloud Function. - const response: any = await cf({ - bucketName, - objectKey, - ceremonyId - }) - - // Return Multi Part Upload ID. - return response.data -} - -/** - * Get chunks and signed urls for a multi part upload. - * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the identifier of the object. - * @param filePath <string> - the local path where the file to be uploaded is located. - * @param uploadId <string> - the multi part upload unique identifier. - * @param expirationInSeconds <number> - the pre signed url expiration in seconds. - * @param ceremonyId <string> - the identifier of the ceremony. - * @returns Promise<Array, Array> - */ -export const getChunksAndPreSignedUrls = async ( - cf: HttpsCallable<unknown, unknown>, - bucketName: string, - objectKey: string, - filePath: string, - uploadId: string, - expirationInSeconds: number, - ceremonyId?: string -): Promise<Array<ChunkWithUrl>> => { - // Configuration checks. - if (!config.CONFIG_STREAM_CHUNK_SIZE_IN_MB) showError(GENERIC_ERRORS.GENERIC_NOT_CONFIGURED_PROPERLY, true) - - // Open a read stream. - const stream = fs.createReadStream(filePath, { highWaterMark: config.CONFIG_STREAM_CHUNK_SIZE_IN_MB * 1024 * 1024 }) - - // Read and store chunks. - const chunks = [] - for await (const chunk of stream) chunks.push(chunk) - - const numberOfParts = chunks.length - if (!numberOfParts) showError(GENERIC_ERRORS.GENERIC_FILE_ERROR, true) - - // Call generatePreSignedUrlsParts() Cloud Function. - const response: any = await cf({ - bucketName, - objectKey, - uploadId, - numberOfParts, - expirationInSeconds, - ceremonyId - }) - - return chunks.map((val1, index) => ({ - partNumber: index + 1, - chunk: val1, - preSignedUrl: response.data[index] - })) -} - -/** - * Make a PUT request to upload each part for a multi part upload. - * @param chunksWithUrls <Array<ChunkWithUrl>> - the array containing chunks and corresponding pre signed urls. - * @param contentType <string | false> - the content type of the file to upload. - * @param cf <HttpsCallable<unknown, unknown>> - the CF for enable resumable upload from last chunk by temporarily store the ETags and PartNumbers of already uploaded chunks. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param alreadyUploadedChunks <any> - the ETag and PartNumber temporary information about the already uploaded chunks. - * @returns <Promise<Array<ETagWithPartNumber>>> - */ -export const uploadParts = async ( - chunksWithUrls: Array<ChunkWithUrl>, - contentType: string | false, - cf?: HttpsCallable<unknown, unknown>, - ceremonyId?: string, - alreadyUploadedChunks?: any -): Promise<Array<ETagWithPartNumber>> => { - // PartNumber and ETags. - let partNumbersAndETags = [] - - // Restore the already uploaded chunks in the same order. - if (alreadyUploadedChunks) partNumbersAndETags = alreadyUploadedChunks - - // Resume from last uploaded chunk (0 for new multi-part upload). - const lastChunkIndex = partNumbersAndETags.length - - // Define a custom progress bar starting from last updated chunk. - const progressBar = customProgressBar(ProgressBarType.UPLOAD) - progressBar.start(chunksWithUrls.length, lastChunkIndex) - - for (let i = lastChunkIndex; i < chunksWithUrls.length; i += 1) { - // Make PUT call. - const putResponse = await fetch(chunksWithUrls[i].preSignedUrl, { - retryOptions: { - retryInitialDelay: 500, // 500 ms. - socketTimeout: 60000, // 60 seconds. - retryMaxDuration: 300000 // 5 minutes. - }, - method: "PUT", - body: chunksWithUrls[i].chunk, - headers: { - "Content-Type": contentType.toString(), - "Content-Length": chunksWithUrls[i].chunk.length.toString() - }, - agent: new https.Agent({ keepAlive: true }) - }) - - // Extract data. - const eTag = putResponse.headers.get("etag") - const { partNumber } = chunksWithUrls[i] - - // Store PartNumber and ETag. - partNumbersAndETags.push({ - ETag: eTag, - PartNumber: partNumber - }) - - // nb. to be done only when contributing. - if (!!ceremonyId && !!cf) - // Call CF to temporary store the chunks ETag and PartNumber info (useful for resumable upload). - await cf({ - ceremonyId, - eTag, - partNumber - }) - - // Increment the progress bar. - progressBar.increment(1) - } - - await sleep(1000) - - progressBar.stop() - - return partNumbersAndETags -} - -/** - * Close the multi part upload in AWS S3 Bucket for a large object. - * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the identifier of the object. - * @param uploadId <string> - the multi part upload unique identifier. - * @param parts Array<ETagWithPartNumber> - the uploaded parts. - * @param ceremonyId <string> - the identifier of the ceremony. - * @returns Promise<string> - the location of the uploaded file. - */ -export const closeMultiPartUpload = async ( - cf: HttpsCallable<unknown, unknown>, - bucketName: string, - objectKey: string, - uploadId: string, - parts: Array<ETagWithPartNumber>, - ceremonyId?: string -): Promise<string> => { - // Call completeMultiPartUpload() Cloud Function. - const response: any = await cf({ - bucketName, - objectKey, - uploadId, - parts, - ceremonyId - }) - - // Return uploaded file location. - return response.data -} - -/** - * Download locally a specified file from the given bucket. - * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the identifier of the object (storage path). - * @param localPath <string> - the path where the file will be written. - * @return <Promise<void>> - */ -export const downloadLocalFileFromBucket = async ( - cf: HttpsCallable<unknown, unknown>, - bucketName: string, - objectKey: string, - localPath: string -): Promise<void> => { - // Call generateGetObjectPreSignedUrl() Cloud Function. - const response: any = await cf({ - bucketName, - objectKey - }) - - // Get the pre-signed url. - const preSignedUrl = response.data - - // Get request. - const getResponse = await fetch(preSignedUrl) - - if (!getResponse.ok) showError(`${GENERIC_ERRORS.GENERIC_FILE_ERROR} - ${getResponse.statusText}`, true) - - const contentLength = Number(getResponse.headers.get(`content-length`)) - const contentLengthInGB = convertToGB(contentLength, true) - - // Create a new write stream. - const writeStream = createWriteStream(localPath) - - // Define a custom progress bar starting from last updated chunk. - const progressBar = customProgressBar(ProgressBarType.DOWNLOAD) - - // Progress bar step size. - const progressBarStepSize = contentLengthInGB / 100 - - let writtenData = 0 - let nextStepSize = progressBarStepSize - - // Init the progress bar. - progressBar.start(contentLengthInGB < 0.01 ? 0.01 : Number(contentLengthInGB.toFixed(2)), 0) - - // Write chunk by chunk. - for await (const chunk of getResponse.body) { - // Write. - writeStream.write(chunk) - - // Update. - writtenData += chunk.length - - // Check if the progress bar must advance. - while (convertToGB(writtenData, true) >= nextStepSize) { - // Update. - nextStepSize += progressBarStepSize - - // Increment bar. - progressBar.update(contentLengthInGB < 0.01 ? 0.01 : parseFloat(nextStepSize.toFixed(2)).valueOf()) - } - } - - await sleep(2000) // workaround for fs close. - - progressBar.stop() -} diff --git a/apps/phase2cli/src/lib/utils.ts b/apps/phase2cli/src/lib/utils.ts deleted file mode 100644 index 0858ea65..00000000 --- a/apps/phase2cli/src/lib/utils.ts +++ /dev/null @@ -1,1403 +0,0 @@ -import { request } from "@octokit/request" -import { DocumentData, QueryDocumentSnapshot, Timestamp } from "firebase/firestore" -import ora, { Ora } from "ora" -import figlet from "figlet" -import clear from "clear" -import { zKey } from "snarkjs" -import winston, { Logger } from "winston" -import { Functions, HttpsCallable, httpsCallable, httpsCallableFromURL } from "firebase/functions" -import { Timer } from "timer-node" -import mime from "mime-types" -import { getDiskInfoSync } from "node-disk-info" -import Drive from "node-disk-info/dist/classes/drive.js" -import open from "open" -import { Presets, SingleBar } from "cli-progress" -import { - FirebaseDocumentInfo, - FirebaseServices, - ParticipantContributionStep, - ParticipantStatus, - ProgressBarType, - Timing, - VerifyContributionComputation -} from "../../types/index.js" -import { collections, emojis, firstZkeyIndex, numIterationsExp, paths, symbols, theme } from "./constants.js" -import { initServices, uploadFileToStorage } from "./firebase.js" -import { GENERIC_ERRORS, GITHUB_ERRORS, showError } from "./errors.js" -import { askForConfirmation, askForEntropyOrBeacon } from "./prompts.js" -import { readFile, readLocalJsonFile, writeFile } from "./files.js" -import { - closeMultiPartUpload, - downloadLocalFileFromBucket, - getChunksAndPreSignedUrls, - openMultiPartUpload, - uploadParts -} from "./storage.js" -import { getAllCeremonies, getCurrentActiveParticipantTimeout, getCurrentContributorContribution } from "./queries.js" - -// Get local configs. -const { firebase, config } = readLocalJsonFile("../../env.json") - -/** - * Get the Github username for the logged in user. - * @param token <string> - the Github OAuth 2.0 token. - * @returns <Promise<string>> - the user Github username. - */ -export const getGithubUsername = async (token: string): Promise<string> => { - // Get user info from Github APIs. - const response = await request("GET https://api.github.com/user", { - headers: { - authorization: `token ${token}` - } - }) - - if (response) return response.data.login - showError(GITHUB_ERRORS.GITHUB_GET_USERNAME_FAILED, true) - - return process.exit(0) // nb. workaround to avoid type issues. -} - -/** - * Get the current amout of available memory for user root disk (mounted in `/` root). - * @returns <number> - the available memory in kB. - */ -export const getParticipantCurrentDiskAvailableSpace = (): number => { - const disks = getDiskInfoSync() - const root = disks.filter((disk: Drive) => disk.mounted === `/`) - - if (root.length !== 1) showError(`Something went wrong while retrieving your root disk available memory`, true) - - const rootDisk = root.at(0)! - - return rootDisk.available -} - -/** - * Convert bytes or chilobytes into gigabytes with customizable precision. - * @param bytesOrKB <number> - bytes or KB to be converted. - * @param isBytes <boolean> - true if the input is in bytes; otherwise false for KB input. - * @returns <number> - */ -export const convertToGB = (bytesOrKB: number, isBytes: boolean): number => - Number(bytesOrKB / 1024 ** (isBytes ? 3 : 2)) - -/** - * Return an array of true of false based on contribution verification result per each circuit. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param participantId <string> - the unique identifier of the contributor. - * @param circuits <Array<FirebaseDocumentInfo>> - the Firestore documents of the ceremony circuits. - * @param finalize <boolean> - true when finalizing; otherwise false. - * @returns <Promise<Array<boolean>>> - */ -export const getContributorContributionsVerificationResults = async ( - ceremonyId: string, - participantId: string, - circuits: Array<FirebaseDocumentInfo>, - finalize: boolean -): Promise<Array<boolean>> => { - // Keep track contributions verification results. - const contributions: Array<boolean> = [] - - // Retrieve valid/invalid contributions. - for await (const circuit of circuits) { - // Get contributions to circuit from contributor. - const contributionsToCircuit = await getCurrentContributorContribution(ceremonyId, circuit.id, participantId) - - let contribution: FirebaseDocumentInfo - - if (finalize) - // There should be two contributions from coordinator (one is finalization). - contribution = contributionsToCircuit - .filter((contribution: FirebaseDocumentInfo) => contribution.data.zkeyIndex === "final") - .at(0)! - // There will be only one contribution. - else contribution = contributionsToCircuit.at(0)! - - if (contribution) { - // Get data. - const contributionData = contribution.data - - if (!contributionData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - // Update contributions validity. - contributions.push(!!contributionData?.valid) - } - } - - return contributions -} - -/** - * Return the attestation made only from valid contributions. - * @param contributionsValidities Array<boolean> - an array of booleans (true when contribution is valid; otherwise false). - * @param circuits <Array<FirebaseDocumentInfo>> - the Firestore documents of the ceremony circuits. - * @param participantData <DocumentData> - the document data of the participant. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param participantId <string> - the unique identifier of the contributor. - * @param attestationPreamble <string> - the preamble of the attestation. - * @param finalize <boolean> - true only when finalizing, otherwise false. - * @returns <Promise<string>> - the complete attestation string. - */ -export const getValidContributionAttestation = async ( - contributionsValidities: Array<boolean>, - circuits: Array<FirebaseDocumentInfo>, - participantData: DocumentData, - ceremonyId: string, - participantId: string, - attestationPreamble: string, - finalize: boolean -): Promise<string> => { - let attestation = attestationPreamble - - // For each contribution validity. - for (let idx = 0; idx < contributionsValidities.length; idx += 1) { - if (contributionsValidities[idx]) { - // Extract data from circuit. - const circuit = circuits[idx] - - let contributionHash: string = "" - - // Get the contribution hash. - if (finalize) { - const numberOfContributions = participantData.contributions.length - contributionHash = participantData.contributions[numberOfContributions / 2 + idx].hash - } else contributionHash = participantData.contributions[idx].hash - - // Get the contribution data. - const contributions = await getCurrentContributorContribution(ceremonyId, circuit.id, participantId) - - let contributionData: DocumentData - - if (finalize) - contributionData = contributions.filter( - (contribution: FirebaseDocumentInfo) => contribution.data.zkeyIndex === "final" - )[0].data! - else contributionData = contributions.at(0)?.data! - - // Attestate. - attestation = `${attestation}\n\nCircuit # ${circuit.data.sequencePosition} (${ - circuit.data.prefix - })\nContributor # ${ - contributionData?.zkeyIndex > 0 ? Number(contributionData?.zkeyIndex) : contributionData?.zkeyIndex - }\n${contributionHash}` - } - } - - return attestation -} - -/** - * Publish a new attestation through a Github Gist. - * @param token <string> - the Github OAuth 2.0 token. - * @param content <string> - the content of the attestation. - * @param ceremonyPrefix <string> - the ceremony prefix. - * @param ceremonyTitle <string> - the ceremony title. - */ -export const publishGist = async ( - token: string, - content: string, - ceremonyPrefix: string, - ceremonyTitle: string -): Promise<string> => { - const response = await request("POST /gists", { - description: `Attestation for ${ceremonyTitle} MPC Phase 2 Trusted Setup ceremony`, - public: true, - files: { - [`${ceremonyPrefix}_attestation.txt`]: { - content - } - }, - headers: { - authorization: `token ${token}` - } - }) - - if (response && response.data.html_url) return response.data.html_url - showError(GITHUB_ERRORS.GITHUB_GIST_PUBLICATION_FAILED, true) - - return process.exit(0) // nb. workaround to avoid type issues. -} - -/** - * Helper for obtaining uid and data for query document snapshots. - * @param queryDocSnap <Array<QueryDocumentSnapshot>> - the array of query document snapshot to be converted. - * @returns Array<FirebaseDocumentInfo> - */ -export const fromQueryToFirebaseDocumentInfo = ( - queryDocSnap: Array<QueryDocumentSnapshot> -): Array<FirebaseDocumentInfo> => - queryDocSnap.map((doc: QueryDocumentSnapshot<DocumentData>) => ({ - id: doc.id, - ref: doc.ref, - data: doc.data() - })) - -/** - * Extract from milliseconds the seconds, minutes, hours and days. - * @param millis <number> - * @returns <Timing> - */ -export const getSecondsMinutesHoursFromMillis = (millis: number): Timing => { - // Get seconds from 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 - } -} - -/** - * Return a string with double digits if the amount is one digit only. - * @param amount <number> - * @returns <string> - */ -export const convertToDoubleDigits = (amount: number): string => (amount < 10 ? `0${amount}` : amount.toString()) - -/** - * Sleeps the function execution for given millis. - * @dev to be used in combination with loggers when writing data into files. - * @param ms <number> - sleep amount in milliseconds - * @returns <Promise<unknown>> - */ -export const sleep = (ms: number): Promise<unknown> => new Promise((resolve) => setTimeout(resolve, ms)) - -/** - * Return a custom spinner. - * @param text <string> - the text that should be displayed as spinner status. - * @param spinnerLogo <any> - the logo. - * @returns <Ora> - a new Ora custom spinner. - */ -export const customSpinner = (text: string, spinnerLogo: any): Ora => - ora({ - text, - spinner: spinnerLogo - }) - -/** - * Return a simple graphical loader to simulate loading or describe an asynchronous task. - * @param loadingText <string> - the text that should be displayed while the loader is spinning. - * @param logo <any> - the logo of the loader. - * @param durationInMillis <number> - the loader duration time in milliseconds. - * @param afterLoadingText <string> - the text that should be displayed for the loader stop. - * @returns <Promise<void>>. - */ -export const simpleLoader = async ( - loadingText: string, - logo: any, - durationInMillis: number, - afterLoadingText?: string -): Promise<void> => { - // Define the loader. - const loader = customSpinner(loadingText, logo) - - loader.start() - - // nb. wait for `durationInMillis` time while loader is spinning. - await sleep(durationInMillis) - - if (afterLoadingText) loader.succeed(afterLoadingText) - else loader.stop() -} - -/** - * Return a custom progress bar. - * @param type <ProgressBarType> - the type of the progress bar. - * @returns <SingleBar> - a new custom (single) progress bar. - */ -export const customProgressBar = (type: ProgressBarType): SingleBar => { - // Formats. - const uploadFormat = `${emojis.arrowUp} Uploading [${theme.magenta("{bar}")}] {percentage}% | {value}/{total} Chunks` - const downloadFormat = `${emojis.arrowDown} Downloading [${theme.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 - ) -} - -/** - * Return the bucket name based on ceremony prefix. - * @param ceremonyPrefix <string> - the ceremony prefix. - * @returns <string> - */ -export const getBucketName = (ceremonyPrefix: string): string => { - if (!config.CONFIG_CEREMONY_BUCKET_POSTFIX) showError(GENERIC_ERRORS.GENERIC_NOT_CONFIGURED_PROPERLY, true) - - return `${ceremonyPrefix}${config.CONFIG_CEREMONY_BUCKET_POSTFIX!}` -} - -/** - * Return the ceremonies prefixes for every ceremony. - * @returns Promise<Array<string>> - */ -export const getCreatedCeremoniesPrefixes = async (): Promise<Array<string>> => { - // Get all ceremonies documents. - const ceremonies = await getAllCeremonies() - - let ceremoniesPrefixes = [] - - // Return prefixes (if any ceremony). - if (ceremonies.length > 0) - ceremoniesPrefixes = ceremonies.map((ceremony: FirebaseDocumentInfo) => ceremony.data.prefix) - - return ceremoniesPrefixes -} - -/** - * Upload a file by subdividing it in chunks to AWS S3 bucket. - * @param startMultiPartUploadCF <HttpsCallable<unknown, unknown>> - the CF for initiating a multi part upload. - * @param generatePreSignedUrlsPartsCF <HttpsCallable<unknown, unknown>> - the CF for generating the pre-signed urls for each chunk. - * @param completeMultiPartUploadCF <HttpsCallable<unknown, unknown>> - the CF for completing a multi part upload. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the path of the object inside the AWS S3 bucket. - * @param localPath <string> - the local path of the file to be uploaded. - * @param temporaryStoreCurrentContributionMultiPartUploadId <HttpsCallable<unknown, unknown>> - the CF for enable resumable upload from last chunk by temporarily store the ETags and PartNumbers of already uploaded chunks. - * @param temporaryStoreCurrentContributionUploadedChunkData <HttpsCallable<unknown, unknown>> - the CF for enable resumable upload from last chunk by temporarily store the ETags and PartNumbers of already uploaded chunks. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param tempContributionData <any> - the temporary information necessary to resume an already started multi-part upload. - */ -export const multiPartUpload = async ( - startMultiPartUploadCF: HttpsCallable<unknown, unknown>, - generatePreSignedUrlsPartsCF: HttpsCallable<unknown, unknown>, - completeMultiPartUploadCF: HttpsCallable<unknown, unknown>, - bucketName: string, - objectKey: string, - localPath: string, - temporaryStoreCurrentContributionMultiPartUploadId?: HttpsCallable<unknown, unknown>, - temporaryStoreCurrentContributionUploadedChunkData?: HttpsCallable<unknown, unknown>, - ceremonyId?: string, - tempContributionData?: any -) => { - // Configuration checks. - if (!config.CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS) - showError(GENERIC_ERRORS.GENERIC_NOT_CONFIGURED_PROPERLY, true) - - // Get content type. - const contentType = mime.lookup(localPath) - - // The Multi-Part Upload unique identifier. - let uploadIdZkey = "" - // Already uploaded chunks temp info (nb. useful only when resuming). - let alreadyUploadedChunks = [] - - // Check if the contributor can resume an already started multi-part upload. - if (!tempContributionData || (!!tempContributionData && !tempContributionData.uploadId)) { - // Start from scratch. - const spinner = customSpinner(`Starting upload process...`, `clock`) - spinner.start() - - uploadIdZkey = await openMultiPartUpload(startMultiPartUploadCF, bucketName, objectKey, ceremonyId) - - if (temporaryStoreCurrentContributionMultiPartUploadId) - // Store Multi-Part Upload ID after generation. - await temporaryStoreCurrentContributionMultiPartUploadId({ - ceremonyId, - uploadId: uploadIdZkey - }) - - spinner.stop() - } else { - // Read temp info from Firestore. - uploadIdZkey = tempContributionData.uploadId - alreadyUploadedChunks = tempContributionData.chunks - } - - // Step 2 - const spinner = customSpinner(`Splitting file in chunks...`, `clock`) - spinner.start() - - const chunksWithUrlsZkey = await getChunksAndPreSignedUrls( - generatePreSignedUrlsPartsCF, - bucketName, - objectKey, - localPath, - uploadIdZkey, - config.CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS!, - ceremonyId - ) - - // Step 3 - const partNumbersAndETagsZkey = await uploadParts( - chunksWithUrlsZkey, - contentType, - temporaryStoreCurrentContributionUploadedChunkData, - ceremonyId, - alreadyUploadedChunks - ) - - // Step 4 - spinner.text = `Completing upload...` - spinner.start() - - await closeMultiPartUpload( - completeMultiPartUploadCF, - bucketName, - objectKey, - uploadIdZkey, - partNumbersAndETagsZkey, - ceremonyId - ) - - spinner.stop() -} - -/** - * Get a value from a key information about a circuit. - * @param circuitInfo <string> - the stringified content of the .r1cs file. - * @param rgx <RegExp> - regular expression to match the key. - * @returns <string> - */ -export const getCircuitMetadataFromR1csFile = (circuitInfo: string, rgx: RegExp): string => { - // Match. - const matchInfo = circuitInfo.match(rgx) - - if (!matchInfo) showError(GENERIC_ERRORS.GENERIC_R1CS_MISSING_INFO, true) - - // Split and return the value. - return matchInfo?.at(0)?.split(":")[1].replace(" ", "").split("#")[0].replace("\n", "")! -} - -/** - * Return the necessary Power of Tau "powers" given the number of circuits constraints. - * @param constraints <number> - the number of circuit contraints. - * @param outputs <number> - the number of circuit outputs. - * @returns <number> - */ -export const estimatePoT = (constraints: number, outputs: number): number => { - let power = 2 - let pot = 2 ** power - - while (constraints + outputs > pot) { - power += 1 - pot = 2 ** power - } - - return power -} - -/** - * Get the powers from pot file name - * @dev the pot files must follow these convention (i_am_a_pot_file_09.ptau) where the numbers before '.ptau' are the powers. - * @param potFileName <string> - * @returns <number> - */ -export const extractPoTFromFilename = (potFileName: string): number => - Number(potFileName.split("_").pop()?.split(".").at(0)) - -/** - * Extract a prefix (like_this) from a provided string with special characters and spaces. - * @dev replaces all symbols and whitespaces with underscore. - * @param str <string> - * @returns <string> - */ -export const extractPrefix = (str: string): string => - // eslint-disable-next-line no-useless-escape - str.replace(/[`\s~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "-").toLowerCase() - -/** - * Format the next zkey index. - * @param progress <number> - the progression in zkey index (= contributions). - * @returns <string> - */ -export const formatZkeyIndex = (progress: number): string => { - let index = progress.toString() - - while (index.length < firstZkeyIndex.length) { - index = `0${index}` - } - - return index -} - -/** - * Convert milliseconds to seconds. - * @param millis <number> - * @returns <number> - */ -export const convertMillisToSeconds = (millis: number): number => Number((millis / 1000).toFixed(2)) - -/** - * Return the current server timestamp in milliseconds. - * @returns <number> - */ -export const getServerTimestampInMillis = (): number => Timestamp.now().toMillis() - -/** - * Bootstrap whatever is needed for a new command execution (clean terminal, print header, init Firebase services). - * @returns <Promise<FirebaseServices>> - */ -export const bootstrapCommandExec = async (): Promise<FirebaseServices> => { - // Clean terminal window. - clear() - - // Print header. - console.log(theme.magenta(figlet.textSync("Phase 2 cli", { font: "Ogre" }))) - - // Initialize Firebase services - return initServices() -} - -/** - * Gracefully terminate the command execution - * @params ghUsername <string> - the Github username of the user. - */ -export const terminate = async (ghUsername: string) => { - console.log(`\nSee you, ${theme.bold(`@${ghUsername}`)} ${emojis.wave}`) - - process.exit(0) -} - -/** - * Make a new countdown and throws an error when time is up. - * @param durationInSeconds <number> - the amount of time to be counted in seconds. - * @param intervalInSeconds <number> - update interval in seconds. - */ -export const createExpirationCountdown = (durationInSeconds: number, intervalInSeconds: number) => { - let seconds = durationInSeconds <= 60 ? durationInSeconds : 60 - - setInterval(() => { - try { - if (durationInSeconds !== 0) { - // Update times. - durationInSeconds -= intervalInSeconds - seconds -= intervalInSeconds - - if (seconds % 60 === 0) seconds = 0 - - process.stdout.write( - `${symbols.warning} Expires in ${theme.bold( - theme.magenta(`00:${Math.floor(durationInSeconds / 60)}:${seconds}`) - )}\r` - ) - } else showError(GENERIC_ERRORS.GENERIC_COUNTDOWN_EXPIRED, true) - } catch (err: any) { - // Workaround to the \r. - process.stdout.write(`\n\n`) - showError(GENERIC_ERRORS.GENERIC_COUNTDOWN_EXPIRATION, true) - } - }, intervalInSeconds * 1000) -} - -/** - * Create and return a simple countdown for a specified amount of time. - * @param remainingTime <number> - the amount of time to be counted. - * @param message <string> - the message to be shown. - * @returns <NodeJS.Timer> - */ -export const simpleCountdown = (remainingTime: number, message: string): NodeJS.Timer => - setInterval(() => { - remainingTime -= 1000 - - const { - seconds: cdSeconds, - minutes: cdMinutes, - hours: cdHours - } = getSecondsMinutesHoursFromMillis(Math.abs(remainingTime)) - - process.stdout.write( - `${message} (${remainingTime < 0 ? theme.bold(`-`) : ``}${convertToDoubleDigits(cdHours)}:${convertToDoubleDigits( - cdMinutes - )}:${convertToDoubleDigits(cdSeconds)})\r` - ) - }, 1000) - -/** - * Handle the request/generation for a random entropy or beacon value. - * @param askEntropy <boolean> - true when requesting entropy; otherwise false. - * @return <Promise<string>> - */ -export const getEntropyOrBeacon = async (askEntropy: boolean): Promise<string> => { - let entropyOrBeacon: any - let randomEntropy = false - - if (askEntropy) { - // Prompt for entropy. - const { confirmation } = await askForConfirmation(`Do you prefer to enter entropy manually?`) - if (confirmation === undefined) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) - - randomEntropy = !confirmation - } - - if (randomEntropy) { - const spinner = customSpinner(`Generating random entropy...`, "clock") - spinner.start() - - // Took inspiration from here https://github.com/glamperd/setup-mpc-ui/blob/master/client/src/state/Compute.tsx#L112. - entropyOrBeacon = new Uint8Array(256).map(() => Math.random() * 256).toString() - - spinner.succeed(`Random entropy successfully generated`) - } - - if (!askEntropy || !randomEntropy) entropyOrBeacon = await askForEntropyOrBeacon(askEntropy) - - return entropyOrBeacon -} - -/** - * Manage the communication of timeout-related messages for a contributor. - * @param participantData <DocumentData> - the data of the participant document. - * @param participantId <string> - the unique identifier of the contributor. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @param isContributing <boolean> - * @param ghUsername <string> - */ -export const handleTimedoutMessageForContributor = async ( - participantData: DocumentData, - participantId: string, - ceremonyId: string, - isContributing: boolean, - ghUsername: string -): Promise<void> => { - // Extract data. - const { status, contributionStep, contributionProgress } = participantData - - // Check if the contributor has been timedout. - if (status === ParticipantStatus.TIMEDOUT && contributionStep !== ParticipantContributionStep.COMPLETED) { - if (!isContributing) console.log(theme.bold(`\n- Circuit # ${theme.magenta(contributionProgress)}`)) - else process.stdout.write(`\n`) - - console.log( - `${symbols.error} ${ - isContributing ? `You have been timedout while contributing` : `Timeout still in progress.` - }\n\n${ - symbols.warning - } This can happen due to network or memory issues, un/intentional crash, or contributions lasting for too long.` - ) - - // nb. workaround to retrieve the latest timeout data from the database. - await simpleLoader(`Checking timeout...`, `clock`, 1000) - - // Check when the participant will be able to retry the contribution. - const activeTimeouts = await getCurrentActiveParticipantTimeout(ceremonyId, participantId) - - if (activeTimeouts.length !== 1) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - const activeTimeoutData = activeTimeouts.at(0)?.data - - if (!activeTimeoutData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis( - activeTimeoutData?.endDate - getServerTimestampInMillis() - ) - - console.log( - `${symbols.info} You can retry your contribution in ${theme.bold( - `${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits( - minutes - )}:${convertToDoubleDigits(seconds)}` - )} (dd/hh/mm/ss)` - ) - - terminate(ghUsername) - } -} - -/** - * Compute a new Groth 16 Phase 2 contribution. - * @param lastZkey <string> - the local path to last zkey. - * @param newZkey <string> - the local path to new zkey. - * @param name <string> - the name of the contributor. - * @param entropyOrBeacon <string> - the value representing the entropy or beacon. - * @param logger <Logger | Console> - custom winston or console logger. - * @param finalize <boolean> - true when finalizing the ceremony with the last contribution; otherwise false. - * @param contributionComputationTime <number> - the contribution computation time in milliseconds for the circuit. - */ -export const computeContribution = async ( - lastZkey: string, - newZkey: string, - name: string, - entropyOrBeacon: string, - logger: Logger | Console, - finalize: boolean, - contributionComputationTime: number -) => { - // Format average contribution time. - const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(contributionComputationTime) - - // Custom spinner for visual feedback. - const text = `${finalize ? `Applying beacon...` : `Computing contribution...`} ${ - contributionComputationTime > 0 - ? `(ETA ${theme.bold( - `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}` - )} |` - : `` - }` - - let counter = 0 - - // Format time. - const { - seconds: counterSeconds, - minutes: counterMinutes, - hours: counterHours - } = getSecondsMinutesHoursFromMillis(counter) - - const spinner = customSpinner( - `${text} ${convertToDoubleDigits(counterHours)}:${convertToDoubleDigits(counterMinutes)}:${convertToDoubleDigits( - counterSeconds - )})\r`, - `clock` - ) - spinner.start() - - const interval = setInterval(() => { - counter += 1000 - - const { - seconds: counterSeconds, - minutes: counterMinutes, - hours: counterHours - } = getSecondsMinutesHoursFromMillis(counter) - - spinner.text = `${text} ${convertToDoubleDigits(counterHours)}:${convertToDoubleDigits( - counterMinutes - )}:${convertToDoubleDigits(counterSeconds)})\r` - }, 1000) - - if (finalize) - // Finalize applying a random beacon. - await zKey.beacon(lastZkey, newZkey, name, entropyOrBeacon, numIterationsExp, logger) - // Compute the next contribution. - else await zKey.contribute(lastZkey, newZkey, name, entropyOrBeacon, logger) - - // nb. workaround to logger descriptor close. - await sleep(1000) - - spinner.stop() - clearInterval(interval) -} - -/** - * Create a custom logger. - * @dev useful for keeping track of `info` logs from snarkjs and use them to generate the contribution transcript. - * @param transcriptFilename <string> - logger output file. - * @returns <Logger> - */ -export const getTranscriptLogger = (transcriptFilename: string): Logger => - // Create a custom logger. - winston.createLogger({ - level: "info", - format: winston.format.printf((log) => log.message), - transports: [ - // Write all logs with importance level of `info` to `transcript.json`. - new winston.transports.File({ - filename: transcriptFilename, - level: "info" - }) - ] - }) - -/** - * Make a progress to the next contribution step for the current contributor. - * @param firebaseFunctions <Functions> - the object containing the firebase functions. - * @param ceremonyId <string> - the ceremony unique identifier. - * @param showSpinner <boolean> - true to show a custom spinner on the terminal; otherwise false. - * @param message <string> - custom message string based on next contribution step value. - */ -export const makeContributionStepProgress = async ( - firebaseFunctions: Functions, - ceremonyId: string, - showSpinner: boolean, - message: string -) => { - // Get CF. - const progressToNextContributionStep = httpsCallable(firebaseFunctions, "progressToNextContributionStep") - - // Custom spinner for visual feedback. - const spinner: Ora = customSpinner(`Getting ready for ${message} step`, "clock") - - if (showSpinner) spinner.start() - - // Progress to next contribution step. - await progressToNextContributionStep({ ceremonyId }) - - if (showSpinner) spinner.stop() -} - -/** - * Return the next circuit where the participant needs to compute or has computed the contribution. - * @param circuits <Array<FirebaseDocumentInfo>> - the ceremony circuits document. - * @param nextCircuitPosition <number> - the position in the sequence of circuits where the next contribution must be done. - * @returns <FirebaseDocumentInfo> - */ -export const getNextCircuitForContribution = ( - circuits: Array<FirebaseDocumentInfo>, - nextCircuitPosition: number -): FirebaseDocumentInfo => { - // Filter for sequence position (should match contribution progress). - const filteredCircuits = circuits.filter( - (circuit: FirebaseDocumentInfo) => circuit.data.sequencePosition === nextCircuitPosition - ) - - // There must be only one. - if (filteredCircuits.length !== 1) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - return filteredCircuits.at(0)! -} - -/** - * Return the memory space requirement for a zkey in GB. - * @param zKeySizeInBytes <number> - the size of the zkey in bytes. - * @returns <number> - */ -export const getZkeysSpaceRequirementsForContributionInGB = (zKeySizeInBytes: number): number => - // nb. mul per 2 is necessary because download latest + compute newest. - convertToGB(zKeySizeInBytes * 2, true) - -/** - * Return the available disk space of the current contributor in GB. - * @returns <number> - */ -export const getContributorAvailableDiskSpaceInGB = (): number => - convertToGB(getParticipantCurrentDiskAvailableSpace(), false) - -/** - * Check if the contributor has enough space before starting the contribution for next circuit. - * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. - * @param nextCircuit <FirebaseDocumentInfo> - the circuit document. - * @param ceremonyId <string> - the unique identifier of the ceremony. - * @return <Promise<void>> - */ -export const handleDiskSpaceRequirementForNextContribution = async ( - cf: HttpsCallable<unknown, unknown>, - nextCircuit: FirebaseDocumentInfo, - ceremonyId: string -): Promise<boolean> => { - // Get memory info. - const zKeysSpaceRequirementsInGB = getZkeysSpaceRequirementsForContributionInGB(nextCircuit.data.zKeySizeInBytes) - const availableDiskSpaceInGB = getContributorAvailableDiskSpaceInGB() - - // Extract data. - const { sequencePosition } = nextCircuit.data - - process.stdout.write(`\n`) - - await simpleLoader(`Checking your memory...`, `clock`, 1000) - - // Check memory requirement. - if (availableDiskSpaceInGB < zKeysSpaceRequirementsInGB) { - console.log(theme.bold(`- Circuit # ${theme.magenta(`${sequencePosition}`)}`)) - - console.log( - `${symbols.error} You do not have enough memory to make a contribution (Required ${ - zKeysSpaceRequirementsInGB < 0.01 ? theme.bold(`< 0.01`) : theme.bold(zKeysSpaceRequirementsInGB) - } GB (available ${ - availableDiskSpaceInGB > 0 ? theme.bold(availableDiskSpaceInGB.toFixed(2)) : theme.bold(0) - } GB)\n` - ) - - if (sequencePosition > 1) { - // The user has computed at least one valid contribution. Therefore, can choose if free up memory and contrinue with next contribution or generate the final attestation. - console.log( - `${symbols.info} You have time until ceremony ends to free up your memory, complete contributions and publish the attestation` - ) - - const { confirmation } = await askForConfirmation( - `Are you sure you want to generate and publish the attestation for your contributions?` - ) - - if (!confirmation) { - process.stdout.write(`\n`) - - // nb. here the user is not able to generate an attestation because does not have contributed yet. Therefore, return an error and exit. - showError(`Please, free up your disk space and run again this command to contribute`, true) - } - } else { - // nb. here the user is not able to generate an attestation because does not have contributed yet. Therefore, return an error and exit. - showError(`Please, free up your disk space and run again this command to contribute`, true) - } - } else { - console.log( - `${symbols.success} You have enough memory for contributing to ${theme.bold( - `Circuit ${theme.magenta(sequencePosition)}` - )}` - ) - - const spinner = customSpinner( - `Joining ${theme.bold(`Circuit ${theme.magenta(sequencePosition)}`)} waiting queue...`, - `clock` - ) - spinner.start() - - await cf({ ceremonyId }) - - spinner.succeed(`All set for contribution to ${theme.bold(`Circuit ${theme.magenta(sequencePosition)}`)}`) - - return false - } - - return true -} - -/** - * Generate the public attestation for the contributor. - * @param ceremonyDoc <FirebaseDocumentInfo> - the ceremony document. - * @param participantId <string> - the unique identifier of the participant. - * @param participantData <DocumentData> - the data of the participant document. - * @param circuits <Array<FirebaseDocumentInfo> - the ceremony circuits documents. - * @param ghUsername <string> - the Github username of the contributor. - * @param ghToken <string> - the Github access token of the contributor. - */ -export const generatePublicAttestation = async ( - ceremonyDoc: FirebaseDocumentInfo, - participantId: string, - participantData: DocumentData, - circuits: Array<FirebaseDocumentInfo>, - ghUsername: string, - ghToken: string -): Promise<void> => { - // Attestation preamble. - const attestationPreamble = `Hey, I'm ${ghUsername} and I have contributed to the ${ceremonyDoc.data.title} MPC Phase2 Trusted Setup ceremony.\nThe following are my contribution signatures:` - - // Return true and false based on contribution verification. - const contributionsValidity = await getContributorContributionsVerificationResults( - ceremonyDoc.id, - participantId, - circuits, - false - ) - const numberOfValidContributions = contributionsValidity.filter(Boolean).length - - console.log( - `\nCongrats, you have successfully contributed to ${theme.magenta( - theme.bold(numberOfValidContributions) - )} out of ${theme.magenta(theme.bold(circuits.length))} circuits ${emojis.tada}` - ) - - // Show valid/invalid contributions per each circuit. - let idx = 0 - - for (const contributionValidity of contributionsValidity) { - console.log( - `${contributionValidity ? symbols.success : symbols.error} ${theme.bold(`Circuit`)} ${theme.bold( - theme.magenta(idx + 1) - )}` - ) - idx += 1 - } - - process.stdout.write(`\n`) - - const spinner = customSpinner("Uploading public attestation...", "clock") - spinner.start() - - // Get only valid contribution hashes. - const attestation = await getValidContributionAttestation( - contributionsValidity, - circuits, - participantData!, - ceremonyDoc.id, - participantId, - attestationPreamble, - false - ) - - writeFile(`${paths.attestationPath}/${ceremonyDoc.data.prefix}_attestation.log`, Buffer.from(attestation)) - await sleep(1000) - - // TODO: If fails for permissions problems, ask to do manually. - const gistUrl = await publishGist(ghToken, attestation, ceremonyDoc.data.prefix, ceremonyDoc.data.title) - - spinner.succeed( - `Public attestation successfully published as Github Gist at this link ${theme.bold(theme.underlined(gistUrl))}` - ) - - // Attestation link via Twitter. - const attestationTweet = `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyDoc.data.title}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20contribute%20here:%20https://github.com/quadratic-funding/mpc-phase2-suite%20You%20can%20view%20my%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP` - - console.log( - `\nWe appreciate your contribution to preserving the ${ceremonyDoc.data.title} security! ${ - emojis.key - } You can tweet about your participation if you'd like (click on the link below ${ - emojis.pointDown - }) \n\n${theme.underlined(attestationTweet)}` - ) - - await open(attestationTweet) -} - -/** - * Download a local copy of the zkey. - * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. - * @param bucketName <string> - the name of the AWS S3 bucket. - * @param objectKey <string> - the identifier of the object (storage path). - * @param localPath <string> - the path where the file will be written. - * @param showSpinner <boolean> - true to show a custom spinner on the terminal; otherwise false. - */ -export const downloadContribution = async ( - cf: HttpsCallable<unknown, unknown>, - bucketName: string, - objectKey: string, - localPath: string, - showSpinner: boolean -) => { - // Custom spinner for visual feedback. - const spinner: Ora = customSpinner(`Downloading contribution...`, "clock") - - if (showSpinner) spinner.start() - - // Download from storage. - await downloadLocalFileFromBucket(cf, bucketName, objectKey, localPath) - - if (showSpinner) spinner.stop() -} - -/** - * Upload the new zkey to the storage. - * @param storagePath <string> - the Storage path where the zkey will be stored. - * @param localPath <string> - the local path where the zkey is stored. - * @param showSpinner <boolean> - true to show a custom spinner on the terminal; otherwise false. - */ -export const uploadContribution = async (storagePath: string, localPath: string, showSpinner: boolean) => { - // Custom spinner for visual feedback. - const spinner = customSpinner("Storing your contribution...", "clock") - if (showSpinner) spinner.start() - - // Upload to storage. - await uploadFileToStorage(localPath, storagePath) - - if (showSpinner) spinner.stop() -} - -/** - * Compute a new Groth16 contribution verification. - * @param ceremony <FirebaseDocumentInfo> - the ceremony document. - * @param circuit <FirebaseDocumentInfo> - the circuit document. - * @param ghUsername <string> - the Github username of the user. - * @param avgVerifyCloudFunctionTime <number> - the average verify Cloud Function execution time in milliseconds. - * @param firebaseFunctions <Functions> - the object containing the firebase functions. - * @returns <Promise<VerifyContributionComputation>> - */ -export const computeVerification = async ( - ceremony: FirebaseDocumentInfo, - circuit: FirebaseDocumentInfo, - ghUsername: string, - avgVerifyCloudFunctionTime: number, - firebaseFunctions: Functions -): Promise<VerifyContributionComputation> => { - // Format average verification time. - const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(avgVerifyCloudFunctionTime) - - // Custom spinner for visual feedback. - const spinner = customSpinner( - `Verifying your contribution... ${ - avgVerifyCloudFunctionTime > 0 - ? `(est. time ${theme.bold( - `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}` - )})` - : `` - }\n`, - "clock" - ) - - spinner.start() - - // Verify contribution callable Cloud Function. - const verifyContribution = httpsCallableFromURL(firebaseFunctions!, firebase.FIREBASE_CF_URL_VERIFY_CONTRIBUTION!, { - timeout: 3600000 - }) - - // The verification must be done remotely (Cloud Functions). - const response = await verifyContribution({ - ceremonyId: ceremony.id, - circuitId: circuit.id, - ghUsername, - bucketName: getBucketName(ceremony.data.prefix) - }) - - spinner.stop() - - if (!response) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - - const { data }: any = response - - return { - valid: data.valid, - verificationComputationTime: data.verificationComputationTime, - verifyCloudFunctionTime: data.verifyCloudFunctionTime, - fullContributionTime: data.fullContributionTime - } -} - -/** - * Compute a new contribution for the participant. - * @param ceremony <FirebaseDocumentInfo> - the ceremony document. - * @param circuit <FirebaseDocumentInfo> - the circuit document. - * @param entropyOrBeacon <any> - the entropy/beacon for the contribution. - * @param ghUsername <string> - the Github username of the user. - * @param finalize <boolean> - true if the contribution finalize the ceremony; otherwise false. - * @param firebaseFunctions <Functions> - the object containing the firebase functions. - * @param newParticipantData <DocumentData> - the object containing the participant data. - * @returns <Promise<string>> - new updated attestation file. - */ -export const makeContribution = async ( - ceremony: FirebaseDocumentInfo, - circuit: FirebaseDocumentInfo, - entropyOrBeacon: any, - ghUsername: string, - finalize: boolean, - firebaseFunctions: Functions, - newParticipantData?: DocumentData -): Promise<void> => { - // Extract data from circuit. - const currentProgress = circuit.data.waitingQueue.completedContributions - const { avgTimings } = circuit.data - - // Compute zkey indexes. - const currentZkeyIndex = formatZkeyIndex(currentProgress) - const nextZkeyIndex = formatZkeyIndex(currentProgress + 1) - - // Paths config. - const transcriptsPath = finalize ? paths.finalTranscriptsPath : paths.contributionTranscriptsPath - const contributionsPath = finalize ? paths.finalZkeysPath : paths.contributionsPath - - // Get custom transcript logger. - const contributionTranscriptLocalPath = `${transcriptsPath}/${circuit.data.prefix}_${ - finalize ? `${ghUsername}_final` : nextZkeyIndex - }.log` - const transcriptLogger = getTranscriptLogger(contributionTranscriptLocalPath) - const bucketName = getBucketName(ceremony.data.prefix) - - // Write first message. - transcriptLogger.info( - `${finalize ? `Final` : `Contribution`} transcript for ${circuit.data.prefix} phase 2 contribution.\n${ - finalize ? `Coordinator: ${ghUsername}` : `Contributor # ${Number(nextZkeyIndex)}` - } (${ghUsername})\n` - ) - - console.log( - `${theme.bold(`\n- Circuit # ${theme.magenta(`${circuit.data.sequencePosition}`)}`)} (Contribution Steps)` - ) - - if ( - finalize || - (!!newParticipantData?.contributionStep && - newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) - ) { - const spinner = customSpinner(`Preparing for download...`, `clock`) - spinner.start() - - // 1. Download last contribution. - const storagePath = `${collections.circuits}/${circuit.data.prefix}/${collections.contributions}/${circuit.data.prefix}_${currentZkeyIndex}.zkey` - const localPath = `${contributionsPath}/${circuit.data.prefix}_${currentZkeyIndex}.zkey` - - // Download w/ Presigned urls. - const generateGetObjectPreSignedUrl = httpsCallable(firebaseFunctions!, "generateGetObjectPreSignedUrl") - - spinner.stop() - - await downloadContribution(generateGetObjectPreSignedUrl, bucketName, storagePath, localPath, false) - - console.log(`${symbols.success} Contribution ${theme.bold(`#${currentZkeyIndex}`)} correctly downloaded`) - - // Make the step if not finalizing. - if (!finalize) await makeContributionStepProgress(firebaseFunctions!, ceremony.id, true, "computation") - } else console.log(`${symbols.success} Contribution ${theme.bold(`#${currentZkeyIndex}`)} already downloaded`) - - if ( - finalize || - (!!newParticipantData?.contributionStep && - newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) || - newParticipantData?.contributionStep === ParticipantContributionStep.COMPUTING - ) { - const contributionComputationTimer = new Timer({ label: "contributionComputation" }) // Compute time (only for statistics). - - // 2.A Compute the new contribution. - contributionComputationTimer.start() - - await computeContribution( - `${contributionsPath}/${circuit.data.prefix}_${currentZkeyIndex}.zkey`, - `${contributionsPath}/${circuit.data.prefix}_${finalize ? `final` : nextZkeyIndex}.zkey`, - ghUsername, - entropyOrBeacon, - transcriptLogger, - finalize, - avgTimings.contributionComputation - ) - - contributionComputationTimer.stop() - - const contributionComputationTime = contributionComputationTimer.ms() - - const spinner = customSpinner(`Storing contribution time and hash...`, `clock`) - spinner.start() - - // nb. workaround for file descriptor close. - await sleep(2000) - - // 2.B Generate attestation from single contribution transcripts from each circuit (queue this contribution). - const transcript = readFile(contributionTranscriptLocalPath) - - const matchContributionHash = transcript.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/) - - if (!matchContributionHash) showError(GENERIC_ERRORS.GENERIC_CONTRIBUTION_HASH_INVALID, true) - - const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "")! - - const permanentlyStoreCurrentContributionTimeAndHash = httpsCallable( - firebaseFunctions!, - "permanentlyStoreCurrentContributionTimeAndHash" - ) - - await permanentlyStoreCurrentContributionTimeAndHash({ - ceremonyId: ceremony.id, - contributionComputationTime, - contributionHash - }) - - const { - seconds: computationSeconds, - minutes: computationMinutes, - hours: computationHours - } = getSecondsMinutesHoursFromMillis(contributionComputationTime) - - spinner.succeed( - `${finalize ? "Contribution" : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}`} computation took ${theme.bold( - `${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits( - computationMinutes - )}:${convertToDoubleDigits(computationSeconds)}` - )}` - ) - - // Make the step if not finalizing. - if (!finalize) await makeContributionStepProgress(firebaseFunctions!, ceremony.id, true, "upload") - } else console.log(`${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} already computed`) - - if ( - finalize || - (!!newParticipantData?.contributionStep && - newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) || - newParticipantData?.contributionStep === ParticipantContributionStep.COMPUTING || - newParticipantData?.contributionStep === ParticipantContributionStep.UPLOADING - ) { - // 3. Store file. - const storagePath = `${collections.circuits}/${circuit.data.prefix}/${collections.contributions}/${ - circuit.data.prefix - }_${finalize ? `final` : nextZkeyIndex}.zkey` - const localPath = `${contributionsPath}/${circuit.data.prefix}_${finalize ? `final` : nextZkeyIndex}.zkey` - - // Upload. - const startMultiPartUpload = httpsCallable(firebaseFunctions, "startMultiPartUpload") - const generatePreSignedUrlsParts = httpsCallable(firebaseFunctions, "generatePreSignedUrlsParts") - const completeMultiPartUpload = httpsCallable(firebaseFunctions, "completeMultiPartUpload") - - if (!finalize) { - const temporaryStoreCurrentContributionMultiPartUploadId = httpsCallable( - firebaseFunctions, - "temporaryStoreCurrentContributionMultiPartUploadId" - ) - const temporaryStoreCurrentContributionUploadedChunk = httpsCallable( - firebaseFunctions, - "temporaryStoreCurrentContributionUploadedChunkData" - ) - - await multiPartUpload( - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload, - bucketName, - storagePath, - localPath, - temporaryStoreCurrentContributionMultiPartUploadId, - temporaryStoreCurrentContributionUploadedChunk, - ceremony.id, - newParticipantData?.tempContributionData - ) - } else - await multiPartUpload( - startMultiPartUpload, - generatePreSignedUrlsParts, - completeMultiPartUpload, - bucketName, - storagePath, - localPath - ) - - console.log( - `${symbols.success} ${ - finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` - } correctly saved on storage` - ) - - // Make the step if not finalizing. - if (!finalize) await makeContributionStepProgress(firebaseFunctions!, ceremony.id, true, "verification") - } else - console.log( - `${symbols.success} ${ - finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` - } already saved on storage` - ) - - if ( - finalize || - (!!newParticipantData?.contributionStep && - newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) || - newParticipantData?.contributionStep === ParticipantContributionStep.COMPUTING || - newParticipantData?.contributionStep === ParticipantContributionStep.UPLOADING || - newParticipantData?.contributionStep === ParticipantContributionStep.VERIFYING - ) { - // 5. Verify contribution. - const { valid, verifyCloudFunctionTime, fullContributionTime } = await computeVerification( - ceremony, - circuit, - ghUsername, - avgTimings.verifyCloudFunction, - firebaseFunctions - ) - - const { - seconds: verificationSeconds, - minutes: verificationMinutes, - hours: verificationHours - } = getSecondsMinutesHoursFromMillis(verifyCloudFunctionTime) - - console.log( - `${valid ? symbols.success : symbols.error} ${ - finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` - } ${valid ? `is ${theme.bold("VALID")}` : `is ${theme.bold("INVALID")}`}` - ) - console.log( - `${symbols.success} ${ - finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` - } verification took ${theme.bold( - `${convertToDoubleDigits(verificationHours)}:${convertToDoubleDigits( - verificationMinutes - )}:${convertToDoubleDigits(verificationSeconds)}` - )}` - ) - - const { - seconds: contributionSeconds, - minutes: contributionMinutes, - hours: contributionHours - } = getSecondsMinutesHoursFromMillis(fullContributionTime + verifyCloudFunctionTime) - console.log( - `${symbols.info} Your contribution took ${theme.bold( - `${convertToDoubleDigits(contributionHours)}:${convertToDoubleDigits( - contributionMinutes - )}:${convertToDoubleDigits(contributionSeconds)}` - )}` - ) - } -} diff --git a/apps/phase2cli/test/index.test.ts b/apps/phase2cli/test/index.test.ts deleted file mode 100644 index a8fc65bd..00000000 --- a/apps/phase2cli/test/index.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe("feature 1", () => { - it("should console log 'Hello, World!'", () => { - console.log("Hello, World!") - }) -}) diff --git a/apps/phase2cli/tsconfig.json b/apps/phase2cli/tsconfig.json deleted file mode 100644 index 6b6f556f..00000000 --- a/apps/phase2cli/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist/", - "declarationDir": "dist/types" - }, - "include": ["src/**/*", "test/**/*", "types/**/*", "*.json"], - "exclude": ["node_modules", "tsconfig.json", "dist/**/*"] -} diff --git a/apps/phase2cli/types/index.ts b/apps/phase2cli/types/index.ts deleted file mode 100644 index 469eb5fa..00000000 --- a/apps/phase2cli/types/index.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { FirebaseApp } from "firebase/app" -import { DocumentData, DocumentReference, Firestore } from "firebase/firestore" -import { Functions } from "firebase/functions" -import { FirebaseStorage } from "firebase/storage" -import { User as FirebaseAuthUser } from "firebase/auth" - -export enum CeremonyState { - SCHEDULED = 1, - OPENED = 2, - PAUSED = 3, - CLOSED = 4, - FINALIZED = 5 -} - -export enum CeremonyType { - PHASE1 = 1, - PHASE2 = 2 -} - -export enum ProgressBarType { - DOWNLOAD = 1, - UPLOAD = 2 -} - -export enum ParticipantStatus { - CREATED = 1, - WAITING = 2, - READY = 3, - CONTRIBUTING = 4, - CONTRIBUTED = 5, - DONE = 6, - FINALIZING = 7, - FINALIZED = 8, - TIMEDOUT = 9, - EXHUMED = 10 -} - -export type GithubOAuthRequest = { - device_code: string - user_code: string - verification_uri: string - expires_in: number - interval: number -} - -export type GithubOAuthResponse = { - clientSecret: string - type: string - tokenType: string - clientType: string - clientId: string - token: string - scopes: string[] -} - -export type FirebaseServices = { - firebaseApp: FirebaseApp - firestoreDatabase: Firestore - firebaseStorage: FirebaseStorage - firebaseFunctions: Functions -} - -export type LocalPathDirectories = { - r1csDirPath: string - metadataDirPath: string - zkeysDirPath: string - ptauDirPath: string -} - -export type FirebaseDocumentInfo = { - id: string - ref: DocumentReference<DocumentData> - data: DocumentData -} - -export type User = { - name: string - username: string - providerId: string - createdAt: Date - lastLoginAt: Date -} - -export type AuthUser = { - user: FirebaseAuthUser - token: string - username: string -} - -export type CeremonyInputData = { - title: string - description: string - startDate: Date - endDate: Date - timeoutMechanismType: CeremonyTimeoutType - penalty: number -} - -export type CircomCompilerData = { - version: string - commitHash: string -} - -export type SourceTemplateData = { - source: string - commitHash: string - paramsConfiguration: Array<string> -} - -export type CircuitInputData = { - name?: string - description: string - timeoutThreshold?: number - timeoutMaxContributionWaitingTime?: number - sequencePosition?: number - prefix?: string - zKeySizeInBytes?: number - compiler: CircomCompilerData - template: SourceTemplateData -} - -export type Ceremony = CeremonyInputData & { - prefix: string - state: CeremonyState - type: CeremonyType - coordinatorId: string - lastUpdated: number -} - -export type CircuitMetadata = { - curve: string - wires: number - constraints: number - privateInputs: number - publicOutputs: number - labels: number - outputs: number - pot: number -} - -export type CircuitFiles = { - files?: { - potFilename: string - r1csFilename: string - initialZkeyFilename: string - potStoragePath: string - r1csStoragePath: string - initialZkeyStoragePath: string - potBlake2bHash: string - r1csBlake2bHash: string - initialZkeyBlake2bHash: string - } -} - -export type CircuitTimings = { - avgTimings?: { - contributionComputation: number - fullContribution: number - verifyCloudFunction: number - } -} - -export type Circuit = CircuitInputData & - CircuitFiles & - CircuitTimings & { - metadata: CircuitMetadata - lastUpdated?: number - } - -export type Timing = { - seconds: number - minutes: number - hours: number - days: number -} - -export type CeremonyTimeoutData = { - type: CeremonyTimeoutType - penalty: number -} - -export type VerifyContributionComputation = { - valid: boolean - verificationComputationTime: number - verifyCloudFunctionTime: number - fullContributionTime: number -} - -export type ChunkWithUrl = { - partNumber: number - chunk: Buffer - preSignedUrl: string -} - -export type ETagWithPartNumber = { - ETag: string | null - PartNumber: number -} - -export enum RequestType { - PUT = 1, - GET = 2 -} - -export enum ParticipantContributionStep { - DOWNLOADING = 1, - COMPUTING = 2, - UPLOADING = 3, - VERIFYING = 4, - COMPLETED = 5 -} - -export enum TimeoutType { - BLOCKING_CONTRIBUTION = 1, - BLOCKING_CLOUD_FUNCTION = 2 -} - -export enum CeremonyTimeoutType { - DYNAMIC = 1, - FIXED = 2 -} diff --git a/apps/phase2cli/types/snarkjs.d.ts b/apps/phase2cli/types/snarkjs.d.ts deleted file mode 100644 index ab419e76..00000000 --- a/apps/phase2cli/types/snarkjs.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** Declaration file generated by dts-gen */ - -declare module "snarkjs" { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - export = snarkjs - - declare const snarkjs: { - groth16: { - exportSolidityCallData: any - fullProve: any - prove: any - verify: any - } - plonk: { - exportSolidityCallData: any - fullProve: any - prove: any - setup: any - verify: any - } - powersOfTau: { - beacon: any - challengeContribute: any - contribute: any - convert: any - exportChallenge: any - exportJson: any - importResponse: any - newAccumulator: any - preparePhase2: any - truncate: any - verify: any - } - r1cs: { - exportJson: any - info: any - print: any - } - wtns: { - calculate: any - debug: any - exportJson: any - } - zKey: { - beacon: any - bellmanContribute: any - contribute: any - exportBellman: any - exportJson: any - exportSolidityVerifier: any - exportVerificationKey: any - importBellman: any - newZKey: any - verifyFromInit: any - verifyFromR1cs: any - } - } -} diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 00000000..05d294cc --- /dev/null +++ b/babel.config.json @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-typescript" + ] +} diff --git a/jest.config.ts b/jest.config.ts deleted file mode 100644 index 55ce7c42..00000000 --- a/jest.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -export default { - rootDir: "./", - collectCoverageFrom: ["**/src/index.ts", "!**/dist/**", "!**/node_modules/**"], - preset: "ts-jest", - testEnvironment: "node", - testPathIgnorePatterns: [".d.ts", ".js"], - clearMocks: true, - collectCoverage: true, - coverageDirectory: "coverage/", - coverageProvider: "v8", - verbose: true, - coverageThreshold: { - global: { - branches: 90, - functions: 95, - lines: 95, - statements: 95 - } - } -} diff --git a/jest.json b/jest.json new file mode 100644 index 00000000..c50a0c91 --- /dev/null +++ b/jest.json @@ -0,0 +1,31 @@ +{ + "testPathIgnorePatterns": [".d.ts", ".js"], + "moduleFileExtensions": ["ts", "js"], + "transform": { + "\\.(t|j)s?$": "babel-jest" + }, + "testMatch": ["**/test/**/*.test.*"], + "globals": { + "ts-jest": { + "tsConfig": "tsconfig.json" + } + }, + "moduleNameMapper": { + "@zkmpc/(.*)$": "<rootDir>/packages/$1" + }, + "setupFiles": ["dotenv/config"], + "collectCoverageFrom": ["<rootDir>/src/**/*.ts", "!<rootDir>/src/**/index.ts", "!<rootDir>/src/**/*.d.ts"], + "verbose": true, + "collectCoverage": true, + "collectCoverageFrom": ["packages/**/*", "!**/dist/**", "!**/node_modules/**"], + "coverageDirectory": "coverage/", + "coverageProvider": "v8", + "coverageThreshold": { + "global": { + "branches": 90, + "functions": 95, + "lines": 95, + "statements": 95 + } + } +} diff --git a/lerna.json b/lerna.json index aebebbab..9d05b6b1 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,6 @@ { - "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "useWorkspaces": true, - "version": "0.0.0" + "packages": ["packages/*"], + "npmClient": "yarn", + "useWorkspaces": true, + "version": "0.0.0" } diff --git a/package.json b/package.json index 7e64d416..ae5c4be7 100644 --- a/package.json +++ b/package.json @@ -1,69 +1,71 @@ { - "name": "mpc-phase2-suite", - "description": "MPC Phase 2 suite of tools for conducting zkSNARKs trusted setup ceremonies", - "repository": "git@github.com:quadratic-funding/mpc-phase2-suite.git", - "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", - "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", - "license": "MIT", - "private": true, - "keywords": [ - "typescript", - "zero-knowledge", - "zk-snarks", - "phase-2", - "trusted-setup", - "ceremony", - "snarkjs", - "circom" - ], - "scripts": { - "build": "lerna run build", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx", - "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", - "prettier": "prettier -c .", - "prettier:fix": "prettier -w .", - "test": "export GOOGLE_APPLICATION_CREDENTIALS=\"./apps/backend/serviceAccountKey.json\" && jest --coverage --detectOpenHandles", - "test:watch": "export GOOGLE_APPLICATION_CREDENTIALS=\"./apps/backend/serviceAccountKey.json\" && jest --coverage --watch --detectOpenHandles", - "commit": "cz", - "precommit": "lint-staged" - }, - "devDependencies": { - "@commitlint/cli": "^17.0.3", - "@commitlint/config-conventional": "^17.0.3", - "@types/chai": "^4.3.1", - "@types/chai-as-promised": "^7.1.5", - "@types/jest": "^28.1.6", - "@types/node": "^18.6.3", - "@typescript-eslint/eslint-plugin": "^5.32.0", - "@typescript-eslint/parser": "^5.32.0", - "chai": "^4.3.6", - "chai-as-promised": "^7.1.1", - "commitizen": "^4.2.5", - "cz-conventional-changelog": "^3.3.0", - "eslint": "^8.21.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "^26.7.0", - "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-react": "^7.30.1", - "eslint-plugin-react-hooks": "^4.6.0", - "jest": "^28.1.3", - "jest-config": "^28.1.3", - "lerna": "^6.0.3", - "lint-staged": "^13.0.3", - "prettier": "^2.7.1", - "ts-jest": "^28.0.7", - "ts-node": "^10.9.1" - }, - "config": { - "commitizen": { - "path": "./node_modules/cz-conventional-changelog" - } - }, - "workspaces": [ - "apps/*", - "packages/*" - ] + "name": "zkmpc", + "description": "MPC Phase 2 suite of tools for conducting zkSNARKs trusted setup ceremonies", + "repository": "git@github.com:quadratic-funding/mpc-phase2-suite.git", + "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", + "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", + "license": "MIT", + "private": true, + "keywords": [ + "typescript", + "zero-knowledge", + "zk-snarks", + "phase-2", + "trusted-setup", + "ceremony", + "snarkjs", + "circom" + ], + "scripts": { + "build": "lerna run build", + "pretest": "yarn build", + "test": "export GOOGLE_APPLICATION_CREDENTIALS=\"./packages/backend/serviceAccountKey.json\" && jest --config=jest.json --detectOpenHandles --forceExit --coverage", + "test:watch": "export GOOGLE_APPLICATION_CREDENTIALS=\"./packages/backend/serviceAccountKey.json\" && jest --config=jest.json --watch --detectOpenHandles --forceExit --coverage", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", + "prettier": "prettier -c .", + "prettier:fix": "prettier -w .", + "precommit": "lint-staged", + "commit": "cz" + }, + "devDependencies": { + "@babel/core": "^7.20.2", + "@babel/preset-env": "^7.20.2", + "@babel/preset-typescript": "^7.18.6", + "@commitlint/cli": "^17.3.0", + "@commitlint/config-conventional": "^17.3.0", + "@rollup/plugin-typescript": "^9.0.2", + "@types/chai": "^4.3.4", + "@types/chai-as-promised": "^7.1.5", + "@types/jest": "^29.2.3", + "@types/node": "^18.11.9", + "@typescript-eslint/eslint-plugin": "^5.44.0", + "@typescript-eslint/parser": "^5.44.0", + "babel-jest": "^29.3.1", + "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", + "commitizen": "^4.2.5", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^8.28.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jest": "^27.1.5", + "jest": "^29.3.1", + "jest-config": "^29.3.1", + "lerna": "^6.0.3", + "lint-staged": "^13.0.3", + "prettier": "^2.8.0", + "rimraf": "^3.0.2", + "rollup": "^3.4.0" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "workspaces": [ + "packages/*" + ] } diff --git a/packages/actions/.env.test.default b/packages/actions/.env.test.default new file mode 100644 index 00000000..e66be979 --- /dev/null +++ b/packages/actions/.env.test.default @@ -0,0 +1,8 @@ +# Firebase. +FIREBASE_FIRESTORE_DATABASE_URL="YOUR-FIREBASE-FIRESTORE-DATABASE-URL-HERE" +FIREBASE_API_KEY="YOUR-FIREBASE-API-KEY" +FIREBASE_AUTH_DOMAIN="YOUR-FIREBASE-AUTH-DOMAIN" +FIREBASE_PROJECT_ID="YOUR-FIREBASE-PROJECT-ID" +FIREBASE_MESSAGING_SENDER_ID="YOUR-FIREBASE-MESSAGING-SENDER-ID" +FIREBASE_APP_ID="YOUR-FIREBASE-APP-ID" +FIREBASE_CF_URL_VERIFY_CONTRIBUTION="YOUR-FIREBASE-CF-URL-VERIFY-CONTRIBUTION" \ No newline at end of file diff --git a/packages/actions/.gitignore b/packages/actions/.gitignore index 1186f217..ed90a519 100644 --- a/packages/actions/.gitignore +++ b/packages/actions/.gitignore @@ -6,9 +6,7 @@ yarn-error.log # environment .env +.env.test # build -dist/ - -# ceremony -output/ \ No newline at end of file +dist/ \ No newline at end of file diff --git a/packages/actions/build.tsconfig.json b/packages/actions/build.tsconfig.json new file mode 100644 index 00000000..ea775ea0 --- /dev/null +++ b/packages/actions/build.tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist/", + "declarationDir": "dist/types" + }, + "include": ["src/**/*", "test/**/*", "types/**/*", "rollup.config.ts"] +} diff --git a/packages/actions/package.json b/packages/actions/package.json index fc5212d4..b358bd2c 100644 --- a/packages/actions/package.json +++ b/packages/actions/package.json @@ -1,39 +1,57 @@ { - "name": "@zkmpc/actions", - "version": "0.0.0", - "description": "A set of actions and helpers for CLI commands", - "type": "module", - "main": "dist/src/index.js", - "repository": "https://github.com/quadratic-funding/mpc-phase2-suite", - "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", - "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", - "license": "MIT", - "private": false, - "types": "dist/types/index.d.ts", - "files": [ - "dist/", - "src/", - "types/" - ], - "keywords": [ - "typescript", - "zero-knowledge", - "zk-snarks", - "phase-2", - "trusted-setup", - "ceremony", - "snarkjs", - "circom" - ], - "scripts": { - "build": "tsc" - }, - "dependencies": { - "@octokit/auth-oauth-device": "^4.0.3", - "chai": "^4.3.7", - "chai-as-promised": "^7.1.1", - "clipboardy": "^3.0.0", - "firebase": "^9.14.0", - "firebase-admin": "^11.2.1" - } + "name": "@zkmpc/actions", + "version": "0.0.1", + "description": "A set of actions and helpers for CLI commands", + "repository": "https://github.com/quadratic-funding/mpc-phase2-suite", + "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", + "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", + "license": "MIT", + "private": false, + "main": "dist/src/index.node.js", + "exports": { + "import": "./dist/src/index.node.mjs", + "require": "./dist/src/index.node.js" + }, + "types": "dist/types/index.d.ts", + "engines": { + "node": "16" + }, + "files": [ + "dist/", + "src/", + "types/" + ], + "keywords": [ + "typescript", + "zero-knowledge", + "zk-snarks", + "phase-2", + "trusted-setup", + "ceremony", + "snarkjs", + "circom" + ], + "scripts": { + "build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript", + "build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript", + "pre:publish": "yarn build" + }, + "dependencies": { + "@octokit/auth-oauth-device": "^4.0.3", + "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", + "dotenv": "^16.0.3", + "firebase": "^9.14.0", + "firebase-admin": "^11.3.0" + }, + "devDependencies": { + "@types/rollup-plugin-auto-external": "^2.0.2", + "rollup-plugin-auto-external": "^2.0.0", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.34.1", + "typescript": "^4.9.3" + }, + "publishConfig": { + "access": "public" + } } diff --git a/packages/actions/rollup.config.ts b/packages/actions/rollup.config.ts new file mode 100644 index 00000000..cc9ac131 --- /dev/null +++ b/packages/actions/rollup.config.ts @@ -0,0 +1,30 @@ +import * as fs from "fs" +import typescript from "rollup-plugin-typescript2" +import cleanup from "rollup-plugin-cleanup" +import autoExternal from "rollup-plugin-auto-external" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf-8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation 2022 + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +export default { + input: "src/index.ts", + output: [ + { file: pkg.exports.require, format: "cjs", banner, exports: "auto" }, + { file: pkg.exports.import, format: "es", banner } + ], + plugins: [ + autoExternal(), + typescript({ + tsconfig: "./build.tsconfig.json", + useTsconfigDeclarationDir: true + }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/actions/src/core/auth/index.ts b/packages/actions/src/core/auth/index.ts index 80cb59a4..d65f1690 100644 --- a/packages/actions/src/core/auth/index.ts +++ b/packages/actions/src/core/auth/index.ts @@ -1,7 +1,7 @@ import { FirebaseApp } from "firebase/app" -import { getAuth, signInWithCredential, User } from "firebase/auth" +import { getAuth, initializeAuth, signInWithCredential, User } from "firebase/auth" import { createOAuthDeviceAuth } from "@octokit/auth-oauth-device" -import { exchangeGithubTokenForFirebaseCredentials, onVerification } from "../lib/utils.js" +import { exchangeGithubTokenForFirebaseCredentials, onVerification } from "../lib/utils" /** * Return the Github OAuth 2.0 token using manual Device Flow authentication process. @@ -9,30 +9,30 @@ import { exchangeGithubTokenForFirebaseCredentials, onVerification } from "../li * @returns <string> the Github OAuth 2.0 token. */ export const getNewOAuthTokenUsingGithubDeviceFlow = async (clientId: string): Promise<string> => { - /** - * Github OAuth 2.0 Device Flow. - * # Step 1: Request device and user verification codes and gets auth verification uri. - * # Step 2: The app prompts the user to enter a user verification code at https://github.com/login/device. - * # Step 3: The app polls/asks for the user authentication status. - */ - - const clientType = "oauth-app" - const tokenType = "oauth" - - // # Step 1. - const auth = createOAuthDeviceAuth({ - clientType, - clientId, - scopes: ["gist"], - onVerification - }) - - // # Step 3. - const { token } = await auth({ - type: tokenType - }) - - return token + /** + * Github OAuth 2.0 Device Flow. + * # Step 1: Request device and user verification codes and gets auth verification uri. + * # Step 2: The app prompts the user to enter a user verification code at https://github.com/login/device. + * # Step 3: The app polls/asks for the user authentication status. + */ + + const clientType = "oauth-app" + const tokenType = "oauth" + + // # Step 1. + const auth = createOAuthDeviceAuth({ + clientType, + clientId, + scopes: ["gist"], + onVerification + }) + + // # Step 3. + const { token } = await auth({ + type: tokenType + }) + + return token } /** @@ -41,11 +41,11 @@ export const getNewOAuthTokenUsingGithubDeviceFlow = async (clientId: string): P * @returns */ export const getCurrentFirebaseAuthUser = (firebaseApp: FirebaseApp): User => { - const user = getAuth(firebaseApp).currentUser + const user = getAuth(firebaseApp).currentUser - if (!user) throw new Error(`Cannot retrieve the current authenticated user for given Firebase Application`) + if (!user) throw new Error(`Cannot retrieve the current authenticated user for given Firebase Application`) - return user + return user } /** @@ -54,6 +54,6 @@ export const getCurrentFirebaseAuthUser = (firebaseApp: FirebaseApp): User => { * @param token <string> - the Github OAuth 2.0 token to be exchanged. */ export const signInToFirebaseWithGithubToken = async (firebaseApp: FirebaseApp, token: string) => { - // Sign in with the credential. - await signInWithCredential(getAuth(firebaseApp), exchangeGithubTokenForFirebaseCredentials(token)) + // Sign in with the credential. + await signInWithCredential(initializeAuth(firebaseApp), exchangeGithubTokenForFirebaseCredentials(token)) } diff --git a/packages/actions/src/core/contribute/index.ts b/packages/actions/src/core/contribute/index.ts index c3543856..c51d20dc 100644 --- a/packages/actions/src/core/contribute/index.ts +++ b/packages/actions/src/core/contribute/index.ts @@ -1,11 +1,6 @@ import { Firestore, where } from "firebase/firestore" -import { - CeremonyCollectionField, - CeremonyState, - Collections, - FirebaseDocumentInfo -} from "packages/actions/types/index.js" -import { queryCollection, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs } from "../../helpers/query.js" +import { CeremonyCollectionField, CeremonyState, Collections, FirebaseDocumentInfo } from "../../../types/index" +import { queryCollection, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs } from "../../helpers/query" /** * Query for opened ceremonies documents and return their data (if any). @@ -13,14 +8,14 @@ import { queryCollection, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs * @returns <Promise<Array<FirebaseDocumentInfo>>> */ export const getOpenedCeremonies = async (firestoreDatabase: Firestore): Promise<Array<FirebaseDocumentInfo>> => { - const runningStateCeremoniesQuerySnap = await queryCollection(firestoreDatabase, Collections.CEREMONIES, [ - where(CeremonyCollectionField.STATE, "==", CeremonyState.OPENED), - where(CeremonyCollectionField.END_DATE, ">=", Date.now()) - ]) + const runningStateCeremoniesQuerySnap = await queryCollection(firestoreDatabase, Collections.CEREMONIES, [ + where(CeremonyCollectionField.STATE, "==", CeremonyState.OPENED), + where(CeremonyCollectionField.END_DATE, ">=", Date.now()) + ]) - return runningStateCeremoniesQuerySnap.empty && runningStateCeremoniesQuerySnap.size === 0 - ? [] - : fromQueryToFirebaseDocumentInfo(runningStateCeremoniesQuerySnap.docs) + return runningStateCeremoniesQuerySnap.empty && runningStateCeremoniesQuerySnap.size === 0 + ? [] + : fromQueryToFirebaseDocumentInfo(runningStateCeremoniesQuerySnap.docs) } /** @@ -30,9 +25,9 @@ export const getOpenedCeremonies = async (firestoreDatabase: Firestore): Promise * @returns Promise<Array<FirebaseDocumentInfo>> */ export const getCeremonyCircuits = async ( - firestoreDatabase: Firestore, - ceremonyId: string + firestoreDatabase: Firestore, + ceremonyId: string ): Promise<Array<FirebaseDocumentInfo>> => - fromQueryToFirebaseDocumentInfo( - await getAllCollectionDocs(firestoreDatabase, `${Collections.CEREMONIES}/${ceremonyId}/${Collections.CIRCUITS}`) - ).sort((a: FirebaseDocumentInfo, b: FirebaseDocumentInfo) => a.data.sequencePosition - b.data.sequencePosition) + fromQueryToFirebaseDocumentInfo( + await getAllCollectionDocs(firestoreDatabase, `${Collections.CEREMONIES}/${ceremonyId}/${Collections.CIRCUITS}`) + ).sort((a: FirebaseDocumentInfo, b: FirebaseDocumentInfo) => a.data.sequencePosition - b.data.sequencePosition) diff --git a/packages/actions/src/core/lib/utils.ts b/packages/actions/src/core/lib/utils.ts index 84a104a0..0506e190 100644 --- a/packages/actions/src/core/lib/utils.ts +++ b/packages/actions/src/core/lib/utils.ts @@ -1,5 +1,5 @@ import open from "open" -import clipboard from "clipboardy" +// import clipboard from "clipboardy" // TODO: need a substitute. import { Verification } from "@octokit/auth-oauth-device/dist-types/types" import { OAuthCredential, GithubAuthProvider } from "firebase/auth" @@ -10,25 +10,25 @@ import { OAuthCredential, GithubAuthProvider } from "firebase/auth" * @param intervalInSeconds <number> - the amount of time that must elapse between updates (default 1s === 1ms). */ const createExpirationCountdown = (durationInSeconds: number, intervalInSeconds = 1000) => { - let seconds = durationInSeconds <= 60 ? durationInSeconds : 60 - - setInterval(() => { - try { - if (durationInSeconds !== 0) { - // Update times. - durationInSeconds -= intervalInSeconds - seconds -= intervalInSeconds - - if (seconds % 60 === 0) seconds = 0 - - process.stdout.write(`Expires in 00:${Math.floor(durationInSeconds / 60)}:${seconds}\r`) - } else console.log(`Expired`) - } catch (err: any) { - // Workaround to the \r. - process.stdout.write(`\n\n`) - console.log(`Expired`) - } - }, intervalInSeconds * 1000) + let seconds = durationInSeconds <= 60 ? durationInSeconds : 60 + + setInterval(() => { + try { + if (durationInSeconds !== 0) { + // Update times. + durationInSeconds -= intervalInSeconds + seconds -= intervalInSeconds + + if (seconds % 60 === 0) seconds = 0 + + process.stdout.write(`Expires in 00:${Math.floor(durationInSeconds / 60)}:${seconds}\r`) + } else console.log(`Expired`) + } catch (err: any) { + // Workaround to the \r. + process.stdout.write(`\n\n`) + console.log(`Expired`) + } + }, intervalInSeconds * 1000) } /** @@ -36,21 +36,22 @@ const createExpirationCountdown = (durationInSeconds: number, intervalInSeconds * @param verification <Verification> - the data from Github OAuth2.0 device flow. */ export const onVerification = async (verification: Verification): Promise<void> => { - // Automatically open the page (# Step 2). - await open(verification.verification_uri) - - // Copy code to clipboard. - clipboard.writeSync(verification.user_code) - clipboard.readSync() - - // Display data. - // TODO. custom theme is missing. - console.log( - `Visit ${verification.verification_uri} on this device to authenticate\nYour auth code: ${verification.user_code}` - ) - - // Countdown for time expiration. - createExpirationCountdown(verification.expires_in, 1) + // Automatically open the page (# Step 2). + await open(verification.verification_uri) + + // TODO: need a substitute for `clipboardy` package. + // Copy code to clipboard. + // clipboard.writeSync(verification.user_code) + // clipboard.readSync() + + // Display data. + // TODO. custom theme is missing. + console.log( + `Visit ${verification.verification_uri} on this device to authenticate\nYour auth code: ${verification.user_code}` + ) + + // Countdown for time expiration. + createExpirationCountdown(verification.expires_in, 1) } /** @@ -59,4 +60,4 @@ export const onVerification = async (verification: Verification): Promise<void> * @returns <OAuthCredential> - the Firebase OAuth credential object. */ export const exchangeGithubTokenForFirebaseCredentials = (token: string): OAuthCredential => - GithubAuthProvider.credential(token) + GithubAuthProvider.credential(token) diff --git a/packages/actions/src/helpers/query.ts b/packages/actions/src/helpers/query.ts index 8f6bf7c0..93b50151 100644 --- a/packages/actions/src/helpers/query.ts +++ b/packages/actions/src/helpers/query.ts @@ -1,14 +1,14 @@ import { - collection as collectionRef, - DocumentData, - Firestore, - getDocs, - query, - QueryConstraint, - QueryDocumentSnapshot, - QuerySnapshot + collection as collectionRef, + DocumentData, + Firestore, + getDocs, + query, + QueryConstraint, + QueryDocumentSnapshot, + QuerySnapshot } from "firebase/firestore" -import { FirebaseDocumentInfo } from "packages/actions/types/index.js" +import { FirebaseDocumentInfo } from "../../types/index" /** * Helper for query a collection based on certain constraints. @@ -18,15 +18,15 @@ import { FirebaseDocumentInfo } from "packages/actions/types/index.js" * @returns <Promise<QuerySnapshot<DocumentData>>> - return the matching documents (if any). */ export const queryCollection = async ( - firestoreDatabase: Firestore, - collection: string, - queryConstraints: Array<QueryConstraint> + firestoreDatabase: Firestore, + collection: string, + queryConstraints: Array<QueryConstraint> ): Promise<QuerySnapshot<DocumentData>> => { - // Make a query. - const q = query(collectionRef(firestoreDatabase, collection), ...queryConstraints) + // Make a query. + const q = query(collectionRef(firestoreDatabase, collection), ...queryConstraints) - // Get docs. - return getDocs(q) + // Get docs. + return getDocs(q) } /** @@ -35,13 +35,13 @@ export const queryCollection = async ( * @returns Array<FirebaseDocumentInfo> */ export const fromQueryToFirebaseDocumentInfo = ( - queryDocSnap: Array<QueryDocumentSnapshot> + queryDocSnap: Array<QueryDocumentSnapshot> ): Array<FirebaseDocumentInfo> => - queryDocSnap.map((doc: QueryDocumentSnapshot<DocumentData>) => ({ - id: doc.id, - ref: doc.ref, - data: doc.data() - })) + queryDocSnap.map((doc: QueryDocumentSnapshot<DocumentData>) => ({ + id: doc.id, + ref: doc.ref, + data: doc.data() + })) /** * Fetch for all documents in a collection. @@ -50,7 +50,7 @@ export const fromQueryToFirebaseDocumentInfo = ( * @returns <Promise<Array<QueryDocumentSnapshot<DocumentData>>>> - return all documents (if any). */ export const getAllCollectionDocs = async ( - firestoreDatabase: Firestore, - collection: string + firestoreDatabase: Firestore, + collection: string ): Promise<Array<QueryDocumentSnapshot<DocumentData>>> => - (await getDocs(collectionRef(firestoreDatabase, collection))).docs + (await getDocs(collectionRef(firestoreDatabase, collection))).docs diff --git a/packages/actions/src/index.ts b/packages/actions/src/index.ts index 4722a47d..0efa5916 100644 --- a/packages/actions/src/index.ts +++ b/packages/actions/src/index.ts @@ -1,14 +1,14 @@ import { - getCurrentFirebaseAuthUser, - getNewOAuthTokenUsingGithubDeviceFlow, - signInToFirebaseWithGithubToken -} from "./core/auth/index.js" -import { getOpenedCeremonies, getCeremonyCircuits } from "./core/contribute/index.js" + getCurrentFirebaseAuthUser, + getNewOAuthTokenUsingGithubDeviceFlow, + signInToFirebaseWithGithubToken +} from "./core/auth/index" +import { getOpenedCeremonies, getCeremonyCircuits } from "./core/contribute/index" export { - getCurrentFirebaseAuthUser, - getNewOAuthTokenUsingGithubDeviceFlow, - signInToFirebaseWithGithubToken, - getOpenedCeremonies, - getCeremonyCircuits + getCurrentFirebaseAuthUser, + getNewOAuthTokenUsingGithubDeviceFlow, + signInToFirebaseWithGithubToken, + getOpenedCeremonies, + getCeremonyCircuits } diff --git a/packages/actions/test/e2e.test.ts b/packages/actions/test/e2e.test.ts deleted file mode 100644 index f5ba58f0..00000000 --- a/packages/actions/test/e2e.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import admin from "firebase-admin" -import { initializeApp } from "firebase/app" -import { getFirestore } from "firebase/firestore" -import { getFunctions, httpsCallable } from "firebase/functions" -import { getAuth, signInAnonymously } from "firebase/auth" -import chai from "chai" -import chaiAsPromised from "chai-as-promised" -import { getCurrentFirebaseAuthUser, getCeremonyCircuits, getOpenedCeremonies } from "../src/index.js" -import { FirebaseDocumentInfo } from "../types/index.js" - -// Config chai. -chai.use(chaiAsPromised) - -describe("e2e Test Sample", () => { - // Sample data for running the test. - let openedCeremonies: Array<FirebaseDocumentInfo> = [] - let selectedCeremonyCircuits: Array<FirebaseDocumentInfo> = [] - let selectedCeremony: FirebaseDocumentInfo - - // Sample user. - const sampleUser = { - uid: "", // Defined after the anonymous sign in. - creationTime: Date.now(), - email: "alice@example.com", - emailVerified: false, - lastSignInTime: Date.now() + 1, - lastUpdated: Date.now() + 1, - name: "Alice", - photoURL: "" - } - - // Sample ceremony. - const sampleCeremony = { - uid: "rcWHse2WuwrmGYEtCDhS", - coordinatorId: "0cqqE9RamNOvOZbHYhftzeno0955", // different from sample user. - description: "Dummy ceremony", - endDate: 1671290221000, - startDate: 1667315821000, - lastUpdated: Date.now() + 1, - penalty: 720, - prefix: "dummy-ceremony", - state: 2, // Opened. - timeoutType: 2, - type: 2 - } - - // Sample circuit. - const sampleCircuit = { - uid: "KInbENc5N6WzuwG89Vil", - avgTimings: { - contributionComputation: 0, - fullContribution: 0, - verifyCloudFunction: 0 - }, - description: "sample circuit", - files: { - initialZkeyBlake2bHash: "", - initialZkeyFilename: "", - initialZkeyStoragePath: "", - potBlake2bHash: "", - potFilename: "", - potStoragePath: "", - r1csBlake2bHash: "", - r1csFilename: "", - r1csStoragePath: "" - }, - lastUpdated: Date.now() + 1, - metadata: { - constraints: 3, - curve: "bn-128", - labels: 8, - outputs: 3, - pot: 3, - privateInputs: 4, - publicOutputs: 0, - wires: 8 - }, - name: "sample circuit", - prefix: "sample_circuit", - sequencePosition: 1, - timeoutMaxContributionWaitingTime: 10, - waitingQueue: { - completedContributions: 0, - contributors: [], - currentContributor: "", - failedContributions: 0 - }, - zKeySizeInBytes: 4380 - } - - // Step 0. Initialization of Firebase Admin SDK and sample user. - admin.initializeApp({}) - const sampleUserApp = initializeApp({}) - - // Get Firebase services for admin. - const adminFirestore = admin.firestore() - - // Sample user. - const sampleUserFirestore = getFirestore(sampleUserApp) - const sampleUserFunctions = getFunctions(sampleUserApp) - - beforeEach(async () => { - // Sign in anonymously. - const auth = getAuth(sampleUserApp) - const aliceCredentials = await signInAnonymously(auth) - - // Set uid. - sampleUser.uid = aliceCredentials.user.uid - - // Step 2.A Create a one dummy ceremony. - await adminFirestore - .collection(`ceremonies`) - .doc(sampleCeremony.uid) - .set({ - ...sampleCeremony - }) - await adminFirestore - .collection(`ceremonies/${sampleCeremony.uid}/circuits`) - .doc(sampleCircuit.uid) - .set({ - ...sampleCircuit - }) - - // Step 2.A Get opened ceremonies (I need to create a one dummy ceremony with at least one circuit) using the query (action helper). - openedCeremonies = await getOpenedCeremonies(sampleUserFirestore) - - // Select the first ceremony. - selectedCeremony = openedCeremonies.at(0)! - - // Step 2.B Get ceremony circuits using the query (action helper). - selectedCeremonyCircuits = await getCeremonyCircuits(sampleUserFirestore, selectedCeremony.id) - }) - - it(`shouldn't be possible for a non authenticated user to call checkParticipantForCeremony() CF to check for eligibility`, async () => { - // should fetch custom claims now. - const user = getCurrentFirebaseAuthUser(sampleUserApp) - console.log(user) - - // Step 2.C Call checkParticipantForCeremony Cloud Function and get the result (should return `true`). - const checkParticipantForCeremony = httpsCallable(sampleUserFunctions, "checkParticipantForCeremony") - - const data = await checkParticipantForCeremony({ ceremonyId: selectedCeremony.id }) - - console.log(data) - console.log(selectedCeremonyCircuits) - }) - - afterAll(() => { - // Step 3. Clean ceremony and user from DB. - // adminFirestore.collection(`users`).doc(alice.uid).delete() - adminFirestore.collection(`ceremonies`).doc(sampleCeremony.uid).delete() - adminFirestore.collection(`ceremonies/${sampleCeremony.uid}/circuits`).doc(sampleCircuit.uid).delete() - }) -}) diff --git a/packages/actions/test/index.test.ts b/packages/actions/test/index.test.ts new file mode 100644 index 00000000..572c98f5 --- /dev/null +++ b/packages/actions/test/index.test.ts @@ -0,0 +1,153 @@ +import admin from "firebase-admin" +import { initializeApp } from "firebase/app" +import { getFirestore } from "firebase/firestore" +import { getFunctions, httpsCallable } from "firebase/functions" +import { getAuth, signInAnonymously } from "firebase/auth" +import chai, { assert } from "chai" +import chaiAsPromised from "chai-as-promised" +import { FirebaseDocumentInfo } from "types" +import dotenv from "dotenv" +import { getOpenedCeremonies } from "../src/index" + +dotenv.config({ path: `${__dirname}/../.env.test` }) + +// Config chai. +chai.use(chaiAsPromised) + +describe("Sample e2e", () => { + // Sample data for running the test. + let openedCeremonies: Array<FirebaseDocumentInfo> = [] + let selectedCeremony: FirebaseDocumentInfo + + // Sample user. + const sampleUser = { + uid: "", // Defined after the anonymous sign in. + creationTime: Date.now(), + email: "sampleuser@example.com", + emailVerified: false, + lastSignInTime: Date.now() + 1, + lastUpdated: Date.now() + 1, + name: "Sample", + photoURL: "" + } + + // Sample ceremony. + const sampleCeremony = { + uid: "rcWHse2WuwrmGYEtCDhS", + coordinatorId: "0cqqE9RamNOvOZbHYhftzeno0955", // different from sample user. + description: "Sample ceremony", + startDate: Date.now() + 86400000, + endDate: Date.now() + 86400000 * 2, + lastUpdated: Date.now(), + penalty: 720, + prefix: "sample-ceremony", + state: 2, // Opened. + timeoutType: 2, + type: 2 + } + + // Sample circuit. + const sampleCircuit = { + uid: "KInbENc5N6WzuwG89Vil", + avgTimings: { + contributionComputation: 0, + fullContribution: 0, + verifyCloudFunction: 0 + }, + description: "sample circuit", + files: { + initialZkeyBlake2bHash: "", + initialZkeyFilename: "", + initialZkeyStoragePath: "", + potBlake2bHash: "", + potFilename: "", + potStoragePath: "", + r1csBlake2bHash: "", + r1csFilename: "", + r1csStoragePath: "" + }, + lastUpdated: Date.now() + 1, + metadata: { + constraints: 3, + curve: "bn-128", + labels: 8, + outputs: 3, + pot: 3, + privateInputs: 4, + publicOutputs: 0, + wires: 8 + }, + name: "sample circuit", + prefix: "sample_circuit", + sequencePosition: 1, + timeoutMaxContributionWaitingTime: 10, + waitingQueue: { + completedContributions: 0, + contributors: [], + currentContributor: "", + failedContributions: 0 + }, + zKeySizeInBytes: 4380 + } + + // Init Firebase App for Admin and Sample user. + admin.initializeApp({ projectId: process.env.FIREBASE_PROJECT_ID }) + const adminFirestore = admin.firestore() + + const sampleUserApp = initializeApp({ + apiKey: process.env.FIREBASE_API_KEY, + authDomain: process.env.FIREBASE_AUTH_DOMAIN, + projectId: process.env.FIREBASE_PROJECT_ID, + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.FIREBASE_APP_ID + }) + + const sampleUserFirestore = getFirestore(sampleUserApp) + const sampleUserFunctions = getFunctions(sampleUserApp) + + beforeEach(async () => { + // Sign in anonymously. + const auth = getAuth(sampleUserApp) + const sampleUserCredentials = await signInAnonymously(auth) + + // Set uid. + sampleUser.uid = sampleUserCredentials.user.uid + + // Create the sample ceremony. + await adminFirestore + .collection(`ceremonies`) + .doc(sampleCeremony.uid) + .set({ + ...sampleCeremony + }) + await adminFirestore + .collection(`ceremonies/${sampleCeremony.uid}/circuits`) + .doc(sampleCircuit.uid) + .set({ + ...sampleCircuit + }) + + // Get opened ceremonies. + openedCeremonies = await getOpenedCeremonies(sampleUserFirestore) + + // Select the first ceremony. + selectedCeremony = openedCeremonies.at(0)! + }) + + it("should reject when user is not authenticated", async () => { + // Call checkParticipantForCeremony Cloud Function and check the result. + const checkParticipantForCeremony = httpsCallable(sampleUserFunctions, "checkParticipantForCeremony", {}) + + assert.isRejected(checkParticipantForCeremony({ ceremonyId: selectedCeremony.id })) + }) + + afterAll(async () => { + // Clean ceremony and user from DB. + adminFirestore.collection(`users`).doc(sampleUser.uid).delete() + await adminFirestore.collection(`ceremonies`).doc(sampleCeremony.uid).delete() + await adminFirestore.collection(`ceremonies/${sampleCeremony.uid}/circuits`).doc(sampleCircuit.uid).delete() + + // Delete admin app. + await Promise.all(admin.apps.map((app) => app?.delete())) + }) +}) diff --git a/packages/actions/tsconfig.json b/packages/actions/tsconfig.json index d6ddd9ec..ea775ea0 100644 --- a/packages/actions/tsconfig.json +++ b/packages/actions/tsconfig.json @@ -1,9 +1,9 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist/", - "declarationDir": "dist/types" - }, - "include": ["src/**/*", "test/**/*", "types/**/*", "*.json", "types"], - "exclude": ["node_modules", "tsconfig.json"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist/", + "declarationDir": "dist/types" + }, + "include": ["src/**/*", "test/**/*", "types/**/*", "rollup.config.ts"] } diff --git a/packages/actions/types/index.ts b/packages/actions/types/index.ts index cd63c6d3..0f720271 100644 --- a/packages/actions/types/index.ts +++ b/packages/actions/types/index.ts @@ -2,37 +2,37 @@ import { DocumentReference, DocumentData } from "firebase/firestore" /** Enumeratives */ export const enum CeremonyState { - SCHEDULED = 1, - OPENED = 2, - PAUSED = 3, - CLOSED = 4, - FINALIZED = 5 + SCHEDULED = 1, + OPENED = 2, + PAUSED = 3, + CLOSED = 4, + FINALIZED = 5 } export const enum Collections { - USERS = "users", - PARTICIPANTS = "participants", - CEREMONIES = "ceremonies", - CIRCUITS = "circuits", - CONTRIBUTIONS = "contributions", - TIMEOUTS = "timeouts" + USERS = "users", + PARTICIPANTS = "participants", + CEREMONIES = "ceremonies", + CIRCUITS = "circuits", + CONTRIBUTIONS = "contributions", + TIMEOUTS = "timeouts" } export const enum CeremonyCollectionField { - COORDINATOR_ID = "coordinatorId", - DESCRIPTION = "description", - START_DATE = "startDate", - END_DATE = "endDate", - LAST_UPDATED = "lastUpdated", - PREFIX = "prefix", - STATE = "state", - TITLE = "title", - TYPE = "type" + COORDINATOR_ID = "coordinatorId", + DESCRIPTION = "description", + START_DATE = "startDate", + END_DATE = "endDate", + LAST_UPDATED = "lastUpdated", + PREFIX = "prefix", + STATE = "state", + TITLE = "title", + TYPE = "type" } /** Types */ export type FirebaseDocumentInfo = { - id: string - ref: DocumentReference<DocumentData> - data: DocumentData + id: string + ref: DocumentReference<DocumentData> + data: DocumentData } diff --git a/apps/backend/.default.env b/packages/backend/.default.env similarity index 100% rename from apps/backend/.default.env rename to packages/backend/.default.env diff --git a/apps/backend/.firebaserc b/packages/backend/.firebaserc similarity index 100% rename from apps/backend/.firebaserc rename to packages/backend/.firebaserc diff --git a/apps/backend/.gitignore b/packages/backend/.gitignore similarity index 100% rename from apps/backend/.gitignore rename to packages/backend/.gitignore diff --git a/packages/backend/README.md b/packages/backend/README.md new file mode 100644 index 00000000..7fff708a --- /dev/null +++ b/packages/backend/README.md @@ -0,0 +1,18 @@ +# mpc-phase2-suite-firebase + +MPC Phase 2 backend for Firebase services management + +- Description (w/ Features). +- Install + Configs + - Firebase login + - Firebase init (select everything except Hosting and Remote Config) + - Console + - App + - Service Account Key generation +- Usage + Examples +- License +- Support + FAQ? + - FIREBASE_CONFIG & GCLOUD_PROJECT not set warn - because we are running on NodeJS and not on cloud. + - Emulator scheduled functions execution: emulator terminal + emulator shell terminal (here, call scheduled function, e.g., scheduledFunction()). + +Coordinator Guide .md for more infos about the Firebase configuration? diff --git a/packages/backend/build.tsconfig.json b/packages/backend/build.tsconfig.json new file mode 100644 index 00000000..ea775ea0 --- /dev/null +++ b/packages/backend/build.tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist/", + "declarationDir": "dist/types" + }, + "include": ["src/**/*", "test/**/*", "types/**/*", "rollup.config.ts"] +} diff --git a/packages/backend/firebase.json b/packages/backend/firebase.json new file mode 100644 index 00000000..ddfa3fb5 --- /dev/null +++ b/packages/backend/firebase.json @@ -0,0 +1,36 @@ +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "functions": { + "predeploy": "yarn --prefix \"$RESOURCE_DIR\" build", + "source": "." + }, + "storage": { + "rules": "storage.rules" + }, + "emulators": { + "auth": { + "port": 9099 + }, + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8080 + }, + "database": { + "port": 9000 + }, + "pubsub": { + "port": 8085 + }, + "storage": { + "port": 9199 + }, + "ui": { + "enabled": true + } + } +} diff --git a/packages/backend/firestore.indexes.json b/packages/backend/firestore.indexes.json new file mode 100644 index 00000000..a96ab11d --- /dev/null +++ b/packages/backend/firestore.indexes.json @@ -0,0 +1,33 @@ +{ + "indexes": [ + { + "collectionGroup": "ceremonies", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "state", + "order": "ASCENDING" + }, + { + "fieldPath": "endDate", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "ceremonies", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "state", + "order": "ASCENDING" + }, + { + "fieldPath": "startDate", + "order": "ASCENDING" + } + ] + } + ], + "fieldOverrides": [] +} diff --git a/apps/backend/firestore.rules b/packages/backend/firestore.rules similarity index 100% rename from apps/backend/firestore.rules rename to packages/backend/firestore.rules diff --git a/packages/backend/package.json b/packages/backend/package.json new file mode 100644 index 00000000..168ef4d0 --- /dev/null +++ b/packages/backend/package.json @@ -0,0 +1,84 @@ +{ + "name": "@zkmpc/backend", + "version": "0.0.1", + "description": "MPC Phase 2 backend for Firebase services management", + "repository": "https://github.com/quadratic-funding/mpc-phase2-suite/apps/backend", + "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", + "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", + "author": { + "name": "Giacomo (0xjei)" + }, + "license": "MIT", + "private": false, + "main": "dist/src/functions/index.node.js", + "exports": { + "import": "./dist/src/functions/index.node.mjs", + "require": "./dist/src/functions/index.node.js" + }, + "type": "module", + "types": "dist/types/index.d.ts", + "engines": { + "node": "16" + }, + "files": [ + "dist/", + "src/", + "test/", + "types", + "README.md" + ], + "keywords": [ + "typescript", + "zero-knowledge", + "zk-snarks", + "phase-2", + "trusted-setup", + "ceremony", + "snarkjs", + "circom" + ], + "scripts": { + "build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript", + "build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript", + "firebase:login": "firebase login", + "firebase:logout": "firebase logout", + "firebase:init": "firebase init", + "firebase:deploy": "yarn firestore:get-indexes && firebase deploy", + "firebase:deploy-functions": "firebase deploy --only functions", + "firebase:deploy-firestore": "yarn firestore:get-indexes && firebase deploy --only firestore", + "firebase:deploy-storage": "firebase deploy --only storage", + "firebase:log-functions": "firebase functions:log", + "firestore:get-indexes": "firebase firestore:indexes > firestore.indexes.json", + "emulator:serve": "yarn build && firebase emulators:start", + "emulator:serve-functions": "yarn build && firebase emulators:start --only functions", + "emulator:shell": "yarn build && firebase functions:shell", + "prepublish": "yarn build" + }, + "devDependencies": { + "@firebase/rules-unit-testing": "^2.0.5", + "@types/rollup-plugin-auto-external": "^2.0.2", + "@types/uuid": "^8.3.4", + "firebase-functions-test": "^3.0.0", + "firebase-tools": "^11.16.1", + "rollup-plugin-auto-external": "^2.0.0", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.34.1", + "typescript": "^4.9.3" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.178.0", + "@aws-sdk/middleware-endpoint": "^3.178.0", + "@aws-sdk/s3-request-presigner": "^3.178.0", + "blakejs": "^1.2.1", + "dotenv": "^16.0.3", + "firebase-admin": "^11.3.0", + "firebase-functions": "^4.1.0", + "snarkjs": "^0.5.0", + "timer-node": "^5.0.6", + "uuid": "^9.0.0", + "winston": "^3.8.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/backend/rollup.config.ts b/packages/backend/rollup.config.ts new file mode 100644 index 00000000..85bd94b5 --- /dev/null +++ b/packages/backend/rollup.config.ts @@ -0,0 +1,30 @@ +import * as fs from "fs" +import typescript from "rollup-plugin-typescript2" +import cleanup from "rollup-plugin-cleanup" +import autoExternal from "rollup-plugin-auto-external" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf-8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation 2022 + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +export default { + input: "src/functions/index.ts", + output: [ + { file: pkg.exports.require, format: "cjs", banner, exports: "auto" }, + { file: pkg.exports.import, format: "es", banner } + ], + plugins: [ + autoExternal(), + typescript({ + tsconfig: "./build.tsconfig.json", + useTsconfigDeclarationDir: true + }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/backend/src/functions/auth.ts b/packages/backend/src/functions/auth.ts new file mode 100644 index 00000000..e2c9bbb9 --- /dev/null +++ b/packages/backend/src/functions/auth.ts @@ -0,0 +1,84 @@ +import * as functions from "firebase-functions" +import { UserRecord } from "firebase-functions/v1/auth" +import admin from "firebase-admin" +import dotenv from "dotenv" +import { GENERIC_ERRORS, logMsg } from "../lib/logs" +import { getCurrentServerTimestampInMillis } from "../lib/utils" +import { MsgType } from "../../types/index" + +dotenv.config() + +/** + * Auth-triggered function which writes a user document to Firestore. + */ +export const registerAuthUser = functions.auth.user().onCreate(async (user: UserRecord) => { + // Get DB. + const firestore = admin.firestore() + + // Get user information. + if (!user.uid) logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + // The user object has basic properties such as display name, email, etc. + const { displayName } = user + const { email } = user + const { photoURL } = user + const { emailVerified } = user + + // Metadata. + const { creationTime } = user.metadata + const { lastSignInTime } = user.metadata + + // The user's ID, unique to the Firebase project. Do NOT use + // this value to authenticate with your backend server, if + // you have one. Use User.getToken() instead. + const { uid } = user + + // Reference to a document using uid. + const userRef = firestore.collection("users").doc(uid) + + // Set document (nb. we refer to providerData[0] because we use Github OAuth provider only). + await userRef.set({ + name: displayName, + // Metadata. + creationTime, + lastSignInTime, + // Optional. + email: email || "", + emailVerified: emailVerified || false, + photoURL: photoURL || "", + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`User ${uid} correctly stored`, MsgType.INFO) +}) + +/** + * Set custom claims for role-based access control on the newly created user. + */ +export const processSignUpWithCustomClaims = functions.auth.user().onCreate(async (user: UserRecord) => { + // Get user information. + if (!user.uid) logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + let customClaims: any + // Check if user meets role criteria to be a coordinator. + if ( + user.email && + (user.email.endsWith(`@${process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN}`) || + user.email === process.env.CUSTOM_CLAIMS_COORDINATOR_EMAIL_ADDRESS_OR_DOMAIN) + ) { + customClaims = { coordinator: true } + + logMsg(`User ${user.uid} identified as coordinator`, MsgType.INFO) + } else { + customClaims = { participant: true } + + logMsg(`User ${user.uid} identified as participant`, MsgType.INFO) + } + + try { + // Set custom user claims on this newly created user. + await admin.auth().setCustomUserClaims(user.uid, customClaims) + } catch (error: any) { + logMsg(`Something went wrong: ${error.toString()}`, MsgType.ERROR) + } +}) diff --git a/packages/backend/src/functions/ceremony.ts b/packages/backend/src/functions/ceremony.ts new file mode 100644 index 00000000..7836be39 --- /dev/null +++ b/packages/backend/src/functions/ceremony.ts @@ -0,0 +1,44 @@ +import * as functions from "firebase-functions" +import dotenv from "dotenv" +import { DocumentSnapshot } from "firebase-functions/v1/firestore" +import { CeremonyState, MsgType } from "../../types/index" +import { queryCeremoniesByStateAndDate } from "../lib/utils" +import { GENERIC_LOGS, logMsg } from "../lib/logs" + +dotenv.config() + +/** + * Automatically look and (if any) start scheduled ceremonies. + */ +export const startCeremony = functions.pubsub.schedule(`every 30 minutes`).onRun(async () => { + // Get ceremonies in `scheduled` state. + const scheduledCeremoniesQuerySnap = await queryCeremoniesByStateAndDate(CeremonyState.SCHEDULED, "startDate", "<=") + + if (scheduledCeremoniesQuerySnap.empty) logMsg(GENERIC_LOGS.GENLOG_NO_CEREMONIES_READY_TO_BE_OPENED, MsgType.INFO) + else { + scheduledCeremoniesQuerySnap.forEach(async (ceremonyDoc: DocumentSnapshot) => { + logMsg(`Ceremony ${ceremonyDoc.id} opened`, MsgType.INFO) + + // Update ceremony state to `running`. + await ceremonyDoc.ref.set({ state: CeremonyState.OPENED }, { merge: true }) + }) + } +}) + +/** + * Automatically look and (if any) stop running ceremonies. + */ +export const stopCeremony = functions.pubsub.schedule(`every 30 minutes`).onRun(async () => { + // Get ceremonies in `running` state. + const runningCeremoniesQuerySnap = await queryCeremoniesByStateAndDate(CeremonyState.OPENED, "endDate", "<=") + + if (runningCeremoniesQuerySnap.empty) logMsg(GENERIC_LOGS.GENLOG_NO_CEREMONIES_READY_TO_BE_CLOSED, MsgType.INFO) + else { + runningCeremoniesQuerySnap.forEach(async (ceremonyDoc: DocumentSnapshot) => { + logMsg(`Ceremony ${ceremonyDoc.id} closed`, MsgType.INFO) + + // Update ceremony state to `finished`. + await ceremonyDoc.ref.set({ state: CeremonyState.CLOSED }, { merge: true }) + }) + } +}) diff --git a/packages/backend/src/functions/contribute.ts b/packages/backend/src/functions/contribute.ts new file mode 100644 index 00000000..a53d7861 --- /dev/null +++ b/packages/backend/src/functions/contribute.ts @@ -0,0 +1,656 @@ +import * as functions from "firebase-functions" +import admin from "firebase-admin" +import dotenv from "dotenv" +import { + CeremonyState, + CeremonyTimeoutType, + MsgType, + ParticipantContributionStep, + ParticipantStatus, + TimeoutType +} from "../../types/index" +import { GENERIC_ERRORS, GENERIC_LOGS, logMsg } from "../lib/logs" +import { collections, timeoutsCollectionFields } from "../lib/constants" +import { + getCeremonyCircuits, + getCurrentServerTimestampInMillis, + getParticipantById, + queryCeremoniesByStateAndDate, + queryValidTimeoutsByDate +} from "../lib/utils" + +dotenv.config() + +/** + * Check if a user can participate for the given ceremony (e.g., new contributor, after timeout expiration, etc.). + */ +export const checkParticipantForCeremony = functions.https.onCall( + async (data: any, context: functions.https.CallableContext) => { + console.log(context.auth) + console.log(context.auth?.token.participant) + console.log(context.auth?.token.coordinator) + + // Check if sender is authenticated. + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for the ceremony. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + + // Check existence. + if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + + // Get ceremony data. + const ceremonyData = ceremonyDoc.data() + + // Check if running. + if (!ceremonyData || ceremonyData.state !== CeremonyState.OPENED) + logMsg(GENERIC_ERRORS.GENERR_CEREMONY_NOT_OPENED, MsgType.ERROR) + + // Look for the user among ceremony participants. + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!participantDoc.exists) { + // Create a new Participant doc for the sender. + await participantDoc.ref.set({ + status: ParticipantStatus.WAITING, + contributionProgress: 0, + contributions: [], + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`User ${userId} has been registered as participant for ceremony ${ceremonyDoc.id}`, MsgType.INFO) + } else { + // Check if the participant has completed the contributions for all circuits. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + const circuits = await getCeremonyCircuits( + `${collections.ceremonies}/${ceremonyDoc.id}/${collections.circuits}` + ) + + // Already contributed to all circuits or currently contributor without any timeout. + if ( + participantData?.contributionProgress === circuits.length && + participantData?.status === ParticipantStatus.DONE + ) { + logMsg( + `Participant ${participantDoc.id} has already contributed to all circuits or is the current contributor to that circuit (no timed out yet)`, + MsgType.DEBUG + ) + + return false + } + + if (participantData?.status === ParticipantStatus.TIMEDOUT) { + // Get `valid` timeouts (i.e., endDate is not expired). + const validTimeoutsQuerySnap = await queryValidTimeoutsByDate( + ceremonyDoc.id, + participantDoc.id, + timeoutsCollectionFields.endDate + ) + + if (validTimeoutsQuerySnap.empty) { + // TODO: need to remove unstable contributions (only one without doc link) and temp data, contributor must restart from step 1. + // The participant can retry the contribution. + await participantDoc.ref.set( + { + status: ParticipantStatus.EXHUMED, + contributionStep: ParticipantContributionStep.DOWNLOADING, + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + + logMsg(`Participant ${participantDoc.id} can retry the contribution from right now`, MsgType.DEBUG) + + return true + } + logMsg(`Participant ${participantDoc.id} cannot retry the contribution yet`, MsgType.DEBUG) + + return false + } + } + + return true + } +) + +/** + * Check and remove the current contributor who is taking more than a specified amount of time for completing the contribution. + */ +export const checkAndRemoveBlockingContributor = functions.pubsub.schedule("every 1 minutes").onRun(async () => { + if (!process.env.CF_RETRY_WAITING_TIME_IN_DAYS) logMsg(GENERIC_ERRORS.GENERR_WRONG_ENV_CONFIGURATION, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + const currentDate = getCurrentServerTimestampInMillis() + + // Get ceremonies in `opened` state. + const openedCeremoniesQuerySnap = await queryCeremoniesByStateAndDate(CeremonyState.OPENED, "endDate", ">=") + + if (openedCeremoniesQuerySnap.empty) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONIES_OPENED, MsgType.ERROR) + + // For each ceremony. + for (const ceremonyDoc of openedCeremoniesQuerySnap.docs) { + if (!ceremonyDoc.exists || !ceremonyDoc.data()) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + + // Get data. + const { timeoutType: ceremonyTimeoutType, penalty } = ceremonyDoc.data() + + // Get circuits. + const circuitsDocs = await getCeremonyCircuits( + `${collections.ceremonies}/${ceremonyDoc.id}/${collections.circuits}` + ) + + // For each circuit. + for (const circuitDoc of circuitsDocs) { + if (!circuitDoc.exists || !circuitDoc.data()) logMsg(GENERIC_ERRORS.GENERR_INVALID_CIRCUIT, MsgType.ERROR) + + const circuitData = circuitDoc.data() + + logMsg(`Circuit document ${circuitDoc.id} okay`, MsgType.DEBUG) + + // Get data. + const { waitingQueue, avgTimings } = circuitData + const { contributors, currentContributor, failedContributions, completedContributions } = waitingQueue + const { fullContribution: avgFullContribution } = avgTimings + + // Check for current contributor. + if (!currentContributor) logMsg(GENERIC_ERRORS.GENERR_NO_CURRENT_CONTRIBUTOR, MsgType.WARN) + + // Check if first contributor. + if ( + !currentContributor && + avgFullContribution === 0 && + completedContributions === 0 && + ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC + ) + logMsg(GENERIC_ERRORS.GENERR_NO_TIMEOUT_FIRST_COTRIBUTOR, MsgType.DEBUG) + + if ( + !!currentContributor && + ((avgFullContribution > 0 && completedContributions > 0) || + ceremonyTimeoutType === CeremonyTimeoutType.FIXED) + ) { + // Get current contributor data (i.e., participant). + const participantDoc = await getParticipantById(ceremonyDoc.id, currentContributor) + + if (!participantDoc.exists || !participantDoc.data()) + logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.WARN) + else { + const participantData = participantDoc.data() + const contributionStartedAt = participantData?.contributionStartedAt + const verificationStartedAt = participantData?.verificationStartedAt + const currentContributionStep = participantData?.contributionStep + + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check for blocking contributions (frontend-side). + const timeoutToleranceThreshold = + ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC + ? (avgFullContribution / 100) * Number(circuitData.timeoutThreshold) + : 0 + + const timeoutExpirationDateInMillisForBlockingContributor = + ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC + ? Number(contributionStartedAt) + + Number(avgFullContribution) + + Number(timeoutToleranceThreshold) + : Number(contributionStartedAt) + + Number(circuitData.timeoutMaxContributionWaitingTime) * 60000 + + logMsg(`Contribution start date ${contributionStartedAt}`, MsgType.DEBUG) + if (ceremonyTimeoutType === CeremonyTimeoutType.DYNAMIC) { + logMsg(`Average contribution per circuit time ${avgFullContribution} ms`, MsgType.DEBUG) + logMsg(`Timeout tolerance threshold set to ${timeoutToleranceThreshold}`, MsgType.DEBUG) + } + logMsg( + `BC Timeout expirartion date ${timeoutExpirationDateInMillisForBlockingContributor} ms`, + MsgType.DEBUG + ) + + // Check for blocking verifications (backend-side). + const timeoutExpirationDateInMillisForBlockingFunction = !verificationStartedAt + ? 0 + : Number(verificationStartedAt) + 3540000 // 3540000 = 59 minutes in ms. + + logMsg(`Verification start date ${verificationStartedAt}`, MsgType.DEBUG) + logMsg( + `CF Timeout expirartion date ${timeoutExpirationDateInMillisForBlockingFunction} ms`, + MsgType.DEBUG + ) + + // Get timeout type. + let timeoutType = 0 + + if ( + timeoutExpirationDateInMillisForBlockingContributor < currentDate && + currentContributionStep >= ParticipantContributionStep.DOWNLOADING && + currentContributionStep <= ParticipantContributionStep.UPLOADING + ) + timeoutType = TimeoutType.BLOCKING_CONTRIBUTION + + if ( + timeoutExpirationDateInMillisForBlockingFunction > 0 && + timeoutExpirationDateInMillisForBlockingFunction < currentDate && + currentContributionStep === ParticipantContributionStep.VERIFYING + ) + timeoutType = TimeoutType.BLOCKING_CLOUD_FUNCTION + + logMsg(`Ceremony Timeout type ${ceremonyTimeoutType}`, MsgType.DEBUG) + logMsg(`Timeout type ${timeoutType}`, MsgType.DEBUG) + + // Check if one timeout should be triggered. + if (timeoutType !== 0) { + // Timeout the participant. + const batch = firestore.batch() + + // 1. Update circuit' waiting queue. + contributors.shift(1) + + let newCurrentContributor = "" + + if (contributors.length > 0) { + // There's someone else ready to contribute. + newCurrentContributor = contributors.at(0) + + // Pass the baton to the next participant. + const newCurrentContributorDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyDoc.id}/${collections.participants}`) + .doc(newCurrentContributor) + .get() + + if (newCurrentContributorDoc.exists) { + batch.update(newCurrentContributorDoc.ref, { + status: ParticipantStatus.WAITING, + lastUpdated: getCurrentServerTimestampInMillis() + }) + } + } + + batch.update(circuitDoc.ref, { + waitingQueue: { + ...circuitData.waitingQueue, + contributors, + currentContributor: newCurrentContributor, + failedContributions: failedContributions + 1 + }, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Batch: update for circuit' waiting queue`, MsgType.DEBUG) + + // 2. Change blocking contributor status. + batch.update(participantDoc.ref, { + status: ParticipantStatus.TIMEDOUT, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Batch: change blocking contributor status to TIMEDOUT`, MsgType.DEBUG) + + // 3. Create a new collection of timeouts (to keep track of participants timeouts). + const retryWaitingTimeInMillis = + timeoutType === TimeoutType.BLOCKING_CONTRIBUTION + ? Number(penalty) * 60000 // 60000 = amount of ms x minute. + : Number(process.env.CF_RETRY_WAITING_TIME_IN_DAYS) * 86400000 // 86400000 = amount of ms x day. + + // Timeout collection. + const timeoutDoc = await firestore + .collection( + `${collections.ceremonies}/${ceremonyDoc.id}/${collections.participants}/${participantDoc.id}/${collections.timeouts}` + ) + .doc() + .get() + + batch.create(timeoutDoc.ref, { + type: timeoutType, + startDate: currentDate, + endDate: currentDate + retryWaitingTimeInMillis + }) + + logMsg(`Batch: add timeout document for blocking contributor`, MsgType.DEBUG) + + await batch.commit() + + logMsg(`Blocking contributor ${participantDoc.id} timedout. Cause ${timeoutType}`, MsgType.INFO) + } else logMsg(GENERIC_LOGS.GENLOG_NO_TIMEOUT, MsgType.INFO) + } + } + } + } +}) + +/** + * Progress to next contribution step for the current contributor of a specified circuit in a given ceremony. + */ +export const progressToNextContributionStep = functions.https.onCall( + async (data: any, context: functions.https.CallableContext) => { + // Check if sender is authenticated. + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for the ceremony. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + + // Check existence. + if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + + // Get ceremony data. + const ceremonyData = ceremonyDoc.data() + + // Check if running. + if (!ceremonyData || ceremonyData.state !== CeremonyState.OPENED) + logMsg(GENERIC_ERRORS.GENERR_CEREMONY_NOT_OPENED, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) + + // Look for the user among ceremony participants. + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + // Check existence. + if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) + + // Get participant data. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check if participant is able to advance to next contribution step. + if (participantData?.status !== ParticipantStatus.CONTRIBUTING) + logMsg(`Participant ${participantDoc.id} is not contributing`, MsgType.ERROR) + + // Make the advancement. + const progress = Number(participantData?.contributionStep) + 1 + + logMsg(`Current contribution step should be ${participantData?.contributionStep}`, MsgType.DEBUG) + logMsg(`Next contribution step should be ${progress}`, MsgType.DEBUG) + + // nb. DOWNLOADING (=1) must be set when coordinating the waiting queue while COMPLETED (=5) must be set in verifyContribution(). + if (progress <= ParticipantContributionStep.DOWNLOADING || progress >= ParticipantContributionStep.COMPLETED) + logMsg(`Wrong contribution step ${progress} for ${participantDoc.id}`, MsgType.ERROR) + + if (progress === ParticipantContributionStep.VERIFYING) + await participantDoc.ref.update({ + contributionStep: progress, + verificationStartedAt: getCurrentServerTimestampInMillis(), + lastUpdated: getCurrentServerTimestampInMillis() + }) + else + await participantDoc.ref.update({ + contributionStep: progress, + lastUpdated: getCurrentServerTimestampInMillis() + }) + } +) + +/** + * Temporary store the contribution computation time for the current contributor. + */ +export const temporaryStoreCurrentContributionComputationTime = functions.https.onCall( + async (data: any, context: functions.https.CallableContext) => { + // Check if sender is authenticated. + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId || data.contributionComputationTime <= 0) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + // Check existence. + if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) + + // Get data. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check if has reached the computing step while contributing. + if (participantData?.contributionStep !== ParticipantContributionStep.COMPUTING) + logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) + + // Update. + await participantDoc.ref.set( + { + ...participantData!, + tempContributionData: { + contributionComputationTime: data.contributionComputationTime + }, + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + } +) + +/** + * Permanently store the contribution computation hash for attestation generation for the current contributor. + */ +export const permanentlyStoreCurrentContributionTimeAndHash = functions.https.onCall( + async (data: any, context: functions.https.CallableContext) => { + // Check if sender is authenticated. + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId || data.contributionComputationTime <= 0 || !data.contributionHash) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + // Check existence. + if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) + + // Get data. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check if has reached the computing step while contributing or is finalizing. + if ( + participantData?.contributionStep === ParticipantContributionStep.COMPUTING || + (context?.auth?.token.coordinator && participantData?.status === ParticipantStatus.FINALIZING) + ) + // Update. + await participantDoc.ref.set( + { + ...participantData!, + contributions: [ + ...participantData!.contributions, + { + hash: data.contributionHash!, + computationTime: data.contributionComputationTime + } + ], + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + else logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) + } +) + +/** + * Temporary store the the Multi-Part Upload identifier for the current contributor. + */ +export const temporaryStoreCurrentContributionMultiPartUploadId = functions.https.onCall( + async (data: any, context: functions.https.CallableContext) => { + // Check if sender is authenticated. + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId || !data.uploadId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + // Check existence. + if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) + + // Get data. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check if has reached the uploading step while contributing. + if (participantData?.contributionStep !== ParticipantContributionStep.UPLOADING) + logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) + + // Update. + await participantDoc.ref.set( + { + ...participantData!, + tempContributionData: { + ...participantData?.tempContributionData, + uploadId: data.uploadId, + chunks: [] + }, + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + } +) + +/** + * Temporary store the ETag and PartNumber for each uploaded chunk in order to make the upload resumable from last chunk. + */ +export const temporaryStoreCurrentContributionUploadedChunkData = functions.https.onCall( + async (data: any, context: functions.https.CallableContext) => { + // Check if sender is authenticated. + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId || !data.eTag || data.partNumber <= 0) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + // Check existence. + if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT, MsgType.ERROR) + + // Get data. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyId} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check if has reached the uploading step while contributing. + if (participantData?.contributionStep !== ParticipantContributionStep.UPLOADING) + logMsg(GENERIC_ERRORS.GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP, MsgType.ERROR) + + const chunks = participantData?.tempContributionData.chunks ? participantData?.tempContributionData.chunks : [] + + // Add last chunk. + chunks.push({ + ETag: data.eTag, + PartNumber: data.partNumber + }) + + // Update. + await participantDoc.ref.set( + { + ...participantData!, + tempContributionData: { + ...participantData?.tempContributionData, + chunks + }, + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + } +) diff --git a/packages/backend/src/functions/finalize.ts b/packages/backend/src/functions/finalize.ts new file mode 100644 index 00000000..8f0b4de3 --- /dev/null +++ b/packages/backend/src/functions/finalize.ts @@ -0,0 +1,245 @@ +import * as functions from "firebase-functions" +import admin from "firebase-admin" +import path from "path" +import os from "os" +import fs from "fs" +import blake from "blakejs" +import { logMsg, GENERIC_ERRORS } from "../lib/logs" +import { collections } from "../lib/constants" +import { CeremonyState, MsgType, ParticipantStatus } from "../../types/index" +import { + getCeremonyCircuits, + getCurrentServerTimestampInMillis, + getFinalContributionDocument, + getS3Client, + tempDownloadFromBucket +} from "../lib/utils" + +/** + * Check and prepare the coordinator for the ceremony finalization. + */ +export const checkAndPrepareCoordinatorForFinalization = functions.https.onCall( + async (data: any, context: functions.https.CallableContext) => { + // Check if sender is authenticated. + if (!context.auth || !context.auth.token.coordinator) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for the ceremony. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + + // Check existence. + if (!ceremonyDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_CEREMONY, MsgType.ERROR) + + // Get ceremony data. + const ceremonyData = ceremonyDoc.data() + + // Check if running. + if (!ceremonyData || ceremonyData.state !== CeremonyState.CLOSED) + logMsg(GENERIC_ERRORS.GENERR_CEREMONY_NOT_CLOSED, MsgType.ERROR) + + // Look for the coordinator among ceremony participant. + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + // Check if the coordinator has completed the contributions for all circuits. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + const circuits = await getCeremonyCircuits( + `${collections.ceremonies}/${ceremonyDoc.id}/${collections.circuits}` + ) + + // Already contributed to all circuits. + if ( + participantData?.contributionProgress === circuits.length + 1 || + participantData?.status === ParticipantStatus.DONE + ) { + // Update participant status. + await participantDoc.ref.set( + { + status: ParticipantStatus.FINALIZING, + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + + logMsg(`Coordinator ${participantDoc.id} ready for finalization`, MsgType.DEBUG) + + return true + } + return false + } +) + +/** + * Add Verifier smart contract and verification key files metadata to the last final contribution for verifiability/integrity of the ceremony. + */ +export const finalizeLastContribution = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + if (!context.auth || !context.auth.token.coordinator) + logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) + + if (!data.ceremonyId || !data.circuitId || !data.bucketName) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get Storage. + const S3 = await getS3Client() + + // Get data. + const { ceremonyId, circuitId, bucketName } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const circuitDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.circuits}`) + .doc(circuitId) + .get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + const contributionDoc = await getFinalContributionDocument( + `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuitId}/${collections.contributions}` + ) + + if (!ceremonyDoc.exists || !circuitDoc.exists || !participantDoc.exists || !contributionDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const circuitData = circuitDoc.data() + const participantData = participantDoc.data() + const contributionData = contributionDoc.data() + + if (!ceremonyData || !circuitData || !participantData || !contributionData) + logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Circuit document ${circuitDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + logMsg(`Contribution document ${contributionDoc.id} okay`, MsgType.DEBUG) + + // Filenames. + const verificationKeyFilename = `${circuitData?.prefix}_vkey.json` + const verifierContractFilename = `${circuitData?.prefix}_verifier.sol` + + // Get storage paths. + const verificationKeyStoragePath = `${collections.circuits}/${circuitData?.prefix}/${verificationKeyFilename}` + const verifierContractStoragePath = `${collections.circuits}/${circuitData?.prefix}/${verifierContractFilename}` + + // Temporary store files from bucket. + const verificationKeyTmpFilePath = path.join(os.tmpdir(), verificationKeyFilename) + const verifierContractTmpFilePath = path.join(os.tmpdir(), verifierContractFilename) + + await tempDownloadFromBucket(S3, bucketName, verificationKeyStoragePath, verificationKeyTmpFilePath) + await tempDownloadFromBucket(S3, bucketName, verifierContractStoragePath, verifierContractTmpFilePath) + + // Compute blake2b hash before unlink. + const verificationKeyBuffer = fs.readFileSync(verificationKeyTmpFilePath) + const verifierContractBuffer = fs.readFileSync(verifierContractTmpFilePath) + + logMsg(`Downloads from storage completed`, MsgType.INFO) + + const verificationKeyBlake2bHash = blake.blake2bHex(verificationKeyBuffer) + const verifierContractBlake2bHash = blake.blake2bHex(verifierContractBuffer) + + // Unlink folders. + fs.unlinkSync(verificationKeyTmpFilePath) + fs.unlinkSync(verifierContractTmpFilePath) + + // Update DB. + const batch = firestore.batch() + + batch.update(contributionDoc.ref, { + files: { + ...contributionData?.files, + verificationKeyBlake2bHash, + verificationKeyFilename, + verificationKeyStoragePath, + verifierContractBlake2bHash, + verifierContractFilename, + verifierContractStoragePath + }, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + await batch.commit() + + logMsg( + `Circuit ${circuitId} correctly finalized - Ceremony ${ceremonyDoc.id} - Coordinator ${participantDoc.id}`, + MsgType.INFO + ) + } +) + +/** + * Finalize a closed ceremony. + */ +export const finalizeCeremony = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + if (!context.auth || !context.auth.token.coordinator) + logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) + + if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + // Update DB. + const batch = firestore.batch() + + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!ceremonyDoc.exists || !participantDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const participantData = participantDoc.data() + + if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check if the ceremony has state equal to closed. + if (ceremonyData?.state === CeremonyState.CLOSED && participantData?.status === ParticipantStatus.FINALIZING) { + // Finalize the ceremony. + batch.update(ceremonyDoc.ref, { state: CeremonyState.FINALIZED }) + + // Update coordinator status. + batch.update(participantDoc.ref, { + status: ParticipantStatus.FINALIZED + }) + + await batch.commit() + + logMsg(`Ceremony ${ceremonyDoc.id} correctly finalized - Coordinator ${participantDoc.id}`, MsgType.INFO) + } + } +) diff --git a/packages/backend/src/functions/index.ts b/packages/backend/src/functions/index.ts new file mode 100644 index 00000000..0dad8244 --- /dev/null +++ b/packages/backend/src/functions/index.ts @@ -0,0 +1,61 @@ +import admin from "firebase-admin" +import { registerAuthUser, processSignUpWithCustomClaims } from "./auth" +import { startCeremony, stopCeremony } from "./ceremony" +import { setupCeremony, initEmptyWaitingQueueForCircuit } from "./setup" +import { + checkParticipantForCeremony, + checkAndRemoveBlockingContributor, + progressToNextContributionStep, + temporaryStoreCurrentContributionComputationTime, + permanentlyStoreCurrentContributionTimeAndHash, + temporaryStoreCurrentContributionMultiPartUploadId, + temporaryStoreCurrentContributionUploadedChunkData +} from "./contribute" +import { + coordinateContributors, + verifycontribution, + refreshParticipantAfterContributionVerification, + makeProgressToNextContribution, + resumeContributionAfterTimeoutExpiration +} from "./waitingQueue" +import { checkAndPrepareCoordinatorForFinalization, finalizeLastContribution, finalizeCeremony } from "./finalize" +import { + createBucket, + checkIfObjectExist, + generateGetObjectPreSignedUrl, + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload +} from "./storage" + +admin.initializeApp() + +export { + registerAuthUser, + processSignUpWithCustomClaims, + startCeremony, + stopCeremony, + checkAndPrepareCoordinatorForFinalization, + finalizeLastContribution, + finalizeCeremony, + setupCeremony, + initEmptyWaitingQueueForCircuit, + checkParticipantForCeremony, + checkAndRemoveBlockingContributor, + progressToNextContributionStep, + temporaryStoreCurrentContributionComputationTime, + permanentlyStoreCurrentContributionTimeAndHash, + temporaryStoreCurrentContributionMultiPartUploadId, + temporaryStoreCurrentContributionUploadedChunkData, + coordinateContributors, + verifycontribution, + refreshParticipantAfterContributionVerification, + makeProgressToNextContribution, + resumeContributionAfterTimeoutExpiration, + createBucket, + checkIfObjectExist, + generateGetObjectPreSignedUrl, + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload +} diff --git a/packages/backend/src/functions/setup.ts b/packages/backend/src/functions/setup.ts new file mode 100644 index 00000000..5607053a --- /dev/null +++ b/packages/backend/src/functions/setup.ts @@ -0,0 +1,108 @@ +import * as functions from "firebase-functions" +import admin from "firebase-admin" +import dotenv from "dotenv" +import { QueryDocumentSnapshot } from "firebase-functions/v1/firestore" +import { CeremonyState, CeremonyType, MsgType } from "../../types/index" +import { GENERIC_ERRORS, logMsg } from "../lib/logs" +import { getCurrentServerTimestampInMillis } from "../lib/utils" +import { collections } from "../lib/constants" + +dotenv.config() + +/** + * Bootstrap/Setup every necessary document for running a ceremony. + */ +export const setupCeremony = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + if (!context.auth || !context.auth.token.coordinator) + logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) + + if (!data.ceremonyInputData || !data.ceremonyPrefix || !data.circuits) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Database. + const firestore = admin.firestore() + const batch = firestore.batch() + + // Get data. + const { ceremonyInputData, ceremonyPrefix, circuits } = data + const userId = context.auth?.uid + + // Ceremonies. + const ceremonyDoc = await firestore.collection(`${collections.ceremonies}/`).doc().get() + + batch.create(ceremonyDoc.ref, { + title: ceremonyInputData.title, + description: ceremonyInputData.description, + startDate: new Date(ceremonyInputData.startDate).valueOf(), + endDate: new Date(ceremonyInputData.endDate).valueOf(), + prefix: ceremonyPrefix, + state: CeremonyState.SCHEDULED, + type: CeremonyType.PHASE2, + penalty: ceremonyInputData.penalty, + timeoutType: ceremonyInputData.timeoutMechanismType, + coordinatorId: userId, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + // Circuits. + if (!circuits.length) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUIT_PROVIDED, MsgType.ERROR) + + for (const circuit of circuits) { + const circuitDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyDoc.ref.id}/${collections.circuits}`) + .doc() + .get() + + batch.create(circuitDoc.ref, { + ...circuit, + lastUpdated: getCurrentServerTimestampInMillis() + }) + } + + await batch.commit() + + logMsg(`Ceremony ${ceremonyDoc.id} setup successfully completed - Coordinator ${userId}`, MsgType.INFO) + } +) + +/** + * Initialize an empty Waiting Queue field for the newly created circuit document. + */ +export const initEmptyWaitingQueueForCircuit = functions.firestore + .document(`/${collections.ceremonies}/{ceremony}/${collections.circuits}/{circuit}`) + .onCreate(async (doc: QueryDocumentSnapshot) => { + // Get DB. + const firestore = admin.firestore() + + // Get doc info. + const circuitId = doc.id + const circuitData = doc.data() + const parentCollectionPath = doc.ref.parent.path // == /ceremonies/{ceremony}/circuits/. + + // Empty waiting queue. + const waitingQueue = { + contributors: [], + currentContributor: "", + completedContributions: 0, // == nextZkeyIndex. + failedContributions: 0 + } + + // Update the circuit document. + await firestore + .collection(parentCollectionPath) + .doc(circuitId) + .set( + { + ...circuitData, + waitingQueue, + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + + logMsg( + `Empty waiting queue successfully initialized for circuit ${circuitId} - Ceremony ${doc.id}`, + MsgType.INFO + ) + }) diff --git a/packages/backend/src/functions/storage.ts b/packages/backend/src/functions/storage.ts new file mode 100644 index 00000000..40622a39 --- /dev/null +++ b/packages/backend/src/functions/storage.ts @@ -0,0 +1,341 @@ +import * as functions from "firebase-functions" +import admin from "firebase-admin" +import { + GetObjectCommand, + CreateMultipartUploadCommand, + UploadPartCommand, + CompleteMultipartUploadCommand, + HeadObjectCommand, + CreateBucketCommand +} from "@aws-sdk/client-s3" +import { getSignedUrl } from "@aws-sdk/s3-request-presigner" +import dotenv from "dotenv" +import { MsgType, ParticipantContributionStep, ParticipantStatus } from "../../types/index" +import { logMsg, GENERIC_ERRORS } from "../lib/logs" +import { getS3Client } from "../lib/utils" +import { collections } from "../lib/constants" + +dotenv.config() + +/** + * Create a new AWS S3 bucket for a particular ceremony. + */ +export const createBucket = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + // Checks. + if (!context.auth || !context.auth.token.coordinator) + logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) + + if (!data.bucketName) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Connect w/ S3. + const S3 = await getS3Client() + + // Prepare command. + const command = new CreateBucketCommand({ + Bucket: data.bucketName, + CreateBucketConfiguration: { + LocationConstraint: process.env.AWS_REGION! + } + }) + + try { + // Send command. + const response = await S3.send(command) + + // Check response. + if (response.$metadata.httpStatusCode === 200 && !!response.Location) { + logMsg(`Bucket successfully created`, MsgType.LOG) + + return true + } + } catch (error: any) { + if (error.$metadata.httpStatusCode === 400 && error.Code === "InvalidBucketName") { + logMsg(`Bucket not created: ${error.Code}`, MsgType.LOG) + } + + logMsg(`Generic error when creating a new S3 bucket: ${error}`, MsgType.ERROR) + } + + return false + } +) + +/** + * Check if a specified object exist in a given AWS S3 bucket. + */ +export const checkIfObjectExist = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + // Checks. + if (!context.auth || !context.auth.token.coordinator) + logMsg(GENERIC_ERRORS.GENERR_NO_COORDINATOR, MsgType.ERROR) + + if (!data.bucketName || !data.objectKey) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Connect w/ S3. + const S3 = await getS3Client() + + // Prepare command. + const command = new HeadObjectCommand({ Bucket: data.bucketName, Key: data.objectKey }) + + try { + // Send command. + const response = await S3.send(command) + + // Check response. + if (response.$metadata.httpStatusCode === 200 && !!response.ETag) { + logMsg(`Object: ${data.objectKey} exists!`, MsgType.LOG) + + return true + } + } catch (error: any) { + if (error.$metadata.httpStatusCode === 404 && !error.ETag) { + logMsg(`Object: ${data.objectKey} does not exist!`, MsgType.LOG) + + return false + } + + logMsg(`Generic error when checking for object on S3 bucket: ${error}`, MsgType.ERROR) + } + + return false + } +) + +/** + * Generate a new AWS S3 pre signed url to upload/download an object (GET). + */ +export const generateGetObjectPreSignedUrl = functions.https.onCall(async (data: any): Promise<any> => { + if (!data.bucketName || !data.objectKey) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Connect w/ S3. + const S3 = await getS3Client() + + // Prepare the command. + const command = new GetObjectCommand({ Bucket: data.bucketName, Key: data.objectKey }) + + // Get the PreSignedUrl. + const url = await getSignedUrl(S3, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) }) + + logMsg(`Single Pre-Signed URL ${url}`, MsgType.LOG) + + return url +}) + +/** + * Initiate a multi part upload for a specific object in AWS S3 bucket. + */ +export const startMultiPartUpload = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.bucketName || !data.objectKey || (context.auth?.token.participant && !data.ceremonyId)) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { bucketName, objectKey, ceremonyId } = data + const userId = context.auth?.uid + + if (context.auth?.token.participant && !!ceremonyId) { + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!ceremonyDoc.exists || !participantDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const participantData = participantDoc.data() + + if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check participant status and contribution step. + const { status, contributionStep } = participantData! + + if (status !== ParticipantStatus.CONTRIBUTING && contributionStep !== ParticipantContributionStep.UPLOADING) + logMsg( + `Participant ${participantDoc.id} is not able to start a multi part upload right now`, + MsgType.ERROR + ) + } + + // Connect w/ S3. + const S3 = await getS3Client() + + // Prepare command. + const command = new CreateMultipartUploadCommand({ Bucket: bucketName, Key: objectKey }) + + // Send command. + const responseInitiate = await S3.send(command) + const uploadId = responseInitiate.UploadId + + logMsg(`Upload ID: ${uploadId}`, MsgType.LOG) + + return uploadId + } +) + +/** + * Generate a PreSignedUrl for each part of the given multi part upload. + */ +export const generatePreSignedUrlsParts = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if ( + !data.bucketName || + !data.objectKey || + !data.uploadId || + data.numberOfParts <= 0 || + (context.auth?.token.participant && !data.ceremonyId) + ) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { bucketName, objectKey, uploadId, numberOfParts, ceremonyId } = data + const userId = context.auth?.uid + + if (context.auth?.token.participant && !!ceremonyId) { + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!ceremonyDoc.exists || !participantDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const participantData = participantDoc.data() + + if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check participant status and contribution step. + const { status, contributionStep } = participantData! + + if (status !== ParticipantStatus.CONTRIBUTING && contributionStep !== ParticipantContributionStep.UPLOADING) + logMsg( + `Participant ${participantDoc.id} is not able to start a multi part upload right now`, + MsgType.ERROR + ) + } + + // Connect w/ S3. + const S3 = await getS3Client() + + const parts = [] + + for (let i = 0; i < numberOfParts; i += 1) { + // Prepare command for each part. + const command = new UploadPartCommand({ + Bucket: bucketName, + Key: objectKey, + PartNumber: i + 1, + UploadId: uploadId + }) + + // Get the PreSignedUrl for uploading the specific part. + const signedUrl = await getSignedUrl(S3, command, { + expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) + }) + + parts.push(signedUrl) + } + + return parts + } +) + +/** + * Ultimate the multi part upload for a specific object in AWS S3 bucket. + */ +export const completeMultiPartUpload = functions.https.onCall( + async (data: any, context: functions.https.CallableContext): Promise<any> => { + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if ( + !data.bucketName || + !data.objectKey || + !data.uploadId || + !data.parts || + (context.auth?.token.participant && !data.ceremonyId) + ) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { bucketName, objectKey, uploadId, parts, ceremonyId } = data + const userId = context.auth?.uid + + if (context.auth?.token.participant && !!ceremonyId) { + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!ceremonyDoc.exists || !participantDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const participantData = participantDoc.data() + + if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + // Check participant status and contribution step. + const { status, contributionStep } = participantData! + + if (status !== ParticipantStatus.CONTRIBUTING && contributionStep !== ParticipantContributionStep.UPLOADING) + logMsg( + `Participant ${participantDoc.id} is not able to start a multi part upload right now`, + MsgType.ERROR + ) + } + + // Connect w/ S3. + const S3 = await getS3Client() + + // Prepare command. + const command = new CompleteMultipartUploadCommand({ + Bucket: bucketName, + Key: objectKey, + UploadId: uploadId, + MultipartUpload: { Parts: parts } + }) + + // Send command. + const responseComplete = await S3.send(command) + + logMsg(`Upload for ${data.uploadId} completed! Object location ${responseComplete.Location}`, MsgType.LOG) + + return responseComplete.Location + } +) diff --git a/packages/backend/src/functions/waitingQueue.ts b/packages/backend/src/functions/waitingQueue.ts new file mode 100644 index 00000000..d6d2e150 --- /dev/null +++ b/packages/backend/src/functions/waitingQueue.ts @@ -0,0 +1,764 @@ +import * as functionsV1 from "firebase-functions/v1" +import * as functionsV2 from "firebase-functions/v2" +import admin from "firebase-admin" +import dotenv from "dotenv" +import { QueryDocumentSnapshot } from "firebase-functions/v1/firestore" +import { Change } from "firebase-functions" +import { zKey } from "snarkjs" +import path from "path" +import os from "os" +import fs from "fs" +import { Timer } from "timer-node" +import blake from "blakejs" +import winston from "winston" +import { FieldValue } from "firebase-admin/firestore" +import { CeremonyState, MsgType, ParticipantContributionStep, ParticipantStatus } from "../../types/index" +import { + deleteObject, + formatZkeyIndex, + getCircuitDocumentByPosition, + getCurrentServerTimestampInMillis, + getS3Client, + sleep, + tempDownloadFromBucket, + uploadFileToBucket +} from "../lib/utils" +import { collections, names } from "../lib/constants" +import { GENERIC_ERRORS, logMsg } from "../lib/logs" + +dotenv.config() + +/** + * Automate the coordination for participants contributions. + * @param circuit <QueryDocumentSnapshot> - the circuit document. + * @param participant <QueryDocumentSnapshot> - the participant document. + * @param ceremonyId <string> - the ceremony identifier. + */ +const coordinate = async (circuit: QueryDocumentSnapshot, participant: QueryDocumentSnapshot, ceremonyId?: string) => { + // Get DB. + const firestore = admin.firestore() + // Update DB. + const batch = firestore.batch() + + // Get info. + const participantId = participant.id + const circuitData = circuit.data() + const participantData = participant.data() + + logMsg(`Circuit document ${circuit.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantId} okay`, MsgType.DEBUG) + + const { waitingQueue } = circuitData + const { contributors } = waitingQueue + let { currentContributor } = waitingQueue + let newParticipantStatus = 0 + let newContributionStep = 0 + + // Case 1: Participant is ready to contribute and there's nobody in the queue. + if (!contributors.length && !currentContributor) { + logMsg( + `Coordination use-case 1: Participant is ready to contribute and there's nobody in the queue`, + MsgType.INFO + ) + + currentContributor = participantId + newParticipantStatus = ParticipantStatus.CONTRIBUTING + newContributionStep = ParticipantContributionStep.DOWNLOADING + } + + // Case 2: Participant is ready to contribute but there's another participant currently contributing. + if (currentContributor !== participantId) { + logMsg( + `Coordination use-case 2: Participant is ready to contribute but there's another participant currently contributing`, + MsgType.INFO + ) + + newParticipantStatus = ParticipantStatus.WAITING + } + + // Case 3: the participant has finished the contribution so this case is used to update the i circuit queue. + if ( + currentContributor === participantId && + (participantData.status === ParticipantStatus.CONTRIBUTED || + participantData.status === ParticipantStatus.DONE) && + participantData.contributionStep === ParticipantContributionStep.COMPLETED + ) { + logMsg( + `Coordination use-case 3: Participant has finished the contribution so this case is used to update the i circuit queue`, + MsgType.INFO + ) + + contributors.shift(1) + + if (contributors.length > 0) { + // There's someone else ready to contribute. + currentContributor = contributors.at(0) + + // Pass the baton to the next participant. + const newCurrentContributorDoc = await firestore + .collection(`${ceremonyId}/${collections.participants}`) + .doc(currentContributor) + .get() + + if (newCurrentContributorDoc.exists) { + batch.update(newCurrentContributorDoc.ref, { + status: ParticipantStatus.WAITING, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Batch update use-case 3: New current contributor`, MsgType.INFO) + } + } else currentContributor = "" + } + + // Updates for cases 1 and 2. + if (newParticipantStatus !== 0) { + contributors.push(participantId) + + batch.update(participant.ref, { + status: newParticipantStatus, + contributionStartedAt: + newParticipantStatus === ParticipantStatus.CONTRIBUTING ? getCurrentServerTimestampInMillis() : 0, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + // Case 1 only. + if (newContributionStep !== 0) + batch.update(participant.ref, { + contributionStep: newContributionStep, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Batch update use-case 1 or 2: participant updates`, MsgType.INFO) + } + + // Update waiting queue. + batch.update(circuit.ref, { + waitingQueue: { + ...waitingQueue, + contributors, + currentContributor + }, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Batch update all use-cases: update circuit waiting queue`, MsgType.INFO) + + await batch.commit() +} + +/** + * Coordinate waiting queue contributors. + */ +export const coordinateContributors = functionsV1.firestore + .document(`${collections.ceremonies}/{ceremonyId}/${collections.participants}/{participantId}`) + .onUpdate(async (change: Change<QueryDocumentSnapshot>) => { + // Before changes. + const participantBefore = change.before + const dataBefore = participantBefore.data() + const { + contributionProgress: beforeContributionProgress, + status: beforeStatus, + contributionStep: beforeContributionStep + } = dataBefore + + // After changes. + const participantAfter = change.after + const dataAfter = participantAfter.data() + const { + contributionProgress: afterContributionProgress, + status: afterStatus, + contributionStep: afterContributionStep + } = dataAfter + + // Get the ceremony identifier (this does not change from before/after). + const ceremonyId = participantBefore.ref.parent.parent!.path + + if (!ceremonyId) logMsg(GENERIC_ERRORS.GENERR_NO_CEREMONY_PROVIDED, MsgType.ERROR) + + logMsg(`Coordinating participants for ceremony ${ceremonyId}`, MsgType.INFO) + + logMsg(`Participant document ${participantBefore.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantAfter.id} okay`, MsgType.DEBUG) + logMsg( + `Participant ${participantBefore.id} the status from ${beforeStatus} to ${afterStatus} and the contribution progress from ${beforeContributionProgress} to ${afterContributionProgress}`, + MsgType.INFO + ) + + // nb. existance checked above. + const circuitsPath = `${participantBefore.ref.parent.parent!.path}/${collections.circuits}` + + // When a participant changes is status to ready, is "ready" to become a contributor. + if (afterStatus === ParticipantStatus.READY) { + // When beforeContributionProgress === 0 is a new participant, when beforeContributionProgress === afterContributionProgress the participant is retrying. + if (beforeContributionProgress === 0 || beforeContributionProgress === afterContributionProgress) { + logMsg( + `Participant has status READY and before contribution progress ${beforeContributionProgress} is different from after contribution progress ${afterContributionProgress}`, + MsgType.INFO + ) + + // i -> k where i == 0 + // (participant newly created). We work only on circuit k. + const circuit = await getCircuitDocumentByPosition(circuitsPath, afterContributionProgress) + + logMsg(`Circuit document ${circuit.id} okay`, MsgType.DEBUG) + + // The circuit info (i.e., the queue) is useful only to check turns for contribution. + // The participant info is useful to really pass the baton (starting the contribution). + // So, the info on the circuit says "it's your turn" while the info on the participant says "okay, i'm ready/waiting etc.". + // The contribution progress number completes everything because indicates which circuit is involved. + await coordinate(circuit, participantAfter) + logMsg(`Circuit ${circuit.id} has been updated (waiting queue)`, MsgType.INFO) + } + + if (afterContributionProgress === beforeContributionProgress + 1 && beforeContributionProgress !== 0) { + logMsg( + `Participant has status READY and before contribution progress ${beforeContributionProgress} is different from before contribution progress ${afterContributionProgress}`, + MsgType.INFO + ) + + // i -> k where k === i + 1 + // (participant has already contributed to i and the contribution has been verified, + // participant now is ready to be put in line for contributing on k circuit). + + const afterCircuit = await getCircuitDocumentByPosition(circuitsPath, afterContributionProgress) + + // logMsg(`Circuit document ${beforeCircuit.id} okay`, MsgType.DEBUG) + logMsg(`Circuit document ${afterCircuit.id} okay`, MsgType.DEBUG) + + // Coordinate after circuit (update waiting queue). + await coordinate(afterCircuit, participantAfter) + logMsg(`After circuit ${afterCircuit.id} has been updated (waiting queue)`, MsgType.INFO) + } + } + + // The contributor has finished the contribution and the waiting queue for the circuit needs to be updated. + if ( + (afterStatus === ParticipantStatus.DONE && beforeStatus !== ParticipantStatus.DONE) || + (beforeContributionProgress === afterContributionProgress && + afterStatus === ParticipantStatus.CONTRIBUTED && + beforeStatus === ParticipantStatus.CONTRIBUTING && + beforeContributionStep === ParticipantContributionStep.VERIFYING && + afterContributionStep === ParticipantContributionStep.COMPLETED) + ) { + logMsg(`Participant has status DONE or has finished the contribution`, MsgType.INFO) + + // Update the last circuits waiting queue. + const beforeCircuit = await getCircuitDocumentByPosition(circuitsPath, beforeContributionProgress) + + logMsg(`Circuit document ${beforeCircuit.id} okay`, MsgType.DEBUG) + + // Coordinate before circuit (update waiting queue + pass the baton to the next). + await coordinate(beforeCircuit, participantAfter, ceremonyId) + logMsg( + `Before circuit ${beforeCircuit.id} has been updated (waiting queue + pass the baton to next)`, + MsgType.INFO + ) + } + }) + +/** + * Automate the contribution verification. + */ +export const verifycontribution = functionsV2.https.onCall( + { memory: "32GiB", cpu: 8, timeoutSeconds: 3600 }, + async (request: functionsV2.https.CallableRequest<any>): Promise<any> => { + const verifyCloudFunctionTimer = new Timer({ label: "verifyCloudFunction" }) + verifyCloudFunctionTimer.start() + + if (!request.auth || (!request.auth.token.participant && !request.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!request.data.ceremonyId || !request.data.circuitId || !request.data.ghUsername || !request.data.bucketName) + logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get Storage. + const S3 = await getS3Client() + + // Get data. + const { ceremonyId, circuitId, ghUsername, bucketName } = request.data + const userId = request.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const circuitDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.circuits}`) + .doc(circuitId) + .get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!ceremonyDoc.exists || !circuitDoc.exists || !participantDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const circuitData = circuitDoc.data() + const participantData = participantDoc.data() + + if (!ceremonyData || !circuitData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Circuit document ${circuitDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + let valid = false + let verificationComputationTime = 0 + let fullContributionTime = 0 + + // Check if is the verification for ceremony finalization. + const finalize = ceremonyData?.state === CeremonyState.CLOSED && request.auth && request.auth.token.coordinator + + if (participantData?.status === ParticipantStatus.CONTRIBUTING || finalize) { + // Compute last zkey index. + const lastZkeyIndex = formatZkeyIndex(circuitData!.waitingQueue.completedContributions + 1) + + // Reconstruct transcript path. + const transcriptFilename = `${circuitData?.prefix}_${ + finalize + ? `${ghUsername}_final_verification_transcript.log` + : `${lastZkeyIndex}_${ghUsername}_verification_transcript.log` + }` + const transcriptStoragePath = `${collections.circuits}/${circuitData?.prefix}/${names.transcripts}/${transcriptFilename}` + const transcriptTempFilePath = path.join(os.tmpdir(), transcriptFilename) + + // Custom logger for verification transcript. + const transcriptLogger = winston.createLogger({ + level: "info", + format: winston.format.printf((log) => log.message), + transports: [ + // Write all logs with importance level of `info` to `transcript.json`. + new winston.transports.File({ + filename: transcriptTempFilePath, + level: "info" + }) + ] + }) + + transcriptLogger.info( + `${finalize ? `Final verification` : `Verification`} transcript for ${ + circuitData?.prefix + } circuit Phase 2 contribution.\n${ + finalize ? `Coordinator ` : `Contributor # ${Number(lastZkeyIndex)}` + } (${ghUsername})\n` + ) + + // Get storage paths. + const potStoragePath = `${names.pot}/${circuitData?.files.potFilename}` + const firstZkeyStoragePath = `${collections.circuits}/${circuitData?.prefix}/${collections.contributions}/${circuitData?.prefix}_00000.zkey` + const lastZkeyStoragePath = `${collections.circuits}/${circuitData?.prefix}/${collections.contributions}/${ + circuitData?.prefix + }_${finalize ? `final` : lastZkeyIndex}.zkey` + + // Temporary store files from bucket. + const { potFilename } = circuitData!.files + const firstZkeyFilename = `${circuitData?.prefix}_00000.zkey` + const lastZkeyFilename = `${circuitData?.prefix}_${finalize ? `final` : lastZkeyIndex}.zkey` + + const potTempFilePath = path.join(os.tmpdir(), potFilename) + const firstZkeyTempFilePath = path.join(os.tmpdir(), firstZkeyFilename) + const lastZkeyTempFilePath = path.join(os.tmpdir(), lastZkeyFilename) + + // Download from AWS S3 bucket. + await tempDownloadFromBucket(S3, bucketName, potStoragePath, potTempFilePath) + logMsg(`${potStoragePath} downloaded`, MsgType.DEBUG) + + await tempDownloadFromBucket(S3, bucketName, firstZkeyStoragePath, firstZkeyTempFilePath) + logMsg(`${firstZkeyStoragePath} downloaded`, MsgType.DEBUG) + + await tempDownloadFromBucket(S3, bucketName, lastZkeyStoragePath, lastZkeyTempFilePath) + logMsg(`${lastZkeyStoragePath} downloaded`, MsgType.DEBUG) + + logMsg(`Downloads from storage completed`, MsgType.INFO) + + // Verify contribution. + const verificationComputationTimer = new Timer({ label: "verificationComputation" }) + verificationComputationTimer.start() + + valid = await zKey.verifyFromInit( + firstZkeyTempFilePath, + potTempFilePath, + lastZkeyTempFilePath, + transcriptLogger + ) + + verificationComputationTimer.stop() + + verificationComputationTime = verificationComputationTimer.ms() + + // Compute blake2b hash before unlink. + const lastZkeyBuffer = fs.readFileSync(lastZkeyTempFilePath) + const lastZkeyBlake2bHash = blake.blake2bHex(lastZkeyBuffer) + + // Unlink folders. + fs.unlinkSync(potTempFilePath) + fs.unlinkSync(firstZkeyTempFilePath) + fs.unlinkSync(lastZkeyTempFilePath) + + logMsg(`Contribution is ${valid ? `valid` : `invalid`}`, MsgType.INFO) + logMsg(`Verification computation time ${verificationComputationTime} ms`, MsgType.INFO) + + // Update DB. + const batch = firestore.batch() + + // Contribution. + const contributionDoc = await firestore + .collection( + `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuitId}/${collections.contributions}` + ) + .doc() + .get() + + if (valid) { + // Sleep ~5 seconds to wait for verification transcription. + await sleep(5000) + + // Upload transcript (small file - multipart upload not required). + await uploadFileToBucket(S3, bucketName, transcriptStoragePath, transcriptTempFilePath) + + // Compute blake2b hash. + const transcriptBuffer = fs.readFileSync(transcriptTempFilePath) + const transcriptBlake2bHash = blake.blake2bHex(transcriptBuffer) + + fs.unlinkSync(transcriptTempFilePath) + + // Get contribution computation time. + const contributions = participantData?.contributions.filter( + (contribution: { hash: string; doc: string; computationTime: number }) => + !!contribution.hash && !!contribution.computationTime && !contribution.doc + ) + + if (contributions.length !== 1) + logMsg(`There should be only one contribution without a doc link`, MsgType.ERROR) + + const contributionComputationTime = contributions[0].computationTime + + // Update only when coordinator is finalizing the ceremony. + batch.create(contributionDoc.ref, { + participantId: participantDoc.id, + contributionComputationTime, + verificationComputationTime, + zkeyIndex: finalize ? `final` : lastZkeyIndex, + files: { + transcriptFilename, + lastZkeyFilename, + transcriptStoragePath, + lastZkeyStoragePath, + transcriptBlake2bHash, + lastZkeyBlake2bHash + }, + valid, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Batch: create contribution document`, MsgType.DEBUG) + + verifyCloudFunctionTimer.stop() + const verifyCloudFunctionTime = verifyCloudFunctionTimer.ms() + + if (!finalize) { + // Circuit. + const { completedContributions, failedContributions } = circuitData!.waitingQueue + const { + contributionComputation: avgContributionComputation, + fullContribution: avgFullContribution, + verifyCloudFunction: avgVerifyCloudFunction + } = circuitData!.avgTimings + + logMsg( + `Current average full contribution (down + comp + up) time ${avgFullContribution} ms`, + MsgType.INFO + ) + logMsg(`Current verify cloud function time ${avgVerifyCloudFunction} ms`, MsgType.INFO) + + // Calculate full contribution time. + fullContributionTime = + Number(participantData?.verificationStartedAt) - Number(participantData?.contributionStartedAt) + + // Update avg timings. + const newAvgContributionComputationTime = + avgContributionComputation > 0 + ? (avgContributionComputation + contributionComputationTime) / 2 + : contributionComputationTime + const newAvgFullContributionTime = + avgFullContribution > 0 + ? (avgFullContribution + fullContributionTime) / 2 + : fullContributionTime + const newAvgVerifyCloudFunctionTime = + avgVerifyCloudFunction > 0 + ? (avgVerifyCloudFunction + verifyCloudFunctionTime) / 2 + : verifyCloudFunctionTime + + logMsg( + `New average contribution computation time ${newAvgContributionComputationTime} ms`, + MsgType.INFO + ) + logMsg( + `New average full contribution (down + comp + up) time ${newAvgFullContributionTime} ms`, + MsgType.INFO + ) + logMsg(`New verify cloud function time ${newAvgVerifyCloudFunctionTime} ms`, MsgType.INFO) + + batch.update(circuitDoc.ref, { + avgTimings: { + contributionComputation: valid + ? newAvgContributionComputationTime + : contributionComputationTime, + fullContribution: valid ? newAvgFullContributionTime : fullContributionTime, + verifyCloudFunction: valid ? newAvgVerifyCloudFunctionTime : verifyCloudFunctionTime + }, + waitingQueue: { + ...circuitData?.waitingQueue, + completedContributions: valid ? completedContributions + 1 : completedContributions, + failedContributions: valid ? failedContributions : failedContributions + 1 + }, + lastUpdated: getCurrentServerTimestampInMillis() + }) + } + + logMsg(`Batch: update timings and waiting queue for circuit`, MsgType.DEBUG) + + await batch.commit() + } else { + // Delete invalid contribution from storage. + await deleteObject(S3, bucketName, lastZkeyStoragePath) + + // Unlink transcript temp file. + fs.unlinkSync(transcriptTempFilePath) + + // Create a new contribution doc without files. + batch.create(contributionDoc.ref, { + participantId: participantDoc.id, + verificationComputationTime, + zkeyIndex: finalize ? `final` : lastZkeyIndex, + valid, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Batch: create invalid contribution document`, MsgType.DEBUG) + + if (!finalize) { + const { failedContributions } = circuitData!.waitingQueue + + // Update the failed contributions. + batch.update(circuitDoc.ref, { + waitingQueue: { + ...circuitData?.waitingQueue, + failedContributions: failedContributions + 1 + }, + lastUpdated: getCurrentServerTimestampInMillis() + }) + } + logMsg(`Batch: update invalid contributions counter`, MsgType.DEBUG) + + await batch.commit() + } + } + + logMsg( + `Participant ${userId} has verified the contribution #${participantData?.contributionProgress}`, + MsgType.INFO + ) + logMsg( + `Returned values: valid ${valid} - verificationComputationTime ${verificationComputationTime}`, + MsgType.INFO + ) + + return { + valid, + fullContributionTime, + verifyCloudFunctionTime: verifyCloudFunctionTimer.ms() + } + } +) + +/** + * Update the participant document after a contribution. + */ +export const refreshParticipantAfterContributionVerification = functionsV1.firestore + .document( + `/${collections.ceremonies}/{ceremony}/${collections.circuits}/{circuit}/${collections.contributions}/{contributions}` + ) + .onCreate(async (doc: QueryDocumentSnapshot) => { + // Get DB. + const firestore = admin.firestore() + + // Get doc info. + const contributionId = doc.id + const contributionData = doc.data() + const ceremonyCircuitsCollectionPath = doc.ref.parent.parent?.parent?.path // == /ceremonies/{ceremony}/circuits/. + const ceremonyParticipantsCollectionPath = `${doc.ref.parent.parent?.parent?.parent?.path}/${collections.participants}` // == /ceremonies/{ceremony}/participants. + + if (!ceremonyCircuitsCollectionPath || !ceremonyParticipantsCollectionPath) + logMsg(GENERIC_ERRORS.GENERR_WRONG_PATHS, MsgType.ERROR) + + // Looks for documents. + const circuits = await firestore.collection(ceremonyCircuitsCollectionPath!).listDocuments() + const participantDoc = await firestore + .collection(ceremonyParticipantsCollectionPath) + .doc(contributionData.participantId) + .get() + + if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data. + const participantData = participantDoc.data() + + if (!participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + const participantContributions = participantData?.contributions + + // Update the only one contribution with missing doc (i.e., the last one). + participantContributions.forEach( + (participantContribution: { hash: string; doc: string; computationTime: number }) => { + if ( + !!participantContribution.hash && + !!participantContribution.computationTime && + !participantContribution.doc + ) { + participantContribution.doc = contributionId + } + } + ) + + // Don't update the participant status and progress when finalizing. + if (participantData!.status !== ParticipantStatus.FINALIZING) { + const newStatus = + participantData!.contributionProgress + 1 > circuits.length + ? ParticipantStatus.DONE + : ParticipantStatus.CONTRIBUTED + + await firestore.collection(ceremonyParticipantsCollectionPath).doc(contributionData.participantId).set( + { + status: newStatus, + contributionStep: ParticipantContributionStep.COMPLETED, + contributions: participantContributions, + tempContributionData: FieldValue.delete(), + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + + logMsg(`Participant ${contributionData.participantId} updated after contribution`, MsgType.DEBUG) + } else { + await firestore.collection(ceremonyParticipantsCollectionPath).doc(contributionData.participantId).set( + { + contributions: participantContributions, + lastUpdated: getCurrentServerTimestampInMillis() + }, + { merge: true } + ) + + logMsg(`Coordinator ${contributionData.participantId} updated after final contribution`, MsgType.DEBUG) + } + }) + +/** + * Make the progress to next contribution after successfully verified the contribution. + */ +export const makeProgressToNextContribution = functionsV1.https.onCall( + async (data: any, context: functionsV1.https.CallableContext): Promise<any> => { + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!ceremonyDoc.exists || !participantDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const participantData = participantDoc.data() + + if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + const { contributionProgress, contributionStep, status } = participantData! + + // Check for contribution completion here. + if (contributionStep !== ParticipantContributionStep.COMPLETED && status !== ParticipantStatus.WAITING) + logMsg(`Cannot progress!`, MsgType.ERROR) + + await participantDoc.ref.update({ + contributionProgress: contributionProgress + 1, + status: ParticipantStatus.READY, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Participant ${userId} progressed to ${contributionProgress + 1}`, MsgType.DEBUG) + } +) + +/** + * Resume a contribution after a timeout expiration. + */ +export const resumeContributionAfterTimeoutExpiration = functionsV1.https.onCall( + async (data: any, context: functionsV1.https.CallableContext): Promise<any> => { + if (!context.auth || (!context.auth.token.participant && !context.auth.token.coordinator)) + logMsg(GENERIC_ERRORS.GENERR_NO_AUTH_USER_FOUND, MsgType.ERROR) + + if (!data.ceremonyId) logMsg(GENERIC_ERRORS.GENERR_MISSING_INPUT, MsgType.ERROR) + + // Get DB. + const firestore = admin.firestore() + + // Get data. + const { ceremonyId } = data + const userId = context.auth?.uid + + // Look for documents. + const ceremonyDoc = await firestore.collection(collections.ceremonies).doc(ceremonyId).get() + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(userId!) + .get() + + if (!ceremonyDoc.exists || !participantDoc.exists) + logMsg(GENERIC_ERRORS.GENERR_INVALID_DOCUMENTS, MsgType.ERROR) + + // Get data from docs. + const ceremonyData = ceremonyDoc.data() + const participantData = participantDoc.data() + + if (!ceremonyData || !participantData) logMsg(GENERIC_ERRORS.GENERR_NO_DATA, MsgType.ERROR) + + logMsg(`Ceremony document ${ceremonyDoc.id} okay`, MsgType.DEBUG) + logMsg(`Participant document ${participantDoc.id} okay`, MsgType.DEBUG) + + const { contributionProgress, status } = participantData! + + // Check if can resume. + if (status !== ParticipantStatus.EXHUMED) + logMsg(`Cannot resume the contribution after a timeout expiration`, MsgType.ERROR) + + await participantDoc.ref.update({ + status: ParticipantStatus.READY, + lastUpdated: getCurrentServerTimestampInMillis() + }) + + logMsg(`Participant ${userId} has resumed the contribution for circuit ${contributionProgress}`, MsgType.DEBUG) + } +) diff --git a/packages/backend/src/lib/constants.ts b/packages/backend/src/lib/constants.ts new file mode 100644 index 00000000..bb4ee804 --- /dev/null +++ b/packages/backend/src/lib/constants.ts @@ -0,0 +1,47 @@ +/** Firebase */ +export const collections = { + users: "users", + participants: "participants", + ceremonies: "ceremonies", + circuits: "circuits", + contributions: "contributions", + timeouts: "timeouts" +} + +export const names = { + output: `output`, + setup: `setup`, + contribute: `contribute`, + pot: `pot`, + zkeys: `zkeys`, + metadata: `metadata`, + transcripts: `transcripts`, + attestation: `attestation` +} + +export const ceremoniesCollectionFields = { + coordinatorId: "coordinatorId", + description: "description", + endDate: "endDate", + lastUpdated: "lastUpdated", + prefix: "prefix", + startDate: "startDate", + state: "state", + title: "title", + type: "type" +} + +export const contributionsCollectionFields = { + contributionTime: "contributionTime", + files: "files", + lastUpdated: "lastUpdated", + participantId: "participantId", + valid: "valid", + verificationTime: "verificationTime", + zkeyIndex: "zKeyIndex" +} + +export const timeoutsCollectionFields = { + endDate: "endDate", + startDate: "startDate" +} diff --git a/packages/backend/src/lib/logs.ts b/packages/backend/src/lib/logs.ts new file mode 100644 index 00000000..969de607 --- /dev/null +++ b/packages/backend/src/lib/logs.ts @@ -0,0 +1,69 @@ +import * as functions from "firebase-functions" +import { MsgType } from "../../types/index" + +export const GENERIC_ERRORS = { + GENERR_MISSING_INPUT: `You have not provided all the necessary data`, + GENERR_NO_AUTH_USER_FOUND: `The given id does not belong to an authenticated user`, + GENERR_NO_COORDINATOR: `The given id does not belong to a coordinator`, + GENERR_NO_CEREMONY_PROVIDED: `No ceremony has been provided`, + GENERR_NO_CIRCUIT_PROVIDED: `No circuit has been provided`, + GENERR_NO_CEREMONIES_OPENED: `No ceremonies are opened to contributions`, + GENERR_INVALID_CEREMONY: `The given ceremony is invalid`, + GENERR_INVALID_CIRCUIT: `The given circuit is invalid`, + GENERR_INVALID_PARTICIPANT: `The given participant is invalid`, + GENERR_CEREMONY_NOT_OPENED: `The given ceremony is not opened to contributions`, + GENERR_CEREMONY_NOT_CLOSED: `The given ceremony is not closed for finalization`, + GENERR_INVALID_PARTICIPANT_STATUS: `The participant has an invalid status`, + GENERR_INVALID_PARTICIPANT_CONTRIBUTION_STEP: `The participant has an invalid contribution step`, + GENERR_INVALID_CONTRIBUTION_PROGRESS: `The contribution progress is invalid`, + GENERR_INVALID_DOCUMENTS: `One or more provided identifier does not belong to a document`, + GENERR_NO_DATA: `Data not found`, + GENERR_NO_CIRCUIT: `Circuits not found`, + GENERR_NO_PARTICIPANT: `Participant not found`, + GENERR_NO_CONTRIBUTION: `Contributions not found`, + GENERR_NO_CURRENT_CONTRIBUTOR: `There is no current contributor for the circuit`, + GENERR_NO_TIMEOUT_FIRST_COTRIBUTOR: `Cannot compute a dynamic timeout for the first contributor`, + GENERR_NO_CIRCUITS: `Circuits not found for the ceremony`, + GENERR_NO_CONTRIBUTIONS: `Contributions not found for the circuit`, + GENERR_NO_RETRY: `The retry waiting time has not passed away yet`, + GENERR_WRONG_PATHS: `Wrong storage or database paths`, + GENERR_WRONG_FIELD: `Wrong document field`, + GENERR_WRONG_ENV_CONFIGURATION: `Your environment variables are not configured properly` +} + +export const GENERIC_LOGS = { + GENLOG_NO_CEREMONIES_READY_TO_BE_OPENED: `There are no cerimonies ready to be opened to contributions`, + GENLOG_NO_CEREMONIES_READY_TO_BE_CLOSED: `There are no cerimonies ready to be closed`, + GENLOG_NO_CURRENT_CONTRIBUTOR: `There is no current contributor for the circuit`, + GENLOG_NO_TIMEOUT: `The timeout must not be triggered yet` +} + +/** + * Print a message customizing the default logger. + * @param msg <string> - the msg to be shown. + * @param msgType <MsgType> - the type of the message (e.g., debug, error). + */ +export const logMsg = (msg: string, msgType: MsgType) => { + switch (msgType) { + case MsgType.INFO: + functions.logger.info(`[INFO] ${msg}`) + break + case MsgType.DEBUG: + functions.logger.debug(`[DEBUG] ${msg}`) + break + case MsgType.WARN: + functions.logger.warn(`[WARN] ${msg}`) + break + case MsgType.ERROR: { + functions.logger.error(`[ERROR] ${msg}`) + process.exit(0) + } + // eslint-disable-next-line + case MsgType.LOG: + functions.logger.log(`[LOG] ${msg}`) + break + default: + console.log(`[LOG] ${msg}`) + break + } +} diff --git a/packages/backend/src/lib/utils.ts b/packages/backend/src/lib/utils.ts new file mode 100644 index 00000000..1bc63b0d --- /dev/null +++ b/packages/backend/src/lib/utils.ts @@ -0,0 +1,324 @@ +import { DocumentData, DocumentSnapshot, Timestamp, WhereFilterOp } from "firebase-admin/firestore" +import admin from "firebase-admin" +import * as functions from "firebase-functions" +import dotenv from "dotenv" +import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3" +import { getSignedUrl } from "@aws-sdk/s3-request-presigner" +import { createWriteStream } from "node:fs" +import { pipeline } from "node:stream" +import { promisify } from "node:util" +import { readFileSync } from "fs" +import fetch from "node-fetch" +import mime from "mime-types" +import { setTimeout } from "timers/promises" +import { GENERIC_ERRORS, logMsg } from "./logs" +import { ceremoniesCollectionFields, collections, timeoutsCollectionFields } from "./constants" +import { CeremonyState, MsgType } from "../../types/index" + +dotenv.config() + +/** + * Return the current server timestamp in milliseconds. + * @returns <number> + */ +export const getCurrentServerTimestampInMillis = (): number => Timestamp.now().toMillis() + +/** + * Query ceremonies by state and (start/end) date value. + * @param state <CeremonyState> - the value of the state to be queried. + * @param dateField <string> - the start or end date field. + * @param check <WhereFilerOp> - the query filter (where check). + * @returns <Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>>> + */ +export const queryCeremoniesByStateAndDate = async ( + state: CeremonyState, + dateField: string, + check: WhereFilterOp +): Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>> => { + // Get DB. + const firestoreDb = admin.firestore() + + if (dateField !== ceremoniesCollectionFields.startDate && dateField !== ceremoniesCollectionFields.endDate) + logMsg(GENERIC_ERRORS.GENERR_WRONG_FIELD, MsgType.ERROR) + + return firestoreDb + .collection(collections.ceremonies) + .where(ceremoniesCollectionFields.state, "==", state) + .where(dateField, check, getCurrentServerTimestampInMillis()) + .get() +} + +/** + * Query timeouts by (start/end) date value. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param participantId <string> - the unique identifier of the participant. + * @param dateField <string> - the name of the date field. + * @returns <Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>>> + */ +export const queryValidTimeoutsByDate = async ( + ceremonyId: string, + participantId: string, + dateField: string +): Promise<admin.firestore.QuerySnapshot<admin.firestore.DocumentData>> => { + // Get DB. + const firestoreDb = admin.firestore() + + if (dateField !== timeoutsCollectionFields.startDate && dateField !== timeoutsCollectionFields.endDate) + logMsg(GENERIC_ERRORS.GENERR_WRONG_FIELD, MsgType.ERROR) + + return firestoreDb + .collection( + `${collections.ceremonies}/${ceremonyId}/${collections.participants}/${participantId}/${collections.timeouts}` + ) + .where(dateField, ">=", getCurrentServerTimestampInMillis()) + .get() +} + +/** + * Return the document belonging to a participant with a specified id (if exist). + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param participantId <string> - the unique identifier of the participant. + * @returns <Promise<DocumentSnapshot<DocumentData>>> + */ +export const getParticipantById = async ( + ceremonyId: string, + participantId: string +): Promise<DocumentSnapshot<DocumentData>> => { + // Get DB. + const firestore = admin.firestore() + + const participantDoc = await firestore + .collection(`${collections.ceremonies}/${ceremonyId}/${collections.participants}`) + .doc(participantId) + .get() + + if (!participantDoc.exists) logMsg(GENERIC_ERRORS.GENERR_NO_PARTICIPANT, MsgType.ERROR) + + return participantDoc +} + +/** + * Return all circuits for a given ceremony (if any). + * @param circuitsPath <string> - the collection path from ceremonies to circuits. + * @returns Promise<Array<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>>> + */ +export const getCeremonyCircuits = async ( + circuitsPath: string +): Promise<Array<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>>> => { + // Get DB. + const firestore = admin.firestore() + + // Query for all docs. + const circuitsQuerySnap = await firestore.collection(circuitsPath).get() + const circuitDocs = circuitsQuerySnap.docs + + if (!circuitDocs) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUITS, MsgType.ERROR) + + return circuitDocs +} + +/** + * Format the next zkey index. + * @param progress <number> - the progression in zkey index (= contributions). + * @returns <string> + */ +export const formatZkeyIndex = (progress: number): string => { + if (!process.env.FIRST_ZKEY_INDEX) logMsg(GENERIC_ERRORS.GENERR_WRONG_ENV_CONFIGURATION, MsgType.ERROR) + + const initialZkeyIndex = process.env.FIRST_ZKEY_INDEX! + + let index = progress.toString() + + while (index.length < initialZkeyIndex.length) { + index = `0${index}` + } + + return index +} + +/** + * Get the document for the circuit of the ceremony with a given sequence position. + * @param circuitsPath <string> - the collection path from ceremonies to circuits. + * @param position <number> - the sequence position of the circuit. + * @returns Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> + */ +export const getCircuitDocumentByPosition = async ( + circuitsPath: string, + position: number +): Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> => { + // Query for all circuit docs. + const circuitDocs = await getCeremonyCircuits(circuitsPath) + + // Filter by position. + const filteredCircuits = circuitDocs.filter( + (circuit: admin.firestore.DocumentData) => circuit.data().sequencePosition === position + ) + + if (!filteredCircuits) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUIT, MsgType.ERROR) + + // Get the circuit (nb. there will be only one circuit w/ that position). + const circuit = filteredCircuits.at(0) + + if (!circuit) logMsg(GENERIC_ERRORS.GENERR_NO_CIRCUIT, MsgType.ERROR) + + functions.logger.info(`Circuit w/ UID ${circuit?.id} at position ${position}`) + + return circuit! +} + +/** + * Get the final contribution document for a specific circuit. + * @param contributionsPath <string> - the collection path from circuit to contributions. + * @returns Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> + */ +export const getFinalContributionDocument = async ( + contributionsPath: string +): Promise<admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>> => { + // Get DB. + const firestore = admin.firestore() + + // Query for all contribution docs for circuit. + const contributionsQuerySnap = await firestore.collection(contributionsPath).get() + const contributionsDocs = contributionsQuerySnap.docs + + if (!contributionsDocs) logMsg(GENERIC_ERRORS.GENERR_NO_CONTRIBUTIONS, MsgType.ERROR) + + // Filter by index. + const filteredContributions = contributionsDocs.filter( + (contribution: admin.firestore.DocumentData) => contribution.data().zkeyIndex === "final" + ) + + if (!filteredContributions) logMsg(GENERIC_ERRORS.GENERR_NO_CONTRIBUTION, MsgType.ERROR) + + // Get the contribution (nb. there will be only one final contribution). + const finalContribution = filteredContributions.at(0) + + if (!finalContribution) logMsg(GENERIC_ERRORS.GENERR_NO_CONTRIBUTION, MsgType.ERROR) + + return finalContribution! +} + +/** + * Return a new instance of the AWS S3 Client. + * @returns <Promise<S3Client> + */ +export const getS3Client = async (): Promise<S3Client> => { + if ( + !process.env.AWS_ACCESS_KEY_ID || + !process.env.AWS_SECRET_ACCESS_KEY || + !process.env.AWS_REGION || + !process.env.AWS_PRESIGNED_URL_EXPIRATION + ) + logMsg(GENERIC_ERRORS.GENERR_WRONG_ENV_CONFIGURATION, MsgType.ERROR) + + // Connect w/ S3. + return new S3Client({ + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY! + }, + region: process.env.AWS_REGION! + }) +} + +/** + * Downloads and temporarily write a file from S3 bucket. + * @param client <S3Client> - the AWS S3 client. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the location of the object in the AWS S3 bucket. + * @param tempFilePath <string> - the local path where the file will be written. + */ +export const tempDownloadFromBucket = async ( + client: S3Client, + bucketName: string, + objectKey: string, + tempFilePath: string +) => { + // Prepare get object command. + const command = new GetObjectCommand({ Bucket: bucketName, Key: objectKey }) + + // Get pre-signed url. + const url = await getSignedUrl(client, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) }) + + // Download the file. + const response: any = await fetch(url, { + method: "GET", + headers: { + "Access-Control-Allow-Origin": "*" + } + }) + + if (!response.ok) + logMsg(`Something went wrong when downloading the file from the bucket: ${response.statusText}`, MsgType.ERROR) + + // Temporarily write the file. + const streamPipeline = promisify(pipeline) + await streamPipeline(response.body!, createWriteStream(tempFilePath)) +} + +/** + * Sleeps the function execution for given millis. + * @dev to be used in combination with loggers when writing data into files. + * @param ms <number> - sleep amount in milliseconds + * @returns <Promise<void>> + */ +export const sleep = async (ms: number): Promise<void> => setTimeout(ms) + +/** + * Upload a file from S3 bucket. + * @param client <S3Client> - the AWS S3 client. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the location of the object in the AWS S3 bucket. + * @param tempFilePath <string> - the local path where the file will be written. + */ +export const uploadFileToBucket = async ( + client: S3Client, + bucketName: string, + objectKey: string, + tempFilePath: string +) => { + // Get file content type. + const contentType = mime.lookup(tempFilePath) || "" + + // Prepare command. + const command = new PutObjectCommand({ Bucket: bucketName, Key: objectKey, ContentType: contentType }) + + // Get pre-signed url. + const url = await getSignedUrl(client, command, { expiresIn: Number(process.env.AWS_PRESIGNED_URL_EXPIRATION!) }) + + // Make upload request (PUT). + const uploadTranscriptResponse = await fetch(url, { + method: "PUT", + body: readFileSync(tempFilePath), + headers: { "Content-Type": contentType } + }) + + // Check response. + if (!uploadTranscriptResponse.ok) + logMsg( + `Something went wrong when uploading the transcript: ${uploadTranscriptResponse.statusText}`, + MsgType.ERROR + ) + + logMsg(`File uploaded successfully`, MsgType.DEBUG) +} + +/** + * Delete a file from S3 bucket. + * @param client <S3Client> - the AWS S3 client. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the location of the object in the AWS S3 bucket. + */ +export const deleteObject = async (client: S3Client, bucketName: string, objectKey: string) => { + try { + // Prepare command. + const command = new DeleteObjectCommand({ Bucket: bucketName, Key: objectKey }) + + // Send command. + const data = await client.send(command) + + logMsg(`Object ${objectKey} successfully deleted: ${data.$metadata.httpStatusCode}`, MsgType.INFO) + } catch (error: any) { + logMsg(`Something went wrong while deleting the ${objectKey} object: ${error}`, MsgType.ERROR) + } +} diff --git a/apps/backend/storage.rules b/packages/backend/storage.rules similarity index 100% rename from apps/backend/storage.rules rename to packages/backend/storage.rules diff --git a/packages/backend/test/index.test.ts b/packages/backend/test/index.test.ts new file mode 100644 index 00000000..158c750a --- /dev/null +++ b/packages/backend/test/index.test.ts @@ -0,0 +1,55 @@ +import chai from "chai" +import chaiAsPromised from "chai-as-promised" +import admin from "firebase-admin" +import firebaseFncTest from "firebase-functions-test" +// Import the exported function definitions from our functions/index.js file +import { registerAuthUser } from "../src/functions/index" + +// Config chai. +chai.use(chaiAsPromised) +const { assert } = chai + +// Initialize the firebase-functions-test SDK using environment variables. +// These variables are automatically set by firebase emulators:exec +// +// This configuration will be used to initialize the Firebase Admin SDK, so +// when we use the Admin SDK in the tests below we can be confident it will +// communicate with the emulators, not production. +const test = firebaseFncTest({ + databaseURL: process.env.FIREBASE_FIRESTORE_DATABASE_URL, + storageBucket: process.env.FIREBASE_STORAGE_BUCKET +}) + +describe("CF Unit Tests", () => { + afterAll(() => { + test.cleanup() + }) + + it("should call an authorized CF and interact with Firestore", async () => { + const wrapped = test.wrap(registerAuthUser) + + // Make a fake user to pass to the function + const uid = `${new Date().getTime()}` + const displayName = "UserA" + const email = `user-${uid}@example.com` + const photoURL = `https://www...."` + + const user = test.auth.makeUserRecord({ + uid, + displayName, + email, + photoURL + }) + + // Call the function + await wrapped(user) + + // Check the data was written to the Firestore emulator + const snap = await admin.firestore().collection("users").doc(uid).get() + const data = snap.data() + + assert.propertyVal(data, "name", displayName) + assert.propertyVal(data, "email", email) + assert.propertyVal(data, "photoURL", photoURL) + }) +}) diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json new file mode 100644 index 00000000..ea775ea0 --- /dev/null +++ b/packages/backend/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist/", + "declarationDir": "dist/types" + }, + "include": ["src/**/*", "test/**/*", "types/**/*", "rollup.config.ts"] +} diff --git a/packages/backend/types/index.ts b/packages/backend/types/index.ts new file mode 100644 index 00000000..85bf3662 --- /dev/null +++ b/packages/backend/types/index.ts @@ -0,0 +1,56 @@ +export enum CeremonyState { + SCHEDULED = 1, + OPENED = 2, + PAUSED = 3, + CLOSED = 4, + FINALIZED = 5 +} + +export enum ParticipantStatus { + CREATED = 1, + WAITING = 2, + READY = 3, + CONTRIBUTING = 4, + CONTRIBUTED = 5, + DONE = 6, + FINALIZING = 7, + FINALIZED = 8, + TIMEDOUT = 9, + EXHUMED = 10 +} + +export enum ParticipantContributionStep { + DOWNLOADING = 1, + COMPUTING = 2, + UPLOADING = 3, + VERIFYING = 4, + COMPLETED = 5 +} + +export enum CeremonyType { + PHASE1 = 1, + PHASE2 = 2 +} + +export enum MsgType { + INFO = 1, + DEBUG = 2, + WARN = 3, + ERROR = 4, + LOG = 5 +} + +export enum RequestType { + PUT = 1, + GET = 2 +} + +export enum TimeoutType { + BLOCKING_CONTRIBUTION = 1, + BLOCKING_CLOUD_FUNCTION = 2 +} + +export enum CeremonyTimeoutType { + DYNAMIC = 1, + FIXED = 2 +} diff --git a/packages/backend/types/snarkjs.d.ts b/packages/backend/types/snarkjs.d.ts new file mode 100644 index 00000000..1ac7c1ca --- /dev/null +++ b/packages/backend/types/snarkjs.d.ts @@ -0,0 +1,58 @@ +/** Declaration file generated by dts-gen */ + +declare module "snarkjs" { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + export = snarkjs + + declare const snarkjs: { + groth16: { + exportSolidityCallData: any + fullProve: any + prove: any + verify: any + } + plonk: { + exportSolidityCallData: any + fullProve: any + prove: any + setup: any + verify: any + } + powersOfTau: { + beacon: any + challengeContribute: any + contribute: any + convert: any + exportChallenge: any + exportJson: any + importResponse: any + newAccumulator: any + preparePhase2: any + truncate: any + verify: any + } + r1cs: { + exportJson: any + info: any + print: any + } + wtns: { + calculate: any + debug: any + exportJson: any + } + zKey: { + beacon: any + bellmanContribute: any + contribute: any + exportBellman: any + exportJson: any + exportSolidityVerifier: any + exportVerificationKey: any + importBellman: any + newZKey: any + verifyFromInit: any + verifyFromR1cs: any + } + } +} diff --git a/packages/phase2cli/.env.default b/packages/phase2cli/.env.default new file mode 100644 index 00000000..f0b916e9 --- /dev/null +++ b/packages/phase2cli/.env.default @@ -0,0 +1,15 @@ +# Firebase. +FIREBASE_FIRESTORE_DATABASE_URL="YOUR-FIREBASE-FIRESTORE-DATABASE-URL-HERE" +FIREBASE_API_KEY="YOUR-FIREBASE-API-KEY" +FIREBASE_AUTH_DOMAIN="YOUR-FIREBASE-AUTH-DOMAIN" +FIREBASE_PROJECT_ID="YOUR-FIREBASE-PROJECT-ID" +FIREBASE_MESSAGING_SENDER_ID="YOUR-FIREBASE-MESSAGING-SENDER-ID" +FIREBASE_APP_ID="YOUR-FIREBASE-APP-ID" +FIREBASE_CF_URL_VERIFY_CONTRIBUTION="YOUR-FIREBASE-CF-URL-VERIFY-CONTRIBUTION" +# Github. +GITHUB_CLIENT_ID="YOUR-GITHUB-CLIENT-ID" +# Misc. +CONFIG_NODE_OPTION_MAX_OLD_SPACE_SIZE="YOUR-CONFIG-NODE-OPTION-MAX-OLD-SPACE-SIZE" +CONFIG_STREAM_CHUNK_SIZE_IN_MB="YOUR-CONFIG-STREAM-CHUNK-SIZE-IN-MB" +CONFIG_CEREMONY_BUCKET_POSTFIX="YOUR-CONFIG-CEREMONY-BUCKET-POSTFIX" +CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS=YOUR-CONFIG-PRESIGNED-URL-EXPIRATION-IN-SECONDS \ No newline at end of file diff --git a/apps/phase2cli/.gitignore b/packages/phase2cli/.gitignore similarity index 96% rename from apps/phase2cli/.gitignore rename to packages/phase2cli/.gitignore index ed2dad8d..a4a65418 100644 --- a/apps/phase2cli/.gitignore +++ b/packages/phase2cli/.gitignore @@ -6,6 +6,7 @@ yarn-error.log # environment env.json +.env # build dist/ diff --git a/apps/phase2cli/README.md b/packages/phase2cli/README.md similarity index 66% rename from apps/phase2cli/README.md rename to packages/phase2cli/README.md index 2d42b144..20130280 100644 --- a/apps/phase2cli/README.md +++ b/packages/phase2cli/README.md @@ -38,11 +38,11 @@ ## Commands -- `phase2cli`: CLI entry point. -- `phase2cli auth`: Starts the Device Flow authentication workflow for Github OAuth 2.0. -- `phase2cli contribute`: Allow a user to participate by computing a contribution for each circuit of a selected ceremony (from those currently running). -- `phase2cli coordinate setup`: Allow the coordinator to setup a new ceremony for a particular set/variants of circuits. -- `phase2cli coordinate observe`: Allow the coordinator to monitor in real-time who is currently contributing for a circuit of a ceremony. +- `phase2cli`: CLI entry point. +- `phase2cli auth`: Starts the Device Flow authentication workflow for Github OAuth 2.0. +- `phase2cli contribute`: Allow a user to participate by computing a contribution for each circuit of a selected ceremony (from those currently running). +- `phase2cli coordinate setup`: Allow the coordinator to setup a new ceremony for a particular set/variants of circuits. +- `phase2cli coordinate observe`: Allow the coordinator to monitor in real-time who is currently contributing for a circuit of a ceremony. ## Getting Started @@ -69,23 +69,23 @@ Navigate to the `phase2cli/` folder and make a copy of the .env.json.default fil ```json { - "firebase": { - "FIREBASE_FIRESTORE_DATABASE_URL": "your-firebase-firestore-database-url", - "FIREBASE_API_KEY": "your-firebase-api-key", - "FIREBASE_AUTH_DOMAIN": "your-firebase-auth-domain", - "FIREBASE_PROJECT_ID": "your-firebase-project-id", - "FIREBASE_STORAGE_BUCKET": "your-firebase-storage-bucket", - "FIREBASE_MESSAGING_SENDER_ID": "your-firebase-messaging-sender-id", - "FIREBASE_APP_ID": "your-firebase-app-id" - }, - "github": { - "GITHUB_CLIENT_ID": "your-github-oauth-app-client-id" - } + "firebase": { + "FIREBASE_FIRESTORE_DATABASE_URL": "your-firebase-firestore-database-url", + "FIREBASE_API_KEY": "your-firebase-api-key", + "FIREBASE_AUTH_DOMAIN": "your-firebase-auth-domain", + "FIREBASE_PROJECT_ID": "your-firebase-project-id", + "FIREBASE_STORAGE_BUCKET": "your-firebase-storage-bucket", + "FIREBASE_MESSAGING_SENDER_ID": "your-firebase-messaging-sender-id", + "FIREBASE_APP_ID": "your-firebase-app-id" + }, + "github": { + "GITHUB_CLIENT_ID": "your-github-oauth-app-client-id" + } } ``` -- The `firebase` object contains your Firebase Application configuration. -- The `github` object contains your Github OAuth Application client identifier. +- The `firebase` object contains your Firebase Application configuration. +- The `github` object contains your Github OAuth Application client identifier. ### Usage diff --git a/packages/phase2cli/build.tsconfig.json b/packages/phase2cli/build.tsconfig.json new file mode 100644 index 00000000..277a5e72 --- /dev/null +++ b/packages/phase2cli/build.tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist/", + "moduleResolution": "node", + "declarationDir": "dist/types" + }, + "include": ["src/**/*", "test/**/*", "types/**/*"] +} diff --git a/packages/phase2cli/package.json b/packages/phase2cli/package.json new file mode 100644 index 00000000..4db13fe1 --- /dev/null +++ b/packages/phase2cli/package.json @@ -0,0 +1,103 @@ +{ + "name": "@zkmpc/phase2cli", + "version": "0.0.1", + "description": "All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies", + "repository": "https://github.com/quadratic-funding/mpc-phase2-suite/cli", + "homepage": "https://github.com/quadratic-funding/mpc-phase2-suite", + "bugs": "https://github.com/quadratic-funding/mpc-phase2-suite/issues", + "author": { + "name": "Giacomo (0xjei)" + }, + "license": "MIT", + "private": false, + "exports": { + "import": "./dist/src/index.node.mjs", + "require": "./dist/src/index.node.js" + }, + "types": "dist/types/src/index.d.ts", + "engines": { + "node": "16" + }, + "files": [ + "dist/", + "src/", + "types/", + "README.md" + ], + "keywords": [ + "typescript", + "zero-knowledge", + "zk-snarks", + "phase-2", + "trusted-setup", + "ceremony", + "snarkjs", + "circom" + ], + "bin": { + "phase2cli": "./dist/src/index.node.mjs" + }, + "scripts": { + "build": "rimraf dist && rollup -c rollup.config.ts --configPlugin typescript", + "build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript", + "pre:publish": "yarn build", + "start": "node ./dist/src/index.node.mjs", + "auth": "yarn start auth", + "contribute": "yarn start contribute", + "clean": "yarn start clean", + "logout": "yarn start logout", + "coordinate:setup": "yarn start coordinate setup", + "coordinate:observe": "yarn start coordinate observe", + "coordinate:finalize": "yarn start coordinate finalize" + }, + "peerDependencies": { + "@zkmpc/actions": "^0.0.1" + }, + "devDependencies": { + "@types/clear": "^0.1.2", + "@types/cli-progress": "^3.11.0", + "@types/conf": "^3.0.0", + "@types/figlet": "^1.5.5", + "@types/mime-types": "^2.1.1", + "@types/node-emoji": "^1.8.2", + "@types/node-fetch": "^2.6.2", + "@types/ora": "^3.2.0", + "@types/prompts": "^2.4.1", + "@types/rollup-plugin-auto-external": "^2.0.2", + "@types/winston": "^2.4.4", + "rollup-plugin-auto-external": "^2.0.0", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.34.1", + "typescript": "^4.9.3" + }, + "dependencies": { + "@adobe/node-fetch-retry": "^2.2.0", + "@octokit/auth-oauth-app": "^5.0.4", + "@octokit/auth-oauth-device": "^4.0.3", + "@octokit/request": "^6.2.2", + "blakejs": "^1.2.1", + "boxen": "^7.0.0", + "chalk": "^5.1.2", + "clear": "^0.1.0", + "cli-progress": "^3.11.2", + "commander": "^9.4.1", + "conf": "^10.2.0", + "dotenv": "^16.0.3", + "figlet": "^1.5.2", + "firebase": "^9.14.0", + "log-symbols": "^5.1.0", + "mime-types": "^2.1.35", + "node-disk-info": "^1.3.0", + "node-emoji": "^1.11.0", + "node-fetch": "^3.3.0", + "open": "^8.4.0", + "ora": "^6.1.2", + "prompts": "^2.4.2", + "snarkjs": "^0.5.0", + "timer-node": "^5.0.6", + "winston": "^3.8.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/phase2cli/rollup.config.ts b/packages/phase2cli/rollup.config.ts new file mode 100644 index 00000000..18c4ce0c --- /dev/null +++ b/packages/phase2cli/rollup.config.ts @@ -0,0 +1,30 @@ +import * as fs from "fs" +import typescript from "rollup-plugin-typescript2" +import autoExternal from "rollup-plugin-auto-external" +import cleanup from "rollup-plugin-cleanup" + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf-8")) +const banner = `/** + * @module ${pkg.name} + * @version ${pkg.version} + * @file ${pkg.description} + * @copyright Ethereum Foundation 2022 + * @license ${pkg.license} + * @see [Github]{@link ${pkg.homepage}} +*/` + +export default { + input: "src/index.ts", + output: [ + { file: pkg.exports.require, format: "cjs", banner, exports: "auto" }, + { file: pkg.exports.import, format: "es", banner } + ], + plugins: [ + autoExternal(), + typescript({ + tsconfig: "./build.tsconfig.json", + useTsconfigDeclarationDir: true + }), + cleanup({ comments: "jsdoc" }) + ] +} diff --git a/packages/phase2cli/src/commands/auth.ts b/packages/phase2cli/src/commands/auth.ts new file mode 100644 index 00000000..f32e9aa5 --- /dev/null +++ b/packages/phase2cli/src/commands/auth.ts @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +import { getNewOAuthTokenUsingGithubDeviceFlow, signInToFirebaseWithGithubToken } from "@zkmpc/actions" +import dotenv from "dotenv" +import { emojis, symbols, theme } from "../lib/constants" +import { FIREBASE_ERRORS, GITHUB_ERRORS, showError } from "../lib/errors" +import { bootstrapCommandExec, getGithubUsername, terminate } from "../lib/utils" +import { getStoredOAuthToken, hasStoredOAuthToken, setStoredOAuthToken } from "../lib/auth" + +dotenv.config() + +/** + * Look for the Github 2.0 OAuth token in the local storage if present; otherwise manage the request for a new token. + * @returns <Promise<string>> + */ +const handleGithubToken = async (): Promise<string> => { + let token: string + + if (hasStoredOAuthToken()) + // Get stored token. + token = String(getStoredOAuthToken()) + else { + if (!process.env.GITHUB_CLIENT_ID) showError(GITHUB_ERRORS.GITHUB_NOT_CONFIGURED_PROPERLY, true) + + // Request a new token. + token = await getNewOAuthTokenUsingGithubDeviceFlow(process.env.GITHUB_CLIENT_ID) + + // Store the new token. + setStoredOAuthToken(token) + } + + return token +} + +/** + * Auth command. + * @dev TODO: add docs. + */ +const auth = async () => { + console.log(process.env.GITHUB_CLIENT_ID) + + try { + const { firebaseApp } = await bootstrapCommandExec() + + if (!process.env.GITHUB_CLIENT_ID) showError(GITHUB_ERRORS.GITHUB_NOT_CONFIGURED_PROPERLY, true) + + // Manage OAuth Github token. + const token = await handleGithubToken() + + // Sign in with credentials. + await signInToFirebaseWithGithubToken(firebaseApp, token) + + // Get Github username. + const ghUsername = await getGithubUsername(token) + + console.log(`${symbols.success} You are authenticated as ${theme.bold(`@${ghUsername}`)}`) + console.log( + `${ + symbols.info + } You can now contribute to zk-SNARK Phase2 Trusted Setup running ceremonies by running ${theme.bold( + theme.italic(`phase2cli contribute`) + )} command` + ) + + terminate(ghUsername) + } catch (err: any) { + const error = err.toString() + + /** Firebase */ + + if (error.includes("Firebase: Unsuccessful check authorization response from Github")) { + showError(FIREBASE_ERRORS.FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS, false) + + // Clean expired token from local storage. + // deleteStoredOAuthToken() + + console.log(`${symbols.success} Removed expired token from your local storage ${emojis.broom}`) + console.log( + `${symbols.info} Please, run \`phase2cli auth\` again to generate a new token and associate your Github account` + ) + + process.exit(0) + } + + if (error.includes("Firebase: Firebase App named '[DEFAULT]' already exists with different options or config")) + showError(FIREBASE_ERRORS.FIREBASE_DEFAULT_APP_DOUBLE_CONFIG, true) + + if (error.includes("Firebase: Error (auth/user-disabled)")) + showError(FIREBASE_ERRORS.FIREBASE_USER_DISABLED, true) + + if (error.includes("Firebase: Error (auth/network-request-failed)")) + showError(FIREBASE_ERRORS.FIREBASE_NETWORK_ERROR, true) + + if (error.includes("Firebase: Remote site 5XX from github.com for VERIFY_CREDENTIAL (auth/invalid-credential)")) + showError(FIREBASE_ERRORS.FIREBASE_FAILED_CREDENTIALS_VERIFICATION, true) + + /** Github */ + + if (error.includes("HttpError: The authorization request was denied")) + showError(GITHUB_ERRORS.GITHUB_ACCOUNT_ASSOCIATION_REJECTED, true) + + if ( + error.includes( + "HttpError: request to https://github.com/login/device/code failed, reason: connect ETIMEDOUT" + ) + ) + showError(GITHUB_ERRORS.GITHUB_SERVER_TIMEDOUT, true) + + /** Generic */ + + showError(`Something went wrong: ${error}`, true) + } +} + +export default auth diff --git a/packages/phase2cli/src/commands/clean.ts b/packages/phase2cli/src/commands/clean.ts new file mode 100644 index 00000000..d92cd6da --- /dev/null +++ b/packages/phase2cli/src/commands/clean.ts @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +import { emojis, paths, symbols, theme } from "../lib/constants" +import { showError } from "../lib/errors" +import { deleteDir, directoryExists } from "../lib/files" +import { askForConfirmation } from "../lib/prompts" +import { bootstrapCommandExec, customSpinner, sleep } from "../lib/utils" + +/** + * Clean command. + */ +const clean = async () => { + try { + // Initialize services. + await bootstrapCommandExec() + + const spinner = customSpinner(`Cleaning up...`, "clock") + + if (directoryExists(paths.outputPath)) { + console.log(theme.bold(`${symbols.warning} Be careful, this action is irreversible!`)) + + const { confirmation } = await askForConfirmation( + "Are you sure you want to continue with the clean up?", + "Yes", + "No" + ) + + if (confirmation) { + spinner.start() + + // Do the clean up. + deleteDir(paths.outputPath) + + // nb. simulate waiting time for 1s. + await sleep(1000) + + spinner.succeed(`Cleanup was successfully completed ${emojis.broom}`) + } + } else { + console.log(`${symbols.info} There is nothing to clean ${emojis.eyes}`) + } + } catch (err: any) { + showError(`Something went wrong: ${err.toString()}`, true) + } +} + +export default clean diff --git a/packages/phase2cli/src/commands/contribute.ts b/packages/phase2cli/src/commands/contribute.ts new file mode 100644 index 00000000..87ff57b3 --- /dev/null +++ b/packages/phase2cli/src/commands/contribute.ts @@ -0,0 +1,165 @@ +#!/usr/bin/env node + +import { getOpenedCeremonies, getCeremonyCircuits } from "@zkmpc/actions" +import { httpsCallable } from "firebase/functions" +import { handleCurrentAuthUserSignIn } from "../lib/auth" +import { theme, emojis, collections, symbols, paths } from "../lib/constants" +import { askForCeremonySelection, getEntropyOrBeacon } from "../lib/prompts" +import { ParticipantContributionStep, ParticipantStatus } from "../../types/index" +import { + bootstrapCommandExec, + terminate, + handleTimedoutMessageForContributor, + getContributorContributionsVerificationResults, + customSpinner, + simpleLoader +} from "../lib/utils" +import { getDocumentById } from "../lib/firebase" +import listenForContribution from "../lib/listeners" +import { FIREBASE_ERRORS, GENERIC_ERRORS, showError } from "../lib/errors" +import { checkAndMakeNewDirectoryIfNonexistent } from "../lib/files" + +/** + * Contribute command. + */ +const contribute = async () => { + try { + // Initialize services. + const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExec() + const checkParticipantForCeremony = httpsCallable(firebaseFunctions, "checkParticipantForCeremony") + + // Handle current authenticated user sign in. + const { user, token, username } = await handleCurrentAuthUserSignIn(firebaseApp) + + // Get running cerimonies info (if any). + const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase) + + if (runningCeremoniesDocs.length === 0) showError(FIREBASE_ERRORS.FIREBASE_CEREMONY_NOT_OPENED, true) + + console.log( + `${symbols.warning} ${theme.bold( + `The contribution process is based on a waiting queue mechanism (one contributor at a time) with an upper-bound time constraint per each contribution (does not restart if the process is halted for any reason).\n${symbols.info} Any contribution could take the bulk of your computational resources and memory based on the size of the circuit` + )} ${emojis.fire}\n` + ) + + // Ask to select a ceremony. + const ceremony = await askForCeremonySelection(runningCeremoniesDocs) + + // Get ceremony circuits. + const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) + const numberOfCircuits = circuits.length + + const spinner = customSpinner(`Checking eligibility...`, `clock`) + spinner.start() + + // Call Cloud Function for participant check and registration. + const { data: canParticipate } = await checkParticipantForCeremony({ ceremonyId: ceremony.id }) + + // Get participant document. + // To be moved (maybe helpers folder? w/ query?) + const participantDoc = await getDocumentById( + `${collections.ceremonies}/${ceremony.id}/${collections.participants}`, + user.uid + ) + + // Get updated data from snap. + const participantData = participantDoc.data() + + if (!participantData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Check if the user can take part of the waiting queue for contributing. + if (canParticipate) { + spinner.succeed(`You are eligible to contribute to the ceremony ${emojis.tada}\n`) + + // Check for output directory. + checkAndMakeNewDirectoryIfNonexistent(paths.outputPath) + checkAndMakeNewDirectoryIfNonexistent(paths.contributePath) + checkAndMakeNewDirectoryIfNonexistent(paths.contributionsPath) + checkAndMakeNewDirectoryIfNonexistent(paths.attestationPath) + checkAndMakeNewDirectoryIfNonexistent(paths.contributionTranscriptsPath) + + // Check if entropy is needed. + let entropy = "" + + if ( + (participantData?.contributionProgress === numberOfCircuits && + participantData?.contributionStep < ParticipantContributionStep.UPLOADING) || + participantData?.contributionProgress < numberOfCircuits + ) + entropy = await getEntropyOrBeacon(true) + + // Listen to circuits and participant document changes. + await listenForContribution( + participantDoc, + ceremony, + firestoreDatabase, + circuits, + firebaseFunctions, + token, + username, + entropy + ) + } else { + spinner.warn(`You are not eligible to contribute to the ceremony right now`) + + await handleTimedoutMessageForContributor(participantData!, participantDoc.id, ceremony.id, false, username) + } + + // Check if already contributed. + if ( + ((!canParticipate && participantData?.status === ParticipantStatus.DONE) || + participantData?.status === ParticipantStatus.FINALIZED) && + participantData?.contributions.length > 0 + ) { + spinner.fail(`You are not eligible to contribute to the ceremony\n`) + + await simpleLoader(`Checking for contributions...`, `clock`, 1500) + + // Return true and false based on contribution verification. + const contributionsValidity = await getContributorContributionsVerificationResults( + ceremony.id, + participantDoc.id, + circuits, + false + ) + const numberOfValidContributions = contributionsValidity.filter(Boolean).length + + if (numberOfValidContributions) { + console.log( + `Congrats, you have already contributed to ${theme.magenta( + theme.bold(numberOfValidContributions) + )} out of ${theme.magenta(theme.bold(numberOfCircuits))} circuits ${emojis.tada}` + ) + + // Show valid/invalid contributions per each circuit. + let idx = 0 + for (const contributionValidity of contributionsValidity) { + console.log( + `${contributionValidity ? symbols.success : symbols.error} ${theme.bold( + `Circuit` + )} ${theme.bold(theme.magenta(idx + 1))}` + ) + idx += 1 + } + + console.log( + `\nWe wanna thank you for your participation in preserving the security for ${theme.bold( + ceremony.data.title + )} Trusted Setup ceremony ${emojis.pray}` + ) + } else + console.log( + `\nYou have not successfully contributed to any of the ${theme.bold( + theme.magenta(circuits.length) + )} circuits ${emojis.upsideDown}` + ) + + // Graceful exit. + terminate(username) + } + } catch (err: any) { + showError(`Something went wrong: ${err.toString()}`, true) + } +} + +export default contribute diff --git a/packages/phase2cli/src/commands/finalize.ts b/packages/phase2cli/src/commands/finalize.ts new file mode 100644 index 00000000..ae92d9db --- /dev/null +++ b/packages/phase2cli/src/commands/finalize.ts @@ -0,0 +1,270 @@ +#!/usr/bin/env node +import crypto from "crypto" +import { zKey } from "snarkjs" +import open from "open" +import { getCeremonyCircuits } from "@zkmpc/actions" +import { httpsCallable } from "firebase/functions" +import { handleCurrentAuthUserSignIn, onlyCoordinator } from "../lib/auth" +import { collections, emojis, paths, solidityVersion, symbols, theme } from "../lib/constants" +import { GENERIC_ERRORS, showError } from "../lib/errors" +import { + checkAndMakeNewDirectoryIfNonexistent, + getLocalFilePath, + readFile, + writeFile, + writeLocalJsonFile +} from "../lib/files" +import { askForCeremonySelection, getEntropyOrBeacon } from "../lib/prompts" +import { getClosedCeremonies } from "../lib/queries" +import { + bootstrapCommandExec, + customSpinner, + getBucketName, + getContributorContributionsVerificationResults, + getValidContributionAttestation, + makeContribution, + multiPartUpload, + publishGist, + sleep, + terminate +} from "../lib/utils" +import { getDocumentById } from "../lib/firebase" + +/** + * Finalize command. + */ +const finalize = async () => { + try { + // Initialize services. + const { firebaseApp, firebaseFunctions, firestoreDatabase } = await bootstrapCommandExec() + + // Setup ceremony callable Cloud Function initialization. + const checkAndPrepareCoordinatorForFinalization = httpsCallable( + firebaseFunctions, + "checkAndPrepareCoordinatorForFinalization" + ) + const finalizeLastContribution = httpsCallable(firebaseFunctions, "finalizeLastContribution") + const finalizeCeremony = httpsCallable(firebaseFunctions, "finalizeCeremony") + + // Handle current authenticated user sign in. + const { user, token, username } = await handleCurrentAuthUserSignIn(firebaseApp) + + // Check custom claims for coordinator role. + await onlyCoordinator(user) + + // Get closed cerimonies info (if any). + const closedCeremoniesDocs = await getClosedCeremonies() + + console.log( + `${symbols.warning} The computation of the final contribution could take the bulk of your computational resources and memory based on the size of the circuit ${emojis.fire}\n` + ) + + // Ask to select a ceremony. + const ceremony = await askForCeremonySelection(closedCeremoniesDocs) + + // Get coordinator participant document. + const participantDoc = await getDocumentById( + `${collections.ceremonies}/${ceremony.id}/${collections.participants}`, + user.uid + ) + + const { data: canFinalize } = await checkAndPrepareCoordinatorForFinalization({ ceremonyId: ceremony.id }) + + if (!canFinalize) showError(`You are not able to finalize the ceremony`, true) + + // Clean directories. + checkAndMakeNewDirectoryIfNonexistent(paths.outputPath) + checkAndMakeNewDirectoryIfNonexistent(paths.finalizePath) + checkAndMakeNewDirectoryIfNonexistent(paths.finalZkeysPath) + checkAndMakeNewDirectoryIfNonexistent(paths.finalPotPath) + checkAndMakeNewDirectoryIfNonexistent(paths.finalAttestationsPath) + checkAndMakeNewDirectoryIfNonexistent(paths.verificationKeysPath) + checkAndMakeNewDirectoryIfNonexistent(paths.verifierContractsPath) + + // Handle random beacon request/generation. + const beacon = await getEntropyOrBeacon(false) + const beaconHashStr = crypto.createHash("sha256").update(beacon).digest("hex") + console.log(`${symbols.info} Your final beacon hash: ${theme.bold(beaconHashStr)}`) + + // Get ceremony circuits. + const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) + + // Attestation preamble. + const attestationPreamble = `Hey, I'm ${username} and I have finalized the ${ceremony.data.title} MPC Phase2 Trusted Setup ceremony.\nThe following are the finalization signatures:` + + // Finalize each circuit + for await (const circuit of circuits) { + await makeContribution(ceremony, circuit, beaconHashStr, username, true, firebaseFunctions) + + // 6. Export the verification key. + + // Paths config. + const finalZkeyLocalPath = `${paths.finalZkeysPath}/${circuit.data.prefix}_final.zkey` + const verificationKeyLocalPath = `${paths.verificationKeysPath}/${circuit.data.prefix}_vkey.json` + const verificationKeyStoragePath = `${collections.circuits}/${circuit.data.prefix}/${circuit.data.prefix}_vkey.json` + + const spinner = customSpinner(`Extracting verification key...`, "clock") + spinner.start() + + // Export vkey. + const verificationKeyJSONData = await zKey.exportVerificationKey(finalZkeyLocalPath) + + spinner.text = `Writing verification key locally...` + + // Write locally. + writeLocalJsonFile(verificationKeyLocalPath, verificationKeyJSONData) + + // nb. need to wait for closing the file descriptor. + await sleep(1500) + + // Upload vkey to storage. + const startMultiPartUpload = httpsCallable(firebaseFunctions, "startMultiPartUpload") + const generatePreSignedUrlsParts = httpsCallable(firebaseFunctions, "generatePreSignedUrlsParts") + const completeMultiPartUpload = httpsCallable(firebaseFunctions, "completeMultiPartUpload") + + const bucketName = getBucketName(ceremony.data.prefix) + + await multiPartUpload( + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload, + bucketName, + verificationKeyStoragePath, + verificationKeyLocalPath + ) + + spinner.succeed(`Verification key correctly stored`) + + // 7. Turn the verifier into a smart contract. + const verifierContractLocalPath = `${paths.verifierContractsPath}/${circuit.data.name}_verifier.sol` + const verifierContractStoragePath = `${collections.circuits}/${circuit.data.prefix}/${circuit.data.prefix}_verifier.sol` + + spinner.text = `Extracting verifier contract...` + spinner.start() + + // Export solidity verifier. + let verifierCode = await zKey.exportSolidityVerifier( + finalZkeyLocalPath, + { + groth16: readFile( + getLocalFilePath("../../../node_modules/snarkjs/templates/verifier_groth16.sol.ejs") + ) + }, + console + ) + + // Update solidity version. + verifierCode = verifierCode.replace( + /pragma solidity \^\d+\.\d+\.\d+/, + `pragma solidity ^${solidityVersion}` + ) + + spinner.text = `Writing verifier contract locally...` + + // Write locally. + writeFile(verifierContractLocalPath, verifierCode) + + // nb. need to wait for closing the file descriptor. + await sleep(1500) + + // Upload vkey to storage. + await multiPartUpload( + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload, + bucketName, + verifierContractStoragePath, + verifierContractLocalPath + ) + spinner.succeed(`Verifier contract correctly stored`) + + spinner.text = `Finalizing circuit...` + spinner.start() + + // Finalize circuit contribution. + await finalizeLastContribution({ + ceremonyId: ceremony.id, + circuitId: circuit.id, + bucketName + }) + + spinner.succeed(`Circuit successfully finalized`) + } + + process.stdout.write(`\n`) + + const spinner = customSpinner(`Finalizing the ceremony...`, "clock") + spinner.start() + + // Setup ceremony on the server. + await finalizeCeremony({ + ceremonyId: ceremony.id + }) + + spinner.succeed( + `Congrats, you have correctly finalized the ${theme.bold(ceremony.data.title)} circuits ${emojis.tada}\n` + ) + + spinner.text = `Generating public finalization attestation...` + spinner.start() + + // Get updated participant data. + const participantData = participantDoc.data() + + if (!participantData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Return true and false based on contribution verification. + const contributionsValidity = await getContributorContributionsVerificationResults( + ceremony.id, + participantDoc.id, + circuits, + true + ) + + // Get only valid contribution hashes. + const attestation = await getValidContributionAttestation( + contributionsValidity, + circuits, + participantData!, + ceremony.id, + participantDoc.id, + attestationPreamble, + true + ) + + writeFile( + `${paths.finalAttestationsPath}/${ceremony.data.prefix}_final_attestation.log`, + Buffer.from(attestation) + ) + + // nb. wait for closing file descriptor. + await sleep(1000) + + spinner.text = `Uploading public finalization attestation as Github Gist...` + + const gistUrl = await publishGist(token, attestation, ceremony.data.prefix, ceremony.data.title) + + spinner.succeed( + `Public finalization attestation successfully published as Github Gist at this link ${theme.bold( + theme.underlined(gistUrl) + )}` + ) + + // Attestation link via Twitter. + const attestationTweet = `https://twitter.com/intent/tweet?text=I%20have%20finalized%20the%20${ceremony.data.title}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20view%20my%20final%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP%20#PSE` + + console.log( + `\nYou can tweet about the ceremony finalization if you'd like (click on the link below ${ + emojis.pointDown + }) \n\n${theme.underlined(attestationTweet)}` + ) + + await open(attestationTweet) + + terminate(username) + } catch (err: any) { + showError(`Something went wrong: ${err.toString()}`, true) + } +} + +export default finalize diff --git a/packages/phase2cli/src/commands/index.ts b/packages/phase2cli/src/commands/index.ts new file mode 100644 index 00000000..94333249 --- /dev/null +++ b/packages/phase2cli/src/commands/index.ts @@ -0,0 +1,9 @@ +import setup from "./setup" +import auth from "./auth" +import contribute from "./contribute" +import observe from "./observe" +import finalize from "./finalize" +import clean from "./clean" +import logout from "./logout" + +export { setup, auth, contribute, observe, finalize, clean, logout } diff --git a/packages/phase2cli/src/commands/logout.ts b/packages/phase2cli/src/commands/logout.ts new file mode 100644 index 00000000..433e0d95 --- /dev/null +++ b/packages/phase2cli/src/commands/logout.ts @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +import { getAuth, signOut } from "firebase/auth" +import { deleteStoredOAuthToken, handleCurrentAuthUserSignIn } from "../lib/auth" +import { emojis, symbols, theme } from "../lib/constants" +import { showError } from "../lib/errors" +import { askForConfirmation } from "../lib/prompts" +import { bootstrapCommandExec, customSpinner } from "../lib/utils" + +/** + * Logout command. + */ +const logout = async () => { + try { + // Initialize services. + const { firebaseApp } = await bootstrapCommandExec() + + // Handle current authenticated user sign in. + await handleCurrentAuthUserSignIn(firebaseApp) + + // Inform the user about deassociation in Github and re run auth + console.log( + `${symbols.warning} We do not use any Github access token for authentication; thus we cannot revoke the authorization from your Github account for this CLI application` + ) + console.log( + `${symbols.info} You can do this manually as reported in the official Github documentation ${ + emojis.pointDown + }\n\n${theme.bold( + theme.underlined( + `https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/reviewing-your-authorized-applications-oauth` + ) + )}\n` + ) + + // Ask for confirmation. + const { confirmation } = await askForConfirmation("Are you sure you want to log out?", "Yes", "No") + + if (confirmation) { + const spinner = customSpinner(`Logging out...`, "clock") + spinner.start() + + // Sign out. + const auth = getAuth() + await signOut(auth) + + // Delete local token. + deleteStoredOAuthToken() + + spinner.stop() + console.log(`${symbols.success} Logout successfully completed ${emojis.wave}`) + } + } catch (err: any) { + showError(`Something went wrong: ${err.toString()}`, true) + } +} + +export default logout diff --git a/packages/phase2cli/src/commands/observe.ts b/packages/phase2cli/src/commands/observe.ts new file mode 100644 index 00000000..dd7b5bc9 --- /dev/null +++ b/packages/phase2cli/src/commands/observe.ts @@ -0,0 +1,181 @@ +#!/usr/bin/env node + +import readline from "readline" +import logSymbols from "log-symbols" +import { getOpenedCeremonies, getCeremonyCircuits } from "@zkmpc/actions" +import { FirebaseDocumentInfo } from "../../types/index" +import { onlyCoordinator, handleCurrentAuthUserSignIn } from "../lib/auth" +import { + bootstrapCommandExec, + convertToDoubleDigits, + customSpinner, + getSecondsMinutesHoursFromMillis, + sleep +} from "../lib/utils" +import { askForCeremonySelection } from "../lib/prompts" +import { getCurrentContributorContribution } from "../lib/queries" +import { GENERIC_ERRORS, showError } from "../lib/errors" +import { theme, emojis, symbols, observationWaitingTimeInMillis } from "../lib/constants" + +/** + * Clean cursor lines from current position back to root (default: zero). + * @param currentCursorPos - the current position of the cursor. + * @returns <number> + */ +const cleanCursorPosBackToRoot = (currentCursorPos: number) => { + while (currentCursorPos < 0) { + // Get back and clean line by line. + readline.cursorTo(process.stdout, 0) + readline.clearLine(process.stdout, 0) + readline.moveCursor(process.stdout, -1, -1) + + currentCursorPos += 1 + } + + return currentCursorPos +} + +/** + * Show the latest updates for the given circuit. + * @param ceremony <FirebaseDocumentInfo> - the Firebase document containing info about the ceremony. + * @param circuit <FirebaseDocumentInfo> - the Firebase document containing info about the circuit. + * @returns Promise<number> return the current position of the cursor (i.e., number of lines displayed). + */ +const displayLatestCircuitUpdates = async ( + ceremony: FirebaseDocumentInfo, + circuit: FirebaseDocumentInfo +): Promise<number> => { + let observation = theme.bold(`- Circuit # ${theme.magenta(circuit.data.sequencePosition)}`) // Observation output. + let cursorPos = -1 // Current cursor position (nb. decrease every time there's a new line!). + + const { waitingQueue } = circuit.data + + // Get info from circuit. + const { currentContributor } = waitingQueue + const { completedContributions } = waitingQueue + + if (!currentContributor) { + observation += `\n> Nobody's currently waiting to contribute ${emojis.eyes}` + cursorPos -= 1 + } else { + // Search for currentContributor' contribution. + const contributions = await getCurrentContributorContribution(ceremony.id, circuit.id, currentContributor) + + if (!contributions.length) { + // The contributor is currently contributing. + observation += `\n> Participant ${theme.bold(`#${completedContributions + 1}`)} (${theme.bold( + currentContributor + )}) is currently contributing ${emojis.fire}` + + cursorPos -= 1 + } else { + // The contributor has contributed. + observation += `\n> Participant ${theme.bold(`#${completedContributions}`)} (${theme.bold( + currentContributor + )}) has completed the contribution ${emojis.tada}` + + cursorPos -= 1 + + // The contributor has finished the contribution. + const contributionData = contributions.at(0)?.data + + if (!contributionData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Convert times to seconds. + const { + seconds: contributionTimeSeconds, + minutes: contributionTimeMinutes, + hours: contributionTimeHours + } = getSecondsMinutesHoursFromMillis(contributionData?.contributionTime) + const { + seconds: verificationTimeSeconds, + minutes: verificationTimeMinutes, + hours: verificationTimeHours + } = getSecondsMinutesHoursFromMillis(contributionData?.verificationTime) + + observation += `\n> The ${theme.bold("computation")} took ${theme.bold( + `${convertToDoubleDigits(contributionTimeHours)}:${convertToDoubleDigits( + contributionTimeMinutes + )}:${convertToDoubleDigits(contributionTimeSeconds)}` + )}` + observation += `\n> The ${theme.bold("verification")} took ${theme.bold( + `${convertToDoubleDigits(verificationTimeHours)}:${convertToDoubleDigits( + verificationTimeMinutes + )}:${convertToDoubleDigits(verificationTimeSeconds)}` + )}` + observation += `\n> Contribution ${ + contributionData?.valid + ? `${theme.bold("VALID")} ${symbols.success}` + : `${theme.bold("INVALID")} ${symbols.error}` + }` + + cursorPos -= 3 + } + } + + // Show observation for circuit. + process.stdout.write(`${observation}\n\n`) + cursorPos -= 1 + + return cursorPos +} + +/** + * Observe command. + */ +const observe = async () => { + try { + // Initialize services. + const { firebaseApp, firestoreDatabase } = await bootstrapCommandExec() + + // Handle current authenticated user sign in. + const { user } = await handleCurrentAuthUserSignIn(firebaseApp) + + // Check custom claims for coordinator role. + await onlyCoordinator(user) + + // Get running cerimonies info (if any). + const runningCeremoniesDocs = await getOpenedCeremonies(firestoreDatabase) + + // Ask to select a ceremony. + const ceremony = await askForCeremonySelection(runningCeremoniesDocs) + + console.log(`${logSymbols.info} Refresh rate set to ~3 seconds for waiting queue updates\n`) + + let cursorPos = 0 // Keep track of current cursor position. + + let spinner = customSpinner(`Getting ready...`, "clock") + spinner.start() + + // Get circuit updates every 3 seconds. + setInterval(async () => { + // Clean cursor position back to root. + cursorPos = cleanCursorPosBackToRoot(cursorPos) + + spinner = customSpinner(`Updating...`, "clock") + spinner.start() + + // Get updates from circuits. + const circuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) + + await sleep(observationWaitingTimeInMillis / 10) // Just for a smoother UX/UI experience. + + spinner.stop() + + // Observe changes for each circuit + for await (const circuit of circuits) cursorPos += await displayLatestCircuitUpdates(ceremony, circuit) + + process.stdout.write(`Press CTRL+C to exit`) + + await sleep(1000) // Just for a smoother UX/UI experience. + }, observationWaitingTimeInMillis) + + await sleep(observationWaitingTimeInMillis) // Wait until the first update. + + spinner.stop() + } catch (err: any) { + showError(`Something went wrong: ${err.toString()}`, true) + } +} + +export default observe diff --git a/packages/phase2cli/src/commands/setup.ts b/packages/phase2cli/src/commands/setup.ts new file mode 100644 index 00000000..97ddfc4a --- /dev/null +++ b/packages/phase2cli/src/commands/setup.ts @@ -0,0 +1,675 @@ +#!/usr/bin/env node + +import { zKey, r1cs } from "snarkjs" +import winston from "winston" +import blake from "blakejs" +import boxen from "boxen" +import { httpsCallable } from "firebase/functions" +import { Dirent, renameSync } from "fs" +import { + theme, + symbols, + emojis, + potFilenameTemplate, + potDownloadUrlTemplate, + paths, + names, + collections +} from "../lib/constants" +import { handleCurrentAuthUserSignIn, onlyCoordinator } from "../lib/auth" +import { + bootstrapCommandExec, + convertToDoubleDigits, + customSpinner, + estimatePoT, + extractPoTFromFilename, + extractPrefix, + getBucketName, + getCircuitMetadataFromR1csFile, + multiPartUpload, + simpleLoader, + sleep, + terminate +} from "../lib/utils" +import { + askCeremonyInputData, + askCircomCompilerVersionAndCommitHash, + askCircuitInputData, + askForCircuitSelectionFromLocalDir, + askForConfirmation, + askForPtauSelectionFromLocalDir, + askForZkeySelectionFromLocalDir, + askPowersOftau +} from "../lib/prompts" +import { + cleanDir, + directoryExists, + downloadFileFromUrl, + getDirFilesSubPaths, + getFileStats, + readFile +} from "../lib/files" +import { CeremonyTimeoutType, Circuit, CircuitFiles, CircuitInputData, CircuitTimings } from "../../types/index" +import { GENERIC_ERRORS, showError } from "../lib/errors" +import { createS3Bucket, objectExist } from "../lib/storage" + +/** + * 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 +} + +/** + * Handle one or more circuit addition for the specified ceremony. + * @param cwd <string> - the current working directory. + * @param cwdR1csFiles <Array<Dirent>> - the list of R1CS files in the current working directory. + * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony. + * @param isCircomVersionEqualAmongCircuits <boolean> - true if the circom compiler version is equal among circuits; otherwise false. + * @returns <Promise<Array<CircuitInputData>>> + */ +const handleCircuitsAddition = async ( + cwd: string, + cwdR1csFiles: Array<Dirent>, + timeoutMechanismType: CeremonyTimeoutType, + isCircomVersionEqualAmongCircuits: boolean +): Promise<Array<CircuitInputData>> => { + const circuitsInputData: Array<CircuitInputData> = [] + + let wannaAddAnotherCircuit = true // Loop flag. + let circuitSequencePosition = 1 // Sequential circuit position for handling the contributions queue for the ceremony. + let leftCircuits: Array<Dirent> = cwdR1csFiles + + // Clear directory. + cleanDir(paths.metadataPath) + + while (wannaAddAnotherCircuit) { + console.log(theme.bold(`\n- Circuit # ${theme.magenta(`${circuitSequencePosition}`)}\n`)) + + // Interactively select a circuit. + const circuitNameWithExt = await askForCircuitSelectionFromLocalDir(leftCircuits) + + // Remove the selected circuit from the list. + leftCircuits = leftCircuits.filter((dirent: Dirent) => dirent.name !== circuitNameWithExt) + + // Ask for circuit input data. + const circuitInputData = await askCircuitInputData(timeoutMechanismType, isCircomVersionEqualAmongCircuits) + + // Remove .r1cs file extension. + const circuitName = circuitNameWithExt.substring(0, circuitNameWithExt.indexOf(".")) + const circuitPrefix = extractPrefix(circuitName) + + // R1CS circuit file path. + const r1csMetadataFilePath = `${paths.metadataPath}/${circuitPrefix}_${names.metadata}.log` + const r1csFilePath = `${cwd}/${circuitName}.r1cs` + + // Custom logger for R1CS metadata save. + const logger = winston.createLogger({ + level: "info", + transports: new winston.transports.File({ + filename: r1csMetadataFilePath, + format: winston.format.printf((log) => log.message), + level: "info" + }) + }) + + const metadataSpinner = customSpinner(`Looking for metadata...`, "clock") + metadataSpinner.start() + + // Read .r1cs file and log/store info. + await r1cs.info(r1csFilePath, logger) + + // Sleep to avoid logger unexpected termination. + await sleep(1000) + + // Store data. + circuitsInputData.push({ + ...circuitInputData, + name: circuitName, + prefix: circuitPrefix, + sequencePosition: circuitSequencePosition + }) + + metadataSpinner.succeed( + `Metadata stored in your working directory ${theme.bold( + theme.underlined(r1csMetadataFilePath.substring(1)) + )}\n` + ) + + let readyToAssembly = false + + // In case of negative confirmation or no more circuits left. + if (leftCircuits.length !== 0) { + // Ask for another circuit. + const { confirmation: wannaAddNewCircuit } = await askForConfirmation( + "Want to add another circuit for the ceremony?", + "Okay", + "No" + ) + + if (wannaAddNewCircuit === undefined) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + if (wannaAddNewCircuit === false) readyToAssembly = true + else circuitSequencePosition += 1 + } else readyToAssembly = true + + // Assembly the ceremony. + if (readyToAssembly) wannaAddAnotherCircuit = false + } + + return circuitsInputData +} + +/** + * Check if the smallest pot has been already downloaded. + * @param neededPowers <number> - the representation of the constraints of the circuit in terms of powers. + * @returns <Promise<boolean>> + */ +const checkIfPotAlreadyDownloaded = async (neededPowers: number): Promise<boolean> => { + // Get files from dir. + const potFiles = await getDirFilesSubPaths(paths.potPath) + + let alreadyDownloaded = false + + for (const potFile of potFiles) { + const powers = extractPoTFromFilename(potFile.name) + + if (powers === neededPowers) alreadyDownloaded = true + } + + return alreadyDownloaded +} + +/** + * Setup a new Groth16 zkSNARK Phase 2 Trusted Setup ceremony. + */ +const setup = async () => { + // Circuit data state. + let circuitsInputData: Array<CircuitInputData> = [] + const circuits: Array<Circuit> = [] + + /** CORE */ + try { + // Get current working directory. + const cwd = process.cwd() + + const { firebaseApp, firebaseFunctions } = await bootstrapCommandExec() + + // Setup ceremony callable Cloud Function initialization. + const setupCeremony = httpsCallable(firebaseFunctions, "setupCeremony") + const createBucket = httpsCallable(firebaseFunctions, "createBucket") + const startMultiPartUpload = httpsCallable(firebaseFunctions, "startMultiPartUpload") + const generatePreSignedUrlsParts = httpsCallable(firebaseFunctions, "generatePreSignedUrlsParts") + const completeMultiPartUpload = httpsCallable(firebaseFunctions, "completeMultiPartUpload") + const checkIfObjectExist = httpsCallable(firebaseFunctions, "checkIfObjectExist") + + // Handle current authenticated user sign in. + const { user, username } = await handleCurrentAuthUserSignIn(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() + const ceremonyPrefix = extractPrefix(ceremonyInputData.title) + + // Check for circom compiler version and commit hash. + 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() + + // Ask to add circuits. + circuitsInputData = await handleCircuitsAddition( + cwd, + cwdR1csFiles, + ceremonyInputData.timeoutMechanismType, + isCircomVersionEqualAmongCircuits + ) + + // Add the data to the circuit input data. + circuitsInputData = circuitsInputData.map((circuitInputData: CircuitInputData) => ({ + ...circuitInputData, + compiler: { version, commitHash } + })) + } else + circuitsInputData = await handleCircuitsAddition( + cwd, + cwdR1csFiles, + ceremonyInputData.timeoutMechanismType, + isCircomVersionEqualAmongCircuits + ) + + await simpleLoader(`Assembling your ceremony...`, `clock`, 2000) + + // Ceremony summary. + let summary = `${`${theme.bold(ceremonyInputData.title)}\n${theme.italic(ceremonyInputData.description)}`} + \n${`Opening: ${theme.bold( + theme.underlined(ceremonyInputData.startDate.toUTCString().replace("GMT", "UTC")) + )}\nEnding: ${theme.bold(theme.underlined(ceremonyInputData.endDate.toUTCString().replace("GMT", "UTC")))}`} + \n${theme.bold( + ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC ? `Dynamic` : `Fixed` + )} Timeout / ${theme.bold(ceremonyInputData.penalty)}m Penalty` + + for (let i = 0; i < circuitsInputData.length; i += 1) { + const circuitInputData = circuitsInputData[i] + + // Read file. + const r1csMetadataFilePath = `${paths.metadataPath}/${circuitInputData.prefix}_metadata.log` + const circuitMetadata = readFile(r1csMetadataFilePath) + + // Extract info from file. + const curve = getCircuitMetadataFromR1csFile(circuitMetadata, /Curve: .+\n/s) + const wires = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Wires: .+\n/s)) + const constraints = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Constraints: .+\n/s)) + const privateInputs = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Private Inputs: .+\n/s)) + const publicOutputs = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Public Inputs: .+\n/s)) + const labels = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Labels: .+\n/s)) + const outputs = Number(getCircuitMetadataFromR1csFile(circuitMetadata, /# of Outputs: .+\n/s)) + const pot = estimatePoT(constraints, outputs) + + // Store info. + circuits.push({ + ...circuitInputData, + metadata: { + curve, + wires, + constraints, + privateInputs, + publicOutputs, + labels, + outputs, + pot + } + }) + + // Show circuit summary. + summary += `\n\n${theme.bold( + `- CIRCUIT # ${theme.bold(theme.magenta(`${circuitInputData.sequencePosition}`))}` + )} + \n${`${theme.bold(circuitInputData.name)}\n${theme.italic(circuitInputData.description)} + \nCurve: ${theme.bold(curve)}\nCompiler: v${theme.bold(`${circuitInputData.compiler.version}`)} (${theme.bold( + circuitInputData.compiler.commitHash?.slice(0, 7) + )})\nSource: ${theme.bold(circuitInputData.template.source.split(`/`).at(-1))}(${theme.bold( + circuitInputData.template.paramsConfiguration + )})\n${ + ceremonyInputData.timeoutMechanismType === CeremonyTimeoutType.DYNAMIC + ? `Threshold: ${theme.bold(circuitInputData.timeoutThreshold)}%` + : `Max Contribution Time: ${theme.bold(circuitInputData.timeoutMaxContributionWaitingTime)}m` + } + \n# Wires: ${theme.bold(wires)}\n# Constraints: ${theme.bold(constraints)}\n# Private Inputs: ${theme.bold( + privateInputs + )}\n# Public Inputs: ${theme.bold(publicOutputs)}\n# Labels: ${theme.bold(labels)}\n# Outputs: ${theme.bold( + outputs + )}\n# PoT: ${theme.bold(pot)}`}` + } + + // Show ceremony summary. + console.log( + boxen(summary, { + title: theme.magenta(`CEREMONY SUMMARY`), + titleAlignment: "center", + textAlignment: "left", + margin: 1, + padding: 1 + }) + ) + + // Ask for confirmation. + const { confirmation } = await askForConfirmation("Please, confirm to create the ceremony", "Okay", "Exit") + + if (confirmation) { + // Create the bucket. + const bucketName = getBucketName(ceremonyPrefix) + + const spinner = customSpinner(`Creating the storage bucket...`, `clock`) + spinner.start() + + await createS3Bucket(createBucket, bucketName) + await sleep(1000) + + spinner.succeed(`Storage bucket ${bucketName} successfully created`) + + // Get local zkeys (if any). + spinner.text = "Checking for pre-computed zkeys..." + spinner.start() + + const cwdZkeysFiles = await getSpecifiedFilesFromCwd(cwd, `.zkey`) + + await sleep(1000) + + spinner.stop() + + let leftPreComputedZkeys: Array<Dirent> = cwdZkeysFiles + + // Circuit setup. + for (let i = 0; i < circuits.length; i += 1) { + // Flag for generation of zkey from scratch. + let wannaGenerateZkey = true + // Flag for PoT download. + let wannaUsePreDownloadedPoT = false + + // Get the current circuit + const circuit = circuits[i] + + // Convert to double digits powers (e.g., 9 -> 09). + let stringifyNeededPowers = convertToDoubleDigits(circuit.metadata.pot) + let smallestPotForCircuit = `${potFilenameTemplate}${stringifyNeededPowers}.ptau` + + // Circuit r1cs and zkey file names. + const r1csFileName = `${circuit.name}.r1cs` + const firstZkeyFileName = `${circuit.prefix}_00000.zkey` + let preComputedZkeyNameWithExt = `` + + const r1csLocalPathAndFileName = `${cwd}/${r1csFileName}` + let potLocalPathAndFileName = `${paths.potPath}/${smallestPotForCircuit}` + let zkeyLocalPathAndFileName = `${paths.zkeysPath}/${firstZkeyFileName}` + + const potStoragePath = `${names.pot}` + const r1csStoragePath = `${collections.circuits}/${circuit.prefix}` + const zkeyStoragePath = `${collections.circuits}/${circuit.prefix}/${collections.contributions}` + + const r1csStorageFilePath = `${r1csStoragePath}/${r1csFileName}` + let potStorageFilePath = `${potStoragePath}/${smallestPotForCircuit}` + const zkeyStorageFilePath = `${zkeyStoragePath}/${firstZkeyFileName}` + + console.log(theme.bold(`\n- Setup for Circuit # ${theme.magenta(`${circuit.sequencePosition}`)}\n`)) + + if (!leftPreComputedZkeys.length) console.log(`${symbols.warning} There are no pre-computed zKeys`) + else { + const { confirmation: preComputedZkeySelection } = await askForConfirmation( + `Do you wanna select a pre-computed zkey for the ${circuit.name} circuit?`, + `Yes`, + `No` + ) + + if (preComputedZkeySelection) { + // Ask for zKey selection. + preComputedZkeyNameWithExt = await askForZkeySelectionFromLocalDir(leftPreComputedZkeys) + + // Switch to pre-computed zkey path. + zkeyLocalPathAndFileName = `${cwd}/${preComputedZkeyNameWithExt}` + + // Switch the flag. + wannaGenerateZkey = false + } + } + + // If the coordinator wants to use a pre-computed zkey, needs to provide the related ptau. + if (!wannaGenerateZkey) { + spinner.text = "Checking for Powers of Tau..." + spinner.start() + + const cwdPtausFiles = await getSpecifiedFilesFromCwd(cwd, `.ptau`) + await sleep(1000) + + if (!cwdPtausFiles.length) { + spinner.warn(`No Powers of Tau (.ptau) files found`) + + // Download the PoT. + const { powers } = await askPowersOftau(circuit.metadata.pot) + + // Convert to double digits powers (e.g., 9 -> 09). + stringifyNeededPowers = convertToDoubleDigits(Number(powers)) + smallestPotForCircuit = `${potFilenameTemplate}${stringifyNeededPowers}.ptau` + + // Override. + potLocalPathAndFileName = `${paths.potPath}/${smallestPotForCircuit}` + potStorageFilePath = `${potStoragePath}/${smallestPotForCircuit}` + } else { + spinner.stop() + + // Ask for ptau selection. + smallestPotForCircuit = await askForPtauSelectionFromLocalDir( + cwdPtausFiles, + circuit.metadata.pot + ) + + // Update. + stringifyNeededPowers = convertToDoubleDigits(extractPoTFromFilename(smallestPotForCircuit)) + + // Switch to new ptau path. + potLocalPathAndFileName = `${cwd}/${smallestPotForCircuit}` + potStorageFilePath = `${potStoragePath}/${smallestPotForCircuit}` + + wannaUsePreDownloadedPoT = true + } + } + + // Check if the smallest pot has been already downloaded. + const alreadyDownloaded = + (await checkIfPotAlreadyDownloaded(Number(smallestPotForCircuit))) || wannaUsePreDownloadedPoT + + if (!alreadyDownloaded) { + // Get smallest suitable pot for circuit. + const downloadSpinner = customSpinner( + `Downloading ${theme.bold(`#${stringifyNeededPowers}`)} Powers of Tau from PPoT...`, + "clock" + ) + downloadSpinner.start() + + // Download PoT file. + const potDownloadUrl = `${potDownloadUrlTemplate}${smallestPotForCircuit}` + const destFilePath = `${paths.potPath}/${smallestPotForCircuit}` + + await downloadFileFromUrl(destFilePath, potDownloadUrl) + + downloadSpinner.succeed( + `Powers of Tau ${theme.bold(`#${stringifyNeededPowers}`)} correctly downloaded` + ) + } else + console.log( + `${symbols.success} Powers of Tau ${theme.bold(`#${stringifyNeededPowers}`)} already downloaded` + ) + + // Check if the smallest pot has been already uploaded. + const alreadyUploadedPot = await objectExist( + checkIfObjectExist, + bucketName, + `${ceremonyPrefix}/${names.pot}/${smallestPotForCircuit}` + ) + + // Validity check for the pre-computed zKey (avoids to upload an invalid combination of r1cs, ptau and zkey files). + if (!wannaGenerateZkey) { + // Check validity. + await simpleLoader(`Checking pre-computed zkey validity...`, `clock`, 1500) + + const valid = await zKey.verifyFromR1cs( + r1csLocalPathAndFileName, + potLocalPathAndFileName, + zkeyLocalPathAndFileName, + console + ) + + // nb. workaround for file descriptor closing. + await sleep(3000) + + if (valid) { + spinner.succeed(`Your pre-computed zKey is valid`) + + // Remove the selected zkey from the list. + leftPreComputedZkeys = leftPreComputedZkeys.filter( + (dirent: Dirent) => dirent.name !== preComputedZkeyNameWithExt + ) + + // Rename to first zkey filename. + renameSync(`${cwd}/${preComputedZkeyNameWithExt}`, `${circuit.prefix}_00000.zkey`) + } else { + spinner.fail(`Something went wrong during the verification of your pre-computed zKey`) + + // Ask to generate a new one from scratch. + const { confirmation: zkeyGeneration } = await askForConfirmation( + `Do you wanna generate a new zkey for the ${circuit.name} circuit? (nb. A negative answer will ABORT the entire setup process)`, + `Yes`, + `No` + ) + + if (!zkeyGeneration) showError(`You have choosen to abort the entire setup process`, true) + else wannaGenerateZkey = true + } + } + + // Generate a brand new zKey. + if (wannaGenerateZkey) { + console.log( + `${symbols.warning} ${theme.bold( + `The computation of your zKey is starting soon (nb. do not interrupt the process because this will ABORT the entire setup process)` + )}\n` + ) + + // Compute first .zkey file (without any contribution). + await zKey.newZKey( + r1csLocalPathAndFileName, + potLocalPathAndFileName, + zkeyLocalPathAndFileName, + console + ) + + console.log( + `\n${symbols.success} First zkey ${theme.bold(firstZkeyFileName)} successfully computed` + ) + } + + // Upload zkey. + await multiPartUpload( + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload, + bucketName, + zkeyStorageFilePath, + zkeyLocalPathAndFileName + ) + + console.log( + `${symbols.success} First zkey ${theme.bold(firstZkeyFileName)} successfully saved on storage` + ) + + // PoT. + if (!alreadyUploadedPot) { + // Upload. + await multiPartUpload( + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload, + bucketName, + potStorageFilePath, + potLocalPathAndFileName + ) + + console.log( + `${symbols.success} Powers of Tau ${theme.bold( + smallestPotForCircuit + )} successfully saved on storage` + ) + } else { + console.log(`${symbols.success} Powers of Tau ${theme.bold(smallestPotForCircuit)} already stored`) + } + + // Upload R1CS. + await multiPartUpload( + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload, + bucketName, + r1csStorageFilePath, + r1csLocalPathAndFileName + ) + + console.log(`${symbols.success} R1CS ${theme.bold(r1csFileName)} successfully saved on storage`) + + // Circuit-related files info. + const circuitFiles: CircuitFiles = { + files: { + r1csFilename: r1csFileName, + potFilename: smallestPotForCircuit, + initialZkeyFilename: firstZkeyFileName, + r1csStoragePath: r1csStorageFilePath, + potStoragePath: potStorageFilePath, + initialZkeyStoragePath: zkeyStorageFilePath, + r1csBlake2bHash: blake.blake2bHex(r1csStorageFilePath), + potBlake2bHash: blake.blake2bHex(potStorageFilePath), + initialZkeyBlake2bHash: blake.blake2bHex(zkeyStorageFilePath) + } + } + + // nb. these will be validated after the first contribution. + const circuitTimings: CircuitTimings = { + avgTimings: { + contributionComputation: 0, + fullContribution: 0, + verifyCloudFunction: 0 + } + } + + circuits[i] = { + ...circuit, + ...circuitFiles, + ...circuitTimings, + zKeySizeInBytes: getFileStats(zkeyLocalPathAndFileName).size + } + + // Reset flags. + wannaGenerateZkey = true + wannaUsePreDownloadedPoT = false + } + + process.stdout.write(`\n`) + + /** POPULATE DB */ + spinner.text = `Storing ceremony data...` + spinner.start() + + // Setup ceremony on the server. + await setupCeremony({ + ceremonyInputData, + ceremonyPrefix, + circuits + }) + + // nb. workaround for CF termination. + await sleep(1000) + + spinner.succeed( + `Congrats, you have successfully completed your ${theme.bold(ceremonyInputData.title)} ceremony setup ${ + emojis.tada + }` + ) + } + + terminate(username) + } catch (err: any) { + showError(`Something went wrong: ${err.toString()}`, true) + } +} + +export default setup diff --git a/packages/phase2cli/src/index.ts b/packages/phase2cli/src/index.ts new file mode 100755 index 00000000..c2599853 --- /dev/null +++ b/packages/phase2cli/src/index.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env node + +import { createCommand } from "commander" +import { setup, auth, contribute, observe, finalize, clean, logout } from "./commands/index" +import { readLocalJsonFile } from "./lib/files" + +// Get pkg info (e.g., name, version). +const pkg = readLocalJsonFile("../../package.json") + +const program = createCommand() + +// Entry point. +program.name(pkg.name).description(pkg.description).version(pkg.version) + +// User commands. +program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth) +program + .command("contribute") + .description("compute contributions for a Phase2 Trusted Setup ceremony circuits") + .action(contribute) +program + .command("clean") + .description("clean up output generated by commands from the current working directory") + .action(clean) +program + .command("logout") + .description("sign out from Firebae Auth service and delete Github OAuth 2.0 token from local storage") + .action(logout) + +// Only coordinator commands. +const ceremony = program.command("coordinate").description("commands for coordinating a ceremony") + +ceremony + .command("setup") + .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits") + .action(setup) + +ceremony + .command("observe") + .description("observe in real-time the waiting queue of each ceremony circuit") + .action(observe) + +ceremony + .command("finalize") + .description( + "finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract" + ) + .action(finalize) + +program.parseAsync() diff --git a/apps/phase2cli/src/lib/auth.ts b/packages/phase2cli/src/lib/auth.ts similarity index 62% rename from apps/phase2cli/src/lib/auth.ts rename to packages/phase2cli/src/lib/auth.ts index d07d5baa..7c9acd55 100644 --- a/apps/phase2cli/src/lib/auth.ts +++ b/packages/phase2cli/src/lib/auth.ts @@ -2,24 +2,24 @@ import Conf from "conf" import { FirebaseApp } from "firebase/app" import { signInToFirebaseWithGithubToken, getCurrentFirebaseAuthUser } from "@zkmpc/actions" import { IdTokenResult, User } from "firebase/auth" -import { AuthUser } from "../../types/index.js" -import { readLocalJsonFile } from "./files.js" -import { GENERIC_ERRORS, GITHUB_ERRORS, showError } from "./errors.js" -import { emojis, theme } from "./constants.js" -import { getGithubUsername } from "./utils.js" +import { AuthUser } from "../../types/index" +import { readLocalJsonFile } from "./files" +import { GENERIC_ERRORS, GITHUB_ERRORS, showError } from "./errors" +import { emojis, theme } from "./constants" +import { getGithubUsername } from "./utils" // Get local configs. const { name } = readLocalJsonFile("../../package.json") // Local configstore for storing auth data (e.g., tokens). const config = new Conf({ - projectName: name, - schema: { - authToken: { - type: "string", - default: "" + projectName: name, + schema: { + authToken: { + type: "string", + default: "" + } } - } }) /** @@ -50,9 +50,9 @@ export const deleteStoredOAuthToken = () => config.delete("authToken") * @returns <Promise<string>> - the Github OAuth 2.0 token. */ export const checkForStoredOAuthToken = async (): Promise<string> => { - if (!hasStoredOAuthToken()) showError(GITHUB_ERRORS.GITHUB_NOT_AUTHENTICATED, true) + if (!hasStoredOAuthToken()) showError(GITHUB_ERRORS.GITHUB_NOT_AUTHENTICATED, true) - return String(getStoredOAuthToken()) + return String(getStoredOAuthToken()) } /** @@ -61,10 +61,10 @@ export const checkForStoredOAuthToken = async (): Promise<string> => { * @returns <Promise<IdTokenResult>> */ const getTokenAndClaims = async (user: User): Promise<IdTokenResult> => { - // Force refresh to update custom claims. - await user.getIdToken(true) + // Force refresh to update custom claims. + await user.getIdToken(true) - return user.getIdTokenResult() + return user.getIdTokenResult() } /** @@ -72,9 +72,9 @@ const getTokenAndClaims = async (user: User): Promise<IdTokenResult> => { * @param user <User> - the current authenticated user. */ export const onlyCoordinator = async (user: User) => { - const userTokenAndClaims = await getTokenAndClaims(user) + const userTokenAndClaims = await getTokenAndClaims(user) - if (!userTokenAndClaims.claims.coordinator) showError(GENERIC_ERRORS.GENERIC_NOT_COORDINATOR, true) + if (!userTokenAndClaims.claims.coordinator) showError(GENERIC_ERRORS.GENERIC_NOT_COORDINATOR, true) } /** @@ -82,25 +82,25 @@ export const onlyCoordinator = async (user: User) => { * @returns <Promise<AuthUser>> */ export const handleCurrentAuthUserSignIn = async (firebaseApp: FirebaseApp): Promise<AuthUser> => { - // Get/Set OAuth Token. - const token = await checkForStoredOAuthToken() + // Get/Set OAuth Token. + const token = await checkForStoredOAuthToken() - // Sign in. - // TODO: maybe this is not correct for #171. - await signInToFirebaseWithGithubToken(firebaseApp, token) + // Sign in. + // TODO: maybe this is not correct for #171. + await signInToFirebaseWithGithubToken(firebaseApp, token) - // Get current authenticated user. - const user = await getCurrentFirebaseAuthUser(firebaseApp) + // Get current authenticated user. + const user = await getCurrentFirebaseAuthUser(firebaseApp) - // Get the username of the authenticated user. - const username = await getGithubUsername(token) + // Get the username of the authenticated user. + const username = await getGithubUsername(token) - // Greet the user. - console.log(`Greetings, @${theme.bold(theme.bold(username))} ${emojis.wave}\n`) + // Greet the user. + console.log(`Greetings, @${theme.bold(theme.bold(username))} ${emojis.wave}\n`) - return { - user, - token, - username - } + return { + user, + token, + username + } } diff --git a/packages/phase2cli/src/lib/constants.ts b/packages/phase2cli/src/lib/constants.ts new file mode 100644 index 00000000..f1b45f17 --- /dev/null +++ b/packages/phase2cli/src/lib/constants.ts @@ -0,0 +1,139 @@ +import chalk from "chalk" +import logSymbols from "log-symbols" +import emoji from "node-emoji" + +/** Theme */ +export const theme = { + yellow: chalk.yellow, + magenta: chalk.magenta, + red: chalk.red, + green: chalk.green, + underlined: chalk.underline, + bold: chalk.bold, + italic: chalk.italic +} + +export const symbols = { + success: logSymbols.success, + warning: logSymbols.warning, + error: logSymbols.error, + info: logSymbols.info +} + +export const 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") +} + +/** ZK related */ +export const potDownloadUrlTemplate = `https://hermez.s3-eu-west-1.amazonaws.com/` +export const potFilenameTemplate = `powersOfTau28_hez_final_` +export const firstZkeyIndex = `00000` +export const numIterationsExp = 10 +export const solidityVersion = "0.8.0" + +/** Commands related */ +export const observationWaitingTimeInMillis = 3000 // 3 seconds. + +/** Shared */ +export const names = { + output: `output`, + setup: `setup`, + contribute: `contribute`, + finalize: `finalize`, + pot: `pot`, + zkeys: `zkeys`, + vkeys: `vkeys`, + metadata: `metadata`, + transcripts: `transcripts`, + attestation: `attestation`, + verifiers: `verifiers` +} + +const outputPath = `./${names.output}` +const setupPath = `${outputPath}/${names.setup}` +const contributePath = `${outputPath}/${names.contribute}` +const finalizePath = `${outputPath}/${names.finalize}` +const potPath = `${setupPath}/${names.pot}` +const zkeysPath = `${setupPath}/${names.zkeys}` +const metadataPath = `${setupPath}/${names.metadata}` +const contributionsPath = `${contributePath}/${names.zkeys}` +const contributionTranscriptsPath = `${contributePath}/${names.transcripts}` +const attestationPath = `${contributePath}/${names.attestation}` +const finalZkeysPath = `${finalizePath}/${names.zkeys}` +const finalPotPath = `${finalizePath}/${names.pot}` +const finalTranscriptsPath = `${finalizePath}/${names.transcripts}` +const finalAttestationsPath = `${finalizePath}/${names.attestation}` +const verificationKeysPath = `${finalizePath}/${names.vkeys}` +const verifierContractsPath = `${finalizePath}/${names.verifiers}` + +export const paths = { + outputPath, + setupPath, + contributePath, + finalizePath, + potPath, + zkeysPath, + metadataPath, + contributionsPath, + contributionTranscriptsPath, + attestationPath, + finalZkeysPath, + finalPotPath, + finalTranscriptsPath, + finalAttestationsPath, + verificationKeysPath, + verifierContractsPath +} + +/** Firebase */ +export const collections = { + users: "users", + participants: "participants", + ceremonies: "ceremonies", + circuits: "circuits", + contributions: "contributions", + timeouts: "timeouts" +} + +export const ceremoniesCollectionFields = { + coordinatorId: "coordinatorId", + description: "description", + endDate: "endDate", + lastUpdated: "lastUpdated", + prefix: "prefix", + startDate: "startDate", + state: "state", + title: "title", + type: "type" +} + +export const contributionsCollectionFields = { + contributionTime: "contributionTime", + files: "files", + lastUpdated: "lastUpdated", + participantId: "participantId", + valid: "valid", + verificationTime: "verificationTime", + zkeyIndex: "zKeyIndex" +} + +export const timeoutsCollectionFields = { + startDate: "startDate", + endDate: "endDate" +} diff --git a/packages/phase2cli/src/lib/errors.ts b/packages/phase2cli/src/lib/errors.ts new file mode 100644 index 00000000..fb42b88d --- /dev/null +++ b/packages/phase2cli/src/lib/errors.ts @@ -0,0 +1,51 @@ +import { symbols } from "./constants" + +/** Firebase */ +export const FIREBASE_ERRORS = { + FIREBASE_NOT_CONFIGURED_PROPERLY: `Check that all FIREBASE environment variables are configured properly`, + FIREBASE_DEFAULT_APP_DOUBLE_CONFIG: `Wrong double default configuration for Firebase application`, + FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `Unsuccessful check authorization response from Github. This usually happens when a token expires or the CLI do not have permissions associated with your Github account`, + FIREBASE_USER_DISABLED: `Your Github account has been disabled and can no longer be used to contribute. Get in touch with the coordinator to find out more`, + FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials. This usually happens due to network errors`, + FIREBASE_NETWORK_ERROR: `Unable to reach Firebase. This usually happens due to network errors`, + FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`, + FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization` +} + +/** Github */ +export const GITHUB_ERRORS = { + GITHUB_NOT_CONFIGURED_PROPERLY: `Github \`CLIENT_ID\` environment variable is not configured properly`, + GITHUB_ACCOUNT_ASSOCIATION_REJECTED: `You refused to associate your Github account with the CLI`, + GITHUB_SERVER_TIMEDOUT: `Github server has timed out. This usually happens due to network error or Github server downtime`, + GITHUB_GET_USERNAME_FAILED: `Something went wrong while retrieving your Github username`, + GITHUB_NOT_AUTHENTICATED: `You are not authenticated. Please, run \`phase2cli auth\` command first`, + GITHUB_GIST_PUBLICATION_FAILED: `Something went wrong while publishing a Gist from your Github account` +} + +/** 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`, + GENERIC_CEREMONY_SELECTION: `You have aborted the ceremony selection process`, + GENERIC_CIRCUIT_SELECTION: `You have aborted the circuit selection process`, + GENERIC_DATA_INPUT: `You have aborted the process without providing any of the requested data`, + GENERIC_CONTRIBUTION_HASH_INVALID: `You have aborted the process and do not have provided the requested data` +} + +/** + * 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. + */ +export const showError = (err: string, doExit: boolean) => { + // Print the error. + console.error(`${symbols.error} ${err}`) + + // Terminate the process. + if (doExit) process.exit(0) +} diff --git a/apps/phase2cli/src/lib/files.ts b/packages/phase2cli/src/lib/files.ts similarity index 64% rename from apps/phase2cli/src/lib/files.ts rename to packages/phase2cli/src/lib/files.ts index 40da18ab..bb362281 100644 --- a/apps/phase2cli/src/lib/files.ts +++ b/packages/phase2cli/src/lib/files.ts @@ -5,8 +5,7 @@ import { promisify } from "node:util" import fetch from "node-fetch" import path from "path" import { fileURLToPath } from "url" -import { GENERIC_ERRORS, showError } from "./errors.js" - +import { GENERIC_ERRORS, showError } from "./errors" /** * Check a directory path * @param filePath <string> - the absolute or relative path. @@ -16,23 +15,23 @@ export const directoryExists = (filePath: string): boolean => fs.existsSync(file /** * Write a new file locally. - * @param path <string> - local path for file with extension. + * @param writePath <string> - local path for file with extension. * @param data <Buffer> - content to be written. */ -export const writeFile = (path: string, data: Buffer): void => fs.writeFileSync(path, data) +export const writeFile = (writePath: string, data: Buffer): void => fs.writeFileSync(writePath, data) /** * Read a new file from local storage. - * @param path <string> - local path for file with extension. + * @param readPath <string> - local path for file with extension. */ -export const readFile = (path: string): string => fs.readFileSync(path, "utf-8") +export const readFile = (readPath: string): string => fs.readFileSync(readPath, "utf-8") /** * Get back the statistics of the provided file. - * @param path <string> - local path for file with extension. + * @param getStatsPath <string> - local path for file with extension. * @returns <Stats> */ -export const getFileStats = (path: string): Stats => fs.statSync(path) +export const getFileStats = (getStatsPath: string): Stats => fs.statSync(getStatsPath) /** * Return the sub paths for each file stored in the given directory. @@ -40,11 +39,11 @@ export const getFileStats = (path: string): Stats => fs.statSync(path) * @returns */ export const getDirFilesSubPaths = async (dirPath: string): Promise<Array<Dirent>> => { - // Get Dirent sub paths for folders and files. - const subPaths = await fs.promises.readdir(dirPath, { withFileTypes: true }) + // Get Dirent sub paths for folders and files. + const subPaths = await fs.promises.readdir(dirPath, { withFileTypes: true }) - // Return Dirent sub paths for files only. - return subPaths.filter((dirent: Dirent) => dirent.isFile()) + // Return Dirent sub paths for files only. + return subPaths.filter((dirent: Dirent) => dirent.isFile()) } /** @@ -54,14 +53,14 @@ export const getDirFilesSubPaths = async (dirPath: string): Promise<Array<Dirent * @returns <string> */ export const getMatchingSubPathFile = (subPaths: Array<Dirent>, fileNameToMatch: string): string => { - // Filter. - const matchingPaths = subPaths.filter((subpath: Dirent) => subpath.name === fileNameToMatch) + // Filter. + const matchingPaths = subPaths.filter((subpath: Dirent) => subpath.name === fileNameToMatch) - // Check. - if (!matchingPaths.length) showError(GENERIC_ERRORS.GENERIC_FILE_ERROR, true) + // Check. + if (!matchingPaths.length) showError(GENERIC_ERRORS.GENERIC_FILE_ERROR, true) - // Return file name. - return matchingPaths[0].name + // Return file name. + return matchingPaths[0].name } /** @@ -69,7 +68,7 @@ export const getMatchingSubPathFile = (subPaths: Array<Dirent>, fileNameToMatch: * @param dirPath <string> - the directory path. */ export const deleteDir = (dirPath: string): void => { - fs.rmSync(dirPath, { recursive: true, force: true }) + fs.rmSync(dirPath, { recursive: true, force: true }) } /** @@ -77,8 +76,8 @@ export const deleteDir = (dirPath: string): void => { * @param dirPath <string> - the directory path. */ export const cleanDir = (dirPath: string): void => { - deleteDir(dirPath) - fs.mkdirSync(dirPath) + deleteDir(dirPath) + fs.mkdirSync(dirPath) } /** @@ -86,7 +85,7 @@ export const cleanDir = (dirPath: string): void => { * @param dirPath <string> - the directory path. */ export const checkAndMakeNewDirectoryIfNonexistent = (dirPath: string): void => { - if (!directoryExists(dirPath)) fs.mkdirSync(dirPath) + if (!directoryExists(dirPath)) fs.mkdirSync(dirPath) } /** @@ -95,9 +94,9 @@ export const checkAndMakeNewDirectoryIfNonexistent = (dirPath: string): void => * @returns <any> */ export const readJSONFile = (filePath: string): any => { - if (!directoryExists(filePath)) showError(GENERIC_ERRORS.GENERIC_FILE_ERROR, true) + if (!directoryExists(filePath)) showError(GENERIC_ERRORS.GENERIC_FILE_ERROR, true) - return JSON.parse(readFile(filePath)) + return JSON.parse(readFile(filePath)) } /** @@ -106,7 +105,7 @@ export const readJSONFile = (filePath: string): any => { * @param data <JSON> */ export const writeLocalJsonFile = (filePath: string, data: JSON) => { - fs.writeFileSync(filePath, JSON.stringify(data), "utf-8") + fs.writeFileSync(filePath, JSON.stringify(data), "utf-8") } /** @@ -114,8 +113,8 @@ export const writeLocalJsonFile = (filePath: string, data: JSON) => { * @returns <string> - the local project (e.g., dist/) directory name. */ export const getLocalDirname = (): string => { - const filename = fileURLToPath(import.meta.url) - return path.dirname(filename) + const filename = fileURLToPath(import.meta.url) + return path.dirname(filename) } /** @@ -138,9 +137,9 @@ export const readLocalJsonFile = (filePath: string): any => readJSONFile(path.jo * @returns <Promise<boolean>> */ export const checkIfDirectoryIsEmpty = async (dirPath: string): Promise<boolean> => { - const dirNumberOfFiles = await getDirFilesSubPaths(dirPath) + const dirNumberOfFiles = await getDirFilesSubPaths(dirPath) - return !(dirNumberOfFiles.length > 0) + return !(dirNumberOfFiles.length > 0) } /** @@ -149,11 +148,11 @@ export const checkIfDirectoryIsEmpty = async (dirPath: string): Promise<boolean> * @param url <string> - the download url. */ export const downloadFileFromUrl = async (dest: string, url: string): Promise<void> => { - const streamPipeline = promisify(pipeline) + const streamPipeline = promisify(pipeline) - const response = await fetch(url) + const response = await fetch(url) - if (!response.ok) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + if (!response.ok) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) - if (response.body) await streamPipeline(response.body, createWriteStream(dest)) + if (response.body) await streamPipeline(response.body, createWriteStream(dest)) } diff --git a/apps/phase2cli/src/lib/firebase.ts b/packages/phase2cli/src/lib/firebase.ts similarity index 53% rename from apps/phase2cli/src/lib/firebase.ts rename to packages/phase2cli/src/lib/firebase.ts index 7b23e1c3..cbaf5b8c 100644 --- a/apps/phase2cli/src/lib/firebase.ts +++ b/packages/phase2cli/src/lib/firebase.ts @@ -1,27 +1,26 @@ import { FirebaseApp, FirebaseOptions, initializeApp } from "firebase/app" import { - collection as collectionRef, - doc, - DocumentData, - DocumentSnapshot, - Firestore, - getDoc, - getDocs, - getFirestore, - query, - QueryConstraint, - QueryDocumentSnapshot, - QuerySnapshot + collection as collectionRef, + doc, + DocumentData, + DocumentSnapshot, + Firestore, + getDoc, + getDocs, + getFirestore, + query, + QueryConstraint, + QueryDocumentSnapshot, + QuerySnapshot } from "firebase/firestore" import { Functions, getFunctions } from "firebase/functions" -import { FirebaseStorage, getBytes, getDownloadURL, getStorage, ref, uploadBytes, UploadResult } from "firebase/storage" +import { FirebaseStorage, getBytes, getDownloadURL, ref, uploadBytes, UploadResult } from "firebase/storage" import { readFileSync } from "fs" -import { FirebaseServices } from "../../types/index.js" -import { FIREBASE_ERRORS, showError } from "./errors.js" -import { readLocalJsonFile } from "./files.js" +import dotenv from "dotenv" +import { FirebaseServices } from "../../types/index" +import { FIREBASE_ERRORS, showError } from "./errors" -// Get local configs. -const { firebase } = readLocalJsonFile("../../env.json") +dotenv.config() /** Firebase App and services */ let firebaseApp: FirebaseApp @@ -43,18 +42,6 @@ const initializeFirebaseApp = (options: FirebaseOptions): FirebaseApp => initial */ const getFirestoreDatabase = (app: FirebaseApp): Firestore => getFirestore(app) -/** - * This method returns the Firestore storage instance associated to the given Firebase application. - * @param app <FirebaseApp> - the Firebase application. - * @returns <Firestore> - the Firebase Storage associated to the application. - */ -const getFirebaseStorage = (app: FirebaseApp): FirebaseStorage => { - if (app.options.storageBucket !== `${`${app.options.projectId}.appspot.com`}`) - showError(FIREBASE_ERRORS.FIREBASE_NOT_CONFIGURED_PROPERLY, true) - - return getStorage(app) -} - /** * This method returns the Cloud Functions instance associated to the given Firebase application. * @param app <FirebaseApp> - the Firebase application. @@ -67,35 +54,31 @@ const getFirebaseFunctions = (app: FirebaseApp): Functions => getFunctions(app) * @returns <Promise<FirebaseServices>> - the initialized Firebase services. */ export const initServices = async (): Promise<FirebaseServices> => { - if ( - !firebase.FIREBASE_API_KEY || - !firebase.FIREBASE_AUTH_DOMAIN || - !firebase.FIREBASE_PROJECT_ID || - !firebase.FIREBASE_STORAGE_BUCKET || - !firebase.FIREBASE_MESSAGING_SENDER_ID || - !firebase.FIREBASE_APP_ID || - !firebase.FIREBASE_CF_URL_VERIFY_CONTRIBUTION - ) - showError(FIREBASE_ERRORS.FIREBASE_NOT_CONFIGURED_PROPERLY, true) - - firebaseApp = initializeFirebaseApp({ - apiKey: firebase.FIREBASE_API_KEY, - authDomain: firebase.FIREBASE_AUTH_DOMAIN, - projectId: firebase.FIREBASE_PROJECT_ID, - storageBucket: firebase.FIREBASE_STORAGE_BUCKET, - messagingSenderId: firebase.FIREBASE_MESSAGING_SENDER_ID, - appId: firebase.FIREBASE_APP_ID - }) - firestoreDatabase = getFirestoreDatabase(firebaseApp) - firebaseStorage = getFirebaseStorage(firebaseApp) - firebaseFunctions = getFirebaseFunctions(firebaseApp) - - return { - firebaseApp, - firestoreDatabase, - firebaseStorage, - firebaseFunctions - } + if ( + !process.env.FIREBASE_API_KEY || + !process.env.FIREBASE_AUTH_DOMAIN || + !process.env.FIREBASE_PROJECT_ID || + !process.env.FIREBASE_MESSAGING_SENDER_ID || + !process.env.FIREBASE_APP_ID || + !process.env.FIREBASE_CF_URL_VERIFY_CONTRIBUTION + ) + showError(FIREBASE_ERRORS.FIREBASE_NOT_CONFIGURED_PROPERLY, true) + + firebaseApp = initializeFirebaseApp({ + apiKey: process.env.FIREBASE_API_KEY, + authDomain: process.env.FIREBASE_AUTH_DOMAIN, + projectId: process.env.FIREBASE_PROJECT_ID, + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.FIREBASE_APP_ID + }) + firestoreDatabase = getFirestoreDatabase(firebaseApp) + firebaseFunctions = getFirebaseFunctions(firebaseApp) + + return { + firebaseApp, + firestoreDatabase, + firebaseFunctions + } } /** @@ -105,12 +88,12 @@ export const initServices = async (): Promise<FirebaseServices> => { * @returns <Promise<DocumentSnapshot<DocumentData>>> - return the document from Firestore. */ export const getDocumentById = async ( - collection: string, - documentUID: string + collection: string, + documentUID: string ): Promise<DocumentSnapshot<DocumentData>> => { - const docRef = doc(firestoreDatabase, collection, documentUID) + const docRef = doc(firestoreDatabase, collection, documentUID) - return getDoc(docRef) + return getDoc(docRef) } /** @@ -120,14 +103,14 @@ export const getDocumentById = async ( * @returns <Promise<QuerySnapshot<DocumentData>>> - return the matching documents (if any). */ export const queryCollection = async ( - collection: string, - queryConstraints: Array<QueryConstraint> + collection: string, + queryConstraints: Array<QueryConstraint> ): Promise<QuerySnapshot<DocumentData>> => { - // Make a query. - const q = query(collectionRef(firestoreDatabase, collection), ...queryConstraints) + // Make a query. + const q = query(collectionRef(firestoreDatabase, collection), ...queryConstraints) - // Get docs. - return getDocs(q) + // Get docs. + return getDocs(q) } /** @@ -136,7 +119,7 @@ export const queryCollection = async ( * @returns <Promise<Array<QueryDocumentSnapshot<DocumentData>>>> - return all documents (if any). */ export const getAllCollectionDocs = async (collection: string): Promise<Array<QueryDocumentSnapshot<DocumentData>>> => - (await getDocs(collectionRef(firestoreDatabase, collection))).docs + (await getDocs(collectionRef(firestoreDatabase, collection))).docs /** * Download locally a zkey file from storage. @@ -144,11 +127,11 @@ export const getAllCollectionDocs = async (collection: string): Promise<Array<Qu * @returns <Promise<any>> */ export const downloadFileFromStorage = async (path: string): Promise<Buffer> => { - // Create a reference with folder path. - const pathReference = ref(firebaseStorage, path) + // Create a reference with folder path. + const pathReference = ref(firebaseStorage, path) - // Bufferized file content. - return Buffer.from(await getBytes(pathReference)) + // Bufferized file content. + return Buffer.from(await getBytes(pathReference)) } /** @@ -158,10 +141,10 @@ export const downloadFileFromStorage = async (path: string): Promise<Buffer> => * @returns <Promise<any>> */ export const uploadFileToStorage = async (localPath: string, storagePath: string): Promise<UploadResult> => { - // Create a reference with folder path. - const pathReference = ref(firebaseStorage, storagePath) + // Create a reference with folder path. + const pathReference = ref(firebaseStorage, storagePath) - return uploadBytes(pathReference, readFileSync(localPath)) + return uploadBytes(pathReference, readFileSync(localPath)) } /** @@ -171,17 +154,17 @@ export const uploadFileToStorage = async (localPath: string, storagePath: string * @returns */ export const checkIfStorageFileExists = async (pathToFile: string): Promise<boolean> => { - try { - // Get a reference. - const pathReference = ref(firebaseStorage, pathToFile) - - // Try to get url for download. - await getDownloadURL(pathReference) - - // Url for download exists (true). - return true - } catch (error) { - // Url does not exists (false). - return false - } + try { + // Get a reference. + const pathReference = ref(firebaseStorage, pathToFile) + + // Try to get url for download. + await getDownloadURL(pathReference) + + // Url for download exists (true). + return true + } catch (error) { + // Url does not exists (false). + return false + } } diff --git a/packages/phase2cli/src/lib/listeners.ts b/packages/phase2cli/src/lib/listeners.ts new file mode 100644 index 00000000..e10aa932 --- /dev/null +++ b/packages/phase2cli/src/lib/listeners.ts @@ -0,0 +1,595 @@ +import { DocumentData, DocumentSnapshot, Firestore, onSnapshot } from "firebase/firestore" +import { Functions, HttpsCallable, httpsCallable } from "firebase/functions" +import { getCeremonyCircuits } from "@zkmpc/actions" +import { FirebaseDocumentInfo, ParticipantContributionStep, ParticipantStatus } from "../../types/index" +import { collections, emojis, symbols, theme } from "./constants" +import { getCurrentContributorContribution } from "./queries" +import { + convertToDoubleDigits, + customSpinner, + formatZkeyIndex, + generatePublicAttestation, + getContributorContributionsVerificationResults, + getNextCircuitForContribution, + getParticipantCurrentDiskAvailableSpace, + getSecondsMinutesHoursFromMillis, + handleTimedoutMessageForContributor, + makeContribution, + simpleCountdown, + simpleLoader, + terminate +} from "./utils" +import { GENERIC_ERRORS, showError } from "./errors" +import { getDocumentById } from "./firebase" +import { askForConfirmation } from "./prompts" +import { convertToGB } from "./storage" + +/** + * Return the memory space requirement for a zkey in GB. + * @param zKeySizeInBytes <number> - the size of the zkey in bytes. + * @returns <number> + */ +const getZkeysSpaceRequirementsForContributionInGB = (zKeySizeInBytes: number): number => + // nb. mul per 2 is necessary because download latest + compute newest. + convertToGB(zKeySizeInBytes * 2, true) + +/** + * Return the available disk space of the current contributor in GB. + * @returns <number> + */ +const getContributorAvailableDiskSpaceInGB = (): number => convertToGB(getParticipantCurrentDiskAvailableSpace(), false) + +/** + * Check if the contributor has enough space before starting the contribution for next circuit. + * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. + * @param nextCircuit <FirebaseDocumentInfo> - the circuit document. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @return <Promise<void>> + */ +const handleDiskSpaceRequirementForNextContribution = async ( + cf: HttpsCallable<unknown, unknown>, + nextCircuit: FirebaseDocumentInfo, + ceremonyId: string +): Promise<boolean> => { + // Get memory info. + const zKeysSpaceRequirementsInGB = getZkeysSpaceRequirementsForContributionInGB(nextCircuit.data.zKeySizeInBytes) + const availableDiskSpaceInGB = getContributorAvailableDiskSpaceInGB() + + // Extract data. + const { sequencePosition } = nextCircuit.data + + process.stdout.write(`\n`) + + await simpleLoader(`Checking your memory...`, `clock`, 1000) + + // Check memory requirement. + if (availableDiskSpaceInGB < zKeysSpaceRequirementsInGB) { + console.log(theme.bold(`- Circuit # ${theme.magenta(`${sequencePosition}`)}`)) + + console.log( + `${symbols.error} You do not have enough memory to make a contribution (Required ${ + zKeysSpaceRequirementsInGB < 0.01 ? theme.bold(`< 0.01`) : theme.bold(zKeysSpaceRequirementsInGB) + } GB (available ${ + availableDiskSpaceInGB > 0 ? theme.bold(availableDiskSpaceInGB.toFixed(2)) : theme.bold(0) + } GB)\n` + ) + + if (sequencePosition > 1) { + // The user has computed at least one valid contribution. Therefore, can choose if free up memory and contrinue with next contribution or generate the final attestation. + console.log( + `${symbols.info} You have time until ceremony ends to free up your memory, complete contributions and publish the attestation` + ) + + const { confirmation } = await askForConfirmation( + `Are you sure you want to generate and publish the attestation for your contributions?` + ) + + if (!confirmation) { + process.stdout.write(`\n`) + + // nb. here the user is not able to generate an attestation because does not have contributed yet. Therefore, return an error and exit. + showError(`Please, free up your disk space and run again this command to contribute`, true) + } + } else { + // nb. here the user is not able to generate an attestation because does not have contributed yet. Therefore, return an error and exit. + showError(`Please, free up your disk space and run again this command to contribute`, true) + } + } else { + console.log( + `${symbols.success} You have enough memory for contributing to ${theme.bold( + `Circuit ${theme.magenta(sequencePosition)}` + )}` + ) + + const spinner = customSpinner( + `Joining ${theme.bold(`Circuit ${theme.magenta(sequencePosition)}`)} waiting queue...`, + `clock` + ) + spinner.start() + + await cf({ ceremonyId }) + + spinner.succeed(`All set for contribution to ${theme.bold(`Circuit ${theme.magenta(sequencePosition)}`)}`) + + return false + } + + return true +} + +/** + * Return the index of a given participant in a circuit waiting queue. + * @param contributors <Array<string>> - the list of the contributors in queue for a circuit. + * @param participantId <string> - the unique identifier of the participant. + * @returns <number> + */ +const getParticipantPositionInQueue = (contributors: Array<string>, participantId: string): number => + contributors.indexOf(participantId) + 1 + +/** + * Listen to circuit document changes and reacts in realtime. + * @param participantId <string> - the unique identifier of the contributor. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param circuit <FirebaseDocumentInfo> - the document information about the current circuit. + */ +const listenToCircuitChanges = (participantId: string, ceremonyId: string, circuit: FirebaseDocumentInfo) => { + const unsubscriberForCircuitDocument = onSnapshot(circuit.ref, async (circuitDocSnap: DocumentSnapshot) => { + // Get updated data from snap. + const newCircuitData = circuitDocSnap.data() + + if (!newCircuitData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Get data. + const { avgTimings, waitingQueue } = newCircuitData! + const { fullContribution, verifyCloudFunction } = avgTimings + const { currentContributor, completedContributions } = waitingQueue + + // Retrieve current contributor data. + const currentContributorDoc = await getDocumentById( + `${collections.ceremonies}/${ceremonyId}/${collections.participants}`, + currentContributor + ) + + // Get updated data from snap. + const currentContributorData = currentContributorDoc.data() + + if (!currentContributorData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Get updated position for contributor in the queue. + const newParticipantPositionInQueue = getParticipantPositionInQueue(waitingQueue.contributors, participantId) + + let newEstimatedWaitingTime = 0 + + // Show new time estimation. + if (fullContribution > 0 && verifyCloudFunction > 0) + newEstimatedWaitingTime = (fullContribution + verifyCloudFunction) * (newParticipantPositionInQueue - 1) + + const { + seconds: estSeconds, + minutes: estMinutes, + hours: estHours + } = getSecondsMinutesHoursFromMillis(newEstimatedWaitingTime) + + // Check if is the current contributor. + if (newParticipantPositionInQueue === 1) { + console.log( + `\n${symbols.success} Your turn has come ${emojis.tada}\n${symbols.info} Your contribution will begin soon` + ) + unsubscriberForCircuitDocument() + } else { + // Position and time. + console.log( + `\n${symbols.info} ${ + newParticipantPositionInQueue === 2 + ? `You are the next contributor` + : `Your position in the waiting queue is ${theme.bold( + theme.magenta(newParticipantPositionInQueue - 1) + )}` + } (${ + newEstimatedWaitingTime > 0 + ? `${theme.bold( + `${convertToDoubleDigits(estHours)}:${convertToDoubleDigits( + estMinutes + )}:${convertToDoubleDigits(estSeconds)}` + )} left before your turn)` + : `no time estimation)` + }` + ) + + // Participant data. + console.log(` - Contributor # ${theme.bold(theme.magenta(completedContributions + 1))}`) + + // Data for displaying info about steps. + const currentZkeyIndex = formatZkeyIndex(completedContributions) + const nextZkeyIndex = formatZkeyIndex(completedContributions + 1) + + let interval: NodeJS.Timer + + const unsubscriberForCurrentContributorDocument = onSnapshot( + currentContributorDoc.ref, + async (currentContributorDocSnap: DocumentSnapshot) => { + // Get updated data from snap. + const newCurrentContributorData = currentContributorDocSnap.data() + + if (!newCurrentContributorData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Get current contributor data. + const { contributionStep, contributionStartedAt } = newCurrentContributorData! + + // Average time. + const timeSpentWhileContributing = Date.now() - contributionStartedAt + const remainingTime = fullContribution - timeSpentWhileContributing + + // Clear previous step interval (if exist). + if (interval) clearInterval(interval) + + switch (contributionStep) { + case ParticipantContributionStep.DOWNLOADING: { + const message = ` ${symbols.info} Downloading contribution ${theme.bold( + `#${currentZkeyIndex}` + )}` + interval = simpleCountdown(remainingTime, message) + + break + } + case ParticipantContributionStep.COMPUTING: { + process.stdout.write( + ` ${symbols.success} Contribution ${theme.bold( + `#${currentZkeyIndex}` + )} correctly downloaded\n` + ) + + const message = ` ${symbols.info} Computing contribution ${theme.bold( + `#${nextZkeyIndex}` + )}` + interval = simpleCountdown(remainingTime, message) + + break + } + case ParticipantContributionStep.UPLOADING: { + process.stdout.write( + ` ${symbols.success} Contribution ${theme.bold( + `#${nextZkeyIndex}` + )} successfully computed\n` + ) + + const message = ` ${symbols.info} Uploading contribution ${theme.bold( + `#${nextZkeyIndex}` + )}` + interval = simpleCountdown(remainingTime, message) + + break + } + case ParticipantContributionStep.VERIFYING: { + process.stdout.write( + ` ${symbols.success} Contribution ${theme.bold( + `#${nextZkeyIndex}` + )} successfully uploaded\n` + ) + + const message = ` ${symbols.info} Contribution verification ${theme.bold( + `#${nextZkeyIndex}` + )}` + interval = simpleCountdown(remainingTime, message) + + break + } + case ParticipantContributionStep.COMPLETED: { + process.stdout.write( + ` ${symbols.success} Contribution ${theme.bold( + `#${nextZkeyIndex}` + )} has been correctly verified\n` + ) + + const currentContributorContributions = await getCurrentContributorContribution( + ceremonyId, + circuit.id, + currentContributorDocSnap.id + ) + + if (currentContributorContributions.length !== 1) + process.stdout.write(` ${symbols.error} We could not recover the contribution data`) + else { + const contribution = currentContributorContributions.at(0) + + const data = contribution?.data + + console.log( + ` ${data?.valid ? symbols.success : symbols.error} Contribution ${theme.bold( + `#${nextZkeyIndex}` + )} is ${data?.valid ? `VALID` : `INVALID`}` + ) + } + + unsubscriberForCurrentContributorDocument() + break + } + default: { + showError(`Wrong contribution step`, true) + break + } + } + } + ) + } + }) +} + +// Listen to changes on the user-related participant document. +export default async ( + participantDoc: DocumentSnapshot<DocumentData>, + ceremony: FirebaseDocumentInfo, + firestoreDatabase: Firestore, + circuits: Array<FirebaseDocumentInfo>, + firebaseFunctions: Functions, + ghToken: string, + ghUsername: string, + entropy: string +) => { + // Get number of circuits for the selected ceremony. + const numberOfCircuits = circuits.length + + // Listen to participant document changes. + const unsubscriberForParticipantDocument = onSnapshot( + participantDoc.ref, + async (participantDocSnap: DocumentSnapshot) => { + // Get updated data from snap. + const newParticipantData = participantDocSnap.data() + const oldParticipantData = participantDoc.data() + + if (!newParticipantData || !oldParticipantData) + showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Extract updated participant document data. + const { contributionProgress, status, contributionStep, contributions, tempContributionData } = + newParticipantData! + const { + contributionStep: oldContributionStep, + tempContributionData: oldTempContributionData, + contributionProgress: oldContributionProgress, + contributions: oldContributions, + status: oldStatus + } = oldParticipantData! + const participantId = participantDoc.id + + // 0. Whem joining for the first time the waiting queue. + if ( + status === ParticipantStatus.WAITING && + !contributionStep && + !contributions.length && + contributionProgress === 0 + ) { + // Get next circuit. + const nextCircuit = getNextCircuitForContribution(circuits, contributionProgress + 1) + + // Check disk space requirements for participant. + const makeProgressToNextContribution = httpsCallable( + firebaseFunctions, + "makeProgressToNextContribution" + ) + await handleDiskSpaceRequirementForNextContribution( + makeProgressToNextContribution, + nextCircuit, + ceremony.id + ) + } + + // A. Do not have completed the contributions for each circuit; move to the next one. + if (contributionProgress > 0 && contributionProgress <= circuits.length) { + // Get updated circuits data. + const updatedCircuits = await getCeremonyCircuits(firestoreDatabase, ceremony.id) + const circuit = updatedCircuits[contributionProgress - 1] + const { waitingQueue } = circuit.data + + // Check if the contribution step is valid for starting/resuming the contribution. + const isStepValidForStartingOrResumingContribution = + (contributionStep === ParticipantContributionStep.DOWNLOADING && + status === ParticipantStatus.CONTRIBUTING && + (!oldContributionStep || + oldContributionStep !== contributionStep || + (oldContributionStep === contributionStep && + status === oldStatus && + oldContributionProgress === contributionProgress) || + oldStatus === ParticipantStatus.EXHUMED)) || + (contributionStep === ParticipantContributionStep.COMPUTING && + oldContributionStep === contributionStep && + oldContributions.length === contributions.length) || + (contributionStep === ParticipantContributionStep.UPLOADING && + !oldTempContributionData && + !tempContributionData && + contributionStep === oldContributionStep) || + (!!oldTempContributionData && + !!tempContributionData && + JSON.stringify(Object.keys(oldTempContributionData).sort()) === + JSON.stringify(Object.keys(tempContributionData).sort()) && + JSON.stringify(Object.values(oldTempContributionData).sort()) === + JSON.stringify(Object.values(tempContributionData).sort())) + + // A.1 If the participant is in `waiting` status, he/she must receive updates from the circuit's waiting queue. + if (status === ParticipantStatus.WAITING && oldStatus !== ParticipantStatus.TIMEDOUT) { + console.log( + `${theme.bold( + `\n- Circuit # ${theme.magenta(`${circuit.data.sequencePosition}`)}` + )} (Waiting Queue)` + ) + + listenToCircuitChanges(participantId, ceremony.id, circuit) + } + // A.2 If the participant is in `contributing` status and is the current contributor, he/she must compute the contribution. + if ( + status === ParticipantStatus.CONTRIBUTING && + contributionStep !== ParticipantContributionStep.VERIFYING && + waitingQueue.currentContributor === participantId && + isStepValidForStartingOrResumingContribution + ) { + console.log( + `\n${symbols.success} Your contribution will ${ + contributionStep === ParticipantContributionStep.DOWNLOADING ? `start` : `resume` + } soon ${emojis.clock}` + ) + + // Compute the contribution. + await makeContribution( + ceremony, + circuit, + entropy, + ghUsername, + false, + firebaseFunctions, + newParticipantData! + ) + } + + // A.3 Current contributor has already started the verification step. + if ( + status === ParticipantStatus.CONTRIBUTING && + waitingQueue.currentContributor === participantId && + contributionStep === oldContributionStep && + contributionStep === ParticipantContributionStep.VERIFYING && + contributionProgress === oldContributionProgress + ) { + const spinner = customSpinner(`Resuming your contribution...`, `clock`) + spinner.start() + + // Get current and next index. + const currentZkeyIndex = formatZkeyIndex(contributionProgress) + const nextZkeyIndex = formatZkeyIndex(contributionProgress + 1) + + // Calculate remaining est. time for verification. + const avgVerifyCloudFunctionTime = circuit.data.avgTimings.verifyCloudFunction + const verificationStartedAt = newParticipantData?.verificationStartedAt + const estRemainingTimeInMillis = avgVerifyCloudFunctionTime - (Date.now() - verificationStartedAt) + const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(estRemainingTimeInMillis) + + spinner.succeed(`Your contribution will resume soon ${emojis.clock}`) + + console.log( + `${theme.bold( + `\n- Circuit # ${theme.magenta(`${circuit.data.sequencePosition}`)}` + )} (Contribution Steps)` + ) + console.log( + `${symbols.success} Contribution ${theme.bold(`#${currentZkeyIndex}`)} already downloaded` + ) + console.log(`${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} already computed`) + console.log( + `${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} already saved on storage` + ) + console.log( + `${symbols.info} Contribution verification already started (est. time ${theme.bold( + `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits( + seconds + )}` + )})` + ) + } + + // A.4 Server has terminated the already started verification step above. + if ( + ((status === ParticipantStatus.DONE && oldStatus === ParticipantStatus.DONE) || + (status === ParticipantStatus.CONTRIBUTED && oldStatus === ParticipantStatus.CONTRIBUTED)) && + oldContributionProgress === contributionProgress - 1 && + contributionStep === ParticipantContributionStep.COMPLETED + ) { + console.log(`\n${symbols.success} Contribute verification has been completed`) + + // Return true and false based on contribution verification. + const contributionsValidity = await getContributorContributionsVerificationResults( + ceremony.id, + participantDoc.id, + updatedCircuits, + false + ) + + // Check last contribution validity. + const isContributionValid = contributionsValidity[oldContributionProgress - 1] + + console.log( + `${isContributionValid ? symbols.success : symbols.error} Your contribution ${ + isContributionValid ? `is ${theme.bold("VALID")}` : `is ${theme.bold("INVALID")}` + }` + ) + } + + // A.5 Current contributor timedout. + if (status === ParticipantStatus.TIMEDOUT && contributionStep !== ParticipantContributionStep.COMPLETED) + await handleTimedoutMessageForContributor( + newParticipantData!, + participantDoc.id, + ceremony.id, + true, + ghUsername + ) + + // A.6 Contributor has finished the contribution and we need to check the memory before progressing. + if ( + status === ParticipantStatus.CONTRIBUTED && + contributionStep === ParticipantContributionStep.COMPLETED + ) { + // Get next circuit for contribution. + const nextCircuit = getNextCircuitForContribution(updatedCircuits, contributionProgress + 1) + + // Check disk space requirements for participant. + const makeProgressToNextContribution = httpsCallable( + firebaseFunctions, + "makeProgressToNextContribution" + ) + const wannaGenerateAttestation = await handleDiskSpaceRequirementForNextContribution( + makeProgressToNextContribution, + nextCircuit, + ceremony.id + ) + + if (wannaGenerateAttestation) { + // Generate attestation with valid contributions. + await generatePublicAttestation( + ceremony, + participantId, + newParticipantData!, + updatedCircuits, + ghUsername, + ghToken + ) + + unsubscriberForParticipantDocument() + terminate(ghUsername) + } + } + + // A.7 If the participant is in `EXHUMED` status can be only after a timeout expiration. + if (status === ParticipantStatus.EXHUMED) { + // Check disk space requirements for participant before resuming the contribution. + const resumeContributionAfterTimeoutExpiration = httpsCallable( + firebaseFunctions, + "resumeContributionAfterTimeoutExpiration" + ) + await handleDiskSpaceRequirementForNextContribution( + resumeContributionAfterTimeoutExpiration, + circuit, + ceremony.id + ) + } + + // B. Already contributed to each circuit. + if ( + status === ParticipantStatus.DONE && + contributionStep === ParticipantContributionStep.COMPLETED && + contributionProgress === numberOfCircuits && + contributions.length === numberOfCircuits + ) { + await generatePublicAttestation( + ceremony, + participantId, + newParticipantData!, + updatedCircuits, + ghUsername, + ghToken + ) + + unsubscriberForParticipantDocument() + terminate(ghUsername) + } + } + } + ) +} diff --git a/packages/phase2cli/src/lib/prompts.ts b/packages/phase2cli/src/lib/prompts.ts new file mode 100644 index 00000000..65d63843 --- /dev/null +++ b/packages/phase2cli/src/lib/prompts.ts @@ -0,0 +1,582 @@ +import { Dirent } from "fs" +import prompts, { Answers, Choice, PromptObject } from "prompts" +import { + CeremonyInputData, + CeremonyTimeoutType, + CircomCompilerData, + CircuitInputData, + FirebaseDocumentInfo +} from "../../types/index" +import { symbols, theme } from "./constants" +import { GENERIC_ERRORS, showError } from "./errors" +import { customSpinner, extractPoTFromFilename, extractPrefix, getCreatedCeremoniesPrefixes } from "./utils" + +/** + * Show a binary question with custom options for confirmation purposes. + * @param question <string> - the question to be answered. + * @param active <string> - the active option (= yes). + * @param inactive <string> - the inactive option (= no). + * @returns <Promise<Answers<string>>> + */ +export const askForConfirmation = async (question: string, active = "yes", inactive = "no"): Promise<Answers<string>> => + prompts({ + type: "toggle", + name: "confirmation", + message: theme.bold(question), + initial: false, + active, + inactive + }) + +/** + * Prompt for entropy or beacon. + * @param askEntropy <boolean> - true when requesting entropy; otherwise false. + * @returns <Promise<string>> + */ +export const askForEntropyOrBeacon = async (askEntropy: boolean): Promise<string> => { + const { entropyOrBeacon } = await prompts({ + type: "text", + name: "entropyOrBeacon", + style: `${askEntropy ? `password` : `text`}`, + message: theme.bold(`Provide ${askEntropy ? `some entropy` : `the final beacon`}`), + validate: (title: string) => + title.length > 0 || + theme.red(`${symbols.error} You must provide a valid value for the ${askEntropy ? `entropy` : `beacon`}!`) + }) + + if (!entropyOrBeacon) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + return entropyOrBeacon +} + +/** + * Handle the request/generation for a random entropy or beacon value. + * @param askEntropy <boolean> - true when requesting entropy; otherwise false. + * @return <Promise<string>> + */ +export const getEntropyOrBeacon = async (askEntropy: boolean): Promise<string> => { + let entropyOrBeacon: any + let randomEntropy = false + + if (askEntropy) { + // Prompt for entropy. + const { confirmation } = await askForConfirmation(`Do you prefer to enter entropy manually?`) + if (confirmation === undefined) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + randomEntropy = !confirmation + } + + if (randomEntropy) { + const spinner = customSpinner(`Generating random entropy...`, "clock") + spinner.start() + + // Took inspiration from here https://github.com/glamperd/setup-mpc-ui/blob/master/client/src/state/Compute.tsx#L112. + entropyOrBeacon = new Uint8Array(256).map(() => Math.random() * 256).toString() + + spinner.succeed(`Random entropy successfully generated`) + } + + if (!askEntropy || !randomEntropy) entropyOrBeacon = await askForEntropyOrBeacon(askEntropy) + + return entropyOrBeacon +} + +/** + * Show a series of questions about the ceremony. + * @returns <Promise<CeremonyInputData>> - the necessary information for the ceremony entered by the coordinator. + */ +export const askCeremonyInputData = async (): Promise<CeremonyInputData> => { + // Get ceremonies prefixes to check for duplicates. + const ceremoniesPrefixes = await getCreatedCeremoniesPrefixes() + + 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. + */ +export const askCircomCompilerVersionAndCommitHash = async (): Promise<CircomCompilerData> => { + const questions: Array<PromptObject> = [ + { + type: "text", + name: "version", + message: theme.bold(`Give the circom compiler version`), + validate: (version: string) => { + if (version.length <= 0) + return theme.red(`${symbols.error} You must provide a valid version (e.g., 2.0.1)`) + + if (!version.match(/^[0-9].[0-9.]*$/)) + return theme.red(`${symbols.error} You must provide a valid version (e.g., 2.0.1)`) + + return true + } + }, + { + type: "text", + name: "commitHash", + message: theme.bold(`Give the commit hash of the circom compiler version`), + validate: (commitHash: string) => + commitHash.length === 40 || + theme.red( + `${symbols.error} You must provide a valid commit hash (e.g., b7ad01b11f9b4195e38ecc772291251260ab2c67)` + ) + } + ] + + const { version, commitHash } = await prompts(questions) + + if (!version || !commitHash) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + return { + version, + commitHash + } +} + +/** + * Show a series of questions about the circuits. + * @param timeoutMechanismType <CeremonyTimeoutType> - the choosen timeout mechanism type for the ceremony. + * @param isCircomVersionDifferentAmongCircuits <boolean> - true if the circom compiler version is equal among circuits; otherwise false. + * @returns Promise<Array<Circuit>> - the necessary information for the circuits entered by the coordinator. + */ +export const askCircuitInputData = async ( + timeoutMechanismType: CeremonyTimeoutType, + isCircomVersionEqualAmongCircuits: boolean +): Promise<CircuitInputData> => { + const circuitQuestions: Array<PromptObject> = [ + { + name: "description", + type: "text", + message: theme.bold(`Add a description`), + validate: (value) => + value.length ? true : theme.red(`${symbols.error} You must provide a valid description`) + }, + { + name: "templateSource", + type: "text", + message: theme.bold(`Give the external reference to the source template (.circom file)`), + validate: (value) => + value.length > 0 && value.match(/(https?:\/\/[^\s]+\.circom$)/g) + ? true + : theme.red(`${symbols.error} You must provide a valid link to the .circom source template`) + }, + { + name: "templateCommitHash", + type: "text", + message: theme.bold(`Give the commit hash of the source template`), + validate: (commitHash: string) => + commitHash.length === 40 || + theme.red( + `${symbols.error} You must provide a valid commit hash (e.g., b7ad01b11f9b4195e38ecc772291251260ab2c67)` + ) + } + ] + + // Prompt for circuit data. + const { description, templateSource, templateCommitHash } = await prompts(circuitQuestions) + + if (!description || !templateSource || !templateCommitHash) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + // Ask for dynamic or fixed data. + let paramsConfiguration: Array<string> = [] + let timeoutThreshold = 0 + let timeoutMaxContributionWaitingTime = 0 + let circomVersion = "" + let circomCommitHash = "" + + // Ask for params config values (if any). + const { confirmation: needConfiguration } = await askForConfirmation( + `Did the source template need configuration?`, + `Yes`, + `No` + ) + + if (needConfiguration) { + const { templateParamsValues } = await prompts({ + name: "templateParamsValues", + type: "text", + message: theme.bold( + `Please, provide a comma-separated list of the parameters values used for configuration` + ), + validate: (value: string) => + value.split(",").length === 1 || + (value.split(`,`).length > 1 && value.includes(",")) || + theme.red( + `${symbols.error} You must provide a valid comma-separated list of parameters values (e.g., 10,2,1,2)` + ) + }) + + if (!templateParamsValues) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + paramsConfiguration = templateParamsValues.split(",") + } + + // Ask for circom info (if different from other circuits). + if (!isCircomVersionEqualAmongCircuits) { + const { version, commitHash } = await askCircomCompilerVersionAndCommitHash() + + if (!version || !commitHash) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + circomVersion = version + circomCommitHash = commitHash + } + + // Ask for dynamic timeout mechanism data. + if (timeoutMechanismType === CeremonyTimeoutType.DYNAMIC) { + const { threshold } = await prompts({ + type: "number", + name: "threshold", + message: theme.bold( + `Provide an additional threshold up to the total average contribution time (in percentage):` + ), + validate: (thresh: number) => { + if (thresh < 0 || thresh > 100) + return theme.red(`${symbols.error} You must provide a threshold between 0 and 100`) + + return true + } + }) + + if (threshold < 0 || threshold > 100) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + timeoutThreshold = threshold + } + + // Ask for fixed timeout mechanism data. + if (timeoutMechanismType === CeremonyTimeoutType.FIXED) { + const { maxContributionWaitingTime } = await prompts({ + type: "number", + name: `maxContributionWaitingTime`, + message: theme.bold(`Specify the max amount of time tolerable while contributing (in minutes):`), + validate: (threshold: number) => { + if (threshold <= 0) + return theme.red( + `${symbols.error} You must provide a maximum contribution waiting time greater than zero` + ) + + return true + } + }) + + if (maxContributionWaitingTime <= 0) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + timeoutMaxContributionWaitingTime = maxContributionWaitingTime + } + + if ( + (timeoutMechanismType === CeremonyTimeoutType.DYNAMIC && timeoutThreshold < 0) || + (timeoutMechanismType === CeremonyTimeoutType.FIXED && timeoutMaxContributionWaitingTime < 0) || + (needConfiguration && paramsConfiguration.length === 0) || + (isCircomVersionEqualAmongCircuits && !!circomVersion && !!circomCommitHash) + ) + showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + return timeoutMechanismType === CeremonyTimeoutType.DYNAMIC + ? { + description, + timeoutThreshold, + compiler: { + version: circomVersion, + commitHash: circomCommitHash + }, + template: { + source: templateSource, + commitHash: templateCommitHash, + paramsConfiguration + } + } + : { + description, + timeoutMaxContributionWaitingTime, + compiler: { + version: circomVersion, + commitHash: circomCommitHash + }, + template: { + source: templateSource, + commitHash: templateCommitHash, + paramsConfiguration + } + } +} + +/** + * Request the powers of the Powers of Tau for a specified circuit. + * @param suggestedPowers <number> - the minimal number of powers necessary for circuit zKey generation. + * @returns Promise<Array<Circuit>> - the necessary information for the circuits entered by the coordinator. + */ +export const askPowersOftau = async (suggestedPowers: number): Promise<any> => { + const question: PromptObject = { + name: "powers", + type: "number", + message: theme.bold( + `Please, provide the amounts of powers you have used to generate the pre-computed zkey (>= ${suggestedPowers}):` + ), + validate: (value) => + value >= suggestedPowers + ? true + : theme.red(`${symbols.error} You must provide a value greater than or equal to ${suggestedPowers}`) + } + + // Prompt for circuit data. + const { powers } = await prompts(question) + + if (powers < suggestedPowers) showError(GENERIC_ERRORS.GENERIC_DATA_INPUT, true) + + return { + powers + } +} + +/** + * Prompt the list of circuits from a specific directory. + * @param circuitsDirents <Array<Dirent>> + * @returns Promise<string> + */ +export const askForCircuitSelectionFromLocalDir = async (circuitsDirents: Array<Dirent>): Promise<string> => { + const choices: Array<Choice> = [] + + // Make a 'Choice' for each circuit. + for (const circuitDirent of circuitsDirents) { + choices.push({ + title: circuitDirent.name, + value: circuitDirent.name + }) + } + + // Ask for selection. + const { circuit } = await prompts({ + type: "select", + name: "circuit", + message: theme.bold("Select a circuit"), + choices, + initial: 0 + }) + + if (!circuit) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) + + return circuit +} + +/** + * Prompt the list of pre-computed zkeys files from a specific directory. + * @param zkeysDirents <Array<Dirent>> + * @returns Promise<string> + */ +export const askForZkeySelectionFromLocalDir = async (zkeysDirents: Array<Dirent>): Promise<string> => { + const choices: Array<Choice> = [] + + // Make a 'Choice' for each zkey. + for (const zkeyDirent of zkeysDirents) { + choices.push({ + title: zkeyDirent.name, + value: zkeyDirent.name + }) + } + + // Ask for selection. + const { zkey } = await prompts({ + type: "select", + name: "zkey", + message: theme.bold("Select a pre-computed zkey"), + choices, + initial: 0 + }) + + if (!zkey) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) + + return zkey +} + +/** + * Prompt the list of ptau files from a specific directory. + * @param ptausDirents <Array<Dirent>> + * @param suggestedPowers <number> - the minimal number of powers necessary for circuit zKey generation. + * @returns Promise<string> + */ +export const askForPtauSelectionFromLocalDir = async ( + ptausDirents: Array<Dirent>, + suggestedPowers: number +): Promise<string> => { + const choices: Array<Choice> = [] + + // Make a 'Choice' for each ptau. + for (const ptauDirent of ptausDirents) { + const powers = extractPoTFromFilename(ptauDirent.name) + + if (powers >= suggestedPowers) + choices.push({ + title: ptauDirent.name, + value: ptauDirent.name + }) + } + + // Ask for selection. + const { ptau } = await prompts({ + type: "select", + name: "ptau", + message: theme.bold("Select the Powers of Tau file used to generate the zKey"), + choices, + initial: 0, + validate: (value) => + extractPoTFromFilename(value) >= suggestedPowers + ? true + : theme.red( + `${symbols.error} You must select a Powers of Tau file having an equal to or greater than ${suggestedPowers} amount of powers` + ) + }) + + if (!ptau) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) + + return ptau +} + +/** + * Prompt the list of opened ceremonies for selection. + * @param openedCeremoniesDocs <Array<FirebaseDocumentInfo>> - The uid and data of opened cerimonies documents. + * @returns Promise<FirebaseDocumentInfo> + */ +export const askForCeremonySelection = async ( + openedCeremoniesDocs: Array<FirebaseDocumentInfo> +): Promise<FirebaseDocumentInfo> => { + const choices: Array<Choice> = [] + + // Make a 'Choice' for each opened ceremony. + for (const ceremonyDoc of openedCeremoniesDocs) { + const now = Date.now() + const daysLeft = Math.ceil(Math.abs(now - ceremonyDoc.data.endDate) / (1000 * 60 * 60 * 24)) + + choices.push({ + title: ceremonyDoc.data.title, + description: `${ceremonyDoc.data.description} (${theme.magenta(daysLeft)} ${ + now - ceremonyDoc.data.endDate < 0 ? `days left` : `days gone since closing` + })`, + value: ceremonyDoc + }) + } + + // Ask for selection. + const { ceremony } = await prompts({ + type: "select", + name: "ceremony", + message: theme.bold("Select a ceremony"), + choices, + initial: 0 + }) + + if (!ceremony) showError(GENERIC_ERRORS.GENERIC_CEREMONY_SELECTION, true) + + return ceremony +} + +/** + * Prompt the list of circuits for a specific ceremony for selection. + * @param circuitsDocs <Array<FirebaseDocumentInfo>> - The uid and data of ceremony circuits. + * @returns Promise<FirebaseDocumentInfo> + */ +export const askForCircuitSelectionFromFirebase = async ( + circuitsDocs: Array<FirebaseDocumentInfo> +): Promise<FirebaseDocumentInfo> => { + const choices: Array<Choice> = [] + + // Make a 'Choice' for each circuit. + for (const circuitDoc of circuitsDocs) { + choices.push({ + title: `${circuitDoc.data.name}`, + description: `(#${theme.magenta(circuitDoc.data.sequencePosition)}) ${circuitDoc.data.description}`, + value: circuitDoc + }) + } + + // Ask for selection. + const { circuit } = await prompts({ + type: "select", + name: "circuit", + message: theme.bold("Select a circuit"), + choices, + initial: 0 + }) + + if (!circuit) showError(GENERIC_ERRORS.GENERIC_CIRCUIT_SELECTION, true) + + return circuit +} diff --git a/packages/phase2cli/src/lib/queries.ts b/packages/phase2cli/src/lib/queries.ts new file mode 100644 index 00000000..e0126e68 --- /dev/null +++ b/packages/phase2cli/src/lib/queries.ts @@ -0,0 +1,119 @@ +import { DocumentData, QueryDocumentSnapshot, Timestamp, where } from "firebase/firestore" +import { FirebaseDocumentInfo, CeremonyState } from "../../types/index" +import { queryCollection, getAllCollectionDocs } from "./firebase" +import { + ceremoniesCollectionFields, + collections, + contributionsCollectionFields, + timeoutsCollectionFields +} from "./constants" +import { FIREBASE_ERRORS, showError } from "./errors" + +/** + * Helper for obtaining uid and data for query document snapshots. + * @param queryDocSnap <Array<QueryDocumentSnapshot>> - the array of query document snapshot to be converted. + * @returns Array<FirebaseDocumentInfo> + */ +export const fromQueryToFirebaseDocumentInfo = ( + queryDocSnap: Array<QueryDocumentSnapshot> +): Array<FirebaseDocumentInfo> => + queryDocSnap.map((doc: QueryDocumentSnapshot<DocumentData>) => ({ + id: doc.id, + ref: doc.ref, + data: doc.data() + })) + +/** + * Query for closed ceremonies documents and return their data (if any). + * @returns <Promise<Array<FirebaseDocumentInfo>>> + */ +export const getClosedCeremonies = async (): Promise<Array<FirebaseDocumentInfo>> => { + let closedStateCeremoniesQuerySnap: any + + try { + closedStateCeremoniesQuerySnap = await queryCollection(collections.ceremonies, [ + where(ceremoniesCollectionFields.state, "==", CeremonyState.CLOSED), + where(ceremoniesCollectionFields.endDate, "<=", Date.now()) + ]) + + if (closedStateCeremoniesQuerySnap.empty && closedStateCeremoniesQuerySnap.size === 0) + showError(FIREBASE_ERRORS.FIREBASE_CEREMONY_NOT_CLOSED, true) + } catch (err: any) { + showError(err.toString(), true) + } + + return fromQueryToFirebaseDocumentInfo(closedStateCeremoniesQuerySnap.docs) +} + +/** + * Retrieve all ceremonies. + * @returns Promise<Array<FirebaseDocumentInfo>> + */ +export const getAllCeremonies = async (): Promise<Array<FirebaseDocumentInfo>> => + fromQueryToFirebaseDocumentInfo(await getAllCollectionDocs(`${collections.ceremonies}`)).sort( + (a: FirebaseDocumentInfo, b: FirebaseDocumentInfo) => a.data.sequencePosition - b.data.sequencePosition + ) + +/** + * Query for contribution from given participant for a given circuit (if any). + * @param ceremonyId <string> - the identifier of the ceremony. + * @param circuitId <string> - the identifier of the circuit. + * @param participantId <string> - the identifier of the participant. + * @returns <Promise<Array<FirebaseDocumentInfo>>> + */ +export const getCurrentContributorContribution = async ( + ceremonyId: string, + circuitId: string, + participantId: string +): Promise<Array<FirebaseDocumentInfo>> => { + const participantContributionQuerySnap = await queryCollection( + `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuitId}/${collections.contributions}`, + [where(contributionsCollectionFields.participantId, "==", participantId)] + ) + + return fromQueryToFirebaseDocumentInfo(participantContributionQuerySnap.docs) +} + +/** + * Query for circuits with a contribution from given participant. + * @param ceremonyId <string> - the identifier of the ceremony. + * @param circuits <Array<FirebaseDocumentInfo>> - the circuits of the ceremony + * @param participantId <string> - the identifier of the participant. + * @returns <Promise<Array<FirebaseDocumentInfo>>> + */ +export const getCircuitsWithParticipantContribution = async ( + ceremonyId: string, + circuits: Array<FirebaseDocumentInfo>, + participantId: string +): Promise<Array<string>> => { + const circuitsWithContributionIds: Array<string> = [] // nb. store circuit identifier. + + for (const circuit of circuits) { + const participantContributionQuerySnap = await queryCollection( + `${collections.ceremonies}/${ceremonyId}/${collections.circuits}/${circuit.id}/${collections.contributions}`, + [where(contributionsCollectionFields.participantId, "==", participantId)] + ) + + if (participantContributionQuerySnap.size === 1) circuitsWithContributionIds.push(circuit.id) + } + + return circuitsWithContributionIds +} + +/** + * Query for the active timeout from given participant for a given ceremony (if any). + * @param ceremonyId <string> - the identifier of the ceremony. + * @param participantId <string> - the identifier of the participant. + * @returns Promise<Array<FirebaseDocumentInfo>> + */ +export const getCurrentActiveParticipantTimeout = async ( + ceremonyId: string, + participantId: string +): Promise<Array<FirebaseDocumentInfo>> => { + const participantTimeoutQuerySnap = await queryCollection( + `${collections.ceremonies}/${ceremonyId}/${collections.participants}/${participantId}/${collections.timeouts}`, + [where(timeoutsCollectionFields.endDate, ">=", Timestamp.now().toMillis())] + ) + + return fromQueryToFirebaseDocumentInfo(participantTimeoutQuerySnap.docs) +} diff --git a/packages/phase2cli/src/lib/storage.ts b/packages/phase2cli/src/lib/storage.ts new file mode 100644 index 00000000..e03b771c --- /dev/null +++ b/packages/phase2cli/src/lib/storage.ts @@ -0,0 +1,325 @@ +import { HttpsCallable } from "firebase/functions" +import fs from "fs" +import fetch from "@adobe/node-fetch-retry" +import { createWriteStream } from "node:fs" +import https from "https" +import dotenv from "dotenv" +import { SingleBar, Presets } from "cli-progress" +import { ChunkWithUrl, ETagWithPartNumber, ProgressBarType } from "../../types/index" +import { GENERIC_ERRORS, showError } from "./errors" +import { emojis, theme } from "./constants" + +dotenv.config() + +/** + * Return a custom progress bar. + * @param type <ProgressBarType> - the type of the progress bar. + * @returns <SingleBar> - a new custom (single) progress bar. + */ +export const customProgressBar = (type: ProgressBarType): SingleBar => { + // Formats. + const uploadFormat = `${emojis.arrowUp} Uploading [${theme.magenta( + "{bar}" + )}] {percentage}% | {value}/{total} Chunks` + const downloadFormat = `${emojis.arrowDown} Downloading [${theme.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 + ) +} + +/** + * Convert bytes or chilobytes into gigabytes with customizable precision. + * @param bytesOrKB <number> - bytes or KB to be converted. + * @param isBytes <boolean> - true if the input is in bytes; otherwise false for KB input. + * @returns <number> + */ +export const convertToGB = (bytesOrKB: number, isBytes: boolean): number => + Number(bytesOrKB / 1024 ** (isBytes ? 3 : 2)) + +export const createS3Bucket = async (cf: HttpsCallable<unknown, unknown>, bucketName: string): Promise<boolean> => { + // Call createBucket() Cloud Function. + const response: any = await cf({ + bucketName + }) + + // Return true if exists, otherwise false. + return response.data +} + +/** + * Check if an object exists in a given AWS S3 bucket. + * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the identifier of the object. + * @returns Promise<string> - true if the object exists, otherwise false. + */ +export const objectExist = async ( + cf: HttpsCallable<unknown, unknown>, + bucketName: string, + objectKey: string +): Promise<boolean> => { + // Call checkIfObjectExist() Cloud Function. + const response: any = await cf({ + bucketName, + objectKey + }) + + // Return true if exists, otherwise false. + return response.data +} + +/** + * Initiate the multi part upload in AWS S3 Bucket for a large object. + * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the identifier of the object. + * @param ceremonyId <string> - the identifier of the ceremony. + * @returns Promise<string> - the Upload ID reference. + */ +export const openMultiPartUpload = async ( + cf: HttpsCallable<unknown, unknown>, + bucketName: string, + objectKey: string, + ceremonyId?: string +): Promise<string> => { + // Call startMultiPartUpload() Cloud Function. + const response: any = await cf({ + bucketName, + objectKey, + ceremonyId + }) + + // Return Multi Part Upload ID. + return response.data +} + +/** + * Get chunks and signed urls for a multi part upload. + * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the identifier of the object. + * @param filePath <string> - the local path where the file to be uploaded is located. + * @param uploadId <string> - the multi part upload unique identifier. + * @param expirationInSeconds <number> - the pre signed url expiration in seconds. + * @param ceremonyId <string> - the identifier of the ceremony. + * @returns Promise<Array, Array> + */ +export const getChunksAndPreSignedUrls = async ( + cf: HttpsCallable<unknown, unknown>, + bucketName: string, + objectKey: string, + filePath: string, + uploadId: string, + expirationInSeconds: number, + ceremonyId?: string +): Promise<Array<ChunkWithUrl>> => { + // Configuration checks. + if (!process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB) showError(GENERIC_ERRORS.GENERIC_NOT_CONFIGURED_PROPERLY, true) + + // Open a read stream. + const stream = fs.createReadStream(filePath, { + highWaterMark: Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB) * 1024 * 1024 + }) + + // Read and store chunks. + const chunks = [] + for await (const chunk of stream) chunks.push(chunk) + + const numberOfParts = chunks.length + if (!numberOfParts) showError(GENERIC_ERRORS.GENERIC_FILE_ERROR, true) + + // Call generatePreSignedUrlsParts() Cloud Function. + const response: any = await cf({ + bucketName, + objectKey, + uploadId, + numberOfParts, + expirationInSeconds, + ceremonyId + }) + + return chunks.map((val1, index) => ({ + partNumber: index + 1, + chunk: val1, + preSignedUrl: response.data[index] + })) +} + +/** + * Make a PUT request to upload each part for a multi part upload. + * @param chunksWithUrls <Array<ChunkWithUrl>> - the array containing chunks and corresponding pre signed urls. + * @param contentType <string | false> - the content type of the file to upload. + * @param cf <HttpsCallable<unknown, unknown>> - the CF for enable resumable upload from last chunk by temporarily store the ETags and PartNumbers of already uploaded chunks. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param alreadyUploadedChunks <any> - the ETag and PartNumber temporary information about the already uploaded chunks. + * @returns <Promise<Array<ETagWithPartNumber>>> + */ +export const uploadParts = async ( + chunksWithUrls: Array<ChunkWithUrl>, + contentType: string | false, + cf?: HttpsCallable<unknown, unknown>, + ceremonyId?: string, + alreadyUploadedChunks?: any +): Promise<Array<ETagWithPartNumber>> => { + // PartNumber and ETags. + let partNumbersAndETags = [] + + // Restore the already uploaded chunks in the same order. + if (alreadyUploadedChunks) partNumbersAndETags = alreadyUploadedChunks + + // Resume from last uploaded chunk (0 for new multi-part upload). + const lastChunkIndex = partNumbersAndETags.length + + // Define a custom progress bar starting from last updated chunk. + const progressBar = customProgressBar(ProgressBarType.UPLOAD) + progressBar.start(chunksWithUrls.length, lastChunkIndex) + + for (let i = lastChunkIndex; i < chunksWithUrls.length; i += 1) { + // Make PUT call. + const putResponse = await fetch(chunksWithUrls[i].preSignedUrl, { + retryOptions: { + retryInitialDelay: 500, // 500 ms. + socketTimeout: 60000, // 60 seconds. + retryMaxDuration: 300000 // 5 minutes. + }, + method: "PUT", + body: chunksWithUrls[i].chunk, + headers: { + "Content-Type": contentType.toString(), + "Content-Length": chunksWithUrls[i].chunk.length.toString() + }, + agent: new https.Agent({ keepAlive: true }) + }) + + // Extract data. + const eTag = putResponse.headers.get("etag") + const { partNumber } = chunksWithUrls[i] + + // Store PartNumber and ETag. + partNumbersAndETags.push({ + ETag: eTag, + PartNumber: partNumber + }) + + // nb. to be done only when contributing. + if (!!ceremonyId && !!cf) + // Call CF to temporary store the chunks ETag and PartNumber info (useful for resumable upload). + await cf({ + ceremonyId, + eTag, + partNumber + }) + + // Increment the progress bar. + progressBar.increment(1) + } + + return partNumbersAndETags +} + +/** + * Close the multi part upload in AWS S3 Bucket for a large object. + * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the identifier of the object. + * @param uploadId <string> - the multi part upload unique identifier. + * @param parts Array<ETagWithPartNumber> - the uploaded parts. + * @param ceremonyId <string> - the identifier of the ceremony. + * @returns Promise<string> - the location of the uploaded file. + */ +export const closeMultiPartUpload = async ( + cf: HttpsCallable<unknown, unknown>, + bucketName: string, + objectKey: string, + uploadId: string, + parts: Array<ETagWithPartNumber>, + ceremonyId?: string +): Promise<string> => { + // Call completeMultiPartUpload() Cloud Function. + const response: any = await cf({ + bucketName, + objectKey, + uploadId, + parts, + ceremonyId + }) + + // Return uploaded file location. + return response.data +} + +/** + * Download locally a specified file from the given bucket. + * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the identifier of the object (storage path). + * @param localPath <string> - the path where the file will be written. + * @return <Promise<void>> + */ +export const downloadLocalFileFromBucket = async ( + cf: HttpsCallable<unknown, unknown>, + bucketName: string, + objectKey: string, + localPath: string +): Promise<void> => { + // Call generateGetObjectPreSignedUrl() Cloud Function. + const response: any = await cf({ + bucketName, + objectKey + }) + + // Get the pre-signed url. + const preSignedUrl = response.data + + // Get request. + const getResponse = await fetch(preSignedUrl) + + if (!getResponse.ok) showError(`${GENERIC_ERRORS.GENERIC_FILE_ERROR} - ${getResponse.statusText}`, true) + + const contentLength = Number(getResponse.headers.get(`content-length`)) + const contentLengthInGB = convertToGB(contentLength, true) + + // Create a new write stream. + const writeStream = createWriteStream(localPath) + + // Define a custom progress bar starting from last updated chunk. + const progressBar = customProgressBar(ProgressBarType.DOWNLOAD) + + // Progress bar step size. + const progressBarStepSize = contentLengthInGB / 100 + + let writtenData = 0 + let nextStepSize = progressBarStepSize + + // Init the progress bar. + progressBar.start(contentLengthInGB < 0.01 ? 0.01 : Number(contentLengthInGB.toFixed(2)), 0) + + // Write chunk by chunk. + for await (const chunk of getResponse.body) { + // Write. + writeStream.write(chunk) + + // Update. + writtenData += chunk.length + + // Check if the progress bar must advance. + while (convertToGB(writtenData, true) >= nextStepSize) { + // Update. + nextStepSize += progressBarStepSize + + // Increment bar. + progressBar.update(contentLengthInGB < 0.01 ? 0.01 : parseFloat(nextStepSize.toFixed(2)).valueOf()) + } + } + + progressBar.stop() +} diff --git a/packages/phase2cli/src/lib/utils.ts b/packages/phase2cli/src/lib/utils.ts new file mode 100644 index 00000000..3956d045 --- /dev/null +++ b/packages/phase2cli/src/lib/utils.ts @@ -0,0 +1,1230 @@ +import { request } from "@octokit/request" +import { DocumentData, Timestamp } from "firebase/firestore" +import ora, { Ora } from "ora" +import figlet from "figlet" +import clear from "clear" +import { zKey } from "snarkjs" +import winston, { Logger } from "winston" +import { Functions, HttpsCallable, httpsCallable, httpsCallableFromURL } from "firebase/functions" +import { Timer } from "timer-node" +import mime from "mime-types" +import { getDiskInfoSync } from "node-disk-info" +import Drive from "node-disk-info/dist/classes/drive" +import open from "open" +import dotenv from "dotenv" +import { + FirebaseDocumentInfo, + FirebaseServices, + ParticipantContributionStep, + ParticipantStatus, + Timing, + VerifyContributionComputation +} from "../../types/index" +import { collections, emojis, firstZkeyIndex, numIterationsExp, paths, symbols, theme } from "./constants" +import { initServices, uploadFileToStorage } from "./firebase" +import { GENERIC_ERRORS, GITHUB_ERRORS, showError } from "./errors" +import { readFile, writeFile } from "./files" +import { + closeMultiPartUpload, + downloadLocalFileFromBucket, + getChunksAndPreSignedUrls, + openMultiPartUpload, + uploadParts +} from "./storage" +import { getAllCeremonies, getCurrentActiveParticipantTimeout, getCurrentContributorContribution } from "./queries" + +dotenv.config() + +/** + * Get the Github username for the logged in user. + * @param token <string> - the Github OAuth 2.0 token. + * @returns <Promise<string>> - the user Github username. + */ +export const getGithubUsername = async (token: string): Promise<string> => { + // Get user info from Github APIs. + const response = await request("GET https://api.github.com/user", { + headers: { + authorization: `token ${token}` + } + }) + + if (response) return response.data.login + showError(GITHUB_ERRORS.GITHUB_GET_USERNAME_FAILED, true) + + return process.exit(0) // nb. workaround to avoid type issues. +} + +/** + * Get the current amout of available memory for user root disk (mounted in `/` root). + * @returns <number> - the available memory in kB. + */ +export const getParticipantCurrentDiskAvailableSpace = (): number => { + const disks = getDiskInfoSync() + const root = disks.filter((disk: Drive) => disk.mounted === `/`) + + if (root.length !== 1) showError(`Something went wrong while retrieving your root disk available memory`, true) + + const rootDisk = root.at(0)! + + return rootDisk.available +} + +/** + * Return an array of true of false based on contribution verification result per each circuit. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param participantId <string> - the unique identifier of the contributor. + * @param circuits <Array<FirebaseDocumentInfo>> - the Firestore documents of the ceremony circuits. + * @param finalize <boolean> - true when finalizing; otherwise false. + * @returns <Promise<Array<boolean>>> + */ +export const getContributorContributionsVerificationResults = async ( + ceremonyId: string, + participantId: string, + circuits: Array<FirebaseDocumentInfo>, + finalize: boolean +): Promise<Array<boolean>> => { + // Keep track contributions verification results. + const contributions: Array<boolean> = [] + + // Retrieve valid/invalid contributions. + for await (const circuit of circuits) { + // Get contributions to circuit from contributor. + const contributionsToCircuit = await getCurrentContributorContribution(ceremonyId, circuit.id, participantId) + + let contribution: FirebaseDocumentInfo + + if (finalize) + // There should be two contributions from coordinator (one is finalization). + contribution = contributionsToCircuit + .filter((contrib: FirebaseDocumentInfo) => contrib.data.zkeyIndex === "final") + .at(0)! + // There will be only one contribution. + else contribution = contributionsToCircuit.at(0)! + + if (contribution) { + // Get data. + const contributionData = contribution.data + + if (!contributionData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + // Update contributions validity. + contributions.push(!!contributionData?.valid) + } + } + + return contributions +} + +/** + * Return the attestation made only from valid contributions. + * @param contributionsValidities Array<boolean> - an array of booleans (true when contribution is valid; otherwise false). + * @param circuits <Array<FirebaseDocumentInfo>> - the Firestore documents of the ceremony circuits. + * @param participantData <DocumentData> - the document data of the participant. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param participantId <string> - the unique identifier of the contributor. + * @param attestationPreamble <string> - the preamble of the attestation. + * @param finalize <boolean> - true only when finalizing, otherwise false. + * @returns <Promise<string>> - the complete attestation string. + */ +export const getValidContributionAttestation = async ( + contributionsValidities: Array<boolean>, + circuits: Array<FirebaseDocumentInfo>, + participantData: DocumentData, + ceremonyId: string, + participantId: string, + attestationPreamble: string, + finalize: boolean +): Promise<string> => { + let attestation = attestationPreamble + + // For each contribution validity. + for (let idx = 0; idx < contributionsValidities.length; idx += 1) { + if (contributionsValidities[idx]) { + // Extract data from circuit. + const circuit = circuits[idx] + + let contributionHash: string = "" + + // Get the contribution hash. + if (finalize) { + const numberOfContributions = participantData.contributions.length + contributionHash = participantData.contributions[numberOfContributions / 2 + idx].hash + } else contributionHash = participantData.contributions[idx].hash + + // Get the contribution data. + const contributions = await getCurrentContributorContribution(ceremonyId, circuit.id, participantId) + + let contributionData: DocumentData + + if (finalize) + contributionData = contributions.filter( + (contribution: FirebaseDocumentInfo) => contribution.data.zkeyIndex === "final" + )[0].data! + else contributionData = contributions.at(0)?.data! + + // Attestate. + attestation = `${attestation}\n\nCircuit # ${circuit.data.sequencePosition} (${ + circuit.data.prefix + })\nContributor # ${ + contributionData?.zkeyIndex > 0 ? Number(contributionData?.zkeyIndex) : contributionData?.zkeyIndex + }\n${contributionHash}` + } + } + + return attestation +} + +/** + * Publish a new attestation through a Github Gist. + * @param token <string> - the Github OAuth 2.0 token. + * @param content <string> - the content of the attestation. + * @param ceremonyPrefix <string> - the ceremony prefix. + * @param ceremonyTitle <string> - the ceremony title. + */ +export const publishGist = async ( + token: string, + content: string, + ceremonyPrefix: string, + ceremonyTitle: string +): Promise<string> => { + const response = await request("POST /gists", { + description: `Attestation for ${ceremonyTitle} MPC Phase 2 Trusted Setup ceremony`, + public: true, + files: { + [`${ceremonyPrefix}_attestation.txt`]: { + content + } + }, + headers: { + authorization: `token ${token}` + } + }) + + if (response && response.data.html_url) return response.data.html_url + showError(GITHUB_ERRORS.GITHUB_GIST_PUBLICATION_FAILED, true) + + return process.exit(0) // nb. workaround to avoid type issues. +} + +/** + * Extract from milliseconds the seconds, minutes, hours and days. + * @param millis <number> + * @returns <Timing> + */ +export const getSecondsMinutesHoursFromMillis = (millis: number): Timing => { + // Get seconds from 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 + } +} + +/** + * Return a string with double digits if the amount is one digit only. + * @param amount <number> + * @returns <string> + */ +export const convertToDoubleDigits = (amount: number): string => (amount < 10 ? `0${amount}` : amount.toString()) + +/** + * Sleeps the function execution for given millis. + * @dev to be used in combination with loggers when writing data into files. + * @param ms <number> - sleep amount in milliseconds + * @returns <Promise<any>> + */ +export const sleep = (ms: number): Promise<any> => new Promise((resolve) => setTimeout(resolve, ms)) + +/** + * Return a custom spinner. + * @param text <string> - the text that should be displayed as spinner status. + * @param spinnerLogo <any> - the logo. + * @returns <Ora> - a new Ora custom spinner. + */ +export const customSpinner = (text: string, spinnerLogo: any): Ora => + ora({ + text, + spinner: spinnerLogo + }) + +/** + * Return a simple graphical loader to simulate loading or describe an asynchronous task. + * @param loadingText <string> - the text that should be displayed while the loader is spinning. + * @param logo <any> - the logo of the loader. + * @param durationInMillis <number> - the loader duration time in milliseconds. + * @param afterLoadingText <string> - the text that should be displayed for the loader stop. + * @returns <Promise<void>>. + */ +export const simpleLoader = async ( + loadingText: string, + logo: any, + durationInMillis: number, + afterLoadingText?: string +): Promise<void> => { + // Define the loader. + const loader = customSpinner(loadingText, logo) + + loader.start() + + // nb. wait for `durationInMillis` time while loader is spinning. + await sleep(durationInMillis) + + if (afterLoadingText) loader.succeed(afterLoadingText) + else loader.stop() +} + +/** + * Return the bucket name based on ceremony prefix. + * @param ceremonyPrefix <string> - the ceremony prefix. + * @returns <string> + */ +export const getBucketName = (ceremonyPrefix: string): string => { + if (!process.env.CONFIG_CEREMONY_BUCKET_POSTFIX) showError(GENERIC_ERRORS.GENERIC_NOT_CONFIGURED_PROPERLY, true) + + return `${ceremonyPrefix}${process.env.CONFIG_CEREMONY_BUCKET_POSTFIX!}` +} + +/** + * Return the ceremonies prefixes for every ceremony. + * @returns Promise<Array<string>> + */ +export const getCreatedCeremoniesPrefixes = async (): Promise<Array<string>> => { + // Get all ceremonies documents. + const ceremonies = await getAllCeremonies() + + let ceremoniesPrefixes = [] + + // Return prefixes (if any ceremony). + if (ceremonies.length > 0) + ceremoniesPrefixes = ceremonies.map((ceremony: FirebaseDocumentInfo) => ceremony.data.prefix) + + return ceremoniesPrefixes +} + +/** + * Upload a file by subdividing it in chunks to AWS S3 bucket. + * @param startMultiPartUploadCF <HttpsCallable<unknown, unknown>> - the CF for initiating a multi part upload. + * @param generatePreSignedUrlsPartsCF <HttpsCallable<unknown, unknown>> - the CF for generating the pre-signed urls for each chunk. + * @param completeMultiPartUploadCF <HttpsCallable<unknown, unknown>> - the CF for completing a multi part upload. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the path of the object inside the AWS S3 bucket. + * @param localPath <string> - the local path of the file to be uploaded. + * @param temporaryStoreCurrentContributionMultiPartUploadId <HttpsCallable<unknown, unknown>> - the CF for enable resumable upload from last chunk by temporarily store the ETags and PartNumbers of already uploaded chunks. + * @param temporaryStoreCurrentContributionUploadedChunkData <HttpsCallable<unknown, unknown>> - the CF for enable resumable upload from last chunk by temporarily store the ETags and PartNumbers of already uploaded chunks. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param tempContributionData <any> - the temporary information necessary to resume an already started multi-part upload. + */ +export const multiPartUpload = async ( + startMultiPartUploadCF: HttpsCallable<unknown, unknown>, + generatePreSignedUrlsPartsCF: HttpsCallable<unknown, unknown>, + completeMultiPartUploadCF: HttpsCallable<unknown, unknown>, + bucketName: string, + objectKey: string, + localPath: string, + temporaryStoreCurrentContributionMultiPartUploadId?: HttpsCallable<unknown, unknown>, + temporaryStoreCurrentContributionUploadedChunkData?: HttpsCallable<unknown, unknown>, + ceremonyId?: string, + tempContributionData?: any +) => { + // Configuration checks. + if (!process.env.CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS) + showError(GENERIC_ERRORS.GENERIC_NOT_CONFIGURED_PROPERLY, true) + + // Get content type. + const contentType = mime.lookup(localPath) + + // The Multi-Part Upload unique identifier. + let uploadIdZkey = "" + // Already uploaded chunks temp info (nb. useful only when resuming). + let alreadyUploadedChunks = [] + + // Check if the contributor can resume an already started multi-part upload. + if (!tempContributionData || (!!tempContributionData && !tempContributionData.uploadId)) { + // Start from scratch. + const spinner = customSpinner(`Starting upload process...`, `clock`) + spinner.start() + + uploadIdZkey = await openMultiPartUpload(startMultiPartUploadCF, bucketName, objectKey, ceremonyId) + + if (temporaryStoreCurrentContributionMultiPartUploadId) + // Store Multi-Part Upload ID after generation. + await temporaryStoreCurrentContributionMultiPartUploadId({ + ceremonyId, + uploadId: uploadIdZkey + }) + + spinner.stop() + } else { + // Read temp info from Firestore. + uploadIdZkey = tempContributionData.uploadId + alreadyUploadedChunks = tempContributionData.chunks + } + + // Step 2 + const spinner = customSpinner(`Splitting file in chunks...`, `clock`) + spinner.start() + + const chunksWithUrlsZkey = await getChunksAndPreSignedUrls( + generatePreSignedUrlsPartsCF, + bucketName, + objectKey, + localPath, + uploadIdZkey, + Number(process.env.CONFIG_PRESIGNED_URL_EXPIRATION_IN_SECONDS!), + ceremonyId + ) + + // Step 3 + const partNumbersAndETagsZkey = await uploadParts( + chunksWithUrlsZkey, + contentType, + temporaryStoreCurrentContributionUploadedChunkData, + ceremonyId, + alreadyUploadedChunks + ) + + // Step 4 + spinner.text = `Completing upload...` + spinner.start() + + await closeMultiPartUpload( + completeMultiPartUploadCF, + bucketName, + objectKey, + uploadIdZkey, + partNumbersAndETagsZkey, + ceremonyId + ) + + spinner.stop() +} + +/** + * Get a value from a key information about a circuit. + * @param circuitInfo <string> - the stringified content of the .r1cs file. + * @param rgx <RegExp> - regular expression to match the key. + * @returns <string> + */ +export const getCircuitMetadataFromR1csFile = (circuitInfo: string, rgx: RegExp): string => { + // Match. + const matchInfo = circuitInfo.match(rgx) + + if (!matchInfo) showError(GENERIC_ERRORS.GENERIC_R1CS_MISSING_INFO, true) + + // Split and return the value. + return matchInfo?.at(0)?.split(":")[1].replace(" ", "").split("#")[0].replace("\n", "")! +} + +/** + * Return the necessary Power of Tau "powers" given the number of circuits constraints. + * @param constraints <number> - the number of circuit contraints. + * @param outputs <number> - the number of circuit outputs. + * @returns <number> + */ +export const estimatePoT = (constraints: number, outputs: number): number => { + let power = 2 + let pot = 2 ** power + + while (constraints + outputs > pot) { + power += 1 + pot = 2 ** power + } + + return power +} + +/** + * Get the powers from pot file name + * @dev the pot files must follow these convention (i_am_a_pot_file_09.ptau) where the numbers before '.ptau' are the powers. + * @param potFileName <string> + * @returns <number> + */ +export const extractPoTFromFilename = (potFileName: string): number => + Number(potFileName.split("_").pop()?.split(".").at(0)) + +/** + * Extract a prefix (like_this) from a provided string with special characters and spaces. + * @dev replaces all symbols and whitespaces with underscore. + * @param str <string> + * @returns <string> + */ +export const extractPrefix = (str: string): string => + // eslint-disable-next-line no-useless-escape + str.replace(/[`\s~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "-").toLowerCase() + +/** + * Format the next zkey index. + * @param progress <number> - the progression in zkey index (= contributions). + * @returns <string> + */ +export const formatZkeyIndex = (progress: number): string => { + let index = progress.toString() + + while (index.length < firstZkeyIndex.length) { + index = `0${index}` + } + + return index +} + +/** + * Convert milliseconds to seconds. + * @param millis <number> + * @returns <number> + */ +export const convertMillisToSeconds = (millis: number): number => Number((millis / 1000).toFixed(2)) + +/** + * Bootstrap whatever is needed for a new command execution (clean terminal, print header, init Firebase services). + * @returns <Promise<FirebaseServices>> + */ +export const bootstrapCommandExec = async (): Promise<FirebaseServices> => { + // Clean terminal window. + clear() + + // Print header. + console.log(theme.magenta(figlet.textSync("Phase 2 cli", { font: "Ogre" }))) + + // Initialize Firebase services + return initServices() +} + +/** + * Gracefully terminate the command execution + * @params ghUsername <string> - the Github username of the user. + */ +export const terminate = async (ghUsername: string) => { + console.log(`\nSee you, ${theme.bold(`@${ghUsername}`)} ${emojis.wave}`) + + process.exit(0) +} + +/** + * Make a new countdown and throws an error when time is up. + * @param durationInSeconds <number> - the amount of time to be counted in seconds. + * @param intervalInSeconds <number> - update interval in seconds. + */ +export const createExpirationCountdown = (durationInSeconds: number, intervalInSeconds: number) => { + let seconds = durationInSeconds <= 60 ? durationInSeconds : 60 + + setInterval(() => { + try { + if (durationInSeconds !== 0) { + // Update times. + durationInSeconds -= intervalInSeconds + seconds -= intervalInSeconds + + if (seconds % 60 === 0) seconds = 0 + + process.stdout.write( + `${symbols.warning} Expires in ${theme.bold( + theme.magenta(`00:${Math.floor(durationInSeconds / 60)}:${seconds}`) + )}\r` + ) + } else showError(GENERIC_ERRORS.GENERIC_COUNTDOWN_EXPIRED, true) + } catch (err: any) { + // Workaround to the \r. + process.stdout.write(`\n\n`) + showError(GENERIC_ERRORS.GENERIC_COUNTDOWN_EXPIRATION, true) + } + }, intervalInSeconds * 1000) +} + +/** + * Create and return a simple countdown for a specified amount of time. + * @param remainingTime <number> - the amount of time to be counted. + * @param message <string> - the message to be shown. + * @returns <NodeJS.Timer> + */ +export const simpleCountdown = (remainingTime: number, message: string): NodeJS.Timer => + setInterval(() => { + remainingTime -= 1000 + + const { + seconds: cdSeconds, + minutes: cdMinutes, + hours: cdHours + } = getSecondsMinutesHoursFromMillis(Math.abs(remainingTime)) + + process.stdout.write( + `${message} (${remainingTime < 0 ? theme.bold(`-`) : ``}${convertToDoubleDigits( + cdHours + )}:${convertToDoubleDigits(cdMinutes)}:${convertToDoubleDigits(cdSeconds)})\r` + ) + }, 1000) + +/** + * Manage the communication of timeout-related messages for a contributor. + * @param participantData <DocumentData> - the data of the participant document. + * @param participantId <string> - the unique identifier of the contributor. + * @param ceremonyId <string> - the unique identifier of the ceremony. + * @param isContributing <boolean> + * @param ghUsername <string> + */ +export const handleTimedoutMessageForContributor = async ( + participantData: DocumentData, + participantId: string, + ceremonyId: string, + isContributing: boolean, + ghUsername: string +): Promise<void> => { + // Extract data. + const { status, contributionStep, contributionProgress } = participantData + + // Check if the contributor has been timedout. + if (status === ParticipantStatus.TIMEDOUT && contributionStep !== ParticipantContributionStep.COMPLETED) { + if (!isContributing) console.log(theme.bold(`\n- Circuit # ${theme.magenta(contributionProgress)}`)) + else process.stdout.write(`\n`) + + console.log( + `${symbols.error} ${ + isContributing ? `You have been timedout while contributing` : `Timeout still in progress.` + }\n\n${ + symbols.warning + } This can happen due to network or memory issues, un/intentional crash, or contributions lasting for too long.` + ) + + // nb. workaround to retrieve the latest timeout data from the database. + await simpleLoader(`Checking timeout...`, `clock`, 1000) + + // Check when the participant will be able to retry the contribution. + const activeTimeouts = await getCurrentActiveParticipantTimeout(ceremonyId, participantId) + + if (activeTimeouts.length !== 1) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + const activeTimeoutData = activeTimeouts.at(0)?.data + + if (!activeTimeoutData) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + const { seconds, minutes, hours, days } = getSecondsMinutesHoursFromMillis( + Number(activeTimeoutData?.endDate) - Timestamp.now().toMillis() + ) + + console.log( + `${symbols.info} You can retry your contribution in ${theme.bold( + `${convertToDoubleDigits(days)}:${convertToDoubleDigits(hours)}:${convertToDoubleDigits( + minutes + )}:${convertToDoubleDigits(seconds)}` + )} (dd/hh/mm/ss)` + ) + + terminate(ghUsername) + } +} + +/** + * Compute a new Groth 16 Phase 2 contribution. + * @param lastZkey <string> - the local path to last zkey. + * @param newZkey <string> - the local path to new zkey. + * @param name <string> - the name of the contributor. + * @param entropyOrBeacon <string> - the value representing the entropy or beacon. + * @param logger <Logger | Console> - custom winston or console logger. + * @param finalize <boolean> - true when finalizing the ceremony with the last contribution; otherwise false. + * @param contributionComputationTime <number> - the contribution computation time in milliseconds for the circuit. + */ +export const computeContribution = async ( + lastZkey: string, + newZkey: string, + name: string, + entropyOrBeacon: string, + logger: Logger | Console, + finalize: boolean, + contributionComputationTime: number +) => { + // Format average contribution time. + const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(contributionComputationTime) + + // Custom spinner for visual feedback. + const text = `${finalize ? `Applying beacon...` : `Computing contribution...`} ${ + contributionComputationTime > 0 + ? `(ETA ${theme.bold( + `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits(seconds)}` + )} |` + : `` + }` + + let counter = 0 + + // Format time. + const { + seconds: counterSeconds, + minutes: counterMinutes, + hours: counterHours + } = getSecondsMinutesHoursFromMillis(counter) + + const spinner = customSpinner( + `${text} ${convertToDoubleDigits(counterHours)}:${convertToDoubleDigits( + counterMinutes + )}:${convertToDoubleDigits(counterSeconds)})\r`, + `clock` + ) + spinner.start() + + const interval = setInterval(() => { + counter += 1000 + + const { + seconds: counterSec, + minutes: counterMin, + hours: counterHrs + } = getSecondsMinutesHoursFromMillis(counter) + + spinner.text = `${text} ${convertToDoubleDigits(counterHrs)}:${convertToDoubleDigits( + counterMin + )}:${convertToDoubleDigits(counterSec)})\r` + }, 1000) + + if (finalize) + // Finalize applying a random beacon. + await zKey.beacon(lastZkey, newZkey, name, entropyOrBeacon, numIterationsExp, logger) + // Compute the next contribution. + else await zKey.contribute(lastZkey, newZkey, name, entropyOrBeacon, logger) + + // nb. workaround to logger descriptor close. + await sleep(1000) + + spinner.stop() + clearInterval(interval) +} + +/** + * Create a custom logger. + * @dev useful for keeping track of `info` logs from snarkjs and use them to generate the contribution transcript. + * @param transcriptFilename <string> - logger output file. + * @returns <Logger> + */ +export const getTranscriptLogger = (transcriptFilename: string): Logger => + // Create a custom logger. + winston.createLogger({ + level: "info", + format: winston.format.printf((log) => log.message), + transports: [ + // Write all logs with importance level of `info` to `transcript.json`. + new winston.transports.File({ + filename: transcriptFilename, + level: "info" + }) + ] + }) + +/** + * Make a progress to the next contribution step for the current contributor. + * @param firebaseFunctions <Functions> - the object containing the firebase functions. + * @param ceremonyId <string> - the ceremony unique identifier. + * @param showSpinner <boolean> - true to show a custom spinner on the terminal; otherwise false. + * @param message <string> - custom message string based on next contribution step value. + */ +export const makeContributionStepProgress = async ( + firebaseFunctions: Functions, + ceremonyId: string, + showSpinner: boolean, + message: string +) => { + // Get CF. + const progressToNextContributionStep = httpsCallable(firebaseFunctions, "progressToNextContributionStep") + + // Custom spinner for visual feedback. + const spinner: Ora = customSpinner(`Getting ready for ${message} step`, "clock") + + if (showSpinner) spinner.start() + + // Progress to next contribution step. + await progressToNextContributionStep({ ceremonyId }) + + if (showSpinner) spinner.stop() +} + +/** + * Return the next circuit where the participant needs to compute or has computed the contribution. + * @param circuits <Array<FirebaseDocumentInfo>> - the ceremony circuits document. + * @param nextCircuitPosition <number> - the position in the sequence of circuits where the next contribution must be done. + * @returns <FirebaseDocumentInfo> + */ +export const getNextCircuitForContribution = ( + circuits: Array<FirebaseDocumentInfo>, + nextCircuitPosition: number +): FirebaseDocumentInfo => { + // Filter for sequence position (should match contribution progress). + const filteredCircuits = circuits.filter( + (circuit: FirebaseDocumentInfo) => circuit.data.sequencePosition === nextCircuitPosition + ) + + // There must be only one. + if (filteredCircuits.length !== 1) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + return filteredCircuits.at(0)! +} + +/** + * Generate the public attestation for the contributor. + * @param ceremonyDoc <FirebaseDocumentInfo> - the ceremony document. + * @param participantId <string> - the unique identifier of the participant. + * @param participantData <DocumentData> - the data of the participant document. + * @param circuits <Array<FirebaseDocumentInfo> - the ceremony circuits documents. + * @param ghUsername <string> - the Github username of the contributor. + * @param ghToken <string> - the Github access token of the contributor. + */ +export const generatePublicAttestation = async ( + ceremonyDoc: FirebaseDocumentInfo, + participantId: string, + participantData: DocumentData, + circuits: Array<FirebaseDocumentInfo>, + ghUsername: string, + ghToken: string +): Promise<void> => { + // Attestation preamble. + const attestationPreamble = `Hey, I'm ${ghUsername} and I have contributed to the ${ceremonyDoc.data.title} MPC Phase2 Trusted Setup ceremony.\nThe following are my contribution signatures:` + + // Return true and false based on contribution verification. + const contributionsValidity = await getContributorContributionsVerificationResults( + ceremonyDoc.id, + participantId, + circuits, + false + ) + const numberOfValidContributions = contributionsValidity.filter(Boolean).length + + console.log( + `\nCongrats, you have successfully contributed to ${theme.magenta( + theme.bold(numberOfValidContributions) + )} out of ${theme.magenta(theme.bold(circuits.length))} circuits ${emojis.tada}` + ) + + // Show valid/invalid contributions per each circuit. + let idx = 0 + + for (const contributionValidity of contributionsValidity) { + console.log( + `${contributionValidity ? symbols.success : symbols.error} ${theme.bold(`Circuit`)} ${theme.bold( + theme.magenta(idx + 1) + )}` + ) + idx += 1 + } + + process.stdout.write(`\n`) + + const spinner = customSpinner("Uploading public attestation...", "clock") + spinner.start() + + // Get only valid contribution hashes. + const attestation = await getValidContributionAttestation( + contributionsValidity, + circuits, + participantData!, + ceremonyDoc.id, + participantId, + attestationPreamble, + false + ) + + writeFile(`${paths.attestationPath}/${ceremonyDoc.data.prefix}_attestation.log`, Buffer.from(attestation)) + await sleep(1000) + + // TODO: If fails for permissions problems, ask to do manually. + const gistUrl = await publishGist(ghToken, attestation, ceremonyDoc.data.prefix, ceremonyDoc.data.title) + + spinner.succeed( + `Public attestation successfully published as Github Gist at this link ${theme.bold(theme.underlined(gistUrl))}` + ) + + // Attestation link via Twitter. + const attestationTweet = `https://twitter.com/intent/tweet?text=I%20contributed%20to%20the%20${ceremonyDoc.data.title}%20Phase%202%20Trusted%20Setup%20ceremony!%20You%20can%20contribute%20here:%20https://github.com/quadratic-funding/mpc-phase2-suite%20You%20can%20view%20my%20attestation%20here:%20${gistUrl}%20#Ethereum%20#ZKP` + + console.log( + `\nWe appreciate your contribution to preserving the ${ceremonyDoc.data.title} security! ${ + emojis.key + } You can tweet about your participation if you'd like (click on the link below ${ + emojis.pointDown + }) \n\n${theme.underlined(attestationTweet)}` + ) + + await open(attestationTweet) +} + +/** + * Download a local copy of the zkey. + * @param cf <HttpsCallable<unknown, unknown>> - the corresponding cloud function. + * @param bucketName <string> - the name of the AWS S3 bucket. + * @param objectKey <string> - the identifier of the object (storage path). + * @param localPath <string> - the path where the file will be written. + * @param showSpinner <boolean> - true to show a custom spinner on the terminal; otherwise false. + */ +export const downloadContribution = async ( + cf: HttpsCallable<unknown, unknown>, + bucketName: string, + objectKey: string, + localPath: string, + showSpinner: boolean +) => { + // Custom spinner for visual feedback. + const spinner: Ora = customSpinner(`Downloading contribution...`, "clock") + + if (showSpinner) spinner.start() + + // Download from storage. + await downloadLocalFileFromBucket(cf, bucketName, objectKey, localPath) + + if (showSpinner) spinner.stop() +} + +/** + * Upload the new zkey to the storage. + * @param storagePath <string> - the Storage path where the zkey will be stored. + * @param localPath <string> - the local path where the zkey is stored. + * @param showSpinner <boolean> - true to show a custom spinner on the terminal; otherwise false. + */ +export const uploadContribution = async (storagePath: string, localPath: string, showSpinner: boolean) => { + // Custom spinner for visual feedback. + const spinner = customSpinner("Storing your contribution...", "clock") + if (showSpinner) spinner.start() + + // Upload to storage. + await uploadFileToStorage(localPath, storagePath) + + if (showSpinner) spinner.stop() +} + +/** + * Compute a new Groth16 contribution verification. + * @param ceremony <FirebaseDocumentInfo> - the ceremony document. + * @param circuit <FirebaseDocumentInfo> - the circuit document. + * @param ghUsername <string> - the Github username of the user. + * @param avgVerifyCloudFunctionTime <number> - the average verify Cloud Function execution time in milliseconds. + * @param firebaseFunctions <Functions> - the object containing the firebase functions. + * @returns <Promise<VerifyContributionComputation>> + */ +export const computeVerification = async ( + ceremony: FirebaseDocumentInfo, + circuit: FirebaseDocumentInfo, + ghUsername: string, + avgVerifyCloudFunctionTime: number, + firebaseFunctions: Functions +): Promise<VerifyContributionComputation> => { + // Format average verification time. + const { seconds, minutes, hours } = getSecondsMinutesHoursFromMillis(avgVerifyCloudFunctionTime) + + // Custom spinner for visual feedback. + const spinner = customSpinner( + `Verifying your contribution... ${ + avgVerifyCloudFunctionTime > 0 + ? `(est. time ${theme.bold( + `${convertToDoubleDigits(hours)}:${convertToDoubleDigits(minutes)}:${convertToDoubleDigits( + seconds + )}` + )})` + : `` + }\n`, + "clock" + ) + + spinner.start() + + // Verify contribution callable Cloud Function. + const verifyContribution = httpsCallableFromURL( + firebaseFunctions!, + process.env.FIREBASE_CF_URL_VERIFY_CONTRIBUTION!, + { + timeout: 3600000 + } + ) + + // The verification must be done remotely (Cloud Functions). + const response = await verifyContribution({ + ceremonyId: ceremony.id, + circuitId: circuit.id, + ghUsername, + bucketName: getBucketName(ceremony.data.prefix) + }) + + spinner.stop() + + if (!response) showError(GENERIC_ERRORS.GENERIC_ERROR_RETRIEVING_DATA, true) + + const { data }: any = response + + return { + valid: data.valid, + verificationComputationTime: data.verificationComputationTime, + verifyCloudFunctionTime: data.verifyCloudFunctionTime, + fullContributionTime: data.fullContributionTime + } +} + +/** + * Compute a new contribution for the participant. + * @param ceremony <FirebaseDocumentInfo> - the ceremony document. + * @param circuit <FirebaseDocumentInfo> - the circuit document. + * @param entropyOrBeacon <any> - the entropy/beacon for the contribution. + * @param ghUsername <string> - the Github username of the user. + * @param finalize <boolean> - true if the contribution finalize the ceremony; otherwise false. + * @param firebaseFunctions <Functions> - the object containing the firebase functions. + * @param newParticipantData <DocumentData> - the object containing the participant data. + * @returns <Promise<string>> - new updated attestation file. + */ +export const makeContribution = async ( + ceremony: FirebaseDocumentInfo, + circuit: FirebaseDocumentInfo, + entropyOrBeacon: any, + ghUsername: string, + finalize: boolean, + firebaseFunctions: Functions, + newParticipantData?: DocumentData +): Promise<void> => { + // Extract data from circuit. + const currentProgress = circuit.data.waitingQueue.completedContributions + const { avgTimings } = circuit.data + + // Compute zkey indexes. + const currentZkeyIndex = formatZkeyIndex(currentProgress) + const nextZkeyIndex = formatZkeyIndex(currentProgress + 1) + + // Paths config. + const transcriptsPath = finalize ? paths.finalTranscriptsPath : paths.contributionTranscriptsPath + const contributionsPath = finalize ? paths.finalZkeysPath : paths.contributionsPath + + // Get custom transcript logger. + const contributionTranscriptLocalPath = `${transcriptsPath}/${circuit.data.prefix}_${ + finalize ? `${ghUsername}_final` : nextZkeyIndex + }.log` + const transcriptLogger = getTranscriptLogger(contributionTranscriptLocalPath) + const bucketName = getBucketName(ceremony.data.prefix) + + // Write first message. + transcriptLogger.info( + `${finalize ? `Final` : `Contribution`} transcript for ${circuit.data.prefix} phase 2 contribution.\n${ + finalize ? `Coordinator: ${ghUsername}` : `Contributor # ${Number(nextZkeyIndex)}` + } (${ghUsername})\n` + ) + + console.log( + `${theme.bold(`\n- Circuit # ${theme.magenta(`${circuit.data.sequencePosition}`)}`)} (Contribution Steps)` + ) + + if ( + finalize || + (!!newParticipantData?.contributionStep && + newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) + ) { + const spinner = customSpinner(`Preparing for download...`, `clock`) + spinner.start() + + // 1. Download last contribution. + const storagePath = `${collections.circuits}/${circuit.data.prefix}/${collections.contributions}/${circuit.data.prefix}_${currentZkeyIndex}.zkey` + const localPath = `${contributionsPath}/${circuit.data.prefix}_${currentZkeyIndex}.zkey` + + // Download w/ Presigned urls. + const generateGetObjectPreSignedUrl = httpsCallable(firebaseFunctions!, "generateGetObjectPreSignedUrl") + + spinner.stop() + + await downloadContribution(generateGetObjectPreSignedUrl, bucketName, storagePath, localPath, false) + + console.log(`${symbols.success} Contribution ${theme.bold(`#${currentZkeyIndex}`)} correctly downloaded`) + + // Make the step if not finalizing. + if (!finalize) await makeContributionStepProgress(firebaseFunctions!, ceremony.id, true, "computation") + } else console.log(`${symbols.success} Contribution ${theme.bold(`#${currentZkeyIndex}`)} already downloaded`) + + if ( + finalize || + (!!newParticipantData?.contributionStep && + newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) || + newParticipantData?.contributionStep === ParticipantContributionStep.COMPUTING + ) { + const contributionComputationTimer = new Timer({ label: "contributionComputation" }) // Compute time (only for statistics). + + // 2.A Compute the new contribution. + contributionComputationTimer.start() + + await computeContribution( + `${contributionsPath}/${circuit.data.prefix}_${currentZkeyIndex}.zkey`, + `${contributionsPath}/${circuit.data.prefix}_${finalize ? `final` : nextZkeyIndex}.zkey`, + ghUsername, + entropyOrBeacon, + transcriptLogger, + finalize, + avgTimings.contributionComputation + ) + + contributionComputationTimer.stop() + + const contributionComputationTime = contributionComputationTimer.ms() + + const spinner = customSpinner(`Storing contribution time and hash...`, `clock`) + spinner.start() + + // nb. workaround for file descriptor close. + await sleep(2000) + + // 2.B Generate attestation from single contribution transcripts from each circuit (queue this contribution). + const transcript = readFile(contributionTranscriptLocalPath) + + const matchContributionHash = transcript.match(/Contribution.+Hash.+\n\t\t.+\n\t\t.+\n.+\n\t\t.+\n/) + + if (!matchContributionHash) showError(GENERIC_ERRORS.GENERIC_CONTRIBUTION_HASH_INVALID, true) + + const contributionHash = matchContributionHash?.at(0)?.replace("\n\t\t", "")! + + const permanentlyStoreCurrentContributionTimeAndHash = httpsCallable( + firebaseFunctions!, + "permanentlyStoreCurrentContributionTimeAndHash" + ) + + await permanentlyStoreCurrentContributionTimeAndHash({ + ceremonyId: ceremony.id, + contributionComputationTime, + contributionHash + }) + + const { + seconds: computationSeconds, + minutes: computationMinutes, + hours: computationHours + } = getSecondsMinutesHoursFromMillis(contributionComputationTime) + + spinner.succeed( + `${ + finalize ? "Contribution" : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` + } computation took ${theme.bold( + `${convertToDoubleDigits(computationHours)}:${convertToDoubleDigits( + computationMinutes + )}:${convertToDoubleDigits(computationSeconds)}` + )}` + ) + + // Make the step if not finalizing. + if (!finalize) await makeContributionStepProgress(firebaseFunctions!, ceremony.id, true, "upload") + } else console.log(`${symbols.success} Contribution ${theme.bold(`#${nextZkeyIndex}`)} already computed`) + + if ( + finalize || + (!!newParticipantData?.contributionStep && + newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) || + newParticipantData?.contributionStep === ParticipantContributionStep.COMPUTING || + newParticipantData?.contributionStep === ParticipantContributionStep.UPLOADING + ) { + // 3. Store file. + const storagePath = `${collections.circuits}/${circuit.data.prefix}/${collections.contributions}/${ + circuit.data.prefix + }_${finalize ? `final` : nextZkeyIndex}.zkey` + const localPath = `${contributionsPath}/${circuit.data.prefix}_${finalize ? `final` : nextZkeyIndex}.zkey` + + // Upload. + const startMultiPartUpload = httpsCallable(firebaseFunctions, "startMultiPartUpload") + const generatePreSignedUrlsParts = httpsCallable(firebaseFunctions, "generatePreSignedUrlsParts") + const completeMultiPartUpload = httpsCallable(firebaseFunctions, "completeMultiPartUpload") + + if (!finalize) { + const temporaryStoreCurrentContributionMultiPartUploadId = httpsCallable( + firebaseFunctions, + "temporaryStoreCurrentContributionMultiPartUploadId" + ) + const temporaryStoreCurrentContributionUploadedChunk = httpsCallable( + firebaseFunctions, + "temporaryStoreCurrentContributionUploadedChunkData" + ) + + await multiPartUpload( + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload, + bucketName, + storagePath, + localPath, + temporaryStoreCurrentContributionMultiPartUploadId, + temporaryStoreCurrentContributionUploadedChunk, + ceremony.id, + newParticipantData?.tempContributionData + ) + } else + await multiPartUpload( + startMultiPartUpload, + generatePreSignedUrlsParts, + completeMultiPartUpload, + bucketName, + storagePath, + localPath + ) + + console.log( + `${symbols.success} ${ + finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` + } correctly saved on storage` + ) + + // Make the step if not finalizing. + if (!finalize) await makeContributionStepProgress(firebaseFunctions!, ceremony.id, true, "verification") + } else + console.log( + `${symbols.success} ${ + finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` + } already saved on storage` + ) + + if ( + finalize || + (!!newParticipantData?.contributionStep && + newParticipantData?.contributionStep === ParticipantContributionStep.DOWNLOADING) || + newParticipantData?.contributionStep === ParticipantContributionStep.COMPUTING || + newParticipantData?.contributionStep === ParticipantContributionStep.UPLOADING || + newParticipantData?.contributionStep === ParticipantContributionStep.VERIFYING + ) { + // 5. Verify contribution. + const { valid, verifyCloudFunctionTime, fullContributionTime } = await computeVerification( + ceremony, + circuit, + ghUsername, + avgTimings.verifyCloudFunction, + firebaseFunctions + ) + + const { + seconds: verificationSeconds, + minutes: verificationMinutes, + hours: verificationHours + } = getSecondsMinutesHoursFromMillis(verifyCloudFunctionTime) + + console.log( + `${valid ? symbols.success : symbols.error} ${ + finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` + } ${valid ? `is ${theme.bold("VALID")}` : `is ${theme.bold("INVALID")}`}` + ) + console.log( + `${symbols.success} ${ + finalize ? `Contribution` : `Contribution ${theme.bold(`#${nextZkeyIndex}`)}` + } verification took ${theme.bold( + `${convertToDoubleDigits(verificationHours)}:${convertToDoubleDigits( + verificationMinutes + )}:${convertToDoubleDigits(verificationSeconds)}` + )}` + ) + + const { + seconds: contributionSeconds, + minutes: contributionMinutes, + hours: contributionHours + } = getSecondsMinutesHoursFromMillis(fullContributionTime + verifyCloudFunctionTime) + console.log( + `${symbols.info} Your contribution took ${theme.bold( + `${convertToDoubleDigits(contributionHours)}:${convertToDoubleDigits( + contributionMinutes + )}:${convertToDoubleDigits(contributionSeconds)}` + )}` + ) + } +} diff --git a/packages/phase2cli/test/index.test.ts b/packages/phase2cli/test/index.test.ts new file mode 100644 index 00000000..1baf9e1d --- /dev/null +++ b/packages/phase2cli/test/index.test.ts @@ -0,0 +1,5 @@ +describe("Sample", () => { + it("should console.log", () => { + console.log("Hello, World!") + }) +}) diff --git a/packages/phase2cli/tsconfig.json b/packages/phase2cli/tsconfig.json new file mode 100644 index 00000000..277a5e72 --- /dev/null +++ b/packages/phase2cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist/", + "moduleResolution": "node", + "declarationDir": "dist/types" + }, + "include": ["src/**/*", "test/**/*", "types/**/*"] +} diff --git a/apps/phase2cli/types/actions.d.ts b/packages/phase2cli/types/actions.d.ts similarity index 100% rename from apps/phase2cli/types/actions.d.ts rename to packages/phase2cli/types/actions.d.ts diff --git a/packages/phase2cli/types/index.ts b/packages/phase2cli/types/index.ts new file mode 100644 index 00000000..bf70d397 --- /dev/null +++ b/packages/phase2cli/types/index.ts @@ -0,0 +1,219 @@ +import { FirebaseApp } from "firebase/app" +import { DocumentData, DocumentReference, Firestore } from "firebase/firestore" +import { Functions } from "firebase/functions" +import { User as FirebaseAuthUser } from "firebase/auth" + +export enum CeremonyState { + SCHEDULED = 1, + OPENED = 2, + PAUSED = 3, + CLOSED = 4, + FINALIZED = 5 +} + +export enum CeremonyType { + PHASE1 = 1, + PHASE2 = 2 +} + +export enum ProgressBarType { + DOWNLOAD = 1, + UPLOAD = 2 +} + +export enum ParticipantStatus { + CREATED = 1, + WAITING = 2, + READY = 3, + CONTRIBUTING = 4, + CONTRIBUTED = 5, + DONE = 6, + FINALIZING = 7, + FINALIZED = 8, + TIMEDOUT = 9, + EXHUMED = 10 +} + +export type GithubOAuthRequest = { + device_code: string + user_code: string + verification_uri: string + expires_in: number + interval: number +} + +export type GithubOAuthResponse = { + clientSecret: string + type: string + tokenType: string + clientType: string + clientId: string + token: string + scopes: string[] +} + +export type FirebaseServices = { + firebaseApp: FirebaseApp + firestoreDatabase: Firestore + firebaseFunctions: Functions +} + +export type LocalPathDirectories = { + r1csDirPath: string + metadataDirPath: string + zkeysDirPath: string + ptauDirPath: string +} + +export type FirebaseDocumentInfo = { + id: string + ref: DocumentReference<DocumentData> + data: DocumentData +} + +export type User = { + name: string + username: string + providerId: string + createdAt: Date + lastLoginAt: Date +} + +export type AuthUser = { + user: FirebaseAuthUser + token: string + username: string +} + +export type CeremonyInputData = { + title: string + description: string + startDate: Date + endDate: Date + timeoutMechanismType: CeremonyTimeoutType + penalty: number +} + +export type CircomCompilerData = { + version: string + commitHash: string +} + +export type SourceTemplateData = { + source: string + commitHash: string + paramsConfiguration: Array<string> +} + +export type CircuitInputData = { + name?: string + description: string + timeoutThreshold?: number + timeoutMaxContributionWaitingTime?: number + sequencePosition?: number + prefix?: string + zKeySizeInBytes?: number + compiler: CircomCompilerData + template: SourceTemplateData +} + +export type Ceremony = CeremonyInputData & { + prefix: string + state: CeremonyState + type: CeremonyType + coordinatorId: string + lastUpdated: number +} + +export type CircuitMetadata = { + curve: string + wires: number + constraints: number + privateInputs: number + publicOutputs: number + labels: number + outputs: number + pot: number +} + +export type CircuitFiles = { + files?: { + potFilename: string + r1csFilename: string + initialZkeyFilename: string + potStoragePath: string + r1csStoragePath: string + initialZkeyStoragePath: string + potBlake2bHash: string + r1csBlake2bHash: string + initialZkeyBlake2bHash: string + } +} + +export type CircuitTimings = { + avgTimings?: { + contributionComputation: number + fullContribution: number + verifyCloudFunction: number + } +} + +export type Circuit = CircuitInputData & + CircuitFiles & + CircuitTimings & { + metadata: CircuitMetadata + lastUpdated?: number + } + +export type Timing = { + seconds: number + minutes: number + hours: number + days: number +} + +export type CeremonyTimeoutData = { + type: CeremonyTimeoutType + penalty: number +} + +export type VerifyContributionComputation = { + valid: boolean + verificationComputationTime: number + verifyCloudFunctionTime: number + fullContributionTime: number +} + +export type ChunkWithUrl = { + partNumber: number + chunk: Buffer + preSignedUrl: string +} + +export type ETagWithPartNumber = { + ETag: string | null + PartNumber: number +} + +export enum RequestType { + PUT = 1, + GET = 2 +} + +export enum ParticipantContributionStep { + DOWNLOADING = 1, + COMPUTING = 2, + UPLOADING = 3, + VERIFYING = 4, + COMPLETED = 5 +} + +export enum TimeoutType { + BLOCKING_CONTRIBUTION = 1, + BLOCKING_CLOUD_FUNCTION = 2 +} + +export enum CeremonyTimeoutType { + DYNAMIC = 1, + FIXED = 2 +} diff --git a/packages/phase2cli/types/snarkjs.d.ts b/packages/phase2cli/types/snarkjs.d.ts new file mode 100644 index 00000000..1ac7c1ca --- /dev/null +++ b/packages/phase2cli/types/snarkjs.d.ts @@ -0,0 +1,58 @@ +/** Declaration file generated by dts-gen */ + +declare module "snarkjs" { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + export = snarkjs + + declare const snarkjs: { + groth16: { + exportSolidityCallData: any + fullProve: any + prove: any + verify: any + } + plonk: { + exportSolidityCallData: any + fullProve: any + prove: any + setup: any + verify: any + } + powersOfTau: { + beacon: any + challengeContribute: any + contribute: any + convert: any + exportChallenge: any + exportJson: any + importResponse: any + newAccumulator: any + preparePhase2: any + truncate: any + verify: any + } + r1cs: { + exportJson: any + info: any + print: any + } + wtns: { + calculate: any + debug: any + exportJson: any + } + zKey: { + beacon: any + bellmanContribute: any + contribute: any + exportBellman: any + exportJson: any + exportSolidityVerifier: any + exportVerificationKey: any + importBellman: any + newZKey: any + verifyFromInit: any + verifyFromR1cs: any + } + } +} diff --git a/tsconfig.json b/tsconfig.json index 82bcd805..41254034 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,20 @@ { - "compilerOptions": { - "baseUrl": ".", - "strict": true, - "allowJs": true, - "target": "ES5", - "module": "esnext", - "moduleResolution": "node", - "esModuleInterop": true, - "resolveJsonModule": true, - "preserveConstEnums": true, - "skipLibCheck": true, - "declaration": true, - "allowSyntheticDefaultImports": true, - "declarationDir": "types", - "typeRoots": ["node_modules/@types", "types"], - "noImplicitReturns": true, - "noUnusedLocals": true, - "sourceMap": true - }, - "ts-node": { "compilerOptions": { - "target": "esnext", - "module": "commonjs" + "baseUrl": ".", + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "preserveConstEnums": true, + "skipLibCheck": true, + "declaration": true, + "allowSyntheticDefaultImports": true, + "declarationDir": "types", + "typeRoots": ["node_modules/@types", "types"], + "paths": { + "@zkmpc/*": ["packages/*/src"] + } } - } }