@devtion/devcli 0.0.0-2319823 → 0.0.0-2e5a17d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.env CHANGED
@@ -23,6 +23,13 @@ FIREBASE_CF_URL_VERIFY_CONTRIBUTION=https://verifycontribution-mq4aqokliq-ew.a.r
23
23
 
24
24
  # The unique identifier for the Github client associated to the OAuth Application.
25
25
  AUTH_GITHUB_CLIENT_ID=e9f8a5fabdfe0d95618c
26
+ ### BANDADA AUTHENTICATION ###
27
+ # The Bandada API URL to be used for authentication.
28
+ BANDADA_API_URL=https://api.bandada.pse.dev/
29
+ # The Bandada group id that will be used for the credentials criteria (e.g. GH followers)
30
+ BANDADA_GROUP_ID=40887196405294111455930028907236
31
+ # The Bandada dashboard URL to administrate the groups
32
+ BANDADA_DASHBOARD_URL=https://bandada.pse.dev/
26
33
 
27
34
  ### AWS S3 STORAGE ###
28
35
  ### These configs are related to the configuration of the interaction with the
package/dist/index.js CHANGED
@@ -2,27 +2,27 @@
2
2
 
3
3
  /**
4
4
  * @module @p0tion/phase2cli
5
- * @version 1.1.0
5
+ * @version 1.1.1
6
6
  * @file All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies
7
7
  * @copyright Ethereum Foundation 2022
8
8
  * @license MIT
9
9
  * @see [Github]{@link https://github.com/privacy-scaling-explorations/p0tion}
10
10
  */
11
11
  import { createCommand } from 'commander';
12
- import fs, { readFileSync, createWriteStream, renameSync } from 'fs';
12
+ import fs, { readFileSync, createWriteStream, existsSync, renameSync } from 'fs';
13
13
  import { dirname } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
- import { zKey } from 'snarkjs';
15
+ import { zKey, groth16 } from 'snarkjs';
16
16
  import boxen from 'boxen';
17
17
  import { pipeline } from 'node:stream';
18
18
  import { promisify } from 'node:util';
19
19
  import fetch$1 from 'node-fetch';
20
- import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract } from '@devtion/actions';
20
+ import { commonTerms, formatZkeyIndex, getZkeyStorageFilePath, finalContributionIndex, createCustomLoggerForFile, getBucketName, progressToNextContributionStep, permanentlyStoreCurrentContributionTimeAndHash, convertToDoubleDigits, multiPartUpload, verifyContribution, generateGetObjectPreSignedUrl, convertBytesOrKbToGb, numExpIterations, getDocumentById, getParticipantsCollectionPath, fromQueryToFirebaseDocumentInfo, getAllCollectionDocs, extractPrefix, autoGenerateEntropy, vmConfigurationTypes, initializeFirebaseCoreServices, signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator, parseCeremonyFile, blake512FromPath, checkIfObjectExist, setupCeremony, genesisZkeyIndex, getR1csStorageFilePath, getWasmStorageFilePath, getPotStorageFilePath, extractPoTFromFilename, potFileDownloadMainUrl, createS3Bucket, potFilenameTemplate, getR1CSInfo, getOpenedCeremonies, getCeremonyCircuits, checkParticipantForCeremony, getCurrentActiveParticipantTimeout, getCircuitBySequencePosition, getCircuitContributionsFromContributor, progressToNextCircuitForContribution, resumeContributionAfterTimeoutExpiration, generateValidContributionsAttestation, getContributionsValidityForContributor, getClosedCeremonies, checkAndPrepareCoordinatorForFinalization, computeSHA256ToHex, finalizeCeremony, getVerificationKeyStorageFilePath, verificationKeyAcronym, getVerifierContractStorageFilePath, verifierSmartContractAcronym, finalizeCircuit, exportVkey, exportVerifierContract, getAllCeremonies } from '@devtion/actions';
21
21
  import fetch from '@adobe/node-fetch-retry';
22
22
  import { request } from '@octokit/request';
23
23
  import { SingleBar, Presets } from 'cli-progress';
24
24
  import dotenv from 'dotenv';
25
- import { GithubAuthProvider, getAuth, signOut } from 'firebase/auth';
25
+ import { GithubAuthProvider, signInWithCustomToken, getAuth, signOut } from 'firebase/auth';
26
26
  import { getDiskInfoSync } from 'node-disk-info';
27
27
  import ora from 'ora';
28
28
  import { Timer } from 'timer-node';
@@ -36,7 +36,10 @@ import figlet from 'figlet';
36
36
  import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device';
37
37
  import clipboard from 'clipboardy';
38
38
  import open from 'open';
39
- import { Timestamp, onSnapshot } from 'firebase/firestore';
39
+ import { Identity } from '@semaphore-protocol/identity';
40
+ import { httpsCallable } from 'firebase/functions';
41
+ import { ApiSdk } from '@bandada/api-sdk';
42
+ import { Timestamp, onSnapshot, doc, collection, getDocs } from 'firebase/firestore';
40
43
  import readline from 'readline';
41
44
 
42
45
  /**
@@ -97,7 +100,7 @@ const CORE_SERVICES_ERRORS = {
97
100
  FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `The Github authorization has failed due to lack of association between your account and the CLI`,
98
101
  FIREBASE_USER_DISABLED: `The Github account has been suspended by the ceremony coordinator(s), blocking the possibility of contribution. Please, contact them to understand the motivation behind it.`,
99
102
  FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials due to network errors. Please, try once again later.`,
100
- FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network erros. Please, try once again later and make sure your Internet connection is stable.`,
103
+ FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network errors. Please, try once again later and make sure your Internet connection is stable.`,
101
104
  FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`,
102
105
  FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization`,
103
106
  AWS_CEREMONY_BUCKET_CREATION: `Unable to create a new bucket for the ceremony. Something went wrong during the creation. Please, repeat the process by providing a new ceremony name of the ceremony.`,
@@ -250,6 +253,10 @@ const config = new Conf({
250
253
  accessToken: {
251
254
  type: "string",
252
255
  default: ""
256
+ },
257
+ bandadaIdentity: {
258
+ type: "string",
259
+ default: ""
253
260
  }
254
261
  }
255
262
  });
@@ -310,6 +317,25 @@ const setLocalAccessToken = (token) => config.set("accessToken", token);
310
317
  * Delete the stored access token.
311
318
  */
312
319
  const deleteLocalAccessToken = () => config.delete("accessToken");
320
+ /**
321
+ * Return the Bandada identity, if present.
322
+ * @returns <string | undefined> - the Bandada identity if present, otherwise undefined.
323
+ */
324
+ const getLocalBandadaIdentity = () => config.get("bandadaIdentity");
325
+ /**
326
+ * Check if the Bandada identity exists in the local storage.
327
+ * @returns <boolean>
328
+ */
329
+ const checkLocalBandadaIdentity = () => config.has("bandadaIdentity") && !!config.get("bandadaIdentity");
330
+ /**
331
+ * Set the Bandada identity.
332
+ * @param identity <string> - the Bandada identity to be stored.
333
+ */
334
+ const setLocalBandadaIdentity = (identity) => config.set("bandadaIdentity", identity);
335
+ /**
336
+ * Delete the stored Bandada identity.
337
+ */
338
+ const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity");
313
339
  /**
314
340
  * Get the complete local file path.
315
341
  * @param cwd <string> - the current working directory path.
@@ -420,7 +446,7 @@ const getGithubAuthenticatedUserGists = async (githubToken, params) => {
420
446
  headers: {
421
447
  authorization: `token ${githubToken}`
422
448
  },
423
- per_page: params.perPage,
449
+ per_page: params.perPage, // max items per page = 100.
424
450
  page: params.page
425
451
  });
426
452
  if (response && response.status === 200)
@@ -468,8 +494,9 @@ const getPublicAttestationGist = async (githubToken, publicAttestationFilename)
468
494
  * @returns <string> - the third-party provider handle of the user.
469
495
  */
