Skip to content

Commit

Permalink
feat: add session keys service
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrlc03 committed Aug 23, 2024
1 parent e72cfbb commit d0d5b6f
Show file tree
Hide file tree
Showing 19 changed files with 744 additions and 5 deletions.
3 changes: 2 additions & 1 deletion packages/coordinator/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ SUBGRAPH_DEPLOY_KEY=
# Subgraph project folder
SUBGRAPH_FOLDER=./node_modules/maci-subgraph


# API Key for Pimlico RPC Bundler
PIMLICO_API_KEY=""
2 changes: 1 addition & 1 deletion packages/coordinator/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
zkeys/
proofs/
tally.json

session-keys.json
13 changes: 12 additions & 1 deletion packages/coordinator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,30 +37,38 @@
"@nestjs/websockets": "^10.3.10",
"@nomicfoundation/hardhat-ethers": "^3.0.6",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@zerodev/ecdsa-validator": "^5.3.1",
"@zerodev/permissions": "^5.4.3",
"@zerodev/sdk": "^5.3.8",
"@zerodev/session-key": "^5.4.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"ethers": "^6.13.1",
"hardhat": "^2.22.6",
"helmet": "^7.1.0",
"lowdb": "^1.0.0",
"maci-circuits": "^2.1.0",
"maci-cli": "^2.1.0",
"maci-contracts": "^2.1.0",
"maci-domainobjs": "^2.0.0",
"maci-subgraph": "^2.1.0",
"mustache": "^4.2.0",
"permissionless": "^0.1.44",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"socket.io": "^4.7.5",
"tar": "^7.4.1",
"ts-node": "^10.9.1"
"ts-node": "^10.9.1",
"viem": "^2.7.15"
},
"devDependencies": {
"@nestjs/cli": "^10.4.2",
"@nestjs/schematics": "^10.1.2",
"@nestjs/testing": "^10.3.10",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/lowdb": "^1.0.15",
"@types/node": "^20.14.11",
"@types/supertest": "^6.0.2",
"fast-check": "^3.20.0",
Expand Down Expand Up @@ -103,6 +111,9 @@
"!<rootDir>/ts/jest/*.js",
"!<rootDir>/hardhat.config.js"
],
"coveragePathIgnorePatterns": [
"<rootDir>/ts/sessionKeys/__tests__/utils.ts"
],
"coverageDirectory": "<rootDir>/coverage",
"testEnvironment": "node"
}
Expand Down
2 changes: 2 additions & 0 deletions packages/coordinator/ts/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ThrottlerModule } from "@nestjs/throttler";
import { CryptoModule } from "./crypto/crypto.module";
import { FileModule } from "./file/file.module";
import { ProofModule } from "./proof/proof.module";
import { SessionKeysModule } from "./sessionKeys/sessionKeys.module";
import { SubgraphModule } from "./subgraph/subgraph.module";

@Module({
Expand All @@ -18,6 +19,7 @@ import { SubgraphModule } from "./subgraph/subgraph.module";
CryptoModule,
SubgraphModule,
ProofModule,
SessionKeysModule,
],
})
export class AppModule {}
17 changes: 17 additions & 0 deletions packages/coordinator/ts/common/__tests__/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { genPimlicoRPCUrl } from "../accountAbstraction";
import { ErrorCodes } from "../errors";

describe("common", () => {
describe("genPimlicoRPCUrl", () => {
test("should return the correct RPCUrl", () => {
const rpcUrl = genPimlicoRPCUrl("optimism-sepolia");
expect(rpcUrl).toBeDefined();
expect(rpcUrl).toContain("https://api.pimlico.io/v2/optimism-sepolia/rpc");
});

test("should throw when PIMLICO_API_KEY is not set", () => {
delete process.env.PIMLICO_API_KEY;
expect(() => genPimlicoRPCUrl("optimism-sepolia")).toThrow(ErrorCodes.PIMLICO_API_KEY_NOT_SET);
});
});
});
20 changes: 20 additions & 0 deletions packages/coordinator/ts/common/accountAbstraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import dotenv from "dotenv";

import { ErrorCodes } from "./errors";

dotenv.config();

