Skip to content

Commit

Permalink
feat(evmnetworkservice): support fetching the nonce from a flashbots rpc
Browse files Browse the repository at this point in the history
Flashbots eth_getTransactionCount requires a signature from the account owner of the address for
privacy reasons, see: https://docs.flashbots.net/flashbots-protect/nonce-management
  • Loading branch information
TheDivic committed Nov 12, 2024
1 parent f299ecc commit aca5f34
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 28 deletions.
85 changes: 81 additions & 4 deletions src/common/network/EVMNetworkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
createPublicClient,
fallback,
http,
keccak256,
toHex,
verifyMessage,
} from "viem";
import { IEVMAccount } from "../../relayer/account";
Expand Down Expand Up @@ -182,6 +184,59 @@ export class EVMNetworkService
return data.result;
}

/**
* Getting the nonce on flasbots requires a signature from the account
* See https://docs.flashbots.net/flashbots-protect/nonce-management
* @param account account to get the nonce for
* @param pending include the nonce of the pending transaction (default: true)
* @returns nonce of the account
*/
async getFlashbotsNonce(
account: IEVMAccount,
pending = true,
): Promise<number> {
if (!this.mevProtectedRpcUrl) {
throw new Error(
`Can't fetch Flashbots nonce if Flashbots RPC URL not set for chainId: ${this.chainId}!`,
);
}
logger.info(`Getting Flashbots nonce for address: ${account.address}`);

const body = JSON.stringify({
jsonrpc: "2.0",
method: "eth_getTransactionCount",
params: pending ? [account.address, "pending"] : [account.address],
id: Date.now(),
});

const signature =
account.address +
":" +
(await account.signMessage(keccak256(toHex(body))));

const response = await axios.post(this.mevProtectedRpcUrl, body, {
headers: {
"Content-Type": "application/json",
"X-Flashbots-Signature": signature,
},
});

const data = response.data;

logger.info(`Flashbots nonce response: ${customJSONStringify(data)}`);

if (!isFlashbotsNonceResponse(data)) {
throw new Error(
`Invalid Flashbots nonce response: ${customJSONStringify(data)}`,
);
}

const nonce = parseInt(data.result, 16);
logger.info({ address: account.address }, `Flashbots nonce: ${nonce}`);

return nonce;
}

