Skip to content

feat: add finalizer support for messages from HubPool to UniversalSpoke #2203

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

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ea38af8
implement new finalizer adapter: helios.ts. Not tested and probably b…
grasphoper Apr 29, 2025
6dd6e53
checkpoint
grasphoper Apr 30, 2025
5ae49c7
change FinalizerPromise returned txns type from Multicall2Call to (Mu…
grasphoper Apr 30, 2025
dfbe2b9
changes to helios.ts to submit individual txs rather than use multicall
grasphoper Apr 30, 2025
36a5e4a
revert logging additions from TransactionClient
grasphoper Apr 30, 2025
5273bf5
remove more
grasphoper Apr 30, 2025
2b75bc0
move ABI import to helios.ts
grasphoper Apr 30, 2025
7973c1b
cleanup
grasphoper Apr 30, 2025
a72fdbe
fix tx sending bugs
grasphoper May 1, 2025
cfc7d58
refactor
grasphoper May 1, 2025
de365d4
update deps. BNB -> BSC
grasphoper May 1, 2025
ade32d2
Merge branch 'master' into if/helios-finalizer
grasphoper May 1, 2025
0dcaa58
return missing EOF whitespace
grasphoper May 1, 2025
6c85303
polish
grasphoper May 1, 2025
9dce411
allow completing half-finalized messages
grasphoper May 1, 2025
12b65d2
Merge remote-tracking branch 'origin/master' into if/helios-finalizer
grasphoper May 1, 2025
c3da00a
remove all BSC TEST crutches; adjust log severity in helios.ts
grasphoper May 1, 2025
cde34f0
polish
grasphoper May 1, 2025
381fc4d
fix lint + use real proof instead of 0x on .update tx submission
grasphoper May 1, 2025
4f8a0c3
add newline to .prettierignore
grasphoper May 1, 2025
d7c7b1d
Merge branch 'master' into if/helios-finalizer
grasphoper May 1, 2025
dabccb9
Merge branch 'master' into if/helios-finalizer
nicholaspai May 2, 2025
9bcc28a
strip abi files
grasphoper May 2, 2025
4340345
address part of the PR comments
grasphoper May 2, 2025
f23444a
Move some code into interface/ utils/ folder instead of helios.ts
grasphoper May 2, 2025
ee3ec3f
remove all but one try - catch statements
grasphoper May 2, 2025
6a5ae93
address the rest of the review issues except .reduce / spreadEventWit…
grasphoper May 2, 2025
910b586
add isAugmentedTransaction + spreadEventWithBlockNumber
grasphoper May 2, 2025
f719359
use stringifyThrownValue
grasphoper May 2, 2025
f5e4361
Merge branch 'master' into if/helios-finalizer
grasphoper May 2, 2025
4adbb2d
merge chainFinalizers entries for BSC
grasphoper May 2, 2025
093d9c5
clarify ZK API response handling
grasphoper May 2, 2025
380cc2b
move contract utility functions to src/utils
grasphoper May 2, 2025
9a281ff
fix lint
grasphoper May 2, 2025
7d160f6
fix 1 more lint warning
grasphoper May 2, 2025
e57ab8f
new SP1Helios address; manual gasLimit; shorter event lookback on L1
grasphoper May 3, 2025
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ coverage*
gasReporterOutput.json
dist
typechain*
addresses.json
12 changes: 12 additions & 0 deletions src/common/ContractAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import BLAST_OPTIMISM_PORTAL_ABI from "./abi/BlastOptimismPortal.json";
import SCROLL_GATEWAY_ROUTER_L1_ABI from "./abi/ScrollGatewayRouterL1.json";
import SCROLL_GATEWAY_ROUTER_L2_ABI from "./abi/ScrollGatewayRouterL2.json";
import SCROLL_GAS_PRICE_ORACLE_ABI from "./abi/ScrollGasPriceOracle.json";
import HUB_POOL_STORE_ABI from "./abi/HubPoolStore.json";
import SP1_HELIOS_ABI from "./abi/SP1Helios.json";

// Constants file exporting hardcoded contract addresses per chain.
export const CONTRACT_ADDRESSES: {
Expand Down Expand Up @@ -199,6 +201,10 @@ export const CONTRACT_ADDRESSES: {
address: "0xc186fA914353c44b2E33eBE05f21846F1048bEda",
abi: HUB_POOL_ABI,
},
hubPoolStore: {
address: "0x1Ace3BbD69b63063F859514Eca29C9BDd8310E61",
abi: HUB_POOL_STORE_ABI,
},
blastBridge: {
address: "0x3a05E5d33d7Ab3864D53aaEc93c8301C1Fa49115",
abi: BLAST_BRIDGE_ABI,
Expand Down Expand Up @@ -249,6 +255,12 @@ export const CONTRACT_ADDRESSES: {
abi: CCTP_TOKEN_MESSENGER_ABI,
},
},
[CHAIN_IDs.BSC]: {
sp1Helios: {
address: "0x3BED21dAe767e4Df894B31b14aD32369cE4bad8b",
abi: SP1_HELIOS_ABI,
},
},
[CHAIN_IDs.POLYGON]: {
withdrawableErc20: {
abi: POLYGON_WITHDRAWABLE_ERC20_ABI,
Expand Down
12 changes: 12 additions & 0 deletions src/common/abi/HubPoolStore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "target", "type": "address" },
{ "indexed": false, "internalType": "bytes", "name": "data", "type": "bytes" },
{ "indexed": true, "internalType": "uint256", "name": "nonce", "type": "uint256" }
],
"name": "StoredCallData",
"type": "event"
}
]
37 changes: 37 additions & 0 deletions src/common/abi/SP1Helios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "uint256", "name": "head", "type": "uint256" },
{ "indexed": true, "internalType": "bytes32", "name": "key", "type": "bytes32" },
{ "indexed": false, "internalType": "bytes32", "name": "value", "type": "bytes32" },
{ "indexed": false, "internalType": "address", "name": "contractAddress", "type": "address" }
],
"name": "StorageSlotVerified",
"type": "event"
},
{
"inputs": [],
"name": "head",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }],
"name": "headers",
"outputs": [{ "internalType": "bytes32", "name": "beaconHeaderRoot", "type": "bytes32" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "bytes", "name": "proof", "type": "bytes" },
{ "internalType": "bytes", "name": "publicValues", "type": "bytes" }
],
"name": "update",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
22 changes: 22 additions & 0 deletions src/common/abi/Universal_SpokePool.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "uint256", "name": "nonce", "type": "uint256" },
{ "indexed": false, "internalType": "address", "name": "caller", "type": "address" }
],
"name": "RelayedCallData",
"type": "event"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_messageNonce", "type": "uint256" },
{ "internalType": "bytes", "name": "_message", "type": "bytes" },
{ "internalType": "uint256", "name": "_blockNumber", "type": "uint256" }
],
"name": "executeMessage",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
102 changes: 67 additions & 35 deletions src/finalizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ import {
Profiler,
stringifyThrownValue,
} from "../utils";
import { ChainFinalizer, CrossChainMessage } from "./types";
import { ChainFinalizer, CrossChainMessage, isAugmentedTransaction } from "./types";
import {
arbStackFinalizer,
binanceL1ToL2Finalizer,
binanceL2ToL1Finalizer,
cctpL1toL2Finalizer,
cctpL2toL1Finalizer,
heliosL1toL2Finalizer,
lineaL1ToL2Finalizer,
lineaL2ToL1Finalizer,
opStackFinalizer,
Expand Down Expand Up @@ -124,7 +125,7 @@ const chainFinalizers: { [chainId: number]: { finalizeOnL2: ChainFinalizer[]; fi
},
[CHAIN_IDs.BSC]: {
finalizeOnL1: [binanceL2ToL1Finalizer],
finalizeOnL2: [binanceL1ToL2Finalizer],
finalizeOnL2: [binanceL1ToL2Finalizer, heliosL1toL2Finalizer],
},
[CHAIN_IDs.SONEIUM]: {
finalizeOnL1: [opStackFinalizer],
Expand Down Expand Up @@ -190,7 +191,8 @@ export async function finalize(

// Note: Could move this into a client in the future to manage # of calls and chunk calls based on
// input byte length.
const finalizationsToBatch: { txn: Multicall2Call; crossChainMessage?: CrossChainMessage }[] = [];
const finalizerResponseTxns: { txn: Multicall2Call | AugmentedTransaction; crossChainMessage?: CrossChainMessage }[] =
[];

// For each chain, delegate to a handler to look up any TokensBridged events and attempt finalization.
await sdkUtils.mapAsync(configuredChainIds, async (chainId) => {
Expand Down Expand Up @@ -264,7 +266,7 @@ export async function finalize(
);

callData.forEach((txn, idx) => {
finalizationsToBatch.push({ txn, crossChainMessage: crossChainMessages[idx] });
finalizerResponseTxns.push({ txn, crossChainMessage: crossChainMessages[idx] });
});

totalWithdrawalsForChain += crossChainMessages.filter(({ type }) => type === "withdrawal").length;
Expand Down Expand Up @@ -313,25 +315,39 @@ export async function finalize(
// counter of the approximate gas estimation and cut off the list of finalizations if it gets too high.

// Ensure each transaction would succeed in isolation.
const finalizations = await sdkUtils.filterAsync(finalizationsToBatch, async ({ txn: _txn, crossChainMessage }) => {
const txnToSubmit: AugmentedTransaction = {
contract: multicall2Lookup[crossChainMessage.destinationChainId],
chainId: crossChainMessage.destinationChainId,
method: "aggregate",
// aggregate() takes an array of tuples: [calldata: bytes, target: address].
args: [[_txn]],
};
const [{ reason, succeed, transaction }] = await txnClient.simulate([txnToSubmit]);

if (succeed) {
// Increase running counter of estimated gas cost for batch finalization.
// gasLimit should be defined if succeed is True.
const updatedGasEstimation = gasEstimation.add(transaction.gasLimit);
if (updatedGasEstimation.lt(batchGasLimit)) {
gasEstimation = updatedGasEstimation;
const finalizations = await sdkUtils.filterAsync(finalizerResponseTxns, async ({ txn: _txn, crossChainMessage }) => {
let simErrorReason: string;
if (!isAugmentedTransaction(_txn)) {
// Multicall transaction simulation flow
const txnToSubmit: AugmentedTransaction = {
contract: multicall2Lookup[crossChainMessage.destinationChainId],
chainId: crossChainMessage.destinationChainId,
method: "aggregate",
// aggregate() takes an array of tuples: [calldata: bytes, target: address].
args: [[_txn]],
};
const [{ reason, succeed, transaction }] = await txnClient.simulate([txnToSubmit]);

if (succeed) {
// Increase running counter of estimated gas cost for batch finalization.
// gasLimit should be defined if succeed is True.
const updatedGasEstimation = gasEstimation.add(transaction.gasLimit);
if (updatedGasEstimation.lt(batchGasLimit)) {
gasEstimation = updatedGasEstimation;
return true;
} else {
return false;
}
} else {
simErrorReason = reason;
}
} else {
// Individual transaction simulation flow
const [{ reason, succeed }] = await txnClient.simulate([_txn]);
if (succeed) {
return true;
} else {
return false;
simErrorReason = reason;
}
}

Expand All @@ -346,7 +362,7 @@ export async function finalize(
// @dev Likely to be the 2nd part of a 2-stage withdrawal (i.e. retrieve() on the Polygon bridge adapter).
message = "Unknown finalizer simulation failure.";
}
logger.warn({ at: "finalizer", message, reason, txn: _txn });
logger.warn({ at: "finalizer", message, simErrorReason, txn: _txn });
return false;
});

Expand All @@ -362,20 +378,36 @@ export async function finalize(
finalizations,
({ crossChainMessage }) => crossChainMessage.destinationChainId
);

// @dev Here, we enqueueTransaction individual transactions right away, and we batch all multicalls into `multicallTxns` to enqueue as a single tx right after
for (const [chainId, finalizations] of Object.entries(finalizationsByChain)) {
const finalizerTxns = finalizations.map(({ txn }) => txn);
const txnToSubmit: AugmentedTransaction = {
contract: multicall2Lookup[Number(chainId)],
chainId: Number(chainId),
method: "aggregate",
args: [finalizerTxns],
gasLimit: gasEstimation,
gasLimitMultiplier: 2,
unpermissioned: true,
message: `Batch finalized ${finalizerTxns.length} txns`,
mrkdwn: `Batch finalized ${finalizerTxns.length} txns`,
};
multicallerClient.enqueueTransaction(txnToSubmit);
const multicallTxns: Multicall2Call[] = [];

finalizations.forEach(({ txn }) => {
if (isAugmentedTransaction(txn)) {
// It's an AugmentedTransaction, enqueue directly
txn.nonMulticall = true; // cautiously enforce an invariant that should already be present
multicallerClient.enqueueTransaction(txn);
} else {
// It's a Multicall2Call, collect for batching
multicallTxns.push(txn);
}
});

if (multicallTxns.length > 0) {
const txnToSubmit: AugmentedTransaction = {
contract: multicall2Lookup[Number(chainId)],
chainId: Number(chainId),
method: "aggregate",
args: [multicallTxns],
gasLimit: gasEstimation,
gasLimitMultiplier: 2,
unpermissioned: true,
message: `Batch finalized ${multicallTxns.length} txns`,
mrkdwn: `Batch finalized ${multicallTxns.length} txns`,
};
multicallerClient.enqueueTransaction(txnToSubmit);
}
}
txnRefLookup = await multicallerClient.executeTxnQueues(!submitFinalizationTransactions);
} catch (_error) {
Expand Down
17 changes: 15 additions & 2 deletions src/finalizer/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Signer } from "ethers";
import { HubPoolClient, SpokePoolClient } from "../clients";
import { AugmentedTransaction, HubPoolClient, SpokePoolClient } from "../clients";
import { Multicall2Call, winston } from "../utils";

/**
Expand Down Expand Up @@ -28,7 +28,10 @@ export type CrossChainMessage = {
}
);

export type FinalizerPromise = { callData: Multicall2Call[]; crossChainMessages: CrossChainMessage[] };
export type FinalizerPromise = {
callData: (Multicall2Call | AugmentedTransaction)[];
crossChainMessages: CrossChainMessage[];
};

export interface ChainFinalizer {
(
Expand All @@ -40,3 +43,13 @@ export interface ChainFinalizer {
l1ToL2AddressesToFinalize: string[]
): Promise<FinalizerPromise>;
}

/**
* Type guard to check if a transaction object is an AugmentedTransaction.
* @param txn The transaction object to check.
* @returns True if the object is an AugmentedTransaction, false otherwise.
*/
export function isAugmentedTransaction(txn: Multicall2Call | AugmentedTransaction): txn is AugmentedTransaction {
// Check for the presence of the 'contract' property, unique to AugmentedTransaction
return txn != null && typeof txn === "object" && "contract" in txn;
}
Loading