Skip to content

Commit

Permalink
wip: fastlane
Browse files Browse the repository at this point in the history
  • Loading branch information
0xSulpiride committed Apr 18, 2024
1 parent d7c844c commit 9fd12d9
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 12 deletions.
28 changes: 18 additions & 10 deletions packages/executor/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ export class Config {

static async init(configOptions: ConfigOptions): Promise<Config> {
const config = new Config(configOptions);
await config.fetchChainId();
try {
await config.fetchChainId();
} catch (err) {
// trying again with skipping ssl errors
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
await config.fetchChainId();
}
return config;
}

Expand Down Expand Up @@ -150,10 +156,7 @@ export class Config {
);

config.minStake = BigNumber.from(
fromEnvVar(
"MIN_STAKE",
config.minStake ?? bundlerDefaultConfigs.minStake
)
fromEnvVar("MIN_STAKE", config.minStake ?? bundlerDefaultConfigs.minStake)
);
config.minUnstakeDelay = Number(
fromEnvVar(
Expand Down Expand Up @@ -237,10 +240,7 @@ export class Config {
);

config.banSlack = Number(
fromEnvVar(
"BAN_SLACK",
config.banSlack || bundlerDefaultConfigs.banSlack
)
fromEnvVar("BAN_SLACK", config.banSlack || bundlerDefaultConfigs.banSlack)
);

config.minInclusionDenominator = Number(
Expand All @@ -261,7 +261,8 @@ export class Config {
config.skipBundleValidation = Boolean(
fromEnvVar(
"SKIP_BUNDLE_VALIDATION",
config.skipBundleValidation || bundlerDefaultConfigs.skipBundleValidation
config.skipBundleValidation ||
bundlerDefaultConfigs.skipBundleValidation
)
);

Expand Down Expand Up @@ -293,6 +294,12 @@ export class Config {
)
);

config.fastlaneValidators = fromEnvVar(
"FASTLANE_VALIDATOR",
config.fastlaneValidators || bundlerDefaultConfigs.fastlaneValidators,
true
) as string[];

// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!config.whitelistedEntities) {
config.whitelistedEntities = bundlerDefaultConfigs.whitelistedEntities;
Expand Down Expand Up @@ -360,6 +367,7 @@ const bundlerDefaultConfigs: BundlerConfig = {
kolibriAuthKey: "",
entryPointForwarder: "",
echoAuthKey: "",
fastlaneValidators: [],
};

function getEnvVar<T>(envVar: string, fallback: T): T | string {
Expand Down
1 change: 1 addition & 0 deletions packages/executor/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export interface NetworkConfig {
entryPointForwarder: string;
// api auth key for echo: https://echo.chainbound.io/docs/usage/api-interface#authentication
echoAuthKey: string;
fastlaneValidators: string[];
}

export type BundlerConfig = Omit<
Expand Down
1 change: 1 addition & 0 deletions packages/executor/src/modules/skandha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class Skandha {
gasFeeInSimulation: this.networkConfig.gasFeeInSimulation,
userOpGasLimit: this.networkConfig.userOpGasLimit,
bundleGasLimit: this.networkConfig.bundleGasLimit,
fastlaneValidators: this.networkConfig.fastlaneValidators
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface IRelayingMode {
isLocked(): boolean;
sendBundle(bundle: Bundle): Promise<void>;
getAvailableRelayersCount(): number;
canSubmitBundle(): Promise<boolean>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export abstract class BaseRelayer implements IRelayingMode {
return this.mutexes.filter((mutex) => !mutex.isLocked()).length;
}

async canSubmitBundle(): Promise<boolean> {
return true;
}

/**
* waits entries to get submitted
* @param hashes user op hashes array
Expand Down
267 changes: 267 additions & 0 deletions packages/executor/src/services/BundlingService/relayers/fastlane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { providers } from "ethers";
import { Logger } from "types/lib";
import { PerChainMetrics } from "monitoring/lib";
import { IEntryPoint__factory } from "types/lib/executor/contracts";
import { chainsWithoutEIP1559 } from "params/lib";
import { AccessList } from "ethers/lib/utils";
import { MempoolEntryStatus } from "types/lib/executor";
import { Relayer } from "../interfaces";
import { Config } from "../../../config";
import { Bundle, NetworkConfig, StorageMap } from "../../../interfaces";
import { MempoolService } from "../../MempoolService";
import { estimateBundleGasLimit } from "../utils";
import { ReputationService } from "../../ReputationService";
import { BaseRelayer } from "./base";
import { now } from "../../../utils";

export class FastlaneRelayer extends BaseRelayer {
private submitTimeout = 10 * 60 * 1000; // 10 minutes

constructor(
logger: Logger,
chainId: number,
provider: providers.JsonRpcProvider,
config: Config,
networkConfig: NetworkConfig,
mempoolService: MempoolService,
reputationService: ReputationService,
metrics: PerChainMetrics | null
) {
super(
logger,
chainId,
provider,
config,
networkConfig,
mempoolService,
reputationService,
metrics
);
if (!this.networkConfig.conditionalTransactions) {
throw new Error("Fastlane: You must enable conditional transactions");
}
if (!this.networkConfig.rpcEndpointSubmit) {
throw new Error("Fastlane: You must set rpcEndpointSubmit");
}
}

async sendBundle(bundle: Bundle): Promise<void> {
const availableIndex = this.getAvailableRelayerIndex();
if (availableIndex == null) {
this.logger.error("Fastlane: No available relayers");
return;
}
const relayer = this.relayers[availableIndex];
const mutex = this.mutexes[availableIndex];

const { entries, storageMap } = bundle;
if (!bundle.entries.length) {
this.logger.error("Fastlane: Bundle is empty");
return;
}

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

const txRequest = entryPointContract.interface.encodeFunctionData(
"handleOps",
[entries.map((entry) => entry.userOp), beneficiary]
);

const transactionRequest: providers.TransactionRequest = {
to: entryPoint,
data: txRequest,
type: 2,
maxPriorityFeePerGas: bundle.maxPriorityFeePerGas,
maxFeePerGas: bundle.maxFeePerGas,
};

if (this.networkConfig.eip2930) {
const { storageMap } = bundle;
const addresses = Object.keys(storageMap);
if (addresses.length) {
const accessList: AccessList = [];
for (const address of addresses) {
const storageKeys = storageMap[address];
if (typeof storageKeys == "object") {
accessList.push({
address,
storageKeys: Object.keys(storageKeys),
});
}
}
transactionRequest.accessList = accessList;
}
}

if (
chainsWithoutEIP1559.some((chainId: number) => chainId === this.chainId)
) {
transactionRequest.gasPrice = bundle.maxFeePerGas;
delete transactionRequest.maxPriorityFeePerGas;
delete transactionRequest.maxFeePerGas;
delete transactionRequest.type;
delete transactionRequest.accessList;
}

const transaction = {
...transactionRequest,
gasLimit: estimateBundleGasLimit(
this.networkConfig.bundleGasLimitMarkup,
bundle.entries
),
chainId: this.provider._network.chainId,
nonce: await relayer.getTransactionCount(),
};

if (!this.networkConfig.skipBundleValidation) {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { gasLimit, ...txWithoutGasLimit } = transactionRequest;
// some chains, like Bifrost, don't allow setting gasLimit in estimateGas
await relayer.estimateGas(txWithoutGasLimit);
} catch (err) {
this.logger.debug(
`${entries
.map((entry) => entry.userOpHash)
.join("; ")} failed on chain estimation. deleting...`
);
this.logger.error(err);
await this.mempoolService.removeAll(entries);
this.reportFailedBundle();
return;
}
}

this.logger.debug(
`Fastlane: Trying to submit userops: ${bundle.entries
.map((entry) => entry.userOpHash)
.join(", ")}`
);

await this.submitTransaction(relayer, transaction, storageMap)
.then(async (txHash: string) => {
this.logger.debug(`Fastlane: Bundle submitted: ${txHash}`);
this.logger.debug(
`Fastlane: User op hashes ${entries.map(
(entry) => entry.userOpHash
)}`
);
await this.mempoolService.setStatus(
entries,
MempoolEntryStatus.Submitted,
txHash
);

await this.waitForEntries(entries).catch((err) =>
this.logger.error(err, "Fastlane: Could not find transaction")
);
this.reportSubmittedUserops(txHash, bundle);
})
.catch(async (err: any) => {
this.reportFailedBundle();
// Put all userops back to the mempool
// if some userop failed, it will be deleted inside handleUserOpFail()
await this.mempoolService.setStatus(entries, MempoolEntryStatus.New);
await this.handleUserOpFail(entries, err);
});
});
}

async canSubmitBundle(): Promise<boolean> {
try {
const validators = await this.provider.send("bor_getCurrentValidators", []);
for (const validator of validators) {
if (
this.networkConfig.fastlaneValidators.some(
(fastlane) => fastlane.toLowerCase() === validator.signer.toLowerCase()
)
) {
return true;
}
}
} catch (err) {
this.logger.error(err, "Fastlane: error on bor_getCurrentValidators");
} finally {
return false;
}
}

/**
* signs & sends a transaction
* @param relayer wallet
* @param transaction transaction request
* @param storageMap storage map
* @returns transaction hash
*/
private async submitTransaction(
relayer: Relayer,
transaction: providers.TransactionRequest,
storageMap: StorageMap
): Promise<string> {
const signedRawTx = await relayer.signTransaction(transaction);
const method = "pfl_sendRawTransactionConditional";

const provider = new providers.JsonRpcProvider(
this.networkConfig.rpcEndpointSubmit
);
const submitStart = now();
return new Promise((resolve, reject) => {
let lock = false;
const handler = async (blockNumber: number): Promise<void> => {
if (now() - submitStart > this.submitTimeout) return reject("timeout");
if (lock) return;
lock = true;

const block = await relayer.provider.getBlock("latest");
const params = [
signedRawTx,
{
knownAccounts: storageMap,
blockNumberMin: block.number,
blockNumberMax: block.number + 180, // ~10 minutes
timestampMin: block.timestamp,
timestampMax: block.timestamp + 420, // 15 minutes
},
];

this.logger.debug({
method,
...transaction,
params,
});

this.logger.debug(`Fastlane: Trying to submit...`);

try {
const hash = await provider.send(method, params);
this.logger.debug(`Fastlane: Sent new bundle ${hash}`);
this.provider.removeListener("block", handler);
return resolve(hash);
} catch (err: any) {
console.log(JSON.stringify(err, undefined, 2));
if (
!err ||
!err.body ||
!err.body.match(/is not participating in FastLane protocol/)
) {
// some other error happened
this.provider.removeListener("block", handler);
return reject(err);
}
this.logger.debug(
`Fastlane: Validator is not participating in FastLane protocol. Trying again...`
);
} finally {
lock = false;
}
};
this.provider.on("block", handler);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import { FlashbotsRelayer } from "./flashbots";
import { MerkleRelayer } from "./merkle";
import { KolibriRelayer } from "./kolibri";
import { EchoRelayer } from "./echo";
import { FastlaneRelayer } from "./fastlane";

export * from "./classic";
export * from "./flashbots";
export * from "./merkle";
export * from "./kolibri";
export * from "./echo";
export * from "./fastlane";

export type RelayerClass =
| typeof ClassicRelayer
| typeof FlashbotsRelayer
| typeof MerkleRelayer
| typeof KolibriRelayer
| typeof EchoRelayer;
| typeof EchoRelayer
| typeof FastlaneRelayer;
Loading

0 comments on commit 9fd12d9

Please sign in to comment.