Skip to content

Commit

Permalink
feat(coordinator): add generic errors
Browse files Browse the repository at this point in the history
- [x] Add errors enum
- [x] Test error codes
- [x] Add cors env configuration
  • Loading branch information
0xmad authored and ctrlc03 committed May 14, 2024
1 parent 83a5adb commit 7541c6f
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 45 deletions.
3 changes: 3 additions & 0 deletions coordinator/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ COORDINATOR_RPC_URL=http://localhost:8545
# Coordinator Ethereum address (see ts/auth/AccountSignatureGuard.service.ts)
COORDINATOR_ADDRESS=

# Allowed origin host
COORDINATOR_ALLOWED_ORIGIN=

120 changes: 95 additions & 25 deletions coordinator/tests/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidationPipe, type INestApplication } from "@nestjs/common";
import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common";
import { Test } from "@nestjs/testing";
import { getBytes, hashMessage, type Signer } from "ethers";
import hardhat from "hardhat";
Expand All @@ -24,6 +24,7 @@ import path from "path";
import type { App } from "supertest/types";

import { AppModule } from "../ts/app.module";
import { ErrorCodes } from "../ts/common";
import { CryptoService } from "../ts/crypto/crypto.service";

const STATE_TREE_DEPTH = 10;
Expand Down Expand Up @@ -122,7 +123,7 @@ describe("AppController (e2e)", () => {
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
Expand All @@ -133,22 +134,34 @@ describe("AppController (e2e)", () => {
useQuadraticVoting: false,
})
.expect(400);

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: ["poll must not be less than 0", "poll must be an integer number"],
});
});

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

await request(app.getHttpServer() as App)
const result = 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,
useQuadraticVoting: false,
})
.expect(400);

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: ["encryptedCoordinatorPrivateKey must be longer than or equal to 1 characters"],
});
});

test("should throw an error if maci address is invalid", async () => {
Expand All @@ -159,17 +172,23 @@ describe("AppController (e2e)", () => {
);
const encryptedHeader = await getAuthorizationHeader();

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

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: ["maciContractAddress must be an Ethereum address"],
});
});

test("should throw an error if tally address is invalid", async () => {
Expand All @@ -180,17 +199,23 @@ describe("AppController (e2e)", () => {
);
const encryptedHeader = await getAuthorizationHeader();

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

expect(result.body).toStrictEqual({
error: "Bad Request",
statusCode: HttpStatus.BAD_REQUEST,
message: ["tallyContractAddress must be an Ethereum address"],
});
});
});

Expand Down Expand Up @@ -221,7 +246,7 @@ describe("AppController (e2e)", () => {
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
Expand All @@ -232,19 +257,22 @@ describe("AppController (e2e)", () => {
useQuadraticVoting: false,
})
.expect(400);
});

test("should throw an error if messages are not merged", async () => {
await timeTravel({ seconds: 30, signer });
expect(result.body).toStrictEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: ErrorCodes.NOT_MERGED_STATE_TREE,
});
});

test("should throw an error if signups are not merged", 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)
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
Expand All @@ -255,10 +283,16 @@ describe("AppController (e2e)", () => {
useQuadraticVoting: false,
})
.expect(400);

expect(result.body).toStrictEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: ErrorCodes.NOT_MERGED_STATE_TREE,
});
});

test("should throw an error if signups are not merged", async () => {
await mergeMessages({ pollId: 0n, signer });
test("should throw an error if messages are not merged", async () => {
await timeTravel({ seconds: 30, signer });
await mergeSignups({ pollId: 0n, signer });

const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
Expand All @@ -267,7 +301,7 @@ describe("AppController (e2e)", () => {
);
const encryptedHeader = await getAuthorizationHeader();

await request(app.getHttpServer() as App)
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
Expand All @@ -278,14 +312,19 @@ describe("AppController (e2e)", () => {
useQuadraticVoting: false,
})
.expect(400);

expect(result.body).toStrictEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: ErrorCodes.NOT_MERGED_MESSAGE_TREE,
});
});

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)
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
Expand All @@ -296,14 +335,17 @@ describe("AppController (e2e)", () => {
useQuadraticVoting: false,
})
.expect(400);

expect(result.body).toStrictEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: ErrorCodes.DECRYPTION,
});
});

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)
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.set("Authorization", encryptedHeader)
.send({
Expand All @@ -314,12 +356,15 @@ describe("AppController (e2e)", () => {
useQuadraticVoting: false,
})
.expect(400);

expect(result.body).toStrictEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: ErrorCodes.POLL_NOT_FOUND,
});
});

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