async getBaseFeePerGas(): Promise<bigint> {
const block = await this.provider.getBlock({
blockTag: "latest",
Expand Down Expand Up @@ -258,13 +313,17 @@ export class EVMNetworkService
/**
* Get the nonce of the user
* @param address address
* @param pendingNonce include the nonce of the pending transaction
* @param pending include the nonce of the pending transaction
* @returns by default returns the next nonce of the address
* if pendingNonce is set to false, returns the nonce of the mined transaction
*/
async getNonce(address: string, pendingNonce = true): Promise<number> {
const params = pendingNonce ? [address, "pending"] : [address];
return await this.sendRpcCall(EthMethodType.GET_TRANSACTION_COUNT, params);
async getNonce(account: IEVMAccount, pending = true): Promise<number> {
if (this.mevProtectedRpcUrl) {
return this.getFlashbotsNonce(account, pending);
}

const params = pending ? [account.address, "pending"] : [account.address];
return this.sendRpcCall(EthMethodType.GET_TRANSACTION_COUNT, params);
}

async sendTransaction(
Expand Down Expand Up @@ -471,3 +530,21 @@ const getCheckReceiptTimeoutMs = (chainId: number) => {
? nodeconfig.get<number>(`chains.checkReceiptTimeout.${chainId}`)
: defaultTimeout;
};

interface FlashbotsNonceResponse {
id: number;
result: Hex;
jsonrpc: "2.0";
}

function isFlashbotsNonceResponse(
response: any,
): response is FlashbotsNonceResponse {
return (
response &&
response.id &&
response.result &&
response.jsonrpc === "2.0" &&
typeof response.result === "string"
);
}
4 changes: 3 additions & 1 deletion src/common/network/interface/INetworkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Type0TransactionGasPriceType,
Type2TransactionGasPriceType,
} from "../types";
import { IEVMAccount } from "../../../relayer/account";

export interface INetworkService<AccountType, RawTransactionType> {
chainId: number;
Expand All @@ -17,7 +18,8 @@ export interface INetworkService<AccountType, RawTransactionType> {
getLegacyGasPrice(): Promise<Type0TransactionGasPriceType>;
getEIP1559FeesPerGas(): Promise<Type2TransactionGasPriceType>;
getBalance(address: string): Promise<bigint>;
getNonce(address: string, pendingNonce?: boolean): Promise<number>;
getNonce(account: IEVMAccount, pendingNonce?: boolean): Promise<number>;
getFlashbotsNonce(account: IEVMAccount): Promise<number>;
estimateGas(params: any): Promise<any>;
sendTransaction(
rawTransactionData: RawTransactionType,
Expand Down
5 changes: 4 additions & 1 deletion src/relayer/account/EVMAccount.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PrivateKeyAccount, privateKeyToAccount } from "viem/accounts";
import { createWalletClient, WalletClient, http, Hex } from "viem";
import { createWalletClient, WalletClient, http, Hex, Address } from "viem";
import { EVMRawTransactionType } from "../../common/types";
import { IEVMAccount } from "./interface/IEVMAccount";
import { logger } from "../../common/logger";
Expand All @@ -8,6 +8,8 @@ import { hideRpcUrlApiKey } from "../../common/network/utils";
export class EVMAccount implements IEVMAccount {
public rpcUrl: string;

public address: Address;

private account: PrivateKeyAccount;

private publicKey: string;
Expand All @@ -26,6 +28,7 @@ export class EVMAccount implements IEVMAccount {
account: privateKeyToAccount(`0x${accountPrivateKey}`),
});
this.publicKey = accountPublicKey;
this.address = this.account.address;
}

getPublicKey(): string {
Expand Down
2 changes: 2 additions & 0 deletions src/relayer/account/interface/IEVMAccount.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Address } from "viem";
import { EVMRawTransactionType } from "../../../common/types";
import { IAccount } from "./IAccount";

export interface IEVMAccount extends IAccount {
rpcUrl: string;
address: Address;
getPublicKey(): string;
signMessage(message: string): Promise<string>;
signTransaction(rawTransaction: EVMRawTransactionType): Promise<string>;
Expand Down
40 changes: 26 additions & 14 deletions src/relayer/nonce-manager/EVMNonceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,37 @@ export class EVMNonceManager
this.usedNonceTracker = new NodeCache();
}

async getNonce(address: string): Promise<number> {
async getNonce(relayer: IEVMAccount): Promise<number> {
let nonce: number | undefined;
try {
nonce = this.pendingNonceTracker.get(address.toLowerCase());
nonce = this.pendingNonceTracker.get(relayer.address.toLowerCase());
log.info(
`Nonce from pendingNonceTracker for account: ${address} on chainId: ${this.chainId} is ${nonce}`,
`Nonce from pendingNonceTracker for account: ${relayer.address} on chainId: ${this.chainId} is ${nonce}`,
);

if (typeof nonce === "number") {
if (nonce === this.usedNonceTracker.get(address.toLowerCase())) {
if (
nonce === this.usedNonceTracker.get(relayer.address.toLowerCase())
) {
log.info(
`Nonce ${nonce} for address ${address} is already used on chainId: ${this.chainId}. So clearing nonce and getting nonce from network`,
`Nonce ${nonce} for address ${relayer.address} is already used on chainId: ${this.chainId}. So clearing nonce and getting nonce from network`,
);
nonce = await this.getAndSetNonceFromNetwork(address);
nonce = await this.getAndSetNonceFromNetwork(relayer);
}
} else {
nonce = await this.getAndSetNonceFromNetwork(address);
nonce = await this.getAndSetNonceFromNetwork(relayer);
}
return nonce;
} catch (error) {
log.error(
`Error in getting nonce for address: ${address} on chainId: ${
`Error in getting nonce for address: ${relayer} on chainId: ${
this.chainId
} with error: ${parseError(error)}`,
);
log.info(
`Fetching nonce from network for address: ${address} on chainId: ${this.chainId}`,
`Fetching nonce from network for address: ${relayer.address} on chainId: ${this.chainId}`,
);
return await this.getAndSetNonceFromNetwork(address);
return await this.getAndSetNonceFromNetwork(relayer);
}
}

Expand All @@ -80,12 +82,22 @@ export class EVMNonceManager
return true;
}

async getAndSetNonceFromNetwork(address: string): Promise<number> {
const nonceFromNetwork = await this.networkService.getNonce(address);
async getAndSetNonceFromNetwork(
account: IEVMAccount,
pending?: boolean,
): Promise<number> {
const nonceFromNetwork = await this.networkService.getNonce(
account,
pending,
);

log.info(
`Nonce from network for account: ${address} on chainId: ${this.chainId} is ${nonceFromNetwork}`,
`Nonce from network for account: ${account.address} on chainId: ${this.chainId} is ${nonceFromNetwork}`,
);
this.pendingNonceTracker.set(
account.address.toLowerCase(),
nonceFromNetwork,
);
this.pendingNonceTracker.set(address.toLowerCase(), nonceFromNetwork);
return nonceFromNetwork;
}
}
7 changes: 4 additions & 3 deletions src/relayer/nonce-manager/interface/INonceManager.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ICacheService } from "../../../common/cache";
import { INetworkService } from "../../../common/network";
import { IEVMAccount } from "../../account";

export interface INonceManager<AccountType, RawTransactionType> {
chainId: number;
networkService: INetworkService<AccountType, RawTransactionType>;
cacheService: ICacheService;

getNonce(address: string, pendingCount?: boolean): Promise<number>;
getNonce(relayer: IEVMAccount, pendingCount?: boolean): Promise<number>;
getAndSetNonceFromNetwork(
address: string,
pendingCount: boolean,
account: IEVMAccount,
pending: boolean,
): Promise<number>;
markUsed(address: string, nonce: number): Promise<void>;
incrementNonce(address: string): Promise<boolean>;
Expand Down
2 changes: 1 addition & 1 deletion src/relayer/relayer-manager/EVMRelayerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ export class EVMRelayerManager
const balance = Number(
toHex(await this.networkService.getBalance(relayerAddress)),
);
const nonce = await this.nonceManager.getNonce(relayerAddress);
const nonce = await this.nonceManager.getNonce(relayer);
log.info(
`Balance of relayer ${relayerAddress} is ${balance} and nonce is ${nonce} on chainId: ${this.chainId} with threshold ${this.fundingBalanceThreshold}`,
);
Expand Down
9 changes: 5 additions & 4 deletions src/relayer/transaction-service/EVMTransactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ export class EVMTransactionService
createTransactionParams;
const relayerAddress = account.getPublicKey();

const nonce = await this.nonceManager.getNonce(relayerAddress);
const nonce = await this.nonceManager.getNonce(account);
log.info(
`Nonce for relayerAddress ${relayerAddress} is ${nonce} for transactionId: ${transactionId} on chainId: ${this.chainId}`,
);
Expand Down Expand Up @@ -678,7 +678,8 @@ export class EVMTransactionService
log.info(
`Nonce too low error for for bundler address: ${rawTransaction.from} for transactionId: ${transactionId} on chainId: ${this.chainId}`,
);
const correctNonce = await this.handleNonceTooLow(rawTransaction);

const correctNonce = await this.handleNonceTooLow(account);
log.info(
`Correct nonce to be used: ${correctNonce} for for bundler address: ${rawTransaction.from} for transactionId: ${transactionId} on chainId: ${this.chainId}`,
);
Expand Down Expand Up @@ -892,9 +893,9 @@ export class EVMTransactionService
}
}

private async handleNonceTooLow(rawTransaction: EVMRawTransactionType) {
private async handleNonceTooLow(account: IEVMAccount) {
const correctNonce = await this.nonceManager.getAndSetNonceFromNetwork(
rawTransaction.from,
account,
true,
);
return correctNonce;
Expand Down

0 comments on commit aca5f34

Please sign in to comment.