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: multiple executors #138

Merged
merged 6 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ Or follow the steps below:
"entryPoints": [ # supported entry points
"0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
],
"relayer": "0xprivateKey", # relayer private key, can access from here or via environment variables (SKANDHA_MUMBAI_RELAYER | SKANDHA_DEV_RELAYER | etc.)
"beneficiary": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", # fee collector, avaiable via env var (SKANDHA_MUMBAI_BENEFICIARY | etc)
"relayers": [
"0xprivateKey",
"0xprivateKey2"
], # relayers private keys, can access from here or via environment variables (SKANDHA_MUMBAI_RELAYERS | SKANDHA_DEV_RELAYERS | etc.)
"beneficiary": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", # optional, fee collector, avaiable via env var (SKANDHA_MUMBAI_BENEFICIARY | etc) - if not set, relayer will be used
"rpcEndpoint": "http://localhost:8545", # rpc provider, also available via env variable (SKANDHA_MUMBAI_RPC | etc)
"minInclusionDenominator": 10, # optional, see EIP-4337
"throttlingSlack": 10, # optional, see EIP-4337
Expand Down
5 changes: 3 additions & 2 deletions config.json.default
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"entryPoints": [
"0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"
],
"relayer": "0xprivateKey",
"beneficiary": "0x690b9a9e9aa1c9db991c7721a92d351db4fac990",
"relayers": [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider an option of using a seed phrase and specify the list of relayers as well?

"0xprivateKey"
],
"rpcEndpoint": "http://localhost:8545"
}
}
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.29-alpha",
"version": "1.0.30-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.29-alpha",
"version": "1.0.30-alpha",
"engines": {
"node": ">=18.0.0"
},
Expand Down
8 changes: 4 additions & 4 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api",
"version": "1.0.29-alpha",
"version": "1.0.30-alpha",
"description": "The API module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -35,13 +35,13 @@
"class-transformer": "0.5.1",
"class-validator": "0.14.0",
"ethers": "5.7.2",
"executor": "^1.0.29-alpha",
"executor": "^1.0.30-alpha",
"fastify": "4.14.1",
"monitoring": "^1.0.29-alpha",
"monitoring": "^1.0.30-alpha",
"pino": "8.11.0",
"pino-pretty": "10.0.0",
"reflect-metadata": "0.1.13",
"types": "^1.0.29-alpha"
"types": "^1.0.30-alpha"
},
"devDependencies": {
"@types/connect": "3.4.35"
Expand Down
14 changes: 7 additions & 7 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cli",
"version": "1.0.29-alpha",
"version": "1.0.30-alpha",
"description": "> TODO: description",
"author": "zincoshine <[email protected]>",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -38,15 +38,15 @@
"@libp2p/peer-id-factory": "2.0.1",
"@libp2p/prometheus-metrics": "1.1.3",
"@multiformats/multiaddr": "12.1.3",
"api": "^1.0.29-alpha",
"db": "^1.0.29-alpha",
"executor": "^1.0.29-alpha",
"api": "^1.0.30-alpha",
"db": "^1.0.30-alpha",
"executor": "^1.0.30-alpha",
"find-up": "5.0.0",
"got": "12.5.3",
"js-yaml": "4.1.0",
"monitoring": "^1.0.29-alpha",
"node": "^1.0.29-alpha",
"types": "^1.0.29-alpha",
"monitoring": "^1.0.30-alpha",
"node": "^1.0.30-alpha",
"types": "^1.0.30-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.29-alpha",
"version": "1.0.30-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.29-alpha"
"types": "^1.0.30-alpha"
},
"devDependencies": {
"@types/rocksdb": "3.0.1",
Expand Down
8 changes: 4 additions & 4 deletions packages/executor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "executor",
"version": "1.0.29-alpha",
"version": "1.0.30-alpha",
"description": "The Relayer module of Etherspot bundler client",
"author": "Etherspot",
"homepage": "https://https://github.com/etherspot/skandha#readme",
Expand Down Expand Up @@ -34,8 +34,8 @@
"@flashbots/ethers-provider-bundle": "0.6.2",
"async-mutex": "0.4.0",
"ethers": "5.7.2",
"monitoring": "^1.0.29-alpha",
"params": "^1.0.29-alpha",
"types": "^1.0.29-alpha"
"monitoring": "^1.0.30-alpha",
"params": "^1.0.30-alpha",
"types": "^1.0.30-alpha"
}
}
31 changes: 21 additions & 10 deletions packages/executor/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,28 @@ export class Config {
return endpoint ? new providers.JsonRpcProvider(endpoint) : null;
}

