Skip to content

Commit

Permalink
feat: userops ttl (#100)
Browse files Browse the repository at this point in the history
* userop ttl

* fix protocol prefix

* chore(release): 1.0.8-alpha
  • Loading branch information
0xSulpiride authored Oct 12, 2023
1 parent 0472004 commit 4502750
Show file tree
Hide file tree
Showing 20 changed files with 118 additions and 83 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ Or follow the steps below:
"gasPriceMarkup": 0, # adds % markup on reported gas price via skandha_getGasPrice, 10000 = 100.00%, 500 = 5%
"enforceGasPrice": false, # do not bundle userops with low gas prices
"enforceGasPriceThreshold": 1000, # gas price threshold in bps. If set to 500, userops' gas price is allowed to be 5% lower than the network's gas price
"eip2930": false # enables eip-2930
"eip2930": false, # enables eip-2930
"useropsTTL": 300 # Userops time to live (in seconds)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "1.0.7-alpha",
"version": "1.0.8-alpha",
"stream": "true",
"command": {
"version": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "1.0.7-alpha",
"version": "1.0.8-alpha",
"engines": {
"node": ">=18.0.0"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api",
"version": "1.0.7-alpha",
"version": "1.0.8-alpha",
"description": "The API module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -35,12 +35,12 @@
"class-transformer": "0.5.1",
"class-validator": "0.14.0",
"ethers": "5.7.2",
"executor": "^1.0.7-alpha",
"executor": "^1.0.8-alpha",
"fastify": "4.14.1",
"pino": "8.11.0",
"pino-pretty": "10.0.0",
"reflect-metadata": "0.1.13",
"types": "^1.0.7-alpha"
"types": "^1.0.8-alpha"
},
"devDependencies": {
"@types/connect": "3.4.35"
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cli",
"version": "1.0.7-alpha",
"version": "1.0.8-alpha",
"description": "> TODO: description",
"author": "zincoshine <[email protected]>",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -38,14 +38,14 @@
"@libp2p/peer-id-factory": "2.0.1",
"@libp2p/prometheus-metrics": "1.1.3",
"@multiformats/multiaddr": "12.1.3",
"api": "^1.0.7-alpha",
"db": "^1.0.7-alpha",
"executor": "^1.0.7-alpha",
"api": "^1.0.8-alpha",
"db": "^1.0.8-alpha",
"executor": "^1.0.8-alpha",
"find-up": "5.0.0",
"got": "12.5.3",
"js-yaml": "4.1.0",
"node": "^1.0.7-alpha",
"types": "^1.0.7-alpha",
"node": "^1.0.8-alpha",
"types": "^1.0.8-alpha",
"yargs": "17.6.2"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "db",
"version": "1.0.7-alpha",
"version": "1.0.8-alpha",
"description": "The DB module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://github.com/etherspot/etherspot-bundler#readme",
Expand Down Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"@chainsafe/ssz": "0.10.1",
"@farcaster/rocksdb": "5.5.0",
"types": "^1.0.7-alpha"
"types": "^1.0.8-alpha"
},
"devDependencies": {
"@types/rocksdb": "3.0.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/executor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "executor",
"version": "1.0.7-alpha",
"version": "1.0.8-alpha",
"description": "The Relayer module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"async-mutex": "0.4.0",
"ethers": "5.7.2",
"params": "^1.0.7-alpha",
"types": "^1.0.7-alpha"
"params": "^1.0.8-alpha",
"types": "^1.0.8-alpha"
}
}
8 changes: 8 additions & 0 deletions packages/executor/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ export class Config {
conf.eip2930 || bundlerDefaultConfigs.eip2930
)
);
conf.useropsTTL = Number(
fromEnvVar(
network,
"USEROPS_TTL",
conf.useropsTTL || bundlerDefaultConfigs.useropsTTL
)
);

return Object.assign({}, bundlerDefaultConfigs, conf);
}
Expand All @@ -235,6 +242,7 @@ const bundlerDefaultConfigs: BundlerConfig = {
enforceGasPrice: false,
enforceGasPriceThreshold: 1000,
eip2930: false,
useropsTTL: 300, // 5 minutes
};