470
496
  const getUserHandleFromProviderUserId = (providerUserId) => {
471
- if (providerUserId.indexOf("-") === -1)
472
- showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true);
497
+ if (providerUserId.indexOf("-") === -1) {
498
+ return providerUserId;
499
+ }
473
500
  return providerUserId.split("-")[0];
474
501
  };
475
502
  /**
@@ -1560,16 +1587,27 @@ const checkAuth = async (firebaseApp) => {
1560
1587
  showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_NOT_AUTHENTICATED, true);
1561
1588
  // Retrieve local access token.
1562
1589
  const token = String(getLocalAccessToken());
1563
- // Get credentials.
1564
- const credentials = exchangeGithubTokenForCredentials(token);
1565
- // Sign in to Firebase using credentials.
1566
- await signInToFirebase(firebaseApp, credentials);
1590
+ let providerUserId;
1591
+ let username;
1592
+ const isLocalBandadaIdentityStored = checkLocalBandadaIdentity();
1593
+ if (isLocalBandadaIdentityStored) {
1594
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
1595
+ providerUserId = userCredentials.user.uid;
1596
+ username = providerUserId;
1597
+ }
1598
+ else {
1599
+ // Get credentials.
1600
+ const credentials = exchangeGithubTokenForCredentials(token);
1601
+ // Sign in to Firebase using credentials.
1602
+ await signInToFirebase(firebaseApp, credentials);
1603
+ // Get Github unique identifier (handle-id).
1604
+ providerUserId = await getGithubProviderUserId(String(token));
1605
+ username = getUserHandleFromProviderUserId(providerUserId);
1606
+ }
1567
1607
  // Get current authenticated user.
1568
1608
  const user = getCurrentFirebaseAuthUser(firebaseApp);
1569
- // Get Github unique identifier (handle-id).
1570
- const providerUserId = await getGithubProviderUserId(String(token));
1571
1609
  // Greet the user.
1572
- console.log(`Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`);
1610
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`);
1573
1611
  return {
1574
1612
  user,
1575
1613
  token,
@@ -1891,12 +1929,20 @@ const setup = async (cmd) => {
1891
1929
  // 3. generate the zKey
1892
1930
  const spinner = customSpinner(`Generating genesis zKey for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1893
1931
  spinner.start();
1894
- await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1895
- spinner.succeed(`Generation of the genesis zKey for citcui ${theme.text.bold(circuit.name)} completed successfully`);
1932
+ if (existsSync(zkeyLocalPathAndFileName)) {
1933
+ spinner.succeed(`The genesis zKey for circuit ${theme.text.bold(circuit.name)} is already present on disk`);
1934
+ }
1935
+ else {
1936
+ await zKey.newZKey(r1csLocalPathAndFileName, getPotLocalFilePath(circuit.files.potFilename), zkeyLocalPathAndFileName, undefined);
1937
+ spinner.succeed(`Generation of the genesis zKey for circuit ${theme.text.bold(circuit.name)} completed successfully`);
1938
+ }
1939
+ const hashSpinner = customSpinner(`Calculating hashes for circuit ${theme.text.bold(circuit.name)}...`, `clock`);
1940
+ hashSpinner.start();
1896
1941
  // 4. calculate the hashes
1897
1942
  const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName);
1898
1943
  const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename));
1899
1944
  const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName);
1945
+ hashSpinner.succeed(`Hashes for circuit ${theme.text.bold(circuit.name)} calculated successfully`);
1900
1946
  // 5. upload the artifacts
1901
1947
  // Upload zKey to Storage.
1902
1948
  await handleCircuitArtifactUploadToStorage(firebaseFunctions, bucketName, circuit.files.initialZkeyStoragePath, zkeyLocalPathAndFileName, circuit.files.initialZkeyFilename);
@@ -2217,6 +2263,100 @@ const auth = async () => {
2217
2263
  terminate(providerUserId);
2218
2264
  };
2219
2265
 
2266
+ const { BANDADA_API_URL } = process.env;
2267
+ const bandadaApi = new ApiSdk(BANDADA_API_URL);
2268
+ const addMemberToGroup = async (groupId, dashboardUrl, identity) => {
2269
+ const commitment = identity.commitment.toString();
2270
+ const group = await bandadaApi.getGroup(groupId);
2271
+ const providerName = group.credentials.id.split("_")[0].toLowerCase();
2272
+ // 6. open a new window with the url:
2273
+ const url = `${dashboardUrl}credentials?group=${groupId}&member=${commitment}&provider=${providerName}`;
2274
+ console.log(`${theme.text.bold(`Verification URL:`)} ${theme.text.underlined(url)}`);
2275
+ open(url);
2276
+ const { confirmation } = await askForConfirmation("Did you join the Bandada group in the browser?");
2277
+ if (!confirmation)
2278
+ showError("You must join the Bandada group to continue the login process", true);
2279
+ };
2280
+ const isGroupMember = async (groupId, identity) => {
2281
+ const commitment = identity.commitment.toString();
2282
+ const isMember = await bandadaApi.isGroupMember(groupId, commitment);
2283
+ return isMember;
2284
+ };
2285
+
2286
+ const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env;
2287
+ const authBandada = async () => {
2288
+ try {
2289
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices();
2290
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`);
2291
+ spinner.start();
2292
+ // 1. check if _identity string exists in local storage
2293
+ let identityString;
2294
+ const isIdentityStringStored = checkLocalBandadaIdentity();
2295
+ if (isIdentityStringStored) {
2296
+ identityString = getLocalBandadaIdentity();
2297
+ spinner.succeed(`Identity seed found\n`);
2298
+ }
2299
+ else {
2300
+ spinner.warn(`Identity seed not found\n`);
2301
+ // 2. generate a random _identity string and save it in local storage
2302
+ const { seed } = await prompts({
2303
+ type: "text",
2304
+ name: "seed",
2305
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
2306
+ initial: false
2307
+ });
2308
+ identityString = seed;
2309
+ setLocalBandadaIdentity(identityString);
2310
+ }
2311
+ // 3. create a semaphore identity with _identity string as a seed
2312
+ const identity = new Identity(identityString);
2313
+ // 4. check if the user is a member of the group
2314
+ console.log(`Checking Bandada membership...`);
2315
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity);
2316
+ if (!isMember) {
2317
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity);
2318
+ }
2319
+ // 5. generate a proof that the user owns the commitment.
2320
+ spinner.text = `Generating proof of identity...`;
2321
+ spinner.start();
2322
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
2323
+ const { proof, publicSignals } = await groth16.fullProve({
2324
+ identityTrapdoor: identity.trapdoor,
2325
+ identityNullifier: identity.nullifier,
2326
+ externalNullifier: BANDADA_GROUP_ID
2327
+ }, `${dirname(fileURLToPath(import.meta.url))}/public/mini-semaphore.wasm`, `${dirname(fileURLToPath(import.meta.url))}/public/mini-semaphore.zkey`);
2328
+ spinner.succeed(`Proof generated.\n`);
2329
+ spinner.text = `Sending proof to verification...`;
2330
+ spinner.start();
2331
+ // 6. send proof to a cloud function that verifies it and checks membership
2332
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof);
2333
+ const result = await cf({
2334
+ proof,
2335
+ publicSignals
2336
+ });
2337
+ const { valid, token, message } = result.data;
2338
+ if (!valid) {
2339
+ showError(message, true);
2340
+ }
2341
+ spinner.succeed(`Proof verified.\n`);
2342
+ spinner.text = `Authenticating...`;
2343
+ spinner.start();
2344
+ // 7. Auth to p0tion firebase
2345
+ const userCredentials = await signInWithCustomToken(getAuth(), token);
2346
+ setLocalAccessToken(token);
2347
+ spinner.succeed(`Authenticated as ${theme.text.bold(userCredentials.user.uid)}.`);
2348
+ console.log(`\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(`phase2cli logout`)} command`);
2349
+ }
2350
+ catch (error) {
2351
+ // Delete local token.
2352
+ console.log("An error crashed the process. Deleting local token and identity.");
2353
+ console.error(error);
2354
+ deleteLocalAccessToken();
2355
+ deleteLocalBandadaIdentity();
2356
+ }
2357
+ process.exit(0);
2358
+ };
2359
+
2220
2360
  /**
2221
2361
  * Return the verification result for latest contribution.
2222
2362
  * @param firestoreDatabase <Firestore> - the Firestore service instance associated to the current Firebase application.
@@ -2408,8 +2548,12 @@ const handlePublicAttestation = async (firestoreDatabase, circuits, ceremonyId,
2408
2548
  // Write public attestation locally.
2409
2549
  writeFile(getAttestationLocalFilePath(`${ceremonyPrefix}_${commonTerms.foldersAndPathsTerms.attestation}.log`), Buffer.from(publicAttestation));
2410
2550
  await sleep(1000); // workaround for file descriptor unexpected close.
2411
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2412
- console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2551
+ let gistUrl = "";
2552
+ const isBandada = checkLocalBandadaIdentity();
2553
+ if (!isBandada) {
2554
+ gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix);
2555
+ console.log(`\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(theme.text.underlined(gistUrl))})`);
2556
+ }
2413
2557
  // Prepare a ready-to-share tweet.
2414
2558
  await handleTweetGeneration(ceremonyName, gistUrl);
2415
2559
  };
@@ -2723,7 +2867,7 @@ const contribute = async (opt) => {
2723
2867
  const userDoc = await getDocumentById(firestoreDatabase, commonTerms.collections.users.name, user.uid);
2724
2868
  const userData = userDoc.data();
2725
2869
  if (!userData) {
2726
- spinner.fail(`Unfortunately we could not find a user document with your information. This likely means that you did not pass the GitHub reputation checks and therefore are not elegible to contribute to any ceremony. If you believe you pass the requirements, it might be possible that your profile is private and we were not able to fetch your real statistics, in this case please consider making your profile public for the duration of the contribution. Please contact the coordinator if you believe this to be an error.`);
2870
+ spinner.fail(`Unfortunately we could not find a user document with your information. This likely means that you did not pass the GitHub reputation checks and therefore are not eligible to contribute to any ceremony. If you believe you pass the requirements, it might be possible that your profile is private and we were not able to fetch your real statistics, in this case please consider making your profile public for the duration of the contribution. Please contact the coordinator if you believe this to be an error.`);
2727
2871
  process.exit(0);
2728
2872
  }
2729
2873
  // Check the user's current participant readiness for contribution status (eligible, already contributed, timed out).
@@ -2923,7 +3067,7 @@ const handleVerificationKey = async (cloudFunctions, bucketName, finalZkeyLocalF
2923
3067
  spinner.text = "Writing verification key...";
2924
3068
  // Write the verification key locally.
2925
3069
  writeLocalJsonFile(verificationKeyLocalFilePath, vKey);
2926
- await sleep(3000); // workaound for file descriptor.
3070
+ await sleep(3000); // workaround for file descriptor.
2927
3071
  // Upload verification key to storage.
2928
3072
  await multiPartUpload(cloudFunctions, bucketName, verificationKeyStorageFilePath, verificationKeyLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2929
3073
  spinner.succeed(`Verification key correctly saved on storage`);
@@ -2949,7 +3093,7 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2949
3093
  spinner.text = `Writing verifier smart contract...`;
2950
3094
  // Write the verification key locally.
2951
3095
  writeFile(verifierContractLocalFilePath, verifierCode);
2952
- await sleep(3000); // workaound for file descriptor.
3096
+ await sleep(3000); // workaround for file descriptor.
2953
3097
  // Upload verifier smart contract to storage.
2954
3098
  await multiPartUpload(cloudFunctions, bucketName, verifierContractStorageFilePath, verifierContractLocalFilePath, Number(process.env.CONFIG_STREAM_CHUNK_SIZE_IN_MB));
2955
3099
  spinner.succeed(`Verifier smart contract correctly saved on storage`);
@@ -2975,7 +3119,7 @@ const handleVerifierSmartContract = async (cloudFunctions, bucketName, finalZkey
2975
3119
  const handleCircuitFinalization = async (cloudFunctions, firestoreDatabase, ceremony, circuit, participant, beacon, coordinatorIdentifier, circuitsLength) => {
2976
3120
  // Step (1).
2977
3121
  await handleStartOrResumeContribution(cloudFunctions, firestoreDatabase, ceremony, circuit, participant, computeSHA256ToHex(beacon), coordinatorIdentifier, true, circuitsLength);
2978
- await sleep(2000); // workaound for descriptors.
3122
+ await sleep(2000); // workaround for descriptors.
2979
3123
  // Extract data.
2980
3124
  const { prefix: circuitPrefix } = circuit.data;
2981
3125
  const { prefix: ceremonyPrefix } = ceremony.data;
@@ -3125,6 +3269,7 @@ const logout = async () => {
3125
3269
  await signOut(auth);
3126
3270
  // Delete local token.
3127
3271
  deleteLocalAccessToken();
3272
+ deleteLocalBandadaIdentity();
3128
3273
  await sleep(3000); // ~3s.
3129
3274
  spinner.stop();
3130
3275
  console.log(`${theme.symbols.success} Logout successfully completed`);
@@ -3186,6 +3331,37 @@ const listCeremonies = async () => {
3186
3331
  }
3187
3332
  };
3188
3333
 
3334
+ const listParticipants = async () => {
3335
+ try {
3336
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices();
3337
+ const allCeremonies = await getAllCeremonies(firestoreDatabase);
3338
+ const selectedCeremony = await promptForCeremonySelection(allCeremonies, true);
3339
+ const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id);
3340
+ const participantsRef = collection(docRef, "participants");
3341
+ const participantsSnapshot = await getDocs(participantsRef);
3342
+ const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data().userId);
3343
+ console.log(participants);
3344
+ /* const usersRef = collection(firestoreDatabase, "users")
3345
+ const usersSnapshot = await getDocs(usersRef)
3346
+ const users = usersSnapshot.docs.map((userDoc) => userDoc.data())
3347
+ console.log(users) */
3348
+ }
3349
+ catch (err) {
3350
+ showError(`Something went wrong: ${err.toString()}`, true);
3351
+ }
3352
+ process.exit(0);
3353
+ };
3354
+
3355
+ const setCeremonyCommands = (program) => {
3356
+ const ceremony = program.command("ceremony").description("manage ceremonies");
3357
+ ceremony
3358
+ .command("participants")
3359
+ .description("retrieve participants list of a ceremony")
3360
+ .requiredOption("-c, --ceremony <string>", "the prefix of the ceremony you want to retrieve information about", "")
3361
+ .action(listParticipants);
3362
+ return ceremony;
3363
+ };
3364
+
3189
3365
  // Get pkg info (e.g., name, version).