getRelayer(network: string): Wallet | providers.JsonRpcSigner | null {
getRelayers(network: string): Wallet[] | providers.JsonRpcSigner[] | null {
const config = this.getNetworkConfig(network);
if (!config) return null;

// fetch from env variables first
const privKey = config.relayer;
const provider = this.getNetworkProvider(network);
if (!provider) {
throw new Error("no provider");
}

if (this.testingMode) {
return provider.getSigner();
return [provider.getSigner()];
}

if (privKey.startsWith("0x")) {
return new Wallet(privKey, provider);
const wallets = [];
for (const privKey of config.relayers) {
if (privKey.startsWith("0x")) {
wallets.push(new Wallet(privKey, provider));
} else {
wallets.push(Wallet.fromMnemonic(privKey).connect(provider));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we can also use a mnemonic? If so we need to document that in README.md

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we can use mnemonics. Added it in README

}
}

return Wallet.fromMnemonic(privKey).connect(provider);
return wallets;
}

getBeneficiary(network: string): string | null {
Expand Down Expand Up @@ -158,11 +160,19 @@ export class Config {
conf.entryPoints,
true
) as string[];
conf.relayer = fromEnvVar(network, "RELAYER", conf.relayer) as string;

conf.relayer = fromEnvVar(network, "RELAYER", conf.relayer) as string; // deprecated
conf.relayers = fromEnvVar(
network,
"RELAYERS",
conf.relayers ?? [conf.relayer], // fallback to `relayer` if `relayers` not found
true
) as string[];

conf.beneficiary = fromEnvVar(
network,
"BENEFICIARY",
conf.beneficiary
conf.beneficiary || bundlerDefaultConfigs.beneficiary
) as string;
conf.rpcEndpoint = fromEnvVar(network, "RPC", conf.rpcEndpoint) as string;

Expand Down Expand Up @@ -322,6 +332,7 @@ export class Config {
}

const bundlerDefaultConfigs: BundlerConfig = {
beneficiary: "",
minInclusionDenominator: 10,
throttlingSlack: 10,
banSlack: 50,
Expand Down
5 changes: 3 additions & 2 deletions packages/executor/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ export type BundlingMode = "auto" | "manual";
export type Executors = Map<number, Executor>;
export interface NetworkConfig {
entryPoints: string[];
relayer: string;
relayer: string; // deprecated, but kept for backwards compatibility
relayers: string[];
beneficiary: string;
name?: NetworkName;
rpcEndpoint: string;
Expand Down Expand Up @@ -149,7 +150,7 @@ export interface NetworkConfig {

export type BundlerConfig = Omit<
NetworkConfig,
"entryPoints" | "rpcEndpoint" | "relayer" | "beneficiary"
"entryPoints" | "rpcEndpoint" | "relayer" | "relayers"
>;

export type Networks = {
Expand Down
7 changes: 7 additions & 0 deletions packages/executor/src/modules/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,13 @@ export class Eth {
private async getUserOperationEvent(
userOpHash: string
): Promise<[IEntryPoint | null, UserOperationEventEvent | null]> {
if (!userOpHash) {
throw new RpcError(
"Missing/invalid userOpHash",
RpcErrorCodes.METHOD_NOT_FOUND
);
}

let event: UserOperationEventEvent[] = [];
for (const addr of await this.getSupportedEntryPoints()) {
const contract = IEntryPoint__factory.connect(addr, this.provider);
Expand Down
10 changes: 8 additions & 2 deletions packages/executor/src/modules/skandha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ export class Skandha {
}

async getConfig(): Promise<GetConfigResponse> {
const wallet = this.config.getRelayer(this.networkName);
const wallets = this.config.getRelayers(this.networkName);
const walletAddresses = [];
if (wallets) {
for (const wallet of wallets) {
walletAddresses.push(await wallet.getAddress());
}
}
const hasEtherscanApiKey = Boolean(this.networkConfig.etherscanApiKey);
const hasExecutionRpc = Boolean(this.networkConfig.rpcEndpointSubmit);
return {
Expand All @@ -80,7 +86,7 @@ export class Skandha {
},
entryPoints: this.networkConfig.entryPoints,
beneficiary: this.networkConfig.beneficiary,
relayer: wallet ? await wallet.getAddress() : "",
relayers: walletAddresses,
minInclusionDenominator: BigNumber.from(
this.networkConfig.minInclusionDenominator
).toNumber(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export type Relayer = Wallet | providers.JsonRpcSigner;

export interface IRelayingMode {
isLocked(): boolean;
sendBundle(bundle: Bundle, beneficiary: string): Promise<void>;
sendBundle(bundle: Bundle): Promise<void>;
}
31 changes: 27 additions & 4 deletions packages/executor/src/services/BundlingService/relayers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ export abstract class BaseRelayer implements IRelayingMode {
protected reputationService: ReputationService,
protected metrics: PerChainMetrics | null
) {
const relayer = this.config.getRelayer(this.network);
if (!relayer) throw new Error("Relayer is not set");
this.relayers = [relayer];
const relayers = this.config.getRelayers(this.network);
if (!relayers) throw new Error("Relayers are not set");
this.relayers = [...relayers];
this.mutexes = this.relayers.map(() => new Mutex());
}

isLocked(): boolean {
return this.mutexes.every((mutex) => mutex.isLocked());
}

sendBundle(_bundle: Bundle, _beneficiary: string): Promise<void> {
sendBundle(_bundle: Bundle): Promise<void> {
throw new Error("Method not implemented.");
}

Expand Down Expand Up @@ -120,4 +120,27 @@ export abstract class BaseRelayer implements IRelayingMode {
});
}
}

/**
* determine who should receive the proceedings of the request.
* if signer's balance is too low, send it to signer. otherwise, send to configured beneficiary.
*/
protected async selectBeneficiary(relayer: Relayer): Promise<string> {
const config = this.config.getNetworkConfig(this.network);
let beneficiary = this.config.getBeneficiary(this.network);
if (!beneficiary || !utils.isAddress(beneficiary)) {
return relayer.getAddress();
}

const signerAddress = await relayer.getAddress();
const currentBalance = await this.provider.getBalance(signerAddress);

if (currentBalance.lte(config!.minSignerBalance) || !beneficiary) {
beneficiary = signerAddress;
this.logger.info(
`low balance on ${signerAddress}. using it as beneficiary`
);
}
return beneficiary;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MempoolService } from "../../MempoolService";
import { estimateBundleGasLimit } from "../utils";
import { ReputationService } from "../../ReputationService";
import { BaseRelayer } from "./base";
import { wait } from "../../../utils";

export class ClassicRelayer extends BaseRelayer {
constructor(
Expand All @@ -38,7 +39,7 @@ export class ClassicRelayer extends BaseRelayer {
);
}

async sendBundle(bundle: Bundle, beneficiary: string): Promise<void> {
async sendBundle(bundle: Bundle): Promise<void> {
const availableIndex = this.getAvailableRelayerIndex();
if (availableIndex == null) return;
const relayer = this.relayers[availableIndex];
Expand All @@ -48,6 +49,7 @@ export class ClassicRelayer extends BaseRelayer {
if (!bundle.entries.length) return;

await mutex.runExclusive(async (): Promise<void> => {
const beneficiary = await this.selectBeneficiary(relayer);
const entryPoint = entries[0]!.entryPoint;
const entryPointContract = IEntryPoint__factory.connect(
entryPoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class FlashbotsRelayer extends BaseRelayer {
);
}

async sendBundle(bundle: Bundle, beneficiary: string): Promise<void> {
async sendBundle(bundle: Bundle): Promise<void> {
const availableIndex = this.getAvailableRelayerIndex();
if (availableIndex == null) return;

Expand All @@ -54,6 +54,7 @@ export class FlashbotsRelayer extends BaseRelayer {
if (!bundle.entries.length) return;

await mutex.runExclusive(async (): Promise<void> => {
const beneficiary = await this.selectBeneficiary(relayer);
const entryPoint = entries[0]!.entryPoint;
const entryPointContract = IEntryPoint__factory.connect(
entryPoint,
Expand Down
Loading
Loading