const NETWORKS_ENV = (): string[] | undefined => {
Expand Down
18 changes: 17 additions & 1 deletion packages/executor/src/entities/MempoolEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { hexValue } from "ethers/lib/utils";
import * as RpcErrorCodes from "types/lib/api/errors/rpc-error-codes";
import RpcError from "types/lib/api/errors/rpc-error";
import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint";
import { now } from "../utils";
import { IMempoolEntry, MempoolEntrySerialized } from "./interfaces";

export class MempoolEntry implements IMempoolEntry {
Expand All @@ -23,6 +24,7 @@ export class MempoolEntry implements IMempoolEntry {
aggregator,
userOpHash,
hash,
lastUpdatedTime,
}: {
chainId: number;
userOp: UserOperationStruct;
Expand All @@ -31,6 +33,7 @@ export class MempoolEntry implements IMempoolEntry {
aggregator?: string | undefined;
userOpHash: string;
hash?: string | undefined;
lastUpdatedTime?: number | undefined;
}) {
this.chainId = chainId;
this.userOp = userOp;
Expand All @@ -43,7 +46,7 @@ export class MempoolEntry implements IMempoolEntry {
if (hash) {
this.hash = hash;
}
this.lastUpdatedTime = new Date().getTime();
this.lastUpdatedTime = lastUpdatedTime ?? now();
this.validateAndTransformUserOp();
}

Expand Down Expand Up @@ -75,6 +78,18 @@ export class MempoolEntry implements IMempoolEntry {
return true;
}

/**
* To replace an entry, a new entry must have at least 10% higher maxPriorityFeePerGas
* and 10% higher maxPriorityFeePerGas than the existingEntry
* Returns true if Entry can replace existingEntry
* @param entry MempoolEntry
* @returns boolaen
*/
canReplaceWithTTL(existingEntry: MempoolEntry, ttl: number): boolean {
if (this.lastUpdatedTime - existingEntry.lastUpdatedTime > ttl * 1000) return true;
return this.canReplace(existingEntry);
}

isEqual(entry: MempoolEntry): boolean {
return (
entry.chainId === this.chainId &&
Expand Down Expand Up @@ -134,6 +149,7 @@ export class MempoolEntry implements IMempoolEntry {
aggregator: this.aggregator,
hash: this.hash,
userOpHash: this.userOpHash,
lastUpdatedTime: this.lastUpdatedTime,
};
}
}
2 changes: 2 additions & 0 deletions packages/executor/src/entities/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface IMempoolEntry {
prefund: BigNumberish;
aggregator?: string;
userOpHash: string;
lastUpdatedTime: number;
hash?: string;
}

Expand All @@ -30,6 +31,7 @@ export interface MempoolEntrySerialized {
aggregator: string | undefined;
userOpHash: string;
hash: string | undefined;
lastUpdatedTime: number;
}

export interface IReputationEntry {
Expand Down
7 changes: 6 additions & 1 deletion packages/executor/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ export class Executor {
this.mempoolService = new MempoolService(
this.db,
this.chainId,
this.reputationService
this.reputationService,
this.networkConfig
);
this.bundlingService = new BundlingService(
this.chainId,
Expand Down Expand Up @@ -162,5 +163,9 @@ export class Executor {
if (this.networkConfig.eip2930) {
this.logger.info(`${this.networkName}: [x] EIP2930 ENABLED`);
}

this.logger.info(
`${this.networkName}: [x] USEROPS TTL - ${this.networkConfig.useropsTTL}`
);
}
}
4 changes: 4 additions & 0 deletions packages/executor/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export interface NetworkConfig {
// pls check if the node supports eip-2930 before enabling this flag
// can not be used in unsafeMode and on chains that dont support 1559
eip2930: boolean;
// Userops time to live in seconds
// default is 300 (5 minutes)
// after ttl you can replace a userop without increasing gas fees
useropsTTL: number;
}

export type BundlerConfig = Omit<
Expand Down
25 changes: 3 additions & 22 deletions packages/executor/src/modules/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ import {
ECDSA_DUMMY_SIGNATURE,
} from "params/lib";
import { getGasFee } from "params/lib";
import { NetworkConfig } from "../interfaces";
import { deepHexlify, packUserOp } from "../utils";
import { UserOpValidationService, MempoolService } from "../services";
import { Logger, Log } from "../interfaces";
import { Logger, Log, NetworkConfig } from "../interfaces";
import {
EstimateUserOperationGasArgs,
SendUserOperationGasArgs,
Expand Down Expand Up @@ -64,16 +63,7 @@ export class Eth {
if (!this.validateEntryPoint(entryPoint)) {
throw new RpcError("Invalid Entrypoint", RpcErrorCodes.INVALID_REQUEST);
}
const validGasFees = await this.mempoolService.isNewOrReplacing(
userOp,
entryPoint
);
if (!validGasFees) {
throw new RpcError(
"User op cannot be replaced: fee too low",
RpcErrorCodes.INVALID_USEROP
);
}
await this.mempoolService.validateUserOpReplaceability(userOp, entryPoint);

this.logger.debug("Validating user op before sending to mempool...");
await this.userOpValidationService.validateGasFee(userOp);
Expand Down Expand Up @@ -279,16 +269,7 @@ export class Eth {
if (!this.validateEntryPoint(entryPoint)) {
throw new RpcError("Invalid Entrypoint", RpcErrorCodes.INVALID_REQUEST);
}
const validGasFees = await this.mempoolService.isNewOrReplacing(
userOp,
entryPoint
);
if (!validGasFees) {
throw new RpcError(
"User op cannot be replaced: fee too low",
RpcErrorCodes.INVALID_USEROP
);
}
await this.mempoolService.validateUserOpReplaceability(userOp, entryPoint);
this.logger.debug(
JSON.stringify(
await this.userOpValidationService.simulateValidation(
Expand Down
65 changes: 38 additions & 27 deletions packages/executor/src/services/MempoolService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { UserOperationStruct } from "types/lib/executor/contracts/EntryPoint";
import { getAddr, now } from "../utils";
import { MempoolEntry } from "../entities/MempoolEntry";
import { IMempoolEntry, MempoolEntrySerialized } from "../entities/interfaces";
import { StakeInfo } from "../interfaces";
import { NetworkConfig, StakeInfo } from "../interfaces";
import { ReputationService } from "./ReputationService";

export class MempoolService {
Expand All @@ -17,7 +17,8 @@ export class MempoolService {
constructor(
private db: IDbController,
private chainId: number,
private reputationService: ReputationService
private reputationService: ReputationService,
private networkConfig: NetworkConfig
) {
this.USEROP_COLLECTION_KEY = `${chainId}:USEROPKEYS`;
this.USEROP_HASHES_COLLECTION_PREFIX = "USEROPHASH:";
Expand Down Expand Up @@ -52,12 +53,7 @@ export class MempoolService {
});
const existingEntry = await this.find(entry);
if (existingEntry) {
if (!entry.canReplace(existingEntry)) {
throw new RpcError(
"User op cannot be replaced: fee too low",
RpcErrorCodes.INVALID_USEROP
);
}
await this.validateReplaceability(entry, existingEntry);
await this.db.put(this.getKey(entry), {
...entry,
lastUpdatedTime: now(),
Expand Down Expand Up @@ -122,25 +118,6 @@ export class MempoolService {
await this.db.del(this.USEROP_COLLECTION_KEY);
}

/**
* checks if the userOp is new or can replace the existing userOp in mempool
* @returns true if new or replacing
*/
async isNewOrReplacing(
userOp: UserOperationStruct,
entryPoint: string
): Promise<boolean> {
const entry = new MempoolEntry({
chainId: this.chainId,
userOp,
entryPoint,
prefund: "0",
userOpHash: "",
});
const existingEntry = await this.find(entry);
return !existingEntry || entry.canReplace(existingEntry);
}

async find(entry: MempoolEntry): Promise<MempoolEntry | null> {
return this.findByKey(this.getKey(entry));
}
Expand All @@ -153,6 +130,39 @@ export class MempoolService {
return null;
}

async validateReplaceability(
newEntry: MempoolEntry,
oldEntry?: MempoolEntry | null
): Promise<boolean> {
if (!oldEntry) {
oldEntry = await this.find(newEntry);
}
if (
!oldEntry ||
newEntry.canReplaceWithTTL(oldEntry, this.networkConfig.useropsTTL)
) {
return true;
}
throw new RpcError(
"User op cannot be replaced: fee too low",
RpcErrorCodes.INVALID_USEROP
);
}

async validateUserOpReplaceability(
userOp: UserOperationStruct,
entryPoint: string
): Promise<boolean> {
const entry = new MempoolEntry({
chainId: this.chainId,
userOp,
entryPoint,
prefund: "0",
userOpHash: "",
});
return this.validateReplaceability(entry);
}

getKey(entry: Pick<IMempoolEntry, "userOp" | "chainId">): string {
const { userOp, chainId } = entry;
return `${chainId}:${userOp.sender.toLowerCase()}:${userOp.nonce}`;
Expand Down Expand Up @@ -203,6 +213,7 @@ export class MempoolService {
aggregator: raw.aggregator,
hash: raw.hash,
userOpHash: raw.userOpHash,
lastUpdatedTime: raw.lastUpdatedTime,
});
}

Expand Down
Loading

0 comments on commit 4502750

Please sign in to comment.