Skip to content

Commit

Permalink
feat(coordinator): auth-z and validation
Browse files Browse the repository at this point in the history
- [x] Add dto for proof generation with validation
- [x] Add account message signing guard
  • Loading branch information
0xmad authored and ctrlc03 committed May 13, 2024
1 parent 93ee900 commit f02fc66
Show file tree
Hide file tree
Showing 15 changed files with 608 additions and 37 deletions.
1 change: 0 additions & 1 deletion .github/workflows/coordinator-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ env:
COORDINATOR_MESSAGE_PROCESS_ZKEY_NAME: "ProcessMessages_10-2-1-2_test"
COORDINATOR_ZKEY_PATH: "./zkeys"
COORDINATOR_RAPIDSNARK_EXE: "~/rapidsnark/build/prover"
COORDINATOR_MNEMONIC: ${{ secrets.COORDINATOR_MNEMONIC }}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down
3 changes: 3 additions & 0 deletions coordinator/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ COORDINATOR_MNEMONIC=
# Coordinator RPC url
COORDINATOR_RPC_URL=http://localhost:8545

# Coordinator Ethereum address (see ts/auth/AccountSignatureGuard.service.ts)
COORDINATOR_ADDRESS=

23 changes: 11 additions & 12 deletions coordinator/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,26 @@ dotenv.config();

const parentDir = __dirname.includes("build") ? ".." : "";

const accounts = process.env.COORDINATOR_MNEMONIC
? {
mnemonic: process.env.COORDINATOR_MNEMONIC,
path: "m/44'/60'/0'/0",
initialIndex: 0,
count: 20,
}
: undefined; // use default accounts for tests

const config: HardhatUserConfig = {
defaultNetwork: "localhost",
networks: {
localhost: {
url: process.env.COORDINATOR_RPC_URL,
loggingEnabled: false,
accounts: {
mnemonic: process.env.COORDINATOR_MNEMONIC,
path: "m/44'/60'/0'/0",
initialIndex: 0,
count: 20,
},
accounts,
},
hardhat: {
loggingEnabled: false,
accounts: {
mnemonic: process.env.COORDINATOR_MNEMONIC,
path: "m/44'/60'/0'/0",
initialIndex: 0,
count: 20,
},
accounts,
},
},
paths: {
Expand Down
3 changes: 3 additions & 0 deletions coordinator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"@nestjs/platform-express": "^10.3.8",
"@nestjs/throttler": "^5.1.2",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"ethers": "^6.12.1",
"hardhat": "^2.22.3",
Expand Down Expand Up @@ -75,6 +77,7 @@
"collectCoverageFrom": [
"**/*.(t|j)s",
"!<rootDir>/ts/main.ts",
"!<rootDir>/ts/jest/*.js",
"!<rootDir>/hardhat.config.ts"
],
"coverageDirectory": "<rootDir>/coverage",
Expand Down
154 changes: 146 additions & 8 deletions coordinator/tests/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ValidationPipe, type INestApplication } from "@nestjs/common";
import { Test } from "@nestjs/testing";
import { getBytes, hashMessage, type Signer } from "ethers";
import hardhat from "hardhat";
import {
type DeployedContracts,
Expand All @@ -19,8 +21,6 @@ import request from "supertest";
import fs from "fs";
import path from "path";

import type { INestApplication } from "@nestjs/common";
import type { Signer } from "ethers";
import type { App } from "supertest/types";

import { AppModule } from "../ts/app.module";
Expand All @@ -39,9 +39,18 @@ describe("AppController (e2e)", () => {
let maciAddresses: DeployedContracts;
let pollContracts: PollContracts;

const getAuthorizationHeader = async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const signature = await signer.signMessage("message");
const digest = Buffer.from(getBytes(hashMessage("message"))).toString("hex");
return CryptoService.getInstance().encrypt(publicKey, `${signature}:${digest}`);
};

beforeAll(async () => {
[signer] = await hardhat.ethers.getSigners();

process.env.COORDINATOR_ADDRESS = await signer.getAddress();

await deployVkRegistryContract({ signer });
await setVerifyingKeys({
quiet: true,
Expand Down Expand Up @@ -82,9 +91,109 @@ describe("AppController (e2e)", () => {
}).compile();

app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.init();
});

describe("validation /v1/proof/generate POST", () => {
beforeAll(async () => {
const user = new Keypair();

await signup({ maciAddress: maciAddresses.maciAddress, maciPubKey: user.pubKey.serialize(), signer });
await publish({
pubkey: user.pubKey.serialize(),
stateIndex: 1n,
voteOptionIndex: 0n,
nonce: 1n,
pollId: 0n,
newVoteWeight: 9n,
maciAddress: maciAddresses.maciAddress,
salt: 0n,
privateKey: user.privKey.serialize(),
signer,
});
});

test("should throw an error if poll id is invalid", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "-1",
encryptedCoordinatorPrivateKey,
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
useQuadraticVoting: false,
})
.expect(400);
});

test("should throw an error if encrypted key is invalid", async () => {
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
encryptedCoordinatorPrivateKey: "",
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
useQuadraticVoting: false,
})
.expect(400);
});

test("should throw an error if maci address is invalid", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
encryptedCoordinatorPrivateKey,
maciContractAddress: "wrong",
tallyContractAddress: pollContracts.tally,
useQuadraticVoting: false,
})
.expect(400);
});

