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 19, 2024
1 parent 000ad4b commit 67e8c3c
Show file tree
Hide file tree
Showing 14 changed files with 556 additions and 9 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
10 changes: 9 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
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 {}
14 changes: 14 additions & 0 deletions packages/coordinator/ts/common/accountAbstraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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("PIMLICO_API_KEY is not set");
}

return `https://api.pimlico.io/v2/${network}/rpc?apikey=${pimlicoAPIKey}`;
};
1 change: 1 addition & 0 deletions packages/coordinator/ts/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export enum ErrorCodes {
ENCRYPTION = "5",
FILE_NOT_FOUND = "6",
SUBGRAPH_DEPLOY = "7",
SESSION_KEY_NOT_FOUND = "8",
}
43 changes: 43 additions & 0 deletions packages/coordinator/ts/file/file.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Injectable, Logger } from "@nestjs/common";
import low from "lowdb";
import FileSync from "lowdb/adapters/FileSync";

import fs from "fs";
import path from "path";
Expand All @@ -7,6 +9,12 @@ import type { IGetPrivateKeyData, IGetPublicKeyData, IGetZkeyFilePathsData } fro

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

/**
* Internal storage structure type.
* named: keys can be queried by name
*/
type TStorage = Record<string, `0x${string}`>;

/**
* FileService is responsible for working with local files like:
* 1. RSA public/private keys
Expand All @@ -19,11 +27,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: `0x${string}`, 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): `0x${string}` | undefined {
return this.db.get(address).value() as `0x${string}` | undefined;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import dotenv from "dotenv";
import { ZeroAddress } from "ethers";

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

dotenv.config();

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

const cryptoService = new CryptoService();
const fileService = new FileService();
const sessionKeysService = new SessionKeysService(cryptoService, fileService);

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();
});

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();
});
});
82 changes: 82 additions & 0 deletions packages/coordinator/ts/sessionKeys/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator";
import { type Policy, serializePermissionAccount, toPermissionValidator } from "@zerodev/permissions";
import { toTimestampPolicy } from "@zerodev/permissions/policies";
import { toECDSASigner } from "@zerodev/permissions/signers";
import { addressToEmptyAccount, createKernelAccount } from "@zerodev/sdk";
import { KERNEL_V3_1 } from "@zerodev/sdk/constants";
import { ENTRYPOINT_ADDRESS_V07 } from "permissionless";
import { createPublicClient, http } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { localhost } from "viem/chains";

// use the latest kernel version
const kernelVersion = KERNEL_V3_1;
// we use the most recent entrypoint
const entryPoint = ENTRYPOINT_ADDRESS_V07;

/**
* Generate a timestamp policy
* @param endTime - The end time of the policy
* @param start - The start time of the policy
* @returns The timestamp policy
*/
export const generateTimestampPolicy = (endTime: number, start?: number): Policy =>
toTimestampPolicy({
validAfter: start,
validUntil: endTime,
});

/**
* Mock a session key approval
* @dev This will fail in hardhat with:
* "InvalidEntryPointError: The entry point address
* (`entryPoint` = 0x0000000071727De22E5E9d8BAf0edAc6f37da032)
* is not a valid entry point. getSenderAddress did not revert with
* a SenderAddressResult error."
*
* @param sessionKeyAddress - The address of the session key
* @returns The approval string
*/
export const mockSessionKeyApproval = async (sessionKeyAddress: `0x${string}`): Promise<string> => {
const policies = [generateTimestampPolicy(Math.floor(Date.now() / 1000) + 1000)];

const publicClient = createPublicClient({
chain: localhost,
transport: http(),
});

const sessionPrivateKey = generatePrivateKey();

const signer = privateKeyToAccount(sessionPrivateKey);

const ecdsaValidator = await signerToEcdsaValidator(publicClient, {
signer,
entryPoint,
kernelVersion,
});

// Create an "empty account" as the signer -- you only need the public
// key (address) to do this.
const emptyAccount = addressToEmptyAccount(sessionKeyAddress);
const emptySessionKeySigner = toECDSASigner({ signer: emptyAccount });

const permissionPlugin = await toPermissionValidator(publicClient, {
entryPoint,
kernelVersion,
signer: emptySessionKeySigner,
policies,
});

const sessionKeyAccount = await createKernelAccount(publicClient, {
entryPoint,
kernelVersion,
plugins: {
sudo: ecdsaValidator,
regular: permissionPlugin,
},
});

const approval = await serializePermissionAccount(sessionKeyAccount);

return approval;
};
50 changes: 50 additions & 0 deletions packages/coordinator/ts/sessionKeys/sessionKeys.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-shadow */
import { Controller, Delete, Get, HttpStatus, Param, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiResponse, ApiTags } from "@nestjs/swagger";

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

import { AccountSignatureGuard } from "../auth/AccountSignatureGuard.service";

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

@ApiTags("v1/session-keys")
@ApiBearerAuth()
@Controller("v1/session-keys")
@UseGuards(AccountSignatureGuard)
export class SessionKeysController {
/**
* Initialize SessionKeysController
*
* @param sessionKeysService - session keys service
*/
constructor(private readonly sessionKeysService: SessionKeysService) {}

/**
* Generate a session key api method
*
* @param args - generate session key dto
* @returns generated session key address
*/
@ApiResponse({ status: HttpStatus.CREATED, description: "The session key was successfully generated" })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: "Forbidden" })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" })
@Get("generate")
generateSessionKey(): IGenerateSessionKeyReturn {
return this.sessionKeysService.generateSessionKey();
}

/**
* Delete a session key api method
*
* @param args - delete session key dto
* @returns deleted session key address
*/
@ApiResponse({ status: HttpStatus.CREATED, description: "The session key was successfully deactivated" })
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: "Forbidden" })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" })
@Delete(":sessionKeyAddress")
deactivateSessionKey(@Param("sessionKeyAddress") sessionKeyAddress: `0x${string}`): void {
this.sessionKeysService.deactivateSessionKey(sessionKeyAddress);
}
}
14 changes: 14 additions & 0 deletions packages/coordinator/ts/sessionKeys/sessionKeys.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";

import { CryptoModule } from "../crypto/crypto.module";
import { FileModule } from "../file/file.module";

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

@Module({
imports: [FileModule, CryptoModule],
controllers: [SessionKeysController],
providers: [SessionKeysService],
})
export class SessionKeysModule {}
Loading

0 comments on commit 67e8c3c

Please sign in to comment.