await request(app.getHttpServer() as App)
const result = await request(app.getHttpServer() as App)
.post("/v1/proof/generate")
.send({
poll: 0,
Expand All @@ -329,11 +374,36 @@ describe("AppController (e2e)", () => {
useQuadraticVoting: false,
})
.expect(403);

expect(result.body).toStrictEqual({
error: "Forbidden",
message: "Forbidden resource",
statusCode: HttpStatus.FORBIDDEN,
});
});

test("should generate proofs properly", async () => {
await mergeSignups({ pollId: 0n, signer });
test("should throw error if coordinator key cannot be decrypted", async () => {
const encryptedHeader = await getAuthorizationHeader();

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

expect(result.body).toStrictEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: ErrorCodes.DECRYPTION,
});
});

test("should generate proofs properly", async () => {
const publicKey = await fs.promises.readFile(process.env.COORDINATOR_PUBLIC_KEY_PATH!);
const encryptedCoordinatorPrivateKey = CryptoService.getInstance().encrypt(
publicKey,
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 @@ -62,7 +62,7 @@ describe("AppController", () => {
mockGeneratorService.generate.mockRejectedValue(error);

await expect(appController.generate(defaultProofGeneratorArgs)).rejects.toThrow(
new HttpException("BadRequest", HttpStatus.BAD_REQUEST, { cause: error.message }),
new HttpException(error.message, HttpStatus.BAD_REQUEST),
);
});
});
Expand Down
2 changes: 1 addition & 1 deletion coordinator/ts/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class AppController {
@Post("generate")
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 });
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
});
}
}
11 changes: 11 additions & 0 deletions coordinator/ts/common/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Error codes that are used for api responses
*/
export enum ErrorCodes {
NOT_MERGED_STATE_TREE = "0",
NOT_MERGED_MESSAGE_TREE = "1",
PRIVATE_KEY_MISMATCH = "2",
POLL_NOT_FOUND = "3",
DECRYPTION = "4",
ENCRYPTION = "5",
}
1 change: 1 addition & 0 deletions coordinator/ts/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ErrorCodes } from "./errors";
13 changes: 13 additions & 0 deletions coordinator/ts/crypto/__tests__/crypto.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@ import fc from "fast-check";

import { generateKeyPairSync } from "crypto";

import { ErrorCodes } from "../../common";
import { CryptoService } from "../crypto.service";

describe("CryptoService", () => {
test("should throw encryption error if key is invalid", () => {
const service = CryptoService.getInstance();

expect(() => service.encrypt("", "")).toThrow(ErrorCodes.ENCRYPTION);
});

test("should throw decryption error if key is invalid", () => {
const service = CryptoService.getInstance();

expect(() => service.decrypt("", "")).toThrow(ErrorCodes.DECRYPTION);
});

test("should encrypt and decrypt properly", () => {
fc.assert(
fc.property(fc.string(), (text: string) => {
Expand Down
18 changes: 14 additions & 4 deletions coordinator/ts/crypto/crypto.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { publicEncrypt, privateDecrypt, type KeyLike } from "crypto";

import { ErrorCodes } from "../common";

/**
* CryptoService is responsible for encrypting and decrypting user sensitive data
*/
Expand Down Expand Up @@ -37,9 +39,13 @@ export class CryptoService {
* @returns ciphertext
*/
encrypt(publicKey: KeyLike, value: string): string {
const encrypted = publicEncrypt(publicKey, Buffer.from(value));
try {
const encrypted = publicEncrypt(publicKey, Buffer.from(value));

return encrypted.toString("base64");
return encrypted.toString("base64");
} catch (error) {
throw new Error(ErrorCodes.ENCRYPTION);
}
}

/**
Expand All @@ -50,8 +56,12 @@ export class CryptoService {
* @returns plaintext
*/
decrypt(privateKey: KeyLike, value: string): string {
const decryptedData = privateDecrypt(privateKey, Buffer.from(value, "base64"));
try {
const decryptedData = privateDecrypt(privateKey, Buffer.from(value, "base64"));

return decryptedData.toString();
return decryptedData.toString();
} catch (error) {
throw new Error(ErrorCodes.DECRYPTION);
}
}
}
2 changes: 1 addition & 1 deletion coordinator/ts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
app.use(helmet());
app.enableCors();
app.enableCors({ origin: process.env.COORDINATOR_ALLOWED_ORIGIN });
await app.listen(3000);
}

Expand Down
Loading

0 comments on commit 7541c6f

Please sign in to comment.