3190
3366
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
3191
3367
  const { description, version, name } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
@@ -3194,6 +3370,10 @@ const program = createCommand();
3194
3370
  program.name(name).description(description).version(version);
3195
3371
  // User commands.
3196
3372
  program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth);
3373
+ program
3374
+ .command("auth-bandada")
3375
+ .description("authenticate yourself in a privacy-perserving manner using Bandada")
3376
+ .action(authBandada);
3197
3377
  program
3198
3378
  .command("contribute")
3199
3379
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
@@ -3212,25 +3392,26 @@ program
3212
3392
  .action(logout);
3213
3393
  program
3214
3394
  .command("validate")
3215
- .description("Validate that a Ceremony Setup file is correct")
3395
+ .description("validate that a Ceremony Setup file is correct")
3216
3396
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
3217
3397
  .option("-c, --constraints <number>", "The number of constraints to check against")
3218
3398
  .action(validate);
3219
3399
  // Only coordinator commands.
3220
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony");
3221
- ceremony
3400
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony");
3401
+ coordinate
3222
3402
  .command("setup")
3223
3403
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
3224
3404
  .option("-t, --template <path>", "The path to the ceremony setup template", "")
3225
3405
  .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
3226
3406
  .action(setup);
3227
- ceremony
3407
+ coordinate
3228
3408
  .command("observe")
