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: fastlane #174

Merged
merged 4 commits into from
Apr 26, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
## Important links

**[Install Skandha](https://etherspot.fyi/skandha/installation)**
| [Chains supported](https://etherspot.fyi/skandha/chains)
| [Chains supported](https://etherspot.fyi/prime-sdk/chains-supported))
| [UserOp Fee history](https://etherspot.fyi/skandha/feehistory)

## ⚙️ How to run (from Source code)
Expand Down
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,
Copy link
Member

Choose a reason for hiding this comment

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

This in theory will be used only during the testing phase, no?

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, that's correct

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
271 changes: 271 additions & 0 deletions packages/executor/src/services/BundlingService/relayers/fastlane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
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 provider = new providers.JsonRpcProvider(
"https://rpc-mainnet.maticvigil.com"
);
const validators = await provider.send("bor_getCurrentValidators", []);
for (let fastlane of this.networkConfig.fastlaneValidators) {
fastlane = fastlane.toLowerCase();
if (
validators.some(
(validator: { signer: string }) =>
validator.signer.toLowerCase() == fastlane
)
) {
return true;
}
}
} catch (err) {
this.logger.error(err, "Fastlane: error on bor_getCurrentValidators");
}
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);
});
}
}
Loading
Loading