test("should throw an error if tally address is invalid", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
encryptedCoordinatorPrivateKey,
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: "invalid",
useQuadraticVoting: false,
})
.expect(400);
});
});

describe("/v1/proof/generate POST", () => {
beforeAll(async () => {
const user = new Keypair();
Expand All @@ -110,11 +219,13 @@ describe("AppController (e2e)", () => {
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
poll: 0,
encryptedCoordinatorPrivateKey,
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
Expand All @@ -131,11 +242,13 @@ describe("AppController (e2e)", () => {
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
poll: 0,
encryptedCoordinatorPrivateKey,
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
Expand All @@ -152,11 +265,13 @@ describe("AppController (e2e)", () => {
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
poll: 0,
encryptedCoordinatorPrivateKey,
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
Expand All @@ -168,10 +283,13 @@ describe("AppController (e2e)", () => {
test("should throw an error if coordinator key decryption is failed", async () => {
await mergeMessages({ pollId: 0n, signer });

const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
poll: 0,
encryptedCoordinatorPrivateKey: coordinatorKeypair.privKey.serialize(),
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
Expand All @@ -183,10 +301,13 @@ describe("AppController (e2e)", () => {
test("should throw an error if there is no such poll", async () => {
await mergeMessages({ pollId: 0n, signer });

const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "9000",
poll: 9000,
encryptedCoordinatorPrivateKey: coordinatorKeypair.privKey.serialize(),
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
Expand All @@ -195,6 +316,21 @@ describe("AppController (e2e)", () => {
.expect(400);
});

test("should throw an error if there is no authorization header", async () => {
await mergeMessages({ pollId: 0n, signer });

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.send({
poll: 0,
encryptedCoordinatorPrivateKey: coordinatorKeypair.privKey.serialize(),
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
useQuadraticVoting: false,
})
.expect(403);
});

test("should generate proofs properly", async () => {
await mergeSignups({ pollId: 0n, signer });

Expand All @@ -203,11 +339,13 @@ describe("AppController (e2e)", () => {
publicKey,
coordinatorKeypair.privKey.serialize(),
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
poll: "0",
poll: 0,
encryptedCoordinatorPrivateKey,
maciContractAddress: maciAddresses.maciAddress,
tallyContractAddress: pollContracts.tally,
Expand Down
2 changes: 1 addition & 1 deletion coordinator/ts/app.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe("AppController", () => {
let appController: AppController;

const defaultProofGeneratorArgs: IGenerateArgs = {
poll: 0n,
poll: 0,
maciContractAddress: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
tallyContractAddress: "0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321",
useQuadraticVoting: false,
Expand Down
9 changes: 6 additions & 3 deletions coordinator/ts/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Body, Controller, HttpException, HttpStatus, Post } from "@nestjs/common";
import { Body, Controller, HttpException, HttpStatus, Post, UseGuards } from "@nestjs/common";

import { AccountSignatureGuard } from "./auth/AccountSignatureGuard.service";
import { GenerateProofDto } from "./proof/dto";
import { ProofGeneratorService } from "./proof/proof.service";
import { IGenerateArgs, IGenerateData } from "./proof/types";
import { IGenerateData } from "./proof/types";

@Controller("v1/proof")
@UseGuards(AccountSignatureGuard)
export class AppController {
constructor(private readonly proofGeneratorService: ProofGeneratorService) {}

@Post("generate")
async generate(@Body() args: IGenerateArgs): Promise<IGenerateData> {
async generate(@Body() args: GenerateProofDto): Promise<IGenerateData> {
return this.proofGeneratorService.generate(args).catch((error: Error) => {
throw new HttpException("BadRequest", HttpStatus.BAD_REQUEST, { cause: error.message });
});
Expand Down
Loading

0 comments on commit f02fc66

Please sign in to comment.