3229
3409
  .description("observe in real-time the waiting queue of each ceremony circuit")
3230
3410
  .action(observe);
3231
- ceremony
3411
+ coordinate
3232
3412
  .command("finalize")
3233
3413
  .description("finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract")
3234
3414
  .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
3235
3415
  .action(finalize);
3416
+ setCeremonyCommands(program);
3236
3417
  program.parseAsync(process.argv);
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ declare const authBandada: () => Promise<never>;
2
+ export default authBandada;
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ declare const setCeremonyCommands: (program: Command) => Command;
3
+ export default setCeremonyCommands;
@@ -0,0 +1,2 @@
1
+ declare const listParticipants: () => Promise<never>;
2
+ export default listParticipants;
@@ -1,5 +1,6 @@
1
1
  export { default as setup } from "./setup.js";
2
2
  export { default as auth } from "./auth.js";
3
+ export { default as authBandada } from "./authBandada.js";
3
4
  export { default as contribute } from "./contribute.js";
4
5
  export { default as observe } from "./observe.js";
5
6
  export { default as finalize } from "./finalize.js";
@@ -0,0 +1,6 @@
1
+ import { GroupResponse } from "@bandada/api-sdk";
2
+ import { Identity } from "@semaphore-protocol/identity";
3
+ export declare const getGroup: (groupId: string) => Promise<GroupResponse | null>;
4
+ export declare const getMembersOfGroup: (groupId: string) => Promise<string[] | null>;
5
+ export declare const addMemberToGroup: (groupId: string, dashboardUrl: string, identity: Identity) => Promise<void>;
6
+ export declare const isGroupMember: (groupId: string, identity: Identity) => Promise<boolean>;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { Dirent, Stats } from "fs";
3
4
  /**
4
5
  * Check a directory path.
@@ -35,6 +35,25 @@ export declare const setLocalAccessToken: (token: string) => void;
35
35
  * Delete the stored access token.
36
36
  */
37
37
  export declare const deleteLocalAccessToken: () => void;
38
+ /**
39
+ * Return the Bandada identity, if present.
40
+ * @returns <string | undefined> - the Bandada identity if present, otherwise undefined.
41
+ */
42
+ export declare const getLocalBandadaIdentity: () => string | unknown;
43
+ /**
44
+ * Check if the Bandada identity exists in the local storage.
45
+ * @returns <boolean>
46
+ */
47
+ export declare const checkLocalBandadaIdentity: () => boolean;
48
+ /**
49
+ * Set the Bandada identity.
50
+ * @param identity <string> - the Bandada identity to be stored.
51
+ */
52
+ export declare const setLocalBandadaIdentity: (identity: string) => void;
53
+ /**
54
+ * Delete the stored Bandada identity.
55
+ */
56
+ export declare const deleteLocalBandadaIdentity: () => void;
38
57
  /**
39
58
  * Get the complete local file path.
40
59
  * @param cwd <string> - the current working directory path.
@@ -63,3 +63,15 @@ export type GithubGistFile = {
63
63
  raw_url: string;
64
64
  size: number;
65
65
  };
66
+ /**
67
+ * Define the return object of the function that verifies the Bandada membership and proof.
68
+ * @typedef {Object} VerifiedBandadaResponse
69
+ * @property {boolean} valid - true if the proof is valid and the user is a member of the group; otherwise false.
70
+ * @property {string} message - a message describing the result of the verification.
71
+ * @property {string} token - the custom access token.
72
+ */
73
+ export type VerifiedBandadaResponse = {
74
+ valid: boolean;
75
+ message: string;
76
+ token: string;
77
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devtion/devcli",
3
3
  "type": "module",
4
- "version": "0.0.0-2319823",
4
+ "version": "0.0.0-2e5a17d",
5
5
  "description": "All-in-one interactive command-line for interfacing with zkSNARK Phase 2 Trusted Setup ceremonies",
6
6
  "repository": "git@github.com:privacy-scaling-explorations/p0tion.git",
7
7
  "homepage": "https://github.com/privacy-scaling-explorations/p0tion",
@@ -34,11 +34,13 @@
34
34
  "build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript",
35
35
  "start": "ts-node --esm ./src/index.ts",
36
36
  "auth": "yarn start auth",
37
+ "auth:bandada": "yarn start auth-bandada",
37
38
  "contribute": "yarn start contribute",
38
39
  "clean": "yarn start clean",
39
40
  "list": "yarn start list",
40
41
  "logout": "yarn start logout",
41
42
  "validate": "yarn start validate",
43
+ "ceremony:participants": "yarn start ceremony participants",
42
44
  "coordinate:setup": "yarn start coordinate setup",
43
45
  "coordinate:observe": "yarn start coordinate observe",
44
46
  "coordinate:finalize": "yarn start coordinate finalize",
@@ -57,6 +59,7 @@
57
59
  "@types/winston": "^2.4.4",
58
60
  "rollup-plugin-auto-external": "^2.0.0",
59
61
  "rollup-plugin-cleanup": "^3.2.1",
62
+ "rollup-plugin-copy": "^3.5.0",
60
63
  "rollup-plugin-typescript2": "^0.34.1",
61
64
  "solc": "^0.8.19",
62
65
  "ts-node": "^10.9.1",
@@ -65,10 +68,12 @@
65
68
  "dependencies": {
66
69
  "@adobe/node-fetch-retry": "^2.2.0",
67
70
  "@aws-sdk/client-s3": "^3.329.0",
71
+ "@bandada/api-sdk": "^1.0.0-beta.1",
68
72
  "@devtion/actions": "latest",
69
73
  "@octokit/auth-oauth-app": "^5.0.5",
70
74
  "@octokit/auth-oauth-device": "^4.0.4",
71
75
  "@octokit/request": "^6.2.3",
76
+ "@semaphore-protocol/identity": "^3.15.1",
72
77
  "blakejs": "^1.2.1",
73
78
  "boxen": "^7.1.0",
74
79
  "chalk": "^5.2.0",
@@ -78,6 +83,7 @@
78
83
  "commander": "^10.0.1",
79
84
  "conf": "^11.0.1",
80
85
  "dotenv": "^16.0.3",
86
+ "ethers": "^6.9.0",
81
87
  "figlet": "^1.6.0",
82
88
  "firebase": "^9.21.0",
83
89
  "log-symbols": "^5.1.0",
@@ -90,12 +96,12 @@
90
96
  "prompts": "^2.4.2",
91
97
  "rimraf": "^5.0.0",
92
98
  "rollup": "^3.21.6",
93
- "snarkjs": "^0.6.11",
99
+ "snarkjs": "0.7.3",
94
100
  "timer-node": "^5.0.7",
95
101
  "winston": "^3.8.2"
96
102
  },
97
103
  "publishConfig": {
98
104
  "access": "public"
99
105
  },
100
- "gitHead": "fbef8ad90209cc9e5d687b5ebff75c4f3eff4790"
106
+ "gitHead": "45ce4ab3de9e13929ad6d6ca87e3e98176ef02ca"
101
107
  }
@@ -0,0 +1,110 @@
1
+ import { Identity } from "@semaphore-protocol/identity"
2
+
3
+ import { commonTerms } from "@devtion/actions"
4
+ import { httpsCallable } from "firebase/functions"
5
+ import { groth16 } from "snarkjs"
6
+ import { dirname } from "path"
7
+ import { getAuth, signInWithCustomToken } from "firebase/auth"
8
+ import prompts from "prompts"
9
+ import { fileURLToPath } from "url"
10
+ import theme from "../lib/theme.js"
11
+ import { customSpinner } from "../lib/utils.js"
12
+ import { VerifiedBandadaResponse } from "../types/index.js"
13
+ import { showError } from "../lib/errors.js"
14
+ import { bootstrapCommandExecutionAndServices } from "../lib/services.js"
15
+ import { addMemberToGroup, isGroupMember } from "../lib/bandada.js"
16
+ import {
17
+ checkLocalBandadaIdentity,
18
+ deleteLocalAccessToken,
19
+ deleteLocalBandadaIdentity,
20
+ getLocalBandadaIdentity,
21
+ setLocalAccessToken,
22
+ setLocalBandadaIdentity
23
+ } from "../lib/localConfigs.js"
24
+
25
+ const { BANDADA_DASHBOARD_URL, BANDADA_GROUP_ID } = process.env
26
+
27
+ const authBandada = async () => {
28
+ try {
29
+ const { firebaseFunctions } = await bootstrapCommandExecutionAndServices()
30
+ const spinner = customSpinner(`Checking identity string for Semaphore...`, `clock`)
31
+ spinner.start()
32
+ // 1. check if _identity string exists in local storage
33
+ let identityString: string | unknown
34
+ const isIdentityStringStored = checkLocalBandadaIdentity()
35
+ if (isIdentityStringStored) {
36
+ identityString = getLocalBandadaIdentity()
37
+ spinner.succeed(`Identity seed found\n`)
38
+ } else {
39
+ spinner.warn(`Identity seed not found\n`)
40
+ // 2. generate a random _identity string and save it in local storage
41
+ const { seed } = await prompts({
42
+ type: "text",
43
+ name: "seed",
44
+ message: theme.text.bold(`Enter a secret string to use as your identity seed in Semaphore:`),
45
+ initial: false
46
+ })
47
+ identityString = seed as string
48
+ setLocalBandadaIdentity(identityString as string)
49
+ }
50
+ // 3. create a semaphore identity with _identity string as a seed
51
+ const identity = new Identity(identityString as string)
52
+
53
+ // 4. check if the user is a member of the group
54
+ console.log(`Checking Bandada membership...`)
55
+ const isMember = await isGroupMember(BANDADA_GROUP_ID, identity)
56
+ if (!isMember) {
57
+ await addMemberToGroup(BANDADA_GROUP_ID, BANDADA_DASHBOARD_URL, identity)
58
+ }
59
+
60
+ // 5. generate a proof that the user owns the commitment.
61
+ spinner.text = `Generating proof of identity...`
62
+ spinner.start()
63
+ // publicSignals = [hash(externalNullifier, identityNullifier), commitment]
64
+ const { proof, publicSignals } = await groth16.fullProve(
65
+ {
66
+ identityTrapdoor: identity.trapdoor,
67
+ identityNullifier: identity.nullifier,
68
+ externalNullifier: BANDADA_GROUP_ID
69
+ },
70
+ `${dirname(fileURLToPath(import.meta.url))}/public/mini-semaphore.wasm`,
71
+ `${dirname(fileURLToPath(import.meta.url))}/public/mini-semaphore.zkey`
72
+ )
73
+ spinner.succeed(`Proof generated.\n`)
74
+ spinner.text = `Sending proof to verification...`
75
+ spinner.start()
76
+ // 6. send proof to a cloud function that verifies it and checks membership
77
+ const cf = httpsCallable(firebaseFunctions, commonTerms.cloudFunctionsNames.bandadaValidateProof)
78
+ const result = await cf({
79
+ proof,
80
+ publicSignals
81
+ })
82
+ const { valid, token, message } = result.data as VerifiedBandadaResponse
83
+ if (!valid) {
84
+ showError(message, true)
85
+ }
86
+ spinner.succeed(`Proof verified.\n`)
87
+ spinner.text = `Authenticating...`
88
+ spinner.start()
89
+ // 7. Auth to p0tion firebase
90
+ const userCredentials = await signInWithCustomToken(getAuth(), token)
91
+ setLocalAccessToken(token)
92
+ spinner.succeed(`Authenticated as ${theme.text.bold(userCredentials.user.uid)}.`)
93
+
94
+ console.log(
95
+ `\n${theme.symbols.warning} You can always log out by running the ${theme.text.bold(
96
+ `phase2cli logout`
97
+ )} command`
98
+ )
99
+ } catch (error) {
100
+ // Delete local token.
101
+ console.log("An error crashed the process. Deleting local token and identity.")
102
+ console.error(error)
103
+ deleteLocalAccessToken()
104
+ deleteLocalBandadaIdentity()
105
+ }
106
+
107
+ process.exit(0)
108
+ }
109
+
110
+ export default authBandada
@@ -0,0 +1,20 @@
1
+ import { Command } from "commander"
2
+ import listParticipants from "./listParticipants.js"
3
+
4
+ const setCeremonyCommands = (program: Command) => {
5
+ const ceremony = program.command("ceremony").description("manage ceremonies")
6
+
7
+ ceremony
8
+ .command("participants")
9
+ .description("retrieve participants list of a ceremony")
10
+ .requiredOption(
11
+ "-c, --ceremony <string>",
12
+ "the prefix of the ceremony you want to retrieve information about",
13
+ ""
14
+ )
15
+ .action(listParticipants)
16
+
17
+ return ceremony
18
+ }
19
+
20
+ export default setCeremonyCommands
@@ -0,0 +1,30 @@
1
+ import { collection, doc, getDocs } from "firebase/firestore"
2
+ import { commonTerms, getAllCeremonies } from "@devtion/actions"
3
+ import { bootstrapCommandExecutionAndServices } from "../../lib/services.js"
4
+ import { showError } from "../../lib/errors.js"
5
+ import { promptForCeremonySelection } from "../../lib/prompts.js"
6
+
7
+ const listParticipants = async () => {
8
+ try {
9
+ const { firestoreDatabase } = await bootstrapCommandExecutionAndServices()
10
+
11
+ const allCeremonies = await getAllCeremonies(firestoreDatabase)
12
+ const selectedCeremony = await promptForCeremonySelection(allCeremonies, true)
13
+
14
+ const docRef = doc(firestoreDatabase, commonTerms.collections.ceremonies.name, selectedCeremony.id)
15
+ const participantsRef = collection(docRef, "participants")
16
+ const participantsSnapshot = await getDocs(participantsRef)
17
+ const participants = participantsSnapshot.docs.map((participantDoc) => participantDoc.data().userId)
18
+ console.log(participants)
19
+
20
+ /* const usersRef = collection(firestoreDatabase, "users")
21
+ const usersSnapshot = await getDocs(usersRef)
22
+ const users = usersSnapshot.docs.map((userDoc) => userDoc.data())
23
+ console.log(users) */
24
+ } catch (err: any) {
25
+ showError(`Something went wrong: ${err.toString()}`, true)
26
+ }
27
+ process.exit(0)
28
+ }
29
+
30
+ export default listParticipants
@@ -41,7 +41,7 @@ import {
41
41
  } from "../lib/utils.js"
