-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(coordinator): add crypto and proof generation services
- [x] Add key generation script - [x] Add crypto service - [x] Add proof generation service - [x] Add launch instructions and code docs
- Loading branch information
Showing
14 changed files
with
550 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,25 @@ | ||
# Rate limit configuation | ||
TTL=60000 | ||
LIMIT=10 | ||
|
||
# Make sure your private and public RSA keys are generated (see package.json scripts) | ||
# Public key must be copied to frontend app to encrypt user sensitive data | ||
COORDINATOR_PUBLIC_KEY_PATH="./pub.key" | ||
COORDINATOR_PRIVATE_KEY_PATH="./priv.key" | ||
|
||
# Make sure you have TallyVotesQv.zkey, TallyVotesQv.wasm | ||
# and TallyVotesNonQv.zkey, TallyVotesNonQv.wasm inside COORDINATOR_ZKEY_PATH folder | ||
# https://maci.pse.dev/docs/trusted-setup | ||
COORDINATOR_TALLY_ZKEY_NAME=TallyVotes | ||
|
||
# Make sure you have ProcessMessagesQv.zkey, ProcessMessagesQv.wasm | ||
# and ProcessMessagesNonQv.zkey, ProcessMessagesNonQv.wasm inside COORDINATOR_ZKEY_PATH folder | ||
# https://maci.pse.dev/docs/trusted-setup | ||
COORDINATOR_MESSAGE_PROCESS_ZKEY_NAME=ProcessMessages | ||
|
||
# Rapidsnark executable path | ||
COORDINATOR_RAPIDSNARK_EXE= | ||
|
||
# Location of zkey, wasm files | ||
COORDINATOR_ZKEY_PATH="./zkeys" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
*.key | ||
*.key | ||
zkeys/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import dotenv from "dotenv"; | ||
|
||
import { generateKeyPairSync } from "crypto"; | ||
import fs from "fs"; | ||
import path from "path"; | ||
|
||
dotenv.config(); | ||
|
||
const MODULUS_LENGTH = 4096; | ||
|
||
export async function generateRsaKeypair(): Promise<void> { | ||
const keypair = generateKeyPairSync("rsa", { | ||
modulusLength: MODULUS_LENGTH, | ||
}); | ||
|
||
const publicKey = keypair.publicKey.export({ type: "pkcs1", format: "pem" }); | ||
const privateKey = keypair.privateKey.export({ type: "pkcs1", format: "pem" }); | ||
|
||
await Promise.all([ | ||
fs.promises.writeFile(path.resolve(process.env.COORDINATOR_PUBLIC_KEY_PATH!), publicKey), | ||
fs.promises.writeFile(path.resolve(process.env.COORDINATOR_PRIVATE_KEY_PATH!), privateKey), | ||
]); | ||
} | ||
|
||
generateRsaKeypair(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import fc from "fast-check"; | ||
|
||
import { generateKeyPairSync } from "crypto"; | ||
|
||
import { CryptoService } from "../crypto.service"; | ||
|
||
describe("CryptoService", () => { | ||
test("should encrypt and decrypt properly", () => { | ||
fc.assert( | ||
fc.property(fc.string(), (text: string) => { | ||
const service = CryptoService.getInstance(); | ||
|
||
const keypair = generateKeyPairSync("rsa", { | ||
modulusLength: 2048, | ||
}); | ||
|
||
const encryptedText = service.encrypt(keypair.publicKey.export({ type: "pkcs1", format: "pem" }), text); | ||
|
||
const decryptedText = service.decrypt( | ||
keypair.privateKey.export({ type: "pkcs1", format: "pem" }), | ||
encryptedText, | ||
); | ||
|
||
return decryptedText === text; | ||
}), | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { publicEncrypt, privateDecrypt, type KeyLike } from "crypto"; | ||
|
||
/** | ||
* CryptoService is responsible for encrypting and decrypting user sensitive data | ||
*/ | ||
export class CryptoService { | ||
/** | ||
* Singleton instance | ||
*/ | ||
private static INSTANCE?: CryptoService; | ||
|
||
/** | ||
* Empty constructor | ||
*/ | ||
private constructor() { | ||
// use singleton initialization | ||
} | ||
|
||
/** | ||
* Get singleton crypto service instance | ||
* | ||
* @returns crypto service instance | ||
*/ | ||
static getInstance(): CryptoService { | ||
if (!CryptoService.INSTANCE) { | ||
CryptoService.INSTANCE = new CryptoService(); | ||
} | ||
|
||
return CryptoService.INSTANCE; | ||
} | ||
|
||
/** | ||
* Encrypt plaintext with public key | ||
* | ||
* @param publicKey - public key | ||
* @param value - plaintext | ||
* @returns ciphertext | ||
*/ | ||
encrypt(publicKey: KeyLike, value: string): string { | ||
const encrypted = publicEncrypt(publicKey, Buffer.from(value)); | ||
|
||
return encrypted.toString("base64"); | ||
} | ||
|
||
/** | ||
* Decrypt ciphertext with private key | ||
* | ||
* @param privateKey - private key | ||
* @param value - ciphertext | ||
* @returns plaintext | ||
*/ | ||
decrypt(privateKey: KeyLike, value: string): string { | ||
const decryptedData = privateDecrypt(privateKey, Buffer.from(value, "base64")); | ||
|
||
return decryptedData.toString(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import dotenv from "dotenv"; | ||
import { ZeroAddress } from "ethers"; | ||
import { Deployment, ProofGenerator } from "maci-contracts"; | ||
|
||
import type { IGenerateArgs } from "../types"; | ||
|
||
import { CryptoService } from "../../crypto/crypto.service"; | ||
import { ProofGeneratorService } from "../proof.service"; | ||
|
||
dotenv.config(); | ||
|
||
jest.mock("hardhat", (): unknown => ({ | ||
network: { | ||
name: "localhost", | ||
config: { | ||
chain: { id: 0x1 }, | ||
}, | ||
}, | ||
})); | ||
|
||
jest.mock("maci-contracts", (): unknown => ({ | ||
...jest.requireActual("maci-contracts"), | ||
Deployment: { | ||
getInstance: jest.fn(), | ||
}, | ||
ProofGenerator: jest.fn(), | ||
})); | ||
|
||
jest.mock("../../crypto/crypto.service", (): unknown => ({ | ||
CryptoService: { | ||
getInstance: jest.fn(), | ||
}, | ||
})); | ||
|
||
describe("ProofGeneratorService", () => { | ||
const defaultArgs: IGenerateArgs = { | ||
poll: 1n, | ||
maciContractAddress: ZeroAddress, | ||
tallyContractAddress: ZeroAddress, | ||
useQuadraticVoting: false, | ||
encryptedCoordinatorPrivateKey: | ||
"siO9W/g7jNVXs9tOUv/pffrcqYdMlgdXw7nSSlqM1q1UvHGSSbhtLJpeT+nJKW7/+xrBTgI0wB866DSkg8Rgr8zD+POUMiKPrGqAO/XhrcmRDL+COURFNDRh9WGeAua6hdiNoufQYvXPl1iWyIYidSDbfmC2wR6F9vVkhg/6KDZyw8Wlr6LUh0RYT+hUHEwwGbz7MeqZJcJQSTpECPF5pnk8NTHL2W/XThaewB4n4HYqjDUbYLmBDLYWsDDMgoPo709a309rTq3uEe0YBgVF8g9aGxucTDhz+/LYYzqaeSxclUwen9Z4BGZjiDSPBZfooOEQEEwIJlViQ2kl1VeOKAmkiWEUVfItivmNbC/PNZchklmfFsGpiu4DT9UU9YVBN2OTcFYHHsslcaqrR7SuesqjluaGjG46oYEmfQlkZ4gXhavdWXw2ant+Tv6HRo4trqjoD1e3jUkN6gJMWomxOeRBTg0czBZlz/IwUtTpBHcKhi3EqGQo8OuQtWww+Ts7ySmeoONuovYUsIAppNuOubfUxvFJoTr2vKbWNAiYetw09kddkjmBe+S8A5PUiFOi262mfc7g5wJwPPP7wpTBY0Fya+2BCPzXqRLMOtNI+1tW3/UQLZYvEY8J0TxmhoAGZaRn8FKaosatRxDZTQS6QUNmKxpmUspkRKzTXN5lznM=", | ||
}; | ||
|
||
let mockContract = { | ||
polls: jest.fn(), | ||
getMainRoot: jest.fn(), | ||
treeDepths: jest.fn(), | ||
extContracts: jest.fn(), | ||
stateAqMerged: jest.fn(), | ||
}; | ||
|
||
let defaultProofGenerator = { | ||
generateMpProofs: jest.fn(), | ||
generateTallyProofs: jest.fn(), | ||
}; | ||
|
||
let defaultCryptoService = { | ||
decrypt: jest.fn(), | ||
}; | ||
|
||
const defaultDeploymentService = { | ||
setHre: jest.fn(), | ||
getDeployer: jest.fn(() => Promise.resolve({})), | ||
getContract: jest.fn(() => Promise.resolve(mockContract)), | ||
}; | ||
|
||
beforeEach(() => { | ||
mockContract = { | ||
polls: jest.fn(() => Promise.resolve()), | ||
getMainRoot: jest.fn(() => Promise.resolve(1n)), | ||
treeDepths: jest.fn(() => Promise.resolve([1, 2, 3])), | ||
extContracts: jest.fn(() => Promise.resolve({ messageAq: ZeroAddress })), | ||
stateAqMerged: jest.fn(() => Promise.resolve(true)), | ||
}; | ||
|
||
defaultProofGenerator = { | ||
generateMpProofs: jest.fn(() => Promise.resolve([1])), | ||
generateTallyProofs: jest.fn(() => Promise.resolve([1])), | ||
}; | ||
|
||
defaultCryptoService = { | ||
decrypt: jest.fn(() => "macisk.6d5efa8ebc6f7a6ee3e9bf573346af2df29b007b29ef420c030aa4a7f3410182"), | ||
}; | ||
|
||
(Deployment.getInstance as jest.Mock).mockReturnValue(defaultDeploymentService); | ||
|
||
(ProofGenerator as unknown as jest.Mock).mockReturnValue(defaultProofGenerator); | ||
|
||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-expect-error | ||
ProofGenerator.prepareState = jest.fn(() => | ||
Promise.resolve({ | ||
polls: new Map<bigint, unknown>([[1n, {}]]), | ||
}), | ||
); | ||
|
||
(CryptoService.getInstance as jest.Mock).mockReturnValue(defaultCryptoService); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test("should throw error if state is not merged yet", async () => { | ||
mockContract.stateAqMerged.mockResolvedValue(false); | ||
|
||
const service = new ProofGeneratorService(); | ||
|
||
await expect(service.generate(defaultArgs)).rejects.toThrow( | ||
"The state tree has not been merged yet. Please use the mergeSignups subcommmand to do so.", | ||
); | ||
}); | ||
|
||
test("should throw error if there is no any poll", async () => { | ||
mockContract.getMainRoot.mockResolvedValue(0n); | ||
|
||
const service = new ProofGeneratorService(); | ||
|
||
await expect(service.generate(defaultArgs)).rejects.toThrow( | ||
"The message tree has not been merged yet. Please use the mergeMessages subcommmand to do so.", | ||
); | ||
}); | ||
|
||
test("should throw error if poll is not found", async () => { | ||
const service = new ProofGeneratorService(); | ||
|
||
await expect(service.generate({ ...defaultArgs, poll: 2n })).rejects.toThrow("Poll 2 not found"); | ||
}); | ||
|
||
test("should throw error if coordinator key cannot be decrypted", async () => { | ||
defaultCryptoService.decrypt.mockReturnValue("unknown"); | ||
|
||
const service = new ProofGeneratorService(); | ||
|
||
await expect(service.generate({ ...defaultArgs, encryptedCoordinatorPrivateKey: "unknown" })).rejects.toThrow( | ||
"Cannot convert 0x to a BigInt", | ||
); | ||
}); | ||
|
||
test("should generate proofs properly for NonQv", async () => { | ||
const service = new ProofGeneratorService(); | ||
|
||
const data = await service.generate(defaultArgs); | ||
|
||
expect(data.processProofs).toHaveLength(1); | ||
expect(data.tallyProofs).toHaveLength(1); | ||
}); | ||
|
||
test("should generate proofs properly for Qv", async () => { | ||
const service = new ProofGeneratorService(); | ||
|
||
const data = await service.generate({ ...defaultArgs, useQuadraticVoting: true }); | ||
|
||
expect(data.processProofs).toHaveLength(1); | ||
expect(data.tallyProofs).toHaveLength(1); | ||
}); | ||
}); |
Oops, something went wrong.