Skip to content

Commit

Permalink
feat(ceremony verification): implemented ceremony finalization verifi…
Browse files Browse the repository at this point in the history
…cation and test fix

Implemented a function that given a ceremony prefix will verify whether the finalization was
successful: all artifacts are uploaded and are valid. These are compared with local copies that are
generated on the fly. Also fixed failing prod tests. Ceremony verification is now a Hardhat task as
it requires smart contract interaction.
  • Loading branch information
ctrlc03 committed Mar 11, 2023
1 parent 600693d commit e7c657a
Show file tree
Hide file tree
Showing 22 changed files with 536 additions and 540 deletions.
2 changes: 1 addition & 1 deletion jest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"testPathIgnorePatterns": [".d.ts", ".js", "packages/actions/test/unit/contract.test.ts", "packages/actions/test/e2e/05-verification.test.ts"],
"testPathIgnorePatterns": [".d.ts", ".js", "packages/actions/test/unit/contract.test.ts"],
"moduleFileExtensions": ["ts", "js"],
"transform": {
"\\.(t|j)s?$": "babel-jest"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"build": "lerna run build",
"test": "yarn test:dev",
"test:dev": "NODE_ENV=dev yarn workspace @zkmpc/backend emulator:exec-test",
"test:prod": "export GOOGLE_APPLICATION_CREDENTIALS=\"./packages/backend/serviceAccountKey.json\" && NODE_ENV=prod yarn workspace @zkmpc/actions test:contracts && NODE_ENV=prod jest --config=jest.json --detectOpenHandles --forceExit --runInBand --coverage",
"test:prod": "yarn workspace @zkmpc/actions test:contracts && export GOOGLE_APPLICATION_CREDENTIALS=\"./packages/backend/serviceAccountKey.json\" && NODE_ENV=prod jest --config=jest.json --detectOpenHandles --forceExit --runInBand --coverage",
"test:ci-prod": "NODE_ENV=prod jest --config=jest.json --detectOpenHandles --forceExit --runInBand --coverage=false",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
Expand Down
1 change: 1 addition & 0 deletions packages/actions/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "@nomiclabs/hardhat-ethers"
import { HardhatUserConfig } from "hardhat/config"
import "./src/helpers/tasks"

const config: HardhatUserConfig = {
solidity: "0.8.18",
Expand Down
5 changes: 2 additions & 3 deletions packages/actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@
"build:watch": "rollup -c rollup.config.ts -w --configPlugin typescript",
"pre:publish": "yarn build",
"compile:contracts": "hardhat compile",
"test:contracts": "hardhat test test/unit/contract.test.ts",
"run:node": "hardhat node",
"test:contractse2e": "export GOOGLE_APPLICATION_CREDENTIALS=\"../backend/serviceAccountKey.json\" && NODE_ENV=prod hardhat test test/e2e/05-verification.test.ts"
"test:contracts": "export GOOGLE_APPLICATION_CREDENTIALS=\"../backend/serviceAccountKey.json\" && NODE_ENV=prod hardhat test test/unit/contract.test.ts",
"verify:ceremony": "export GOOGLE_APPLICATION_CREDENTIALS=\"../backend/serviceAccountKey.json\" && hardhat verifyCeremony"
},
"dependencies": {
"@octokit/auth-oauth-device": "^4.0.3",
Expand Down
62 changes: 49 additions & 13 deletions packages/actions/src/helpers/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import {
downloadAllCeremonyArtifacts,
exportVerifierAndVKey,
generateGROTH16Proof,
generateZkeyFromScratch,
getFinalContributionBeacon,
verifyGROTH16Proof,
verifyZKey
} from "./verification"
import { compareHashes } from "./crypto"
import { finalContributionIndex, verificationKeyAcronym, verifierSmartContractAcronym } from "./constants"

/**
* Formats part of a GROTH16 SNARK proof
Expand Down Expand Up @@ -147,19 +151,20 @@ export const verifyCeremony = async (
circuitInputs: object,
verifierTemplatePath: string,
signer: Signer,
coordinatorId: string,
logger?: any
): Promise<boolean> => {
// download all ceremony artifacts
): Promise<void> => {
// 1. download all ceremony artifacts
const ceremonyArtifacts = await downloadAllCeremonyArtifacts(functions, firestore, ceremonyPrefix, outputDirectory)

// if there are no ceremony artifacts, we throw an error
if (ceremonyArtifacts.length === 0)
throw new Error(
"There was an error while downloading all ceremony artifacts. Please review your ceremony prefix and try again."
)

// we verify each circuit separately
for (const ceremonyArtifact of ceremonyArtifacts) {
// 1. verify the zkeys
// 2. verify the final zKey
const isValid = await verifyZKey(
ceremonyArtifact.r1csLocalFilePath,
ceremonyArtifact.finalZkeyLocalFilePath,
Expand All @@ -171,11 +176,34 @@ export const verifyCeremony = async (
`The zkey for Circuit ${ceremonyArtifact.circuitPrefix} is not valid. Please check that the artifact is correct. If not, you might have to re run the final contribution to compute a valid final zKey.`
)

// re generate the zkey using the beacon and check hashes
// 3. get the final contribution beacon
const contributionBeacon = await getFinalContributionBeacon(
firestore,
ceremonyArtifact.ceremonyId,
ceremonyArtifact.circuitId,
coordinatorId
)
const generatedFinalZkeyPath = `${ceremonyArtifact.directoryRoot}/${ceremonyArtifact.circuitPrefix}_${finalContributionIndex}_verification.zkey`
// 4. re generate the zkey using the beacon and check hashes
await generateZkeyFromScratch(
true,
ceremonyArtifact.r1csLocalFilePath,
ceremonyArtifact.potLocalFilePath,
generatedFinalZkeyPath,
logger,
ceremonyArtifact.lastZkeyLocalFilePath,
coordinatorId,
contributionBeacon
)
const zKeysMatching = await compareHashes(generatedFinalZkeyPath, ceremonyArtifact.finalZkeyLocalFilePath)
if (!zKeysMatching)
throw new Error(
`The final zkey for the Circuit ${ceremonyArtifact.circuitPrefix} does not match the one generated from the beacon. Please confirm manually by downloading from the S3 bucket.`
)

// 2. extract the verifier and the vKey
const verifierLocalPath = `${ceremonyArtifact.directoryRoot}/Verifier_${ceremonyArtifact.circuitPrefix}.sol`
const vKeyLocalPath = `${ceremonyArtifact.directoryRoot}/${ceremonyArtifact.circuitPrefix}_vkey.json`
// 5. extract the verifier and the vKey
const verifierLocalPath = `${ceremonyArtifact.directoryRoot}/${ceremonyArtifact.circuitPrefix}_${verifierSmartContractAcronym}_verification.sol`
const vKeyLocalPath = `${ceremonyArtifact.directoryRoot}/${ceremonyArtifact.circuitPrefix}_${verificationKeyAcronym}_verification.json`
await exportVerifierAndVKey(
solidityVersion,
ceremonyArtifact.finalZkeyLocalFilePath,
Expand All @@ -184,9 +212,19 @@ export const verifyCeremony = async (
verifierTemplatePath
)

// compare with uploaded verifier and vkey
// 6. verify that the generated verifier and vkey match the ones downloaded from S3
const verifierMatching = await compareHashes(verifierLocalPath, ceremonyArtifact.verifierLocalFilePath)
if (!verifierMatching)
throw new Error(
`The verifier contract for the Contract ${ceremonyArtifact.circuitPrefix} does not match the one downloaded from S3. Please confirm manually by downloading from the S3 bucket.`
)
const vKeyMatching = await compareHashes(vKeyLocalPath, ceremonyArtifact.verificationKeyLocalFilePath)
if (!vKeyMatching)
throw new Error(
`The verification key for the Contract ${ceremonyArtifact.circuitPrefix} does not match the one downloaded from S3. Please confirm manually by downloading from the S3 bucket.`
)

// 3. generate a proof and verify it locally
// 7. generate a proof and verify it locally (use either of the downloaded or generated as the hashes will have matched at this point)
const { proof, publicSignals } = await generateGROTH16Proof(
circuitInputs,
ceremonyArtifact.finalZkeyLocalFilePath,
Expand All @@ -199,7 +237,7 @@ export const verifyCeremony = async (
`Could not verify the proof for Circuit ${ceremonyArtifact.circuitPrefix}. Please check that the artifacts are correct as well as the inputs to the circuit, and try again.`
)

// 4. deploy Verifier contract and verify the proof on-chain
// 8. deploy Verifier contract and verify the proof on-chain
const verifierContract = await deployVerifierContract(verifierLocalPath, signer)
const formattedProof = await formatSolidityCalldata(publicSignals, proof)
const isProofValidOnChain = await verifyGROTH16ProofOnChain(verifierContract, formattedProof)
Expand All @@ -208,6 +246,4 @@ export const verifyCeremony = async (
`Could not verify the proof on-chain for Circuit ${ceremonyArtifact.circuitPrefix}. Please check that the artifacts are correct as well as the inputs to the circuit, and try again.`
)
}

return true
}
36 changes: 36 additions & 0 deletions packages/actions/src/helpers/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { task } from "hardhat/config"
import { verifyCeremony } from "./contracts"
import { initializeUserServices } from "../../test/utils"

task("verifyCeremony", "A task that can be used to verify a ceremony finalization validity")
.addPositionalParam("ceremonyPrefix")
.addPositionalParam("outputDirectory")
.addPositionalParam("solidityVersion")
.addPositionalParam("wasmPath")
.addPositionalParam("circuitInputs")
.addPositionalParam("verifierTemplatePath")
.addPositionalParam("coordinatorUID")
.setAction(async (taskArgs, hre) => {
// get a signer
const [deployer] = await hre.ethers.getSigners()

// init user app
const { userFirestore, userFunctions } = initializeUserServices()

// verify ceremony
await verifyCeremony(
userFunctions,
userFirestore,
taskArgs.ceremonyPrefix,
taskArgs.outputDirectory,
taskArgs.solidityVersion,
taskArgs.wasmPath,
taskArgs.circuitInputs,
taskArgs.verifierTemplatePath,
deployer,
taskArgs.coordinatorUID
)

// if we are here it is because it didn't throw so we can safely assume that it all was veriifer
console.log(`[+] The artifacts generated by the ceremony ${taskArgs.ceremonyPrefix} are valid`)
})
75 changes: 67 additions & 8 deletions packages/actions/src/helpers/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,27 @@ import { groth16, zKey } from "snarkjs"
import fs from "fs"
import { Firestore, where } from "firebase/firestore"
import { Functions } from "firebase/functions"
import { numExpIterations, commonTerms, finalContributionIndex } from "./constants"
import {
numExpIterations,
commonTerms,
finalContributionIndex,
verifierSmartContractAcronym,
verificationKeyAcronym
} from "./constants"
import { compareHashes } from "./crypto"
import { downloadCeremonyArtifact, getBucketName, getZkeyStorageFilePath } from "./storage"
import { fromQueryToFirebaseDocumentInfo, getCeremonyCircuits, queryCollection } from "./database"
import {
downloadCeremonyArtifact,
getBucketName,
getVerificationKeyStorageFilePath,
getVerifierContractStorageFilePath,
getZkeyStorageFilePath
} from "./storage"
import {
fromQueryToFirebaseDocumentInfo,
getCeremonyCircuits,
getCircuitContributionsFromContributor,
queryCollection
} from "./database"
import { formatZkeyIndex } from "./utils"
import { CeremonyArtifacts } from "../types"

Expand Down Expand Up @@ -177,7 +194,7 @@ export const generateZkeyFromScratch = async (
} else await zKey.newZKey(r1csLocalPath, potLocalPath, zkeyLocalPath, logger)
}

/*
/**
* Helper function used to compare two ceremony artifacts
* @param firebaseFunctions <Functions> Firebase functions object
* @param localPath1 <string> Local path to store the first artifact
Expand Down Expand Up @@ -213,7 +230,7 @@ export const compareCeremonyArtifacts = async (
return res
}

/*
/**
* Given a ceremony prefix, download all the ceremony artifacts
* @param functions <Functions> firebase functions instance
* @param firestore <Firestore> firebase firestore instance
Expand Down Expand Up @@ -271,24 +288,66 @@ export const downloadAllCeremonyArtifacts = async (
)
const lastZKeyLocalPath = `${circuitDir}/${circuit.data.prefix}_${zkeyIndex}.zkey`
const finalZKeyName = `${circuit.data.prefix}_${finalContributionIndex}.zkey`
const finalZkeyPath = getZkeyStorageFilePath(circuit.data.prefix, finalZKeyName)
const finalZkeyStoragePath = getZkeyStorageFilePath(circuit.data.prefix, finalZKeyName)
const finalZKeyLocalPath = `${circuitDir}/${finalZKeyName}`

const verifierStoragePath = getVerifierContractStorageFilePath(
circuit.data.prefix,
`${verifierSmartContractAcronym}.sol`
)
const verifierLocalPath = `${circuitDir}/${circuit.data.prefix}_${verifierSmartContractAcronym}.sol`

const vKeyStoragePath = getVerificationKeyStorageFilePath(circuit.data.prefix, `${verificationKeyAcronym}.json`)
const vKeyLocalPath = `${circuitDir}/${circuit.data.prefix}_${verificationKeyAcronym}.json`

// download everything
await downloadCeremonyArtifact(functions, bucketName, potStoragePath, potLocalPath)
await downloadCeremonyArtifact(functions, bucketName, r1csStoragePath, r1csLocalPath)
await downloadCeremonyArtifact(functions, bucketName, lastZKeyStoragePath, lastZKeyLocalPath)
await downloadCeremonyArtifact(functions, bucketName, finalZkeyPath, finalZKeyLocalPath)
await downloadCeremonyArtifact(functions, bucketName, finalZkeyStoragePath, finalZKeyLocalPath)
await downloadCeremonyArtifact(functions, bucketName, verifierStoragePath, verifierLocalPath)
await downloadCeremonyArtifact(functions, bucketName, vKeyStoragePath, vKeyLocalPath)

ceremonyArtifacts.push({
ceremonyId: ceremony.id,
circuitPrefix: circuit.data.prefix,
circuitId: circuit.id,
directoryRoot: circuitDir,
potLocalFilePath: potLocalPath,
r1csLocalFilePath: r1csLocalPath,
finalZkeyLocalFilePath: finalZKeyLocalPath,
lastZkeyLocalFilePath: lastZKeyLocalPath
lastZkeyLocalFilePath: lastZKeyLocalPath,
verifierLocalFilePath: verifierLocalPath,
verificationKeyLocalFilePath: vKeyLocalPath
})
}

return ceremonyArtifacts
}

/**
* Fetch the final contribution beacon from Firestore
* @param firestore <Firestore> firebase firestore instance
* @param ceremonyId <string> ceremony id
* @param circuitId <string> circuit id
* @param participantId <string> participant id
* @returns <Promise<string>> final contribution beacon
*/
export const getFinalContributionBeacon = async (
firestore: Firestore,
ceremonyId: string,
circuitId: string,
participantId: string
): Promise<string> => {
const contributions = await getCircuitContributionsFromContributor(firestore, ceremonyId, circuitId, participantId)

const filtered = contributions
.filter((contributionDocument: any) => contributionDocument.data.zkeyIndex === finalContributionIndex)
.at(0)
if (!filtered)
throw new Error(
"Final contribution not found. Please check that you provided the correct input data and try again."
)

return filtered.data.beacon.value
}
19 changes: 19 additions & 0 deletions packages/actions/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,13 +480,32 @@ export type ParticipantDocumentReferenceAndData = {
data: ParticipantDocument
}

/**
* Define a ceremony artifacts with their local paths.
* @typedef {Object} CeremonyArtifacts
* @property {string} ceremonyId - the unique identifier of the ceremony.
* @property {string} circuitPrefix - the prefix of the circuit.
* @property {string} circuitId - the unique identifier of the circuit.
* @property {string} directoryRoot - the root directory of the ceremony.
* @property {string} potLocalFilePath - the local path of the pot file.
* @property {string} r1csLocalFilePath - the local path of the r1cs file.
* @property {string} finalZkeyLocalFilePath - the local path of the final zKey file.
* @property {string} lastZkeyLocalFilePath - the local path of the last zKey file.
* @property {string} verifierLocalFilePath - the local path of the verifier file.
* @property {string} verificationKeyLocalFilePath - the local path of the verification key file.
* @dev must be used for generating fake/mock documents when testing.
*/
export type CeremonyArtifacts = {
ceremonyId: string
circuitPrefix: string
circuitId: string
directoryRoot: string
potLocalFilePath: string
r1csLocalFilePath: string
finalZkeyLocalFilePath: string
lastZkeyLocalFilePath: string
verifierLocalFilePath: string
verificationKeyLocalFilePath: string
}

/**
Expand Down
Binary file not shown.
Loading

0 comments on commit e7c657a

Please sign in to comment.