Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: userops ttl #100

Merged
merged 7 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading