Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(coordinator): add crypto and proof generation services #1424

Merged
merged 1 commit into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/coordinator-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ 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"
COORDINATOR_RAPIDSNARK_EXE: ""

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";
23 changes: 23 additions & 0 deletions coordinator/.env.example
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
ctrlc03 marked this conversation as resolved.
Show resolved Hide resolved
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
0xmad marked this conversation as resolved.
Show resolved Hide resolved
# 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"

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({ path: [path.resolve(__dirname, "../.env"), path.resolve(__dirname, "../.env.example")] });

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();
}
}
4 changes: 3 additions & 1 deletion coordinator/ts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { NestFactory } from "@nestjs/core";
import dotenv from "dotenv";
import helmet from "helmet";

dotenv.config();
import path from "path";

dotenv.config({ path: [path.resolve(__dirname, "../.env"), path.resolve(__dirname, "../.env.example")] });

async function bootstrap() {
const { AppModule } = await import("./app.module");
Expand Down
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
Loading