Skip to content

Commit

Permalink
feat(compare artifacts hashes): added utilities to compare artifacts …
Browse files Browse the repository at this point in the history
…hashes

Added utility functions that can be used to verify whether two artifacts are the same. This can be
used for ceremony verification.
  • Loading branch information
ctrlc03 committed Mar 7, 2023
1 parent 335b5bb commit 6e96502
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 12 deletions.
13 changes: 13 additions & 0 deletions packages/actions/src/helpers/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,16 @@ export const blake512FromPath = async (path: fs.PathLike): Promise<string> => {
* @returns <string> - the HEX format of the SHA256 hash of the given value
*/
export const computeSHA256ToHex = (value: string): string => crypto.createHash("sha256").update(value).digest("hex")

/**
* Helper function that can be used to compare whether two files' hashes are equal or not.
* @param path1 <string> Path to the first file.
* @param path2 <string> Path to the second file.
* @returns <Promise<boolean>> Whether the files are equal or not.
*/
export const compareHashes = async (path1: string, path2: string): Promise<boolean> => {
const hash1 = await blake512FromPath(path1)
const hash2 = await blake512FromPath(path2)

return hash1 === hash2
}
38 changes: 37 additions & 1 deletion packages/actions/src/helpers/storage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Functions } from "firebase/functions"
import mime from "mime-types"
import fs from "fs"
import fs, { createWriteStream } from "fs"
import fetch from "@adobe/node-fetch-retry"
import https from "https"
import { ETagWithPartNumber, ChunkWithUrl, TemporaryParticipantContributionData } from "../types"
import { commonTerms } from "./constants"
import {
completeMultiPartUpload,
generateGetObjectPreSignedUrl,
generatePreSignedUrlsParts,
openMultiPartUpload,
temporaryStoreCurrentContributionMultiPartUploadId,
Expand Down Expand Up @@ -210,6 +211,41 @@ export const multiPartUpload = async (
)
}

/**
* Download an artifact from S3 (only for authorized users)
* @param cloudFunctions <Functions> Firebase cloud functions instance.
* @param bucketName <string> Name of the bucket where the artifact is stored.
* @param storagePath <string> Path to the artifact in the bucket.
* @param localPath <string> Path to the local file where the artifact will be saved.
*/
export const downloadCeremonyArtifact = async (
cloudFunctions: Functions,
bucketName: string,
storagePath: string,
localPath: string
) => {
// Request pre-signed url to make GET download request.
const getPreSignedUrl = await generateGetObjectPreSignedUrl(cloudFunctions, bucketName, storagePath)

// Make fetch to get info about the artifact.
const response = await fetch(getPreSignedUrl)

if (response.status !== 200 && !response.ok)
throw new Error(
`There was an erorr while downloading the object ${storagePath} from the bucket ${bucketName}. Please check the function inputs and try again.`
)

const content = response.body
// Prepare stream.
const writeStream = createWriteStream(localPath)

// Write chunk by chunk.
for await (const chunk of content) {
// Write chunk.
writeStream.write(chunk)
}
}

