Skip to content

Commit

Permalink
improve(LineaFinalizer): Force RPC calls through custom provider
Browse files Browse the repository at this point in the history
The functions that we use in the Linea SDK make event queries from block 0 to "latest" by default which means that some of our RPC's can't be used with the Finalizer. Additionally, we're missing out our custom provider retry and caching logic.

This PR removes all instances of making RPC calls via the Linea SDK's provider and replace with making the same call through our custom Provider.

The latest [Linea SDK](https://github.com/Consensys/linea-monorepo/blob/3fbe660683a318b6fa1b63ec518f948791536352/sdk/src/sdk/LineaSDK.ts#L56) code looks like it'll support injecting custom providers but its not released yet on NPM. So this code should be removed eventually once a later SDK version is released.
  • Loading branch information
nicholaspai committed Nov 26, 2024
1 parent be49e3c commit 91fae57
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 39 deletions.
84 changes: 70 additions & 14 deletions src/finalizer/utils/linea/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
getNodeUrlList,
getRedisCache,
paginatedEventQuery,
retryAsync,
CHAIN_IDs,
} from "../../../utils";
import { HubPoolClient } from "../../../clients";
Expand All @@ -40,8 +39,11 @@ export function initLineaSdk(l1ChainId: number, l2ChainId: number): LineaSDK {
}

