diff --git a/packages/api-server/src/cache/guard.ts b/packages/api-server/src/cache/guard.ts new file mode 100644 index 00000000..1c14d680 --- /dev/null +++ b/packages/api-server/src/cache/guard.ts @@ -0,0 +1,91 @@ +import { Store } from "./store"; +import { HexString } from "@ckb-lumos/base"; +import { envConfig } from "../base/env-config"; + +const RedisPrefixName = "access"; +export const CACHE_EXPIRED_TIME_MILSECS = 1 * 60 * 1000; // milsec, default 1 minutes + +export interface MaxRpmMap { + [reqRouter: string]: number; +} + +export class AccessGuard { + public store: Store; + public maxRpmMap: MaxRpmMap; + + constructor( + enableExpired = true, + expiredTimeMilsecs = CACHE_EXPIRED_TIME_MILSECS, // milsec, default 1 minutes + store?: Store + ) { + this.store = + store || new Store(envConfig.redisUrl, enableExpired, expiredTimeMilsecs); + this.maxRpmMap = {}; + } + + isConnected() { + return this.store.client.isOpen; + } + + async connect() { + if (!this.isConnected()) { + await this.store.client.connect(); + } + } + + async setMaxRpm(rpcRouter: string, maxRpm: number) { + this.maxRpmMap[rpcRouter] = maxRpm; + } + + async getCount(rpcRouter: string, reqId: string) { + const id = getId(rpcRouter, reqId); + const count = await this.store.get(id); + if (count == null) { + return null; + } + return +count; + } + + async add(rpcRouter: string, reqId: string): Promise { + const isExist = await this.isExist(rpcRouter, reqId); + if (!isExist) { + const id = getId(rpcRouter, reqId); + await this.store.insert(id, 0); + return id; + } + } + + async updateCount(rpcRouter: string, reqId: string) { + const preCount = await this.getCount(rpcRouter, reqId); + if (preCount != null) { + const afterCount = preCount + 1; + const id = getId(rpcRouter, reqId); + await this.store.insert(id, afterCount); + } + } + + async isExist(rpcRouter: string, reqId: string) { + const id = getId(rpcRouter, reqId); + const data = await this.store.get(id); + if (data == null) return false; + return true; + } + + async isOverRate(rpcRouter: string, reqId: string): Promise { + const id = getId(rpcRouter, reqId); + const data = await this.store.get(id); + if (data == null) return false; + if (this.maxRpmMap[rpcRouter] == null) return false; + + const count = +data; + const maxNumber = this.maxRpmMap[rpcRouter]; + if (count > maxNumber) { + return true; + } + return false; + } +} + +export function getId(rpcRouter: string, reqUniqueId: string): HexString { + return `${RedisPrefixName}.${rpcRouter}.${reqUniqueId}`; +} diff --git a/packages/api-server/src/methods/modules/poly.ts b/packages/api-server/src/methods/modules/poly.ts index dac96255..898fbbc7 100644 --- a/packages/api-server/src/methods/modules/poly.ts +++ b/packages/api-server/src/methods/modules/poly.ts @@ -20,14 +20,26 @@ import { } from "@polyjuice-provider/godwoken/lib/addressTypes"; import { GodwokenClient } from "@godwoken-web3/godwoken"; import { parseGwRpcError } from "../gw-error"; +import { AccessGuard } from "../../cache/guard"; + +const MAX_RPM = { + poly_executeRawL2Transaction: 30, // max: 0.5 req/s = 30 req/m +}; export class Poly { private query: Query; private rpc: GodwokenClient; + private accessGuard: AccessGuard; constructor() { this.query = new Query(); this.rpc = new GodwokenClient(envConfig.godwokenJsonRpc); + this.accessGuard = new AccessGuard(); + this.accessGuard.connect(); + this.accessGuard.setMaxRpm( + "poly_executeRawL2Transaction", + MAX_RPM.poly_executeRawL2Transaction + ); this.getEthAddressByGodwokenShortAddress = middleware( this.getEthAddressByGodwokenShortAddress.bind(this), @@ -88,6 +100,23 @@ export class Poly { const txWithAddressMapping: RawL2TransactionWithAddressMapping = deserializeRawL2TransactionWithAddressMapping(data); const rawL2Tx = txWithAddressMapping.raw_tx; + + // access control + const rpcRouter = "poly_executeRawL2Transaction"; + const reqId = rawL2Tx.from_id; + const isExist = await this.accessGuard.isExist(rpcRouter, reqId); + if (!isExist) { + await this.accessGuard.add(rpcRouter, reqId); + } + const isOverRate = await this.accessGuard.isOverRate(rpcRouter, reqId); + if (isOverRate) { + throw new Error( + "you are temporally restrict to the service, please wait." + ); + } + await this.accessGuard.updateCount(rpcRouter, reqId); + // end of access control + const result = await this.rpc.executeRawL2Transaction(rawL2Tx); // if result is fine, then tx is legal, we can start thinking to store the address mapping await saveAddressMapping(this.query, this.rpc, txWithAddressMapping);