Skip to content

Commit

Permalink
feat(coordinator): add crypto and proof generation services
Browse files Browse the repository at this point in the history
- [x] Add key generation script
- [x] Add crypto service
- [x] Add proof generation service
- [x] Add launch instructions and code docs
  • Loading branch information
0xmad committed May 3, 2024
1 parent 28e41e2 commit 4d8958d
Show file tree
Hide file tree
Showing 14 changed files with 543 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/coordinator-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ on:
branches: [dev]
pull_request:

env:
COORDINATOR_PUBLIC_KEY_PATH: "./pub.key"
COORDINATOR_PRIVATE_KEY_PATH: "./priv.key"
COORDINATOR_TALLY_ZKEY_NAME: "TallyVotes"
COORDINATOR_MESSAGE_PROCESS_ZKEY_NAME: "ProcessMessages"
COORDINATOR_ZKEY_PATH: "./zkeys"

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
Expand Down
4 changes: 3 additions & 1 deletion contracts/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export { formatProofForVerifierContract, getDefaultSigner, getDefaultNetwork, ge
export { EMode } from "./constants";
export { Deployment } from "../tasks/helpers/Deployment";
export { ContractStorage } from "../tasks/helpers/ContractStorage";
export { ProofGenerator } from "../tasks/helpers/ProofGenerator";
export { Prover } from "../tasks/helpers/Prover";
export { EContracts } from "../tasks/helpers/types";
export { linkPoseidonLibraries } from "../tasks/helpers/abi";

export type { IVerifyingKeyStruct, SnarkProof, Groth16Proof } from "./types";
export type { IVerifyingKeyStruct, SnarkProof, Groth16Proof, Proof } from "./types";
export * from "../typechain-types";
20 changes: 20 additions & 0 deletions coordinator/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,22 @@
# 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

# Location of zkey, wasm files
COORDINATOR_ZKEY_PATH="./zkeys"

4 changes: 4 additions & 0 deletions coordinator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.key
*.key
zkeys/

6 changes: 5 additions & 1 deletion coordinator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
"README.md"
],
"scripts": {
"prebuild": "pnpm run generate-keypair",
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main",
"test": "jest",
"test:coverage": "jest --coverage",
"types": "tsc -p tsconfig.json --noEmit"
"types": "tsc -p tsconfig.json --noEmit",
"generate-keypair": "ts-node ./scripts/generateKeypair.ts"
},
"dependencies": {
"@nestjs/common": "^10.3.8",
Expand All @@ -42,9 +44,11 @@
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"fast-check": "^3.18.0",
"jest": "^29.5.0",
"supertest": "^7.0.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1",
"typescript": "^5.4.5"
},
"jest": {
Expand Down
25 changes: 25 additions & 0 deletions coordinator/scripts/generateKeypair.ts
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();
28 changes: 28 additions & 0 deletions coordinator/ts/crypto/__tests__/crypto.service.test.ts
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;
}),
);
});
});
57 changes: 57 additions & 0 deletions coordinator/ts/crypto/crypto.service.ts
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();
}
}
158 changes: 158 additions & 0 deletions coordinator/ts/proof/__tests__/proof.service.test.ts
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);
});
});
Loading

0 comments on commit 4d8958d

Please sign in to comment.