export function makeGetMessagesWithStatusByTxHash(
srcMessageService: L1MessageServiceContract | L2MessageServiceContract,
dstClaimingService: L1ClaimingService | L2MessageServiceContract
l2Provider: ethers.providers.Provider,
l1Provider: ethers.providers.Provider,
l2MessageService: L2MessageServiceContract,
l1ClaimingService: L1ClaimingService,
l1SearchConfig: EventSearchConfig
) {
/**
* Retrieves Linea's MessageSent events for a given transaction hash and enhances them with their status.
Expand All @@ -51,18 +53,16 @@ export function makeGetMessagesWithStatusByTxHash(
*/
return async (txHashOrReceipt: string | TransactionReceipt): Promise<MessageWithStatus[]> => {
const txReceipt =
typeof txHashOrReceipt === "string"
? await srcMessageService.provider.getTransactionReceipt(txHashOrReceipt)
: txHashOrReceipt;
typeof txHashOrReceipt === "string" ? await l2Provider.getTransactionReceipt(txHashOrReceipt) : txHashOrReceipt;

if (!txReceipt) {
return [];
}

const messages = txReceipt.logs
.filter((log) => log.address === srcMessageService.contract.address)
.filter((log) => log.address === l2MessageService.contract.address)
.flatMap((log) => {
const parsedLog = srcMessageService.contract.interface.parseLog(log);
const parsedLog = l2MessageService.contract.interface.parseLog(log);

if (!parsedLog || parsedLog.name !== "MessageSent") {
return [];
Expand All @@ -84,10 +84,10 @@ export function makeGetMessagesWithStatusByTxHash(
};
});

// The Linea SDK MessageServiceContract constructs its own Provider without our retry logic so we retry each call
// twice with a 1 second delay between in case of intermittent RPC failures.
const messageStatus = await Promise.all(
messages.map((message) => retryAsync(() => dstClaimingService.getMessageStatus(message.messageHash), 2, 1))
messages.map((message) =>
getL2L1MessageStatusUsingCustomProvider(l1ClaimingService, message.messageHash, l1Provider, l1SearchConfig)
)
);
return messages.map((message, index) => ({
...message,
Expand All @@ -96,6 +96,62 @@ export function makeGetMessagesWithStatusByTxHash(
};
}

// Temporary re-implementations of the SDK's various `getMessageStatus` functions that allow us to use
// our custom provider, with retry and caching logic, to get around the SDK's hardcoded logic to query events
// from 0 to "latest" which will not work on all RPC's.
export async function getL1ToL2MessageStatusUsingCustomProvider(
messageService: L2MessageServiceContract,
messageHash: string,
l2Provider: ethers.providers.Provider
): Promise<OnChainMessageStatus> {
const l2Contract = new Contract(messageService.contract.address, messageService.contract.interface, l2Provider);
const status: BigNumber = await l2Contract.inboxL1L2MessageStatus(messageHash);
switch (status.toString()) {
case "0":
return OnChainMessageStatus.UNKNOWN;
case "1":
return OnChainMessageStatus.CLAIMABLE;
case "2":
return OnChainMessageStatus.CLAIMED;
}
}
async function getL2L1MessageStatusUsingCustomProvider(
messageService: L1ClaimingService,
messageHash: string,
l1Provider: ethers.providers.Provider,
l1SearchConfig: EventSearchConfig
): Promise<OnChainMessageStatus> {
return await getMessageStatusUsingMessageHash(messageHash, messageService, l1Provider, l1SearchConfig);
}
async function getMessageStatusUsingMessageHash(
messageHash: string,
messageService: L1ClaimingService,
l1Provider: ethers.providers.Provider,
l1SearchConfig: EventSearchConfig
): Promise<OnChainMessageStatus> {
const l1Contract = new Contract(
messageService.l1Contract.contract.address,
messageService.l1Contract.contract.interface,
l1Provider
);
const onchainStatus: BigNumber = await l1Contract.inboxL2L1MessageStatus(messageHash);
if (onchainStatus.eq(0)) {
const events = await paginatedEventQuery(
l1Contract,
l1Contract.filters.MessageClaimed(messageHash),
l1SearchConfig
);
if (events.length > 0) {
return OnChainMessageStatus.CLAIMED;
} else {
return OnChainMessageStatus.UNKNOWN;
}
} else if (onchainStatus.eq(1)) {
return OnChainMessageStatus.CLAIMABLE;
} else {
return OnChainMessageStatus.CLAIMED;
}
}
export async function getBlockRangeByHoursOffsets(
chainId: number,
fromBlockHoursOffsetToNow: number,
Expand Down Expand Up @@ -188,13 +244,13 @@ export function determineMessageType(
}

export async function findMessageSentEvents(
contract: L1MessageServiceContract | L2MessageServiceContract,
contract: Contract,
l1ToL2AddressesToFinalize: string[],
searchConfig: EventSearchConfig
): Promise<MessageSentEvent[]> {
return paginatedEventQuery(
contract.contract,
(contract.contract as Contract).filters.MessageSent(l1ToL2AddressesToFinalize, l1ToL2AddressesToFinalize),
contract,
contract.filters.MessageSent(l1ToL2AddressesToFinalize, l1ToL2AddressesToFinalize),
searchConfig
) as Promise<MessageSentEvent[]>;
}
Expand Down
21 changes: 16 additions & 5 deletions src/finalizer/utils/linea/l1ToL2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { Contract } from "ethers";
import { groupBy } from "lodash";
import { HubPoolClient, SpokePoolClient } from "../../../clients";
import { CHAIN_MAX_BLOCK_LOOKBACK, CONTRACT_ADDRESSES } from "../../../common";
import { EventSearchConfig, Signer, convertFromWei, retryAsync, winston, CHAIN_IDs } from "../../../utils";
import { EventSearchConfig, Signer, convertFromWei, winston, CHAIN_IDs } from "../../../utils";
import { CrossChainMessage, FinalizerPromise } from "../../types";
import {
determineMessageType,
findMessageFromTokenBridge,
findMessageFromUsdcBridge,
findMessageSentEvents,
getBlockRangeByHoursOffsets,
getL1ToL2MessageStatusUsingCustomProvider,
initLineaSdk,
} from "./common";

Expand Down Expand Up @@ -58,7 +59,15 @@ export async function lineaL1ToL2Finalizer(
};

const [wethAndRelayEvents, tokenBridgeEvents, usdcBridgeEvents] = await Promise.all([
findMessageSentEvents(l1MessageServiceContract, l1ToL2AddressesToFinalize, searchConfig),
findMessageSentEvents(
new Contract(
l1MessageServiceContract.contractAddress,
l1MessageServiceContract.contract.interface,
hubPoolClient.hubPool.provider
),
l1ToL2AddressesToFinalize,
searchConfig
),
findMessageFromTokenBridge(l1TokenBridge, l1MessageServiceContract, l1ToL2AddressesToFinalize, searchConfig),
findMessageFromUsdcBridge(l1UsdcBridge, l1MessageServiceContract, l1ToL2AddressesToFinalize, searchConfig),
]);
Expand All @@ -73,9 +82,11 @@ export async function lineaL1ToL2Finalizer(
// It's unlikely that our multicall will have multiple transactions to bridge to Linea
// so we can grab the statuses individually.

// The Linea SDK MessageServiceContract constructs its own Provider without our retry logic so we retry each call
// twice with a 1 second delay between in case of intermittent RPC failures.
const messageStatus = await retryAsync(() => l2MessageServiceContract.getMessageStatus(_messageHash), 2, 1);
const messageStatus = await getL1ToL2MessageStatusUsingCustomProvider(
l2MessageServiceContract,
_messageHash,
_spokePoolClient.spokePool.provider
);
return {
messageSender: _from,
destination: _to,
Expand Down
37 changes: 17 additions & 20 deletions src/finalizer/utils/linea/l2ToL1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Wallet } from "ethers";
import { groupBy } from "lodash";

import { HubPoolClient, SpokePoolClient } from "../../../clients";
import { Signer, winston, convertFromWei, getL1TokenInfo } from "../../../utils";
import { Signer, winston, convertFromWei, getL1TokenInfo, getProvider } from "../../../utils";
import { FinalizerPromise, CrossChainMessage } from "../../types";
import { TokensBridged } from "../../../interfaces";
import {
Expand All @@ -12,6 +12,7 @@ import {
MessageWithStatus,
getBlockRangeByHoursOffsets,
} from "./common";
import { CHAIN_MAX_BLOCK_LOOKBACK } from "../../../common";

export async function lineaL2ToL1Finalizer(
logger: winston.Logger,
Expand All @@ -24,7 +25,19 @@ export async function lineaL2ToL1Finalizer(
const l2Contract = lineaSdk.getL2Contract();
const l1Contract = lineaSdk.getL1Contract();
const l1ClaimingService = lineaSdk.getL1ClaimingService(l1Contract.contractAddress);
const getMessagesWithStatusByTxHash = makeGetMessagesWithStatusByTxHash(l2Contract, l1ClaimingService);
const { fromBlock: l1FromBlock, toBlock: l1ToBlock } = await getBlockRangeByHoursOffsets(l1ChainId, 24 * 7, 0);
const l1SearchConfig = {
fromBlock: l1FromBlock,
toBlock: l1ToBlock,
maxBlockLookBack: CHAIN_MAX_BLOCK_LOOKBACK[l1ChainId] || 10_000,
};
const getMessagesWithStatusByTxHash = makeGetMessagesWithStatusByTxHash(
await getProvider(l2ChainId),
await getProvider(l1ChainId),
l2Contract,
l1ClaimingService,
l1SearchConfig
);

// Optimize block range for querying relevant source events on L2.
// Linea L2->L1 messages are claimable after 6 - 32 hours
Expand Down Expand Up @@ -65,24 +78,8 @@ export async function lineaL2ToL1Finalizer(
// Populate txns for claimable messages
const populatedTxns = await Promise.all(
claimable.map(async ({ message }) => {
const isProofNeeded = await l1ClaimingService.isClaimingNeedingProof(message.messageHash);

if (isProofNeeded) {
const proof = await l1ClaimingService.getMessageProof(message.messageHash);
return l1ClaimingService.l1Contract.contract.populateTransaction.claimMessageWithProof({
from: message.messageSender,
to: message.destination,
fee: message.fee,
value: message.value,
feeRecipient: (signer as Wallet).address,
data: message.calldata,
messageNumber: message.messageNonce,
proof: proof.proof,
leafIndex: proof.leafIndex,
merkleRoot: proof.root,
});
}

// In prior Linea message service contract versions, a proof would have to be submitted to claim messages, but
// of the latest version the message can be claimed directly.
return l1ClaimingService.l1Contract.contract.populateTransaction.claimMessage(
message.messageSender,
message.destination,
Expand Down

0 comments on commit 91fae57

Please sign in to comment.