/**
* Generate the RPCUrl for Pimlico based on the chain we need to interact with
* @param network - the network we want to interact with
* @returns the RPCUrl for the network
*/
export const genPimlicoRPCUrl = (network: string): string => {
const pimlicoAPIKey = process.env.PIMLICO_API_KEY;

if (!pimlicoAPIKey) {
throw new Error(ErrorCodes.PIMLICO_API_KEY_NOT_SET);
}

return `https://api.pimlico.io/v2/${network}/rpc?apikey=${pimlicoAPIKey}`;
};
3 changes: 3 additions & 0 deletions packages/coordinator/ts/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export enum ErrorCodes {
ENCRYPTION = "5",
FILE_NOT_FOUND = "6",
SUBGRAPH_DEPLOY = "7",
SESSION_KEY_NOT_FOUND = "8",
PIMLICO_API_KEY_NOT_SET = "9",
INVALID_APPROVAL = "10",
}
28 changes: 28 additions & 0 deletions packages/coordinator/ts/file/__tests__/file.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ describe("FileService", () => {
jest.clearAllMocks();
});

test("should save session key properly", () => {
const service = new FileService();

const sessionKeyAddress = "0x123";
const sessionKey = "0x456";

service.storeSessionKey(sessionKey, sessionKeyAddress);

const storedSessionKey = service.getSessionKey(sessionKeyAddress);

expect(storedSessionKey).toEqual(sessionKey);
});

test("should delete session key properly", () => {
const service = new FileService();

const sessionKeyAddress = "0x123";
const sessionKey = "0x456";

service.storeSessionKey(sessionKey, sessionKeyAddress);

service.deleteSessionKey(sessionKeyAddress);

const deletedSessionKey = service.getSessionKey(sessionKeyAddress);

expect(deletedSessionKey).toBeUndefined();
});

