From ea38af8976721217962b581ee94547474b4a3c6a Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Mon, 28 Apr 2025 20:18:56 -0700 Subject: [PATCH 01/31] implement new finalizer adapter: helios.ts. Not tested and probably broken. Init commit Signed-off-by: Ihor Farion --- package.json | 2 +- src/common/ContractAddresses.ts | 12 + src/common/abi/HubPoolStore.json | 43 ++ src/common/abi/SP1Helios.json | 376 +++++++++++++++ src/finalizer/index.ts | 5 + src/finalizer/utils/helios.ts | 791 +++++++++++++++++++++++++++++++ src/finalizer/utils/index.ts | 1 + src/utils/Sp1HeliosUtils.ts | 5 + yarn.lock | 8 +- 9 files changed, 1238 insertions(+), 5 deletions(-) create mode 100644 src/common/abi/HubPoolStore.json create mode 100644 src/common/abi/SP1Helios.json create mode 100644 src/finalizer/utils/helios.ts create mode 100644 src/utils/Sp1HeliosUtils.ts diff --git a/package.json b/package.json index 490640f030..2d3bd23049 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "node": ">=20.18.0" }, "dependencies": { - "@across-protocol/constants": "^3.1.52", + "@across-protocol/constants": "^3.1.54", "@across-protocol/contracts": "^4.0.5", "@across-protocol/sdk": "4.1.46", "@arbitrum/sdk": "^4.0.2", diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index bed274ad19..7d004634f2 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -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: { @@ -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, @@ -249,6 +255,12 @@ export const CONTRACT_ADDRESSES: { abi: CCTP_TOKEN_MESSENGER_ABI, }, }, + [CHAIN_IDs.BNB]: { + sp1Helios: { + address: "0x6999526e507Cc3b03b180BbE05E1Ff938259A874", // todo: change for prod. This uses a mock proof verifier + abi: SP1_HELIOS_ABI, + }, + }, [CHAIN_IDs.POLYGON]: { withdrawableErc20: { abi: POLYGON_WITHDRAWABLE_ERC20_ABI, diff --git a/src/common/abi/HubPoolStore.json b/src/common/abi/HubPoolStore.json new file mode 100644 index 0000000000..42d01153fd --- /dev/null +++ b/src/common/abi/HubPoolStore.json @@ -0,0 +1,43 @@ +[ + { + "inputs": [{ "internalType": "address", "name": "_hubPool", "type": "address" }], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "inputs": [], "name": "NotHubPool", "type": "error" }, + { + "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" + }, + { + "inputs": [], + "name": "hubPool", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "relayMessageCallData", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "target", "type": "address" }, + { "internalType": "bytes", "name": "data", "type": "bytes" }, + { "internalType": "bool", "name": "isAdminSender", "type": "bool" } + ], + "name": "storeRelayMessageCalldata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/common/abi/SP1Helios.json b/src/common/abi/SP1Helios.json new file mode 100644 index 0000000000..1bac6e8c52 --- /dev/null +++ b/src/common/abi/SP1Helios.json @@ -0,0 +1,376 @@ +[ + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "executionStateRoot", "type": "bytes32" }, + { "internalType": "uint256", "name": "genesisTime", "type": "uint256" }, + { "internalType": "uint256", "name": "head", "type": "uint256" }, + { "internalType": "bytes32", "name": "header", "type": "bytes32" }, + { "internalType": "bytes32", "name": "heliosProgramVkey", "type": "bytes32" }, + { "internalType": "uint256", "name": "secondsPerSlot", "type": "uint256" }, + { "internalType": "uint256", "name": "slotsPerEpoch", "type": "uint256" }, + { "internalType": "uint256", "name": "slotsPerPeriod", "type": "uint256" }, + { "internalType": "bytes32", "name": "syncCommitteeHash", "type": "bytes32" }, + { "internalType": "address", "name": "verifier", "type": "address" }, + { "internalType": "address[]", "name": "updaters", "type": "address[]" } + ], + "internalType": "struct SP1Helios.InitParams", + "name": "params", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "inputs": [], "name": "AccessControlBadConfirmation", "type": "error" }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "bytes32", "name": "neededRole", "type": "bytes32" } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], + "name": "ExecutionStateRootMismatch", + "type": "error" + }, + { + "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], + "name": "NewHeaderMismatch", + "type": "error" + }, + { "inputs": [], "name": "NoUpdatersProvided", "type": "error" }, + { + "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], + "name": "NonIncreasingHead", + "type": "error" + }, + { + "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], + "name": "PreviousHeadTooOld", + "type": "error" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "given", "type": "bytes32" }, + { "internalType": "bytes32", "name": "expected", "type": "bytes32" } + ], + "name": "PreviousHeaderMismatch", + "type": "error" + }, + { + "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], + "name": "PreviousHeaderNotSet", + "type": "error" + }, + { + "inputs": [{ "internalType": "uint256", "name": "period", "type": "uint256" }], + "name": "SyncCommitteeAlreadySet", + "type": "error" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "given", "type": "bytes32" }, + { "internalType": "bytes32", "name": "expected", "type": "bytes32" } + ], + "name": "SyncCommitteeStartMismatch", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "uint256", "name": "slot", "type": "uint256" }, + { "indexed": true, "internalType": "bytes32", "name": "root", "type": "bytes32" } + ], + "name": "HeadUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "previousAdminRole", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "newAdminRole", "type": "bytes32" } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "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" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "uint256", "name": "period", "type": "uint256" }, + { "indexed": true, "internalType": "bytes32", "name": "root", "type": "bytes32" } + ], + "name": "SyncCommitteeUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "updater", "type": "address" }], + "name": "UpdaterAdded", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GENESIS_TIME", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_SLOT_AGE", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SECONDS_PER_SLOT", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SLOTS_PER_EPOCH", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SLOTS_PER_PERIOD", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UPDATER_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }, + { "internalType": "address", "name": "contractAddress", "type": "address" }, + { "internalType": "bytes32", "name": "storageSlot", "type": "bytes32" } + ], + "name": "computeStorageKey", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }], + "name": "executionStateRoots", + "outputs": [{ "internalType": "bytes32", "name": "executionStateRoot", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentEpoch", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleAdmin", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "getRoleMember", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleMemberCount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }, + { "internalType": "address", "name": "contractAddress", "type": "address" }, + { "internalType": "bytes32", "name": "storageSlot", "type": "bytes32" } + ], + "name": "getStorageSlot", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], + "name": "getSyncCommitteePeriod", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "hasRole", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "head", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "headTimestamp", + "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": [], + "name": "heliosProgramVkey", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "callerConfirmation", "type": "address" } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], + "name": "slotTimestamp", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "computedStorageKey", "type": "bytes32" }], + "name": "storageValues", + "outputs": [{ "internalType": "bytes32", "name": "storageValue", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "syncCommitteePeriod", "type": "uint256" }], + "name": "syncCommittees", + "outputs": [{ "internalType": "bytes32", "name": "syncCommitteeHash", "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" + }, + { + "inputs": [], + "name": "verifier", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index ac249a810c..b8355b93cb 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -37,6 +37,7 @@ import { arbStackFinalizer, cctpL1toL2Finalizer, cctpL2toL1Finalizer, + heliosL1toL2Finalizer, lineaL1ToL2Finalizer, lineaL2ToL1Finalizer, opStackFinalizer, @@ -68,6 +69,10 @@ const chainFinalizers: { [chainId: number]: { finalizeOnL2: ChainFinalizer[]; fi finalizeOnL1: [opStackFinalizer, cctpL2toL1Finalizer], finalizeOnL2: [cctpL1toL2Finalizer], }, + [CHAIN_IDs.BNB]: { + finalizeOnL1: [heliosL1toL2Finalizer], + finalizeOnL2: [], + }, [CHAIN_IDs.POLYGON]: { finalizeOnL1: [polygonFinalizer, cctpL2toL1Finalizer], finalizeOnL2: [cctpL1toL2Finalizer], diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts new file mode 100644 index 0000000000..33f57b8c9d --- /dev/null +++ b/src/finalizer/utils/helios.ts @@ -0,0 +1,791 @@ +import { ethers } from "ethers"; +import { HubPoolClient, SpokePoolClient } from "../../clients"; +import { + EventSearchConfig, + Signer, + Multicall2Call, + winston, + paginatedEventQuery, + compareAddressesSimple, +} from "../../utils"; +import { FinalizerPromise, CrossChainMessage } from "../types"; +import { getSp1Helios } from "../../utils/Sp1HeliosUtils"; +import { Log } from "../../interfaces"; +import { CONTRACT_ADDRESSES } from "../../common"; +import axios from "axios"; + +// Define interfaces for the event arguments for clarity +interface StoredCallDataEventArgs { + target: string; + data: string; + nonce: ethers.BigNumber; +} + +interface StorageSlotVerifiedEventArgs { + head: ethers.BigNumber; + key: string; // bytes32 + value: string; // bytes32 + contractAddress: string; +} + +// Type for the structured StoredCallData event +type StoredCallDataEvent = Log & { args: StoredCallDataEventArgs }; +// Type for the structured StorageSlotVerified event +type StorageSlotVerifiedEvent = Log & { args: StorageSlotVerifiedEventArgs }; + +// --- API Interaction Types --- +interface ApiProofRequest { + src_chain_contract_address: string; + src_chain_storage_slot: string; + src_chain_block_number: number; // Changed from u64 for JS compatibility + dst_chain_contract_from_head: number; // Changed from u64 for JS compatibility + dst_chain_contract_from_header: string; +} + +type ProofStatus = "pending" | "success" | "errored"; + +interface SP1HeliosProofData { + proof: string; + public_values: string; +} + +interface ProofStateResponse { + proof_id: string; + status: ProofStatus; + update_calldata?: SP1HeliosProofData; // Present only if status is "success" + error_message?: string; // Present only if status is "errored" +} + +// Define the structure for ProofOutputs to decode public_values +const proofOutputsAbiTuple = `tuple( + bytes32 executionStateRoot, + bytes32 newHeader, + bytes32 nextSyncCommitteeHash, + uint256 newHead, + bytes32 prevHeader, + uint256 prevHead, + bytes32 syncCommitteeHash, + bytes32 startSyncCommitteeHash, + tuple(bytes32 key, bytes32 value, address contractAddress)[] slots ++)`; + +// Type for the decoded ProofOutputs structure +type DecodedProofOutputs = { + executionStateRoot: string; + newHeader: string; + nextSyncCommitteeHash: string; + newHead: ethers.BigNumber; // Access the newHead value + prevHeader: string; + prevHead: ethers.BigNumber; + syncCommitteeHash: string; + startSyncCommitteeHash: string; + slots: { key: string; value: string; contractAddress: string }[]; // Added contractAddress +}; + +// Type for successful proof data, augmented with source info. +type SuccessfulProof = { + proofData: SP1HeliosProofData; + sourceNonce: ethers.BigNumber; + target: string; + sourceMessageData: string; // Original calldata from HubPoolStore event +}; + +export async function heliosL1toL2Finalizer( + logger: winston.Logger, + _signer: Signer, + hubPoolClient: HubPoolClient, + l2SpokePoolClient: SpokePoolClient, // Used for filtering target address + l1SpokePoolClient: SpokePoolClient, + _senderAddresses: string[] +): Promise { + const l1ChainId = hubPoolClient.chainId; + const l2ChainId = l2SpokePoolClient.chainId; + const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; // Get L2 SpokePool address + + // --- Step 1: Query and Filter L1 Events --- + const relevantStoredCallDataEvents = await getAndFilterL1Events( + logger, + hubPoolClient, + l1SpokePoolClient, + l1ChainId, + l2ChainId, + l2SpokePoolAddress + ); + if (!relevantStoredCallDataEvents || relevantStoredCallDataEvents.length === 0) { + return { callData: [], crossChainMessages: [] }; + } + + // --- Step 2: Query L2 Verification Events --- + const verifiedKeys = await getL2VerifiedKeys(logger, l2SpokePoolClient, l2ChainId); + if (verifiedKeys === null) { + // Indicates an error occurred fetching L2 events + return { callData: [], crossChainMessages: [] }; + } + + // --- Step 3: Identify Unfinalized Messages --- + const unfinalizedMessages = findUnfinalizedMessages(logger, relevantStoredCallDataEvents, verifiedKeys, l2ChainId); + if (unfinalizedMessages.length === 0) { + return { callData: [], crossChainMessages: [] }; + } + + // --- Step 4: Get Proofs for Unfinalized Messages --- + const proofsToSubmit = await processUnfinalizedHeliosMessages( + logger, + unfinalizedMessages, // Pass the unfinalized messages containing original event data + l2SpokePoolClient, + l1ChainId + ); + if (proofsToSubmit.length === 0) { + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, + message: "No successful proofs retrieved to submit.", + }); + return { callData: [], crossChainMessages: [] }; + } + + // --- Step 5: Generate Multicall Data from Proofs --- + return generateHeliosMulticallData( + logger, + proofsToSubmit, + l1ChainId, + l2ChainId, + l2SpokePoolClient // Pass the client to access spokePool contract + ); +} + +// ================================== +// Step-by-step Helper Functions +// ================================== + +/** STEP 1: Query and Filter L1 Events */ +async function getAndFilterL1Events( + logger: winston.Logger, + hubPoolClient: HubPoolClient, + l1SpokePoolClient: SpokePoolClient, + l1ChainId: number, + l2ChainId: number, + l2SpokePoolAddress: string +): Promise { + const l1Provider = hubPoolClient.hubPool.provider; // Get provider from HubPoolClient + + const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; + if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, + message: `HubPoolStore contract address or ABI not found for L1 chain ${l1ChainId}.`, + }); + return null; + } + + const hubPoolStoreContract = new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, l1Provider); + + const l1SearchConfig: EventSearchConfig = { + fromBlock: l1SpokePoolClient.eventSearchConfig.fromBlock, + toBlock: l1SpokePoolClient.latestBlockSearched, + maxBlockLookBack: l1SpokePoolClient.eventSearchConfig.maxBlockLookBack, + }; + + const storedCallDataFilter = hubPoolStoreContract.filters.StoredCallData(); + + try { + logger.debug({ + at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, + message: `Querying StoredCallData events on L1 (${l1ChainId})`, + hubPoolStoreAddress: hubPoolStoreInfo.address, + fromBlock: l1SearchConfig.fromBlock, + toBlock: l1SearchConfig.toBlock, + }); + + const rawLogs = await paginatedEventQuery(hubPoolStoreContract, storedCallDataFilter, l1SearchConfig); + + // Explicitly cast logs to the correct type + const events: StoredCallDataEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as StoredCallDataEventArgs, // todo: is this type correct? + })); + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, + message: `Found ${events.length} StoredCallData events on L1 (${l1ChainId})`, + }); + + const relevantStoredCallDataEvents = events.filter( + (event) => + compareAddressesSimple(event.args.target, l2SpokePoolAddress) || + compareAddressesSimple(event.args.target, ethers.constants.AddressZero) + ); + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, + message: `Filtered ${events.length} StoredCallData events down to ${relevantStoredCallDataEvents.length} relevant targets (${l2SpokePoolAddress} or zero address).`, + }); + + return relevantStoredCallDataEvents; + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, + message: `Failed to query StoredCallData events from L1 (${l1ChainId})`, + hubPoolStoreAddress: hubPoolStoreInfo.address, + error, + }); + return null; // Return null on error + } +} + +/** STEP 2: Query L2 Verification Events and return verified keys */ +async function getL2VerifiedKeys( + logger: winston.Logger, + l2SpokePoolClient: SpokePoolClient, + l2ChainId: number +): Promise | null> { + const l2Provider = l2SpokePoolClient.spokePool.provider; // Get provider from L2 client + + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); + if (!sp1HeliosAddress || !sp1HeliosAbi) { + logger.warn({ + at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, + message: `SP1Helios contract not found for destination chain ${l2ChainId}. Cannot verify Helios messages.`, + }); + return null; + } + const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); + + const l2SearchConfig: EventSearchConfig = { + fromBlock: l2SpokePoolClient.eventSearchConfig.fromBlock, + toBlock: l2SpokePoolClient.latestBlockSearched, + maxBlockLookBack: l2SpokePoolClient.eventSearchConfig.maxBlockLookBack, + }; + const storageVerifiedFilter = sp1HeliosContract.filters.StorageSlotVerified(); + + try { + logger.debug({ + at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, + message: `Querying StorageSlotVerified events on L2 (${l2ChainId})`, + sp1HeliosAddress, + fromBlock: l2SearchConfig.fromBlock, + toBlock: l2SearchConfig.toBlock, + }); + + const rawLogs = await paginatedEventQuery(sp1HeliosContract, storageVerifiedFilter, l2SearchConfig); + + // Explicitly cast logs to the correct type + const events: StorageSlotVerifiedEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as StorageSlotVerifiedEventArgs, // todo: is this a correct type? + })); + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, + message: `Found ${events.length} StorageSlotVerified events on L2 (${l2ChainId})`, + }); + return new Set(events.map((event) => event.args.key)); + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, + message: `Failed to query StorageSlotVerified events from L2 (${l2ChainId})`, + sp1HeliosAddress, + error, + }); + return null; + } +} + +/** STEP 3: Identify Unfinalized Messages */ +function findUnfinalizedMessages( + logger: winston.Logger, + relevantStoredCallDataEvents: StoredCallDataEvent[], + verifiedKeys: Set, + l2ChainId: number +): StoredCallDataEvent[] { + const unfinalizedMessages = relevantStoredCallDataEvents.filter((event) => { + const expectedStorageSlot = calculateHubPoolStoreStorageSlot(event.args); + return !verifiedKeys.has(expectedStorageSlot); + }); + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:findUnfinalizedMessages:${l2ChainId}`, + message: `Detected ${unfinalizedMessages.length} unfinalized Helios messages after target filtering and verification check.`, + totalRelevantStoredCallData: relevantStoredCallDataEvents.length, + totalStorageVerified: verifiedKeys.size, + }); + + return unfinalizedMessages; +} + +/** STEP 5: Generate Multicall Data */ +async function generateHeliosMulticallData( + logger: winston.Logger, + proofsToSubmit: SuccessfulProof[], + l1ChainId: number, + l2ChainId: number, + l2SpokePoolClient: SpokePoolClient +): Promise { + const multiCallData: Multicall2Call[] = []; + const crossChainMessages: CrossChainMessage[] = []; + + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); + if (!sp1HeliosAddress || !sp1HeliosAbi) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:generateHeliosMulticallData:${l2ChainId}`, + message: `SP1Helios contract missing for L2 chain ${l2ChainId} during multicall generation.`, + }); + return { callData: [], crossChainMessages: [] }; + } + const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any); + const spokePoolContract = l2SpokePoolClient.spokePool; // Get contract instance from client + + for (const proof of proofsToSubmit) { + try { + const proofBytes = ethers.utils.hexlify(proof.proofData.proof); + const publicValuesBytes = ethers.utils.hexlify(proof.proofData.public_values); + + let decodedOutputs: DecodedProofOutputs; + try { + const decodedResult = ethers.utils.defaultAbiCoder.decode([proofOutputsAbiTuple], publicValuesBytes)[0]; + decodedOutputs = { + executionStateRoot: decodedResult[0], + newHeader: decodedResult[1], + nextSyncCommitteeHash: decodedResult[2], + newHead: decodedResult[3], + prevHeader: decodedResult[4], + prevHead: decodedResult[5], + syncCommitteeHash: decodedResult[6], + startSyncCommitteeHash: decodedResult[7], + slots: decodedResult[8].map((slot: any[]) => ({ key: slot[0], value: slot[1], contractAddress: slot[2] })), + }; + } catch (decodeError) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:decodePublicValues:${l2ChainId}`, + message: `Failed to decode public_values for nonce ${proof.sourceNonce.toString()}`, + publicValues: publicValuesBytes, + error: decodeError, + }); + continue; + } + + // 1. SP1Helios.update transaction + const updateTx = await sp1HeliosContract.populateTransaction.update(proofBytes, publicValuesBytes); + multiCallData.push({ target: sp1HeliosAddress, callData: updateTx.data! }); + + // 2. SpokePool.executeMessage transaction + const executeTx = await spokePoolContract.populateTransaction.executeMessage( + proof.sourceNonce, + proof.sourceMessageData, + decodedOutputs.newHead + ); + multiCallData.push({ target: spokePoolContract.address, callData: executeTx.data! }); + + // 3. CrossChainMessage log entry + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:generateMulticallItem:${l2ChainId}`, // Renamed log point + message: `Failed to populate transaction for proof of nonce ${proof.sourceNonce.toString()}`, + error, + proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, // Log less data + }); + continue; + } + } + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:generateHeliosMulticallData:${l2ChainId}`, + message: `Generated ${multiCallData.length} calls (${multiCallData.length / 2} finalizations) for multicall.`, + proofNoncesFinalized: proofsToSubmit.map((p) => p.sourceNonce.toString()), + }); + + return { callData: multiCallData, crossChainMessages: crossChainMessages }; +} + +// ================================== +// Lower-Level Helper Functions +// ================================== + +/** + * Queries StoredCallData events from the HubPoolStore contract on L1. + */ +async function getL1StoredCallDataEvents( + logger: winston.Logger, + hubPoolClient: HubPoolClient, + l1SpokePoolClient: SpokePoolClient, // Used for block range + l1ChainId: number, + l2ChainId: number // For logging context +): Promise { + const l1Provider = hubPoolClient.hubPool.provider; // Get provider from HubPoolClient + + const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; + if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, + message: `HubPoolStore contract address or ABI not found for L1 chain ${l1ChainId}.`, + }); + return null; + } + + const hubPoolStoreContract = new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, l1Provider); + + const l1SearchConfig: EventSearchConfig = { + fromBlock: l1SpokePoolClient.eventSearchConfig.fromBlock, + toBlock: l1SpokePoolClient.latestBlockSearched, + maxBlockLookBack: l1SpokePoolClient.eventSearchConfig.maxBlockLookBack, + }; + + const storedCallDataFilter = hubPoolStoreContract.filters.StoredCallData(); + + try { + logger.debug({ + at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, + message: `Querying StoredCallData events on L1 (${l1ChainId})`, + hubPoolStoreAddress: hubPoolStoreInfo.address, + fromBlock: l1SearchConfig.fromBlock, + toBlock: l1SearchConfig.toBlock, + }); + + const rawLogs = await paginatedEventQuery(hubPoolStoreContract, storedCallDataFilter, l1SearchConfig); + + // Explicitly cast logs to the correct type + const events: StoredCallDataEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as StoredCallDataEventArgs, // todo: is this type correct? + })); + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, + message: `Found ${events.length} StoredCallData events on L1 (${l1ChainId})`, + }); + return events; + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, + message: `Failed to query StoredCallData events from L1 (${l1ChainId})`, + hubPoolStoreAddress: hubPoolStoreInfo.address, + error, + }); + return null; // Return null on error + } +} + +/** + * Queries StorageSlotVerified events from the SP1Helios contract on L2. + */ +async function getL2StorageVerifiedEvents( + logger: winston.Logger, + l2SpokePoolClient: SpokePoolClient, + l2ChainId: number +): Promise { + const l2Provider = l2SpokePoolClient.spokePool.provider; // Get provider from L2 client + + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); + if (!sp1HeliosAddress || !sp1HeliosAbi) { + logger.warn({ + at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, + message: `SP1Helios contract not found for destination chain ${l2ChainId}. Cannot verify Helios messages.`, + }); + return []; // Return empty array if contract not found, allows finalizer to potentially process other types + } + const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); + + const l2SearchConfig: EventSearchConfig = { + fromBlock: l2SpokePoolClient.eventSearchConfig.fromBlock, + toBlock: l2SpokePoolClient.latestBlockSearched, + maxBlockLookBack: l2SpokePoolClient.eventSearchConfig.maxBlockLookBack, + }; + const storageVerifiedFilter = sp1HeliosContract.filters.StorageSlotVerified(); + + try { + logger.debug({ + at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, + message: `Querying StorageSlotVerified events on L2 (${l2ChainId})`, + sp1HeliosAddress, + fromBlock: l2SearchConfig.fromBlock, + toBlock: l2SearchConfig.toBlock, + }); + + const rawLogs = await paginatedEventQuery(sp1HeliosContract, storageVerifiedFilter, l2SearchConfig); + + // Explicitly cast logs to the correct type + const events: StorageSlotVerifiedEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as StorageSlotVerifiedEventArgs, // todo: is this a correct type? + })); + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, + message: `Found ${events.length} StorageSlotVerified events on L2 (${l2ChainId})`, + }); + return events; + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, + message: `Failed to query StorageSlotVerified events from L2 (${l2ChainId})`, + sp1HeliosAddress, + error, + }); + return null; // Return null on error + } +} + +/** + * Calculates the storage slot in the HubPoolStore contract for a given nonce. + * This assumes the data is stored in a mapping at slot 0, keyed by nonce. + * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) + */ +// ! todo: check this!!! Try with 0xdD6Fa55b12aA2a937BA053d610D76f20cC235c09 + slot 0 and then getStorageAt() +function calculateHubPoolStoreStorageSlot(eventArgs: StoredCallDataEventArgs): string { + const nonce = eventArgs.nonce; + const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 + + // Ensure nonce and slot position are correctly padded to 32 bytes (64 hex chars + 0x prefix) + const paddedNonce = ethers.utils.hexZeroPad(nonce.toHexString(), 32); + const paddedSlot = ethers.utils.hexZeroPad(ethers.BigNumber.from(mappingSlotPosition).toHexString(), 32); + + // Concatenate the padded key (nonce) and slot position + // ethers.utils.concat expects Uint8Array or hex string inputs + const concatenated = ethers.utils.concat([paddedNonce, paddedSlot]); + + // Calculate the Keccak256 hash + const storageSlot = ethers.utils.keccak256(concatenated); + + return storageSlot; +} + +/** + * Processes unfinalized messages by interacting with the ZK Proof API. + * Returns a list of successfully retrieved proofs. + */ +async function processUnfinalizedHeliosMessages( + logger: winston.Logger, + unfinalizedMessages: StoredCallDataEvent[], + l2SpokePoolClient: SpokePoolClient, + l1ChainId: number +): Promise { + const l2ChainId = l2SpokePoolClient.chainId; + const l2Provider = l2SpokePoolClient.spokePool.provider; + const apiBaseUrl = process.env.HELIOS_PROOF_API_URL; + + if (!apiBaseUrl) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, + message: "HELIOS_PROOF_API_URL environment variable not set. Cannot process Helios messages.", + }); + return []; + } + + const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; + if (!hubPoolStoreInfo?.address) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, + message: `HubPoolStore contract address not found for L1 chain ${l1ChainId}.`, + }); + return []; + } + const hubPoolStoreAddress = hubPoolStoreInfo.address; + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); + if (!sp1HeliosAddress || !sp1HeliosAbi) { + logger.warn({ + // Warn because maybe other finalizers can run + at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, + message: `SP1Helios contract not found for L2 chain ${l2ChainId}. Cannot get head/header.`, + }); + return []; + } + const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); + + let currentHead: number; + let currentHeader: string; + try { + const headBn: ethers.BigNumber = await sp1HeliosContract.head(); + currentHead = headBn.toNumber(); // Convert BigNumber head to number + currentHeader = await sp1HeliosContract.headers(headBn); + if (!currentHeader || currentHeader === ethers.constants.HashZero) { + throw new Error(`Invalid header found for head ${currentHead}`); + } + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, + message: `Using SP1Helios head ${currentHead} and header ${currentHeader} for proof requests.`, + sp1HeliosAddress, + }); + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, + message: `Failed to read current head/header from SP1Helios contract ${sp1HeliosAddress}`, + error, + }); + return []; + } + + const successfulProofs: SuccessfulProof[] = []; + + // Process messages one by one for now, can optimize with Promise.all later + for (const message of unfinalizedMessages) { + const logContext = { + at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, + l1TxHash: message.transactionHash, + nonce: message.args.nonce.toString(), + target: message.args.target, + }; + + try { + // Use the CORRECT function to get the storage slot needed for the proof API + const storageSlot = calculateHubPoolStoreStorageSlot(message.args); + + const apiRequest: ApiProofRequest = { + src_chain_contract_address: hubPoolStoreAddress, + src_chain_storage_slot: storageSlot, + src_chain_block_number: message.blockNumber, + dst_chain_contract_from_head: currentHead, + dst_chain_contract_from_header: currentHeader, + }; + + const proofId = calculateProofId(apiRequest); + const getProofUrl = `${apiBaseUrl}/api/proofs/${proofId}`; + + logger.debug({ ...logContext, message: "Attempting to get proof", proofId, getProofUrl, storageSlot }); + + let proofState: ProofStateResponse | null = null; + let getError: any = null; + + try { + const response = await axios.get(getProofUrl); + proofState = response.data; + logger.debug({ ...logContext, message: "Proof state received", proofId, status: proofState.status }); + } catch (error: any) { + getError = error; + } + + // --- API Interaction Flow --- + + // 1. Try to get proof + if (getError && axios.isAxiosError(getError) && getError.response?.status === 404) { + // 1a. NOTFOUND -> Request proof + logger.info({ ...logContext, message: "Proof not found (404), requesting...", proofId }); + try { + const requestProofUrl = `${apiBaseUrl}/api/proofs`; + await axios.post(requestProofUrl, apiRequest); + logger.info({ ...logContext, message: "Proof requested successfully.", proofId }); + // Exit flow for this message, will check again next run + } catch (postError: any) { + logger.error({ + ...logContext, + message: "Failed to request proof after 404.", + proofId, + postUrl: `${apiBaseUrl}/api/proofs`, + postError: postError.message, + postResponseData: postError.response?.data, + }); + // Exit flow for this message + } + } else if (getError) { + // Other error during GET + logger.error({ + ...logContext, + message: "Failed to get proof state.", + proofId, + getUrl: getProofUrl, + getError: getError.message, + getResponseData: getError.response?.data, + }); + // Exit flow for this message + } else if (proofState) { + // GET successful, check status + if (proofState.status === "pending") { + // 1b. SUCCESS ("pending") -> Log and exit flow + logger.info({ ...logContext, message: "Proof generation is pending.", proofId }); + // Exit flow for this message + } else if (proofState.status === "errored") { + // 1c. SUCCESS ("errored") -> Log high severity, request again, exit flow + logger.error({ + // Use error level log + ...logContext, + message: "Proof generation errored. Requesting again.", + proofId, + errorMessage: proofState.error_message, + }); + try { + const requestProofUrl = `${apiBaseUrl}/api/proofs`; + await axios.post(requestProofUrl, apiRequest); + logger.info({ ...logContext, message: "Errored proof requested again successfully.", proofId }); + } catch (postError: any) { + logger.error({ + ...logContext, + message: "Failed to re-request errored proof.", + proofId, + postUrl: `${apiBaseUrl}/api/proofs`, + postError: postError.message, + postResponseData: postError.response?.data, + }); + } + // Exit flow for this message + } else if (proofState.status === "success") { + // 1d. SUCCESS ("success") -> Collect proof data for later processing + if (proofState.update_calldata) { + logger.info({ ...logContext, message: "Proof successfully retrieved.", proofId }); + successfulProofs.push({ + proofData: proofState.update_calldata, + sourceNonce: message.args.nonce, + target: message.args.target, + sourceMessageData: message.args.data, + }); + } else { + logger.error({ + ...logContext, + message: "Proof status is success but update_calldata is missing.", + proofId, + proofState, + }); + // Treat as error, exit flow for this message + } + } else { + logger.error({ + ...logContext, + message: "Received unexpected proof status.", + proofId, + status: proofState.status, + }); + // Exit flow for this message + } + } + // Implicitly exits flow for the message if none of the success conditions were met + } catch (processingError) { + logger.error({ + ...logContext, + message: "Error processing unfinalized message for proof.", + error: processingError, + }); + } + } // end loop over messages + + return successfulProofs; +} + +/** + * Calculates the deterministic Proof ID based on the request parameters. + * Matches the Rust implementation using RLP encoding and Keccak256. + */ +/* +! todo: check this against Rust IDs. Ensure RLP encoding matches exactly. +! todo: theoretically, we could always just call request_proof first and get proof id that way. Maybe that's better flow actually. Hmmm no that won't deal with proof errors because of how API behaves:( +{ + "src_chain_block_number": 22250284, + "src_chain_contract_address": "0xdD6Fa55b12aA2a937BA053d610D76f20cC235c09", + "src_chain_storage_slot": "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", + "dst_chain_contract_from_head": 11467072, + "dst_chain_contract_from_header": "0xd74e51183c05867b80c091d5beab2e8be3fd88a642ce6fd36907e23fb09b2b13" +} +should match `9e08efd841455439cd6ca9548dacc1c77348ab19cab804ff6493a24d38195b2c` +*/ +function calculateProofId(request: ApiProofRequest): string { + let encoded = ethers.utils.RLP.encode([ + request.src_chain_contract_address, + request.src_chain_storage_slot, + ethers.BigNumber.from(request.src_chain_block_number).toHexString(), // Ensure block number is hex encoded for RLP + ethers.BigNumber.from(request.dst_chain_contract_from_head).toHexString(), // Ensure head is hex encoded for RLP + request.dst_chain_contract_from_header, + ]); + return ethers.utils.keccak256(encoded); +} diff --git a/src/finalizer/utils/index.ts b/src/finalizer/utils/index.ts index 4e93f4533a..16a6939188 100644 --- a/src/finalizer/utils/index.ts +++ b/src/finalizer/utils/index.ts @@ -5,3 +5,4 @@ export * from "./zkSync"; export * from "./scroll"; export * from "./cctp"; export * from "./linea"; +export * from "./helios"; diff --git a/src/utils/Sp1HeliosUtils.ts b/src/utils/Sp1HeliosUtils.ts new file mode 100644 index 0000000000..8b63b0f601 --- /dev/null +++ b/src/utils/Sp1HeliosUtils.ts @@ -0,0 +1,5 @@ +import { CONTRACT_ADDRESSES } from "../common"; + +export function getSp1Helios(dstChainId: number): { address?: string; abi?: unknown[] } { + return CONTRACT_ADDRESSES[dstChainId].sp1Helios; +} diff --git a/yarn.lock b/yarn.lock index 41453d6c59..b73626111b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,10 +21,10 @@ resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.51.tgz#992beaba4c7540f62bfcde180403494ea5ac989a" integrity sha512-XuXZXqbTxt/r+WQKwyMKnlg7z63SdN7AehdD1uWzF+FCM6u9XAECB8RlK1+wBj1RAJuuZGbrNMyfS9szq+X11Q== -"@across-protocol/constants@^3.1.52": - version "3.1.52" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.52.tgz#e58914d7b0787b86db0b9be3330afb3db50cfbad" - integrity sha512-MFghLk+te8WNfG+gBb5813aNcVujcCaNCM5gY8nBcHy02XjoK04UKn8CibzccQ1gWTvJTMmFgzBXv89kEW20Kw== +"@across-protocol/constants@^3.1.54": + version "3.1.54" + resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.54.tgz#e15a4a1c906b8c8b96320ce76c356b7ff5357e39" + integrity sha512-W8HjikhxoXhKZ2Mp+WcPTMwHp4nUv7AaOGawQV329hnmkYAz9tIxwrscuaWLtylNDCMLn+xY10mBiIAaPCtx9Q== "@across-protocol/contracts@^0.1.4": version "0.1.4" From 6dd6e53287ea4256c90f4c92369c3d195c7dab5f Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 30 Apr 2025 11:13:29 -0700 Subject: [PATCH 02/31] checkpoint Signed-off-by: Ihor Farion --- src/clients/TransactionClient.ts | 30 +- src/common/ClientHelper.ts | 34 +- src/common/Constants.ts | 1 + src/common/ContractAddresses.ts | 10 +- src/common/abi/Universal_SpokePool.json | 1135 +++++++++++++++++++++++ src/finalizer/index.ts | 8 +- src/finalizer/utils/helios.ts | 138 ++- src/utils/TransactionUtils.ts | 26 + 8 files changed, 1345 insertions(+), 37 deletions(-) create mode 100644 src/common/abi/Universal_SpokePool.json diff --git a/src/clients/TransactionClient.ts b/src/clients/TransactionClient.ts index 84a1e092de..f1929fe7a9 100644 --- a/src/clients/TransactionClient.ts +++ b/src/clients/TransactionClient.ts @@ -52,7 +52,35 @@ export class TransactionClient { // Each transaction is simulated in isolation; but on-chain execution may produce different // results due to execution sequence or intermediate changes in on-chain state. simulate(txns: AugmentedTransaction[]): Promise { - return Promise.all(txns.map((txn: AugmentedTransaction) => this._simulate(txn))); + return Promise.all( + txns.map(async (txn: AugmentedTransaction) => { + this.logger.info({ + at: "TransactionClient#simulate", + message: "Simulating transaction...", + chainId: txn.chainId, + contract: txn.contract.address, + method: txn.method, + args: JSON.stringify(txn.args), + value: txn.value?.toString(), + canFail: txn.canFailInSimulation, + }); + + const result = await this._simulate(txn); + + this.logger.info({ + at: "TransactionClient#simulate", + message: "Simulation result", + chainId: txn.chainId, + contract: txn.contract.address, + method: txn.method, + succeed: result.succeed, + reason: result.reason, + gasLimit: result.transaction.gasLimit?.toString(), + }); + + return result; + }) + ); } protected _submit(txn: AugmentedTransaction, nonce: number | null = null): Promise { diff --git a/src/common/ClientHelper.ts b/src/common/ClientHelper.ts index a6ff304662..e19adc7a97 100644 --- a/src/common/ClientHelper.ts +++ b/src/common/ClientHelper.ts @@ -19,6 +19,7 @@ import { CommonConfig } from "./Config"; import { SpokePoolClientsByChain } from "../interfaces"; import { caching, clients, utils as sdkUtils } from "@across-protocol/sdk"; import V3_SPOKE_POOL_ABI from "./abi/V3SpokePool.json"; +import UNIVERSAL_SPOKE_ABI from "./abi/Universal_SpokePool.json"; export interface Clients { hubPoolClient: HubPoolClient; @@ -204,15 +205,30 @@ export async function constructSpokePoolClientsWithStartBlocks( const spokePoolSigners = await getSpokePoolSigners(baseSigner, enabledChains); const spokePools = await Promise.all( enabledChains.map(async (chainId) => { - const spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]); - // TODO: initialize using typechain factory after V3.5 migration. - // const spokePoolContract = SpokePool.connect(spokePoolAddr, spokePoolSigners[chainId]); - const spokePoolContract = new ethers.Contract( - spokePoolAddr, - [...SpokePool.abi, ...V3_SPOKE_POOL_ABI], - spokePoolSigners[chainId] - ); - const registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]); + let spokePoolAddr: string; + let spokePoolContract: ethers.Contract; + let registrationBlock: number; + + if (chainId === 56) { + // ---- START BSC TEST CODE ---- + spokePoolAddr = "0x4e8E101924eDE233C13e2D8622DC8aED2872d505"; // Hardcoded BSC SpokePool address + registrationBlock = 48762336; // Hardcoded BSC registration block + // Use UniversalSpokePool ABI for BSC + spokePoolContract = new ethers.Contract(spokePoolAddr, [...UNIVERSAL_SPOKE_ABI], spokePoolSigners[chainId]); + // ---- END BSC TEST CODE ---- + } else { + // --- Logic for all other chains --- + spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]); + // TODO: initialize using typechain factory after V3.5 migration. + // const spokePoolContract = SpokePool.connect(spokePoolAddr, spokePoolSigners[chainId]); + spokePoolContract = new ethers.Contract( + spokePoolAddr, + [...SpokePool.abi, ...V3_SPOKE_POOL_ABI], + spokePoolSigners[chainId] + ); + registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]); + } + return { chainId, contract: spokePoolContract, registrationBlock }; }) ); diff --git a/src/common/Constants.ts b/src/common/Constants.ts index fbd5c2f440..595a65d66b 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -218,6 +218,7 @@ export const CHAIN_CACHE_FOLLOW_DISTANCE: { [chainId: number]: number } = { [CHAIN_IDs.BASE]: 120, [CHAIN_IDs.BLAST]: 120, [CHAIN_IDs.BOBA]: 0, + [CHAIN_IDs.BNB]: 3, // todo: idk what i'm doing. internet says BSC has 3-block finality [CHAIN_IDs.UNICHAIN]: 120, [CHAIN_IDs.INK]: 120, // Follows Optimism [CHAIN_IDs.LENS]: 512, diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 7d004634f2..1bde1321ff 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -202,7 +202,10 @@ export const CONTRACT_ADDRESSES: { abi: HUB_POOL_ABI, }, hubPoolStore: { - address: "0x1Ace3BbD69b63063F859514Eca29C9BDd8310E61", + // ---- START BSC TEST CODE ---- + // ! todo: mock hubPool controlled by dev wallet + address: "0xdD6Fa55b12aA2a937BA053d610D76f20cC235c09", + // ---- END BSC TEST CODE ---- abi: HUB_POOL_STORE_ABI, }, blastBridge: { @@ -257,7 +260,10 @@ export const CONTRACT_ADDRESSES: { }, [CHAIN_IDs.BNB]: { sp1Helios: { - address: "0x6999526e507Cc3b03b180BbE05E1Ff938259A874", // todo: change for prod. This uses a mock proof verifier + // ---- START BSC TEST CODE ---- + // ! todo: this uses a mock proof verifier + address: "0x6999526e507Cc3b03b180BbE05E1Ff938259A874", + // ---- END BSC TEST CODE ---- abi: SP1_HELIOS_ABI, }, }, diff --git a/src/common/abi/Universal_SpokePool.json b/src/common/abi/Universal_SpokePool.json new file mode 100644 index 0000000000..14076301b9 --- /dev/null +++ b/src/common/abi/Universal_SpokePool.json @@ -0,0 +1,1135 @@ +[ + { + "inputs": [ + { "internalType": "uint256", "name": "_adminUpdateBufferSeconds", "type": "uint256" }, + { "internalType": "address", "name": "_helios", "type": "address" }, + { "internalType": "address", "name": "_hubPoolStore", "type": "address" }, + { "internalType": "address", "name": "_wrappedNativeTokenAddress", "type": "address" }, + { "internalType": "uint32", "name": "_depositQuoteTimeBuffer", "type": "uint32" }, + { "internalType": "uint32", "name": "_fillDeadlineBuffer", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "_l2Usdc", "type": "address" }, + { "internalType": "contract ITokenMessenger", "name": "_cctpTokenMessenger", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "inputs": [], "name": "AdminCallAlreadySet", "type": "error" }, + { "inputs": [], "name": "AdminCallNotValidated", "type": "error" }, + { "inputs": [], "name": "AdminUpdateTooCloseToLastHeliosUpdate", "type": "error" }, + { "inputs": [], "name": "AlreadyExecuted", "type": "error" }, + { "inputs": [], "name": "ClaimedMerkleLeaf", "type": "error" }, + { "inputs": [], "name": "DelegateCallFailed", "type": "error" }, + { "inputs": [], "name": "DepositsArePaused", "type": "error" }, + { "inputs": [], "name": "DisabledRoute", "type": "error" }, + { "inputs": [], "name": "ExpiredFillDeadline", "type": "error" }, + { "inputs": [], "name": "FillsArePaused", "type": "error" }, + { "inputs": [], "name": "InsufficientSpokePoolBalanceToExecuteLeaf", "type": "error" }, + { "inputs": [], "name": "InvalidBytes32", "type": "error" }, + { "inputs": [], "name": "InvalidChainId", "type": "error" }, + { "inputs": [], "name": "InvalidCrossDomainAdmin", "type": "error" }, + { "inputs": [], "name": "InvalidDepositorSignature", "type": "error" }, + { "inputs": [], "name": "InvalidExclusiveRelayer", "type": "error" }, + { "inputs": [], "name": "InvalidFillDeadline", "type": "error" }, + { "inputs": [], "name": "InvalidMerkleLeaf", "type": "error" }, + { "inputs": [], "name": "InvalidMerkleProof", "type": "error" }, + { "inputs": [], "name": "InvalidPayoutAdjustmentPct", "type": "error" }, + { "inputs": [], "name": "InvalidQuoteTimestamp", "type": "error" }, + { "inputs": [], "name": "InvalidRelayerFeePct", "type": "error" }, + { "inputs": [], "name": "InvalidSlowFillRequest", "type": "error" }, + { "inputs": [], "name": "InvalidWithdrawalRecipient", "type": "error" }, + { + "inputs": [{ "internalType": "bytes", "name": "data", "type": "bytes" }], + "name": "LowLevelCallFailed", + "type": "error" + }, + { "inputs": [], "name": "MaxTransferSizeExceeded", "type": "error" }, + { "inputs": [], "name": "MsgValueDoesNotMatchInputAmount", "type": "error" }, + { "inputs": [], "name": "NoRelayerRefundToClaim", "type": "error" }, + { "inputs": [], "name": "NoSlowFillsInExclusivityWindow", "type": "error" }, + { "inputs": [], "name": "NotEOA", "type": "error" }, + { "inputs": [], "name": "NotExclusiveRelayer", "type": "error" }, + { "inputs": [], "name": "NotImplemented", "type": "error" }, + { "inputs": [], "name": "NotTarget", "type": "error" }, + { "inputs": [], "name": "RelayFilled", "type": "error" }, + { "inputs": [], "name": "SlotValueMismatch", "type": "error" }, + { "inputs": [], "name": "WrongERC7683OrderId", "type": "error" }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "previousAdmin", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "beacon", "type": "address" }], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "l2TokenAddress", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "refundAddress", "type": "bytes32" }, + { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "indexed": true, "internalType": "address", "name": "caller", "type": "address" } + ], + "name": "ClaimedRelayerRefund", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "uint256", "name": "rootBundleId", "type": "uint256" }], + "name": "EmergencyDeletedRootBundle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "originToken", "type": "address" }, + { "indexed": true, "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "indexed": false, "internalType": "bool", "name": "enabled", "type": "bool" } + ], + "name": "EnabledDepositRoute", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "uint256", "name": "amountToReturn", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "chainId", "type": "uint256" }, + { "indexed": false, "internalType": "uint256[]", "name": "refundAmounts", "type": "uint256[]" }, + { "indexed": true, "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, + { "indexed": true, "internalType": "uint32", "name": "leafId", "type": "uint32" }, + { "indexed": false, "internalType": "address", "name": "l2TokenAddress", "type": "address" }, + { "indexed": false, "internalType": "address[]", "name": "refundAddresses", "type": "address[]" }, + { "indexed": false, "internalType": "bool", "name": "deferredRefunds", "type": "bool" }, + { "indexed": false, "internalType": "address", "name": "caller", "type": "address" } + ], + "name": "ExecutedRelayerRefundRoot", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "relayer", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "messageHash", "type": "bytes32" }, + { + "components": [ + { "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "updatedMessageHash", "type": "bytes32" }, + { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, + { "internalType": "enum V3SpokePoolInterface.FillType", "name": "fillType", "type": "uint8" } + ], + "indexed": false, + "internalType": "struct V3SpokePoolInterface.V3RelayExecutionEventInfo", + "name": "relayExecutionInfo", + "type": "tuple" + } + ], + "name": "FilledRelay", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "relayer", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "depositor", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, + { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" }, + { + "components": [ + { "internalType": "address", "name": "updatedRecipient", "type": "address" }, + { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, + { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, + { "internalType": "enum V3SpokePoolInterface.FillType", "name": "fillType", "type": "uint8" } + ], + "indexed": false, + "internalType": "struct V3SpokePoolInterface.LegacyV3RelayExecutionEventInfo", + "name": "relayExecutionInfo", + "type": "tuple" + } + ], + "name": "FilledV3Relay", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "indexed": false, "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "indexed": true, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "FundsDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "uint8", "name": "version", "type": "uint8" }], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "bool", "name": "isPaused", "type": "bool" }], + "name": "PausedDeposits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": false, "internalType": "bool", "name": "isPaused", "type": "bool" }], + "name": "PausedFills", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "indexed": false, "internalType": "address", "name": "caller", "type": "address" } + ], + "name": "RelayedCallData", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, + { "indexed": true, "internalType": "bytes32", "name": "relayerRefundRoot", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "slowRelayRoot", "type": "bytes32" } + ], + "name": "RelayedRootBundle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "messageHash", "type": "bytes32" } + ], + "name": "RequestedSlowFill", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "indexed": true, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, + { "indexed": false, "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } + ], + "name": "RequestedSpeedUpDeposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, + { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, + { "indexed": true, "internalType": "address", "name": "depositor", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "updatedRecipient", "type": "address" }, + { "indexed": false, "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, + { "indexed": false, "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } + ], + "name": "RequestedSpeedUpV3Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "depositor", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, + { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "RequestedV3SlowFill", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "newWithdrawalRecipient", "type": "address" }], + "name": "SetWithdrawalRecipient", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "newAdmin", "type": "address" }], + "name": "SetXDomainAdmin", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "uint256", "name": "amountToReturn", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "chainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint32", "name": "leafId", "type": "uint32" }, + { "indexed": true, "internalType": "bytes32", "name": "l2TokenAddress", "type": "bytes32" }, + { "indexed": false, "internalType": "address", "name": "caller", "type": "address" } + ], + "name": "TokensBridged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "implementation", "type": "address" }], + "name": "Upgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "indexed": true, "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "indexed": true, "internalType": "address", "name": "depositor", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, + { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "V3FundsDeposited", + "type": "event" + }, + { + "inputs": [], + "name": "ADMIN_UPDATE_BUFFER", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "DEPRECATED_enabledDepositRoutes", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EMPTY_RELAYER", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EMPTY_REPAYMENT_CHAIN_ID", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "HUB_POOL_STORE_CALLDATA_MAPPING_SLOT_INDEX", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "INFINITE_FILL_DEADLINE", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_EXCLUSIVITY_PERIOD_SECONDS", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_TRANSFER_SIZE", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UPDATE_BYTES32_DEPOSIT_DETAILS_HASH", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint32", "name": "_initialDepositId", "type": "uint32" }, + { "internalType": "address", "name": "_crossDomainAdmin", "type": "address" }, + { "internalType": "address", "name": "_withdrawalRecipient", "type": "address" } + ], + "name": "__SpokePool_init", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes", "name": "_message", "type": "bytes" }], + "name": "adminExecuteMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "cctpTokenMessenger", + "outputs": [{ "internalType": "contract ITokenMessenger", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cctpV2", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "chainId", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "l2TokenAddress", "type": "bytes32" }, + { "internalType": "bytes32", "name": "refundAddress", "type": "bytes32" } + ], + "name": "claimRelayerRefund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "crossDomainAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "address", "name": "originToken", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "internalType": "int64", "name": "relayerFeePct", "type": "int64" }, + { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "depositDeprecated_5947912356", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "depositor", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "address", "name": "originToken", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "internalType": "int64", "name": "relayerFeePct", "type": "int64" }, + { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "depositFor", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "uint32", "name": "fillDeadlineOffset", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "depositNow", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "depositQuoteTimeBuffer", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "depositor", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "address", "name": "inputToken", "type": "address" }, + { "internalType": "address", "name": "outputToken", "type": "address" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, + { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "depositV3", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "depositor", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "address", "name": "inputToken", "type": "address" }, + { "internalType": "address", "name": "outputToken", "type": "address" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, + { "internalType": "uint32", "name": "fillDeadlineOffset", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "depositV3Now", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "rootBundleId", "type": "uint256" }], + "name": "emergencyDeleteRootBundle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "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" + }, + { + "inputs": [ + { "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, + { + "components": [ + { "internalType": "uint256", "name": "amountToReturn", "type": "uint256" }, + { "internalType": "uint256", "name": "chainId", "type": "uint256" }, + { "internalType": "uint256[]", "name": "refundAmounts", "type": "uint256[]" }, + { "internalType": "uint32", "name": "leafId", "type": "uint32" }, + { "internalType": "address", "name": "l2TokenAddress", "type": "address" }, + { "internalType": "address[]", "name": "refundAddresses", "type": "address[]" } + ], + "internalType": "struct SpokePoolInterface.RelayerRefundLeaf", + "name": "relayerRefundLeaf", + "type": "tuple" + }, + { "internalType": "bytes32[]", "name": "proof", "type": "bytes32[]" } + ], + "name": "executeRelayerRefundLeaf", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "internalType": "struct V3SpokePoolInterface.V3RelayData", + "name": "relayData", + "type": "tuple" + }, + { "internalType": "uint256", "name": "chainId", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" } + ], + "internalType": "struct V3SpokePoolInterface.V3SlowFill", + "name": "slowFillLeaf", + "type": "tuple" + }, + { "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, + { "internalType": "bytes32[]", "name": "proof", "type": "bytes32[]" } + ], + "name": "executeSlowRelayLeaf", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "executedMessages", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "orderId", "type": "bytes32" }, + { "internalType": "bytes", "name": "originData", "type": "bytes" }, + { "internalType": "bytes", "name": "fillerData", "type": "bytes" } + ], + "name": "fill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "fillDeadlineBuffer", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "internalType": "struct V3SpokePoolInterface.V3RelayData", + "name": "relayData", + "type": "tuple" + }, + { "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, + { "internalType": "bytes32", "name": "repaymentAddress", "type": "bytes32" } + ], + "name": "fillRelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "internalType": "struct V3SpokePoolInterface.V3RelayData", + "name": "relayData", + "type": "tuple" + }, + { "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, + { "internalType": "bytes32", "name": "repaymentAddress", "type": "bytes32" }, + { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, + { "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, + { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, + { "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } + ], + "name": "fillRelayWithUpdatedDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "name": "fillStatuses", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "depositor", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, + { "internalType": "address", "name": "inputToken", "type": "address" }, + { "internalType": "address", "name": "outputToken", "type": "address" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "internalType": "uint32", "name": "depositId", "type": "uint32" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "internalType": "struct V3SpokePoolInterface.V3RelayDataLegacy", + "name": "relayData", + "type": "tuple" + }, + { "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" } + ], + "name": "fillV3Relay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentTime", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "l2TokenAddress", "type": "address" }, + { "internalType": "address", "name": "refundAddress", "type": "address" } + ], + "name": "getRelayerRefund", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "_nonce", "type": "uint256" }], + "name": "getSlotKey", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "msgSender", "type": "address" }, + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "uint256", "name": "depositNonce", "type": "uint256" } + ], + "name": "getUnsafeDepositId", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "internalType": "struct V3SpokePoolInterface.V3RelayData", + "name": "relayData", + "type": "tuple" + } + ], + "name": "getV3RelayHash", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "helios", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hubPoolStore", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint32", "name": "_initialDepositId", "type": "uint32" }, + { "internalType": "address", "name": "_crossDomainAdmin", "type": "address" }, + { "internalType": "address", "name": "_withdrawalRecipient", "type": "address" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes[]", "name": "data", "type": "bytes[]" }], + "name": "multicall", + "outputs": [{ "internalType": "bytes[]", "name": "results", "type": "bytes[]" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "numberOfDeposits", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bool", "name": "pause", "type": "bool" }], + "name": "pauseDeposits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bool", "name": "pause", "type": "bool" }], + "name": "pauseFills", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "pausedDeposits", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pausedFills", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "recipientCircleDomainId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "relayerRefundRoot", "type": "bytes32" }, + { "internalType": "bytes32", "name": "slowRelayRoot", "type": "bytes32" } + ], + "name": "relayRootBundle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "relayerRefund", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, + { "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "internalType": "struct V3SpokePoolInterface.V3RelayData", + "name": "relayData", + "type": "tuple" + } + ], + "name": "requestSlowFill", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "name": "rootBundles", + "outputs": [ + { "internalType": "bytes32", "name": "slowRelayRoot", "type": "bytes32" }, + { "internalType": "bytes32", "name": "relayerRefundRoot", "type": "bytes32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newCrossDomainAdmin", "type": "address" }], + "name": "setCrossDomainAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newWithdrawalRecipient", "type": "address" }], + "name": "setWithdrawalRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, + { "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, + { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, + { "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } + ], + "name": "speedUpDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "depositor", "type": "address" }, + { "internalType": "uint256", "name": "depositId", "type": "uint256" }, + { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, + { "internalType": "address", "name": "updatedRecipient", "type": "address" }, + { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, + { "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } + ], + "name": "speedUpV3Deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes[]", "name": "data", "type": "bytes[]" }], + "name": "tryMulticall", + "outputs": [ + { + "components": [ + { "internalType": "bool", "name": "success", "type": "bool" }, + { "internalType": "bytes", "name": "returnData", "type": "bytes" } + ], + "internalType": "struct MultiCallerUpgradeable.Result[]", + "name": "results", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, + { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, + { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, + { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, + { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, + { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, + { "internalType": "uint256", "name": "depositNonce", "type": "uint256" }, + { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, + { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, + { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, + { "internalType": "bytes", "name": "message", "type": "bytes" } + ], + "name": "unsafeDeposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "newImplementation", "type": "address" }], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newImplementation", "type": "address" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "usdcToken", + "outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawalRecipient", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "wrappedNativeToken", + "outputs": [{ "internalType": "contract WETH9Interface", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } +] diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index b8355b93cb..eae978c603 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -70,8 +70,8 @@ const chainFinalizers: { [chainId: number]: { finalizeOnL2: ChainFinalizer[]; fi finalizeOnL2: [cctpL1toL2Finalizer], }, [CHAIN_IDs.BNB]: { - finalizeOnL1: [heliosL1toL2Finalizer], - finalizeOnL2: [], + finalizeOnL1: [], + finalizeOnL2: [heliosL1toL2Finalizer], }, [CHAIN_IDs.POLYGON]: { finalizeOnL1: [polygonFinalizer, cctpL2toL1Finalizer], @@ -507,6 +507,10 @@ export class FinalizerConfig extends DataworkerConfig { export async function runFinalizer(_logger: winston.Logger, baseSigner: Signer): Promise { logger = _logger; + // ---- START BSC TEST CODE ---- + logger.level = "info"; + // ---- END BSC TEST CODE ---- + // Same config as Dataworker for now. const config = new FinalizerConfig(process.env); const profiler = new Profiler({ diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 33f57b8c9d..45028441cf 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -67,7 +67,7 @@ const proofOutputsAbiTuple = `tuple( bytes32 syncCommitteeHash, bytes32 startSyncCommitteeHash, tuple(bytes32 key, bytes32 value, address contractAddress)[] slots -+)`; +)`; // Type for the decoded ProofOutputs structure type DecodedProofOutputs = { @@ -103,6 +103,7 @@ export async function heliosL1toL2Finalizer( const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; // Get L2 SpokePool address // --- Step 1: Query and Filter L1 Events --- + // ✅ Step 1 tested and working const relevantStoredCallDataEvents = await getAndFilterL1Events( logger, hubPoolClient, @@ -111,24 +112,79 @@ export async function heliosL1toL2Finalizer( l2ChainId, l2SpokePoolAddress ); + + // ---- START BSC TEST CODE ---- + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_1:${l2ChainId}`, + message: "--- DEBUGGING STEP 1: StoredCallData Events ---", + count: relevantStoredCallDataEvents?.length ?? "null", + events: relevantStoredCallDataEvents // Log the actual events for inspection + ? relevantStoredCallDataEvents.map((e) => ({ + txHash: e.transactionHash, + blockNumber: e.blockNumber, + target: e.args.target, + nonce: e.args.nonce.toString(), + dataLength: e.args.data.length, + data: e.args.data, + })) + : "null", + }); + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_1:${l2ChainId}`, + message: "--- RETURNING EARLY AFTER STEP 1 FOR TESTING ---", + }); + // ---- END BSC TEST CODE ---- + if (!relevantStoredCallDataEvents || relevantStoredCallDataEvents.length === 0) { return { callData: [], crossChainMessages: [] }; } // --- Step 2: Query L2 Verification Events --- + // ✅ Step 2 tested and working const verifiedKeys = await getL2VerifiedKeys(logger, l2SpokePoolClient, l2ChainId); + + // ---- START BSC TEST CODE (STEP 2) ---- + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_2:${l2ChainId}`, + message: "--- DEBUGGING STEP 2: Verified Keys ---", + count: verifiedKeys?.size ?? "null (error occurred)", + keys: verifiedKeys ? [...verifiedKeys] : "null (error occurred)", // Convert Set to Array for logging + }); + // ---- END BSC TEST CODE (STEP 2) ---- + if (verifiedKeys === null) { // Indicates an error occurred fetching L2 events return { callData: [], crossChainMessages: [] }; } // --- Step 3: Identify Unfinalized Messages --- + // ✅ Step 3 tested and working const unfinalizedMessages = findUnfinalizedMessages(logger, relevantStoredCallDataEvents, verifiedKeys, l2ChainId); + // todo: Testing, uncomment above after + // const unfinalizedMessages = relevantStoredCallDataEvents; + + // ---- START BSC TEST CODE (STEP 3) ---- + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_3:${l2ChainId}`, + message: "--- DEBUGGING STEP 3: Unfinalized Messages ---", + count: unfinalizedMessages.length, + messages: unfinalizedMessages.map((m) => ({ + txHash: m.transactionHash, + blockNumber: m.blockNumber, + target: m.args.target, + nonce: m.args.nonce.toString(), + // Optionally calculate expected slot for verification + expectedSlot: calculateHubPoolStoreStorageSlot(m.args), + })), + }); + // ---- END BSC TEST CODE (STEP 3) ---- + if (unfinalizedMessages.length === 0) { return { callData: [], crossChainMessages: [] }; } // --- Step 4: Get Proofs for Unfinalized Messages --- + // ✅ Step 4. tested and working. Didn't test ALL branches; but happy-cases work const proofsToSubmit = await processUnfinalizedHeliosMessages( logger, unfinalizedMessages, // Pass the unfinalized messages containing original event data @@ -201,7 +257,7 @@ async function getAndFilterL1Events( // Explicitly cast logs to the correct type const events: StoredCallDataEvent[] = rawLogs.map((log) => ({ ...log, - args: log.args as StoredCallDataEventArgs, // todo: is this type correct? + args: log.args as StoredCallDataEventArgs, })); logger.info({ @@ -336,8 +392,11 @@ async function generateHeliosMulticallData( for (const proof of proofsToSubmit) { try { - const proofBytes = ethers.utils.hexlify(proof.proofData.proof); - const publicValuesBytes = ethers.utils.hexlify(proof.proofData.public_values); + // Ensure the hex strings have the '0x' prefix, adding it only if missing. + const proofBytes = proof.proofData.proof.startsWith("0x") ? proof.proofData.proof : "0x" + proof.proofData.proof; + const publicValuesBytes = proof.proofData.public_values.startsWith("0x") + ? proof.proofData.public_values + : "0x" + proof.proofData.public_values; let decodedOutputs: DecodedProofOutputs; try { @@ -364,24 +423,38 @@ async function generateHeliosMulticallData( } // 1. SP1Helios.update transaction - const updateTx = await sp1HeliosContract.populateTransaction.update(proofBytes, publicValuesBytes); + // todo: uncomment for prod. Mock verifier wants empty proof, so give it that + // const updateTx = await sp1HeliosContract.populateTransaction.update(proofBytes, publicValuesBytes); + const updateTx = await sp1HeliosContract.populateTransaction.update("0x", publicValuesBytes); multiCallData.push({ target: sp1HeliosAddress, callData: updateTx.data! }); + logger.error({ + message: `spokePool addr: ${spokePoolContract.address}`, + }); + // 2. SpokePool.executeMessage transaction const executeTx = await spokePoolContract.populateTransaction.executeMessage( proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead ); - multiCallData.push({ target: spokePoolContract.address, callData: executeTx.data! }); + // todo: uncomment for prod. Check that this is working + // multiCallData.push({ target: spokePoolContract.address, callData: executeTx.data! }); - // 3. CrossChainMessage log entry + // 3. 2 X CrossChainMessage log entry crossChainMessages.push({ type: "misc", miscReason: "ZK bridge finalization", originationChainId: l1ChainId, destinationChainId: l2ChainId, }); + // todo: uncomment for prod. Check that this is working + // crossChainMessages.push({ + // type: "misc", + // miscReason: "ZK bridge finalization", + // originationChainId: l1ChainId, + // destinationChainId: l2ChainId, + // }); } catch (error) { logger.error({ at: `Finalizer#heliosL1toL2Finalizer:generateMulticallItem:${l2ChainId}`, // Renamed log point @@ -535,7 +608,7 @@ async function getL2StorageVerifiedEvents( * This assumes the data is stored in a mapping at slot 0, keyed by nonce. * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) */ -// ! todo: check this!!! Try with 0xdD6Fa55b12aA2a937BA053d610D76f20cC235c09 + slot 0 and then getStorageAt() +// ✅ tested and working function calculateHubPoolStoreStorageSlot(eventArgs: StoredCallDataEventArgs): string { const nonce = eventArgs.nonce; const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 @@ -600,6 +673,8 @@ async function processUnfinalizedHeliosMessages( let currentHeader: string; try { const headBn: ethers.BigNumber = await sp1HeliosContract.head(); + // todo: well, currently we're taking currentHead to use as prevHead in our ZK proof. There's a particular scenario where we could speed up proofs + // todo: (by not making them to wait for finality longer than needed) if our blockNumber that we need a proved slot for is older than this head. currentHead = headBn.toNumber(); // Convert BigNumber head to number currentHeader = await sp1HeliosContract.headers(headBn); if (!currentHeader || currentHeader === ethers.constants.HashZero) { @@ -621,7 +696,7 @@ async function processUnfinalizedHeliosMessages( const successfulProofs: SuccessfulProof[] = []; - // Process messages one by one for now, can optimize with Promise.all later + // todo? Process messages one by one for now, can optimize with Promise.all later for (const message of unfinalizedMessages) { const logContext = { at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, @@ -630,8 +705,36 @@ async function processUnfinalizedHeliosMessages( target: message.args.target, }; + // const storageSlot = calculateHubPoolStoreStorageSlot(message.args); + + // logger.info({ + // ...logContext, + // message: `Calculated this storage slot for stored message with nonce ${message.args.nonce} : ${storageSlot}`, + // }); + + // const apiRequest: ApiProofRequest = { + // src_chain_contract_address: hubPoolStoreAddress, + // src_chain_storage_slot: storageSlot, + // src_chain_block_number: message.blockNumber, + // dst_chain_contract_from_head: currentHead, + // dst_chain_contract_from_header: currentHeader, + // }; + + // const proofId = calculateProofId(apiRequest); + + // logger.info({ + // ...logContext, + // message: `Args: src_chain_contract_address=${apiRequest.src_chain_contract_address}, src_chain_storage_slot=${apiRequest.src_chain_storage_slot}, src_chain_block_number=${apiRequest.src_chain_block_number}, dst_chain_contract_from_head=${apiRequest.dst_chain_contract_from_head}, dst_chain_contract_from_header=${apiRequest.dst_chain_contract_from_header}, proofId=${proofId}`, + // }); + + // logger.info({ + // ...logContext, + // message: `Calculated this proofId for stored message with nonce ${message.args.nonce} : ${proofId}`, + // }); + + // continue; + try { - // Use the CORRECT function to get the storage slot needed for the proof API const storageSlot = calculateHubPoolStoreStorageSlot(message.args); const apiRequest: ApiProofRequest = { @@ -767,20 +870,9 @@ async function processUnfinalizedHeliosMessages( * Calculates the deterministic Proof ID based on the request parameters. * Matches the Rust implementation using RLP encoding and Keccak256. */ -/* -! todo: check this against Rust IDs. Ensure RLP encoding matches exactly. -! todo: theoretically, we could always just call request_proof first and get proof id that way. Maybe that's better flow actually. Hmmm no that won't deal with proof errors because of how API behaves:( -{ - "src_chain_block_number": 22250284, - "src_chain_contract_address": "0xdD6Fa55b12aA2a937BA053d610D76f20cC235c09", - "src_chain_storage_slot": "0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5", - "dst_chain_contract_from_head": 11467072, - "dst_chain_contract_from_header": "0xd74e51183c05867b80c091d5beab2e8be3fd88a642ce6fd36907e23fb09b2b13" -} -should match `9e08efd841455439cd6ca9548dacc1c77348ab19cab804ff6493a24d38195b2c` -*/ +// ✅ tested and working function calculateProofId(request: ApiProofRequest): string { - let encoded = ethers.utils.RLP.encode([ + const encoded = ethers.utils.RLP.encode([ request.src_chain_contract_address, request.src_chain_storage_slot, ethers.BigNumber.from(request.src_chain_block_number).toHexString(), // Ensure block number is hex encoded for RLP diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index 4bc1e132e0..ef2b3a8545 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -186,8 +186,17 @@ export async function getGasPrice( } export async function willSucceed(transaction: AugmentedTransaction): Promise { + console.log("\n--- willSucceed START ---"); + console.log( + `[willSucceed] Simulating: ${transaction.method} on ${transaction.contract.address} (Chain: ${transaction.chainId})` + ); + console.log(`[willSucceed] Args: ${JSON.stringify(transaction.args)}`); + console.log(`[willSucceed] Value: ${transaction.value?.toString()}`); + // If the transaction already has a gasLimit, it should have been simulated in advance. if (transaction.canFailInSimulation || isDefined(transaction.gasLimit)) { + console.log("[willSucceed] Skipping simulation: canFailInSimulation or gasLimit already defined."); + console.log("--- willSucceed END ---"); return { transaction, succeed: true }; } @@ -200,22 +209,39 @@ export async function willSucceed(transaction: AugmentedTransaction): Promise Date: Wed, 30 Apr 2025 12:50:01 -0700 Subject: [PATCH 03/31] change FinalizerPromise returned txns type from Multicall2Call to (Multicall2Call | AugmentedTransaction) to enable returning individual txns from finalizer adapters Signed-off-by: Ihor Farion --- src/finalizer/index.ts | 101 +++++++++++++++++++++++----------- src/finalizer/types.ts | 7 ++- src/finalizer/utils/helios.ts | 4 +- 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index eae978c603..8ad5854f9f 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -189,7 +189,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) => { @@ -263,7 +264,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; @@ -312,25 +313,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 ("callData" in _txn && "target" in _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, transaction: _ }] = await txnClient.simulate([_txn]); + if (succeed) { return true; } else { - return false; + simErrorReason = reason; } } @@ -345,7 +360,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; }); @@ -362,19 +377,39 @@ export async function finalize( ({ crossChainMessage }) => crossChainMessage.destinationChainId ); 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); + // Separate Multicall2Call and AugmentedTransaction objects + const multicallTxns: Multicall2Call[] = []; + + // Process each finalization separately + finalizations.forEach(({ txn }) => { + // Check if this is a Multicall2Call (doesn't have contract property) + // or an AugmentedTransaction (has contract property) + if ("contract" in txn) { + // It's an AugmentedTransaction, enqueue directly + // todo? we might want to enqueue these after the batch call + 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 we have any multicall transactions, bundle them together + 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); + } } txnHashLookup = await multicallerClient.executeTxnQueues(!submitFinalizationTransactions); } catch (_error) { diff --git a/src/finalizer/types.ts b/src/finalizer/types.ts index 781e59b472..8782c75b3f 100644 --- a/src/finalizer/types.ts +++ b/src/finalizer/types.ts @@ -1,5 +1,5 @@ import { Signer } from "ethers"; -import { HubPoolClient, SpokePoolClient } from "../clients"; +import { AugmentedTransaction, HubPoolClient, SpokePoolClient } from "../clients"; import { Multicall2Call, winston } from "../utils"; /** @@ -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 { ( diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 45028441cf..2145715f39 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -200,7 +200,7 @@ export async function heliosL1toL2Finalizer( } // --- Step 5: Generate Multicall Data from Proofs --- - return generateHeliosMulticallData( + return generateHeliosTxns( logger, proofsToSubmit, l1ChainId, @@ -369,7 +369,7 @@ function findUnfinalizedMessages( } /** STEP 5: Generate Multicall Data */ -async function generateHeliosMulticallData( +async function generateHeliosTxns( logger: winston.Logger, proofsToSubmit: SuccessfulProof[], l1ChainId: number, From dfbe2b9ebf1009d33b89b9bd14b75d35a6614b1e Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 30 Apr 2025 13:11:51 -0700 Subject: [PATCH 04/31] changes to helios.ts to submit individual txs rather than use multicall Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 89 ++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 2145715f39..837c5c0769 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -13,6 +13,8 @@ import { getSp1Helios } from "../../utils/Sp1HeliosUtils"; import { Log } from "../../interfaces"; import { CONTRACT_ADDRESSES } from "../../common"; import axios from "axios"; +// --- Structs --- +import { AugmentedTransaction } from "../../clients"; // Define interfaces for the event arguments for clarity interface StoredCallDataEventArgs { @@ -92,7 +94,7 @@ type SuccessfulProof = { export async function heliosL1toL2Finalizer( logger: winston.Logger, - _signer: Signer, + signer: Signer, hubPoolClient: HubPoolClient, l2SpokePoolClient: SpokePoolClient, // Used for filtering target address l1SpokePoolClient: SpokePoolClient, @@ -200,13 +202,7 @@ export async function heliosL1toL2Finalizer( } // --- Step 5: Generate Multicall Data from Proofs --- - return generateHeliosTxns( - logger, - proofsToSubmit, - l1ChainId, - l2ChainId, - l2SpokePoolClient // Pass the client to access spokePool contract - ); + return generateHeliosTxns(logger, proofsToSubmit, l1ChainId, l2ChainId, l2SpokePoolClient, signer); } // ================================== @@ -374,9 +370,10 @@ async function generateHeliosTxns( proofsToSubmit: SuccessfulProof[], l1ChainId: number, l2ChainId: number, - l2SpokePoolClient: SpokePoolClient + l2SpokePoolClient: SpokePoolClient, + signer: Signer ): Promise { - const multiCallData: Multicall2Call[] = []; + const transactions: AugmentedTransaction[] = []; const crossChainMessages: CrossChainMessage[] = []; const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); @@ -387,7 +384,9 @@ async function generateHeliosTxns( }); return { callData: [], crossChainMessages: [] }; } - const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any); + // Create contract instances with a signer/provider if needed for AugmentedTransaction + // Assuming l2SpokePoolClient.spokePool has a signer or provider attached + const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, signer); const spokePoolContract = l2SpokePoolClient.spokePool; // Get contract instance from client for (const proof of proofsToSubmit) { @@ -423,43 +422,55 @@ async function generateHeliosTxns( } // 1. SP1Helios.update transaction - // todo: uncomment for prod. Mock verifier wants empty proof, so give it that - // const updateTx = await sp1HeliosContract.populateTransaction.update(proofBytes, publicValuesBytes); - const updateTx = await sp1HeliosContract.populateTransaction.update("0x", publicValuesBytes); - multiCallData.push({ target: sp1HeliosAddress, callData: updateTx.data! }); - - logger.error({ - message: `spokePool addr: ${spokePoolContract.address}`, - }); - - // 2. SpokePool.executeMessage transaction - const executeTx = await spokePoolContract.populateTransaction.executeMessage( - proof.sourceNonce, - proof.sourceMessageData, - decodedOutputs.newHead - ); - // todo: uncomment for prod. Check that this is working - // multiCallData.push({ target: spokePoolContract.address, callData: executeTx.data! }); - - // 3. 2 X CrossChainMessage log entry + // todo: Change "0x" to `proofBytes` for production when not using mock verifier + const updateArgs = ["0x", publicValuesBytes]; // Use actual args + const updateTx: AugmentedTransaction = { + contract: sp1HeliosContract, + chainId: l2ChainId, + method: "update", + args: updateArgs, + unpermissioned: false, + nonMulticall: true, + message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, + }; + transactions.push(updateTx); crossChainMessages.push({ type: "misc", - miscReason: "ZK bridge finalization", + miscReason: "ZK bridge finalization (Helios Update)", originationChainId: l1ChainId, destinationChainId: l2ChainId, }); - // todo: uncomment for prod. Check that this is working + + logger.debug({ + // Changed from error to debug for this specific log + message: `SpokePool address for executeMessage: ${spokePoolContract.address}`, + nonce: proof.sourceNonce.toString(), + }); + + // 2. SpokePool.executeMessage transaction + // todo: uncomment when we're working with SpokePool that respects the set HubPoolStore address. Otherwise txns will just revert. + // const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; + // const executeTx: AugmentedTransaction = { + // contract: spokePoolContract, + // chainId: l2ChainId, + // method: "executeMessage", + // args: executeArgs, + // // todo: check this + // unpermissioned: true, + // message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, + // }; + // transactions.push(executeTx); // crossChainMessages.push({ // type: "misc", - // miscReason: "ZK bridge finalization", + // miscReason: "ZK bridge finalization (Execute Message)", // originationChainId: l1ChainId, // destinationChainId: l2ChainId, // }); } catch (error) { logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateMulticallItem:${l2ChainId}`, // Renamed log point - message: `Failed to populate transaction for proof of nonce ${proof.sourceNonce.toString()}`, - error, + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, // Renamed log point + message: `Failed to prepare transaction for proof of nonce ${proof.sourceNonce.toString()}`, + error: error, // Use stringify helper proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, // Log less data }); continue; @@ -467,12 +478,12 @@ async function generateHeliosTxns( } logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:generateHeliosMulticallData:${l2ChainId}`, - message: `Generated ${multiCallData.length} calls (${multiCallData.length / 2} finalizations) for multicall.`, + at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, + message: `Generated ${transactions.length} transactions (${transactions.length / 2} finalizations).`, proofNoncesFinalized: proofsToSubmit.map((p) => p.sourceNonce.toString()), }); - return { callData: multiCallData, crossChainMessages: crossChainMessages }; + return { callData: transactions, crossChainMessages: crossChainMessages }; } // ================================== From 36a5e4aecc4fba6ecdb530b9d8f581b4d3ca5943 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 30 Apr 2025 13:15:47 -0700 Subject: [PATCH 05/31] revert logging additions from TransactionClient Signed-off-by: Ihor Farion --- src/clients/TransactionClient.ts | 30 +----------------------------- src/utils/TransactionUtils.ts | 28 +++------------------------- 2 files changed, 4 insertions(+), 54 deletions(-) diff --git a/src/clients/TransactionClient.ts b/src/clients/TransactionClient.ts index f1929fe7a9..84a1e092de 100644 --- a/src/clients/TransactionClient.ts +++ b/src/clients/TransactionClient.ts @@ -52,35 +52,7 @@ export class TransactionClient { // Each transaction is simulated in isolation; but on-chain execution may produce different // results due to execution sequence or intermediate changes in on-chain state. simulate(txns: AugmentedTransaction[]): Promise { - return Promise.all( - txns.map(async (txn: AugmentedTransaction) => { - this.logger.info({ - at: "TransactionClient#simulate", - message: "Simulating transaction...", - chainId: txn.chainId, - contract: txn.contract.address, - method: txn.method, - args: JSON.stringify(txn.args), - value: txn.value?.toString(), - canFail: txn.canFailInSimulation, - }); - - const result = await this._simulate(txn); - - this.logger.info({ - at: "TransactionClient#simulate", - message: "Simulation result", - chainId: txn.chainId, - contract: txn.contract.address, - method: txn.method, - succeed: result.succeed, - reason: result.reason, - gasLimit: result.transaction.gasLimit?.toString(), - }); - - return result; - }) - ); + return Promise.all(txns.map((txn: AugmentedTransaction) => this._simulate(txn))); } protected _submit(txn: AugmentedTransaction, nonce: number | null = null): Promise { diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index ef2b3a8545..8840b62cb3 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -186,17 +186,8 @@ export async function getGasPrice( } export async function willSucceed(transaction: AugmentedTransaction): Promise { - console.log("\n--- willSucceed START ---"); - console.log( - `[willSucceed] Simulating: ${transaction.method} on ${transaction.contract.address} (Chain: ${transaction.chainId})` - ); - console.log(`[willSucceed] Args: ${JSON.stringify(transaction.args)}`); - console.log(`[willSucceed] Value: ${transaction.value?.toString()}`); - // If the transaction already has a gasLimit, it should have been simulated in advance. if (transaction.canFailInSimulation || isDefined(transaction.gasLimit)) { - console.log("[willSucceed] Skipping simulation: canFailInSimulation or gasLimit already defined."); - console.log("--- willSucceed END ---"); return { transaction, succeed: true }; } @@ -209,39 +200,26 @@ export async function willSucceed(transaction: AugmentedTransaction): Promise Date: Wed, 30 Apr 2025 13:16:57 -0700 Subject: [PATCH 06/31] remove more Signed-off-by: Ihor Farion --- src/utils/TransactionUtils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index 8840b62cb3..4bc1e132e0 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -209,17 +209,13 @@ export async function willSucceed(transaction: AugmentedTransaction): Promise Date: Wed, 30 Apr 2025 13:48:31 -0700 Subject: [PATCH 07/31] move ABI import to helios.ts Signed-off-by: Ihor Farion --- src/common/ClientHelper.ts | 41 +++++++++++++------------ src/finalizer/utils/helios.ts | 56 +++++++++++++++++------------------ 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/src/common/ClientHelper.ts b/src/common/ClientHelper.ts index e19adc7a97..83ac9c6518 100644 --- a/src/common/ClientHelper.ts +++ b/src/common/ClientHelper.ts @@ -19,7 +19,6 @@ import { CommonConfig } from "./Config"; import { SpokePoolClientsByChain } from "../interfaces"; import { caching, clients, utils as sdkUtils } from "@across-protocol/sdk"; import V3_SPOKE_POOL_ABI from "./abi/V3SpokePool.json"; -import UNIVERSAL_SPOKE_ABI from "./abi/Universal_SpokePool.json"; export interface Clients { hubPoolClient: HubPoolClient; @@ -200,34 +199,34 @@ export async function constructSpokePoolClientsWithStartBlocks( const blockFinder = undefined; const redis = await getRedisCache(logger); - // Set up Spoke signers and connect them to spoke pool contract objects: const spokePoolSigners = await getSpokePoolSigners(baseSigner, enabledChains); const spokePools = await Promise.all( enabledChains.map(async (chainId) => { - let spokePoolAddr: string; - let spokePoolContract: ethers.Contract; - let registrationBlock: number; - - if (chainId === 56) { - // ---- START BSC TEST CODE ---- - spokePoolAddr = "0x4e8E101924eDE233C13e2D8622DC8aED2872d505"; // Hardcoded BSC SpokePool address - registrationBlock = 48762336; // Hardcoded BSC registration block - // Use UniversalSpokePool ABI for BSC - spokePoolContract = new ethers.Contract(spokePoolAddr, [...UNIVERSAL_SPOKE_ABI], spokePoolSigners[chainId]); - // ---- END BSC TEST CODE ---- + let spokePoolAddr; + + // ---- START BSC TEST CODE ---- + if (chainId == 56) { + spokePoolAddr = "0x4e8E101924eDE233C13e2D8622DC8aED2872d505"; } else { - // --- Logic for all other chains --- spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]); - // TODO: initialize using typechain factory after V3.5 migration. - // const spokePoolContract = SpokePool.connect(spokePoolAddr, spokePoolSigners[chainId]); - spokePoolContract = new ethers.Contract( - spokePoolAddr, - [...SpokePool.abi, ...V3_SPOKE_POOL_ABI], - spokePoolSigners[chainId] - ); + } + // ---- END BSC TEST CODE ---- + // TODO: initialize using typechain factory after V3.5 migration. + // const spokePoolContract = SpokePool.connect(spokePoolAddr, spokePoolSigners[chainId]); + const spokePoolContract = new ethers.Contract( + spokePoolAddr, + [...SpokePool.abi, ...V3_SPOKE_POOL_ABI], + spokePoolSigners[chainId] + ); + let registrationBlock; + // ---- START BSC TEST CODE ---- + if (chainId == 56) { + registrationBlock = 48762336; + } else { registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]); } + // ---- END BSC TEST CODE ---- return { chainId, contract: spokePoolContract, registrationBlock }; }) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 837c5c0769..d8416662ae 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -1,20 +1,13 @@ import { ethers } from "ethers"; import { HubPoolClient, SpokePoolClient } from "../../clients"; -import { - EventSearchConfig, - Signer, - Multicall2Call, - winston, - paginatedEventQuery, - compareAddressesSimple, -} from "../../utils"; +import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple } from "../../utils"; import { FinalizerPromise, CrossChainMessage } from "../types"; import { getSp1Helios } from "../../utils/Sp1HeliosUtils"; import { Log } from "../../interfaces"; import { CONTRACT_ADDRESSES } from "../../common"; import axios from "axios"; -// --- Structs --- import { AugmentedTransaction } from "../../clients"; +import UNIVERSAL_SPOKE_ABI from "../../common/abi/Universal_SpokePool.json"; // Define interfaces for the event arguments for clarity interface StoredCallDataEventArgs { @@ -448,30 +441,37 @@ async function generateHeliosTxns( }); // 2. SpokePool.executeMessage transaction + const universalSpokePoolContract = new ethers.Contract( + spokePoolContract.address, + [...UNIVERSAL_SPOKE_ABI], + // todo: is this the correct signer? Is the executeMessage on Universal_SpokePool permissioned even? + signer + ); + + const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; + const executeTx: AugmentedTransaction = { + contract: universalSpokePoolContract, + chainId: l2ChainId, + method: "executeMessage", + args: executeArgs, + // todo: check this + unpermissioned: true, + message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, + }; // todo: uncomment when we're working with SpokePool that respects the set HubPoolStore address. Otherwise txns will just revert. - // const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; - // const executeTx: AugmentedTransaction = { - // contract: spokePoolContract, - // chainId: l2ChainId, - // method: "executeMessage", - // args: executeArgs, - // // todo: check this - // unpermissioned: true, - // message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, - // }; - // transactions.push(executeTx); - // crossChainMessages.push({ - // type: "misc", - // miscReason: "ZK bridge finalization (Execute Message)", - // originationChainId: l1ChainId, - // destinationChainId: l2ChainId, - // }); + transactions.push(executeTx); + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization (Execute Message)", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); } catch (error) { logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, // Renamed log point + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, message: `Failed to prepare transaction for proof of nonce ${proof.sourceNonce.toString()}`, error: error, // Use stringify helper - proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, // Log less data + proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, }); continue; } From 7973c1b44897a4a524842804c3326eca840d6d21 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 30 Apr 2025 14:08:29 -0700 Subject: [PATCH 08/31] cleanup Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 32 +++----------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index d8416662ae..918e4400d6 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -423,6 +423,7 @@ async function generateHeliosTxns( method: "update", args: updateArgs, unpermissioned: false, + canFailInSimulation: false, nonMulticall: true, message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, }; @@ -456,6 +457,8 @@ async function generateHeliosTxns( args: executeArgs, // todo: check this unpermissioned: true, + // @dev Notice, in order for this tx to succeed in simulation, the SP1Helios.update(..) would be required to have updated the blockchain state already. Which is not how our sim. is done in index.ts + canFailInSimulation: true, message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, }; // todo: uncomment when we're working with SpokePool that respects the set HubPoolStore address. Otherwise txns will just revert. @@ -716,35 +719,6 @@ async function processUnfinalizedHeliosMessages( target: message.args.target, }; - // const storageSlot = calculateHubPoolStoreStorageSlot(message.args); - - // logger.info({ - // ...logContext, - // message: `Calculated this storage slot for stored message with nonce ${message.args.nonce} : ${storageSlot}`, - // }); - - // const apiRequest: ApiProofRequest = { - // src_chain_contract_address: hubPoolStoreAddress, - // src_chain_storage_slot: storageSlot, - // src_chain_block_number: message.blockNumber, - // dst_chain_contract_from_head: currentHead, - // dst_chain_contract_from_header: currentHeader, - // }; - - // const proofId = calculateProofId(apiRequest); - - // logger.info({ - // ...logContext, - // message: `Args: src_chain_contract_address=${apiRequest.src_chain_contract_address}, src_chain_storage_slot=${apiRequest.src_chain_storage_slot}, src_chain_block_number=${apiRequest.src_chain_block_number}, dst_chain_contract_from_head=${apiRequest.dst_chain_contract_from_head}, dst_chain_contract_from_header=${apiRequest.dst_chain_contract_from_header}, proofId=${proofId}`, - // }); - - // logger.info({ - // ...logContext, - // message: `Calculated this proofId for stored message with nonce ${message.args.nonce} : ${proofId}`, - // }); - - // continue; - try { const storageSlot = calculateHubPoolStoreStorageSlot(message.args); From a72fdbe01fb0454bd472fc708040c23d251700cf Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 30 Apr 2025 18:52:55 -0700 Subject: [PATCH 09/31] fix tx sending bugs Signed-off-by: Ihor Farion --- src/clients/TransactionClient.ts | 5 +++++ src/finalizer/utils/helios.ts | 24 +++++++++++------------- src/utils/TransactionUtils.ts | 17 ++++++++++++++++- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/clients/TransactionClient.ts b/src/clients/TransactionClient.ts index 84a1e092de..5389f6c860 100644 --- a/src/clients/TransactionClient.ts +++ b/src/clients/TransactionClient.ts @@ -79,7 +79,12 @@ export class TransactionClient { throw new Error(`chainId mismatch for method ${txn.method} (${txn.chainId} !== ${chainId})`); } + // todo: here, nonce is wrong. chainId is probably wrong const nonce = this.nonces[chainId] ? this.nonces[chainId] + 1 : undefined; + this.logger.info({ + at: "TransactionClient#submit", + message: `XXX ChainID ${chainId} nonce ${nonce}.`, + }); // @dev It's assumed that nobody ever wants to discount the gasLimit. const gasLimitMultiplier = txn.gasLimitMultiplier ?? DEFAULT_GASLIMIT_MULTIPLIER; diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 918e4400d6..24065e4237 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -87,7 +87,7 @@ type SuccessfulProof = { export async function heliosL1toL2Finalizer( logger: winston.Logger, - signer: Signer, + _signer: Signer, hubPoolClient: HubPoolClient, l2SpokePoolClient: SpokePoolClient, // Used for filtering target address l1SpokePoolClient: SpokePoolClient, @@ -195,7 +195,7 @@ export async function heliosL1toL2Finalizer( } // --- Step 5: Generate Multicall Data from Proofs --- - return generateHeliosTxns(logger, proofsToSubmit, l1ChainId, l2ChainId, l2SpokePoolClient, signer); + return generateHeliosTxns(logger, proofsToSubmit, l1ChainId, l2ChainId, l2SpokePoolClient); } // ================================== @@ -363,8 +363,7 @@ async function generateHeliosTxns( proofsToSubmit: SuccessfulProof[], l1ChainId: number, l2ChainId: number, - l2SpokePoolClient: SpokePoolClient, - signer: Signer + l2SpokePoolClient: SpokePoolClient ): Promise { const transactions: AugmentedTransaction[] = []; const crossChainMessages: CrossChainMessage[] = []; @@ -377,10 +376,9 @@ async function generateHeliosTxns( }); return { callData: [], crossChainMessages: [] }; } - // Create contract instances with a signer/provider if needed for AugmentedTransaction - // Assuming l2SpokePoolClient.spokePool has a signer or provider attached - const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, signer); const spokePoolContract = l2SpokePoolClient.spokePool; // Get contract instance from client + // Create SP1Helios contracts instance with new ABI and spokePoolContract signer + const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, spokePoolContract.signer); for (const proof of proofsToSubmit) { try { @@ -422,10 +420,11 @@ async function generateHeliosTxns( chainId: l2ChainId, method: "update", args: updateArgs, + // gasLimit: BigNumber.from(1000000), // todo: testing unpermissioned: false, canFailInSimulation: false, nonMulticall: true, - message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, + message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, }; transactions.push(updateTx); crossChainMessages.push({ @@ -436,7 +435,6 @@ async function generateHeliosTxns( }); logger.debug({ - // Changed from error to debug for this specific log message: `SpokePool address for executeMessage: ${spokePoolContract.address}`, nonce: proof.sourceNonce.toString(), }); @@ -445,8 +443,7 @@ async function generateHeliosTxns( const universalSpokePoolContract = new ethers.Contract( spokePoolContract.address, [...UNIVERSAL_SPOKE_ABI], - // todo: is this the correct signer? Is the executeMessage on Universal_SpokePool permissioned even? - signer + spokePoolContract.signer ); const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; @@ -455,11 +452,12 @@ async function generateHeliosTxns( chainId: l2ChainId, method: "executeMessage", args: executeArgs, + // gasLimit: BigNumber.from(1000000), // todo: testing // todo: check this unpermissioned: true, // @dev Notice, in order for this tx to succeed in simulation, the SP1Helios.update(..) would be required to have updated the blockchain state already. Which is not how our sim. is done in index.ts canFailInSimulation: true, - message: `Finalize Helios msg (nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, + message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, }; // todo: uncomment when we're working with SpokePool that respects the set HubPoolStore address. Otherwise txns will just revert. transactions.push(executeTx); @@ -473,7 +471,7 @@ async function generateHeliosTxns( logger.error({ at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, message: `Failed to prepare transaction for proof of nonce ${proof.sourceNonce.toString()}`, - error: error, // Use stringify helper + error: error, proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, }); continue; diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index 4bc1e132e0..0bf88075ec 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -16,9 +16,13 @@ import { toBNWei, winston, stringifyThrownValue, + CHAIN_IDs, } from "../utils"; dotenv.config(); +// Define chains that require legacy (type 0) transactions +const LEGACY_TRANSACTION_CHAINS = new Set([CHAIN_IDs.BNB]); + export type TransactionSimulationResult = { transaction: AugmentedTransaction; succeed: boolean; @@ -81,13 +85,24 @@ export async function runTransaction( Number(process.env[`MAX_FEE_PER_GAS_SCALER_${chainId}`] || process.env.MAX_FEE_PER_GAS_SCALER) || DEFAULT_GAS_FEE_SCALERS[chainId]?.maxFeePerGasScaler; - const gas = await getGasPrice( + let gas = await getGasPrice( provider, priorityFeeScaler, maxFeePerGasScaler, await contract.populateTransaction[method](...(args as Array), { value }) ); + // Check if the chain requires legacy transactions + if (LEGACY_TRANSACTION_CHAINS.has(chainId)) { + gas = { gasPrice: gas.maxFeePerGas }; + logger.debug({ + at: "TxUtil#runTransaction", + message: `Forcing legacy gasPrice for chainId ${chainId}`, + chainId, + gasPrice: gas.gasPrice?.toString(), + }); + } + logger.debug({ at: "TxUtil", message: "Send tx", From cfc7d586ef334a6b44edd696f90b2560896e27d4 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Wed, 30 Apr 2025 21:05:16 -0700 Subject: [PATCH 10/31] refactor Signed-off-by: Ihor Farion --- src/clients/TransactionClient.ts | 1 - src/common/ClientHelper.ts | 14 +- src/finalizer/utils/helios.ts | 476 +++++++++++-------------------- 3 files changed, 180 insertions(+), 311 deletions(-) diff --git a/src/clients/TransactionClient.ts b/src/clients/TransactionClient.ts index 5389f6c860..605b2a56e2 100644 --- a/src/clients/TransactionClient.ts +++ b/src/clients/TransactionClient.ts @@ -79,7 +79,6 @@ export class TransactionClient { throw new Error(`chainId mismatch for method ${txn.method} (${txn.chainId} !== ${chainId})`); } - // todo: here, nonce is wrong. chainId is probably wrong const nonce = this.nonces[chainId] ? this.nonces[chainId] + 1 : undefined; this.logger.info({ at: "TransactionClient#submit", diff --git a/src/common/ClientHelper.ts b/src/common/ClientHelper.ts index 83ac9c6518..558f971490 100644 --- a/src/common/ClientHelper.ts +++ b/src/common/ClientHelper.ts @@ -13,6 +13,7 @@ import { isDefined, getRedisCache, getArweaveJWKSigner, + CHAIN_IDs, } from "../utils"; import { HubPoolClient, MultiCallerClient, ConfigStoreClient, SpokePoolClient } from "../clients"; import { CommonConfig } from "./Config"; @@ -203,10 +204,13 @@ export async function constructSpokePoolClientsWithStartBlocks( const spokePoolSigners = await getSpokePoolSigners(baseSigner, enabledChains); const spokePools = await Promise.all( enabledChains.map(async (chainId) => { - let spokePoolAddr; - + let spokePoolAddr: string; // ---- START BSC TEST CODE ---- - if (chainId == 56) { + /* + todo: this is the only way to test finalizations without a mock HubPool contract + *Theoretically*, we could have some devnet setup with whatever we want. + */ + if (chainId == CHAIN_IDs.BNB) { spokePoolAddr = "0x4e8E101924eDE233C13e2D8622DC8aED2872d505"; } else { spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]); @@ -219,9 +223,9 @@ export async function constructSpokePoolClientsWithStartBlocks( [...SpokePool.abi, ...V3_SPOKE_POOL_ABI], spokePoolSigners[chainId] ); - let registrationBlock; + let registrationBlock: number; // ---- START BSC TEST CODE ---- - if (chainId == 56) { + if (chainId == CHAIN_IDs.BNB) { registrationBlock = 48762336; } else { registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]); diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 24065e4237..31f4e874df 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -32,8 +32,8 @@ type StorageSlotVerifiedEvent = Log & { args: StorageSlotVerifiedEventArgs }; interface ApiProofRequest { src_chain_contract_address: string; src_chain_storage_slot: string; - src_chain_block_number: number; // Changed from u64 for JS compatibility - dst_chain_contract_from_head: number; // Changed from u64 for JS compatibility + src_chain_block_number: number; // u64 in Rustland + dst_chain_contract_from_head: number; // u64 in Rustland dst_chain_contract_from_header: string; } @@ -64,17 +64,16 @@ const proofOutputsAbiTuple = `tuple( tuple(bytes32 key, bytes32 value, address contractAddress)[] slots )`; -// Type for the decoded ProofOutputs structure -type DecodedProofOutputs = { +type ProofOutputs = { executionStateRoot: string; newHeader: string; nextSyncCommitteeHash: string; - newHead: ethers.BigNumber; // Access the newHead value + newHead: ethers.BigNumber; prevHeader: string; prevHead: ethers.BigNumber; syncCommitteeHash: string; startSyncCommitteeHash: string; - slots: { key: string; value: string; contractAddress: string }[]; // Added contractAddress + slots: { key: string; value: string; contractAddress: string }[]; }; // Type for successful proof data, augmented with source info. @@ -89,16 +88,15 @@ export async function heliosL1toL2Finalizer( logger: winston.Logger, _signer: Signer, hubPoolClient: HubPoolClient, - l2SpokePoolClient: SpokePoolClient, // Used for filtering target address + l2SpokePoolClient: SpokePoolClient, l1SpokePoolClient: SpokePoolClient, _senderAddresses: string[] ): Promise { const l1ChainId = hubPoolClient.chainId; const l2ChainId = l2SpokePoolClient.chainId; - const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; // Get L2 SpokePool address + const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; - // --- Step 1: Query and Filter L1 Events --- - // ✅ Step 1 tested and working + // --- Step 1: Query and Filter L1 Events --- ✅ tested const relevantStoredCallDataEvents = await getAndFilterL1Events( logger, hubPoolClient, @@ -134,8 +132,7 @@ export async function heliosL1toL2Finalizer( return { callData: [], crossChainMessages: [] }; } - // --- Step 2: Query L2 Verification Events --- - // ✅ Step 2 tested and working + // --- Step 2: Query L2 Verification Events --- ✅ tested const verifiedKeys = await getL2VerifiedKeys(logger, l2SpokePoolClient, l2ChainId); // ---- START BSC TEST CODE (STEP 2) ---- @@ -152,8 +149,7 @@ export async function heliosL1toL2Finalizer( return { callData: [], crossChainMessages: [] }; } - // --- Step 3: Identify Unfinalized Messages --- - // ✅ Step 3 tested and working + // --- Step 3: Identify Unfinalized Messages --- ✅ tested const unfinalizedMessages = findUnfinalizedMessages(logger, relevantStoredCallDataEvents, verifiedKeys, l2ChainId); // todo: Testing, uncomment above after // const unfinalizedMessages = relevantStoredCallDataEvents; @@ -178,8 +174,7 @@ export async function heliosL1toL2Finalizer( return { callData: [], crossChainMessages: [] }; } - // --- Step 4: Get Proofs for Unfinalized Messages --- - // ✅ Step 4. tested and working. Didn't test ALL branches; but happy-cases work + // --- Step 4: Get Proofs for Unfinalized Messages --- ✅ tested. Didn't test ALL branches; but happy-cases work const proofsToSubmit = await processUnfinalizedHeliosMessages( logger, unfinalizedMessages, // Pass the unfinalized messages containing original event data @@ -194,7 +189,7 @@ export async function heliosL1toL2Finalizer( return { callData: [], crossChainMessages: [] }; } - // --- Step 5: Generate Multicall Data from Proofs --- + // --- Step 5: Generate Multicall Data from Proofs --- ⚠️ testing in progress return generateHeliosTxns(logger, proofsToSubmit, l1ChainId, l2ChainId, l2SpokePoolClient); } @@ -211,7 +206,7 @@ async function getAndFilterL1Events( l2ChainId: number, l2SpokePoolAddress: string ): Promise { - const l1Provider = hubPoolClient.hubPool.provider; // Get provider from HubPoolClient + const l1Provider = hubPoolClient.hubPool.provider; const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { @@ -283,7 +278,7 @@ async function getL2VerifiedKeys( l2SpokePoolClient: SpokePoolClient, l2ChainId: number ): Promise | null> { - const l2Provider = l2SpokePoolClient.spokePool.provider; // Get provider from L2 client + const l2Provider = l2SpokePoolClient.spokePool.provider; const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); if (!sp1HeliosAddress || !sp1HeliosAbi) { @@ -316,7 +311,7 @@ async function getL2VerifiedKeys( // Explicitly cast logs to the correct type const events: StorageSlotVerifiedEvent[] = rawLogs.map((log) => ({ ...log, - args: log.args as StorageSlotVerifiedEventArgs, // todo: is this a correct type? + args: log.args as StorageSlotVerifiedEventArgs, })); logger.info({ @@ -357,289 +352,8 @@ function findUnfinalizedMessages( return unfinalizedMessages; } -/** STEP 5: Generate Multicall Data */ -async function generateHeliosTxns( - logger: winston.Logger, - proofsToSubmit: SuccessfulProof[], - l1ChainId: number, - l2ChainId: number, - l2SpokePoolClient: SpokePoolClient -): Promise { - const transactions: AugmentedTransaction[] = []; - const crossChainMessages: CrossChainMessage[] = []; - - const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); - if (!sp1HeliosAddress || !sp1HeliosAbi) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateHeliosMulticallData:${l2ChainId}`, - message: `SP1Helios contract missing for L2 chain ${l2ChainId} during multicall generation.`, - }); - return { callData: [], crossChainMessages: [] }; - } - const spokePoolContract = l2SpokePoolClient.spokePool; // Get contract instance from client - // Create SP1Helios contracts instance with new ABI and spokePoolContract signer - const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, spokePoolContract.signer); - - for (const proof of proofsToSubmit) { - try { - // Ensure the hex strings have the '0x' prefix, adding it only if missing. - const proofBytes = proof.proofData.proof.startsWith("0x") ? proof.proofData.proof : "0x" + proof.proofData.proof; - const publicValuesBytes = proof.proofData.public_values.startsWith("0x") - ? proof.proofData.public_values - : "0x" + proof.proofData.public_values; - - let decodedOutputs: DecodedProofOutputs; - try { - const decodedResult = ethers.utils.defaultAbiCoder.decode([proofOutputsAbiTuple], publicValuesBytes)[0]; - decodedOutputs = { - executionStateRoot: decodedResult[0], - newHeader: decodedResult[1], - nextSyncCommitteeHash: decodedResult[2], - newHead: decodedResult[3], - prevHeader: decodedResult[4], - prevHead: decodedResult[5], - syncCommitteeHash: decodedResult[6], - startSyncCommitteeHash: decodedResult[7], - slots: decodedResult[8].map((slot: any[]) => ({ key: slot[0], value: slot[1], contractAddress: slot[2] })), - }; - } catch (decodeError) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:decodePublicValues:${l2ChainId}`, - message: `Failed to decode public_values for nonce ${proof.sourceNonce.toString()}`, - publicValues: publicValuesBytes, - error: decodeError, - }); - continue; - } - - // 1. SP1Helios.update transaction - // todo: Change "0x" to `proofBytes` for production when not using mock verifier - const updateArgs = ["0x", publicValuesBytes]; // Use actual args - const updateTx: AugmentedTransaction = { - contract: sp1HeliosContract, - chainId: l2ChainId, - method: "update", - args: updateArgs, - // gasLimit: BigNumber.from(1000000), // todo: testing - unpermissioned: false, - canFailInSimulation: false, - nonMulticall: true, - message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, - }; - transactions.push(updateTx); - crossChainMessages.push({ - type: "misc", - miscReason: "ZK bridge finalization (Helios Update)", - originationChainId: l1ChainId, - destinationChainId: l2ChainId, - }); - - logger.debug({ - message: `SpokePool address for executeMessage: ${spokePoolContract.address}`, - nonce: proof.sourceNonce.toString(), - }); - - // 2. SpokePool.executeMessage transaction - const universalSpokePoolContract = new ethers.Contract( - spokePoolContract.address, - [...UNIVERSAL_SPOKE_ABI], - spokePoolContract.signer - ); - - const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; - const executeTx: AugmentedTransaction = { - contract: universalSpokePoolContract, - chainId: l2ChainId, - method: "executeMessage", - args: executeArgs, - // gasLimit: BigNumber.from(1000000), // todo: testing - // todo: check this - unpermissioned: true, - // @dev Notice, in order for this tx to succeed in simulation, the SP1Helios.update(..) would be required to have updated the blockchain state already. Which is not how our sim. is done in index.ts - canFailInSimulation: true, - message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, - }; - // todo: uncomment when we're working with SpokePool that respects the set HubPoolStore address. Otherwise txns will just revert. - transactions.push(executeTx); - crossChainMessages.push({ - type: "misc", - miscReason: "ZK bridge finalization (Execute Message)", - originationChainId: l1ChainId, - destinationChainId: l2ChainId, - }); - } catch (error) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, - message: `Failed to prepare transaction for proof of nonce ${proof.sourceNonce.toString()}`, - error: error, - proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, - }); - continue; - } - } - - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, - message: `Generated ${transactions.length} transactions (${transactions.length / 2} finalizations).`, - proofNoncesFinalized: proofsToSubmit.map((p) => p.sourceNonce.toString()), - }); - - return { callData: transactions, crossChainMessages: crossChainMessages }; -} - -// ================================== -// Lower-Level Helper Functions -// ================================== - -/** - * Queries StoredCallData events from the HubPoolStore contract on L1. - */ -async function getL1StoredCallDataEvents( - logger: winston.Logger, - hubPoolClient: HubPoolClient, - l1SpokePoolClient: SpokePoolClient, // Used for block range - l1ChainId: number, - l2ChainId: number // For logging context -): Promise { - const l1Provider = hubPoolClient.hubPool.provider; // Get provider from HubPoolClient - - const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; - if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, - message: `HubPoolStore contract address or ABI not found for L1 chain ${l1ChainId}.`, - }); - return null; - } - - const hubPoolStoreContract = new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, l1Provider); - - const l1SearchConfig: EventSearchConfig = { - fromBlock: l1SpokePoolClient.eventSearchConfig.fromBlock, - toBlock: l1SpokePoolClient.latestBlockSearched, - maxBlockLookBack: l1SpokePoolClient.eventSearchConfig.maxBlockLookBack, - }; - - const storedCallDataFilter = hubPoolStoreContract.filters.StoredCallData(); - - try { - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, - message: `Querying StoredCallData events on L1 (${l1ChainId})`, - hubPoolStoreAddress: hubPoolStoreInfo.address, - fromBlock: l1SearchConfig.fromBlock, - toBlock: l1SearchConfig.toBlock, - }); - - const rawLogs = await paginatedEventQuery(hubPoolStoreContract, storedCallDataFilter, l1SearchConfig); - - // Explicitly cast logs to the correct type - const events: StoredCallDataEvent[] = rawLogs.map((log) => ({ - ...log, - args: log.args as StoredCallDataEventArgs, // todo: is this type correct? - })); - - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, - message: `Found ${events.length} StoredCallData events on L1 (${l1ChainId})`, - }); - return events; - } catch (error) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:getL1StoredCallDataEvents:${l2ChainId}`, - message: `Failed to query StoredCallData events from L1 (${l1ChainId})`, - hubPoolStoreAddress: hubPoolStoreInfo.address, - error, - }); - return null; // Return null on error - } -} - -/** - * Queries StorageSlotVerified events from the SP1Helios contract on L2. - */ -async function getL2StorageVerifiedEvents( - logger: winston.Logger, - l2SpokePoolClient: SpokePoolClient, - l2ChainId: number -): Promise { - const l2Provider = l2SpokePoolClient.spokePool.provider; // Get provider from L2 client - - const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); - if (!sp1HeliosAddress || !sp1HeliosAbi) { - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, - message: `SP1Helios contract not found for destination chain ${l2ChainId}. Cannot verify Helios messages.`, - }); - return []; // Return empty array if contract not found, allows finalizer to potentially process other types - } - const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); - - const l2SearchConfig: EventSearchConfig = { - fromBlock: l2SpokePoolClient.eventSearchConfig.fromBlock, - toBlock: l2SpokePoolClient.latestBlockSearched, - maxBlockLookBack: l2SpokePoolClient.eventSearchConfig.maxBlockLookBack, - }; - const storageVerifiedFilter = sp1HeliosContract.filters.StorageSlotVerified(); - - try { - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, - message: `Querying StorageSlotVerified events on L2 (${l2ChainId})`, - sp1HeliosAddress, - fromBlock: l2SearchConfig.fromBlock, - toBlock: l2SearchConfig.toBlock, - }); - - const rawLogs = await paginatedEventQuery(sp1HeliosContract, storageVerifiedFilter, l2SearchConfig); - - // Explicitly cast logs to the correct type - const events: StorageSlotVerifiedEvent[] = rawLogs.map((log) => ({ - ...log, - args: log.args as StorageSlotVerifiedEventArgs, // todo: is this a correct type? - })); - - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, - message: `Found ${events.length} StorageSlotVerified events on L2 (${l2ChainId})`, - }); - return events; - } catch (error) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:getL2StorageVerifiedEvents:${l2ChainId}`, - message: `Failed to query StorageSlotVerified events from L2 (${l2ChainId})`, - sp1HeliosAddress, - error, - }); - return null; // Return null on error - } -} - -/** - * Calculates the storage slot in the HubPoolStore contract for a given nonce. - * This assumes the data is stored in a mapping at slot 0, keyed by nonce. - * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) - */ -// ✅ tested and working -function calculateHubPoolStoreStorageSlot(eventArgs: StoredCallDataEventArgs): string { - const nonce = eventArgs.nonce; - const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 - - // Ensure nonce and slot position are correctly padded to 32 bytes (64 hex chars + 0x prefix) - const paddedNonce = ethers.utils.hexZeroPad(nonce.toHexString(), 32); - const paddedSlot = ethers.utils.hexZeroPad(ethers.BigNumber.from(mappingSlotPosition).toHexString(), 32); - - // Concatenate the padded key (nonce) and slot position - // ethers.utils.concat expects Uint8Array or hex string inputs - const concatenated = ethers.utils.concat([paddedNonce, paddedSlot]); - - // Calculate the Keccak256 hash - const storageSlot = ethers.utils.keccak256(concatenated); - - return storageSlot; -} - /** + * --- Step 4: Get Proofs for Unfinalized Messages --- * Processes unfinalized messages by interacting with the ZK Proof API. * Returns a list of successfully retrieved proofs. */ @@ -687,7 +401,7 @@ async function processUnfinalizedHeliosMessages( const headBn: ethers.BigNumber = await sp1HeliosContract.head(); // todo: well, currently we're taking currentHead to use as prevHead in our ZK proof. There's a particular scenario where we could speed up proofs // todo: (by not making them to wait for finality longer than needed) if our blockNumber that we need a proved slot for is older than this head. - currentHead = headBn.toNumber(); // Convert BigNumber head to number + currentHead = headBn.toNumber(); currentHeader = await sp1HeliosContract.headers(headBn); if (!currentHeader || currentHeader === ethers.constants.HashZero) { throw new Error(`Invalid header found for head ${currentHead}`); @@ -849,11 +563,163 @@ async function processUnfinalizedHeliosMessages( return successfulProofs; } +/** STEP 5: Generate Multicall Data */ +async function generateHeliosTxns( + logger: winston.Logger, + proofsToSubmit: SuccessfulProof[], + l1ChainId: number, + l2ChainId: number, + l2SpokePoolClient: SpokePoolClient +): Promise { + const transactions: AugmentedTransaction[] = []; + const crossChainMessages: CrossChainMessage[] = []; + + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); + if (!sp1HeliosAddress || !sp1HeliosAbi) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:generateHeliosMulticallData:${l2ChainId}`, + message: `SP1Helios contract missing for L2 chain ${l2ChainId} during multicall generation.`, + }); + return { callData: [], crossChainMessages: [] }; + } + const spokePoolContract = l2SpokePoolClient.spokePool; // Get contract instance from client + // Create SP1Helios contracts instance with new ABI and spokePoolContract signer + const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, spokePoolContract.signer); + + for (const proof of proofsToSubmit) { + try { + // Ensure the hex strings have the '0x' prefix, adding it only if missing. + const proofBytes = proof.proofData.proof.startsWith("0x") ? proof.proofData.proof : "0x" + proof.proofData.proof; + const publicValuesBytes = proof.proofData.public_values.startsWith("0x") + ? proof.proofData.public_values + : "0x" + proof.proofData.public_values; + + let decodedOutputs: ProofOutputs; + try { + const decodedResult = ethers.utils.defaultAbiCoder.decode([proofOutputsAbiTuple], publicValuesBytes)[0]; + decodedOutputs = { + executionStateRoot: decodedResult[0], + newHeader: decodedResult[1], + nextSyncCommitteeHash: decodedResult[2], + newHead: decodedResult[3], + prevHeader: decodedResult[4], + prevHead: decodedResult[5], + syncCommitteeHash: decodedResult[6], + startSyncCommitteeHash: decodedResult[7], + slots: decodedResult[8].map((slot: any[]) => ({ key: slot[0], value: slot[1], contractAddress: slot[2] })), + }; + } catch (decodeError) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:decodePublicValues:${l2ChainId}`, + message: `Failed to decode public_values for nonce ${proof.sourceNonce.toString()}`, + publicValues: publicValuesBytes, + error: decodeError, + }); + continue; + } + + // 1. SP1Helios.update transaction + // todo: Change "0x" to `proofBytes` for production when not using mock verifier + const updateArgs = ["0x", publicValuesBytes]; // Use actual args + const updateTx: AugmentedTransaction = { + contract: sp1HeliosContract, + chainId: l2ChainId, + method: "update", + args: updateArgs, + // gasLimit: BigNumber.from(1000000), // todo: testing + unpermissioned: false, + canFailInSimulation: false, + nonMulticall: true, + message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, + }; + transactions.push(updateTx); + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization (Helios Update)", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); + + logger.debug({ + message: `SpokePool address for executeMessage: ${spokePoolContract.address}`, + nonce: proof.sourceNonce.toString(), + }); + + // 2. SpokePool.executeMessage transaction + const universalSpokePoolContract = new ethers.Contract( + spokePoolContract.address, + [...UNIVERSAL_SPOKE_ABI], + spokePoolContract.signer + ); + + const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; + const executeTx: AugmentedTransaction = { + contract: universalSpokePoolContract, + chainId: l2ChainId, + method: "executeMessage", + args: executeArgs, + // gasLimit: BigNumber.from(1000000), // todo: testing + // todo: check this + unpermissioned: true, + // @dev Notice, in order for this tx to succeed in simulation, the SP1Helios.update(..) would be required to have updated the blockchain state already. Which is not how our sim. is done in index.ts + canFailInSimulation: true, + message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, + }; + // todo: uncomment when we're working with SpokePool that respects the set HubPoolStore address. Otherwise txns will just revert. + transactions.push(executeTx); + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization (Execute Message)", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, + message: `Failed to prepare transaction for proof of nonce ${proof.sourceNonce.toString()}`, + error: error, + proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, + }); + continue; + } + } + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, + message: `Generated ${transactions.length} transactions (${transactions.length / 2} finalizations).`, + proofNoncesFinalized: proofsToSubmit.map((p) => p.sourceNonce.toString()), + }); + + return { callData: transactions, crossChainMessages: crossChainMessages }; +} + +/** + * Calculates the storage slot in the HubPoolStore contract for a given nonce. + * This assumes the data is stored in a mapping at slot 0, keyed by nonce. + * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) ✅ tested + */ +function calculateHubPoolStoreStorageSlot(eventArgs: StoredCallDataEventArgs): string { + const nonce = eventArgs.nonce; + const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 + + // Ensure nonce and slot position are correctly padded to 32 bytes (64 hex chars + 0x prefix) + const paddedNonce = ethers.utils.hexZeroPad(nonce.toHexString(), 32); + const paddedSlot = ethers.utils.hexZeroPad(ethers.BigNumber.from(mappingSlotPosition).toHexString(), 32); + + // Concatenate the padded key (nonce) and slot position + // ethers.utils.concat expects Uint8Array or hex string inputs + const concatenated = ethers.utils.concat([paddedNonce, paddedSlot]); + + // Calculate the Keccak256 hash + const storageSlot = ethers.utils.keccak256(concatenated); + + return storageSlot; +} + /** * Calculates the deterministic Proof ID based on the request parameters. - * Matches the Rust implementation using RLP encoding and Keccak256. + * Matches the Rust implementation using RLP encoding and Keccak256. ✅ tested */ -// ✅ tested and working function calculateProofId(request: ApiProofRequest): string { const encoded = ethers.utils.RLP.encode([ request.src_chain_contract_address, From de365d47622da928623f4d25eed33e9be99fc24e Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 11:13:48 -0700 Subject: [PATCH 11/31] update deps. BNB -> BSC Signed-off-by: Ihor Farion --- package.json | 8 +-- src/common/ClientHelper.ts | 4 +- src/common/Constants.ts | 2 +- src/common/ContractAddresses.ts | 2 +- src/finalizer/index.ts | 2 +- src/utils/TransactionUtils.ts | 2 +- yarn.lock | 107 +++++++++++++++++++++++++++----- 7 files changed, 102 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 2d3bd23049..f024abb377 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "node": ">=20.18.0" }, "dependencies": { - "@across-protocol/constants": "^3.1.54", - "@across-protocol/contracts": "^4.0.5", - "@across-protocol/sdk": "4.1.46", + "@across-protocol/constants": "^3.1.63", + "@across-protocol/contracts": "^4.0.9-beta.0", + "@across-protocol/sdk": "4.1.54", "@arbitrum/sdk": "^4.0.2", "@consensys/linea-sdk": "^0.2.1", "@defi-wonderland/smock": "^2.3.5", @@ -127,4 +127,4 @@ "secp256k1@4.0.3": "4.0.4", "secp256k1@5.0.0": "5.0.1" } -} +} \ No newline at end of file diff --git a/src/common/ClientHelper.ts b/src/common/ClientHelper.ts index 558f971490..e4273468db 100644 --- a/src/common/ClientHelper.ts +++ b/src/common/ClientHelper.ts @@ -210,7 +210,7 @@ export async function constructSpokePoolClientsWithStartBlocks( todo: this is the only way to test finalizations without a mock HubPool contract *Theoretically*, we could have some devnet setup with whatever we want. */ - if (chainId == CHAIN_IDs.BNB) { + if (chainId == CHAIN_IDs.BSC) { spokePoolAddr = "0x4e8E101924eDE233C13e2D8622DC8aED2872d505"; } else { spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]); @@ -225,7 +225,7 @@ export async function constructSpokePoolClientsWithStartBlocks( ); let registrationBlock: number; // ---- START BSC TEST CODE ---- - if (chainId == CHAIN_IDs.BNB) { + if (chainId == CHAIN_IDs.BSC) { registrationBlock = 48762336; } else { registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]); diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 595a65d66b..f118839fa3 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -218,7 +218,7 @@ export const CHAIN_CACHE_FOLLOW_DISTANCE: { [chainId: number]: number } = { [CHAIN_IDs.BASE]: 120, [CHAIN_IDs.BLAST]: 120, [CHAIN_IDs.BOBA]: 0, - [CHAIN_IDs.BNB]: 3, // todo: idk what i'm doing. internet says BSC has 3-block finality + [CHAIN_IDs.BSC]: 3, // todo: idk what i'm doing. internet says BSC has 3-block finality [CHAIN_IDs.UNICHAIN]: 120, [CHAIN_IDs.INK]: 120, // Follows Optimism [CHAIN_IDs.LENS]: 512, diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 1bde1321ff..3c6e46b6c2 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -258,7 +258,7 @@ export const CONTRACT_ADDRESSES: { abi: CCTP_TOKEN_MESSENGER_ABI, }, }, - [CHAIN_IDs.BNB]: { + [CHAIN_IDs.BSC]: { sp1Helios: { // ---- START BSC TEST CODE ---- // ! todo: this uses a mock proof verifier diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index 8ad5854f9f..c59248a3fc 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -69,7 +69,7 @@ const chainFinalizers: { [chainId: number]: { finalizeOnL2: ChainFinalizer[]; fi finalizeOnL1: [opStackFinalizer, cctpL2toL1Finalizer], finalizeOnL2: [cctpL1toL2Finalizer], }, - [CHAIN_IDs.BNB]: { + [CHAIN_IDs.BSC]: { finalizeOnL1: [], finalizeOnL2: [heliosL1toL2Finalizer], }, diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index 0bf88075ec..549c7f00b2 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -21,7 +21,7 @@ import { dotenv.config(); // Define chains that require legacy (type 0) transactions -const LEGACY_TRANSACTION_CHAINS = new Set([CHAIN_IDs.BNB]); +const LEGACY_TRANSACTION_CHAINS = new Set([CHAIN_IDs.BSC]); export type TransactionSimulationResult = { transaction: AugmentedTransaction; diff --git a/yarn.lock b/yarn.lock index b73626111b..4bfbe6cf7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,15 +16,10 @@ resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.46.tgz#9f567eecd47eaa5035460193c49307f413623167" integrity sha512-EHwFVLlSfwIOOQsqnL53UUPtjw9GUN1Y7PDEXUDttPxuqcc/IRDSp7xBnYgn7OWhrEg3404SzBKmc2A1l3Sqhw== -"@across-protocol/constants@^3.1.51": - version "3.1.51" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.51.tgz#992beaba4c7540f62bfcde180403494ea5ac989a" - integrity sha512-XuXZXqbTxt/r+WQKwyMKnlg7z63SdN7AehdD1uWzF+FCM6u9XAECB8RlK1+wBj1RAJuuZGbrNMyfS9szq+X11Q== - -"@across-protocol/constants@^3.1.54": - version "3.1.54" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.54.tgz#e15a4a1c906b8c8b96320ce76c356b7ff5357e39" - integrity sha512-W8HjikhxoXhKZ2Mp+WcPTMwHp4nUv7AaOGawQV329hnmkYAz9tIxwrscuaWLtylNDCMLn+xY10mBiIAaPCtx9Q== +"@across-protocol/constants@^3.1.63": + version "3.1.63" + resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.63.tgz#c031fb30c79ff86448b5ccaca8a411d552ef144f" + integrity sha512-61MyPKT7X+qaZsAATDpcWpZoVAK1VwfWua0zTX5SU5UExzCartV+CsAfsrYaF5ttGBebtI33cS+754MGfA71bA== "@across-protocol/contracts@^0.1.4": version "0.1.4" @@ -65,13 +60,42 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@4.1.46": - version "4.1.46" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-4.1.46.tgz#9baae08b09072db78bddd685a3333cc9770ecb18" - integrity sha512-UrObJckBdg7j0vszBa6X+hGv+4OcuOTE7Zrclzwhk8etUGagCGt5mdDEevOX9Jb9FIXgjbV2pxfkXHOSg/C9TA== +"@across-protocol/contracts@^4.0.9-beta.0": + version "4.0.9-beta.0" + resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-4.0.9-beta.0.tgz#d2f165cff694252ad42c2e2192a9d7d7de98439f" + integrity sha512-gCfyNzlg92VmiZ/se09hptx9NCwGuHC2qUAchzGhXYSNcKPANy/t0SVmlCzLfCQSSMbmbyYJkdPY1kudHOrLJw== + dependencies: + "@across-protocol/constants" "^3.1.63" + "@coral-xyz/anchor" "^0.31.1" + "@defi-wonderland/smock" "^2.3.4" + "@eth-optimism/contracts" "^0.5.40" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@openzeppelin/contracts" "4.9.6" + "@openzeppelin/contracts-upgradeable" "4.9.6" + "@scroll-tech/contracts" "^0.1.0" + "@solana-program/token" "^0.5.1" + "@solana/kit" "^2.1.0" + "@solana/spl-token" "^0.4.6" + "@solana/web3.js" "1.98.2" + "@types/yargs" "^17.0.33" + "@uma/common" "^2.37.3" + "@uma/contracts-node" "^0.4.17" + "@uma/core" "^2.61.0" + axios "^1.7.4" + bs58 "^6.0.0" + prettier-plugin-rust "^0.1.9" + yargs "^17.7.2" + zksync-web3 "^0.14.3" + +"@across-protocol/sdk@4.1.54": + version "4.1.54" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-4.1.54.tgz#cca1fcd737a6f98cd8335c0a14f1816a179ef1f8" + integrity sha512-gw3seCyVch5jeiqAOlfypJAGMHeAJP6wAUfktOFrDrn7l+jILnjCGxvTK+PXxAzhw3kJSv31A8ej/tUH18FnTw== dependencies: "@across-protocol/across-token" "^1.0.0" - "@across-protocol/constants" "^3.1.51" + "@across-protocol/constants" "^3.1.63" "@across-protocol/contracts" "^4.0.5" "@coral-xyz/anchor" "^0.30.1" "@eth-optimism/sdk" "^3.3.1" @@ -396,6 +420,11 @@ resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30" integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ== +"@coral-xyz/anchor-errors@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.31.1.tgz#d635cbac2533973ae6bfb5d3ba1de89ce5aece2d" + integrity sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ== + "@coral-xyz/anchor@^0.30.1": version "0.30.1" resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d" @@ -417,6 +446,25 @@ superstruct "^0.15.4" toml "^3.0.0" +"@coral-xyz/anchor@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.31.1.tgz#0fdeebf45a3cb2e47e8ebbb815ca98542152962c" + integrity sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA== + dependencies: + "@coral-xyz/anchor-errors" "^0.31.1" + "@coral-xyz/borsh" "^0.31.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.69.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + eventemitter3 "^4.0.7" + pako "^2.0.3" + superstruct "^0.15.4" + toml "^3.0.0" + "@coral-xyz/borsh@^0.30.1": version "0.30.1" resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.30.1.tgz#869d8833abe65685c72e9199b8688477a4f6b0e3" @@ -425,6 +473,14 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" +"@coral-xyz/borsh@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.31.1.tgz#5328e1e0921b75d7f4a62dd3f61885a938bc7241" + integrity sha512-9N8AU9F0ubriKfNE3g1WF0/4dtlGXoBN/hd1PvbNBamBNwRgHxH4P+o3Zt7rSEloW1HUs6LfZEchlx9fW7POYw== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -2810,7 +2866,7 @@ "@solana/codecs-core" "2.0.0-rc.1" "@solana/errors" "2.0.0-rc.1" -"@solana/codecs-numbers@2.1.0": +"@solana/codecs-numbers@2.1.0", "@solana/codecs-numbers@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.1.0.tgz#f6a1a9009ace56238d8d9478dd5d375b09c6342a" integrity sha512-XMu4yw5iCgQnMKsxSWPPOrGgtaohmupN3eyAtYv3K3C/MJEc5V90h74k5B1GUCiHvcrdUDO9RclNjD9lgbjFag== @@ -3194,6 +3250,27 @@ "@solana/rpc-types" "2.1.0" "@solana/transaction-messages" "2.1.0" +"@solana/web3.js@1.98.2", "@solana/web3.js@^1.69.0": + version "1.98.2" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.2.tgz#45167a5cfb64436944bf4dc1e8be8482bd6d4c14" + integrity sha512-BqVwEG+TaG2yCkBMbD3C4hdpustR4FpuUFRPUmqRZYYlPI9Hg4XMWxHWOWRzHE9Lkc9NDjzXFX7lDXSgzC7R1A== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + "@solana/codecs-numbers" "^2.1.0" + agentkeepalive "^4.5.0" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@solana/web3.js@^1.31.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.95.2": version "1.95.4" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.4.tgz#771603f60d75cf7556ad867e1fd2efae32f9ad09" From 0dcaa58041396f13080dce3f31062f97e6407335 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 11:17:11 -0700 Subject: [PATCH 12/31] return missing EOF whitespace Signed-off-by: Ihor Farion --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f024abb377..20c6c98c4f 100644 --- a/package.json +++ b/package.json @@ -127,4 +127,4 @@ "secp256k1@4.0.3": "4.0.4", "secp256k1@5.0.0": "5.0.1" } -} \ No newline at end of file +} From 6c85303b0ac97c5a5f7648b346b16c142f0c7cb9 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 11:26:11 -0700 Subject: [PATCH 13/31] polish Signed-off-by: Ihor Farion --- src/clients/TransactionClient.ts | 4 ---- src/finalizer/index.ts | 8 ++------ src/finalizer/utils/helios.ts | 5 ++++- src/utils/Sp1HeliosUtils.ts | 5 ----- 4 files changed, 6 insertions(+), 16 deletions(-) delete mode 100644 src/utils/Sp1HeliosUtils.ts diff --git a/src/clients/TransactionClient.ts b/src/clients/TransactionClient.ts index 605b2a56e2..84a1e092de 100644 --- a/src/clients/TransactionClient.ts +++ b/src/clients/TransactionClient.ts @@ -80,10 +80,6 @@ export class TransactionClient { } const nonce = this.nonces[chainId] ? this.nonces[chainId] + 1 : undefined; - this.logger.info({ - at: "TransactionClient#submit", - message: `XXX ChainID ${chainId} nonce ${nonce}.`, - }); // @dev It's assumed that nobody ever wants to discount the gasLimit. const gasLimitMultiplier = txn.gasLimitMultiplier ?? DEFAULT_GASLIMIT_MULTIPLIER; diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index 531c094f7b..cfb7cbb799 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -376,17 +376,14 @@ 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)) { - // Separate Multicall2Call and AugmentedTransaction objects const multicallTxns: Multicall2Call[] = []; - // Process each finalization separately finalizations.forEach(({ txn }) => { - // Check if this is a Multicall2Call (doesn't have contract property) - // or an AugmentedTransaction (has contract property) if ("contract" in txn) { // It's an AugmentedTransaction, enqueue directly - // todo? we might want to enqueue these after the batch call txn.nonMulticall = true; // cautiously enforce an invariant that should already be present multicallerClient.enqueueTransaction(txn); } else { @@ -395,7 +392,6 @@ export async function finalize( } }); - // If we have any multicall transactions, bundle them together if (multicallTxns.length > 0) { const txnToSubmit: AugmentedTransaction = { contract: multicall2Lookup[Number(chainId)], diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 31f4e874df..53733b154b 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -2,7 +2,6 @@ import { ethers } from "ethers"; import { HubPoolClient, SpokePoolClient } from "../../clients"; import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple } from "../../utils"; import { FinalizerPromise, CrossChainMessage } from "../types"; -import { getSp1Helios } from "../../utils/Sp1HeliosUtils"; import { Log } from "../../interfaces"; import { CONTRACT_ADDRESSES } from "../../common"; import axios from "axios"; @@ -730,3 +729,7 @@ function calculateProofId(request: ApiProofRequest): string { ]); return ethers.utils.keccak256(encoded); } + +function getSp1Helios(dstChainId: number): { address?: string; abi?: unknown[] } { + return CONTRACT_ADDRESSES[dstChainId].sp1Helios; +} diff --git a/src/utils/Sp1HeliosUtils.ts b/src/utils/Sp1HeliosUtils.ts deleted file mode 100644 index 8b63b0f601..0000000000 --- a/src/utils/Sp1HeliosUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { CONTRACT_ADDRESSES } from "../common"; - -export function getSp1Helios(dstChainId: number): { address?: string; abi?: unknown[] } { - return CONTRACT_ADDRESSES[dstChainId].sp1Helios; -} From 9dce411b8d8ab6e9660730d4aed99fccffcb1193 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 12:37:37 -0700 Subject: [PATCH 14/31] allow completing half-finalized messages Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 493 ++++++++++++++++++++++++---------- 1 file changed, 349 insertions(+), 144 deletions(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 53733b154b..001a5809c6 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -22,17 +22,26 @@ interface StorageSlotVerifiedEventArgs { contractAddress: string; } +interface RelayedCallDataEventArgs { + nonce: ethers.BigNumber; + caller: string; +} +// -------------------------------- + // Type for the structured StoredCallData event type StoredCallDataEvent = Log & { args: StoredCallDataEventArgs }; // Type for the structured StorageSlotVerified event type StorageSlotVerifiedEvent = Log & { args: StorageSlotVerifiedEventArgs }; +// Type for the structured RelayedCallData event +type RelayedCallDataEvent = Log & { args: RelayedCallDataEventArgs }; +// ------------------------------------ // --- API Interaction Types --- interface ApiProofRequest { src_chain_contract_address: string; src_chain_storage_slot: string; - src_chain_block_number: number; // u64 in Rustland - dst_chain_contract_from_head: number; // u64 in Rustland + src_chain_block_number: number; // u64 on Rust API side + dst_chain_contract_from_head: number; // u64 on Rust API side dst_chain_contract_from_header: string; } @@ -75,12 +84,21 @@ type ProofOutputs = { slots: { key: string; value: string; contractAddress: string }[]; }; -// Type for successful proof data, augmented with source info. +type HeliosMessageStatus = "NeedsProofAndExecution" | "NeedsExecutionOnly"; + +interface PendingHeliosMessage { + l1Event: StoredCallDataEvent; // The original L1 event triggering the flow + status: HeliosMessageStatus; + verifiedHead?: ethers.BigNumber; // Head from the StorageSlotVerified event, only present if status is NeedsExecutionOnly +} +// --------------------------------------- + +// Type for successful proof data, augmented with HubPoolStore event info. type SuccessfulProof = { proofData: SP1HeliosProofData; sourceNonce: ethers.BigNumber; target: string; - sourceMessageData: string; // Original calldata from HubPoolStore event + sourceMessageData: string; }; export async function heliosL1toL2Finalizer( @@ -93,111 +111,185 @@ export async function heliosL1toL2Finalizer( ): Promise { const l1ChainId = hubPoolClient.chainId; const l2ChainId = l2SpokePoolClient.chainId; - const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; - // --- Step 1: Query and Filter L1 Events --- ✅ tested - const relevantStoredCallDataEvents = await getAndFilterL1Events( + // --- Step 1: Identify Pending Messages --- + const pendingMessages = await identifyPendingHeliosMessages( logger, hubPoolClient, l1SpokePoolClient, + l2SpokePoolClient, l1ChainId, - l2ChainId, - l2SpokePoolAddress + l2ChainId ); - // ---- START BSC TEST CODE ---- - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_1:${l2ChainId}`, - message: "--- DEBUGGING STEP 1: StoredCallData Events ---", - count: relevantStoredCallDataEvents?.length ?? "null", - events: relevantStoredCallDataEvents // Log the actual events for inspection - ? relevantStoredCallDataEvents.map((e) => ({ - txHash: e.transactionHash, - blockNumber: e.blockNumber, - target: e.args.target, - nonce: e.args.nonce.toString(), - dataLength: e.args.data.length, - data: e.args.data, - })) - : "null", - }); - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_1:${l2ChainId}`, - message: "--- RETURNING EARLY AFTER STEP 1 FOR TESTING ---", - }); - // ---- END BSC TEST CODE ---- - - if (!relevantStoredCallDataEvents || relevantStoredCallDataEvents.length === 0) { - return { callData: [], crossChainMessages: [] }; - } - - // --- Step 2: Query L2 Verification Events --- ✅ tested - const verifiedKeys = await getL2VerifiedKeys(logger, l2SpokePoolClient, l2ChainId); - - // ---- START BSC TEST CODE (STEP 2) ---- - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_2:${l2ChainId}`, - message: "--- DEBUGGING STEP 2: Verified Keys ---", - count: verifiedKeys?.size ?? "null (error occurred)", - keys: verifiedKeys ? [...verifiedKeys] : "null (error occurred)", // Convert Set to Array for logging - }); - // ---- END BSC TEST CODE (STEP 2) ---- - - if (verifiedKeys === null) { - // Indicates an error occurred fetching L2 events + if (!pendingMessages || pendingMessages.length === 0) { + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, + message: "No pending Helios messages found requiring action.", + }); return { callData: [], crossChainMessages: [] }; } - // --- Step 3: Identify Unfinalized Messages --- ✅ tested - const unfinalizedMessages = findUnfinalizedMessages(logger, relevantStoredCallDataEvents, verifiedKeys, l2ChainId); - // todo: Testing, uncomment above after - // const unfinalizedMessages = relevantStoredCallDataEvents; + // Separate messages based on required action + const needsProofAndExecution = pendingMessages.filter((m) => m.status === "NeedsProofAndExecution"); + const needsExecutionOnly = pendingMessages.filter((m) => m.status === "NeedsExecutionOnly"); - // ---- START BSC TEST CODE (STEP 3) ---- logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:DEBUG_STEP_3:${l2ChainId}`, - message: "--- DEBUGGING STEP 3: Unfinalized Messages ---", - count: unfinalizedMessages.length, - messages: unfinalizedMessages.map((m) => ({ - txHash: m.transactionHash, - blockNumber: m.blockNumber, - target: m.args.target, - nonce: m.args.nonce.toString(), - // Optionally calculate expected slot for verification - expectedSlot: calculateHubPoolStoreStorageSlot(m.args), - })), + at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, + message: `Identified ${pendingMessages.length} total pending messages.`, + counts: { + needsProofAndExecution: needsProofAndExecution.length, + needsExecutionOnly: needsExecutionOnly.length, + }, + needsExecutionNonces: needsExecutionOnly.map((m) => m.l1Event.args.nonce.toString()), // Log nonces needing only execution }); - // ---- END BSC TEST CODE (STEP 3) ---- - if (unfinalizedMessages.length === 0) { - return { callData: [], crossChainMessages: [] }; + // --- Step 2: Get Proofs for Messages Needing Full Finalization --- + let proofsToSubmit: SuccessfulProof[] = []; + if (needsProofAndExecution.length > 0) { + // Pass only the messages that need proofs + proofsToSubmit = await processUnfinalizedHeliosMessages( + logger, + needsProofAndExecution, // Pass PendingHeliosMessage[] here + l2SpokePoolClient, + l1ChainId + ); + if (proofsToSubmit.length === 0) { + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, + message: "No successful proofs retrieved for messages needing full finalization.", + }); + // Don't return yet, might still have needsExecutionOnly messages + } } - // --- Step 4: Get Proofs for Unfinalized Messages --- ✅ tested. Didn't test ALL branches; but happy-cases work - const proofsToSubmit = await processUnfinalizedHeliosMessages( - logger, - unfinalizedMessages, // Pass the unfinalized messages containing original event data - l2SpokePoolClient, - l1ChainId - ); - if (proofsToSubmit.length === 0) { + // --- Step 3: Generate Multicall Data from Proofs and Partially Finalized Messages --- + if (proofsToSubmit.length === 0 && needsExecutionOnly.length === 0) { logger.info({ at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, - message: "No successful proofs retrieved to submit.", + message: "No proofs obtained and no messages need execution only. Nothing to submit.", }); return { callData: [], crossChainMessages: [] }; } - // --- Step 5: Generate Multicall Data from Proofs --- ⚠️ testing in progress - return generateHeliosTxns(logger, proofsToSubmit, l1ChainId, l2ChainId, l2SpokePoolClient); + return generateHeliosTxns( + logger, + proofsToSubmit, + needsExecutionOnly, // Pass messages needing only execution + l1ChainId, + l2ChainId, + l2SpokePoolClient + ); } // ================================== // Step-by-step Helper Functions // ================================== -/** STEP 1: Query and Filter L1 Events */ -async function getAndFilterL1Events( +/** --- Step 1 --- + * Identifies messages stored on L1 HubPoolStore that require action on L2. + * Fetches L1 StoredCallData events, L2 StorageSlotVerified events, and L2 RelayedCallData events. + * Determines if a message needs both proof+execution or just execution or no actions. + */ +async function identifyPendingHeliosMessages( + logger: winston.Logger, + hubPoolClient: HubPoolClient, + l1SpokePoolClient: SpokePoolClient, + l2SpokePoolClient: SpokePoolClient, + l1ChainId: number, + l2ChainId: number +): Promise { + const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; + + // --- Substep 1: Query and Filter L1 Events --- + const relevantStoredCallDataEvents = await getRelevantL1Events( + logger, + hubPoolClient, + l1SpokePoolClient, + l1ChainId, + l2ChainId, + l2SpokePoolAddress + ); + + if (!relevantStoredCallDataEvents || relevantStoredCallDataEvents.length === 0) { + logger.debug({ + at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, + message: "No relevant StoredCallData events found on L1.", + }); + return []; + } + + // --- Substep 2: Query L2 Verification Events (StorageSlotVerified) --- + // Store as Map to easily access head later + const verifiedSlotsMap = await getL2VerifiedSlotsMap(logger, l2SpokePoolClient, l2ChainId); + if (verifiedSlotsMap === null) { + // Error already logged in helper + return null; // Propagate error state + } + + // --- Substep 3: Query L2 Execution Events (RelayedCallData) --- + const relayedNonces = await getL2RelayedNonces(logger, l2SpokePoolClient, l2ChainId); + if (relayedNonces === null) { + // Error already logged in helper + return null; // Propagate error state + } + + // --- Determine Status for each L1 Event --- + const pendingMessages: PendingHeliosMessage[] = []; + for (const l1Event of relevantStoredCallDataEvents) { + const expectedStorageSlot = calculateHubPoolStoreStorageSlot(l1Event.args); + const nonce = l1Event.args.nonce; + + const isExecuted = relayedNonces.has(nonce.toString()); // Use nonce string as key + + if (!isExecuted) { + if (verifiedSlotsMap.has(expectedStorageSlot) /* isVerified */) { + // Verified but not executed -> Needs Execution Only + const verifiedEvent = verifiedSlotsMap.get(expectedStorageSlot); + pendingMessages.push({ + l1Event: l1Event, + status: "NeedsExecutionOnly", + verifiedHead: verifiedEvent.args.head, + }); + // Log a warning for partially finalized messages + logger.warn({ + at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, + message: `Message requires execution only (already verified). Will generate SpokePool.executeMessage tx.`, + l1TxHash: l1Event.transactionHash, + nonce: nonce.toString(), + storageSlot: expectedStorageSlot, + verifiedOnL2TxHash: verifiedEvent.transactionHash, + verifiedHead: verifiedEvent.args.head.toString(), + }); + } else { + // Not verified and not executed -> Needs Proof and Execution + pendingMessages.push({ + l1Event: l1Event, + status: "NeedsProofAndExecution", + // verifiedHead is undefined here + }); + } + } + // If isExecuted is true, the message is fully finalized, do nothing. + } + + // todo? Change to debug + logger.info({ + at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, + message: `Finished identifying pending messages.`, + totalL1StoredCallData: relevantStoredCallDataEvents.length, + totalL2VerifiedSlots: verifiedSlotsMap.size, + totalL2RelayedNonces: relayedNonces.size, + pendingMessagesCount: pendingMessages.length, + needsProofCount: pendingMessages.filter((m) => m.status === "NeedsProofAndExecution").length, + needsExecutionOnlyCount: pendingMessages.filter((m) => m.status === "NeedsExecutionOnly").length, + }); + + return pendingMessages; +} + +/** Query and Filter L1 Events */ +async function getRelevantL1Events( logger: winston.Logger, hubPoolClient: HubPoolClient, l1SpokePoolClient: SpokePoolClient, @@ -271,21 +363,22 @@ async function getAndFilterL1Events( } } -/** STEP 2: Query L2 Verification Events and return verified keys */ -async function getL2VerifiedKeys( +/** Query L2 Verification Events and return verified slots map */ +async function getL2VerifiedSlotsMap( logger: winston.Logger, l2SpokePoolClient: SpokePoolClient, l2ChainId: number -): Promise | null> { +): Promise | null> { const l2Provider = l2SpokePoolClient.spokePool.provider; const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); if (!sp1HeliosAddress || !sp1HeliosAbi) { + // todo? Consider erroring here logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, message: `SP1Helios contract not found for destination chain ${l2ChainId}. Cannot verify Helios messages.`, }); - return null; + return new Map(); // Return empty map if contract not found, allows process to continue if needed elsewhere } const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); @@ -298,7 +391,7 @@ async function getL2VerifiedKeys( try { logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, + at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, message: `Querying StorageSlotVerified events on L2 (${l2ChainId})`, sp1HeliosAddress, fromBlock: l2SearchConfig.fromBlock, @@ -314,54 +407,102 @@ async function getL2VerifiedKeys( })); logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, + at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, message: `Found ${events.length} StorageSlotVerified events on L2 (${l2ChainId})`, }); - return new Set(events.map((event) => event.args.key)); + + // Store events in a map keyed by the storage slot (key) + const verifiedSlotsMap = new Map(); + events.forEach((event) => { + // Handle potential duplicates (though unlikely with paginated query): favour latest block/logIndex + const existing = verifiedSlotsMap.get(event.args.key); + if ( + !existing || + event.blockNumber > existing.blockNumber || + (event.blockNumber === existing.blockNumber && event.logIndex > existing.logIndex) + ) { + verifiedSlotsMap.set(event.args.key, event); + } + }); + return verifiedSlotsMap; } catch (error) { logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, + at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, message: `Failed to query StorageSlotVerified events from L2 (${l2ChainId})`, sp1HeliosAddress, error, }); - return null; + return null; // Return null on error } } -/** STEP 3: Identify Unfinalized Messages */ -function findUnfinalizedMessages( +/** --- Query L2 Execution Events (RelayedCallData) */ +async function getL2RelayedNonces( logger: winston.Logger, - relevantStoredCallDataEvents: StoredCallDataEvent[], - verifiedKeys: Set, + l2SpokePoolClient: SpokePoolClient, l2ChainId: number -): StoredCallDataEvent[] { - const unfinalizedMessages = relevantStoredCallDataEvents.filter((event) => { - const expectedStorageSlot = calculateHubPoolStoreStorageSlot(event.args); - return !verifiedKeys.has(expectedStorageSlot); - }); +): Promise | null> { + const l2Provider = l2SpokePoolClient.spokePool.provider; + const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; + // Use the Universal Spoke Pool ABI for this event + const spokePoolContract = new ethers.Contract(l2SpokePoolAddress, UNIVERSAL_SPOKE_ABI, l2Provider); - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:findUnfinalizedMessages:${l2ChainId}`, - message: `Detected ${unfinalizedMessages.length} unfinalized Helios messages after target filtering and verification check.`, - totalRelevantStoredCallData: relevantStoredCallDataEvents.length, - totalStorageVerified: verifiedKeys.size, - }); + const l2SearchConfig: EventSearchConfig = { + fromBlock: l2SpokePoolClient.eventSearchConfig.fromBlock, + toBlock: l2SpokePoolClient.latestBlockSearched, + maxBlockLookBack: l2SpokePoolClient.eventSearchConfig.maxBlockLookBack, + }; + const relayedCallDataFilter = spokePoolContract.filters.RelayedCallData(); - return unfinalizedMessages; + try { + logger.debug({ + at: `Finalizer#heliosL1toL2Finalizer:getL2RelayedNonces:${l2ChainId}`, + message: `Querying RelayedCallData events on L2 SpokePool (${l2ChainId})`, + spokePoolAddress: l2SpokePoolAddress, + fromBlock: l2SearchConfig.fromBlock, + toBlock: l2SearchConfig.toBlock, + }); + + const rawLogs = await paginatedEventQuery(spokePoolContract, relayedCallDataFilter, l2SearchConfig); + + const events: RelayedCallDataEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as RelayedCallDataEventArgs, + })); + + logger.info({ + at: `Finalizer#heliosL1toL2Finalizer:getL2RelayedNonces:${l2ChainId}`, + message: `Found ${events.length} RelayedCallData events on L2 (${l2ChainId})`, + }); + + // Return a Set of nonces (as strings for easy comparison) + return new Set(events.map((event) => event.args.nonce.toString())); + } catch (error) { + logger.warn({ + at: `Finalizer#heliosL1toL2Finalizer:getL2RelayedNonces:${l2ChainId}`, + message: `Failed to query RelayedCallData events from L2 (${l2ChainId})`, + spokePoolAddress: l2SpokePoolAddress, + error, + }); + return null; // Return null on error + } } /** - * --- Step 4: Get Proofs for Unfinalized Messages --- - * Processes unfinalized messages by interacting with the ZK Proof API. + * --- Get Proofs for Unfinalized Messages --- + * Processes messages needing proof+execution by interacting with the ZK Proof API. * Returns a list of successfully retrieved proofs. */ async function processUnfinalizedHeliosMessages( logger: winston.Logger, - unfinalizedMessages: StoredCallDataEvent[], + messagesToProcess: PendingHeliosMessage[], l2SpokePoolClient: SpokePoolClient, l1ChainId: number ): Promise { + // Filter within the function just in case, though the caller should have already filtered + const unfinalizedMessages = messagesToProcess.filter((m) => m.status === "NeedsProofAndExecution"); + if (unfinalizedMessages.length === 0) return []; + const l2ChainId = l2SpokePoolClient.chainId; const l2Provider = l2SpokePoolClient.spokePool.provider; const apiBaseUrl = process.env.HELIOS_PROOF_API_URL; @@ -421,22 +562,24 @@ async function processUnfinalizedHeliosMessages( const successfulProofs: SuccessfulProof[] = []; - // todo? Process messages one by one for now, can optimize with Promise.all later - for (const message of unfinalizedMessages) { + // todo? Can be optimized with Promise.All + // Process messages one by one + for (const pendingMessage of unfinalizedMessages) { + const l1Event = pendingMessage.l1Event; // Extract the L1 event const logContext = { at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - l1TxHash: message.transactionHash, - nonce: message.args.nonce.toString(), - target: message.args.target, + l1TxHash: l1Event.transactionHash, + nonce: l1Event.args.nonce.toString(), + target: l1Event.args.target, }; try { - const storageSlot = calculateHubPoolStoreStorageSlot(message.args); + const storageSlot = calculateHubPoolStoreStorageSlot(l1Event.args); const apiRequest: ApiProofRequest = { src_chain_contract_address: hubPoolStoreAddress, src_chain_storage_slot: storageSlot, - src_chain_block_number: message.blockNumber, + src_chain_block_number: l1Event.blockNumber, // Use block number from L1 event dst_chain_contract_from_head: currentHead, dst_chain_contract_from_header: currentHeader, }; @@ -458,7 +601,6 @@ async function processUnfinalizedHeliosMessages( } // --- API Interaction Flow --- - // 1. Try to get proof if (getError && axios.isAxiosError(getError) && getError.response?.status === 404) { // 1a. NOTFOUND -> Request proof @@ -526,9 +668,9 @@ async function processUnfinalizedHeliosMessages( logger.info({ ...logContext, message: "Proof successfully retrieved.", proofId }); successfulProofs.push({ proofData: proofState.update_calldata, - sourceNonce: message.args.nonce, - target: message.args.target, - sourceMessageData: message.args.data, + sourceNonce: l1Event.args.nonce, // Use nonce from L1 event + target: l1Event.args.target, // Use target from L1 event + sourceMessageData: l1Event.args.data, // Use data from L1 event }); } else { logger.error({ @@ -562,10 +704,11 @@ async function processUnfinalizedHeliosMessages( return successfulProofs; } -/** STEP 5: Generate Multicall Data */ +/** --- Generate Multicall Data --- */ async function generateHeliosTxns( logger: winston.Logger, - proofsToSubmit: SuccessfulProof[], + successfulProofs: SuccessfulProof[], // Renamed from proofsToSubmit + needsExecutionOnlyMessages: PendingHeliosMessage[], // Added new input l1ChainId: number, l2ChainId: number, l2SpokePoolClient: SpokePoolClient @@ -574,18 +717,90 @@ async function generateHeliosTxns( const crossChainMessages: CrossChainMessage[] = []; const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); - if (!sp1HeliosAddress || !sp1HeliosAbi) { + // SP1Helios contract is only needed for the 'update' transaction part + let sp1HeliosContract: ethers.Contract | null = null; + if (sp1HeliosAddress && sp1HeliosAbi && successfulProofs.length > 0) { + // Only create if needed + sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2SpokePoolClient.spokePool.signer); + } else if (successfulProofs.length > 0) { logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateHeliosMulticallData:${l2ChainId}`, - message: `SP1Helios contract missing for L2 chain ${l2ChainId} during multicall generation.`, + at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, + message: `SP1Helios contract missing for L2 chain ${l2ChainId}, but proofs were provided. Cannot generate 'update' txns.`, }); - return { callData: [], crossChainMessages: [] }; + // Cannot proceed with proofs without the contract + // We might still be able to process needsExecutionOnly, so don't return yet } - const spokePoolContract = l2SpokePoolClient.spokePool; // Get contract instance from client - // Create SP1Helios contracts instance with new ABI and spokePoolContract signer - const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, spokePoolContract.signer); - for (const proof of proofsToSubmit) { + // Universal Spoke Pool contract is needed for 'executeMessage' + const spokePoolAddress = l2SpokePoolClient.spokePool.address; + const universalSpokePoolContract = new ethers.Contract( + spokePoolAddress, + [...UNIVERSAL_SPOKE_ABI], // Ensure ABI has executeMessage + l2SpokePoolClient.spokePool.signer + ); + + // --- Process messages needing only execution --- + for (const message of needsExecutionOnlyMessages) { + const { l1Event, verifiedHead } = message; + const nonce = l1Event.args.nonce; + const sourceMessageData = l1Event.args.data; + + if (!verifiedHead) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, + message: `Logic error: Message ${nonce.toString()} needs execution only but verifiedHead is missing. Skipping.`, + l1TxHash: l1Event.transactionHash, + }); + continue; + } + + try { + logger.warn({ + // Log warning again here when actually generating the tx + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, + message: `Generating SpokePool.executeMessage ONLY for partially finalized message.`, + nonce: nonce.toString(), + l1TxHash: l1Event.transactionHash, + verifiedHead: verifiedHead.toString(), + }); + + const executeArgs = [nonce, sourceMessageData, verifiedHead]; // Use verifiedHead from the message + const executeTx: AugmentedTransaction = { + contract: universalSpokePoolContract, + chainId: l2ChainId, + method: "executeMessage", + args: executeArgs, + unpermissioned: true, // Assuming anyone can call executeMessage if requirements met + canFailInSimulation: true, // Still true, depends on state potentially changing + message: `Finalize Helios msg (HubPoolStore nonce ${nonce.toString()}) - Step 2 ONLY: Execute on SpokePool`, + }; + transactions.push(executeTx); + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization (Execute Message Only)", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); + } catch (error) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, + message: `Failed to prepare executeMessage transaction for partially finalized nonce ${nonce.toString()}`, + error: error, + l1Event: { txHash: l1Event.transactionHash, nonce: nonce.toString(), target: l1Event.args.target }, + }); + continue; + } + } + + // --- Process messages needing proof and execution --- + for (const proof of successfulProofs) { + if (!sp1HeliosContract) { + logger.error({ + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, + message: `SP1Helios contract instance not available. Skipping full finalization for nonce ${proof.sourceNonce.toString()}`, + }); + continue; // Skip this proof if contract is missing + } try { // Ensure the hex strings have the '0x' prefix, adding it only if missing. const proofBytes = proof.proofData.proof.startsWith("0x") ? proof.proofData.proof : "0x" + proof.proofData.proof; @@ -639,18 +854,7 @@ async function generateHeliosTxns( destinationChainId: l2ChainId, }); - logger.debug({ - message: `SpokePool address for executeMessage: ${spokePoolContract.address}`, - nonce: proof.sourceNonce.toString(), - }); - // 2. SpokePool.executeMessage transaction - const universalSpokePoolContract = new ethers.Contract( - spokePoolContract.address, - [...UNIVERSAL_SPOKE_ABI], - spokePoolContract.signer - ); - const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; const executeTx: AugmentedTransaction = { contract: universalSpokePoolContract, @@ -664,7 +868,6 @@ async function generateHeliosTxns( canFailInSimulation: true, message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, }; - // todo: uncomment when we're working with SpokePool that respects the set HubPoolStore address. Otherwise txns will just revert. transactions.push(executeTx); crossChainMessages.push({ type: "misc", @@ -683,10 +886,12 @@ async function generateHeliosTxns( } } + const totalFinalizations = successfulProofs.length + needsExecutionOnlyMessages.length; logger.info({ at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, - message: `Generated ${transactions.length} transactions (${transactions.length / 2} finalizations).`, - proofNoncesFinalized: proofsToSubmit.map((p) => p.sourceNonce.toString()), + message: `Generated ${transactions.length} transactions for ${totalFinalizations} finalizations (${successfulProofs.length} full, ${needsExecutionOnlyMessages.length} exec only).`, + proofNoncesFinalized: successfulProofs.map((p) => p.sourceNonce.toString()), + execOnlyNoncesFinalized: needsExecutionOnlyMessages.map((m) => m.l1Event.args.nonce.toString()), }); return { callData: transactions, crossChainMessages: crossChainMessages }; From c3da00a8c16ede5246cd7923e99902fe9e03036c Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 14:57:48 -0700 Subject: [PATCH 15/31] remove all BSC TEST crutches; adjust log severity in helios.ts Signed-off-by: Ihor Farion --- src/common/ClientHelper.ts | 23 +---- src/common/ContractAddresses.ts | 10 +-- src/finalizer/index.ts | 4 - src/finalizer/utils/helios.ts | 150 ++++++++++++++------------------ 4 files changed, 71 insertions(+), 116 deletions(-) diff --git a/src/common/ClientHelper.ts b/src/common/ClientHelper.ts index e4273468db..19e5ae12e3 100644 --- a/src/common/ClientHelper.ts +++ b/src/common/ClientHelper.ts @@ -204,18 +204,7 @@ export async function constructSpokePoolClientsWithStartBlocks( const spokePoolSigners = await getSpokePoolSigners(baseSigner, enabledChains); const spokePools = await Promise.all( enabledChains.map(async (chainId) => { - let spokePoolAddr: string; - // ---- START BSC TEST CODE ---- - /* - todo: this is the only way to test finalizations without a mock HubPool contract - *Theoretically*, we could have some devnet setup with whatever we want. - */ - if (chainId == CHAIN_IDs.BSC) { - spokePoolAddr = "0x4e8E101924eDE233C13e2D8622DC8aED2872d505"; - } else { - spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]); - } - // ---- END BSC TEST CODE ---- + const spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]); // TODO: initialize using typechain factory after V3.5 migration. // const spokePoolContract = SpokePool.connect(spokePoolAddr, spokePoolSigners[chainId]); const spokePoolContract = new ethers.Contract( @@ -223,15 +212,7 @@ export async function constructSpokePoolClientsWithStartBlocks( [...SpokePool.abi, ...V3_SPOKE_POOL_ABI], spokePoolSigners[chainId] ); - let registrationBlock: number; - // ---- START BSC TEST CODE ---- - if (chainId == CHAIN_IDs.BSC) { - registrationBlock = 48762336; - } else { - registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]); - } - // ---- END BSC TEST CODE ---- - + const registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]); return { chainId, contract: spokePoolContract, registrationBlock }; }) ); diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index 3c6e46b6c2..b2ced1ded4 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -202,10 +202,7 @@ export const CONTRACT_ADDRESSES: { abi: HUB_POOL_ABI, }, hubPoolStore: { - // ---- START BSC TEST CODE ---- - // ! todo: mock hubPool controlled by dev wallet - address: "0xdD6Fa55b12aA2a937BA053d610D76f20cC235c09", - // ---- END BSC TEST CODE ---- + address: "0x1Ace3BbD69b63063F859514Eca29C9BDd8310E61", abi: HUB_POOL_STORE_ABI, }, blastBridge: { @@ -260,10 +257,7 @@ export const CONTRACT_ADDRESSES: { }, [CHAIN_IDs.BSC]: { sp1Helios: { - // ---- START BSC TEST CODE ---- - // ! todo: this uses a mock proof verifier - address: "0x6999526e507Cc3b03b180BbE05E1Ff938259A874", - // ---- END BSC TEST CODE ---- + address: "0xcdf08CB3d3436c3c21F277b6AD45E3D7aB1Ce12F", abi: SP1_HELIOS_ABI, }, }, diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index cfb7cbb799..d18932a8df 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -534,10 +534,6 @@ export class FinalizerConfig extends DataworkerConfig { export async function runFinalizer(_logger: winston.Logger, baseSigner: Signer): Promise { logger = _logger; - // ---- START BSC TEST CODE ---- - logger.level = "info"; - // ---- END BSC TEST CODE ---- - // Same config as Dataworker for now. const config = new FinalizerConfig(process.env); const profiler = new Profiler({ diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 001a5809c6..3b665a5941 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -123,7 +123,7 @@ export async function heliosL1toL2Finalizer( ); if (!pendingMessages || pendingMessages.length === 0) { - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, message: "No pending Helios messages found requiring action.", }); @@ -134,7 +134,7 @@ export async function heliosL1toL2Finalizer( const needsProofAndExecution = pendingMessages.filter((m) => m.status === "NeedsProofAndExecution"); const needsExecutionOnly = pendingMessages.filter((m) => m.status === "NeedsExecutionOnly"); - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, message: `Identified ${pendingMessages.length} total pending messages.`, counts: { @@ -155,7 +155,7 @@ export async function heliosL1toL2Finalizer( l1ChainId ); if (proofsToSubmit.length === 0) { - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, message: "No successful proofs retrieved for messages needing full finalization.", }); @@ -165,21 +165,14 @@ export async function heliosL1toL2Finalizer( // --- Step 3: Generate Multicall Data from Proofs and Partially Finalized Messages --- if (proofsToSubmit.length === 0 && needsExecutionOnly.length === 0) { - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, message: "No proofs obtained and no messages need execution only. Nothing to submit.", }); return { callData: [], crossChainMessages: [] }; } - return generateHeliosTxns( - logger, - proofsToSubmit, - needsExecutionOnly, // Pass messages needing only execution - l1ChainId, - l2ChainId, - l2SpokePoolClient - ); + return generateHeliosTxns(logger, proofsToSubmit, needsExecutionOnly, l1ChainId, l2ChainId, l2SpokePoolClient); } // ================================== @@ -254,7 +247,7 @@ async function identifyPendingHeliosMessages( // Log a warning for partially finalized messages logger.warn({ at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, - message: `Message requires execution only (already verified). Will generate SpokePool.executeMessage tx.`, + message: `Message requires execution only (already verified in SP1Helios). Will generate SpokePool.executeMessage tx.`, l1TxHash: l1Event.transactionHash, nonce: nonce.toString(), storageSlot: expectedStorageSlot, @@ -273,8 +266,7 @@ async function identifyPendingHeliosMessages( // If isExecuted is true, the message is fully finalized, do nothing. } - // todo? Change to debug - logger.info({ + logger.debug({ at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, message: `Finished identifying pending messages.`, totalL1StoredCallData: relevantStoredCallDataEvents.length, @@ -335,25 +327,20 @@ async function getRelevantL1Events( args: log.args as StoredCallDataEventArgs, })); - logger.info({ - at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, - message: `Found ${events.length} StoredCallData events on L1 (${l1ChainId})`, - }); - const relevantStoredCallDataEvents = events.filter( (event) => compareAddressesSimple(event.args.target, l2SpokePoolAddress) || compareAddressesSimple(event.args.target, ethers.constants.AddressZero) ); - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, - message: `Filtered ${events.length} StoredCallData events down to ${relevantStoredCallDataEvents.length} relevant targets (${l2SpokePoolAddress} or zero address).`, + message: `Found ${relevantStoredCallDataEvents.length} StoredCallData events on L1 (${l1ChainId})`, }); return relevantStoredCallDataEvents; } catch (error) { - logger.error({ + logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, message: `Failed to query StoredCallData events from L1 (${l1ChainId})`, hubPoolStoreAddress: hubPoolStoreInfo.address, @@ -373,12 +360,11 @@ async function getL2VerifiedSlotsMap( const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); if (!sp1HeliosAddress || !sp1HeliosAbi) { - // todo? Consider erroring here - logger.warn({ + logger.error({ at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, message: `SP1Helios contract not found for destination chain ${l2ChainId}. Cannot verify Helios messages.`, }); - return new Map(); // Return empty map if contract not found, allows process to continue if needed elsewhere + return null; } const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); @@ -390,14 +376,6 @@ async function getL2VerifiedSlotsMap( const storageVerifiedFilter = sp1HeliosContract.filters.StorageSlotVerified(); try { - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, - message: `Querying StorageSlotVerified events on L2 (${l2ChainId})`, - sp1HeliosAddress, - fromBlock: l2SearchConfig.fromBlock, - toBlock: l2SearchConfig.toBlock, - }); - const rawLogs = await paginatedEventQuery(sp1HeliosContract, storageVerifiedFilter, l2SearchConfig); // Explicitly cast logs to the correct type @@ -406,9 +384,12 @@ async function getL2VerifiedSlotsMap( args: log.args as StorageSlotVerifiedEventArgs, })); - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, message: `Found ${events.length} StorageSlotVerified events on L2 (${l2ChainId})`, + sp1HeliosAddress, + fromBlock: l2SearchConfig.fromBlock, + toBlock: l2SearchConfig.toBlock, }); // Store events in a map keyed by the storage slot (key) @@ -426,7 +407,7 @@ async function getL2VerifiedSlotsMap( }); return verifiedSlotsMap; } catch (error) { - logger.error({ + logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, message: `Failed to query StorageSlotVerified events from L2 (${l2ChainId})`, sp1HeliosAddress, @@ -455,14 +436,6 @@ async function getL2RelayedNonces( const relayedCallDataFilter = spokePoolContract.filters.RelayedCallData(); try { - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getL2RelayedNonces:${l2ChainId}`, - message: `Querying RelayedCallData events on L2 SpokePool (${l2ChainId})`, - spokePoolAddress: l2SpokePoolAddress, - fromBlock: l2SearchConfig.fromBlock, - toBlock: l2SearchConfig.toBlock, - }); - const rawLogs = await paginatedEventQuery(spokePoolContract, relayedCallDataFilter, l2SearchConfig); const events: RelayedCallDataEvent[] = rawLogs.map((log) => ({ @@ -470,9 +443,12 @@ async function getL2RelayedNonces( args: log.args as RelayedCallDataEventArgs, })); - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:getL2RelayedNonces:${l2ChainId}`, message: `Found ${events.length} RelayedCallData events on L2 (${l2ChainId})`, + spokePoolAddress: l2SpokePoolAddress, + fromBlock: l2SearchConfig.fromBlock, + toBlock: l2SearchConfig.toBlock, }); // Return a Set of nonces (as strings for easy comparison) @@ -526,10 +502,9 @@ async function processUnfinalizedHeliosMessages( const hubPoolStoreAddress = hubPoolStoreInfo.address; const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); if (!sp1HeliosAddress || !sp1HeliosAbi) { - logger.warn({ - // Warn because maybe other finalizers can run + logger.error({ at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - message: `SP1Helios contract not found for L2 chain ${l2ChainId}. Cannot get head/header.`, + message: `SP1Helios contract not found for L2 chain ${l2ChainId}.`, }); return []; } @@ -546,13 +521,13 @@ async function processUnfinalizedHeliosMessages( if (!currentHeader || currentHeader === ethers.constants.HashZero) { throw new Error(`Invalid header found for head ${currentHead}`); } - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, message: `Using SP1Helios head ${currentHead} and header ${currentHeader} for proof requests.`, sp1HeliosAddress, }); } catch (error) { - logger.error({ + logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, message: `Failed to read current head/header from SP1Helios contract ${sp1HeliosAddress}`, error, @@ -562,7 +537,7 @@ async function processUnfinalizedHeliosMessages( const successfulProofs: SuccessfulProof[] = []; - // todo? Can be optimized with Promise.All + // todo? Can use Promise.All if we really want to // Process messages one by one for (const pendingMessage of unfinalizedMessages) { const l1Event = pendingMessage.l1Event; // Extract the L1 event @@ -604,14 +579,14 @@ async function processUnfinalizedHeliosMessages( // 1. Try to get proof if (getError && axios.isAxiosError(getError) && getError.response?.status === 404) { // 1a. NOTFOUND -> Request proof - logger.info({ ...logContext, message: "Proof not found (404), requesting...", proofId }); + logger.debug({ ...logContext, message: "Proof not found (404), requesting...", proofId }); try { const requestProofUrl = `${apiBaseUrl}/api/proofs`; await axios.post(requestProofUrl, apiRequest); - logger.info({ ...logContext, message: "Proof requested successfully.", proofId }); + logger.debug({ ...logContext, message: "Proof requested successfully.", proofId }); // Exit flow for this message, will check again next run } catch (postError: any) { - logger.error({ + logger.warn({ ...logContext, message: "Failed to request proof after 404.", proofId, @@ -623,7 +598,7 @@ async function processUnfinalizedHeliosMessages( } } else if (getError) { // Other error during GET - logger.error({ + logger.warn({ ...logContext, message: "Failed to get proof state.", proofId, @@ -636,23 +611,23 @@ async function processUnfinalizedHeliosMessages( // GET successful, check status if (proofState.status === "pending") { // 1b. SUCCESS ("pending") -> Log and exit flow - logger.info({ ...logContext, message: "Proof generation is pending.", proofId }); + logger.debug({ ...logContext, message: "Proof generation is pending.", proofId }); // Exit flow for this message } else if (proofState.status === "errored") { // 1c. SUCCESS ("errored") -> Log high severity, request again, exit flow logger.error({ // Use error level log ...logContext, - message: "Proof generation errored. Requesting again.", + message: "Proof generation errored on ZK API side. Requesting again.", proofId, errorMessage: proofState.error_message, }); try { const requestProofUrl = `${apiBaseUrl}/api/proofs`; await axios.post(requestProofUrl, apiRequest); - logger.info({ ...logContext, message: "Errored proof requested again successfully.", proofId }); + logger.debug({ ...logContext, message: "Errored proof requested again successfully.", proofId }); } catch (postError: any) { - logger.error({ + logger.warn({ ...logContext, message: "Failed to re-request errored proof.", proofId, @@ -665,7 +640,7 @@ async function processUnfinalizedHeliosMessages( } else if (proofState.status === "success") { // 1d. SUCCESS ("success") -> Collect proof data for later processing if (proofState.update_calldata) { - logger.info({ ...logContext, message: "Proof successfully retrieved.", proofId }); + logger.debug({ ...logContext, message: "Proof successfully retrieved.", proofId }); successfulProofs.push({ proofData: proofState.update_calldata, sourceNonce: l1Event.args.nonce, // Use nonce from L1 event @@ -673,7 +648,7 @@ async function processUnfinalizedHeliosMessages( sourceMessageData: l1Event.args.data, // Use data from L1 event }); } else { - logger.error({ + logger.warn({ ...logContext, message: "Proof status is success but update_calldata is missing.", proofId, @@ -682,9 +657,9 @@ async function processUnfinalizedHeliosMessages( // Treat as error, exit flow for this message } } else { - logger.error({ + logger.warn({ ...logContext, - message: "Received unexpected proof status.", + message: "Received unexpected proof status. Will try again next run.", proofId, status: proofState.status, }); @@ -693,7 +668,7 @@ async function processUnfinalizedHeliosMessages( } // Implicitly exits flow for the message if none of the success conditions were met } catch (processingError) { - logger.error({ + logger.warn({ ...logContext, message: "Error processing unfinalized message for proof.", error: processingError, @@ -707,8 +682,8 @@ async function processUnfinalizedHeliosMessages( /** --- Generate Multicall Data --- */ async function generateHeliosTxns( logger: winston.Logger, - successfulProofs: SuccessfulProof[], // Renamed from proofsToSubmit - needsExecutionOnlyMessages: PendingHeliosMessage[], // Added new input + successfulProofs: SuccessfulProof[], + needsExecutionOnlyMessages: PendingHeliosMessage[], l1ChainId: number, l2ChainId: number, l2SpokePoolClient: SpokePoolClient @@ -717,10 +692,8 @@ async function generateHeliosTxns( const crossChainMessages: CrossChainMessage[] = []; const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); - // SP1Helios contract is only needed for the 'update' transaction part let sp1HeliosContract: ethers.Contract | null = null; if (sp1HeliosAddress && sp1HeliosAbi && successfulProofs.length > 0) { - // Only create if needed sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2SpokePoolClient.spokePool.signer); } else if (successfulProofs.length > 0) { logger.error({ @@ -731,11 +704,10 @@ async function generateHeliosTxns( // We might still be able to process needsExecutionOnly, so don't return yet } - // Universal Spoke Pool contract is needed for 'executeMessage' const spokePoolAddress = l2SpokePoolClient.spokePool.address; const universalSpokePoolContract = new ethers.Contract( spokePoolAddress, - [...UNIVERSAL_SPOKE_ABI], // Ensure ABI has executeMessage + [...UNIVERSAL_SPOKE_ABI], l2SpokePoolClient.spokePool.signer ); @@ -743,9 +715,11 @@ async function generateHeliosTxns( for (const message of needsExecutionOnlyMessages) { const { l1Event, verifiedHead } = message; const nonce = l1Event.args.nonce; - const sourceMessageData = l1Event.args.data; + const l1Target = l1Event.args.target; // Get target from L1 event + const l1Data = l1Event.args.data; // Get data from L1 event if (!verifiedHead) { + // @dev This shouldn't happen. If it does, there's a bug that needs fixing logger.error({ at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, message: `Logic error: Message ${nonce.toString()} needs execution only but verifiedHead is missing. Skipping.`, @@ -755,8 +729,8 @@ async function generateHeliosTxns( } try { + // @dev Warn about messages that require only half of finalization. Means that either a tx from prev. run got stuck or failed or something else weird happened logger.warn({ - // Log warning again here when actually generating the tx at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, message: `Generating SpokePool.executeMessage ONLY for partially finalized message.`, nonce: nonce.toString(), @@ -764,14 +738,18 @@ async function generateHeliosTxns( verifiedHead: verifiedHead.toString(), }); - const executeArgs = [nonce, sourceMessageData, verifiedHead]; // Use verifiedHead from the message + // --- Encode the message parameter --- + const encodedMessage = ethers.utils.defaultAbiCoder.encode(["address", "bytes"], [l1Target, l1Data]); + // ------------------------------------ + + const executeArgs = [nonce, encodedMessage, verifiedHead]; // Use encodedMessage const executeTx: AugmentedTransaction = { contract: universalSpokePoolContract, chainId: l2ChainId, method: "executeMessage", args: executeArgs, - unpermissioned: true, // Assuming anyone can call executeMessage if requirements met - canFailInSimulation: true, // Still true, depends on state potentially changing + unpermissioned: true, + canFailInSimulation: true, message: `Finalize Helios msg (HubPoolStore nonce ${nonce.toString()}) - Step 2 ONLY: Execute on SpokePool`, }; transactions.push(executeTx); @@ -782,7 +760,8 @@ async function generateHeliosTxns( destinationChainId: l2ChainId, }); } catch (error) { - logger.error({ + // most likely encoding error. Not sure if this can ever happen actually + logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, message: `Failed to prepare executeMessage transaction for partially finalized nonce ${nonce.toString()}`, error: error, @@ -823,7 +802,7 @@ async function generateHeliosTxns( slots: decodedResult[8].map((slot: any[]) => ({ key: slot[0], value: slot[1], contractAddress: slot[2] })), }; } catch (decodeError) { - logger.error({ + logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:decodePublicValues:${l2ChainId}`, message: `Failed to decode public_values for nonce ${proof.sourceNonce.toString()}`, publicValues: publicValuesBytes, @@ -833,8 +812,7 @@ async function generateHeliosTxns( } // 1. SP1Helios.update transaction - // todo: Change "0x" to `proofBytes` for production when not using mock verifier - const updateArgs = ["0x", publicValuesBytes]; // Use actual args + const updateArgs = ["0x", publicValuesBytes]; // todo: Change "0x" to `proofBytes` for production const updateTx: AugmentedTransaction = { contract: sp1HeliosContract, chainId: l2ChainId, @@ -855,7 +833,13 @@ async function generateHeliosTxns( }); // 2. SpokePool.executeMessage transaction - const executeArgs = [proof.sourceNonce, proof.sourceMessageData, decodedOutputs.newHead]; + // --- Encode the message parameter --- + const l1Target = proof.target; // Get target from SuccessfulProof + const l1Data = proof.sourceMessageData; // Get data from SuccessfulProof + const encodedMessage = ethers.utils.defaultAbiCoder.encode(["address", "bytes"], [l1Target, l1Data]); + // ------------------------------------ + + const executeArgs = [proof.sourceNonce, encodedMessage, decodedOutputs.newHead]; // Use encodedMessage const executeTx: AugmentedTransaction = { contract: universalSpokePoolContract, chainId: l2ChainId, @@ -864,7 +848,7 @@ async function generateHeliosTxns( // gasLimit: BigNumber.from(1000000), // todo: testing // todo: check this unpermissioned: true, - // @dev Notice, in order for this tx to succeed in simulation, the SP1Helios.update(..) would be required to have updated the blockchain state already. Which is not how our sim. is done in index.ts + // @dev Simulation of `executeMessage` depends on prior state update via SP1Helios.update canFailInSimulation: true, message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, }; @@ -876,7 +860,7 @@ async function generateHeliosTxns( destinationChainId: l2ChainId, }); } catch (error) { - logger.error({ + logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, message: `Failed to prepare transaction for proof of nonce ${proof.sourceNonce.toString()}`, error: error, @@ -887,7 +871,7 @@ async function generateHeliosTxns( } const totalFinalizations = successfulProofs.length + needsExecutionOnlyMessages.length; - logger.info({ + logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, message: `Generated ${transactions.length} transactions for ${totalFinalizations} finalizations (${successfulProofs.length} full, ${needsExecutionOnlyMessages.length} exec only).`, proofNoncesFinalized: successfulProofs.map((p) => p.sourceNonce.toString()), From cde34f04345c85c60cff1052ca9a284607046b6d Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 14:59:40 -0700 Subject: [PATCH 16/31] polish Signed-off-by: Ihor Farion --- src/common/ClientHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/ClientHelper.ts b/src/common/ClientHelper.ts index 19e5ae12e3..a6ff304662 100644 --- a/src/common/ClientHelper.ts +++ b/src/common/ClientHelper.ts @@ -13,7 +13,6 @@ import { isDefined, getRedisCache, getArweaveJWKSigner, - CHAIN_IDs, } from "../utils"; import { HubPoolClient, MultiCallerClient, ConfigStoreClient, SpokePoolClient } from "../clients"; import { CommonConfig } from "./Config"; @@ -200,6 +199,7 @@ export async function constructSpokePoolClientsWithStartBlocks( const blockFinder = undefined; const redis = await getRedisCache(logger); + // Set up Spoke signers and connect them to spoke pool contract objects: const spokePoolSigners = await getSpokePoolSigners(baseSigner, enabledChains); const spokePools = await Promise.all( From 381fc4d62ca80eb5d929ee57f180a62f956792a3 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 15:04:45 -0700 Subject: [PATCH 17/31] fix lint + use real proof instead of 0x on .update tx submission Signed-off-by: Ihor Farion --- .prettierignore | 1 + src/finalizer/index.ts | 2 +- src/finalizer/utils/helios.ts | 17 ++++++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.prettierignore b/.prettierignore index 373f496d81..dc944472a5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ coverage* gasReporterOutput.json dist typechain* +addresses.json \ No newline at end of file diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index d18932a8df..e87c2ef3ff 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -341,7 +341,7 @@ export async function finalize( } } else { // Individual transaction simulation flow - const [{ reason, succeed, transaction: _ }] = await txnClient.simulate([_txn]); + const [{ reason, succeed }] = await txnClient.simulate([_txn]); if (succeed) { return true; } else { diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 3b665a5941..d308887f8a 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -1,11 +1,10 @@ import { ethers } from "ethers"; -import { HubPoolClient, SpokePoolClient } from "../../clients"; +import { HubPoolClient, SpokePoolClient, AugmentedTransaction } from "../../clients"; import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple } from "../../utils"; import { FinalizerPromise, CrossChainMessage } from "../types"; import { Log } from "../../interfaces"; import { CONTRACT_ADDRESSES } from "../../common"; import axios from "axios"; -import { AugmentedTransaction } from "../../clients"; import UNIVERSAL_SPOKE_ABI from "../../common/abi/Universal_SpokePool.json"; // Define interfaces for the event arguments for clarity @@ -107,6 +106,7 @@ export async function heliosL1toL2Finalizer( hubPoolClient: HubPoolClient, l2SpokePoolClient: SpokePoolClient, l1SpokePoolClient: SpokePoolClient, + // eslint-disable-next-line @typescript-eslint/no-unused-vars _senderAddresses: string[] ): Promise { const l1ChainId = hubPoolClient.chainId; @@ -247,7 +247,8 @@ async function identifyPendingHeliosMessages( // Log a warning for partially finalized messages logger.warn({ at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, - message: `Message requires execution only (already verified in SP1Helios). Will generate SpokePool.executeMessage tx.`, + message: + "Message requires execution only (already verified in SP1Helios). Will generate SpokePool.executeMessage tx.", l1TxHash: l1Event.transactionHash, nonce: nonce.toString(), storageSlot: expectedStorageSlot, @@ -268,7 +269,7 @@ async function identifyPendingHeliosMessages( logger.debug({ at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, - message: `Finished identifying pending messages.`, + message: "Finished identifying pending messages.", totalL1StoredCallData: relevantStoredCallDataEvents.length, totalL2VerifiedSlots: verifiedSlotsMap.size, totalL2RelayedNonces: relayedNonces.size, @@ -477,7 +478,9 @@ async function processUnfinalizedHeliosMessages( ): Promise { // Filter within the function just in case, though the caller should have already filtered const unfinalizedMessages = messagesToProcess.filter((m) => m.status === "NeedsProofAndExecution"); - if (unfinalizedMessages.length === 0) return []; + if (unfinalizedMessages.length === 0) { + return []; + } const l2ChainId = l2SpokePoolClient.chainId; const l2Provider = l2SpokePoolClient.spokePool.provider; @@ -732,7 +735,7 @@ async function generateHeliosTxns( // @dev Warn about messages that require only half of finalization. Means that either a tx from prev. run got stuck or failed or something else weird happened logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, - message: `Generating SpokePool.executeMessage ONLY for partially finalized message.`, + message: "Generating SpokePool.executeMessage ONLY for partially finalized message.", nonce: nonce.toString(), l1TxHash: l1Event.transactionHash, verifiedHead: verifiedHead.toString(), @@ -812,7 +815,7 @@ async function generateHeliosTxns( } // 1. SP1Helios.update transaction - const updateArgs = ["0x", publicValuesBytes]; // todo: Change "0x" to `proofBytes` for production + const updateArgs = [proofBytes, publicValuesBytes]; // Use proofBytes as intended const updateTx: AugmentedTransaction = { contract: sp1HeliosContract, chainId: l2ChainId, From 4f8a0c339f57e60dc389f68d8458e9c2fc86298f Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 15:05:33 -0700 Subject: [PATCH 18/31] add newline to .prettierignore Signed-off-by: Ihor Farion --- .prettierignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index dc944472a5..05b0846ade 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,4 @@ coverage* gasReporterOutput.json dist typechain* -addresses.json \ No newline at end of file +addresses.json From 9bcc28a582bcf2ed3bbd3c0a8f471b9b65926028 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 21:09:01 -0700 Subject: [PATCH 19/31] strip abi files Signed-off-by: Ihor Farion --- src/common/abi/HubPoolStore.json | 31 - src/common/abi/SP1Helios.json | 339 ------- src/common/abi/Universal_SpokePool.json | 1115 +---------------------- src/finalizer/utils/helios.ts | 5 +- 4 files changed, 2 insertions(+), 1488 deletions(-) diff --git a/src/common/abi/HubPoolStore.json b/src/common/abi/HubPoolStore.json index 42d01153fd..c7282ecaad 100644 --- a/src/common/abi/HubPoolStore.json +++ b/src/common/abi/HubPoolStore.json @@ -1,10 +1,4 @@ [ - { - "inputs": [{ "internalType": "address", "name": "_hubPool", "type": "address" }], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { "inputs": [], "name": "NotHubPool", "type": "error" }, { "anonymous": false, "inputs": [ @@ -14,30 +8,5 @@ ], "name": "StoredCallData", "type": "event" - }, - { - "inputs": [], - "name": "hubPool", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "name": "relayMessageCallData", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "target", "type": "address" }, - { "internalType": "bytes", "name": "data", "type": "bytes" }, - { "internalType": "bool", "name": "isAdminSender", "type": "bool" } - ], - "name": "storeRelayMessageCalldata", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" } ] diff --git a/src/common/abi/SP1Helios.json b/src/common/abi/SP1Helios.json index 1bac6e8c52..098b4f7e55 100644 --- a/src/common/abi/SP1Helios.json +++ b/src/common/abi/SP1Helios.json @@ -1,123 +1,4 @@ [ - { - "inputs": [ - { - "components": [ - { "internalType": "bytes32", "name": "executionStateRoot", "type": "bytes32" }, - { "internalType": "uint256", "name": "genesisTime", "type": "uint256" }, - { "internalType": "uint256", "name": "head", "type": "uint256" }, - { "internalType": "bytes32", "name": "header", "type": "bytes32" }, - { "internalType": "bytes32", "name": "heliosProgramVkey", "type": "bytes32" }, - { "internalType": "uint256", "name": "secondsPerSlot", "type": "uint256" }, - { "internalType": "uint256", "name": "slotsPerEpoch", "type": "uint256" }, - { "internalType": "uint256", "name": "slotsPerPeriod", "type": "uint256" }, - { "internalType": "bytes32", "name": "syncCommitteeHash", "type": "bytes32" }, - { "internalType": "address", "name": "verifier", "type": "address" }, - { "internalType": "address[]", "name": "updaters", "type": "address[]" } - ], - "internalType": "struct SP1Helios.InitParams", - "name": "params", - "type": "tuple" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { "inputs": [], "name": "AccessControlBadConfirmation", "type": "error" }, - { - "inputs": [ - { "internalType": "address", "name": "account", "type": "address" }, - { "internalType": "bytes32", "name": "neededRole", "type": "bytes32" } - ], - "name": "AccessControlUnauthorizedAccount", - "type": "error" - }, - { - "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], - "name": "ExecutionStateRootMismatch", - "type": "error" - }, - { - "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], - "name": "NewHeaderMismatch", - "type": "error" - }, - { "inputs": [], "name": "NoUpdatersProvided", "type": "error" }, - { - "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], - "name": "NonIncreasingHead", - "type": "error" - }, - { - "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], - "name": "PreviousHeadTooOld", - "type": "error" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "given", "type": "bytes32" }, - { "internalType": "bytes32", "name": "expected", "type": "bytes32" } - ], - "name": "PreviousHeaderMismatch", - "type": "error" - }, - { - "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], - "name": "PreviousHeaderNotSet", - "type": "error" - }, - { - "inputs": [{ "internalType": "uint256", "name": "period", "type": "uint256" }], - "name": "SyncCommitteeAlreadySet", - "type": "error" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "given", "type": "bytes32" }, - { "internalType": "bytes32", "name": "expected", "type": "bytes32" } - ], - "name": "SyncCommitteeStartMismatch", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "uint256", "name": "slot", "type": "uint256" }, - { "indexed": true, "internalType": "bytes32", "name": "root", "type": "bytes32" } - ], - "name": "HeadUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "previousAdminRole", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "newAdminRole", "type": "bytes32" } - ], - "name": "RoleAdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } - ], - "name": "RoleGranted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } - ], - "name": "RoleRevoked", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -129,157 +10,6 @@ "name": "StorageSlotVerified", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "uint256", "name": "period", "type": "uint256" }, - { "indexed": true, "internalType": "bytes32", "name": "root", "type": "bytes32" } - ], - "name": "SyncCommitteeUpdate", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": true, "internalType": "address", "name": "updater", "type": "address" }], - "name": "UpdaterAdded", - "type": "event" - }, - { - "inputs": [], - "name": "DEFAULT_ADMIN_ROLE", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "GENESIS_TIME", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "MAX_SLOT_AGE", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SECONDS_PER_SLOT", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SLOTS_PER_EPOCH", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SLOTS_PER_PERIOD", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "UPDATER_ROLE", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }, - { "internalType": "address", "name": "contractAddress", "type": "address" }, - { "internalType": "bytes32", "name": "storageSlot", "type": "bytes32" } - ], - "name": "computeStorageKey", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }], - "name": "executionStateRoots", - "outputs": [{ "internalType": "bytes32", "name": "executionStateRoot", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getCurrentEpoch", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], - "name": "getRoleAdmin", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "uint256", "name": "index", "type": "uint256" } - ], - "name": "getRoleMember", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], - "name": "getRoleMemberCount", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }, - { "internalType": "address", "name": "contractAddress", "type": "address" }, - { "internalType": "bytes32", "name": "storageSlot", "type": "bytes32" } - ], - "name": "getStorageSlot", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], - "name": "getSyncCommitteePeriod", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "grantRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "hasRole", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "head", @@ -287,13 +17,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "headTimestamp", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, { "inputs": [{ "internalType": "uint256", "name": "beaconSlot", "type": "uint256" }], "name": "headers", @@ -301,61 +24,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "heliosProgramVkey", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "callerConfirmation", "type": "address" } - ], - "name": "renounceRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "revokeRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "slot", "type": "uint256" }], - "name": "slotTimestamp", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes32", "name": "computedStorageKey", "type": "bytes32" }], - "name": "storageValues", - "outputs": [{ "internalType": "bytes32", "name": "storageValue", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }], - "name": "supportsInterface", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "syncCommitteePeriod", "type": "uint256" }], - "name": "syncCommittees", - "outputs": [{ "internalType": "bytes32", "name": "syncCommitteeHash", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { "internalType": "bytes", "name": "proof", "type": "bytes" }, @@ -365,12 +33,5 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" - }, - { - "inputs": [], - "name": "verifier", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" } ] diff --git a/src/common/abi/Universal_SpokePool.json b/src/common/abi/Universal_SpokePool.json index 14076301b9..aff7beee3d 100644 --- a/src/common/abi/Universal_SpokePool.json +++ b/src/common/abi/Universal_SpokePool.json @@ -1,229 +1,4 @@ [ - { - "inputs": [ - { "internalType": "uint256", "name": "_adminUpdateBufferSeconds", "type": "uint256" }, - { "internalType": "address", "name": "_helios", "type": "address" }, - { "internalType": "address", "name": "_hubPoolStore", "type": "address" }, - { "internalType": "address", "name": "_wrappedNativeTokenAddress", "type": "address" }, - { "internalType": "uint32", "name": "_depositQuoteTimeBuffer", "type": "uint32" }, - { "internalType": "uint32", "name": "_fillDeadlineBuffer", "type": "uint32" }, - { "internalType": "contract IERC20", "name": "_l2Usdc", "type": "address" }, - { "internalType": "contract ITokenMessenger", "name": "_cctpTokenMessenger", "type": "address" } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { "inputs": [], "name": "AdminCallAlreadySet", "type": "error" }, - { "inputs": [], "name": "AdminCallNotValidated", "type": "error" }, - { "inputs": [], "name": "AdminUpdateTooCloseToLastHeliosUpdate", "type": "error" }, - { "inputs": [], "name": "AlreadyExecuted", "type": "error" }, - { "inputs": [], "name": "ClaimedMerkleLeaf", "type": "error" }, - { "inputs": [], "name": "DelegateCallFailed", "type": "error" }, - { "inputs": [], "name": "DepositsArePaused", "type": "error" }, - { "inputs": [], "name": "DisabledRoute", "type": "error" }, - { "inputs": [], "name": "ExpiredFillDeadline", "type": "error" }, - { "inputs": [], "name": "FillsArePaused", "type": "error" }, - { "inputs": [], "name": "InsufficientSpokePoolBalanceToExecuteLeaf", "type": "error" }, - { "inputs": [], "name": "InvalidBytes32", "type": "error" }, - { "inputs": [], "name": "InvalidChainId", "type": "error" }, - { "inputs": [], "name": "InvalidCrossDomainAdmin", "type": "error" }, - { "inputs": [], "name": "InvalidDepositorSignature", "type": "error" }, - { "inputs": [], "name": "InvalidExclusiveRelayer", "type": "error" }, - { "inputs": [], "name": "InvalidFillDeadline", "type": "error" }, - { "inputs": [], "name": "InvalidMerkleLeaf", "type": "error" }, - { "inputs": [], "name": "InvalidMerkleProof", "type": "error" }, - { "inputs": [], "name": "InvalidPayoutAdjustmentPct", "type": "error" }, - { "inputs": [], "name": "InvalidQuoteTimestamp", "type": "error" }, - { "inputs": [], "name": "InvalidRelayerFeePct", "type": "error" }, - { "inputs": [], "name": "InvalidSlowFillRequest", "type": "error" }, - { "inputs": [], "name": "InvalidWithdrawalRecipient", "type": "error" }, - { - "inputs": [{ "internalType": "bytes", "name": "data", "type": "bytes" }], - "name": "LowLevelCallFailed", - "type": "error" - }, - { "inputs": [], "name": "MaxTransferSizeExceeded", "type": "error" }, - { "inputs": [], "name": "MsgValueDoesNotMatchInputAmount", "type": "error" }, - { "inputs": [], "name": "NoRelayerRefundToClaim", "type": "error" }, - { "inputs": [], "name": "NoSlowFillsInExclusivityWindow", "type": "error" }, - { "inputs": [], "name": "NotEOA", "type": "error" }, - { "inputs": [], "name": "NotExclusiveRelayer", "type": "error" }, - { "inputs": [], "name": "NotImplemented", "type": "error" }, - { "inputs": [], "name": "NotTarget", "type": "error" }, - { "inputs": [], "name": "RelayFilled", "type": "error" }, - { "inputs": [], "name": "SlotValueMismatch", "type": "error" }, - { "inputs": [], "name": "WrongERC7683OrderId", "type": "error" }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "address", "name": "previousAdmin", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "newAdmin", "type": "address" } - ], - "name": "AdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": true, "internalType": "address", "name": "beacon", "type": "address" }], - "name": "BeaconUpgraded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "l2TokenAddress", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "refundAddress", "type": "bytes32" }, - { "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }, - { "indexed": true, "internalType": "address", "name": "caller", "type": "address" } - ], - "name": "ClaimedRelayerRefund", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": true, "internalType": "uint256", "name": "rootBundleId", "type": "uint256" }], - "name": "EmergencyDeletedRootBundle", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "address", "name": "originToken", "type": "address" }, - { "indexed": true, "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "indexed": false, "internalType": "bool", "name": "enabled", "type": "bool" } - ], - "name": "EnabledDepositRoute", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "uint256", "name": "amountToReturn", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "chainId", "type": "uint256" }, - { "indexed": false, "internalType": "uint256[]", "name": "refundAmounts", "type": "uint256[]" }, - { "indexed": true, "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, - { "indexed": true, "internalType": "uint32", "name": "leafId", "type": "uint32" }, - { "indexed": false, "internalType": "address", "name": "l2TokenAddress", "type": "address" }, - { "indexed": false, "internalType": "address[]", "name": "refundAddresses", "type": "address[]" }, - { "indexed": false, "internalType": "bool", "name": "deferredRefunds", "type": "bool" }, - { "indexed": false, "internalType": "address", "name": "caller", "type": "address" } - ], - "name": "ExecutedRelayerRefundRoot", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "relayer", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "messageHash", "type": "bytes32" }, - { - "components": [ - { "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "updatedMessageHash", "type": "bytes32" }, - { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, - { "internalType": "enum V3SpokePoolInterface.FillType", "name": "fillType", "type": "uint8" } - ], - "indexed": false, - "internalType": "struct V3SpokePoolInterface.V3RelayExecutionEventInfo", - "name": "relayExecutionInfo", - "type": "tuple" - } - ], - "name": "FilledRelay", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "relayer", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "depositor", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, - { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" }, - { - "components": [ - { "internalType": "address", "name": "updatedRecipient", "type": "address" }, - { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, - { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, - { "internalType": "enum V3SpokePoolInterface.FillType", "name": "fillType", "type": "uint8" } - ], - "indexed": false, - "internalType": "struct V3SpokePoolInterface.LegacyV3RelayExecutionEventInfo", - "name": "relayExecutionInfo", - "type": "tuple" - } - ], - "name": "FilledV3Relay", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "indexed": false, "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "indexed": true, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "FundsDeposited", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": false, "internalType": "uint8", "name": "version", "type": "uint8" }], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": false, "internalType": "bool", "name": "isPaused", "type": "bool" }], - "name": "PausedDeposits", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": false, "internalType": "bool", "name": "isPaused", "type": "bool" }], - "name": "PausedFills", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -233,377 +8,6 @@ "name": "RelayedCallData", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, - { "indexed": true, "internalType": "bytes32", "name": "relayerRefundRoot", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "slowRelayRoot", "type": "bytes32" } - ], - "name": "RelayedRootBundle", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "messageHash", "type": "bytes32" } - ], - "name": "RequestedSlowFill", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "indexed": true, "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, - { "indexed": false, "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, - { "indexed": false, "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } - ], - "name": "RequestedSpeedUpDeposit", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, - { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, - { "indexed": true, "internalType": "address", "name": "depositor", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "updatedRecipient", "type": "address" }, - { "indexed": false, "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, - { "indexed": false, "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } - ], - "name": "RequestedSpeedUpV3Deposit", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "depositor", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, - { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "RequestedV3SlowFill", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": true, "internalType": "address", "name": "newWithdrawalRecipient", "type": "address" }], - "name": "SetWithdrawalRecipient", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": true, "internalType": "address", "name": "newAdmin", "type": "address" }], - "name": "SetXDomainAdmin", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "uint256", "name": "amountToReturn", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "chainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint32", "name": "leafId", "type": "uint32" }, - { "indexed": true, "internalType": "bytes32", "name": "l2TokenAddress", "type": "bytes32" }, - { "indexed": false, "internalType": "address", "name": "caller", "type": "address" } - ], - "name": "TokensBridged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [{ "indexed": true, "internalType": "address", "name": "implementation", "type": "address" }], - "name": "Upgraded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "indexed": false, "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "indexed": true, "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "indexed": true, "internalType": "uint32", "name": "depositId", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "indexed": false, "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "indexed": true, "internalType": "address", "name": "depositor", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" }, - { "indexed": false, "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, - { "indexed": false, "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "V3FundsDeposited", - "type": "event" - }, - { - "inputs": [], - "name": "ADMIN_UPDATE_BUFFER", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "", "type": "address" }, - { "internalType": "uint256", "name": "", "type": "uint256" } - ], - "name": "DEPRECATED_enabledDepositRoutes", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "EMPTY_RELAYER", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "EMPTY_REPAYMENT_CHAIN_ID", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "HUB_POOL_STORE_CALLDATA_MAPPING_SLOT_INDEX", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "INFINITE_FILL_DEADLINE", - "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "MAX_EXCLUSIVITY_PERIOD_SECONDS", - "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "MAX_TRANSFER_SIZE", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "UPDATE_BYTES32_DEPOSIT_DETAILS_HASH", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint32", "name": "_initialDepositId", "type": "uint32" }, - { "internalType": "address", "name": "_crossDomainAdmin", "type": "address" }, - { "internalType": "address", "name": "_withdrawalRecipient", "type": "address" } - ], - "name": "__SpokePool_init", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes", "name": "_message", "type": "bytes" }], - "name": "adminExecuteMessage", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "cctpTokenMessenger", - "outputs": [{ "internalType": "contract ITokenMessenger", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "cctpV2", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "chainId", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "l2TokenAddress", "type": "bytes32" }, - { "internalType": "bytes32", "name": "refundAddress", "type": "bytes32" } - ], - "name": "claimRelayerRefund", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "crossDomainAdmin", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "deposit", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "recipient", "type": "address" }, - { "internalType": "address", "name": "originToken", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" }, - { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "internalType": "int64", "name": "relayerFeePct", "type": "int64" }, - { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" }, - { "internalType": "uint256", "name": "", "type": "uint256" } - ], - "name": "depositDeprecated_5947912356", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "depositor", "type": "address" }, - { "internalType": "address", "name": "recipient", "type": "address" }, - { "internalType": "address", "name": "originToken", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" }, - { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "internalType": "int64", "name": "relayerFeePct", "type": "int64" }, - { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" }, - { "internalType": "uint256", "name": "", "type": "uint256" } - ], - "name": "depositFor", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "uint32", "name": "fillDeadlineOffset", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "depositNow", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "depositQuoteTimeBuffer", - "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "depositor", "type": "address" }, - { "internalType": "address", "name": "recipient", "type": "address" }, - { "internalType": "address", "name": "inputToken", "type": "address" }, - { "internalType": "address", "name": "outputToken", "type": "address" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, - { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "depositV3", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "depositor", "type": "address" }, - { "internalType": "address", "name": "recipient", "type": "address" }, - { "internalType": "address", "name": "inputToken", "type": "address" }, - { "internalType": "address", "name": "outputToken", "type": "address" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, - { "internalType": "uint32", "name": "fillDeadlineOffset", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "depositV3Now", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "rootBundleId", "type": "uint256" }], - "name": "emergencyDeleteRootBundle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { "internalType": "uint256", "name": "_messageNonce", "type": "uint256" }, @@ -614,522 +18,5 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" - }, - { - "inputs": [ - { "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, - { - "components": [ - { "internalType": "uint256", "name": "amountToReturn", "type": "uint256" }, - { "internalType": "uint256", "name": "chainId", "type": "uint256" }, - { "internalType": "uint256[]", "name": "refundAmounts", "type": "uint256[]" }, - { "internalType": "uint32", "name": "leafId", "type": "uint32" }, - { "internalType": "address", "name": "l2TokenAddress", "type": "address" }, - { "internalType": "address[]", "name": "refundAddresses", "type": "address[]" } - ], - "internalType": "struct SpokePoolInterface.RelayerRefundLeaf", - "name": "relayerRefundLeaf", - "type": "tuple" - }, - { "internalType": "bytes32[]", "name": "proof", "type": "bytes32[]" } - ], - "name": "executeRelayerRefundLeaf", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "components": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "internalType": "struct V3SpokePoolInterface.V3RelayData", - "name": "relayData", - "type": "tuple" - }, - { "internalType": "uint256", "name": "chainId", "type": "uint256" }, - { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" } - ], - "internalType": "struct V3SpokePoolInterface.V3SlowFill", - "name": "slowFillLeaf", - "type": "tuple" - }, - { "internalType": "uint32", "name": "rootBundleId", "type": "uint32" }, - { "internalType": "bytes32[]", "name": "proof", "type": "bytes32[]" } - ], - "name": "executeSlowRelayLeaf", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "name": "executedMessages", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "orderId", "type": "bytes32" }, - { "internalType": "bytes", "name": "originData", "type": "bytes" }, - { "internalType": "bytes", "name": "fillerData", "type": "bytes" } - ], - "name": "fill", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "fillDeadlineBuffer", - "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "internalType": "struct V3SpokePoolInterface.V3RelayData", - "name": "relayData", - "type": "tuple" - }, - { "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, - { "internalType": "bytes32", "name": "repaymentAddress", "type": "bytes32" } - ], - "name": "fillRelay", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "internalType": "struct V3SpokePoolInterface.V3RelayData", - "name": "relayData", - "type": "tuple" - }, - { "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" }, - { "internalType": "bytes32", "name": "repaymentAddress", "type": "bytes32" }, - { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, - { "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, - { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, - { "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } - ], - "name": "fillRelayWithUpdatedDeposit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "name": "fillStatuses", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { "internalType": "address", "name": "depositor", "type": "address" }, - { "internalType": "address", "name": "recipient", "type": "address" }, - { "internalType": "address", "name": "exclusiveRelayer", "type": "address" }, - { "internalType": "address", "name": "inputToken", "type": "address" }, - { "internalType": "address", "name": "outputToken", "type": "address" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "internalType": "uint32", "name": "depositId", "type": "uint32" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "internalType": "struct V3SpokePoolInterface.V3RelayDataLegacy", - "name": "relayData", - "type": "tuple" - }, - { "internalType": "uint256", "name": "repaymentChainId", "type": "uint256" } - ], - "name": "fillV3Relay", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "getCurrentTime", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "l2TokenAddress", "type": "address" }, - { "internalType": "address", "name": "refundAddress", "type": "address" } - ], - "name": "getRelayerRefund", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "_nonce", "type": "uint256" }], - "name": "getSlotKey", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "msgSender", "type": "address" }, - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "uint256", "name": "depositNonce", "type": "uint256" } - ], - "name": "getUnsafeDepositId", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "internalType": "struct V3SpokePoolInterface.V3RelayData", - "name": "relayData", - "type": "tuple" - } - ], - "name": "getV3RelayHash", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "helios", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "hubPoolStore", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint32", "name": "_initialDepositId", "type": "uint32" }, - { "internalType": "address", "name": "_crossDomainAdmin", "type": "address" }, - { "internalType": "address", "name": "_withdrawalRecipient", "type": "address" } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes[]", "name": "data", "type": "bytes[]" }], - "name": "multicall", - "outputs": [{ "internalType": "bytes[]", "name": "results", "type": "bytes[]" }], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "numberOfDeposits", - "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "bool", "name": "pause", "type": "bool" }], - "name": "pauseDeposits", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bool", "name": "pause", "type": "bool" }], - "name": "pauseFills", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "pausedDeposits", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pausedFills", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proxiableUUID", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "recipientCircleDomainId", - "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "relayerRefundRoot", "type": "bytes32" }, - { "internalType": "bytes32", "name": "slowRelayRoot", "type": "bytes32" } - ], - "name": "relayRootBundle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "", "type": "address" }, - { "internalType": "address", "name": "", "type": "address" } - ], - "name": "relayerRefund", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { "inputs": [], "name": "renounceOwnership", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "components": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "originChainId", "type": "uint256" }, - { "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityDeadline", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "internalType": "struct V3SpokePoolInterface.V3RelayData", - "name": "relayData", - "type": "tuple" - } - ], - "name": "requestSlowFill", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "name": "rootBundles", - "outputs": [ - { "internalType": "bytes32", "name": "slowRelayRoot", "type": "bytes32" }, - { "internalType": "bytes32", "name": "relayerRefundRoot", "type": "bytes32" } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "newCrossDomainAdmin", "type": "address" }], - "name": "setCrossDomainAdmin", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "newWithdrawalRecipient", "type": "address" }], - "name": "setWithdrawalRecipient", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, - { "internalType": "bytes32", "name": "updatedRecipient", "type": "bytes32" }, - { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, - { "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } - ], - "name": "speedUpDeposit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "depositor", "type": "address" }, - { "internalType": "uint256", "name": "depositId", "type": "uint256" }, - { "internalType": "uint256", "name": "updatedOutputAmount", "type": "uint256" }, - { "internalType": "address", "name": "updatedRecipient", "type": "address" }, - { "internalType": "bytes", "name": "updatedMessage", "type": "bytes" }, - { "internalType": "bytes", "name": "depositorSignature", "type": "bytes" } - ], - "name": "speedUpV3Deposit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes[]", "name": "data", "type": "bytes[]" }], - "name": "tryMulticall", - "outputs": [ - { - "components": [ - { "internalType": "bool", "name": "success", "type": "bool" }, - { "internalType": "bytes", "name": "returnData", "type": "bytes" } - ], - "internalType": "struct MultiCallerUpgradeable.Result[]", - "name": "results", - "type": "tuple[]" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "depositor", "type": "bytes32" }, - { "internalType": "bytes32", "name": "recipient", "type": "bytes32" }, - { "internalType": "bytes32", "name": "inputToken", "type": "bytes32" }, - { "internalType": "bytes32", "name": "outputToken", "type": "bytes32" }, - { "internalType": "uint256", "name": "inputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "outputAmount", "type": "uint256" }, - { "internalType": "uint256", "name": "destinationChainId", "type": "uint256" }, - { "internalType": "bytes32", "name": "exclusiveRelayer", "type": "bytes32" }, - { "internalType": "uint256", "name": "depositNonce", "type": "uint256" }, - { "internalType": "uint32", "name": "quoteTimestamp", "type": "uint32" }, - { "internalType": "uint32", "name": "fillDeadline", "type": "uint32" }, - { "internalType": "uint32", "name": "exclusivityParameter", "type": "uint32" }, - { "internalType": "bytes", "name": "message", "type": "bytes" } - ], - "name": "unsafeDeposit", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "newImplementation", "type": "address" }], - "name": "upgradeTo", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "newImplementation", "type": "address" }, - { "internalType": "bytes", "name": "data", "type": "bytes" } - ], - "name": "upgradeToAndCall", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "usdcToken", - "outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "withdrawalRecipient", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "wrappedNativeToken", - "outputs": [{ "internalType": "contract WETH9Interface", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { "stateMutability": "payable", "type": "receive" } + } ] diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index d308887f8a..3e8ef30989 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -815,13 +815,12 @@ async function generateHeliosTxns( } // 1. SP1Helios.update transaction - const updateArgs = [proofBytes, publicValuesBytes]; // Use proofBytes as intended + const updateArgs = [proofBytes, publicValuesBytes]; const updateTx: AugmentedTransaction = { contract: sp1HeliosContract, chainId: l2ChainId, method: "update", args: updateArgs, - // gasLimit: BigNumber.from(1000000), // todo: testing unpermissioned: false, canFailInSimulation: false, nonMulticall: true, @@ -848,8 +847,6 @@ async function generateHeliosTxns( chainId: l2ChainId, method: "executeMessage", args: executeArgs, - // gasLimit: BigNumber.from(1000000), // todo: testing - // todo: check this unpermissioned: true, // @dev Simulation of `executeMessage` depends on prior state update via SP1Helios.update canFailInSimulation: true, From 43403453269f31b32a4e8e04148643572ecdbac0 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 1 May 2025 21:55:45 -0700 Subject: [PATCH 20/31] address part of the PR comments Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 73 +++++++++-------------------------- src/utils/TransactionUtils.ts | 6 --- 2 files changed, 18 insertions(+), 61 deletions(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 3e8ef30989..976ef00134 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -1,6 +1,6 @@ import { ethers } from "ethers"; import { HubPoolClient, SpokePoolClient, AugmentedTransaction } from "../../clients"; -import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple } from "../../utils"; +import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple, Provider } from "../../utils"; import { FinalizerPromise, CrossChainMessage } from "../types"; import { Log } from "../../interfaces"; import { CONTRACT_ADDRESSES } from "../../common"; @@ -294,11 +294,7 @@ async function getRelevantL1Events( const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, - message: `HubPoolStore contract address or ABI not found for L1 chain ${l1ChainId}.`, - }); - return null; + throw new Error(`HubPoolStore contract address or ABI not found for L1 chain ${l1ChainId}.`); } const hubPoolStoreContract = new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, l1Provider); @@ -358,16 +354,7 @@ async function getL2VerifiedSlotsMap( l2ChainId: number ): Promise | null> { const l2Provider = l2SpokePoolClient.spokePool.provider; - - const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); - if (!sp1HeliosAddress || !sp1HeliosAbi) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedKeys:${l2ChainId}`, - message: `SP1Helios contract not found for destination chain ${l2ChainId}. Cannot verify Helios messages.`, - }); - return null; - } - const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); + const sp1HeliosContract = getSp1HeliosContract(l2ChainId, l2Provider); const l2SearchConfig: EventSearchConfig = { fromBlock: l2SpokePoolClient.eventSearchConfig.fromBlock, @@ -388,7 +375,7 @@ async function getL2VerifiedSlotsMap( logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, message: `Found ${events.length} StorageSlotVerified events on L2 (${l2ChainId})`, - sp1HeliosAddress, + sp1HeliosAddress: sp1HeliosContract.address, fromBlock: l2SearchConfig.fromBlock, toBlock: l2SearchConfig.toBlock, }); @@ -411,7 +398,7 @@ async function getL2VerifiedSlotsMap( logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, message: `Failed to query StorageSlotVerified events from L2 (${l2ChainId})`, - sp1HeliosAddress, + sp1HeliosAddress: sp1HeliosContract, error, }); return null; // Return null on error @@ -426,18 +413,17 @@ async function getL2RelayedNonces( ): Promise | null> { const l2Provider = l2SpokePoolClient.spokePool.provider; const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; - // Use the Universal Spoke Pool ABI for this event - const spokePoolContract = new ethers.Contract(l2SpokePoolAddress, UNIVERSAL_SPOKE_ABI, l2Provider); + const universalSpokePoolContract = new ethers.Contract(l2SpokePoolAddress, UNIVERSAL_SPOKE_ABI, l2Provider); const l2SearchConfig: EventSearchConfig = { fromBlock: l2SpokePoolClient.eventSearchConfig.fromBlock, toBlock: l2SpokePoolClient.latestBlockSearched, maxBlockLookBack: l2SpokePoolClient.eventSearchConfig.maxBlockLookBack, }; - const relayedCallDataFilter = spokePoolContract.filters.RelayedCallData(); + const relayedCallDataFilter = universalSpokePoolContract.filters.RelayedCallData(); try { - const rawLogs = await paginatedEventQuery(spokePoolContract, relayedCallDataFilter, l2SearchConfig); + const rawLogs = await paginatedEventQuery(universalSpokePoolContract, relayedCallDataFilter, l2SearchConfig); const events: RelayedCallDataEvent[] = rawLogs.map((log) => ({ ...log, @@ -503,15 +489,7 @@ async function processUnfinalizedHeliosMessages( return []; } const hubPoolStoreAddress = hubPoolStoreInfo.address; - const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); - if (!sp1HeliosAddress || !sp1HeliosAbi) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - message: `SP1Helios contract not found for L2 chain ${l2ChainId}.`, - }); - return []; - } - const sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2Provider); + const sp1HeliosContract = getSp1HeliosContract(l2ChainId, l2Provider); let currentHead: number; let currentHeader: string; @@ -527,12 +505,12 @@ async function processUnfinalizedHeliosMessages( logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, message: `Using SP1Helios head ${currentHead} and header ${currentHeader} for proof requests.`, - sp1HeliosAddress, + sp1HeliosAddress: sp1HeliosContract.address, }); } catch (error) { logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - message: `Failed to read current head/header from SP1Helios contract ${sp1HeliosAddress}`, + message: `Failed to read current head/header from SP1Helios contract ${sp1HeliosContract.address}`, error, }); return []; @@ -694,19 +672,7 @@ async function generateHeliosTxns( const transactions: AugmentedTransaction[] = []; const crossChainMessages: CrossChainMessage[] = []; - const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = getSp1Helios(l2ChainId); - let sp1HeliosContract: ethers.Contract | null = null; - if (sp1HeliosAddress && sp1HeliosAbi && successfulProofs.length > 0) { - sp1HeliosContract = new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, l2SpokePoolClient.spokePool.signer); - } else if (successfulProofs.length > 0) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, - message: `SP1Helios contract missing for L2 chain ${l2ChainId}, but proofs were provided. Cannot generate 'update' txns.`, - }); - // Cannot proceed with proofs without the contract - // We might still be able to process needsExecutionOnly, so don't return yet - } - + const sp1HeliosContract = getSp1HeliosContract(l2ChainId, l2SpokePoolClient.spokePool.signer); const spokePoolAddress = l2SpokePoolClient.spokePool.address; const universalSpokePoolContract = new ethers.Contract( spokePoolAddress, @@ -776,13 +742,6 @@ async function generateHeliosTxns( // --- Process messages needing proof and execution --- for (const proof of successfulProofs) { - if (!sp1HeliosContract) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, - message: `SP1Helios contract instance not available. Skipping full finalization for nonce ${proof.sourceNonce.toString()}`, - }); - continue; // Skip this proof if contract is missing - } try { // Ensure the hex strings have the '0x' prefix, adding it only if missing. const proofBytes = proof.proofData.proof.startsWith("0x") ? proof.proofData.proof : "0x" + proof.proofData.proof; @@ -919,6 +878,10 @@ function calculateProofId(request: ApiProofRequest): string { return ethers.utils.keccak256(encoded); } -function getSp1Helios(dstChainId: number): { address?: string; abi?: unknown[] } { - return CONTRACT_ADDRESSES[dstChainId].sp1Helios; +function getSp1HeliosContract(dstChainId: number, signerOrProvider: Signer | Provider): ethers.Contract { + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = CONTRACT_ADDRESSES[dstChainId].sp1Helios; + if (!sp1HeliosAddress || !sp1HeliosAbi) { + throw new Error(`SP1Helios contract not found for destination chain ${dstChainId}. Cannot verify Helios messages.`); + } + return new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, signerOrProvider); } diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index 549c7f00b2..57c55bbf23 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -95,12 +95,6 @@ export async function runTransaction( // Check if the chain requires legacy transactions if (LEGACY_TRANSACTION_CHAINS.has(chainId)) { gas = { gasPrice: gas.maxFeePerGas }; - logger.debug({ - at: "TxUtil#runTransaction", - message: `Forcing legacy gasPrice for chainId ${chainId}`, - chainId, - gasPrice: gas.gasPrice?.toString(), - }); } logger.debug({ From f23444ab699a5d087f3b33cbdc978265b75b7f58 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 11:50:54 -0700 Subject: [PATCH 21/31] Move some code into interface/ utils/ folder instead of helios.ts Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 218 ++++++++-------------------------- src/interfaces/Helios.ts | 12 ++ src/interfaces/Universal.ts | 17 +++ src/interfaces/ZkApi.ts | 49 ++++++++ src/utils/UniversalUtils.ts | 26 ++++ src/utils/ZkApiUtils.ts | 47 ++++++++ 6 files changed, 200 insertions(+), 169 deletions(-) create mode 100644 src/interfaces/Helios.ts create mode 100644 src/interfaces/Universal.ts create mode 100644 src/interfaces/ZkApi.ts create mode 100644 src/utils/UniversalUtils.ts create mode 100644 src/utils/ZkApiUtils.ts diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 976ef00134..d9a0c5d366 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -2,92 +2,25 @@ import { ethers } from "ethers"; import { HubPoolClient, SpokePoolClient, AugmentedTransaction } from "../../clients"; import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple, Provider } from "../../utils"; import { FinalizerPromise, CrossChainMessage } from "../types"; -import { Log } from "../../interfaces"; import { CONTRACT_ADDRESSES } from "../../common"; import axios from "axios"; import UNIVERSAL_SPOKE_ABI from "../../common/abi/Universal_SpokePool.json"; - -// Define interfaces for the event arguments for clarity -interface StoredCallDataEventArgs { - target: string; - data: string; - nonce: ethers.BigNumber; -} - -interface StorageSlotVerifiedEventArgs { - head: ethers.BigNumber; - key: string; // bytes32 - value: string; // bytes32 - contractAddress: string; -} - -interface RelayedCallDataEventArgs { - nonce: ethers.BigNumber; - caller: string; -} -// -------------------------------- - -// Type for the structured StoredCallData event -type StoredCallDataEvent = Log & { args: StoredCallDataEventArgs }; -// Type for the structured StorageSlotVerified event -type StorageSlotVerifiedEvent = Log & { args: StorageSlotVerifiedEventArgs }; -// Type for the structured RelayedCallData event -type RelayedCallDataEvent = Log & { args: RelayedCallDataEventArgs }; -// ------------------------------------ - -// --- API Interaction Types --- -interface ApiProofRequest { - src_chain_contract_address: string; - src_chain_storage_slot: string; - src_chain_block_number: number; // u64 on Rust API side - dst_chain_contract_from_head: number; // u64 on Rust API side - dst_chain_contract_from_header: string; -} - -type ProofStatus = "pending" | "success" | "errored"; - -interface SP1HeliosProofData { - proof: string; - public_values: string; -} - -interface ProofStateResponse { - proof_id: string; - status: ProofStatus; - update_calldata?: SP1HeliosProofData; // Present only if status is "success" - error_message?: string; // Present only if status is "errored" -} - -// Define the structure for ProofOutputs to decode public_values -const proofOutputsAbiTuple = `tuple( - bytes32 executionStateRoot, - bytes32 newHeader, - bytes32 nextSyncCommitteeHash, - uint256 newHead, - bytes32 prevHeader, - uint256 prevHead, - bytes32 syncCommitteeHash, - bytes32 startSyncCommitteeHash, - tuple(bytes32 key, bytes32 value, address contractAddress)[] slots -)`; - -type ProofOutputs = { - executionStateRoot: string; - newHeader: string; - nextSyncCommitteeHash: string; - newHead: ethers.BigNumber; - prevHeader: string; - prevHead: ethers.BigNumber; - syncCommitteeHash: string; - startSyncCommitteeHash: string; - slots: { key: string; value: string; contractAddress: string }[]; -}; - -type HeliosMessageStatus = "NeedsProofAndExecution" | "NeedsExecutionOnly"; - -interface PendingHeliosMessage { - l1Event: StoredCallDataEvent; // The original L1 event triggering the flow - status: HeliosMessageStatus; +import { + RelayedCallDataEvent, + RelayedCallDataEventArgs, + StoredCallDataEvent, + StoredCallDataEventArgs, +} from "../../interfaces/Universal"; +import { ApiProofRequest, ProofOutputs, ProofStateResponse, SP1HeliosProofData } from "../../interfaces/ZkApi"; +import { StorageSlotVerifiedEvent, StorageSlotVerifiedEventArgs } from "../../interfaces/Helios"; +import { calculateProofId, decodeProofOutputs } from "../../utils/ZkApiUtils"; +import { calculateHubPoolStoreStorageSlot } from "../../utils/UniversalUtils"; + +type CrossChainMessageStatus = "NeedsProofAndExecution" | "NeedsExecutionOnly"; + +interface PendingCrosschainMessage { + l1Event: StoredCallDataEvent; // The original HubPoolStore event triggering the flow + status: CrossChainMessageStatus; verifiedHead?: ethers.BigNumber; // Head from the StorageSlotVerified event, only present if status is NeedsExecutionOnly } // --------------------------------------- @@ -191,7 +124,7 @@ async function identifyPendingHeliosMessages( l2SpokePoolClient: SpokePoolClient, l1ChainId: number, l2ChainId: number -): Promise { +): Promise { const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; // --- Substep 1: Query and Filter L1 Events --- @@ -228,7 +161,7 @@ async function identifyPendingHeliosMessages( } // --- Determine Status for each L1 Event --- - const pendingMessages: PendingHeliosMessage[] = []; + const pendingMessages: PendingCrosschainMessage[] = []; for (const l1Event of relevantStoredCallDataEvents) { const expectedStorageSlot = calculateHubPoolStoreStorageSlot(l1Event.args); const nonce = l1Event.args.nonce; @@ -291,13 +224,7 @@ async function getRelevantL1Events( l2SpokePoolAddress: string ): Promise { const l1Provider = hubPoolClient.hubPool.provider; - - const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; - if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { - throw new Error(`HubPoolStore contract address or ABI not found for L1 chain ${l1ChainId}.`); - } - - const hubPoolStoreContract = new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, l1Provider); + const hubPoolStoreContract = getHubPoolStoreContract(l1ChainId, l1Provider); const l1SearchConfig: EventSearchConfig = { fromBlock: l1SpokePoolClient.eventSearchConfig.fromBlock, @@ -311,7 +238,7 @@ async function getRelevantL1Events( logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, message: `Querying StoredCallData events on L1 (${l1ChainId})`, - hubPoolStoreAddress: hubPoolStoreInfo.address, + hubPoolStoreAddress: hubPoolStoreContract.address, fromBlock: l1SearchConfig.fromBlock, toBlock: l1SearchConfig.toBlock, }); @@ -340,7 +267,7 @@ async function getRelevantL1Events( logger.warn({ at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, message: `Failed to query StoredCallData events from L1 (${l1ChainId})`, - hubPoolStoreAddress: hubPoolStoreInfo.address, + hubPoolStoreAddress: hubPoolStoreContract.address, error, }); return null; // Return null on error @@ -458,7 +385,7 @@ async function getL2RelayedNonces( */ async function processUnfinalizedHeliosMessages( logger: winston.Logger, - messagesToProcess: PendingHeliosMessage[], + messagesToProcess: PendingCrosschainMessage[], l2SpokePoolClient: SpokePoolClient, l1ChainId: number ): Promise { @@ -482,11 +409,7 @@ async function processUnfinalizedHeliosMessages( const hubPoolStoreInfo = CONTRACT_ADDRESSES[l1ChainId]?.hubPoolStore; if (!hubPoolStoreInfo?.address) { - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - message: `HubPoolStore contract address not found for L1 chain ${l1ChainId}.`, - }); - return []; + throw new Error(`HubPoolStore address not available for chain: ${l1ChainId}. Cannot process Helios messages.`); } const hubPoolStoreAddress = hubPoolStoreInfo.address; const sp1HeliosContract = getSp1HeliosContract(l2ChainId, l2Provider); @@ -664,7 +587,7 @@ async function processUnfinalizedHeliosMessages( async function generateHeliosTxns( logger: winston.Logger, successfulProofs: SuccessfulProof[], - needsExecutionOnlyMessages: PendingHeliosMessage[], + needsExecutionOnlyMessages: PendingCrosschainMessage[], l1ChainId: number, l2ChainId: number, l2SpokePoolClient: SpokePoolClient @@ -749,29 +672,8 @@ async function generateHeliosTxns( ? proof.proofData.public_values : "0x" + proof.proofData.public_values; - let decodedOutputs: ProofOutputs; - try { - const decodedResult = ethers.utils.defaultAbiCoder.decode([proofOutputsAbiTuple], publicValuesBytes)[0]; - decodedOutputs = { - executionStateRoot: decodedResult[0], - newHeader: decodedResult[1], - nextSyncCommitteeHash: decodedResult[2], - newHead: decodedResult[3], - prevHeader: decodedResult[4], - prevHead: decodedResult[5], - syncCommitteeHash: decodedResult[6], - startSyncCommitteeHash: decodedResult[7], - slots: decodedResult[8].map((slot: any[]) => ({ key: slot[0], value: slot[1], contractAddress: slot[2] })), - }; - } catch (decodeError) { - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:decodePublicValues:${l2ChainId}`, - message: `Failed to decode public_values for nonce ${proof.sourceNonce.toString()}`, - publicValues: publicValuesBytes, - error: decodeError, - }); - continue; - } + // @dev Will throw on decode errors here. + let decodedOutputs: ProofOutputs = decodeProofOutputs(publicValuesBytes); // 1. SP1Helios.update transaction const updateArgs = [proofBytes, publicValuesBytes]; @@ -818,14 +720,13 @@ async function generateHeliosTxns( originationChainId: l1ChainId, destinationChainId: l2ChainId, }); - } catch (error) { - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, - message: `Failed to prepare transaction for proof of nonce ${proof.sourceNonce.toString()}`, - error: error, - proofData: { sourceNonce: proof.sourceNonce.toString(), target: proof.target }, - }); - continue; + } catch (error: any) { + // Rethrow the error, adding context about the specific proof being processed. + const nonceStr = proof.sourceNonce.toString(); + const targetAddr = proof.target; + throw new Error( + `Failed to prepare transaction for proof of nonce ${nonceStr} (target: ${targetAddr}): ${error.message}` + ); } } @@ -841,47 +742,26 @@ async function generateHeliosTxns( } /** - * Calculates the storage slot in the HubPoolStore contract for a given nonce. - * This assumes the data is stored in a mapping at slot 0, keyed by nonce. - * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) ✅ tested + * Retrieves an ethers.Contract instance for the SP1Helios contract on the specified chain. + * @throws {Error} If the SP1Helios contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES. */ -function calculateHubPoolStoreStorageSlot(eventArgs: StoredCallDataEventArgs): string { - const nonce = eventArgs.nonce; - const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 - - // Ensure nonce and slot position are correctly padded to 32 bytes (64 hex chars + 0x prefix) - const paddedNonce = ethers.utils.hexZeroPad(nonce.toHexString(), 32); - const paddedSlot = ethers.utils.hexZeroPad(ethers.BigNumber.from(mappingSlotPosition).toHexString(), 32); - - // Concatenate the padded key (nonce) and slot position - // ethers.utils.concat expects Uint8Array or hex string inputs - const concatenated = ethers.utils.concat([paddedNonce, paddedSlot]); - - // Calculate the Keccak256 hash - const storageSlot = ethers.utils.keccak256(concatenated); - - return storageSlot; +function getSp1HeliosContract(chainId: number, signerOrProvider: Signer | Provider): ethers.Contract { + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = CONTRACT_ADDRESSES[chainId].sp1Helios; + if (!sp1HeliosAddress || !sp1HeliosAbi) { + throw new Error(`SP1Helios contract not found for chain ${chainId}. Cannot verify Helios messages.`); + } + return new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, signerOrProvider); } /** - * Calculates the deterministic Proof ID based on the request parameters. - * Matches the Rust implementation using RLP encoding and Keccak256. ✅ tested + * Retrieves an ethers.Contract instance for the HubPoolStore contract on the specified chain. + * @throws {Error} If the HubPoolStore contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES. */ -function calculateProofId(request: ApiProofRequest): string { - const encoded = ethers.utils.RLP.encode([ - request.src_chain_contract_address, - request.src_chain_storage_slot, - ethers.BigNumber.from(request.src_chain_block_number).toHexString(), // Ensure block number is hex encoded for RLP - ethers.BigNumber.from(request.dst_chain_contract_from_head).toHexString(), // Ensure head is hex encoded for RLP - request.dst_chain_contract_from_header, - ]); - return ethers.utils.keccak256(encoded); -} - -function getSp1HeliosContract(dstChainId: number, signerOrProvider: Signer | Provider): ethers.Contract { - const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = CONTRACT_ADDRESSES[dstChainId].sp1Helios; - if (!sp1HeliosAddress || !sp1HeliosAbi) { - throw new Error(`SP1Helios contract not found for destination chain ${dstChainId}. Cannot verify Helios messages.`); +function getHubPoolStoreContract(chainId: number, signerOrProvider: Signer | Provider) { + const hubPoolStoreInfo = CONTRACT_ADDRESSES[chainId]?.hubPoolStore; + if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { + throw new Error(`HubPoolStore contract address or ABI not found for chain ${chainId}.`); } - return new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, signerOrProvider); + + return new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, signerOrProvider); } diff --git a/src/interfaces/Helios.ts b/src/interfaces/Helios.ts new file mode 100644 index 0000000000..3132d581cf --- /dev/null +++ b/src/interfaces/Helios.ts @@ -0,0 +1,12 @@ +import { Log } from "."; +import { BigNumber } from "../utils"; + +export interface StorageSlotVerifiedEventArgs { + head: BigNumber; + key: string; // bytes32 + value: string; // bytes32 + contractAddress: string; +} + +// Event type for Sp1Helios used in v4 messaging +export type StorageSlotVerifiedEvent = Log & { args: StorageSlotVerifiedEventArgs }; diff --git a/src/interfaces/Universal.ts b/src/interfaces/Universal.ts new file mode 100644 index 0000000000..3e5d695b63 --- /dev/null +++ b/src/interfaces/Universal.ts @@ -0,0 +1,17 @@ +import { Log } from "."; +import { BigNumber } from "../utils"; + +export interface StoredCallDataEventArgs { + target: string; + data: string; + nonce: BigNumber; +} + +export interface RelayedCallDataEventArgs { + nonce: BigNumber; + caller: string; +} + +// Event types for HubPoolStore and Universal_SpokePool used in v4 messaging +export type StoredCallDataEvent = Log & { args: StoredCallDataEventArgs }; +export type RelayedCallDataEvent = Log & { args: RelayedCallDataEventArgs }; diff --git a/src/interfaces/ZkApi.ts b/src/interfaces/ZkApi.ts new file mode 100644 index 0000000000..068f15c6f8 --- /dev/null +++ b/src/interfaces/ZkApi.ts @@ -0,0 +1,49 @@ +import { BigNumber } from "../utils"; + +// --- API Interaction Types --- +export interface ApiProofRequest { + src_chain_contract_address: string; + src_chain_storage_slot: string; + src_chain_block_number: number; // u64 on Rust API side + dst_chain_contract_from_head: number; // u64 on Rust API side + dst_chain_contract_from_header: string; +} + +export type ProofStatus = "pending" | "success" | "errored"; + +export interface SP1HeliosProofData { + proof: string; + public_values: string; +} + +export interface ProofStateResponse { + proof_id: string; + status: ProofStatus; + update_calldata?: SP1HeliosProofData; // Present only if status is "success" + error_message?: string; // Present only if status is "errored" +} + +// ABI for `public_values` returned from ZK API as part of `SP1HeliosProofData` +export const PROOF_OUTPUTS_ABI_TUPLE = `tuple( + bytes32 executionStateRoot, + bytes32 newHeader, + bytes32 nextSyncCommitteeHash, + uint256 newHead, + bytes32 prevHeader, + uint256 prevHead, + bytes32 syncCommitteeHash, + bytes32 startSyncCommitteeHash, + tuple(bytes32 key, bytes32 value, address contractAddress)[] slots + )`; + +export type ProofOutputs = { + executionStateRoot: string; + newHeader: string; + nextSyncCommitteeHash: string; + newHead: BigNumber; + prevHeader: string; + prevHead: BigNumber; + syncCommitteeHash: string; + startSyncCommitteeHash: string; + slots: { key: string; value: string; contractAddress: string }[]; +}; diff --git a/src/utils/UniversalUtils.ts b/src/utils/UniversalUtils.ts new file mode 100644 index 0000000000..d8c74a927d --- /dev/null +++ b/src/utils/UniversalUtils.ts @@ -0,0 +1,26 @@ +import { ethers } from "ethers"; +import { BigNumber } from "."; +import { StoredCallDataEventArgs } from "../interfaces/Universal"; + +/** + * Calculates the storage slot in the HubPoolStore contract for a given nonce. + * This assumes the data is stored in a mapping at slot 0, keyed by nonce. + * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) + */ +export function calculateHubPoolStoreStorageSlot(eventArgs: StoredCallDataEventArgs): string { + const nonce = eventArgs.nonce; + const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 + + // Ensure nonce and slot position are correctly padded to 32 bytes (64 hex chars + 0x prefix) + const paddedNonce = ethers.utils.hexZeroPad(nonce.toHexString(), 32); + const paddedSlot = ethers.utils.hexZeroPad(BigNumber.from(mappingSlotPosition).toHexString(), 32); + + // Concatenate the padded key (nonce) and slot position + // ethers.utils.concat expects Uint8Array or hex string inputs + const concatenated = ethers.utils.concat([paddedNonce, paddedSlot]); + + // Calculate the Keccak256 hash + const storageSlot = ethers.utils.keccak256(concatenated); + + return storageSlot; +} diff --git a/src/utils/ZkApiUtils.ts b/src/utils/ZkApiUtils.ts new file mode 100644 index 0000000000..b3543fed76 --- /dev/null +++ b/src/utils/ZkApiUtils.ts @@ -0,0 +1,47 @@ +import { BigNumber, ethers } from "."; +import { ApiProofRequest, PROOF_OUTPUTS_ABI_TUPLE, ProofOutputs } from "../interfaces/ZkApi"; + +/** + * Calculates the deterministic Proof ID based on the request parameters. + * Matches the Rust implementation using RLP encoding and Keccak256. + */ +export function calculateProofId(request: ApiProofRequest): string { + const encoded = ethers.utils.RLP.encode([ + request.src_chain_contract_address, + request.src_chain_storage_slot, + BigNumber.from(request.src_chain_block_number).toHexString(), // Ensure block number is hex encoded for RLP + BigNumber.from(request.dst_chain_contract_from_head).toHexString(), // Ensure head is hex encoded for RLP + request.dst_chain_contract_from_header, + ]); + return ethers.utils.keccak256(encoded); +} + +/** + * Decodes the ABI-encoded public_values string from the ZK Proof API into a structured ProofOutputs object. + * @param publicValuesBytes The ABI-encoded hex string (with or without 0x prefix) containing the proof outputs. + * @returns The decoded ProofOutputs object. + * @throws {Error} If the decoding fails (e.g., invalid format). + */ +export function decodeProofOutputs(publicValuesBytes: string): ProofOutputs { + // Ensure 0x prefix for decoder + const prefixedBytes = publicValuesBytes.startsWith("0x") ? publicValuesBytes : "0x" + publicValuesBytes; + const decodedResult = ethers.utils.defaultAbiCoder.decode([PROOF_OUTPUTS_ABI_TUPLE], prefixedBytes)[0]; + + // Map the decoded array elements to the ProofOutputs type properties + // @dev Notice, if `decodedResult` is not what we expect, this will implicitly throw an error. + return { + executionStateRoot: decodedResult[0], + newHeader: decodedResult[1], + nextSyncCommitteeHash: decodedResult[2], + newHead: decodedResult[3], // Already a BigNumber from decoder + prevHeader: decodedResult[4], + prevHead: decodedResult[5], // Already a BigNumber from decoder + syncCommitteeHash: decodedResult[6], + startSyncCommitteeHash: decodedResult[7], + slots: decodedResult[8].map((slot: any[]) => ({ + key: slot[0], + value: slot[1], + contractAddress: slot[2], + })), + }; +} From ee3ec3fb2b474718246aeb64559ce5db38772b8d Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 12:12:22 -0700 Subject: [PATCH 22/31] remove all but one try - catch statements Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 554 +++++++++++++--------------------- 1 file changed, 210 insertions(+), 344 deletions(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index d9a0c5d366..bc005d6970 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -234,44 +234,21 @@ async function getRelevantL1Events( const storedCallDataFilter = hubPoolStoreContract.filters.StoredCallData(); - try { - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, - message: `Querying StoredCallData events on L1 (${l1ChainId})`, - hubPoolStoreAddress: hubPoolStoreContract.address, - fromBlock: l1SearchConfig.fromBlock, - toBlock: l1SearchConfig.toBlock, - }); - - const rawLogs = await paginatedEventQuery(hubPoolStoreContract, storedCallDataFilter, l1SearchConfig); - - // Explicitly cast logs to the correct type - const events: StoredCallDataEvent[] = rawLogs.map((log) => ({ - ...log, - args: log.args as StoredCallDataEventArgs, - })); - - const relevantStoredCallDataEvents = events.filter( - (event) => - compareAddressesSimple(event.args.target, l2SpokePoolAddress) || - compareAddressesSimple(event.args.target, ethers.constants.AddressZero) - ); - - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, - message: `Found ${relevantStoredCallDataEvents.length} StoredCallData events on L1 (${l1ChainId})`, - }); + const rawLogs = await paginatedEventQuery(hubPoolStoreContract, storedCallDataFilter, l1SearchConfig); + + // Explicitly cast logs to the correct type + const events: StoredCallDataEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as StoredCallDataEventArgs, + })); + + const relevantStoredCallDataEvents = events.filter( + (event) => + compareAddressesSimple(event.args.target, l2SpokePoolAddress) || + compareAddressesSimple(event.args.target, ethers.constants.AddressZero) + ); - return relevantStoredCallDataEvents; - } catch (error) { - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:getAndFilterL1Events:${l2ChainId}`, - message: `Failed to query StoredCallData events from L1 (${l1ChainId})`, - hubPoolStoreAddress: hubPoolStoreContract.address, - error, - }); - return null; // Return null on error - } + return relevantStoredCallDataEvents; } /** Query L2 Verification Events and return verified slots map */ @@ -290,46 +267,28 @@ async function getL2VerifiedSlotsMap( }; const storageVerifiedFilter = sp1HeliosContract.filters.StorageSlotVerified(); - try { - const rawLogs = await paginatedEventQuery(sp1HeliosContract, storageVerifiedFilter, l2SearchConfig); - - // Explicitly cast logs to the correct type - const events: StorageSlotVerifiedEvent[] = rawLogs.map((log) => ({ - ...log, - args: log.args as StorageSlotVerifiedEventArgs, - })); - - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, - message: `Found ${events.length} StorageSlotVerified events on L2 (${l2ChainId})`, - sp1HeliosAddress: sp1HeliosContract.address, - fromBlock: l2SearchConfig.fromBlock, - toBlock: l2SearchConfig.toBlock, - }); - - // Store events in a map keyed by the storage slot (key) - const verifiedSlotsMap = new Map(); - events.forEach((event) => { - // Handle potential duplicates (though unlikely with paginated query): favour latest block/logIndex - const existing = verifiedSlotsMap.get(event.args.key); - if ( - !existing || - event.blockNumber > existing.blockNumber || - (event.blockNumber === existing.blockNumber && event.logIndex > existing.logIndex) - ) { - verifiedSlotsMap.set(event.args.key, event); - } - }); - return verifiedSlotsMap; - } catch (error) { - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:getL2VerifiedSlotsMap:${l2ChainId}`, - message: `Failed to query StorageSlotVerified events from L2 (${l2ChainId})`, - sp1HeliosAddress: sp1HeliosContract, - error, - }); - return null; // Return null on error - } + const rawLogs = await paginatedEventQuery(sp1HeliosContract, storageVerifiedFilter, l2SearchConfig); + + // Explicitly cast logs to the correct type + const events: StorageSlotVerifiedEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as StorageSlotVerifiedEventArgs, + })); + + // Store events in a map keyed by the storage slot (key) + const verifiedSlotsMap = new Map(); + events.forEach((event) => { + // Handle potential duplicates (though unlikely with paginated query): favour latest block/logIndex + const existing = verifiedSlotsMap.get(event.args.key); + if ( + !existing || + event.blockNumber > existing.blockNumber || + (event.blockNumber === existing.blockNumber && event.logIndex > existing.logIndex) + ) { + verifiedSlotsMap.set(event.args.key, event); + } + }); + return verifiedSlotsMap; } /** --- Query L2 Execution Events (RelayedCallData) */ @@ -349,33 +308,15 @@ async function getL2RelayedNonces( }; const relayedCallDataFilter = universalSpokePoolContract.filters.RelayedCallData(); - try { - const rawLogs = await paginatedEventQuery(universalSpokePoolContract, relayedCallDataFilter, l2SearchConfig); + const rawLogs = await paginatedEventQuery(universalSpokePoolContract, relayedCallDataFilter, l2SearchConfig); - const events: RelayedCallDataEvent[] = rawLogs.map((log) => ({ - ...log, - args: log.args as RelayedCallDataEventArgs, - })); - - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:getL2RelayedNonces:${l2ChainId}`, - message: `Found ${events.length} RelayedCallData events on L2 (${l2ChainId})`, - spokePoolAddress: l2SpokePoolAddress, - fromBlock: l2SearchConfig.fromBlock, - toBlock: l2SearchConfig.toBlock, - }); + const events: RelayedCallDataEvent[] = rawLogs.map((log) => ({ + ...log, + args: log.args as RelayedCallDataEventArgs, + })); - // Return a Set of nonces (as strings for easy comparison) - return new Set(events.map((event) => event.args.nonce.toString())); - } catch (error) { - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:getL2RelayedNonces:${l2ChainId}`, - message: `Failed to query RelayedCallData events from L2 (${l2ChainId})`, - spokePoolAddress: l2SpokePoolAddress, - error, - }); - return null; // Return null on error - } + // Return a Set of nonces (as strings for easy comparison) + return new Set(events.map((event) => event.args.nonce.toString())); } /** @@ -416,27 +357,14 @@ async function processUnfinalizedHeliosMessages( let currentHead: number; let currentHeader: string; - try { - const headBn: ethers.BigNumber = await sp1HeliosContract.head(); - // todo: well, currently we're taking currentHead to use as prevHead in our ZK proof. There's a particular scenario where we could speed up proofs - // todo: (by not making them to wait for finality longer than needed) if our blockNumber that we need a proved slot for is older than this head. - currentHead = headBn.toNumber(); - currentHeader = await sp1HeliosContract.headers(headBn); - if (!currentHeader || currentHeader === ethers.constants.HashZero) { - throw new Error(`Invalid header found for head ${currentHead}`); - } - logger.debug({ - at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - message: `Using SP1Helios head ${currentHead} and header ${currentHeader} for proof requests.`, - sp1HeliosAddress: sp1HeliosContract.address, - }); - } catch (error) { - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - message: `Failed to read current head/header from SP1Helios contract ${sp1HeliosContract.address}`, - error, - }); - return []; + + const headBn: ethers.BigNumber = await sp1HeliosContract.head(); + // todo: well, currently we're taking currentHead to use as prevHead in our ZK proof. There's a particular scenario where we could speed up proofs + // todo: (by not making them to wait for finality longer than needed) if our blockNumber that we need a proved slot for is older than this head. + currentHead = headBn.toNumber(); + currentHeader = await sp1HeliosContract.headers(headBn); + if (!currentHeader || currentHeader === ethers.constants.HashZero) { + throw new Error(`Invalid header found for head ${currentHead}`); } const successfulProofs: SuccessfulProof[] = []; @@ -452,131 +380,94 @@ async function processUnfinalizedHeliosMessages( target: l1Event.args.target, }; + const storageSlot = calculateHubPoolStoreStorageSlot(l1Event.args); + + const apiRequest: ApiProofRequest = { + src_chain_contract_address: hubPoolStoreAddress, + src_chain_storage_slot: storageSlot, + src_chain_block_number: l1Event.blockNumber, // Use block number from L1 event + dst_chain_contract_from_head: currentHead, + dst_chain_contract_from_header: currentHeader, + }; + + const proofId = calculateProofId(apiRequest); + const getProofUrl = `${apiBaseUrl}/api/proofs/${proofId}`; + + logger.debug({ ...logContext, message: "Attempting to get proof", proofId, getProofUrl, storageSlot }); + + let proofState: ProofStateResponse | null = null; + let getError: any = null; + + // @dev We need try - catch here because of how API responds to non-existing proofs: with NotFound status try { - const storageSlot = calculateHubPoolStoreStorageSlot(l1Event.args); - - const apiRequest: ApiProofRequest = { - src_chain_contract_address: hubPoolStoreAddress, - src_chain_storage_slot: storageSlot, - src_chain_block_number: l1Event.blockNumber, // Use block number from L1 event - dst_chain_contract_from_head: currentHead, - dst_chain_contract_from_header: currentHeader, - }; - - const proofId = calculateProofId(apiRequest); - const getProofUrl = `${apiBaseUrl}/api/proofs/${proofId}`; - - logger.debug({ ...logContext, message: "Attempting to get proof", proofId, getProofUrl, storageSlot }); - - let proofState: ProofStateResponse | null = null; - let getError: any = null; - - try { - const response = await axios.get(getProofUrl); - proofState = response.data; - logger.debug({ ...logContext, message: "Proof state received", proofId, status: proofState.status }); - } catch (error: any) { - getError = error; - } + const response = await axios.get(getProofUrl); + proofState = response.data; + logger.debug({ ...logContext, message: "Proof state received", proofId, status: proofState.status }); + } catch (error: any) { + getError = error; + } - // --- API Interaction Flow --- - // 1. Try to get proof - if (getError && axios.isAxiosError(getError) && getError.response?.status === 404) { - // 1a. NOTFOUND -> Request proof - logger.debug({ ...logContext, message: "Proof not found (404), requesting...", proofId }); - try { - const requestProofUrl = `${apiBaseUrl}/api/proofs`; - await axios.post(requestProofUrl, apiRequest); - logger.debug({ ...logContext, message: "Proof requested successfully.", proofId }); - // Exit flow for this message, will check again next run - } catch (postError: any) { - logger.warn({ - ...logContext, - message: "Failed to request proof after 404.", - proofId, - postUrl: `${apiBaseUrl}/api/proofs`, - postError: postError.message, - postResponseData: postError.response?.data, - }); - // Exit flow for this message - } - } else if (getError) { - // Other error during GET - logger.warn({ + // --- API Interaction Flow --- + // 1. Try to get proof + if (getError && axios.isAxiosError(getError) && getError.response?.status === 404) { + // 1a. NOTFOUND -> Request proof + logger.debug({ ...logContext, message: "Proof not found (404), requesting...", proofId }); + await axios.post(`${apiBaseUrl}/api/proofs`, apiRequest); + logger.debug({ ...logContext, message: "Proof requested successfully.", proofId }); + // Exit, will check again next run + } else if (getError) { + // Other error during GET + logger.warn({ + ...logContext, + message: "Failed to get proof state.", + proofId, + getUrl: getProofUrl, + getError: getError.message, + getResponseData: getError.response?.data, + }); + } else if (proofState) { + // GET successful, check status + if (proofState.status === "pending") { + // 1b. SUCCESS ("pending") -> Log and exit flow + logger.debug({ ...logContext, message: "Proof generation is pending.", proofId }); + // Exit, will check again next run + } else if (proofState.status === "errored") { + // 1c. SUCCESS ("errored") -> Log high severity, request again, exit flow + // @dev API tried to generate a proof but failed. Log as error and re-request proof + logger.error({ ...logContext, - message: "Failed to get proof state.", + message: "Proof generation errored on ZK API side. Requesting again.", proofId, - getUrl: getProofUrl, - getError: getError.message, - getResponseData: getError.response?.data, + errorMessage: proofState.error_message, }); - // Exit flow for this message - } else if (proofState) { - // GET successful, check status - if (proofState.status === "pending") { - // 1b. SUCCESS ("pending") -> Log and exit flow - logger.debug({ ...logContext, message: "Proof generation is pending.", proofId }); - // Exit flow for this message - } else if (proofState.status === "errored") { - // 1c. SUCCESS ("errored") -> Log high severity, request again, exit flow - logger.error({ - // Use error level log - ...logContext, - message: "Proof generation errored on ZK API side. Requesting again.", - proofId, - errorMessage: proofState.error_message, + + await axios.post(`${apiBaseUrl}/api/proofs`, apiRequest); + logger.debug({ ...logContext, message: "Errored proof requested again successfully.", proofId }); + // Exit, will check again next run + } else if (proofState.status === "success") { + // 1d. SUCCESS ("success") -> Collect proof data for later processing + if (proofState.update_calldata) { + logger.debug({ ...logContext, message: "Proof successfully retrieved.", proofId }); + successfulProofs.push({ + proofData: proofState.update_calldata, + sourceNonce: l1Event.args.nonce, // Use nonce from L1 event + target: l1Event.args.target, // Use target from L1 event + sourceMessageData: l1Event.args.data, // Use data from L1 event }); - try { - const requestProofUrl = `${apiBaseUrl}/api/proofs`; - await axios.post(requestProofUrl, apiRequest); - logger.debug({ ...logContext, message: "Errored proof requested again successfully.", proofId }); - } catch (postError: any) { - logger.warn({ - ...logContext, - message: "Failed to re-request errored proof.", - proofId, - postUrl: `${apiBaseUrl}/api/proofs`, - postError: postError.message, - postResponseData: postError.response?.data, - }); - } - // Exit flow for this message - } else if (proofState.status === "success") { - // 1d. SUCCESS ("success") -> Collect proof data for later processing - if (proofState.update_calldata) { - logger.debug({ ...logContext, message: "Proof successfully retrieved.", proofId }); - successfulProofs.push({ - proofData: proofState.update_calldata, - sourceNonce: l1Event.args.nonce, // Use nonce from L1 event - target: l1Event.args.target, // Use target from L1 event - sourceMessageData: l1Event.args.data, // Use data from L1 event - }); - } else { - logger.warn({ - ...logContext, - message: "Proof status is success but update_calldata is missing.", - proofId, - proofState, - }); - // Treat as error, exit flow for this message - } } else { + // todo? Might want to log an error here logger.warn({ ...logContext, - message: "Received unexpected proof status. Will try again next run.", + message: "Proof status is success but update_calldata is missing.", proofId, - status: proofState.status, + proofState, }); - // Exit flow for this message + // Exit, will check again next run } + } else { + // @dev status should always be "pending", "success" or "errored" + throw new Error(`Received unexpected proof status for proof ${proofId}`); } - // Implicitly exits flow for the message if none of the success conditions were met - } catch (processingError) { - logger.warn({ - ...logContext, - message: "Error processing unfinalized message for proof.", - error: processingError, - }); } } // end loop over messages @@ -611,123 +502,98 @@ async function generateHeliosTxns( const l1Data = l1Event.args.data; // Get data from L1 event if (!verifiedHead) { - // @dev This shouldn't happen. If it does, there's a bug that needs fixing - logger.error({ - at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, - message: `Logic error: Message ${nonce.toString()} needs execution only but verifiedHead is missing. Skipping.`, - l1TxHash: l1Event.transactionHash, - }); - continue; + // @dev This shouldn't happen. If it does, there's a bug that needs fixing. + throw new Error(`Logic error: Message ${nonce.toString()} needs execution only but verifiedHead is missing.`); } - try { - // @dev Warn about messages that require only half of finalization. Means that either a tx from prev. run got stuck or failed or something else weird happened - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, - message: "Generating SpokePool.executeMessage ONLY for partially finalized message.", - nonce: nonce.toString(), - l1TxHash: l1Event.transactionHash, - verifiedHead: verifiedHead.toString(), - }); + // @dev Warn about messages that require only half of finalization. Means that either a tx from prev. run got stuck or failed or something else weird happened + logger.warn({ + at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, + message: "Generating SpokePool.executeMessage ONLY for partially finalized message.", + nonce: nonce.toString(), + l1TxHash: l1Event.transactionHash, + verifiedHead: verifiedHead.toString(), + }); - // --- Encode the message parameter --- - const encodedMessage = ethers.utils.defaultAbiCoder.encode(["address", "bytes"], [l1Target, l1Data]); - // ------------------------------------ - - const executeArgs = [nonce, encodedMessage, verifiedHead]; // Use encodedMessage - const executeTx: AugmentedTransaction = { - contract: universalSpokePoolContract, - chainId: l2ChainId, - method: "executeMessage", - args: executeArgs, - unpermissioned: true, - canFailInSimulation: true, - message: `Finalize Helios msg (HubPoolStore nonce ${nonce.toString()}) - Step 2 ONLY: Execute on SpokePool`, - }; - transactions.push(executeTx); - crossChainMessages.push({ - type: "misc", - miscReason: "ZK bridge finalization (Execute Message Only)", - originationChainId: l1ChainId, - destinationChainId: l2ChainId, - }); - } catch (error) { - // most likely encoding error. Not sure if this can ever happen actually - logger.warn({ - at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, - message: `Failed to prepare executeMessage transaction for partially finalized nonce ${nonce.toString()}`, - error: error, - l1Event: { txHash: l1Event.transactionHash, nonce: nonce.toString(), target: l1Event.args.target }, - }); - continue; - } + // --- Encode the message parameter --- + const encodedMessage = ethers.utils.defaultAbiCoder.encode(["address", "bytes"], [l1Target, l1Data]); + // ------------------------------------ + + const executeArgs = [nonce, encodedMessage, verifiedHead]; // Use encodedMessage + const executeTx: AugmentedTransaction = { + contract: universalSpokePoolContract, + chainId: l2ChainId, + method: "executeMessage", + args: executeArgs, + unpermissioned: true, + canFailInSimulation: true, + message: `Finalize Helios msg (HubPoolStore nonce ${nonce.toString()}) - Step 2 ONLY: Execute on SpokePool`, + }; + transactions.push(executeTx); + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization (Execute Message Only)", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); } // --- Process messages needing proof and execution --- for (const proof of successfulProofs) { - try { - // Ensure the hex strings have the '0x' prefix, adding it only if missing. - const proofBytes = proof.proofData.proof.startsWith("0x") ? proof.proofData.proof : "0x" + proof.proofData.proof; - const publicValuesBytes = proof.proofData.public_values.startsWith("0x") - ? proof.proofData.public_values - : "0x" + proof.proofData.public_values; - - // @dev Will throw on decode errors here. - let decodedOutputs: ProofOutputs = decodeProofOutputs(publicValuesBytes); - - // 1. SP1Helios.update transaction - const updateArgs = [proofBytes, publicValuesBytes]; - const updateTx: AugmentedTransaction = { - contract: sp1HeliosContract, - chainId: l2ChainId, - method: "update", - args: updateArgs, - unpermissioned: false, - canFailInSimulation: false, - nonMulticall: true, - message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, - }; - transactions.push(updateTx); - crossChainMessages.push({ - type: "misc", - miscReason: "ZK bridge finalization (Helios Update)", - originationChainId: l1ChainId, - destinationChainId: l2ChainId, - }); + // Ensure the hex strings have the '0x' prefix, adding it only if missing. + const proofBytes = proof.proofData.proof.startsWith("0x") ? proof.proofData.proof : "0x" + proof.proofData.proof; + const publicValuesBytes = proof.proofData.public_values.startsWith("0x") + ? proof.proofData.public_values + : "0x" + proof.proofData.public_values; + + // @dev Will throw on decode errors here. + let decodedOutputs: ProofOutputs = decodeProofOutputs(publicValuesBytes); + + // 1. SP1Helios.update transaction + const updateArgs = [proofBytes, publicValuesBytes]; + const updateTx: AugmentedTransaction = { + contract: sp1HeliosContract, + chainId: l2ChainId, + method: "update", + args: updateArgs, + unpermissioned: false, + canFailInSimulation: false, + nonMulticall: true, + message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 1: Update SP1Helios`, + }; + transactions.push(updateTx); + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization (Helios Update)", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); - // 2. SpokePool.executeMessage transaction - // --- Encode the message parameter --- - const l1Target = proof.target; // Get target from SuccessfulProof - const l1Data = proof.sourceMessageData; // Get data from SuccessfulProof - const encodedMessage = ethers.utils.defaultAbiCoder.encode(["address", "bytes"], [l1Target, l1Data]); - // ------------------------------------ - - const executeArgs = [proof.sourceNonce, encodedMessage, decodedOutputs.newHead]; // Use encodedMessage - const executeTx: AugmentedTransaction = { - contract: universalSpokePoolContract, - chainId: l2ChainId, - method: "executeMessage", - args: executeArgs, - unpermissioned: true, - // @dev Simulation of `executeMessage` depends on prior state update via SP1Helios.update - canFailInSimulation: true, - message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, - }; - transactions.push(executeTx); - crossChainMessages.push({ - type: "misc", - miscReason: "ZK bridge finalization (Execute Message)", - originationChainId: l1ChainId, - destinationChainId: l2ChainId, - }); - } catch (error: any) { - // Rethrow the error, adding context about the specific proof being processed. - const nonceStr = proof.sourceNonce.toString(); - const targetAddr = proof.target; - throw new Error( - `Failed to prepare transaction for proof of nonce ${nonceStr} (target: ${targetAddr}): ${error.message}` - ); - } + // 2. SpokePool.executeMessage transaction + // --- Encode the message parameter --- + const l1Target = proof.target; // Get target from SuccessfulProof + const l1Data = proof.sourceMessageData; // Get data from SuccessfulProof + const encodedMessage = ethers.utils.defaultAbiCoder.encode(["address", "bytes"], [l1Target, l1Data]); + // ------------------------------------ + + const executeArgs = [proof.sourceNonce, encodedMessage, decodedOutputs.newHead]; // Use encodedMessage + const executeTx: AugmentedTransaction = { + contract: universalSpokePoolContract, + chainId: l2ChainId, + method: "executeMessage", + args: executeArgs, + unpermissioned: true, + // @dev Simulation of `executeMessage` depends on prior state update via SP1Helios.update + canFailInSimulation: true, + message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, + }; + transactions.push(executeTx); + crossChainMessages.push({ + type: "misc", + miscReason: "ZK bridge finalization (Execute Message)", + originationChainId: l1ChainId, + destinationChainId: l2ChainId, + }); } const totalFinalizations = successfulProofs.length + needsExecutionOnlyMessages.length; From 6a5ae9307f020e429616b942d71843059555b5ef Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 12:38:04 -0700 Subject: [PATCH 23/31] address the rest of the review issues except .reduce / spreadEventWithBlockNumber ones Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 62 ++++++++++------------------------- src/utils/TransactionUtils.ts | 4 +-- 2 files changed, 20 insertions(+), 46 deletions(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index bc005d6970..d24dcc1100 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -55,7 +55,7 @@ export async function heliosL1toL2Finalizer( l2ChainId ); - if (!pendingMessages || pendingMessages.length === 0) { + if (pendingMessages.length === 0) { logger.debug({ at: `Finalizer#heliosL1toL2Finalizer:${l2ChainId}`, message: "No pending Helios messages found requiring action.", @@ -124,9 +124,7 @@ async function identifyPendingHeliosMessages( l2SpokePoolClient: SpokePoolClient, l1ChainId: number, l2ChainId: number -): Promise { - const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; - +): Promise { // --- Substep 1: Query and Filter L1 Events --- const relevantStoredCallDataEvents = await getRelevantL1Events( logger, @@ -134,10 +132,10 @@ async function identifyPendingHeliosMessages( l1SpokePoolClient, l1ChainId, l2ChainId, - l2SpokePoolAddress + l2SpokePoolClient.spokePool.address ); - if (!relevantStoredCallDataEvents || relevantStoredCallDataEvents.length === 0) { + if (relevantStoredCallDataEvents.length === 0) { logger.debug({ at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, message: "No relevant StoredCallData events found on L1.", @@ -146,19 +144,10 @@ async function identifyPendingHeliosMessages( } // --- Substep 2: Query L2 Verification Events (StorageSlotVerified) --- - // Store as Map to easily access head later - const verifiedSlotsMap = await getL2VerifiedSlotsMap(logger, l2SpokePoolClient, l2ChainId); - if (verifiedSlotsMap === null) { - // Error already logged in helper - return null; // Propagate error state - } + const verifiedSlotsMap = await getL2VerifiedSlotsMap(l2SpokePoolClient, l2ChainId); // --- Substep 3: Query L2 Execution Events (RelayedCallData) --- - const relayedNonces = await getL2RelayedNonces(logger, l2SpokePoolClient, l2ChainId); - if (relayedNonces === null) { - // Error already logged in helper - return null; // Propagate error state - } + const relayedNonces = await getL2RelayedNonces(l2SpokePoolClient); // --- Determine Status for each L1 Event --- const pendingMessages: PendingCrosschainMessage[] = []; @@ -175,7 +164,7 @@ async function identifyPendingHeliosMessages( pendingMessages.push({ l1Event: l1Event, status: "NeedsExecutionOnly", - verifiedHead: verifiedEvent.args.head, + verifiedHead: verifiedEvent.args.head, // set verifiedHead as it's needed for execution }); // Log a warning for partially finalized messages logger.warn({ @@ -197,7 +186,7 @@ async function identifyPendingHeliosMessages( }); } } - // If isExecuted is true, the message is fully finalized, do nothing. + // If `isExecuted` is true, the message is fully finalized, do nothing. } logger.debug({ @@ -216,13 +205,13 @@ async function identifyPendingHeliosMessages( /** Query and Filter L1 Events */ async function getRelevantL1Events( - logger: winston.Logger, + _logger: winston.Logger, hubPoolClient: HubPoolClient, l1SpokePoolClient: SpokePoolClient, l1ChainId: number, - l2ChainId: number, + _l2ChainId: number, l2SpokePoolAddress: string -): Promise { +): Promise { const l1Provider = hubPoolClient.hubPool.provider; const hubPoolStoreContract = getHubPoolStoreContract(l1ChainId, l1Provider); @@ -253,10 +242,9 @@ async function getRelevantL1Events( /** Query L2 Verification Events and return verified slots map */ async function getL2VerifiedSlotsMap( - logger: winston.Logger, l2SpokePoolClient: SpokePoolClient, l2ChainId: number -): Promise | null> { +): Promise> { const l2Provider = l2SpokePoolClient.spokePool.provider; const sp1HeliosContract = getSp1HeliosContract(l2ChainId, l2Provider); @@ -292,11 +280,7 @@ async function getL2VerifiedSlotsMap( } /** --- Query L2 Execution Events (RelayedCallData) */ -async function getL2RelayedNonces( - logger: winston.Logger, - l2SpokePoolClient: SpokePoolClient, - l2ChainId: number -): Promise | null> { +async function getL2RelayedNonces(l2SpokePoolClient: SpokePoolClient): Promise> { const l2Provider = l2SpokePoolClient.spokePool.provider; const l2SpokePoolAddress = l2SpokePoolClient.spokePool.address; const universalSpokePoolContract = new ethers.Contract(l2SpokePoolAddress, UNIVERSAL_SPOKE_ABI, l2Provider); @@ -355,14 +339,11 @@ async function processUnfinalizedHeliosMessages( const hubPoolStoreAddress = hubPoolStoreInfo.address; const sp1HeliosContract = getSp1HeliosContract(l2ChainId, l2Provider); - let currentHead: number; - let currentHeader: string; - const headBn: ethers.BigNumber = await sp1HeliosContract.head(); // todo: well, currently we're taking currentHead to use as prevHead in our ZK proof. There's a particular scenario where we could speed up proofs // todo: (by not making them to wait for finality longer than needed) if our blockNumber that we need a proved slot for is older than this head. - currentHead = headBn.toNumber(); - currentHeader = await sp1HeliosContract.headers(headBn); + const currentHead = headBn.toNumber(); + const currentHeader = await sp1HeliosContract.headers(headBn); if (!currentHeader || currentHeader === ethers.constants.HashZero) { throw new Error(`Invalid header found for head ${currentHead}`); } @@ -416,15 +397,8 @@ async function processUnfinalizedHeliosMessages( logger.debug({ ...logContext, message: "Proof requested successfully.", proofId }); // Exit, will check again next run } else if (getError) { - // Other error during GET - logger.warn({ - ...logContext, - message: "Failed to get proof state.", - proofId, - getUrl: getProofUrl, - getError: getError.message, - getResponseData: getError.response?.data, - }); + // Other error during GET - something might be wrong with the API. Throw and let finalizer retry this next run + throw new Error(`Failed to get proof state for proofId ${proofId}: ${getError.message}`); } else if (proofState) { // GET successful, check status if (proofState.status === "pending") { @@ -547,7 +521,7 @@ async function generateHeliosTxns( : "0x" + proof.proofData.public_values; // @dev Will throw on decode errors here. - let decodedOutputs: ProofOutputs = decodeProofOutputs(publicValuesBytes); + const decodedOutputs: ProofOutputs = decodeProofOutputs(publicValuesBytes); // 1. SP1Helios.update transaction const updateArgs = [proofBytes, publicValuesBytes]; diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts index 57c55bbf23..a0bb308b40 100644 --- a/src/utils/TransactionUtils.ts +++ b/src/utils/TransactionUtils.ts @@ -21,7 +21,7 @@ import { dotenv.config(); // Define chains that require legacy (type 0) transactions -const LEGACY_TRANSACTION_CHAINS = new Set([CHAIN_IDs.BSC]); +const LEGACY_TRANSACTION_CHAINS = [CHAIN_IDs.BSC]; export type TransactionSimulationResult = { transaction: AugmentedTransaction; @@ -93,7 +93,7 @@ export async function runTransaction( ); // Check if the chain requires legacy transactions - if (LEGACY_TRANSACTION_CHAINS.has(chainId)) { + if (LEGACY_TRANSACTION_CHAINS.includes(chainId)) { gas = { gasPrice: gas.maxFeePerGas }; } From 910b586a9686f294fd430c2a093dd3fca3685188 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 13:20:59 -0700 Subject: [PATCH 24/31] add isAugmentedTransaction + spreadEventWithBlockNumber Signed-off-by: Ihor Farion --- src/finalizer/index.ts | 6 +-- src/finalizer/types.ts | 10 +++++ src/finalizer/utils/helios.ts | 79 +++++++++++++++-------------------- src/interfaces/Helios.ts | 8 ++-- src/interfaces/Universal.ts | 12 +++--- src/utils/UniversalUtils.ts | 4 +- 6 files changed, 56 insertions(+), 63 deletions(-) diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index e87c2ef3ff..803d2b8050 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -32,7 +32,7 @@ import { Profiler, stringifyThrownValue, } from "../utils"; -import { ChainFinalizer, CrossChainMessage } from "./types"; +import { ChainFinalizer, CrossChainMessage, isAugmentedTransaction } from "./types"; import { arbStackFinalizer, cctpL1toL2Finalizer, @@ -315,7 +315,7 @@ export async function finalize( // Ensure each transaction would succeed in isolation. const finalizations = await sdkUtils.filterAsync(finalizerResponseTxns, async ({ txn: _txn, crossChainMessage }) => { let simErrorReason: string; - if ("callData" in _txn && "target" in _txn) { + if (!isAugmentedTransaction(_txn)) { // Multicall transaction simulation flow const txnToSubmit: AugmentedTransaction = { contract: multicall2Lookup[crossChainMessage.destinationChainId], @@ -382,7 +382,7 @@ export async function finalize( const multicallTxns: Multicall2Call[] = []; finalizations.forEach(({ txn }) => { - if ("contract" in 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); diff --git a/src/finalizer/types.ts b/src/finalizer/types.ts index 8782c75b3f..110279629c 100644 --- a/src/finalizer/types.ts +++ b/src/finalizer/types.ts @@ -43,3 +43,13 @@ export interface ChainFinalizer { l1ToL2AddressesToFinalize: string[] ): Promise; } + +/** + * 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; +} diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index d24dcc1100..88e6cf1025 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -1,18 +1,14 @@ import { ethers } from "ethers"; import { HubPoolClient, SpokePoolClient, AugmentedTransaction } from "../../clients"; import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple, Provider } from "../../utils"; +import { spreadEventWithBlockNumber } from "../../utils/EventUtils"; import { FinalizerPromise, CrossChainMessage } from "../types"; import { CONTRACT_ADDRESSES } from "../../common"; import axios from "axios"; import UNIVERSAL_SPOKE_ABI from "../../common/abi/Universal_SpokePool.json"; -import { - RelayedCallDataEvent, - RelayedCallDataEventArgs, - StoredCallDataEvent, - StoredCallDataEventArgs, -} from "../../interfaces/Universal"; +import { RelayedCallDataEvent, StoredCallDataEvent } from "../../interfaces/Universal"; import { ApiProofRequest, ProofOutputs, ProofStateResponse, SP1HeliosProofData } from "../../interfaces/ZkApi"; -import { StorageSlotVerifiedEvent, StorageSlotVerifiedEventArgs } from "../../interfaces/Helios"; +import { StorageSlotVerifiedEvent } from "../../interfaces/Helios"; import { calculateProofId, decodeProofOutputs } from "../../utils/ZkApiUtils"; import { calculateHubPoolStoreStorageSlot } from "../../utils/UniversalUtils"; @@ -74,7 +70,7 @@ export async function heliosL1toL2Finalizer( needsProofAndExecution: needsProofAndExecution.length, needsExecutionOnly: needsExecutionOnly.length, }, - needsExecutionNonces: needsExecutionOnly.map((m) => m.l1Event.args.nonce.toString()), // Log nonces needing only execution + needsExecutionNonces: needsExecutionOnly.map((m) => m.l1Event.nonce.toString()), // Log nonces needing only execution }); // --- Step 2: Get Proofs for Messages Needing Full Finalization --- @@ -152,8 +148,8 @@ async function identifyPendingHeliosMessages( // --- Determine Status for each L1 Event --- const pendingMessages: PendingCrosschainMessage[] = []; for (const l1Event of relevantStoredCallDataEvents) { - const expectedStorageSlot = calculateHubPoolStoreStorageSlot(l1Event.args); - const nonce = l1Event.args.nonce; + const expectedStorageSlot = calculateHubPoolStoreStorageSlot(l1Event.nonce); + const nonce = l1Event.nonce; const isExecuted = relayedNonces.has(nonce.toString()); // Use nonce string as key @@ -164,18 +160,18 @@ async function identifyPendingHeliosMessages( pendingMessages.push({ l1Event: l1Event, status: "NeedsExecutionOnly", - verifiedHead: verifiedEvent.args.head, // set verifiedHead as it's needed for execution + verifiedHead: verifiedEvent.head, // set verifiedHead as it's needed for execution }); // Log a warning for partially finalized messages logger.warn({ at: `Finalizer#identifyPendingHeliosMessages:${l2ChainId}`, message: "Message requires execution only (already verified in SP1Helios). Will generate SpokePool.executeMessage tx.", - l1TxHash: l1Event.transactionHash, + l1TxRef: l1Event.txnRef, nonce: nonce.toString(), storageSlot: expectedStorageSlot, - verifiedOnL2TxHash: verifiedEvent.transactionHash, - verifiedHead: verifiedEvent.args.head.toString(), + verifiedOnL2TxnRef: verifiedEvent.txnRef, + verifiedHead: verifiedEvent.head.toString(), }); } else { // Not verified and not executed -> Needs Proof and Execution @@ -225,16 +221,12 @@ async function getRelevantL1Events( const rawLogs = await paginatedEventQuery(hubPoolStoreContract, storedCallDataFilter, l1SearchConfig); - // Explicitly cast logs to the correct type - const events: StoredCallDataEvent[] = rawLogs.map((log) => ({ - ...log, - args: log.args as StoredCallDataEventArgs, - })); + const events: StoredCallDataEvent[] = rawLogs.map((log) => spreadEventWithBlockNumber(log) as StoredCallDataEvent); const relevantStoredCallDataEvents = events.filter( (event) => - compareAddressesSimple(event.args.target, l2SpokePoolAddress) || - compareAddressesSimple(event.args.target, ethers.constants.AddressZero) + compareAddressesSimple(event.target, l2SpokePoolAddress) || + compareAddressesSimple(event.target, ethers.constants.AddressZero) ); return relevantStoredCallDataEvents; @@ -257,23 +249,22 @@ async function getL2VerifiedSlotsMap( const rawLogs = await paginatedEventQuery(sp1HeliosContract, storageVerifiedFilter, l2SearchConfig); - // Explicitly cast logs to the correct type - const events: StorageSlotVerifiedEvent[] = rawLogs.map((log) => ({ - ...log, - args: log.args as StorageSlotVerifiedEventArgs, - })); + // Use spreadEventWithBlockNumber and cast to the flattened type + const events: StorageSlotVerifiedEvent[] = rawLogs.map( + (log) => spreadEventWithBlockNumber(log) as StorageSlotVerifiedEvent + ); // Store events in a map keyed by the storage slot (key) const verifiedSlotsMap = new Map(); events.forEach((event) => { // Handle potential duplicates (though unlikely with paginated query): favour latest block/logIndex - const existing = verifiedSlotsMap.get(event.args.key); + const existing = verifiedSlotsMap.get(event.key); if ( !existing || event.blockNumber > existing.blockNumber || (event.blockNumber === existing.blockNumber && event.logIndex > existing.logIndex) ) { - verifiedSlotsMap.set(event.args.key, event); + verifiedSlotsMap.set(event.key, event); } }); return verifiedSlotsMap; @@ -294,13 +285,11 @@ async function getL2RelayedNonces(l2SpokePoolClient: SpokePoolClient): Promise ({ - ...log, - args: log.args as RelayedCallDataEventArgs, - })); + // Use spreadEventWithBlockNumber and cast to the flattened type + const events: RelayedCallDataEvent[] = rawLogs.map((log) => spreadEventWithBlockNumber(log) as RelayedCallDataEvent); // Return a Set of nonces (as strings for easy comparison) - return new Set(events.map((event) => event.args.nonce.toString())); + return new Set(events.map((event) => event.nonce.toString())); } /** @@ -356,12 +345,12 @@ async function processUnfinalizedHeliosMessages( const l1Event = pendingMessage.l1Event; // Extract the L1 event const logContext = { at: `Finalizer#heliosL1toL2Finalizer:processUnfinalizedHeliosMessages:${l2ChainId}`, - l1TxHash: l1Event.transactionHash, - nonce: l1Event.args.nonce.toString(), - target: l1Event.args.target, + l1TxHash: l1Event.txnRef, + nonce: l1Event.nonce.toString(), + target: l1Event.target, }; - const storageSlot = calculateHubPoolStoreStorageSlot(l1Event.args); + const storageSlot = calculateHubPoolStoreStorageSlot(l1Event.nonce); const apiRequest: ApiProofRequest = { src_chain_contract_address: hubPoolStoreAddress, @@ -424,9 +413,9 @@ async function processUnfinalizedHeliosMessages( logger.debug({ ...logContext, message: "Proof successfully retrieved.", proofId }); successfulProofs.push({ proofData: proofState.update_calldata, - sourceNonce: l1Event.args.nonce, // Use nonce from L1 event - target: l1Event.args.target, // Use target from L1 event - sourceMessageData: l1Event.args.data, // Use data from L1 event + sourceNonce: l1Event.nonce, // Use nonce from L1 event + target: l1Event.target, // Use target from L1 event + sourceMessageData: l1Event.data, // Use data from L1 event }); } else { // todo? Might want to log an error here @@ -471,9 +460,9 @@ async function generateHeliosTxns( // --- Process messages needing only execution --- for (const message of needsExecutionOnlyMessages) { const { l1Event, verifiedHead } = message; - const nonce = l1Event.args.nonce; - const l1Target = l1Event.args.target; // Get target from L1 event - const l1Data = l1Event.args.data; // Get data from L1 event + const nonce = l1Event.nonce; + const l1Target = l1Event.target; // Get target from L1 event + const l1Data = l1Event.data; // Get data from L1 event if (!verifiedHead) { // @dev This shouldn't happen. If it does, there's a bug that needs fixing. @@ -485,7 +474,7 @@ async function generateHeliosTxns( at: `Finalizer#heliosL1toL2Finalizer:generateTxnItem:${l2ChainId}`, message: "Generating SpokePool.executeMessage ONLY for partially finalized message.", nonce: nonce.toString(), - l1TxHash: l1Event.transactionHash, + l1TxHash: l1Event.txnRef, verifiedHead: verifiedHead.toString(), }); @@ -575,7 +564,7 @@ async function generateHeliosTxns( at: `Finalizer#heliosL1toL2Finalizer:generateHeliosTxns:${l2ChainId}`, message: `Generated ${transactions.length} transactions for ${totalFinalizations} finalizations (${successfulProofs.length} full, ${needsExecutionOnlyMessages.length} exec only).`, proofNoncesFinalized: successfulProofs.map((p) => p.sourceNonce.toString()), - execOnlyNoncesFinalized: needsExecutionOnlyMessages.map((m) => m.l1Event.args.nonce.toString()), + execOnlyNoncesFinalized: needsExecutionOnlyMessages.map((m) => m.l1Event.nonce.toString()), }); return { callData: transactions, crossChainMessages: crossChainMessages }; diff --git a/src/interfaces/Helios.ts b/src/interfaces/Helios.ts index 3132d581cf..f6888d87e7 100644 --- a/src/interfaces/Helios.ts +++ b/src/interfaces/Helios.ts @@ -1,12 +1,10 @@ -import { Log } from "."; +import { SortableEvent } from "."; import { BigNumber } from "../utils"; -export interface StorageSlotVerifiedEventArgs { +// Event type for Sp1Helios used in v4 messaging (Flattened) +export interface StorageSlotVerifiedEvent extends SortableEvent { head: BigNumber; key: string; // bytes32 value: string; // bytes32 contractAddress: string; } - -// Event type for Sp1Helios used in v4 messaging -export type StorageSlotVerifiedEvent = Log & { args: StorageSlotVerifiedEventArgs }; diff --git a/src/interfaces/Universal.ts b/src/interfaces/Universal.ts index 3e5d695b63..eec124cb33 100644 --- a/src/interfaces/Universal.ts +++ b/src/interfaces/Universal.ts @@ -1,17 +1,15 @@ -import { Log } from "."; +import { SortableEvent } from "."; import { BigNumber } from "../utils"; -export interface StoredCallDataEventArgs { +// Flattened HubPoolStore Event +export interface StoredCallDataEvent extends SortableEvent { target: string; data: string; nonce: BigNumber; } -export interface RelayedCallDataEventArgs { +// Flattened Universal SpokePool Event +export interface RelayedCallDataEvent extends SortableEvent { nonce: BigNumber; caller: string; } - -// Event types for HubPoolStore and Universal_SpokePool used in v4 messaging -export type StoredCallDataEvent = Log & { args: StoredCallDataEventArgs }; -export type RelayedCallDataEvent = Log & { args: RelayedCallDataEventArgs }; diff --git a/src/utils/UniversalUtils.ts b/src/utils/UniversalUtils.ts index d8c74a927d..b86cdb85d6 100644 --- a/src/utils/UniversalUtils.ts +++ b/src/utils/UniversalUtils.ts @@ -1,14 +1,12 @@ import { ethers } from "ethers"; import { BigNumber } from "."; -import { StoredCallDataEventArgs } from "../interfaces/Universal"; /** * Calculates the storage slot in the HubPoolStore contract for a given nonce. * This assumes the data is stored in a mapping at slot 0, keyed by nonce. * storage_slot = keccak256(h(k) . h(p)) where k = nonce, p = mapping slot position (0) */ -export function calculateHubPoolStoreStorageSlot(eventArgs: StoredCallDataEventArgs): string { - const nonce = eventArgs.nonce; +export function calculateHubPoolStoreStorageSlot(nonce: BigNumber): string { const mappingSlotPosition = 0; // The relayMessageCallData mapping is at slot 0 // Ensure nonce and slot position are correctly padded to 32 bytes (64 hex chars + 0x prefix) From f7193591977398c2fb41b0b65b28b5d88668ec65 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 13:27:14 -0700 Subject: [PATCH 25/31] use stringifyThrownValue Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 88e6cf1025..c6ede136ba 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -11,6 +11,7 @@ import { ApiProofRequest, ProofOutputs, ProofStateResponse, SP1HeliosProofData } import { StorageSlotVerifiedEvent } from "../../interfaces/Helios"; import { calculateProofId, decodeProofOutputs } from "../../utils/ZkApiUtils"; import { calculateHubPoolStoreStorageSlot } from "../../utils/UniversalUtils"; +import { stringifyThrownValue } from "../../utils/LogUtils"; type CrossChainMessageStatus = "NeedsProofAndExecution" | "NeedsExecutionOnly"; @@ -387,7 +388,7 @@ async function processUnfinalizedHeliosMessages( // Exit, will check again next run } else if (getError) { // Other error during GET - something might be wrong with the API. Throw and let finalizer retry this next run - throw new Error(`Failed to get proof state for proofId ${proofId}: ${getError.message}`); + throw new Error(`Failed to get proof state for proofId ${proofId}: ${stringifyThrownValue(getError)}`); } else if (proofState) { // GET successful, check status if (proofState.status === "pending") { From 4adbb2d63e83c28999b60e8f49246fd70b5f452f Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 15:04:52 -0700 Subject: [PATCH 26/31] merge chainFinalizers entries for BSC Signed-off-by: Ihor Farion --- src/finalizer/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/finalizer/index.ts b/src/finalizer/index.ts index 7b20e552cf..296afdc349 100644 --- a/src/finalizer/index.ts +++ b/src/finalizer/index.ts @@ -71,10 +71,6 @@ const chainFinalizers: { [chainId: number]: { finalizeOnL2: ChainFinalizer[]; fi finalizeOnL1: [opStackFinalizer, cctpL2toL1Finalizer], finalizeOnL2: [cctpL1toL2Finalizer], }, - [CHAIN_IDs.BSC]: { - finalizeOnL1: [], - finalizeOnL2: [heliosL1toL2Finalizer], - }, [CHAIN_IDs.POLYGON]: { finalizeOnL1: [polygonFinalizer, cctpL2toL1Finalizer], finalizeOnL2: [cctpL1toL2Finalizer], @@ -129,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], From 093d9c5bce6c0a1de0408810ff0304002715ab29 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 15:43:11 -0700 Subject: [PATCH 27/31] clarify ZK API response handling Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 82 ++++++++++++++++------------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index c6ede136ba..47c1702eb5 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -367,9 +367,9 @@ async function processUnfinalizedHeliosMessages( logger.debug({ ...logContext, message: "Attempting to get proof", proofId, getProofUrl, storageSlot }); let proofState: ProofStateResponse | null = null; - let getError: any = null; // @dev We need try - catch here because of how API responds to non-existing proofs: with NotFound status + let getError: any = null; try { const response = await axios.get(getProofUrl); proofState = response.data; @@ -378,60 +378,54 @@ async function processUnfinalizedHeliosMessages( getError = error; } - // --- API Interaction Flow --- - // 1. Try to get proof - if (getError && axios.isAxiosError(getError) && getError.response?.status === 404) { - // 1a. NOTFOUND -> Request proof - logger.debug({ ...logContext, message: "Proof not found (404), requesting...", proofId }); - await axios.post(`${apiBaseUrl}/api/proofs`, apiRequest); - logger.debug({ ...logContext, message: "Proof requested successfully.", proofId }); - // Exit, will check again next run - } else if (getError) { - // Other error during GET - something might be wrong with the API. Throw and let finalizer retry this next run - throw new Error(`Failed to get proof state for proofId ${proofId}: ${stringifyThrownValue(getError)}`); - } else if (proofState) { - // GET successful, check status - if (proofState.status === "pending") { - // 1b. SUCCESS ("pending") -> Log and exit flow + // Axios error. Handle based on whether was a NOTFOUND or another error + if (getError) { + const isNotFoundError = axios.isAxiosError(getError) && getError.response?.status === 404; + if (isNotFoundError) { + // NOTFOUND error -> Request proof + logger.debug({ ...logContext, message: "Proof not found (404), requesting...", proofId }); + await axios.post(`${apiBaseUrl}/api/proofs`, apiRequest); + logger.debug({ ...logContext, message: "Proof requested successfully.", proofId }); + continue; + } else { + // If other error is returned -- throw and alert PD; this shouldn't happen + throw new Error(`Failed to get proof state for proofId ${proofId}: ${stringifyThrownValue(getError)}`); + } + } + + // No axios error, process `proofState` + switch (proofState.status) { + case "pending": + // If proof generation is pending -- there's nothing for us to do yet. Will check this proof next run logger.debug({ ...logContext, message: "Proof generation is pending.", proofId }); - // Exit, will check again next run - } else if (proofState.status === "errored") { - // 1c. SUCCESS ("errored") -> Log high severity, request again, exit flow - // @dev API tried to generate a proof but failed. Log as error and re-request proof + break; + case "errored": + // Proof generation errored on the API side. This is concerning, so we log an error. But nothing to do for us other than to re-request logger.error({ ...logContext, message: "Proof generation errored on ZK API side. Requesting again.", proofId, - errorMessage: proofState.error_message, + errorMessage: proofState.error_message!, }); await axios.post(`${apiBaseUrl}/api/proofs`, apiRequest); logger.debug({ ...logContext, message: "Errored proof requested again successfully.", proofId }); - // Exit, will check again next run - } else if (proofState.status === "success") { - // 1d. SUCCESS ("success") -> Collect proof data for later processing - if (proofState.update_calldata) { - logger.debug({ ...logContext, message: "Proof successfully retrieved.", proofId }); - successfulProofs.push({ - proofData: proofState.update_calldata, - sourceNonce: l1Event.nonce, // Use nonce from L1 event - target: l1Event.target, // Use target from L1 event - sourceMessageData: l1Event.data, // Use data from L1 event - }); - } else { - // todo? Might want to log an error here - logger.warn({ - ...logContext, - message: "Proof status is success but update_calldata is missing.", - proofId, - proofState, - }); - // Exit, will check again next run + break; + case "success": + if (!proofState.update_calldata) { + throw new Error(`Proof status is success but update_calldata is missing for proofId ${proofId}`); } - } else { - // @dev status should always be "pending", "success" or "errored" + logger.debug({ ...logContext, message: "Proof successfully retrieved.", proofId }); + successfulProofs.push({ + // @dev `proofData` should exist if proofState.status is "success" + proofData: proofState.update_calldata, + sourceNonce: l1Event.nonce, + target: l1Event.target, + sourceMessageData: l1Event.data, + }); + break; + default: throw new Error(`Received unexpected proof status for proof ${proofId}`); - } } } // end loop over messages From 380cc2b91db6b54a60077eb4d6216b701481605c Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 15:47:56 -0700 Subject: [PATCH 28/31] move contract utility functions to src/utils Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 28 ++-------------------------- src/utils/HeliosUtils.ts | 14 ++++++++++++++ src/utils/UniversalUtils.ts | 16 +++++++++++++++- 3 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 src/utils/HeliosUtils.ts diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 47c1702eb5..2918290c3b 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -10,8 +10,9 @@ import { RelayedCallDataEvent, StoredCallDataEvent } from "../../interfaces/Univ import { ApiProofRequest, ProofOutputs, ProofStateResponse, SP1HeliosProofData } from "../../interfaces/ZkApi"; import { StorageSlotVerifiedEvent } from "../../interfaces/Helios"; import { calculateProofId, decodeProofOutputs } from "../../utils/ZkApiUtils"; -import { calculateHubPoolStoreStorageSlot } from "../../utils/UniversalUtils"; +import { calculateHubPoolStoreStorageSlot, getHubPoolStoreContract } from "../../utils/UniversalUtils"; import { stringifyThrownValue } from "../../utils/LogUtils"; +import { getSp1HeliosContract } from "../../utils/HeliosUtils"; type CrossChainMessageStatus = "NeedsProofAndExecution" | "NeedsExecutionOnly"; @@ -564,28 +565,3 @@ async function generateHeliosTxns( return { callData: transactions, crossChainMessages: crossChainMessages }; } - -/** - * Retrieves an ethers.Contract instance for the SP1Helios contract on the specified chain. - * @throws {Error} If the SP1Helios contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES. - */ -function getSp1HeliosContract(chainId: number, signerOrProvider: Signer | Provider): ethers.Contract { - const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = CONTRACT_ADDRESSES[chainId].sp1Helios; - if (!sp1HeliosAddress || !sp1HeliosAbi) { - throw new Error(`SP1Helios contract not found for chain ${chainId}. Cannot verify Helios messages.`); - } - return new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, signerOrProvider); -} - -/** - * Retrieves an ethers.Contract instance for the HubPoolStore contract on the specified chain. - * @throws {Error} If the HubPoolStore contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES. - */ -function getHubPoolStoreContract(chainId: number, signerOrProvider: Signer | Provider) { - const hubPoolStoreInfo = CONTRACT_ADDRESSES[chainId]?.hubPoolStore; - if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { - throw new Error(`HubPoolStore contract address or ABI not found for chain ${chainId}.`); - } - - return new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, signerOrProvider); -} diff --git a/src/utils/HeliosUtils.ts b/src/utils/HeliosUtils.ts new file mode 100644 index 0000000000..c8c45b80f0 --- /dev/null +++ b/src/utils/HeliosUtils.ts @@ -0,0 +1,14 @@ +import { ethers, Provider, Signer } from "."; +import { CONTRACT_ADDRESSES } from "../common/ContractAddresses"; + +/** + * Retrieves an ethers.Contract instance for the SP1Helios contract on the specified chain. + * @throws {Error} If the SP1Helios contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES. + */ +export function getSp1HeliosContract(chainId: number, signerOrProvider: Signer | Provider): ethers.Contract { + const { address: sp1HeliosAddress, abi: sp1HeliosAbi } = CONTRACT_ADDRESSES[chainId].sp1Helios; + if (!sp1HeliosAddress || !sp1HeliosAbi) { + throw new Error(`SP1Helios contract not found for chain ${chainId}. Cannot verify Helios messages.`); + } + return new ethers.Contract(sp1HeliosAddress, sp1HeliosAbi as any, signerOrProvider); +} diff --git a/src/utils/UniversalUtils.ts b/src/utils/UniversalUtils.ts index b86cdb85d6..757b3206c5 100644 --- a/src/utils/UniversalUtils.ts +++ b/src/utils/UniversalUtils.ts @@ -1,5 +1,6 @@ import { ethers } from "ethers"; -import { BigNumber } from "."; +import { BigNumber, Signer, Provider } from "."; +import { CONTRACT_ADDRESSES } from "../common"; /** * Calculates the storage slot in the HubPoolStore contract for a given nonce. @@ -22,3 +23,16 @@ export function calculateHubPoolStoreStorageSlot(nonce: BigNumber): string { return storageSlot; } + +/** + * Retrieves an ethers.Contract instance for the HubPoolStore contract on the specified chain. + * @throws {Error} If the HubPoolStore contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES. + */ +export function getHubPoolStoreContract(chainId: number, signerOrProvider: Signer | Provider): ethers.Contract { + const hubPoolStoreInfo = CONTRACT_ADDRESSES[chainId]?.hubPoolStore; + if (!hubPoolStoreInfo?.address || !hubPoolStoreInfo.abi) { + throw new Error(`HubPoolStore contract address or ABI not found for chain ${chainId}.`); + } + + return new ethers.Contract(hubPoolStoreInfo.address, hubPoolStoreInfo.abi as any, signerOrProvider); +} From 9a281ff578c81da560f504b4af94fd1db007222f Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 15:53:14 -0700 Subject: [PATCH 29/31] fix lint Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 2918290c3b..3c1d32dbec 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -1,6 +1,6 @@ import { ethers } from "ethers"; import { HubPoolClient, SpokePoolClient, AugmentedTransaction } from "../../clients"; -import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple, Provider } from "../../utils"; +import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple } from "../../utils"; import { spreadEventWithBlockNumber } from "../../utils/EventUtils"; import { FinalizerPromise, CrossChainMessage } from "../types"; import { CONTRACT_ADDRESSES } from "../../common"; From 7d160f67b80b6a906457395854065167bcc2d374 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 2 May 2025 15:54:34 -0700 Subject: [PATCH 30/31] fix 1 more lint warning Signed-off-by: Ihor Farion --- src/finalizer/utils/helios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 3c1d32dbec..06e2127202 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -406,7 +406,7 @@ async function processUnfinalizedHeliosMessages( ...logContext, message: "Proof generation errored on ZK API side. Requesting again.", proofId, - errorMessage: proofState.error_message!, + errorMessage: proofState.error_message, }); await axios.post(`${apiBaseUrl}/api/proofs`, apiRequest); From e57ab8ffd199103220e3a07d2085b9f7508a90ab Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Sat, 3 May 2025 09:30:15 -0700 Subject: [PATCH 31/31] new SP1Helios address; manual gasLimit; shorter event lookback on L1 Signed-off-by: Ihor Farion --- src/common/ContractAddresses.ts | 2 +- src/finalizer/utils/helios.ts | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/common/ContractAddresses.ts b/src/common/ContractAddresses.ts index b2ced1ded4..41dd1b10d4 100644 --- a/src/common/ContractAddresses.ts +++ b/src/common/ContractAddresses.ts @@ -257,7 +257,7 @@ export const CONTRACT_ADDRESSES: { }, [CHAIN_IDs.BSC]: { sp1Helios: { - address: "0xcdf08CB3d3436c3c21F277b6AD45E3D7aB1Ce12F", + address: "0x3BED21dAe767e4Df894B31b14aD32369cE4bad8b", abi: SP1_HELIOS_ABI, }, }, diff --git a/src/finalizer/utils/helios.ts b/src/finalizer/utils/helios.ts index 06e2127202..5a4ea8e6db 100644 --- a/src/finalizer/utils/helios.ts +++ b/src/finalizer/utils/helios.ts @@ -1,6 +1,13 @@ -import { ethers } from "ethers"; import { HubPoolClient, SpokePoolClient, AugmentedTransaction } from "../../clients"; -import { EventSearchConfig, Signer, winston, paginatedEventQuery, compareAddressesSimple } from "../../utils"; +import { + EventSearchConfig, + Signer, + winston, + paginatedEventQuery, + compareAddressesSimple, + ethers, + BigNumber, +} from "../../utils"; import { spreadEventWithBlockNumber } from "../../utils/EventUtils"; import { FinalizerPromise, CrossChainMessage } from "../types"; import { CONTRACT_ADDRESSES } from "../../common"; @@ -213,9 +220,15 @@ async function getRelevantL1Events( const l1Provider = hubPoolClient.hubPool.provider; const hubPoolStoreContract = getHubPoolStoreContract(l1ChainId, l1Provider); + /** + * @dev We artificially shorten the lookback time peiod for L1 events by a factor of 2. We want to avoid race conditions where + * we see an old event on L1, but not look back far enough on L2 to see that the event has been executed successfully. + */ + const toBlock = l1SpokePoolClient.latestBlockSearched; + const fromBlock = Math.floor((l1SpokePoolClient.eventSearchConfig.fromBlock + toBlock) / 2); const l1SearchConfig: EventSearchConfig = { - fromBlock: l1SpokePoolClient.eventSearchConfig.fromBlock, - toBlock: l1SpokePoolClient.latestBlockSearched, + fromBlock: fromBlock, + toBlock: toBlock, maxBlockLookBack: l1SpokePoolClient.eventSearchConfig.maxBlockLookBack, }; @@ -544,6 +557,8 @@ async function generateHeliosTxns( unpermissioned: true, // @dev Simulation of `executeMessage` depends on prior state update via SP1Helios.update canFailInSimulation: true, + // todo? this hardcoded gas limit of 2 mil could be improved if we were able to simulate this tx on top of blockchain state created by the tx above + gasLimit: BigNumber.from(2000000), message: `Finalize Helios msg (HubPoolStore nonce ${proof.sourceNonce.toString()}) - Step 2: Execute on SpokePool`, }; transactions.push(executeTx);