42
42
  import { COMMAND_ERRORS, showError } from "../lib/errors.js"
43
43
  import { authWithToken, bootstrapCommandExecutionAndServices, checkAuth } from "../lib/services.js"
44
- import { getAttestationLocalFilePath, localPaths } from "../lib/localConfigs.js"
44
+ import { checkLocalBandadaIdentity, getAttestationLocalFilePath, localPaths } from "../lib/localConfigs.js"
45
45
  import theme from "../lib/theme.js"
46
46
  import { checkAndMakeNewDirectoryIfNonexistent, writeFile } from "../lib/files.js"
47
47
 
@@ -419,14 +419,19 @@ export const handlePublicAttestation = async (
419
419
 
420
420
  await sleep(1000) // workaround for file descriptor unexpected close.
421
421
 
422
- const gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix)
423
-
424
- console.log(
425
- `\n${theme.symbols.info} Your public attestation has been successfully posted as Github Gist (${theme.text.bold(
426
- theme.text.underlined(gistUrl)
427
- )})`
428
- )
422
+ let gistUrl = ""
423
+ const isBandada = checkLocalBandadaIdentity()
424
+ if (!isBandada) {
425
+ gistUrl = await publishGist(participantAccessToken, publicAttestation, ceremonyName, ceremonyPrefix)
429
426
 
427
+ console.log(
428
+ `\n${
429
+ theme.symbols.info
430
+ } Your public attestation has been successfully posted as Github Gist (${theme.text.bold(
431
+ theme.text.underlined(gistUrl)
432
+ )})`
433
+ )
434
+ }
430
435
  // Prepare a ready-to-share tweet.
431
436
  await handleTweetGeneration(ceremonyName, gistUrl)
432
437
  }