/**
* Get R1CS file path tied to a particular circuit of a ceremony in the storage.
* @notice each R1CS file in the storage must be stored in the following path: `circuits/<circuitPrefix>/<completeR1csFilename>`.
Expand Down
39 changes: 39 additions & 0 deletions packages/actions/src/helpers/verification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { groth16, zKey } from "snarkjs"
import fs from "fs"
import { Functions } from "firebase/functions"
import { downloadCeremonyArtifact } from "./storage"
import { compareHashes } from "./crypto"

/**
* Verify that a zKey is valid
Expand Down Expand Up @@ -126,3 +129,39 @@ export const exportVerifierAndVKey = async (
const verificationKeyJSONData = await exportVkey(finalZkeyPath)
fs.writeFileSync(vKeyLocalPath, JSON.stringify(verificationKeyJSONData))
}

/**
* Helper function used to compare two ceremony artifacts
* @param firebaseFunctions <Functions> Firebase functions object
* @param localPath1 <string> Local path to store the first artifact
* @param localPath2 <string> Local path to store the second artifact
* @param storagePath1 <string> Storage path to the first artifact
* @param storagePath2 <string> Storage path to the second artifact
* @param bucketName1 <string> Bucket name of the first artifact
* @param bucketName2 <string> Bucket name of the second artifact
* @param cleanup <boolean> Whether to delete the downloaded files or not
* @returns <Promise<boolean>> true if the hashes match, false otherwise
*/
export const compareCeremonyArtifacts = async (
firebaseFunctions: Functions,
localPath1: string,
localPath2: string,
storagePath1: string,
storagePath2: string,
bucketName1: string,
bucketName2: string,
cleanup: boolean
): Promise<boolean> => {
// 1. download files
await downloadCeremonyArtifact(firebaseFunctions, bucketName1, storagePath1, localPath1)
await downloadCeremonyArtifact(firebaseFunctions, bucketName2, storagePath2, localPath2)
// 2. compare hashes
const res = await compareHashes(localPath1, localPath2)
// 3. cleanup
if (cleanup) {
fs.unlinkSync(localPath1)
fs.unlinkSync(localPath2)
}
// 4. return result
return res
}
8 changes: 5 additions & 3 deletions packages/actions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
downloadCeremonyArtifact,
getBucketName,
multiPartUpload,
getR1csStorageFilePath,
Expand All @@ -24,11 +25,13 @@ export {
getCeremonyCircuits
} from "./helpers/database"
export {
compareCeremonyArtifacts,
exportVerifierAndVKey,
exportVerifierContract,
exportVkey,
generateGROTH16Proof,
verifyGROTH16Proof
verifyGROTH16Proof,
verifyZKey
} from "./helpers/verification"
export { initializeFirebaseCoreServices } from "./helpers/services"
export { signInToFirebaseWithCredentials, getCurrentFirebaseAuthUser, isCoordinator } from "./helpers/authentication"
Expand Down Expand Up @@ -77,5 +80,4 @@ export {
finalizeCircuit,
finalizeCeremony
} from "./helpers/functions"
export { toHex, blake512FromPath, computeSHA256ToHex } from "./helpers/crypto"
export { verifyZKey } from "./helpers/verification"
export { toHex, blake512FromPath, computeSHA256ToHex, compareHashes } from "./helpers/crypto"
124 changes: 116 additions & 8 deletions packages/actions/test/unit/verification.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
import chai, { expect } from "chai"
import chaiAsPromised from "chai-as-promised"
import { getAuth, signInWithEmailAndPassword } from "firebase/auth"
import dotenv from "dotenv"
import { cwd } from "process"
import fs from "fs"
import {
compareCeremonyArtifacts,
createS3Bucket,
exportVerifierAndVKey,
exportVerifierContract,
exportVkey,
generateGROTH16Proof,
getBucketName,
verifyGROTH16Proof,
verifyZKey
} from "../../src"
import { envType } from "../utils"
import {
cleanUpMockCeremony,
cleanUpMockUsers,
createMockCeremony,
createMockUser,
deleteAdminApp,
deleteBucket,
deleteObjectFromS3,
envType,
generateUserPasswords,
getStorageConfiguration,
initializeAdminServices,
initializeUserServices,
sleep,
uploadFileToS3
} from "../utils"
import { TestingEnvironment } from "../../src/types/enums"
import { fakeCeremoniesData, fakeCircuitsData, fakeUsersData } from "../data/samples"

chai.use(chaiAsPromised)
dotenv.config()

/**
* Unit test for Verification utilities.
*/