test("should return public key properly", async () => {
const service = new FileService();

Expand Down
44 changes: 44 additions & 0 deletions packages/coordinator/ts/file/file.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { Injectable, Logger } from "@nestjs/common";
import low from "lowdb";
import FileSync from "lowdb/adapters/FileSync";

import fs from "fs";
import path from "path";

import type { IGetPrivateKeyData, IGetPublicKeyData, IGetZkeyFilePathsData } from "./types";
import type { Hex } from "viem";

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

/**
* Internal storage structure type.
* named: keys can be queried by name
*/
type TStorage = Record<string, Hex>;

/**
* FileService is responsible for working with local files like:
* 1. RSA public/private keys
Expand All @@ -19,11 +28,46 @@ export class FileService {
*/
private readonly logger: Logger;

/**
* Json file database instance
*/
private db: low.LowdbSync<TStorage>;

/**
* Initialize service
*/
constructor() {
this.logger = new Logger(FileService.name);
this.db = low(new FileSync<TStorage>(path.resolve(__dirname, "..", "..", "./session-keys.json")));
}

/**
* Store session key
*
* @param sessionKey - session key
* @param address - key address
*/
storeSessionKey(sessionKey: Hex, address: string): void {
this.db.set(address, sessionKey).write();
}

/**
* Delete session key
*
* @param address - key address
*/
deleteSessionKey(address: string): void {
this.db.unset(address).write();
}

/**
* Get session key
*
* @param address - key name
* @returns session key
*/
getSessionKey(address: string): Hex | undefined {
return this.db.get(address).value() as Hex | undefined;
}

/**
Expand Down
1 change: 0 additions & 1 deletion packages/coordinator/ts/proof/proof.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export class ProofGeneratorService {
) {
this.deployment = Deployment.getInstance(hre);
this.deployment.setHre(hre);
this.fileService = fileService;
this.logger = new Logger(ProofGeneratorService.name);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Test } from "@nestjs/testing";
import { zeroAddress } from "viem";

import type { IGenerateSessionKeyReturn } from "../types";

import { SessionKeysController } from "../sessionKeys.controller";
import { SessionKeysService } from "../sessionKeys.service";

describe("SessionKeysController", () => {
let sessionKeysController: SessionKeysController;

const mockSessionKeysService = {
generateSessionKey: jest.fn(),
deactivateSessionKey: jest.fn(),
};

const defaultGenerateSessionKeyReturn: IGenerateSessionKeyReturn = {
sessionKeyAddress: zeroAddress,
};

beforeEach(async () => {
const app = await Test.createTestingModule({
controllers: [SessionKeysController],
})
.useMocker((token) => {
if (token === SessionKeysService) {
mockSessionKeysService.generateSessionKey.mockResolvedValue(defaultGenerateSessionKeyReturn);
return mockSessionKeysService;
}

return jest.fn();
})
.compile();

sessionKeysController = app.get<SessionKeysController>(SessionKeysController);
});

afterEach(() => {
jest.clearAllMocks();
});

describe("v1/session-keys/generate", () => {
test("should return a session key address", async () => {
const data = await sessionKeysController.generateSessionKey();
expect(data).toStrictEqual(defaultGenerateSessionKeyReturn);
});
});

describe("v1/session-keys/delete", () => {
test("should delete a session key", () => {
sessionKeysController.deactivateSessionKey({
sessionKeyAddress: zeroAddress,
});
expect(mockSessionKeysService.deactivateSessionKey).toHaveBeenCalledWith(zeroAddress);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import dotenv from "dotenv";
import { ZeroAddress } from "ethers";
import { zeroAddress } from "viem";
import { optimismSepolia } from "viem/chains";

import { KeyLike } from "crypto";

import { ErrorCodes } from "../../common";
import { CryptoService } from "../../crypto/crypto.service";
import { FileService } from "../../file/file.service";
import { SessionKeysService } from "../sessionKeys.service";

import { mockSessionKeyApproval } from "./utils";

dotenv.config();

describe("SessionKeysService", () => {
afterEach(() => {
jest.clearAllMocks();
});

const cryptoService = new CryptoService();
const fileService = new FileService();
const sessionKeysService = new SessionKeysService(cryptoService, fileService);
let publicKey: KeyLike;

beforeAll(async () => {
publicKey = (await fileService.getPublicKey()).publicKey;
});

describe("generateSessionKey", () => {
test("should generate and store a session key", () => {
const sessionKeyAddress = sessionKeysService.generateSessionKey();
expect(sessionKeyAddress).toBeDefined();
expect(sessionKeyAddress).not.toEqual(ZeroAddress);

const sessionKey = fileService.getSessionKey(sessionKeyAddress.sessionKeyAddress);
expect(sessionKey).toBeDefined();
});
});

describe("deactivateSessionKey", () => {
test("should delete a session key", () => {
const sessionKeyAddress = sessionKeysService.generateSessionKey();
expect(sessionKeyAddress).toBeDefined();
expect(sessionKeyAddress).not.toEqual(ZeroAddress);

const sessionKey = fileService.getSessionKey(sessionKeyAddress.sessionKeyAddress);
expect(sessionKey).toBeDefined();

sessionKeysService.deactivateSessionKey(sessionKeyAddress.sessionKeyAddress);
const sessionKeyDeleted = fileService.getSessionKey(sessionKeyAddress.sessionKeyAddress);
expect(sessionKeyDeleted).toBeUndefined();
});
});

describe("generateClientFromSessionKey", () => {
test("should fail to generate a client with an invalid approval", async () => {
const sessionKeyAddress = sessionKeysService.generateSessionKey();
const approval = await mockSessionKeyApproval(sessionKeyAddress.sessionKeyAddress);
const encryptedApproval = cryptoService.encrypt(publicKey, approval);
await expect(
sessionKeysService.generateClientFromSessionKey(
sessionKeyAddress.sessionKeyAddress,
encryptedApproval,
optimismSepolia,
),
).rejects.toThrow(ErrorCodes.INVALID_APPROVAL);
});

test("should throw when given a non existent session key address", async () => {
const approval = await mockSessionKeyApproval(zeroAddress);
const encryptedApproval = cryptoService.encrypt(publicKey, approval);
await expect(
sessionKeysService.generateClientFromSessionKey(zeroAddress, encryptedApproval, optimismSepolia),
).rejects.toThrow(ErrorCodes.SESSION_KEY_NOT_FOUND);
});
});
});
Loading

0 comments on commit d0d5b6f

Please sign in to comment.