@@ -952,7 +957,7 @@ const contribute = async (opt: any) => {
952
957
  const userData = userDoc.data()
953
958
  if (!userData) {
954
959
  spinner.fail(
955
- `Unfortunately we could not find a user document with your information. This likely means that you did not pass the GitHub reputation checks and therefore are not elegible to contribute to any ceremony. If you believe you pass the requirements, it might be possible that your profile is private and we were not able to fetch your real statistics, in this case please consider making your profile public for the duration of the contribution. Please contact the coordinator if you believe this to be an error.`
960
+ `Unfortunately we could not find a user document with your information. This likely means that you did not pass the GitHub reputation checks and therefore are not eligible to contribute to any ceremony. If you believe you pass the requirements, it might be possible that your profile is private and we were not able to fetch your real statistics, in this case please consider making your profile public for the duration of the contribution. Please contact the coordinator if you believe this to be an error.`
956
961
  )
957
962
  process.exit(0)
958
963
  }
@@ -74,7 +74,7 @@ export const handleVerificationKey = async (
74
74
  // Write the verification key locally.
75
75
  writeLocalJsonFile(verificationKeyLocalFilePath, vKey)
76
76
 
77
- await sleep(3000) // workaound for file descriptor.
77
+ await sleep(3000) // workaround for file descriptor.
78
78
 
79
79
  // Upload verification key to storage.
80
80
  await multiPartUpload(
@@ -122,7 +122,7 @@ export const handleVerifierSmartContract = async (
122
122
  // Write the verification key locally.
123
123
  writeFile(verifierContractLocalFilePath, verifierCode)
124
124
 
125
- await sleep(3000) // workaound for file descriptor.
125
+ await sleep(3000) // workaround for file descriptor.
126
126
 
127
127
  // Upload verifier smart contract to storage.
128
128
  await multiPartUpload(
@@ -177,7 +177,7 @@ export const handleCircuitFinalization = async (
177
177
  circuitsLength
178
178
  )
179
179
 
180
- await sleep(2000) // workaound for descriptors.
180
+ await sleep(2000) // workaround for descriptors.
181
181
 
182
182
  // Extract data.
183
183
  const { prefix: circuitPrefix } = circuit.data
@@ -1,5 +1,6 @@
1
1
  export { default as setup } from "./setup.js"
2
2
  export { default as auth } from "./auth.js"
3
+ export { default as authBandada } from "./authBandada.js"
3
4
  export { default as contribute } from "./contribute.js"
4
5
  export { default as observe } from "./observe.js"
5
6
  export { default as finalize } from "./finalize.js"
@@ -6,7 +6,7 @@ import { showError } from "../lib/errors.js"
6
6
  import { askForConfirmation } from "../lib/prompts.js"
7
7
  import { customSpinner, sleep, terminate } from "../lib/utils.js"
8
8
  import theme from "../lib/theme.js"
9
- import { deleteLocalAccessToken } from "../lib/localConfigs.js"
9
+ import { deleteLocalAccessToken, deleteLocalBandadaIdentity } from "../lib/localConfigs.js"
10
10
 
11
11
  /**
12
12
  * Logout command.
@@ -53,6 +53,7 @@ const logout = async () => {
53
53
 
54
54
  // Delete local token.
55
55
  deleteLocalAccessToken()
56
+ deleteLocalBandadaIdentity()
56
57
 
57
58
  await sleep(3000) // ~3s.
58
59
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { zKey } from "snarkjs"
4
4
  import boxen from "boxen"
5
- import { createWriteStream, Dirent, renameSync } from "fs"
5
+ import { createWriteStream, Dirent, renameSync, existsSync } from "fs"
6
6
  import { pipeline } from "node:stream"
7
7
  import { promisify } from "node:util"
8
8
  import fetch from "node-fetch"
@@ -537,20 +537,33 @@ const setup = async (cmd: { template?: string; auth?: string }) => {
537
537
  `clock`
538
538
  )
539
539
  spinner.start()
540
- await zKey.newZKey(
541
- r1csLocalPathAndFileName,
542
- getPotLocalFilePath(circuit.files.potFilename),
543
- zkeyLocalPathAndFileName,
544
- undefined
545
- )
546
- spinner.succeed(
547
- `Generation of the genesis zKey for citcui ${theme.text.bold(circuit.name)} completed successfully`
548
- )
549
540
 
541
+ if (existsSync(zkeyLocalPathAndFileName)) {
542
+ spinner.succeed(
543
+ `The genesis zKey for circuit ${theme.text.bold(circuit.name)} is already present on disk`
544
+ )
545
+ } else {
546
+ await zKey.newZKey(
547
+ r1csLocalPathAndFileName,
548
+ getPotLocalFilePath(circuit.files.potFilename),
549
+ zkeyLocalPathAndFileName,
550
+ undefined
551
+ )
552
+ spinner.succeed(
553
+ `Generation of the genesis zKey for circuit ${theme.text.bold(circuit.name)} completed successfully`
554
+ )
555
+ }
556
+
557
+ const hashSpinner = customSpinner(
558
+ `Calculating hashes for circuit ${theme.text.bold(circuit.name)}...`,
559
+ `clock`
560
+ )
561
+ hashSpinner.start()
550
562
  // 4. calculate the hashes
551
563
  const wasmBlake2bHash = await blake512FromPath(wasmLocalPathAndFileName)
552
564
  const potBlake2bHash = await blake512FromPath(getPotLocalFilePath(circuit.files.potFilename))
553
565
  const initialZkeyBlake2bHash = await blake512FromPath(zkeyLocalPathAndFileName)
566
+ hashSpinner.succeed(`Hashes for circuit ${theme.text.bold(circuit.name)} calculated successfully`)
554
567
 
555
568
  // 5. upload the artifacts
556
569
 
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import { fileURLToPath } from "url"
7
7
  import {
8
8
  setup,
9
9
  auth,
10
+ authBandada,
10
11
  contribute,
11
12
  observe,
12
13
  finalize,
@@ -15,6 +16,7 @@ import {
15
16
  validate,
16
17
  listCeremonies
17
18
  } from "./commands/index.js"
19
+ import setCeremonyCommands from "./commands/ceremony/index.js"
18
20
 
19
21
  // Get pkg info (e.g., name, version).
20
22
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`
@@ -26,6 +28,10 @@ program.name(name).description(description).version(version)
26
28
 
27
29
  // User commands.
28
30
  program.command("auth").description("authenticate yourself using your Github account (OAuth 2.0)").action(auth)
31
+ program
32
+ .command("auth-bandada")
33
+ .description("authenticate yourself in a privacy-perserving manner using Bandada")
34
+ .action(authBandada)
29
35
  program
30
36
  .command("contribute")
31
37
  .description("compute contributions for a Phase2 Trusted Setup ceremony circuits")
@@ -44,27 +50,27 @@ program
44
50
  .action(logout)
45
51
  program
46
52
  .command("validate")
47
- .description("Validate that a Ceremony Setup file is correct")
53
+ .description("validate that a Ceremony Setup file is correct")
48
54
  .requiredOption("-t, --template <path>", "The path to the ceremony setup template", "")
49
55
  .option("-c, --constraints <number>", "The number of constraints to check against")
50
56
  .action(validate)
51
57
 
52
58
  // Only coordinator commands.
53
- const ceremony = program.command("coordinate").description("commands for coordinating a ceremony")
59
+ const coordinate = program.command("coordinate").description("commands for coordinating a ceremony")
54
60
 
55
- ceremony
61
+ coordinate
56
62
  .command("setup")
57
63
  .description("setup a Groth16 Phase 2 Trusted Setup ceremony for zk-SNARK circuits")
58
64
  .option("-t, --template <path>", "The path to the ceremony setup template", "")
59
65
  .option("-a, --auth <string>", "The Github OAuth 2.0 token", "")
60
66
  .action(setup)
61
67
 
62
- ceremony
68
+ coordinate
63
69
  .command("observe")
64
70
  .description("observe in real-time the waiting queue of each ceremony circuit")
65
71
  .action(observe)
66
72
 
67
- ceremony
73
+ coordinate
68
74
  .command("finalize")
69
75
  .description(
70
76
  "finalize a Phase2 Trusted Setup ceremony by applying a beacon, exporting verification key and verifier contract"
@@ -72,4 +78,6 @@ ceremony
72
78
  .option("-a, --auth <string>", "the Github OAuth 2.0 token", "")
73
79
  .action(finalize)
74
80
 
81
+ setCeremonyCommands(program)
82
+
75
83
  program.parseAsync(process.argv)
@@ -0,0 +1,51 @@
1
+ import { ApiSdk, GroupResponse } from "@bandada/api-sdk"
2
+ import { Identity } from "@semaphore-protocol/identity"
3
+ import open from "open"
4
+
5
+ import { askForConfirmation } from "../lib/prompts.js"
6
+ import { showError } from "./errors.js"
7
+ import theme from "../lib/theme.js"
8
+
9
+ const { BANDADA_API_URL } = process.env
10
+
11
+ const bandadaApi = new ApiSdk(BANDADA_API_URL)
12
+
13
+ export const getGroup = async (groupId: string): Promise<GroupResponse | null> => {
14
+ try {
15
+ const group = await bandadaApi.getGroup(groupId)
16
+ return group
17
+ } catch (error: any) {
18
+ showError(`Bandada getGroup error: ${error}`, true)
19
+ return null
20
+ }
21
+ }
22
+
23
+ export const getMembersOfGroup = async (groupId: string): Promise<string[] | null> => {
24
+ try {
25
+ const group = await bandadaApi.getGroup(groupId)
26
+ return group.members
27
+ } catch (error: any) {
28
+ showError(`Bandada getMembersOfGroup error: ${error}`, true)
29
+ return null
30
+ }
31
+ }
32
+
33
+ export const addMemberToGroup = async (groupId: string, dashboardUrl: string, identity: Identity) => {
34
+ const commitment = identity.commitment.toString()
35
+ const group = await bandadaApi.getGroup(groupId)
36
+ const providerName = group.credentials.id.split("_")[0].toLowerCase()
37
+
38
+ // 6. open a new window with the url:
39
+ const url = `${dashboardUrl}credentials?group=${groupId}&member=${commitment}&provider=${providerName}`
40
+ console.log(`${theme.text.bold(`Verification URL:`)} ${theme.text.underlined(url)}`)
41
+ open(url)
42
+
43
+ const { confirmation } = await askForConfirmation("Did you join the Bandada group in the browser?")
44
+ if (!confirmation) showError("You must join the Bandada group to continue the login process", true)
45
+ }
46
+
47
+ export const isGroupMember = async (groupId: string, identity: Identity): Promise<boolean> => {
48
+ const commitment = identity.commitment.toString()
49
+ const isMember: boolean = await bandadaApi.isGroupMember(groupId, commitment)
50
+ return isMember
51
+ }
package/src/lib/errors.ts CHANGED
@@ -6,7 +6,7 @@ export const CORE_SERVICES_ERRORS = {
6
6
  FIREBASE_TOKEN_EXPIRED_REMOVED_PERMISSIONS: `The Github authorization has failed due to lack of association between your account and the CLI`,
7
7
  FIREBASE_USER_DISABLED: `The Github account has been suspended by the ceremony coordinator(s), blocking the possibility of contribution. Please, contact them to understand the motivation behind it.`,
8
8
  FIREBASE_FAILED_CREDENTIALS_VERIFICATION: `Firebase cannot verify your Github credentials due to network errors. Please, try once again later.`,
9
- FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network erros. Please, try once again later and make sure your Internet connection is stable.`,
9
+ FIREBASE_NETWORK_ERROR: `Unable to reach Firebase due to network errors. Please, try once again later and make sure your Internet connection is stable.`,
10
10
  FIREBASE_CEREMONY_NOT_OPENED: `There are no ceremonies opened to contributions`,
11
11
  FIREBASE_CEREMONY_NOT_CLOSED: `There are no ceremonies ready to finalization`,
12
12
  AWS_CEREMONY_BUCKET_CREATION: `Unable to create a new bucket for the ceremony. Something went wrong during the creation. Please, repeat the process by providing a new ceremony name of the ceremony.`,
@@ -24,6 +24,10 @@ const config = new Conf({
24
24
  accessToken: {
25
25
  type: "string",
26
26
  default: ""
27
+ },
28
+ bandadaIdentity: {
29
+ type: "string",
30
+ default: ""
27
31
  }
28
32
  }
29
33
  })
@@ -91,6 +95,29 @@ export const setLocalAccessToken = (token: string) => config.set("accessToken",
91
95
  */
92
96
  export const deleteLocalAccessToken = () => config.delete("accessToken")
93
97
 
98
+ /**
99
+ * Return the Bandada identity, if present.
100
+ * @returns <string | undefined> - the Bandada identity if present, otherwise undefined.
101
+ */
102
+ export const getLocalBandadaIdentity = (): string | unknown => config.get("bandadaIdentity")
103
+
104
+ /**
105
+ * Check if the Bandada identity exists in the local storage.
106
+ * @returns <boolean>
107
+ */
108
+ export const checkLocalBandadaIdentity = (): boolean => config.has("bandadaIdentity") && !!config.get("bandadaIdentity")
109
+
110
+ /**
111
+ * Set the Bandada identity.
112
+ * @param identity <string> - the Bandada identity to be stored.
113
+ */
114
+ export const setLocalBandadaIdentity = (identity: string) => config.set("bandadaIdentity", identity)
115
+
116
+ /**
117
+ * Delete the stored Bandada identity.
118
+ */
119
+ export const deleteLocalBandadaIdentity = () => config.delete("bandadaIdentity")
120
+
94
121
  /**
95
122
  * Get the complete local file path.
96
123
  * @param cwd <string> - the current working directory path.
@@ -6,13 +6,18 @@ import {
6
6
  import clear from "clear"
7
7
  import figlet from "figlet"
8
8
  import { FirebaseApp } from "firebase/app"
9
- import { OAuthCredential } from "firebase/auth"
9
+ import { OAuthCredential, getAuth, signInWithCustomToken } from "firebase/auth"
10
10
  import dotenv from "dotenv"
11
11
  import { fileURLToPath } from "url"
12
12
  import { dirname } from "path"
13
13
  import { AuthUser } from "../types/index.js"
14
14
  import { CONFIG_ERRORS, CORE_SERVICES_ERRORS, showError, THIRD_PARTY_SERVICES_ERRORS } from "./errors.js"
15
- import { checkLocalAccessToken, deleteLocalAccessToken, getLocalAccessToken } from "./localConfigs.js"
15
+ import {
16
+ checkLocalAccessToken,
17
+ checkLocalBandadaIdentity,
18
+ deleteLocalAccessToken,
19
+ getLocalAccessToken
20
+ } from "./localConfigs.js"
16
21
  import theme from "./theme.js"
17
22
  import { exchangeGithubTokenForCredentials, getGithubProviderUserId, getUserHandleFromProviderUserId } from "./utils.js"
18
23
 
@@ -164,22 +169,30 @@ export const checkAuth = async (firebaseApp: FirebaseApp): Promise<AuthUser> =>
164
169
  // Retrieve local access token.
165
170
  const token = String(getLocalAccessToken())
166
171
 
167
- // Get credentials.
168
- const credentials = exchangeGithubTokenForCredentials(token)
169
-
170
- // Sign in to Firebase using credentials.
171
- await signInToFirebase(firebaseApp, credentials)
172
+ let providerUserId: string
173
+ let username: string
174
+ const isLocalBandadaIdentityStored = checkLocalBandadaIdentity()
175
+ if (isLocalBandadaIdentityStored) {
176
+ const userCredentials = await signInWithCustomToken(getAuth(), token)
177
+ providerUserId = userCredentials.user.uid
178
+ username = providerUserId
179
+ } else {
180
+ // Get credentials.
181
+ const credentials = exchangeGithubTokenForCredentials(token)
182
+
183
+ // Sign in to Firebase using credentials.
184
+ await signInToFirebase(firebaseApp, credentials)
185
+
186
+ // Get Github unique identifier (handle-id).
187
+ providerUserId = await getGithubProviderUserId(String(token))
188
+ username = getUserHandleFromProviderUserId(providerUserId)
189
+ }
172
190
 
173
191
  // Get current authenticated user.
174
192
  const user = getCurrentFirebaseAuthUser(firebaseApp)
175
193
 
176
- // Get Github unique identifier (handle-id).
177
- const providerUserId = await getGithubProviderUserId(String(token))
178
-
179
194
  // Greet the user.
180
- console.log(
181
- `Greetings, @${theme.text.bold(getUserHandleFromProviderUserId(providerUserId))} ${theme.emojis.wave}\n`
182
- )
195
+ console.log(`Greetings, @${theme.text.bold(username)} ${theme.emojis.wave}\n`)
183
196
 
184
197
  return {
185
198
  user,
package/src/lib/utils.ts CHANGED
@@ -155,7 +155,9 @@ export const getPublicAttestationGist = async (
155
155
  * @returns <string> - the third-party provider handle of the user.
156
156
  */
157
157
  export const getUserHandleFromProviderUserId = (providerUserId: string): string => {
158
- if (providerUserId.indexOf("-") === -1) showError(THIRD_PARTY_SERVICES_ERRORS.GITHUB_GET_GITHUB_ACCOUNT_INFO, true)
158
+ if (providerUserId.indexOf("-") === -1) {
159
+ return providerUserId
160
+ }
159
161
 
160
162
  return providerUserId.split("-")[0]
161
163
  }
@@ -68,3 +68,16 @@ export type GithubGistFile = {
68
68
  raw_url: string
69
69
  size: number
70
70
  }
71
+
72
+ /**
73
+ * Define the return object of the function that verifies the Bandada membership and proof.
74
+ * @typedef {Object} VerifiedBandadaResponse
75
+ * @property {boolean} valid - true if the proof is valid and the user is a member of the group; otherwise false.
76
+ * @property {string} message - a message describing the result of the verification.
77
+ * @property {string} token - the custom access token.
78
+ */
79
+ export type VerifiedBandadaResponse = {
80
+ valid: boolean
81
+ message: string
82
+ token: string
83
+ }