describe("Verification utilities", () => {
let wasmPath: string = ""
let zkeyPath: string = ""
Expand All @@ -36,6 +55,32 @@ describe("Verification utilities", () => {
vkeyPath = `${cwd()}/packages/actions/test/data/artifacts/verification_key_circuit.json`
}

const finalZkeyPath = `${cwd()}/packages/actions/test/data/artifacts/circuit-small_00001.zkey`
const verifierExportPath = `${cwd()}/packages/actions/test/data/artifacts/verifier.sol`
const vKeyExportPath = `${cwd()}/packages/actions/test/data/artifacts/vkey.json`
const solidityVersion = "0.8.10"

const { ceremonyBucketPostfix } = getStorageConfiguration()

// Initialize admin and user services.
const { adminFirestore, adminAuth } = initializeAdminServices()
const { userApp, userFunctions } = initializeUserServices()
const userAuth = getAuth(userApp)

const users = [fakeUsersData.fakeUser1]
const passwords = generateUserPasswords(users.length)

beforeAll(async () => {
for (let i = 0; i < users.length; i++) {
users[i].uid = await createMockUser(userApp, users[i].data.email, passwords[i], true, adminAuth)
}
})

afterAll(async () => {
await cleanUpMockUsers(adminAuth, adminFirestore, users)
await deleteAdminApp()
})

describe("generateGROTH16Proof", () => {
it("should generate a GROTH16 proof", async () => {
const inputs = {
Expand Down Expand Up @@ -87,12 +132,6 @@ describe("Verification utilities", () => {
).to.be.rejected
})
})

const finalZkeyPath = `${cwd()}/packages/actions/test/data/artifacts/circuit-small_00001.zkey`
const verifierExportPath = `${cwd()}/packages/actions/test/data/artifacts/verifier.sol`
const vKeyExportPath = `${cwd()}/packages/actions/test/data/artifacts/vkey.json`
const solidityVersion = "0.8.10"

describe("exportVerifierContract", () => {
if (envType === TestingEnvironment.PRODUCTION) {
it("should export the verifier contract", async () => {
Expand Down Expand Up @@ -199,6 +238,75 @@ describe("Verification utilities", () => {
)
})
})
if (envType === TestingEnvironment.PRODUCTION) {
describe("compareCeremonyArtifacts", () => {
const ceremony = fakeCeremoniesData.fakeCeremonyOpenedDynamic
const bucketName = getBucketName(ceremony.data.prefix!, ceremonyBucketPostfix)
const storagePath1 = "zkey1.zkey"
const storagePath2 = "zkey2.zkey"
const storagePath3 = "wasm.wasm"
const localPath1 = `${cwd()}/packages/actions/test/data/artifacts/zkey1.zkey`
const localPath2 = `${cwd()}/packages/actions/test/data/artifacts/zkey2.zkey`
const localPath3 = `${cwd()}/packages/actions/test/data/artifacts/wasm.wasm`
beforeAll(async () => {
// sign in as coordinator
await signInWithEmailAndPassword(userAuth, users[0].data.email, passwords[0])
// create mock ceremony
await createMockCeremony(adminFirestore, ceremony, fakeCircuitsData.fakeCircuitSmallNoContributors)
// create ceremony bucket
await createS3Bucket(userFunctions, bucketName)
await sleep(1000)
// need to upload files to S3
await uploadFileToS3(bucketName, storagePath1, zkeyPath)
await uploadFileToS3(bucketName, storagePath2, zkeyPath)
await uploadFileToS3(bucketName, storagePath3, wasmPath)
})
it("should return true when two artifacts are the same", async () => {
expect(
await compareCeremonyArtifacts(
userFunctions,
localPath1,
localPath2,
storagePath1,
storagePath2,
bucketName,
bucketName,
true
)
).to.be.true
})
it("should return false when two artifacts are not the same", async () => {
expect(
await compareCeremonyArtifacts(
userFunctions,
localPath1,
localPath3,
storagePath1,
storagePath3,
bucketName,
bucketName,
true
)
).to.be.false
})
afterAll(async () => {
await deleteObjectFromS3(bucketName, storagePath1)
await deleteObjectFromS3(bucketName, storagePath2)
await deleteObjectFromS3(bucketName, storagePath3)
await deleteBucket(bucketName)

if (fs.existsSync(localPath1)) fs.unlinkSync(localPath1)
if (fs.existsSync(localPath2)) fs.unlinkSync(localPath2)
if (fs.existsSync(localPath3)) fs.unlinkSync(localPath3)

await cleanUpMockCeremony(
adminFirestore,
ceremony.uid,
fakeCircuitsData.fakeCircuitSmallNoContributors.uid
)
})
})
}
afterAll(() => {
if (fs.existsSync(verifierExportPath)) {
fs.unlinkSync(verifierExportPath)
Expand Down

0 comments on commit 6e96502

Please sign in to comment.